# ========================================================================================= # SCCM / ConfigMgr provider # # Talks to real infrastructure: the ConfigMgr PowerShell module (computer associations), # the site database over SQL (inventory + installed software), Active Directory (OU and # group membership), DNS/ICMP (online checks) and the devices' admin shares over SMB # (free disk space and USMT progress logs). # # Requirements: Windows PowerShell 5.1, the ConfigMgr console (ConfigurationManager # module), read access to the site database, and admin-share access to the devices. # # Every provider implements the same functions: # Initialize-Provider, Open-ProviderCycle, Close-ProviderCycle, pingDevice, # Resolve-DeviceIP, Get-DiskSpace, Get-LogInfo, Get-DeviceAssociations, # Get-DeviceInventory, Get-InstalledSoftware # and optionally Get-DeviceLastSeen. # ========================================================================================= $script:SccmCfg = $config.SCCM function Initialize-Provider { $ModulePath = Join-Path (Split-Path $env:SMS_ADMIN_UI_PATH) "ConfigurationManager.psd1" Import-Module $ModulePath -ErrorAction Stop } function Open-ProviderCycle { # Flush DNS cache at the start of every cycle so stale A records and cached # negative (NXDOMAIN) results don't cause false timeouts -- especially for # devices freshly plugged into the network that were looked up before they registered. if ($config.Worker.FlushDnsEachCycle) { try { $null = ipconfig /flushdns if ($v) { Write-Host "DNS cache flushed." -ForegroundColor DarkGray } } catch { Write-Host "DNS flush failed: $($_.Exception.Message)" -ForegroundColor DarkYellow } } $SiteCode = $script:SccmCfg.SiteCode $session = @{ Sql = $null; PreviousLocation = Get-Location } $session.Sql = New-Object System.Data.SqlClient.SqlConnection("Server=$($script:SccmCfg.SqlServer);Database=$($script:SccmCfg.Database);Integrated Security=True;") try { $session.Sql.Open() } catch { Write-Host "Warning: Could not open Master SQL Connection" -ForegroundColor Red } if (-not (Get-PSDrive -Name $SiteCode -PSProvider CMSite -ErrorAction SilentlyContinue)) { if ($v) { Write-Host "Connecting to SCCM Drive ($SiteCode)..." -ForegroundColor DarkGray } New-PSDrive -Name $SiteCode -PSProvider CMSite -Root $script:SccmCfg.ProviderMachineName -Scope Global | Out-Null } Set-Location "$($SiteCode):" return $session } function Close-ProviderCycle { param($Session) if ($Session.Sql) { if ($Session.Sql.State -eq 'Open') { $Session.Sql.Close() } $Session.Sql.Dispose() } Set-Location $Session.PreviousLocation } function pingDevice { param ([string]$computer) if ([string]::IsNullOrWhiteSpace($computer) -or $computer -eq "N/A") { return "Not Found" } if ($v) { Write-Host " > Fast Ping: $computer... " -ForegroundColor DarkGray -NoNewline } try { # Use Resolve-DnsName instead of GetHostAddressesAsync: the latter reads the # Windows DNS client cache, which can hold STALE A records (wrong IP) long after # DNS itself has the current record. Resolve-DnsName queries more directly and # returns the live record. -DnsOnly skips the local cache; we still take all A records. $allIps = @() try { $dnsRecs = Resolve-DnsName -Name $computer -Type A -DnsOnly -ErrorAction Stop | Where-Object { $_.Type -eq 'A' } $allIps = @($dnsRecs | ForEach-Object { $_.IPAddress } | Select-Object -Unique) } catch { # Resolve-DnsName failed (no record / DNS error) -- fall back to .NET resolver. try { $task = [System.Net.Dns]::GetHostAddressesAsync($computer) if ($task.Wait(1000)) { $allIps = @($task.Result | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | ForEach-Object { $_.IPAddressToString } | Select-Object -Unique) } } catch {} } if ($allIps.Count -eq 0) { if ($v) { Write-Host "Not Found (No IP)." -ForegroundColor Red } return "Not Found" } # --- Ping every resolved IP; online if ANY responds --- # Handles duplicate/stale DNS records by testing all candidates. $p = $null $liveIp = $null try { $p = [System.Net.NetworkInformation.Ping]::new() foreach ($ip in $allIps) { foreach ($attempt in 1..2) { $res = $p.Send($ip, 1000) if ($res.Status -eq 'Success') { $liveIp = $ip; break } } if ($liveIp) { break } } } finally { if ($null -ne $p) { $p.Dispose() } } if (-not $liveIp) { if ($v) { Write-Host "Timed Out." -ForegroundColor DarkYellow } return "Timed Out" } # --- Reverse DNS validation on the IP that actually responded --- # Only flag DNS Issue if reverse DNS SUCCEEDS but names a DIFFERENT machine. # A missing PTR record is common and is NOT an issue -- treat as online. try { $reverseHost = [System.Net.Dns]::GetHostEntry($liveIp).HostName $reverseShort = ($reverseHost -split '\.')[0].ToUpper() if ($reverseShort -ne $computer.ToUpper()) { if ($v) { Write-Host "DNS Issue ($liveIp resolves back to $reverseShort)." -ForegroundColor Red } return "DNS Issue" } } catch { # No PTR record -- not a conflict. } # -------------------------------- if ($v) { Write-Host "Online" -ForegroundColor Green } return "Online" } catch { if ($v) { Write-Host "Not Found (DNS)." -ForegroundColor Red } return "Not Found" } } function Resolve-DeviceIP { param([string]$computerName) try { $ipRecord = [System.Net.Dns]::GetHostAddresses($computerName) | Where-Object { $_.AddressFamily -eq 'InterNetwork' } | Select-Object -First 1 if ($ipRecord) { return $ipRecord.IPAddressToString } } catch {} return $null } function Get-DiskSpace { param([string]$computer) if ($v) { Write-Host " > Checking C: Drive Storage (SMB Method)... " -ForegroundColor DarkGray -NoNewline } try { # Using SMB (Port 445) completely bypasses WMI/DCOM hangs $fso = New-Object -ComObject Scripting.FileSystemObject $uncPath = "\\$computer\$($script:SccmCfg.AdminShare)" # Check if the path exists to avoid a long COM hang if ([System.IO.Directory]::Exists($uncPath)) { $drive = $fso.GetDrive($uncPath) $total = [math]::Round($drive.TotalSize / 1GB, 1) $free = [math]::Round($drive.FreeSpace / 1GB, 1) if ($v) { Write-Host "${free}GB Free out of ${total}GB" -ForegroundColor Green } return @{ Total = "$total GB"; Free = "$free GB" } } else { if ($v) { Write-Host "$($script:SccmCfg.AdminShare) Share Unreachable" -ForegroundColor Red } } } catch { if ($v) { Write-Host "SMB Access Error" -ForegroundColor Red } } return @{ Total = "N/A"; Free = "N/A" } } function Get-LogInfo { param ([string]$computer, [string]$logName) $basePaths = @($script:SccmCfg.LogFolders | ForEach-Object { "\\$computer\$($script:SccmCfg.AdminShare)\$_" }) $currentDrive = Get-Location Set-Location "C:\" $result = New-LogInfoResult if ($v) { Write-Host " > Scraping logs for '$logName'..." -ForegroundColor DarkGray } foreach ($folder in $basePaths) { if ($v) { Write-Host " Checking $folder... " -ForegroundColor DarkGray -NoNewline } try { if ([System.IO.Directory]::Exists($folder)) { $files = [System.IO.Directory]::GetFiles($folder, "$logName*") if ($files.Count -gt 0) { $realPath = $files[0] if ($v) { Write-Host "Found!" -ForegroundColor Green } $result.LastModified = (Get-Item $realPath).LastWriteTime $content = Get-Content -Path $realPath -Tail 1 -ErrorAction SilentlyContinue if (-not $content) { $content = Get-Content -Path $realPath -Tail 1 -Encoding Unicode -ErrorAction SilentlyContinue } if ($content) { Set-LogInfoLine $result $content.Trim() $result.Found = $true; $result.Path = $realPath } break } else { if ($v) { Write-Host "Empty/Not there." -ForegroundColor DarkGray } } } else { if ($v) { Write-Host "Path doesn't exist." -ForegroundColor DarkGray } } } catch { if ($v) { Write-Host "Access Denied / Error." -ForegroundColor Red } $result.Line = "Access Denied" } } Set-Location $currentDrive return $result } function Get-DeviceAssociations { param([string]$SourceComputer) return Get-CMComputerAssociation -SourceComputer $SourceComputer -ErrorAction SilentlyContinue } # ----------------------------------------------------------------------------------------- # Inventory: AD (OU + groups) and the site database (hardware, users, asset-info class) # Returns only the fields it could find; the worker fills the rest with "N/A". # ----------------------------------------------------------------------------------------- function Test-SqlName([string]$name) { return ($name -match '^[A-Za-z0-9_]+$') } function Read-AssetInventoryRow { # Applies the configured column map + room-type flags from a data reader to $res param($reader, $res) $ai = $script:SccmCfg.AssetInventory foreach ($field in $ai.Columns.Keys) { $val = $reader["AI_$field"] if ($val -isnot [DBNull] -and "$val") { $res[$field] = "$val" } } $rTypes = @() for ($i = 0; $i -lt $ai.RoomTypes.Count; $i++) { if ("$($reader["RT_$i"])" -eq "1") { $rTypes += $ai.RoomTypes[$i].Label } } return $rTypes } function Get-AssetInventorySelect { param([string]$alias) $ai = $script:SccmCfg.AssetInventory $parts = @() foreach ($field in $ai.Columns.Keys) { $col = $ai.Columns[$field] if ((Test-SqlName $col) -and (Test-SqlName $field)) { $parts += "$alias.[$col] AS [AI_$field]" } } for ($i = 0; $i -lt $ai.RoomTypes.Count; $i++) { $col = $ai.RoomTypes[$i].Column if (Test-SqlName $col) { $parts += "$alias.[$col] AS [RT_$i]" } } return ($parts -join ", ") } function Get-DeviceInventory { param([string]$computerName, $Session) $res = @{} $SqlConn = $Session.Sql $searcher = $null try { # Only letters, digits and - _ . can reach the LDAP filter if ($computerName -notmatch '^[A-Za-z0-9._-]+$') { throw "invalid name" } $searcher = [adsisearcher]"(&(objectCategory=computer)(name=$computerName))" $searcher.PropertiesToLoad.Add("distinguishedname") | Out-Null $searcher.PropertiesToLoad.Add("memberof") | Out-Null $adResult = $searcher.FindOne() $res.InAD = [bool]$adResult # the lookup worked, so "no result" really means "not in AD" if ($adResult) { $dn = $adResult.Properties["distinguishedname"][0] if ($dn) { $ouParts = @($dn -split ',' | Where-Object { $_ -match '^OU=' } | ForEach-Object { $_.Substring(3) }) [array]::Reverse($ouParts) $res.OU = if ($ouParts.Count -gt 0) { $ouParts -join '/' } else { $dn } } $groupDNs = $adResult.Properties["memberof"] if ($groupDNs) { $groups = $groupDNs | ForEach-Object { ($_ -split ',')[0].Substring(3) } $res.Groups = $groups -join "
" } } } catch {} finally { if ($null -ne $searcher) { $searcher.Dispose() } } $ai = $script:SccmCfg.AssetInventory $useAsset = $ai.Enabled -and (Test-SqlName $ai.View) try { if ($SqlConn -and $SqlConn.State -eq 'Open') { $cmd = $SqlConn.CreateCommand() $assetSelect = "" $assetJoin = "" if ($useAsset) { $assetSelect = ", ai.ResourceID AS AI_ResourceID, " + (Get-AssetInventorySelect "ai") $assetJoin = "LEFT JOIN $($ai.View) ai ON sys.ResourceID = ai.ResourceID" } $cmd.CommandText = @" SELECT TOP 1 sys.User_Name0, sys.Client0, sys.Active0, cs.Model0, mem.TotalPhysicalMemory0, mac.MAC_Addresses0, ip.IP_Addresses0 $assetSelect FROM v_R_System sys $assetJoin LEFT JOIN v_GS_COMPUTER_SYSTEM cs ON sys.ResourceID = cs.ResourceID LEFT JOIN v_GS_X86_PC_MEMORY mem ON sys.ResourceID = mem.ResourceID LEFT JOIN (SELECT ResourceID, MAX(MAC_Addresses0) as MAC_Addresses0 FROM v_RA_System_MACAddresses GROUP BY ResourceID) mac ON sys.ResourceID = mac.ResourceID LEFT JOIN (SELECT ResourceID, MAX(IP_Addresses0) as IP_Addresses0 FROM v_RA_System_IPAddresses GROUP BY ResourceID) ip ON sys.ResourceID = ip.ResourceID WHERE sys.Name0 = @CompName "@ $cmd.Parameters.AddWithValue("@CompName", $computerName) | Out-Null $reader = $cmd.ExecuteReader() $assetExists = $false $res.InSCCM = $false if ($reader.Read()) { $res.InSCCM = $true if ($reader["MAC_Addresses0"] -isnot [DBNull] -and $reader["MAC_Addresses0"]) { $res.MAC = $reader["MAC_Addresses0"] } if ($reader["Model0"] -isnot [DBNull] -and $reader["Model0"]) { $res.Model = $reader["Model0"] } if ($reader["TotalPhysicalMemory0"] -isnot [DBNull] -and $reader["TotalPhysicalMemory0"]) { $res.RAM = "$([math]::Round([long]$reader["TotalPhysicalMemory0"] / 1048576, 1)) GB" } if ($reader["IP_Addresses0"] -isnot [DBNull] -and $reader["IP_Addresses0"]) { $res.InventoryIP = $reader["IP_Addresses0"] } if ($reader["User_Name0"] -isnot [DBNull] -and $reader["User_Name0"]) { $res.LastUser = $reader["User_Name0"] } $cStat = if ("$($reader["Client0"])" -eq "1") { "Installed" } else { "Not Installed" } $aStat = if ("$($reader["Active0"])" -eq "1") { "Active" } else { "Inactive" } $res.ClientStatus = "$cStat / $aStat" if ($useAsset) { # Check if the asset-info record actually exists if ($reader["AI_ResourceID"] -isnot [DBNull]) { $assetExists = $true } $rTypes = Read-AssetInventoryRow $reader $res $res.RoomType = if ($rTypes.Count -gt 0) { $rTypes -join ", " } else { "Standard" } } } $reader.Dispose() # --- Fallback asset view (Only if the primary row was completely missing) --- if ($useAsset -and -not $assetExists -and (Test-SqlName $ai.FallbackView)) { if ($v) { Write-Host " -> Not in $($ai.View), trying $($ai.FallbackView) fallback..." -ForegroundColor DarkYellow } $fallbackCmd = $SqlConn.CreateCommand() $fallbackCmd.CommandText = @" SELECT TOP 1 $(Get-AssetInventorySelect "fb") FROM v_R_System sys INNER JOIN $($ai.FallbackView) fb ON sys.ResourceID = fb.ResourceID WHERE sys.Name0 = @CompName "@ $fallbackCmd.Parameters.AddWithValue("@CompName", $computerName) | Out-Null $fbReader = $fallbackCmd.ExecuteReader() if ($fbReader.Read()) { $rTypes = Read-AssetInventoryRow $fbReader $res if ($rTypes.Count -gt 0) { $res.RoomType = $rTypes -join ", " } } $fbReader.Dispose() $fallbackCmd.Dispose() } # ------------------------------------------ $cmd.CommandText = "SELECT UniqueUserName FROM v_UserMachineRelationship WHERE MachineResourceName = @CompName2" $cmd.Parameters.AddWithValue("@CompName2", $computerName) | Out-Null $reader2 = $cmd.ExecuteReader() $uda = @() while ($reader2.Read()) { $uName = $reader2["UniqueUserName"].ToString() if ($uName -match '^[^\\]+\\(.*)') { $uName = $Matches[1] } # strip DOMAIN\ $uda += $uName } if ($uda.Count -gt 0) { $res.SCCMPrimaryUsers = $uda -join "
" } $reader2.Dispose() $cmd.Dispose() } } catch { } # --- Physical Disk Query --- try { if ($SqlConn -and $SqlConn.State -eq 'Open') { $diskCmd = $SqlConn.CreateCommand() $diskCmd.CommandText = @" SELECT TOP 1 pd.Model0, pd.SerialNumber0, pd.Size0 FROM v_R_System sys LEFT JOIN v_GS_PHYSICAL_DISK pd ON sys.ResourceID = pd.ResourceID WHERE sys.Name0 = @DiskCompName AND pd.MediaType0 = 4 ORDER BY pd.Size0 DESC "@ $diskCmd.Parameters.AddWithValue("@DiskCompName", $computerName) | Out-Null $diskReader = $diskCmd.ExecuteReader() if ($diskReader.Read()) { $diskModel = if ($diskReader["Model0"] -isnot [DBNull]) { $diskReader["Model0"] } else { "N/A" } $diskSerial = if ($diskReader["SerialNumber0"] -isnot [DBNull]) { $diskReader["SerialNumber0"] } else { "N/A" } $diskSizeGB = if ($diskReader["Size0"] -isnot [DBNull] -and $diskReader["Size0"]) { "$([math]::Round([long]$diskReader["Size0"] / 1GB)) GB" } else { "N/A" } $res.Disk = "Size: $diskSizeGB
Model: $diskModel
S/N: $diskSerial" } $diskReader.Dispose() $diskCmd.Dispose() } } catch {} # --------------------------- # --- Monitors (WmiMonitorID hardware inventory) --- $mon = $script:SccmCfg.Monitors if ($mon -and $mon.Enabled -and (Test-SqlName $mon.View) -and (Test-SqlName $mon.NameColumn) -and (Test-SqlName $mon.ManufacturerColumn)) { try { if ($SqlConn -and $SqlConn.State -eq 'Open') { $monCmd = $SqlConn.CreateCommand() $monCmd.CommandText = @" SELECT m.[$($mon.NameColumn)] AS MonName, m.[$($mon.ManufacturerColumn)] AS MonMfr FROM v_R_System sys INNER JOIN $($mon.View) m ON sys.ResourceID = m.ResourceID WHERE sys.Name0 = @MonCompName "@ $monCmd.Parameters.AddWithValue("@MonCompName", $computerName) | Out-Null $monReader = $monCmd.ExecuteReader() $names = @() while ($monReader.Read()) { $name = ConvertFrom-WmiCharCodes $monReader["MonName"] $mfr = ConvertFrom-WmiCharCodes $monReader["MonMfr"] if ($mon.InternalPattern -and $mfr -match $mon.InternalPattern -and -not $name) { continue } # laptop panel $names += $(if ($name) { $name } elseif ($mfr) { "$mfr (unnamed)" } else { "Unknown monitor" }) } $monReader.Dispose(); $monCmd.Dispose() $res.MonitorCount = "$($names.Count)" $res.Monitors = if ($names.Count -gt 0) { $names -join "
" } else { "None" } } } catch {} } return $res } # WmiMonitorID strings are uint16 arrays; SCCM inventories them as "72,80,32,69,50,52,0,0" function ConvertFrom-WmiCharCodes($value) { if ($null -eq $value -or $value -is [DBNull]) { return "" } $text = "$value".Trim() if ($text -match '^\d+(\s*,\s*\d+)*$') { $text = -join ($text -split '\s*,\s*' | Where-Object { [int]$_ -gt 0 } | ForEach-Object { [char][int]$_ }) } return $text.Trim() } # When a device is tracked for the first time and doesn't answer a ping, "last online" is # backfilled from the client's last activity (heartbeat / policy / status messages). function Get-DeviceLastSeen { param([string]$computerName, $Session) $SqlConn = $Session.Sql if (-not $SqlConn -or $SqlConn.State -ne 'Open') { return $null } $cmd = $SqlConn.CreateCommand() try { $cmd.CommandText = "SELECT TOP 1 ch.LastActiveTime FROM v_R_System sys INNER JOIN v_CH_ClientSummary ch ON sys.ResourceID = ch.ResourceID WHERE sys.Name0 = @SeenName" $cmd.Parameters.AddWithValue("@SeenName", $computerName) | Out-Null $val = $cmd.ExecuteScalar() if ($val -and $val -isnot [DBNull]) { return ([datetime]$val).ToLocalTime() } } catch {} finally { $cmd.Dispose() } return $null } # ----------------------------------------------------------------------------------------- # Installed software (used by Get-Software.ps1) # ----------------------------------------------------------------------------------------- function Get-InstalledSoftware { param([string]$ComputerName) $sql = @" SELECT DISTINCT ARPDisplayName0 AS Name, ProductVersion0 AS Version, Publisher0 AS Publisher, InstallSource0 AS Source, InstallDate0 AS InstallDate FROM v_GS_INSTALLED_SOFTWARE SW INNER JOIN v_R_System SYS ON SW.ResourceID = SYS.ResourceID WHERE SYS.Name0 = @CompName AND ARPDisplayName0 IS NOT NULL ORDER BY ARPDisplayName0 "@ $rows = @() $conn = New-Object System.Data.SqlClient.SqlConnection $conn.ConnectionString = "Server=$($script:SccmCfg.SqlServer);Database=$($script:SccmCfg.Database);Integrated Security=True;" $conn.Open() try { $cmd = $conn.CreateCommand() $cmd.CommandText = $sql $cmd.CommandTimeout = 30 $cmd.Parameters.AddWithValue("@CompName", $ComputerName) | Out-Null $adapter = New-Object System.Data.SqlClient.SqlDataAdapter $cmd $dataset = New-Object System.Data.DataSet $null = $adapter.Fill($dataset) if ($dataset.Tables.Count -gt 0) { foreach ($row in $dataset.Tables[0].Rows) { $rows += [PSCustomObject]@{ Name = "$($row.Name)" Version = $row.Version Publisher = $row.Publisher Source = $row.Source InstallDate = if ($row.InstallDate -isnot [DBNull]) { $row.InstallDate } else { "" } } } } } finally { $conn.Close() } return $rows }