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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- **`Import-NmapScan` imports IPv6 hosts.** nmap `-oX` `address` elements with
`addrtype='ipv6'` are accepted when a host has no IPv4 address. Dual-stack
hosts keep a single node identity: IPv4 is preferred when both families are
present. Addresses are parsed with `[System.Net.IPAddress]::Parse` and stored
in canonical form; malformed addresses are skipped (same as invalid IPv4).

### Changed
- **`Import-Inventory` accepts dual-stack addresses.** Device `ip` and subnet
`cidr` values may be IPv4 or IPv6. Both families are normalized (canonical
Expand Down
31 changes: 24 additions & 7 deletions NetDiagram-PS/Public/Import-NmapScan.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,13 @@ function Import-NmapScan {
Imports topology from nmap XML output
.DESCRIPTION
Parses nmap -oX XML and returns a Topology with Nodes derived from hosts
whose status is up. Maps address/hostname/vendor/OS/ports. Role stays
unknown unless nmap data clearly indicates otherwise.
whose status is up. Reads IPv4 and IPv6 address elements (addrtype
ipv4/ipv6). When both families are present on one host, IPv4 is preferred
so the host keeps a single node identity; IPv6 is used only when no IPv4
address is present. Addresses are validated with [System.Net.IPAddress]::Parse
and stored in canonical form; malformed addresses are skipped. Maps
hostname/vendor/OS/ports. Role stays unknown unless nmap data clearly
indicates otherwise.
.PARAMETER Path
Path to nmap XML file
.EXAMPLE
Expand Down Expand Up @@ -37,14 +42,26 @@ function Import-NmapScan {
if ($state -and $state -ne 'up') { continue }

$ipv4 = $null
$ipv6 = $null
foreach ($addr in @($hostNode.address)) {
if ($addr.addrtype -eq 'ipv4' -and $addr.addr) { $ipv4 = $addr.addr; break }
if (-not $addr.addr) { continue }
if ($addr.addrtype -eq 'ipv4' -and -not $ipv4) { $ipv4 = $addr.addr }
elseif ($addr.addrtype -eq 'ipv6' -and -not $ipv6) { $ipv6 = $addr.addr }
}
if (-not $ipv4) { continue }

try { $null = [System.Net.IPAddress]::Parse($ipv4) } catch { continue }
# One node per nmap host: prefer IPv4 when both families are present.
$chosen = if ($ipv4) { $ipv4 } else { $ipv6 }
if (-not $chosen) { continue }

$hostname = $ipv4
try {
$parsed = [System.Net.IPAddress]::Parse($chosen)
}
catch {
continue
}
$ip = $parsed.ToString()

$hostname = $ip
if ($hostNode.PSObject.Properties['hostnames'] -and $hostNode.hostnames -and $hostNode.hostnames.PSObject.Properties['hostname'] -and $hostNode.hostnames.hostname) {
$hn = @($hostNode.hostnames.hostname)[0]
if ($hn.PSObject.Properties['name'] -and $hn.name) { $hostname = $hn.name }
Expand Down Expand Up @@ -73,7 +90,7 @@ function Import-NmapScan {
}

$nodes += [pscustomobject]@{
IP = $ipv4
IP = $ip
Hostname = $hostname
Role = 'unknown'
Vendor = $vendor
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ becomes `192.168.1.0/24` and `2001:db8::1/32` becomes `2001:db8::/32`).
Uniqueness is the normalized address string, so compressed and expanded IPv6
forms that parse to the same address are duplicates. A dual-stack host that
must appear on both families is two rows; when only one identity is needed,
prefer the IPv4 address. Mixed-family subnet placement does not match.
prefer the IPv4 address. `Import-NmapScan` applies the same rule: IPv4 when
both `addrtype` families are present on one nmap host, otherwise IPv6. Mixed-family subnet placement does not match.
Unknown or omitted roles remain valid and are placed in the Access layer.

See `examples/inventory-template.json` for a complete template.
Expand Down Expand Up @@ -306,7 +307,7 @@ After importing the module, you have access to:
| `Export-Topology` / `Import-Topology` | Persist topology to JSON |
| `Export-NodeInventoryCsv` | Export inventory CSV |
| `Export-Metadata` | Save scan statistics |
| `Import-NmapScan` | Import nmap XML |
| `Import-NmapScan` | Import nmap XML (IPv4 preferred; IPv6-only hosts imported) |
| `Invoke-NetworkDiscovery` | Scan a CIDR without repo clone |
| `Compare-NetworkScans` | Diff two topology snapshots |
| `Invoke-SnmpWalk` | Direct SNMP queries |
Expand Down
27 changes: 27 additions & 0 deletions tests/NetDiagram.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,33 @@ Describe 'Import-NmapScan (#36 regression)' {
{ Import-NmapScan -Path $bad } | Should -Throw "*$bad*"
{ Import-NmapScan -Path $bad } | Should -Throw "*nmaprun*"
}

It 'Imports an IPv6-only up host and prefers IPv4 on dual-stack hosts (#63)' {
$fixture = Join-Path $PSScriptRoot 'fixtures' 'nmap-sample-ipv6.xml'
$topo = Import-NmapScan -Path $fixture
$topo.Nodes.Count | Should -Be 2

$v6Only = $topo.Nodes | Where-Object IP -eq '2001:db8::10'
$v6Only | Should -Not -BeNullOrEmpty
$v6Only.Hostname | Should -Be 'v6only.example.com'
$v6Only.Vendor | Should -Be 'V6Vendor'
$v6Only.OpenPorts | Should -Contain '22'

$dual = $topo.Nodes | Where-Object IP -eq '192.168.1.30'
$dual | Should -Not -BeNullOrEmpty
$dual.Hostname | Should -Be 'dual.example.com'
($topo.Nodes | Where-Object IP -eq '2001:db8::30') | Should -BeNullOrEmpty

($topo.Nodes | Where-Object IP -eq '2001:db8::99') | Should -BeNullOrEmpty
($topo.Nodes | Where-Object IP -eq 'not-an-ip') | Should -BeNullOrEmpty
}

It 'Help DESCRIPTION mentions IPv4 and IPv6 address elements' {
$help = Get-Help Import-NmapScan
$text = @($help.Description | ForEach-Object { $_.Text }) -join ' '
$text | Should -Match 'IPv4'
$text | Should -Match 'IPv6'
}
}

Describe 'Topology persistence (#34 regression)' {
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/nmap-sample-ipv6.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<nmaprun scanner="nmap" args="nmap -6 -oX scan.xml 2001:db8::/64">
<host><status state="up"/><address addr="2001:db8::10" addrtype="ipv6"/><address addr="AA:BB:CC:DD:EE:01" addrtype="mac" vendor="V6Vendor"/><hostnames><hostname name="v6only.example.com" type="PTR"/></hostnames><ports><port protocol="tcp" portid="22"><state state="open"/></port></ports></host>
<host><status state="up"/><address addr="192.168.1.30" addrtype="ipv4"/><address addr="2001:db8::30" addrtype="ipv6"/><hostnames><hostname name="dual.example.com" type="PTR"/></hostnames></host>
<host><status state="down"/><address addr="2001:db8::99" addrtype="ipv6"/><hostnames><hostname name="down-v6.example.com"/></hostnames></host>
<host><status state="up"/><address addr="not-an-ip" addrtype="ipv6"/></host>
</nmaprun>