From b0969317883324c69b50f6e3779939540de23a0c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 17 Sep 2026 18:51:27 +0000 Subject: [PATCH] fix: use LiteralPath for remaining exporter Out-File writes Extend bracket-path safety to Export-Metadata, Export-Topology, Export-Mermaid, Export-NodeInventoryCsv, and Export-NetBox. Add Pester coverage for bracketed output paths and SNMP credential maps. Co-authored-by: Jacobrakai --- CHANGELOG.md | 4 + NetDiagram-PS/Public/Export-Mermaid.ps1 | 2 +- NetDiagram-PS/Public/Export-Metadata.ps1 | 2 +- NetDiagram-PS/Public/Export-NetBox.ps1 | 2 +- .../Public/Export-NodeInventoryCsv.ps1 | 2 +- NetDiagram-PS/Public/Export-Topology.ps1 | 2 +- tests/NetDiagram.Tests.ps1 | 75 +++++++++++++++++++ 7 files changed, 84 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1620681..1086e9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 SNMP credential-map readers, `Compare-NetworkScans` metadata inputs, and `Export-DrawIO` now use `-LiteralPath` so paths containing `[]` or other wildcard metacharacters work on all platforms. +- **Literal path handling for remaining exporters.** `Export-Metadata`, + `Export-Topology`, `Export-Mermaid`, `Export-NodeInventoryCsv`, and + `Export-NetBox` now write with `Out-File -LiteralPath` (CSV row export + already used `-LiteralPath`). ## [1.4.0] - 2026-09-09 diff --git a/NetDiagram-PS/Public/Export-Mermaid.ps1 b/NetDiagram-PS/Public/Export-Mermaid.ps1 index efaa734..444ab21 100644 --- a/NetDiagram-PS/Public/Export-Mermaid.ps1 +++ b/NetDiagram-PS/Public/Export-Mermaid.ps1 @@ -93,7 +93,7 @@ function Export-Mermaid { } try { - $sb.ToString() | Out-File -FilePath $OutFile -Encoding utf8 -Force + $sb.ToString() | Out-File -LiteralPath $OutFile -Encoding utf8 -Force Write-Verbose "Exported Mermaid to $OutFile" } catch { diff --git a/NetDiagram-PS/Public/Export-Metadata.ps1 b/NetDiagram-PS/Public/Export-Metadata.ps1 index b6ac365..9dda9d4 100644 --- a/NetDiagram-PS/Public/Export-Metadata.ps1 +++ b/NetDiagram-PS/Public/Export-Metadata.ps1 @@ -69,7 +69,7 @@ function Export-Metadata { } try { - $metadata | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutFile -Encoding utf8 -Force + $metadata | ConvertTo-Json -Depth 10 | Out-File -LiteralPath $OutFile -Encoding utf8 -Force Write-Verbose "Exported metadata to $OutFile" } catch { diff --git a/NetDiagram-PS/Public/Export-NetBox.ps1 b/NetDiagram-PS/Public/Export-NetBox.ps1 index e8a6e7e..42e63a6 100644 --- a/NetDiagram-PS/Public/Export-NetBox.ps1 +++ b/NetDiagram-PS/Public/Export-NetBox.ps1 @@ -84,7 +84,7 @@ function Export-NetBox { } if ($null -eq $rows -or @($rows).Count -eq 0) { - 'name,role,manufacturer,device_type,site,status,primary_ip4,primary_ip6' | Out-File -FilePath $OutFile -Encoding utf8 -Force + 'name,role,manufacturer,device_type,site,status,primary_ip4,primary_ip6' | Out-File -LiteralPath $OutFile -Encoding utf8 -Force } else { $rows | Export-Csv -LiteralPath $OutFile -NoTypeInformation -Encoding utf8 -Force diff --git a/NetDiagram-PS/Public/Export-NodeInventoryCsv.ps1 b/NetDiagram-PS/Public/Export-NodeInventoryCsv.ps1 index 4163f94..3e4caa5 100644 --- a/NetDiagram-PS/Public/Export-NodeInventoryCsv.ps1 +++ b/NetDiagram-PS/Public/Export-NodeInventoryCsv.ps1 @@ -54,7 +54,7 @@ function Export-NodeInventoryCsv { } # Ensure header-only file for empty topology (Export-Csv would create 0-byte file) if ($null -eq $rows -or @($rows).Count -eq 0) { - 'IP,Hostname,Role,Layer,Vendor,OS,Reachable,MACAddress,OpenPorts' | Out-File -FilePath $OutFile -Encoding utf8 -Force + 'IP,Hostname,Role,Layer,Vendor,OS,Reachable,MACAddress,OpenPorts' | Out-File -LiteralPath $OutFile -Encoding utf8 -Force } else { $rows | Export-Csv -LiteralPath $OutFile -NoTypeInformation -Encoding utf8 -Force diff --git a/NetDiagram-PS/Public/Export-Topology.ps1 b/NetDiagram-PS/Public/Export-Topology.ps1 index f8c0d7b..d07bde8 100644 --- a/NetDiagram-PS/Public/Export-Topology.ps1 +++ b/NetDiagram-PS/Public/Export-Topology.ps1 @@ -38,7 +38,7 @@ function Export-Topology { return } try { - $Topology | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutFile -Encoding utf8 -Force + $Topology | ConvertTo-Json -Depth 10 | Out-File -LiteralPath $OutFile -Encoding utf8 -Force Write-Verbose "Exported topology to $OutFile" } catch { diff --git a/tests/NetDiagram.Tests.ps1 b/tests/NetDiagram.Tests.ps1 index a51a150..12c24a5 100644 --- a/tests/NetDiagram.Tests.ps1 +++ b/tests/NetDiagram.Tests.ps1 @@ -557,6 +557,14 @@ Describe 'Export-Metadata' { $metadata = Get-Content -Path $metaPath -Raw | ConvertFrom-Json $metadata.credSetsUsed | Should -HaveCount 0 } + + It 'Writes metadata to paths that contain wildcard metacharacters literally' { + $topology = Import-Inventory -Path $script:TestInventoryPath + $metaPath = Join-Path $TestDrive 'scan-meta[1].json' + $topology | Export-Metadata -OutFile $metaPath -Force + Test-Path -LiteralPath $metaPath | Should -Be $true + (Get-Content -LiteralPath $metaPath -Raw | ConvertFrom-Json).nodeCount | Should -Be $topology.Nodes.Count + } } Describe 'Compare-NetworkScans' { @@ -603,6 +611,26 @@ Describe 'Compare-NetworkScans' { $report | Should -Match 'Added Nodes' $report | Should -Match '192.168.1.30' } + + It 'Compares scans when metadata, topology, and report paths contain bracket metacharacters' { + $baseline = Import-Inventory -Path $script:TestInventoryPath + $baselineMeta = Join-Path $TestDrive 'baseline[1]-meta.json' + $baselineTopo = Join-Path $TestDrive 'baseline[1]-topo.json' + $baseline | Export-Metadata -OutFile $baselineMeta -Force + $baseline | Export-Topology -OutFile $baselineTopo -Force + + $current = Import-Inventory -Path $script:TestInventoryPath + $currentMeta = Join-Path $TestDrive 'current[1]-meta.json' + $currentTopo = Join-Path $TestDrive 'current[1]-topo.json' + $current | Export-Metadata -OutFile $currentMeta -Force + $current | Export-Topology -OutFile $currentTopo -Force + + $reportPath = Join-Path $TestDrive 'compare[1].md' + { Compare-NetworkScans -BaselineMetadata $baselineMeta -BaselineTopology $baselineTopo ` + -CurrentMetadata $currentMeta -CurrentTopology $currentTopo ` + -OutFile $reportPath } | Should -Not -Throw + Test-Path -LiteralPath $reportPath | Should -Be $true + } } Describe 'Round-trip Test' { @@ -732,6 +760,14 @@ Describe 'Topology persistence (#34 regression)' { $topo | Export-Topology -OutFile $whatIfPath -WhatIf Test-Path $whatIfPath | Should -Be $false } + + It 'Round-trips topology files whose paths contain bracket metacharacters' { + $topo = Import-Inventory -Path $script:TestInventoryPath + $path = Join-Path $TestDrive 'topology[export].json' + $topo | Export-Topology -OutFile $path -Force + $reloaded = Import-Topology -Path $path + $reloaded.Nodes.Count | Should -Be $topo.Nodes.Count + } } Describe 'Export-NodeInventoryCsv (#41 regression)' { @@ -780,6 +816,14 @@ Describe 'Export-NodeInventoryCsv (#41 regression)' { $lines[0] | Should -Be 'IP,Hostname,Role,Layer,Vendor,OS,Reachable,MACAddress,OpenPorts' } + It 'Writes header-only CSV to bracketed paths literally' { + $topo = [pscustomobject]@{ Nodes=@(); Edges=@(); Subnets=@() } + $path = Join-Path $TestDrive 'nodes[empty].csv' + $topo | Export-NodeInventoryCsv -OutFile $path -Force + Test-Path -LiteralPath $path | Should -Be $true + (Get-Content -LiteralPath $path).Count | Should -Be 1 + } + It 'Joins OpenPorts with semicolon and respects -Force/-WhatIf' { $topo = [pscustomobject]@{ Nodes = @([pscustomobject]@{ IP='10.0.0.9'; Hostname='n'; Role='server'; Layer='Servers'; Vendor='X'; OS='X'; Reachable=$true; OpenPorts=@(80,443) }) @@ -1366,6 +1410,17 @@ Describe 'Get-SnmpNeighbors node eligibility' { Should -Invoke Invoke-SnmpWalk -ModuleName 'NetDiagram-PS' -Times 3 -Exactly } + It 'Reads credential map paths that contain wildcard metacharacters literally' { + $credPath = Join-Path $TestDrive 'credmap[lab].json' + '{}' | Set-Content -LiteralPath $credPath + $topology = Import-Inventory -Path $script:TestInventoryPath + Mock Invoke-SnmpWalk { @() } -ModuleName 'NetDiagram-PS' + + $null = $topology | Get-SnmpNeighbors -CredentialMapPath $credPath -TryPublic -WarningAction SilentlyContinue + + Should -Invoke Invoke-SnmpWalk -ModuleName 'NetDiagram-PS' -Times 3 -Exactly + } + It 'Skips untested nodes when -OnlyReachable is specified' { $topology = Import-Inventory -Path $script:TestInventoryPath Mock Invoke-SnmpWalk { @() } -ModuleName 'NetDiagram-PS' @@ -2192,6 +2247,18 @@ Describe 'Export-Mermaid (#37 regression)' { $content | Should -Not -Match '"bad' } + It 'Writes Mermaid to paths that contain wildcard metacharacters literally' { + $topo = [pscustomobject]@{ + Nodes = @([pscustomobject]@{ IP='10.0.0.1'; Hostname='gw'; Role='unknown'; Layer='Core'; Vendor='X'; OS='X'; Reachable=$null }) + Edges = @() + Subnets = @([pscustomobject]@{ CIDR='10.0.0.0/24'; Label='Net'; VLAN=1 }) + } + $path = Join-Path $TestDrive 'diagram[1].mmd' + $topo | Export-Mermaid -OutFile $path -Force + Test-Path -LiteralPath $path | Should -Be $true + (Get-Content -LiteralPath $path -Raw) | Should -Match '^flowchart LR' + } + It 'Respects -Force and -WhatIf' { $topo = Invoke-NetworkDiscovery -Cidr '10.0.1.0/24' -ScanDepth Quick $path = Join-Path $script:TestDataPath 'mermaid-force.mmd' @@ -2243,6 +2310,14 @@ Describe 'Export-NetBox (#38 regression)' { $topo | Export-NetBox -OutFile $whatIf -Site 'HQ' -WhatIf Test-Path $whatIf | Should -Be $false } + + It 'Writes header-only NetBox CSV to bracketed paths literally' { + $topo = [pscustomobject]@{ Nodes=@(); Edges=@(); Subnets=@() } + $path = Join-Path $TestDrive 'netbox[empty].csv' + $topo | Export-NetBox -OutFile $path -Site 'HQ' -Force + Test-Path -LiteralPath $path | Should -Be $true + (Get-Content -LiteralPath $path).Count | Should -Be 1 + } } Describe 'Export-NetBox (#72 IPv6 columns and unknown reachability)' {