# =========================================================================================
# 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
# =========================================================================================
$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 {
$searcher = [adsisearcher]"(&(objectCategory=computer)(name=$computerName))"
$searcher.PropertiesToLoad.Add("distinguishedname") | Out-Null
$searcher.PropertiesToLoad.Add("memberof") | Out-Null
$adResult = $searcher.FindOne()
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
if ($reader.Read()) {
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 {}
# ---------------------------
return $res
}
# -----------------------------------------------------------------------------------------
# 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
}