Public/Get-VBSNMPIdentity.ps1

function Get-VBSNMPIdentity {
<#
.SYNOPSIS
    Query a device via SNMP v1/v2c to retrieve sysDescr, sysName, and sysLocation (Layer 7).
 
.DESCRIPTION
    Uses the Windows-native olePrn.OleSNMP COM object (no third-party module required).
    Iterates $Context.SNMPCommunityStrings in order -- first string that gets a
    response wins. Records which community string succeeded in CommunityUsed.
 
    OIDs queried:
        1.3.6.1.2.1.1.1.0 sysDescr -> SNMPDescr
        1.3.6.1.2.1.1.5.0 sysName -> Hostname
        1.3.6.1.2.1.1.6.0 sysLocation -> Location
 
    The COM object is always released in a finally block on every code path.
    Timeout is enforced by setting the OleSNMP Timeout property before Open().
 
    Prerequisites: $Context.SNMPAvailable must be $true.
 
.PARAMETER IPAddress
    The RFC1918 / CGNAT / link-local IP address to query.
 
.PARAMETER Context
    Environment context from Get-VBEnrichmentContext. Provides SNMPAvailable,
    SNMPCommunityStrings, and DefaultTimeoutMs.SNMP.
 
.PARAMETER TimeoutMs
    SNMP response timeout in milliseconds per community string attempt.
    Defaults to $Context.DefaultTimeoutMs.SNMP (2000).
 
.OUTPUTS
    [PSCustomObject] -- base layer result fields plus:
        Hostname [string] sysName OID value
        SNMPDescr [string] sysDescr OID value
        Location [string] sysLocation OID value
        CommunityUsed [string] the community string that succeeded
        SNMPVersion [string] 'v1' (olePrn always uses v1/v2c)
 
.EXAMPLE
    $ctx = Get-VBEnrichmentContext -SNMPCommunityStrings 'public','readonly'
    Get-VBSNMPIdentity -IPAddress '192.168.1.200' -Context $ctx
 
.EXAMPLE
    '192.168.1.200','192.168.1.201' | Get-VBSNMPIdentity -Context $ctx
 
.NOTES
    Version: 1.0.0
    MinPSVersion: 5.1
    Author: VB
    ChangeLog:
        1.0.0 -- 2026-05-11 -- Initial release
#>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string]$IPAddress,

        [Parameter()]
        [PSCustomObject]$Context,

        [Parameter()]
        [int]$TimeoutMs = 2000
    )

    begin {
        $LAYER_NUM  = 7
        $LAYER_NAME = 'SNMP'

        if (-not $Context) {
            Write-Warning "[$LAYER_NAME] No context provided -- running without prerequisite validation."
        }

        if ($PSBoundParameters.ContainsKey('TimeoutMs') -eq $false -and $Context -and $Context.DefaultTimeoutMs) {
            $TimeoutMs = $Context.DefaultTimeoutMs.SNMP
        }

        $communityStrings = if ($Context -and $Context.SNMPCommunityStrings.Count -gt 0) {
            @($Context.SNMPCommunityStrings | ForEach-Object {
                [Runtime.InteropServices.Marshal]::PtrToStringAuto(
                    [Runtime.InteropServices.Marshal]::SecureStringToBSTR($_))
            })
        }
        else {
            @('public')
        }

        # OleSNMP Timeout property is in milliseconds
        $snmpTimeoutMs = $TimeoutMs
    }

    process {
        $sw = [System.Diagnostics.Stopwatch]::StartNew()

        if ($Context -and -not $Context.SNMPAvailable) {
            $sw.Stop()
            return New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME `
                -Status 'Skipped' -ExecutionMs $sw.ElapsedMilliseconds `
                -SkipReason 'SNMPUnavailable' `
                -Impact 'No SNMP sysName or sysLocation -- printers and network devices may be unidentified'
        }

        $snmp = $null
        $lastError = $null

        foreach ($community in $communityStrings) {
            try {
                $snmp = New-Object -ComObject olePrn.OleSNMP -ErrorAction Stop
                $snmp.Timeout = $snmpTimeoutMs
                $snmp.Open($IPAddress, $community, 2, $snmpTimeoutMs)

                $sysDescr    = $snmp.Get('1.3.6.1.2.1.1.1.0')
                $sysName     = $snmp.Get('1.3.6.1.2.1.1.5.0')
                $sysLocation = $snmp.Get('1.3.6.1.2.1.1.6.0')

                $snmpHostname  = if ([string]::IsNullOrWhiteSpace($sysName))     { $null } else { $sysName.Trim() }
                $snmpDescr     = if ([string]::IsNullOrWhiteSpace($sysDescr))    { $null } else { $sysDescr.Trim() }
                $snmpLocation  = if ([string]::IsNullOrWhiteSpace($sysLocation)) { $null } else { $sysLocation.Trim() }

                $sw.Stop()
                return New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME `
                    -Status 'Success' -ExecutionMs $sw.ElapsedMilliseconds `
                    -ExtraFields @{
                        Hostname      = $snmpHostname
                        SNMPDescr     = $snmpDescr
                        Location      = $snmpLocation
                        CommunityUsed = $community
                        SNMPVersion   = 'v1'
                    }
            }
            catch {
                $lastError = $_.Exception.Message
                Write-Verbose "[$LAYER_NAME] $IPAddress community '$community' failed: $lastError"
            }
            finally {
                if ($snmp) {
                    try { $snmp.Close() }   catch { }
                    try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($snmp) | Out-Null } catch { }
                    $snmp = $null
                }
            }
        }

        # All community strings exhausted
        $sw.Stop()
        if ($lastError -match 'timeout|timed out|no response' -or $lastError -match '0x80004005') {
            # Timeout = device not running SNMP or port filtered
            New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME `
                -Status 'NoResult' -ExecutionMs $sw.ElapsedMilliseconds
        }
        else {
            New-VBLayerResult -IPAddress $IPAddress -Layer $LAYER_NUM -LayerName $LAYER_NAME `
                -Status 'Failed' -ExecutionMs $sw.ElapsedMilliseconds `
                -ErrorDetail $lastError
        }
    }
}