diff --git a/CHANGELOG.md b/CHANGELOG.md index c66a01f..5fb6e79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/NetDiagram-PS/Private/Resolve-SnmpCredentialConfig.ps1 b/NetDiagram-PS/Private/Resolve-SnmpCredentialConfig.ps1 index df3a539..696cedc 100644 --- a/NetDiagram-PS/Private/Resolve-SnmpCredentialConfig.ps1 +++ b/NetDiagram-PS/Private/Resolve-SnmpCredentialConfig.ps1 @@ -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) { diff --git a/NetDiagram-PS/Public/Get-SnmpNeighbors.ps1 b/NetDiagram-PS/Public/Get-SnmpNeighbors.ps1 index b850af9..7d7adbd 100644 --- a/NetDiagram-PS/Public/Get-SnmpNeighbors.ps1 +++ b/NetDiagram-PS/Public/Get-SnmpNeighbors.ps1 @@ -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. diff --git a/examples/credmap.json b/examples/credmap.json index 62113e6..d587238 100644 --- a/examples/credmap.json +++ b/examples/credmap.json @@ -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." } } diff --git a/tests/NetDiagram.Tests.ps1 b/tests/NetDiagram.Tests.ps1 index f94cda5..46b67a0 100644 --- a/tests/NetDiagram.Tests.ps1 +++ b/tests/NetDiagram.Tests.ps1 @@ -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