Public/Get-VBEnrichmentContext.ps1
|
function Get-VBEnrichmentContext { <# .SYNOPSIS Build the environment context object that drives every other VB.DNSEnrichment function. .DESCRIPTION Probes the local environment to determine which enrichment layers are usable in the current runtime. Returns a single PSCustomObject containing PowerShell version flags, machine identity, per-layer availability flags, storage paths, performance defaults, and a PrerequisiteReport array (one row per check). Unless -Quiet is supplied, prints a formatted console report so the operator knows exactly which layers will run, which will skip, and why. Checks performed (in order): 1. PowerShell version -> PSMajor, CanUseParallel, CanSkipCertCheck 2. Domain join -> IsDomainJoined, DomainName 3. DC role -> IsDomainController 4. DNS resolution works -> DNSAvailable 5. Local DNS role -> DNSIsLocal 6. AD module + Get-ADDomain -> ADAvailable, ADIsLocal 7. DhcpServer module -> DHCPAvailable 8. DHCP server reachable -> DHCPAvailable confirmed 9. Local DHCP role -> DHCPIsLocal 10. SNMP olePrn COM -> SNMPAvailable 11. OUI file -> OUIFileAvailable, OUIFileAge 12. mDNS dns-sd.exe -> mDNSAvailable 13. PSSQLite module -> CanUsePSSQLite 14. SQLite database init -> DatabaseInitialized This function MUST be called before any other VB.DNSEnrichment function. .PARAMETER DHCPServer FQDN or IP of the DHCP server to query in subsequent runs. Optional -- omit to skip DHCP probing entirely. .PARAMETER DHCPScopeIds Array of DHCP scope IDs (e.g. '192.168.1.0') the cache should pre-load. Optional -- omit to enumerate all scopes the server exposes. .PARAMETER SwitchTargets Array of managed-switch IPs for the optional Switch ARP layer (10). Empty = layer 10 skipped. .PARAMETER SNMPCommunityStrings Array of SNMP community strings to try in order. First match wins. Defaults to @('public'). .PARAMETER OUIFilePath Path to the IEEE OUI CSV. Defaults to "<module>\Data\oui.csv". .PARAMETER DatabasePath Path to the SQLite database. Defaults to "$env:LOCALAPPDATA\VB.DNSEnrichment\enrichment.db". .PARAMETER ParallelThrottleLimit Maximum parallel runspaces for active probes on PS 7. Defaults to 10. .PARAMETER DisableNetworkProbe Switch -- disables active layers (4-10) entirely. Use when network probing is not permitted (e.g. running from a hardened jumpbox). .PARAMETER Quiet Switch -- suppress the console prerequisite report. The PrerequisiteReport property on the returned object is still populated. .OUTPUTS [PSCustomObject] -- the full context object. See module design v2 section 7 for the complete schema. .EXAMPLE $ctx = Get-VBEnrichmentContext .EXAMPLE $ctx = Get-VBEnrichmentContext -DHCPServer 'dhcp01.corp.local' ` -SNMPCommunityStrings 'public','readonly' -Quiet .NOTES Version: 1.0.0 MinPSVersion: 5.1 Author: VB ChangeLog: 1.0.0 -- 2026-05-10 -- Initial release #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter()] [string]$DHCPServer, [Parameter()] [string[]]$DHCPScopeIds = @(), [Parameter()] [string[]]$SwitchTargets = @(), [Parameter()] [string[]]$SNMPCommunityStrings = @('public'), [Parameter()] [string]$OUIFilePath, [Parameter()] [string]$DatabasePath, [Parameter()] [int]$ParallelThrottleLimit = 10, [Parameter()] [switch]$DisableNetworkProbe, [Parameter()] [switch]$Quiet ) $sw = [System.Diagnostics.Stopwatch]::StartNew() $report = New-Object System.Collections.Generic.List[PSCustomObject] # Resolve default paths -- derive module root from the .psm1 path, not $PSScriptRoot if (-not $OUIFilePath) { $moduleRoot = Split-Path -Path (Get-Module -Name 'VB.DNSEnrichment').Path -Parent $OUIFilePath = Join-Path $moduleRoot 'Data\oui.csv' Write-Verbose "[Context] OUI path: $OUIFilePath" } if (-not $DatabasePath) { $DatabasePath = Join-Path $env:LOCALAPPDATA 'VB.DNSEnrichment\enrichment.db' } # --- Helper: append a PrerequisiteReport row --- $addReport = { param($Layer, $Prerequisite, $Status, $Detail, $LayersAffected, $SkippedLayers, $Impact, $Remediation, $Severity = 'Info') $report.Add([PSCustomObject]@{ Layer = $Layer Prerequisite = $Prerequisite Status = $Status Severity = $Severity Detail = $Detail LayersAffected = $LayersAffected SkippedLayers = $SkippedLayers Impact = $Impact Remediation = $Remediation }) } # ============================================================ # CHECK 1 -- PowerShell version # ============================================================ $psVersion = $PSVersionTable.PSVersion $psMajor = $psVersion.Major $psEditionStr = $PSVersionTable.PSEdition $canUseParallel = $false if ($psMajor -ge 7) { try { $null = [System.Management.Automation.Runspaces.Runspace]::DefaultRunspace $canUseParallel = $true } catch { Write-Verbose "[Context] ForEach-Object -Parallel not available in this runspace: $($_.Exception.Message)" } } $canSkipCertCheck = ($psMajor -ge 6) & $addReport 0 'PowerShell Version' 'Available' "$psVersion ($psEditionStr)" ` 'All layers' '' '' '' 'Info' # ============================================================ # CHECK 2 -- Domain join # ============================================================ $isDomainJoined = $false $domainName = $null try { $cs = Get-CimInstance -ClassName Win32_ComputerSystem -OperationTimeoutSec 5 -ErrorAction Stop $isDomainJoined = [bool]$cs.PartOfDomain if ($isDomainJoined) { $domainName = $cs.Domain } } catch { Write-Verbose "[Context] Win32_ComputerSystem query failed: $($_.Exception.Message)" } # ============================================================ # CHECK 3 -- Domain Controller role # ============================================================ $isDomainController = $false try { $productOptions = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\ProductOptions' ` -ErrorAction Stop if ($productOptions.ProductType -eq 'LanmanNT') { $isDomainController = $true } } catch { Write-Verbose "[Context] ProductOptions registry read failed: $($_.Exception.Message)" } # ============================================================ # CHECK 4 -- DNS resolution works (Layer 3 -- PTR) # ============================================================ $dnsAvailable = $false $dnsServer = $null try { if (Get-Command -Name Resolve-DnsName -ErrorAction SilentlyContinue) { $null = Resolve-DnsName -Name '127.0.0.1' -Type PTR -ErrorAction Stop $dnsAvailable = $true } else { $null = [System.Net.Dns]::GetHostEntry('127.0.0.1') $dnsAvailable = $true } } catch { Write-Verbose "[Context] DNS resolution test failed: $($_.Exception.Message)" } try { $dnsClient = Get-DnsClientServerAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | Where-Object { $_.ServerAddresses.Count -gt 0 } | Select-Object -First 1 if ($dnsClient) { $dnsServer = $dnsClient.ServerAddresses[0] } } catch { Write-Verbose "[Context] DNS client server enumeration failed: $($_.Exception.Message)" } if ($dnsAvailable) { & $addReport 3 'DNS (PTR)' 'Available' 'Resolve-DnsName / GetHostEntry works' ` 'Layer 3 (PTR)' '' '' '' 'Info' } else { & $addReport 3 'DNS (PTR)' 'Unavailable' 'Resolve-DnsName failed' ` 'Layer 3 (PTR)' 'Layer 3 (PTR)' ` 'PTR-based hostname resolution unavailable' ` 'Verify DNS client configuration; try Test-NetConnection <ip> -InformationLevel Detailed' ` 'Critical' } # ============================================================ # CHECK 5 -- Local DNS server role # ============================================================ $dnsIsLocal = $false $dnsService = Get-Service -Name 'DNS' -ErrorAction SilentlyContinue if ($dnsService -and $dnsService.Status -eq 'Running') { $dnsIsLocal = $true } # ============================================================ # CHECK 6 -- AD module + Get-ADDomain # ============================================================ $adAvailable = $false $adIsLocal = $isDomainController try { Import-Module -Name ActiveDirectory -ErrorAction Stop $null = Get-ADDomain -ErrorAction Stop $adAvailable = $true } catch { Write-Verbose "[Context] AD module/Get-ADDomain failed: $($_.Exception.Message)" } if ($adAvailable) { $detail = if ($adIsLocal) { 'Local DC -- auth-free queries' } else { 'Remote AD via RSAT' } & $addReport 1 'Active Directory' 'Available' $detail ` 'Layer 1 (AD)' '' '' '' 'Info' } else { & $addReport 1 'Active Directory' 'Unavailable' 'AD module not loaded or Get-ADDomain failed' ` 'Layer 1 (AD)' 'Layer 1 (AD)' ` 'Cannot resolve domain-joined hosts via AD -- relies on DHCP/PTR/probe layers' ` 'Install RSAT-AD-PowerShell: Add-WindowsCapability -Online -Name "Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0"' ` 'Critical' } # ============================================================ # CHECK 7 + 8 -- DhcpServer module + reachable # ============================================================ $dhcpAvailable = $false $dhcpScopeIdsResolved = @() try { Import-Module -Name DhcpServer -ErrorAction Stop if ($DHCPServer) { $scopes = Get-DhcpServerv4Scope -ComputerName $DHCPServer -ErrorAction Stop $dhcpAvailable = $true if ($DHCPScopeIds.Count -gt 0) { $dhcpScopeIdsResolved = $DHCPScopeIds } else { $dhcpScopeIdsResolved = @($scopes | ForEach-Object { $_.ScopeId.ToString() }) } } elseif ($isDomainController -or (Get-Service -Name 'DHCPServer' -ErrorAction SilentlyContinue)) { # Local DHCP role available -- try localhost $scopes = Get-DhcpServerv4Scope -ErrorAction Stop $dhcpAvailable = $true $dhcpScopeIdsResolved = @($scopes | ForEach-Object { $_.ScopeId.ToString() }) } } catch { Write-Verbose "[Context] DhcpServer module/scope query failed: $($_.Exception.Message)" } if ($dhcpAvailable) { $detail = if ($DHCPServer) { "Server: $DHCPServer ($($dhcpScopeIdsResolved.Count) scopes)" } else { "Local DHCP role ($($dhcpScopeIdsResolved.Count) scopes)" } & $addReport 2 'DHCP Leases' 'Available' $detail ` 'Layer 2 (DHCP)' '' '' '' 'Info' } elseif ($DHCPServer) { & $addReport 2 'DHCP Leases' 'Unavailable' "DhcpServer module load or scope query against '$DHCPServer' failed" ` 'Layer 2 (DHCP)' 'Layer 2 (DHCP)' ` 'No DHCP-derived hostname/MAC for leased IPs' ` 'Install RSAT-DHCP: Add-WindowsCapability -Online -Name "Rsat.DHCP.Tools~~~~0.0.1.0"' ` 'Warning' } else { & $addReport 2 'DHCP Leases' 'NotConfigured' 'No -DHCPServer supplied and no local DHCP role detected' ` 'Layer 2 (DHCP)' 'Layer 2 (DHCP)' ` 'No DHCP-derived hostname/MAC for leased IPs' ` 'Re-run Get-VBEnrichmentContext with -DHCPServer <fqdn>' ` 'Warning' } # ============================================================ # CHECK 9 -- Local DHCP server role # ============================================================ $dhcpIsLocal = $false $dhcpService = Get-Service -Name 'DHCPServer' -ErrorAction SilentlyContinue if ($dhcpService -and $dhcpService.Status -eq 'Running') { $dhcpIsLocal = $true } # ============================================================ # CHECK 10 -- SNMP olePrn COM # ============================================================ $snmpAvailable = $false $snmp = $null try { $snmp = New-Object -ComObject olePrn.OleSNMP -ErrorAction Stop $snmpAvailable = $true } catch { Write-Verbose "[Context] olePrn.OleSNMP COM creation failed: $($_.Exception.Message)" } finally { if ($snmp) { try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($snmp) | Out-Null } catch { } $snmp = $null } } if ($snmpAvailable) { & $addReport 7 'SNMP (olePrn)' 'Available' 'COM object created OK' ` 'Layer 7 (SNMP), Layer 10 (Switch ARP)' '' '' '' 'Info' } else { & $addReport 7 'SNMP (olePrn)' 'Unavailable' 'olePrn.OleSNMP COM not creatable' ` 'Layer 7 (SNMP), Layer 10 (Switch ARP)' 'Layer 7 (SNMP), Layer 10 (Switch)' ` 'No SNMP sysName/sysLocation; no switch ARP/port lookup' ` 'Install Print and Document Services -> LPD Service feature; reboot if required' ` 'Warning' } # ============================================================ # CHECK 11 -- OUI file # ============================================================ $ouiFileAvailable = $false $ouiFileAge = $null if (Test-Path -LiteralPath $OUIFilePath) { $ouiItem = Get-Item -LiteralPath $OUIFilePath # Per design: "if file > 1MB" treat as valid; otherwise treat as missing/corrupt if ($ouiItem.Length -gt 1MB) { $ouiFileAvailable = $true $ouiFileAge = (Get-Date) - $ouiItem.LastWriteTime } } if ($ouiFileAvailable) { $sizeMB = [math]::Round($ouiItem.Length / 1MB, 1) $ageDays = [int]$ouiFileAge.TotalDays & $addReport 11 'OUI Vendor Lookup' 'Available' "oui.csv $sizeMB MB, age $ageDays days" ` 'Layer 11 (OUI)' '' '' '' 'Info' } else { & $addReport 11 'OUI Vendor Lookup' 'NotConfigured' "oui.csv missing or < 1MB at $OUIFilePath" ` 'Layer 11 (OUI)' '' ` 'No vendor lookup until first run -- Get-VBOUIVendor downloads on first call' ` 'Will be auto-downloaded on first run; or pre-stage with Invoke-WebRequest -Uri https://standards-oui.ieee.org/oui/oui.csv' ` 'Warning' } # ============================================================ # CHECK 12 -- mDNS dns-sd.exe # ============================================================ $mDNSAvailable = $false if (Get-Command -Name 'dns-sd.exe' -ErrorAction SilentlyContinue) { $mDNSAvailable = $true } if ($mDNSAvailable) { & $addReport 9 'mDNS Discovery' 'Available' 'dns-sd.exe found on PATH' ` 'Layer 9 (mDNS)' '' '' '' 'Info' } else { & $addReport 9 'mDNS Discovery' 'Unavailable' 'dns-sd.exe not on PATH' ` 'Layer 9 (mDNS)' 'Layer 9 (mDNS)' ` 'Printers/scanners using mDNS only may be missed' ` 'Install Bonjour Print Services from Apple, or skip if mDNS is not in use' ` 'Info' } # ============================================================ # CHECK 13 -- PSSQLite module # ============================================================ $canUsePSSQLite = [bool](Get-Module -Name PSSQLite -ListAvailable) if ($canUsePSSQLite) { & $addReport 0 'PSSQLite Module' 'Available' 'Module loadable' ` 'Storage layer' '' '' '' 'Info' } else { & $addReport 0 'PSSQLite Module' 'Unavailable' 'PSSQLite not installed' ` 'Storage layer' 'All caching/persistence' ` 'No SQLite cache -- every run re-probes every IP' ` 'Install-Module PSSQLite -Scope CurrentUser' ` 'Warning' } # ============================================================ # CHECK 14 -- SQLite database init # ============================================================ $databaseInitialized = $false if ($canUsePSSQLite) { try { $sqlFolder = Join-Path (Split-Path (Get-Module 'VB.DNSEnrichment').Path -Parent) 'Sql' if (-not (Test-Path -LiteralPath $sqlFolder)) { & $addReport 0 'SQLite Database' 'Unavailable' ` "Sql migration folder not found at: $sqlFolder" ` 'Storage layer' 'All caching/persistence' ` 'Cannot persist enrichment results' ` "Verify the module is imported from its installed location (Sql\ folder must exist beside .psm1)" ` 'Critical' } else { $initResult = Initialize-VBEnrichmentDatabase -DatabasePath $DatabasePath if ($initResult.Status -eq 'Success') { $databaseInitialized = $true & $addReport 0 'SQLite Database' 'Available' "$DatabasePath (schema v$($initResult.Version))" ` 'Storage layer' '' '' '' 'Info' } else { & $addReport 0 'SQLite Database' 'Unavailable' "$($initResult.Error) | Expected SQL folder: $sqlFolder" ` 'Storage layer' 'All caching/persistence' ` 'Cannot persist enrichment results' ` "Verify write access to $DatabasePath and that Sql\001_init.sql exists" ` 'Critical' } } } catch { $sqlFolderHint = Join-Path (Split-Path (Get-Module 'VB.DNSEnrichment').Path -Parent) 'Sql' & $addReport 0 'SQLite Database' 'Unavailable' "$($_.Exception.Message) | Expected SQL folder: $sqlFolderHint" ` 'Storage layer' 'All caching/persistence' ` 'Cannot persist enrichment results' ` "Verify write access to $DatabasePath and that Sql\001_init.sql exists" ` 'Critical' } } # ============================================================ # NetworkProbeEnabled / RTSPProbeEnabled / report rows for layers 4-8, 10 # ============================================================ $networkProbeEnabled = -not $DisableNetworkProbe $rtspProbeEnabled = $networkProbeEnabled if ($networkProbeEnabled) { & $addReport 4 'ARP Cache' 'Available' 'Native -- arp -a' 'Layer 4 (ARP)' '' '' '' 'Info' & $addReport 5 'TCP Port Scan' 'Available' 'Network probe enabled' 'Layer 5 (TCP)' '' '' '' 'Info' $httpBannerDetail = if ($canSkipCertCheck) { 'PS 6+ native cert bypass' } else { 'PS 5.1 -- cert callback workaround' } & $addReport 6 'HTTP Banner' 'Available' $httpBannerDetail 'Layer 6 (HTTP)' '' '' '' 'Info' & $addReport 8 'RTSP Probe' 'Available' 'Network probe enabled' 'Layer 8 (RTSP)' '' '' '' 'Info' } else { & $addReport 4 'ARP Cache' 'Skipped' 'Network probe disabled' 'Layer 4 (ARP)' 'Layer 4' 'No MAC for unleased IPs' 'Re-run without -DisableNetworkProbe' 'Info' & $addReport 5 'TCP Port Scan' 'Skipped' 'Network probe disabled' 'Layer 5 (TCP)' 'Layer 5' 'No port-based device class signals' 'Re-run without -DisableNetworkProbe' 'Info' & $addReport 6 'HTTP Banner' 'Skipped' 'Network probe disabled' 'Layer 6 (HTTP)' 'Layer 6' 'No HTTP banner -- many printers/cameras unidentified' 'Re-run without -DisableNetworkProbe' 'Info' & $addReport 8 'RTSP Probe' 'Skipped' 'Network probe disabled' 'Layer 8 (RTSP)' 'Layer 8' 'No RTSP banner for cameras' 'Re-run without -DisableNetworkProbe' 'Info' } if ($SwitchTargets.Count -gt 0 -and $snmpAvailable) { & $addReport 10 'Switch ARP' 'Available' "$($SwitchTargets.Count) switch(es) configured" 'Layer 10 (Switch)' '' '' '' 'Info' } elseif ($SwitchTargets.Count -gt 0 -and -not $snmpAvailable) { & $addReport 10 'Switch ARP' 'Unavailable' 'SNMP unavailable' 'Layer 10 (Switch)' 'Layer 10' 'No switch port location lookup' 'Resolve SNMP COM availability' 'Warning' } else { & $addReport 10 'Switch ARP' 'NotConfigured' 'No SwitchTargets supplied' 'Layer 10 (Switch)' 'Layer 10' "Unresolved IPs won't get switch port location" 'Re-run with -SwitchTargets <ip[,ip...]>' 'Info' } $sw.Stop() # ============================================================ # Build the context object # ============================================================ $context = [PSCustomObject]@{ # PowerShell environment PSVersion = $psVersion PSMajor = $psMajor PSEdition = $psEditionStr CanUseParallel = $canUseParallel CanSkipCertCheck = $canSkipCertCheck CanUsePSSQLite = $canUsePSSQLite # Machine identity ComputerName = $env:COMPUTERNAME IsDomainJoined = $isDomainJoined IsDomainController = $isDomainController DomainName = $domainName # Layer availability DNSAvailable = $dnsAvailable DNSIsLocal = $dnsIsLocal DNSServer = $dnsServer DHCPAvailable = $dhcpAvailable DHCPIsLocal = $dhcpIsLocal DHCPServer = $DHCPServer DHCPScopeIds = $dhcpScopeIdsResolved ADAvailable = $adAvailable ADIsLocal = $adIsLocal NetworkProbeEnabled = $networkProbeEnabled SNMPAvailable = $snmpAvailable SNMPCommunityStrings = @($SNMPCommunityStrings | ForEach-Object { ConvertTo-SecureString $_ -AsPlainText -Force }) OUIFileAvailable = $ouiFileAvailable OUIFilePath = $OUIFilePath OUIFileAge = $ouiFileAge mDNSAvailable = $mDNSAvailable RTSPProbeEnabled = $rtspProbeEnabled SwitchTargets = $SwitchTargets # Storage DatabasePath = $DatabasePath DatabaseInitialized = $databaseInitialized # Performance ParallelThrottleLimit = $ParallelThrottleLimit DefaultTimeoutMs = @{ TCP = 1000 HTTP = 5000 SNMP = 3000 RTSP = 3000 } # Reporting PrerequisiteReport = $report.ToArray() ContextBuiltAt = (Get-Date) ContextDurationMs = [int]$sw.ElapsedMilliseconds } if (-not $Quiet) { Format-VBEnrichmentContext -Context $context } $context } |