Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
OUI coverage (built-in table remains a 31-prefix sample).

### Changed
- **`Resolve-SnmpCredentialConfig` accepts IPv6 CIDR keys.** Longest-prefix
match works on mixed v4/v6 maps. A wrong-family lookup returns no
credential (or `Default`); invalid CIDR keys still fail closed.
- **`Import-Inventory` accepts dual-stack addresses.** Device `ip` and subnet
`cidr` values may be IPv4 or IPv6. Both families are normalized (canonical
string form; CIDR host bits cleared). Uniqueness is the normalized address
Expand Down
23 changes: 15 additions & 8 deletions NetDiagram-PS/Private/Resolve-SnmpCredentialConfig.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,23 @@ function Resolve-SnmpCredentialConfig {
continue
}

if ($entry.Name -notmatch '^(\d{1,3}(?:\.\d{1,3}){3})\/(\d{1,2})$') {
throw "Invalid SNMP credential-map CIDR '$($entry.Name)'. Expected an IPv4 CIDR or Default."
$cidrParts = $entry.Name -split '/', 2
$networkAddress = $null
$prefixLength = 0
$maxPrefix = -1
if ($cidrParts.Count -eq 2 -and
[int]::TryParse($cidrParts[1], [ref]$prefixLength) -and
[System.Net.IPAddress]::TryParse($cidrParts[0], [ref]$networkAddress)) {
if ($networkAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) {
$maxPrefix = 32
}
elseif ($networkAddress.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6) {
$maxPrefix = 128
}
}

$prefixLength = [int]$matches[2]
$networkAddress = $null
if ($prefixLength -gt 32 -or
-not [System.Net.IPAddress]::TryParse($matches[1], [ref]$networkAddress) -or
$networkAddress.AddressFamily -ne [System.Net.Sockets.AddressFamily]::InterNetwork) {
throw "Invalid SNMP credential-map CIDR '$($entry.Name)'. Expected an IPv4 CIDR or Default."
if ($maxPrefix -lt 0 -or $prefixLength -lt 0 -or $prefixLength -gt $maxPrefix) {
throw "Invalid SNMP credential-map CIDR '$($entry.Name)'. Expected an IPv4 or IPv6 CIDR or Default."
}

if (Test-IPInSubnet -IP $IPAddress -CIDR $entry.Name) {
Expand Down
2 changes: 1 addition & 1 deletion NetDiagram-PS/Public/Get-SnmpNeighbors.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function Get-SnmpNeighbors {
does not assign verified L2-SNMP confidence because it does not fully correlate
structured LLDP/CDP table rows.

CREDENTIAL MATCHING: The most specific matching IPv4 CIDR wins regardless of
CREDENTIAL MATCHING: The most specific matching IPv4 or IPv6 CIDR wins regardless of
JSON property order. Default is used only when no CIDR matches. If the selected
entry has no available community secret, the node is skipped unless -TryPublic
was explicitly supplied.
Expand Down
2 changes: 1 addition & 1 deletion examples/credmap.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"setup": "1. Install: Install-Module Microsoft.PowerShell.SecretManagement",
"store_secret": "2. Store secrets: Set-Secret -Name 'YourSNMPSecretName' -Secret 'your-community-string'",
"usage": "3. Use: Get-SnmpNeighbors -CredentialMapPath .\\examples\\credmap.json",
"matching": "The most specific matching IPv4 CIDR wins. Default is used only when no CIDR matches.",
"matching": "The most specific matching IPv4 or IPv6 CIDR wins. Default is used only when no CIDR matches.",
"missing_secret": "A missing or unavailable selected secret skips that node unless -TryPublic is explicitly supplied."
}
}
59 changes: 59 additions & 0 deletions tests/NetDiagram.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1256,6 +1256,65 @@ Describe 'SNMP credential-map precedence (#17 regression)' {
}
}

Describe 'SNMP credential-map IPv6 CIDR keys (#74)' {
It 'Matches an IPv6 CIDR key' {
InModuleScope 'NetDiagram-PS' {
$map = '{"2001:db8::/32":{"communitySecret":"v6lab"}}' | ConvertFrom-Json
$match = Resolve-SnmpCredentialConfig -SnmpMap $map -IPAddress '2001:db8::1'

$match.CIDR | Should -Be '2001:db8::/32'
$match.Config.communitySecret | Should -Be 'v6lab'
}
}

It 'Chooses the longest matching IPv6 prefix' {
InModuleScope 'NetDiagram-PS' {
$map = '{"2001:db8::/32":{"communitySecret":"site"},"2001:db8:1::/48":{"communitySecret":"floor"}}' | ConvertFrom-Json
$match = Resolve-SnmpCredentialConfig -SnmpMap $map -IPAddress '2001:db8:1::10'

$match.CIDR | Should -Be '2001:db8:1::/48'
$match.Config.communitySecret | Should -Be 'floor'
}
}

It 'Keeps IPv4 longest-prefix matching unchanged on a mixed map' {
InModuleScope 'NetDiagram-PS' {
$map = '{"192.168.0.0/16":{"communitySecret":"v4site"},"192.168.1.0/24":{"communitySecret":"v4floor"},"2001:db8::/32":{"communitySecret":"v6lab"}}' | ConvertFrom-Json
$match = Resolve-SnmpCredentialConfig -SnmpMap $map -IPAddress '192.168.1.40'

$match.CIDR | Should -Be '192.168.1.0/24'
$match.Config.communitySecret | Should -Be 'v4floor'

$v6 = Resolve-SnmpCredentialConfig -SnmpMap $map -IPAddress '2001:db8::1'
$v6.CIDR | Should -Be '2001:db8::/32'
$v6.Config.communitySecret | Should -Be 'v6lab'
}
}

It 'Returns no credential for a wrong-family address (does not throw)' {
InModuleScope 'NetDiagram-PS' {
$v6only = '{"2001:db8::/32":{"communitySecret":"v6lab"}}' | ConvertFrom-Json
{ Resolve-SnmpCredentialConfig -SnmpMap $v6only -IPAddress '192.168.1.40' } | Should -Not -Throw
$fromV6Map = Resolve-SnmpCredentialConfig -SnmpMap $v6only -IPAddress '192.168.1.40'
$fromV6Map | Should -BeNullOrEmpty

$v4only = '{"10.0.0.0/8":{"communitySecret":"internal"}}' | ConvertFrom-Json
{ Resolve-SnmpCredentialConfig -SnmpMap $v4only -IPAddress '2001:db8::1' } | Should -Not -Throw
$fromV4Map = Resolve-SnmpCredentialConfig -SnmpMap $v4only -IPAddress '2001:db8::1'
$fromV4Map | Should -BeNullOrEmpty
}
}

It 'Rejects an invalid IPv6 CIDR entry' {
InModuleScope 'NetDiagram-PS' {
$map = '{"2001:db8::/129":{"communitySecret":"bad"}}' | ConvertFrom-Json

{ Resolve-SnmpCredentialConfig -SnmpMap $map -IPAddress '2001:db8::1' } |
Should -Throw '*Invalid SNMP credential-map CIDR*'
}
}
}

Describe 'Get-SnmpNeighbors node eligibility' {
BeforeEach {
# Empty credential map is valid; -TryPublic supplies the community so no
Expand Down