Skip to content

Commit 43e49a4

Browse files
JustinGroteJustinGrote
authored andcommitted
Feature: Add Powershell Real Time Pipeline Support (#7)
Fixes #6 Also refactored output processing to be cleaner
2 parents 742032a + 4ec7dd3 commit 43e49a4

11 files changed

+206
-141
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using namespace Newtonsoft.Json
2+
function ConvertFromXML {
3+
#TODO: This requires newtonsoft.json. Present in 6.1 but add a check for 5.1
4+
<#
5+
.SYNOPSIS
6+
Uses the newtonsoft.json library to convert XML into an intermediate object. Supports PSObject (Default) and JSON
7+
#>
8+
[CmdletBinding()]
9+
param (
10+
#The XML to convert to an object
11+
[Parameter(ValueFromPipeline)][XML.XMLElement]$Xml,
12+
#Output as JSON instead of just PSObject
13+
[Switch]$AsJSON,
14+
#Use the raw element processings that newtonsoft.json uses. Only need this if you have conflicting names between your attributes and elements
15+
[Switch]$Raw
16+
)
17+
18+
$json = [JsonConvert]::SerializeXmlNode($Xml,'Indented')
19+
20+
if (-not $Raw) {
21+
[Regex]$MatchConvertedAmpersand = '(?m)(?<=\s+\")(@)(?=.+\"\:)'
22+
$convertedJson = $json -replace $MatchConvertedAmpersand,''
23+
}
24+
25+
26+
if ($AsJson) {return $convertedJson}
27+
28+
return ConvertFrom-Json $convertedJson
29+
30+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
3+
function FormatNmapOutput {
4+
<#
5+
.SYNOPSIS
6+
Takes the raw formatting from ConvertFrom-NmapXML and makes a useful Powershell Object out of the output. Meant to be called from ConvertFrom-NmapXml
7+
.INPUTS
8+
[PSCustomObject]
9+
.OUTPUTS
10+
[PoshNmapResult]
11+
.NOTES
12+
The raw formatting is still available as the nmaprun property on the object, to maintain compatibility
13+
#>
14+
15+
[CmdletBinding()]
16+
param (
17+
#Nmaprun output from ConvertFrom-NmapXml. Should basically be XML -> Json -> PSObject output
18+
[Parameter(ValueFromPipeline)][PSCustomObject]$InputNmapObject,
19+
#Return a summary of the scan rather than individual hosts
20+
[Switch]$Summary
21+
)
22+
23+
if (-not $inputNmapObject.nmaprun) {throwUser "This is not a valid Object output from Convert-NmapXML"}
24+
$nmaprun = $inputNmapObject.nmaprun
25+
26+
#Only return a summary if that was requested
27+
if ($summary) {return (FormatNmapOutputSummary $nmapRun)}
28+
29+
#Generate nicer host entries
30+
$i=1
31+
$itotal = $nmaprun.host | measure | % count
32+
foreach ($hostnode in $nmaprun.host) {
33+
write-progress -Activity "Parsing NMAP Result" -Status "Processing Scan Entries" -CurrentOperation "Processing $i of $itotal" -PercentComplete (($i/$itotal)*100)
34+
FormatPoshNmapHost $hostnode
35+
}
36+
}

PoshNmap/Private/FormatNmapXmlSummary.ps1 renamed to PoshNmap/Private/FormatNmapOutputSummary.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
function FormatNmapXmlSummary ($nmapRun) {
2+
function FormatNmapOutputSummary ($nmapRun) {
33

44
#Parse the scanned services list
55
foreach ($scanInfoItem in $nmaprun.scaninfo) {
Lines changed: 11 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,16 @@
1+
using namespace Management.Automation
12
Update-TypeData -TypeName PoshNmapHost -DefaultDisplayPropertySet IPv4,FQDN,Status,OpenPorts -Force
2-
3-
function FormatNmapXml {
4-
<#
5-
.SYNOPSIS
6-
Takes the raw formatting from ConvertFrom-NmapXML and makes a useful Powershell Object out of the output. Meant to be called from ConvertFrom-NmapXml
7-
.INPUTS
8-
[Hashtable]
9-
.OUTPUTS
10-
[PoshNmapResult]
11-
.NOTES
12-
The raw formatting is still available as the nmaprun property on the object, to maintain compatibility
13-
#>
14-
3+
function FormatPoshNmapHost {
154
[CmdletBinding()]
165
param (
17-
#Nmaprun output from ConvertFrom-NmapXml. We use hashtable because it's the easiest to manipulate quickly
18-
[Parameter(ValueFromPipeline)]$InputNmapXml,
19-
#Return a summary of the scan rather than individual hosts
20-
[Switch]$Summary
6+
[Parameter(ValueFromPipeline)][pscustomobject]$PoshNmapHost
217
)
228

23-
if (-not $inputNmapXml.nmaprun) {throwUser "This is not a valid Hashtable output from Convert-NmapXML"}
24-
25-
$nmaprun = $inputNmapXml.nmaprun
26-
27-
#Only return a summary if that was requested
28-
if ($summary) {return (FormatNmapXmlSummary $nmapRun)}
9+
process {
10+
$hostnode = $PoshNmapHost
2911

30-
#Generate nicer host entries
31-
$i=1
32-
$itotal = $nmaprun.host | measure | % count
33-
foreach ($hostnode in $nmaprun.host) {
34-
write-progress -Activity "Parsing NMAP Result" -Status "Processing Scan Entries" -CurrentOperation "Processing $i of $itotal" -PercentComplete (($i/$itotal)*100)
12+
#Deep copy the nmap result so when we output it as the nmapresult property, it is "unchanged"
13+
$nmapResult = [PSSerializer]::Serialize($hostnode, [int32]::MaxValue)
3514

3615
# Init variables, with $entry being the custom object for each <host>.
3716
$service = $null
@@ -46,7 +25,7 @@ The raw formatting is still available as the nmaprun property on the object, to
4625
MAC = $null
4726
#Arraylist used for performance as this can get large quickly
4827
Ports = New-Object Collections.ArrayList
49-
OpenPorts = $hostnode.ports | measure | % count
28+
OpenPorts = $hostnode.ports.port | measure | % count
5029
}
5130
$entry.FQDN = $entry.FQDNs | select -first 1
5231
$entry.Hostname = $entry.FQDN -replace '^(\w+)\..*$','$1'
@@ -72,12 +51,12 @@ The raw formatting is still available as the nmaprun property on the object, to
7251
}
7352
$portResult | FormatStringOut -scriptblock {$this.protocol,$this.port -join ':'}
7453
$portResult.State | FormatStringOut -scriptblock {$this.state}
75-
$portResult.Services | FormatStringOut -scriptblock {($this.name,$this.product -join ':') + " ($($this.conf * 10)%)"}
54+
$portResult.Services | FormatStringOut -scriptblock {($this.name,$this.product -join ':') + " ($([int]($this.conf) * 10)%)"}
7655

7756
#TODO: Refactor this now that I'm better at Powershell :)
7857
# Build Services property. What a mess...but exclude non-open/non-open|filtered ports and blank service info, and exclude servicefp too for the sake of tidiness.
7958
if ($_.state.state -like "open*" -and ($_.service.tunnel.length -gt 2 -or $_.service.product.length -gt 2 -or $_.service.proto.length -gt 2)) {
80-
59+
$OutputDelimiter = ', '
8160
$entry.Services += ($_.protocol,$_.portid,$service -join ':')+
8261
':'+
8362
($_.service.product,$_.service.version,$_.service.tunnel,$_.service.proto,$_.service.rpcnum -join " ").Trim() +
@@ -107,7 +86,6 @@ The raw formatting is still available as the nmaprun property on the object, to
10786

10887
$portResult.scriptResult[$scriptItem.id] = [pscustomobject]$scriptResultEntry
10988
}
110-
11189
$entry.Ports.Add($portResult) > $null
11290
}
11391

@@ -119,16 +97,13 @@ The raw formatting is still available as the nmaprun property on the object, to
11997
$entry.OSGuesses = $hostnode.os.osmatch
12098
if (@($entry.OSGuesses).count -lt 1) { $entry.OS = $null }
12199

122-
123100
if ($hostnode.hostscript -ne $null) {
124101
$hostnode.hostscript.script | foreach-object {
125102
$entry.Script += '<HostScript id="' + $_.id + '">' + $OutputDelimiter + ($_.output.replace("`n","$OutputDelimiter")) + "$OutputDelimiter</HostScript> $OutputDelimiter $OutputDelimiter"
126103
}
127104
}
128-
$i++ #Progress counter...
129105

130-
#Add raw host reference
131-
$entry.nmapResult = $hostnode
106+
$entry.NmapResult = [PSSerializer]::Deserialize($nmapResult)
132107

133108
[PSCustomObject]$entry
134109
}

PoshNmap/Private/InvokeNmapExe.ps1

Lines changed: 0 additions & 35 deletions
This file was deleted.

PoshNmap/Private/StartNmap.ps1

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
function InvokeNmapExe ($argumentList,$computerName) {
2+
<#
3+
.SYNOPSIS
4+
Starts Nmap with supplied argument list and computername list
5+
.NOTES
6+
This function primarily exists to be mocked by Pester
7+
#>
8+
$nmapexe = (Get-Command nmap).path
9+
& "$nmapexe" $argumentList $computerName
10+
}
11+
12+
function StartNmap ($argumentList,$computerName,[int]$updateInterval=200,[Switch]$Raw) {
13+
[Collections.Arraylist]$nmapExeOutput = New-Object Collections.ArrayList
14+
[Collections.Arraylist]$hostEntry = New-Object Collections.ArrayList
15+
if (-not $Raw) {
16+
$ArgumentList += "-oX","-",'--stats-every',"${updateInterval}ms"
17+
}
18+
19+
write-verbose "Invoking nmap $argumentList $computerName"
20+
InvokeNmapExe $argumentList $computerName | Foreach-Object {
21+
if ($Raw) {$PSItem} else {
22+
#Strip taskprogress items and report them as progress instead
23+
if ($PSItem -match '^<taskprogress') {
24+
$taskprogress = ([xml]$PSItem).taskprogress
25+
#write-debug "Task $($taskProgress.task) is $([int]($taskProgress.percent))% complete with $($taskProgress.remaining) items left"
26+
27+
$ETA = [TimeSpan]::FromSeconds($taskProgress.etc - $taskProgress.time)
28+
$WriteProgressParams = @{
29+
Id=10
30+
Activity = "Invoke-Nmap Scan of $($computername -join ',')"
31+
Status = "$($taskProgress.task)"
32+
CurrentOperation = "$($taskProgress.remaining) items remaining. ETA $ETA"
33+
PercentComplete = $taskProgress.percent
34+
}
35+
Write-Progress @WriteProgressParams
36+
} else {
37+
$PSItem
38+
}
39+
}
40+
}
41+
42+
write-progress -id 10 -Activity 'Invoke-Nmap Scan' -Completed
43+
}
44+

PoshNmap/Public/ConvertFrom-NmapXML.ps1

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,68 +4,89 @@ function ConvertFrom-NmapXml {
44
<#
55
.SYNOPSIS
66
Converts NmapXML into various formats. Currently supported are JSON, PSObject, NmapReport
7+
.NOTES
8+
Only supports nmap reports piped from nmap directly. In the future will support existing full nmap reports
79
.EXAMPLE
810
nmap localhost -oX - | ConvertFrom-NmapXML -OutFormat JSON
911
Takes an NMAP run output and converts it into JSON
10-
1112
#>
12-
[CmdletBinding(DefaultParameterSetName="String")]
13+
[CmdletBinding()]
1314
param (
14-
#The NMAPXML content
15-
[Parameter(ParameterSetName='String',ValueFromPipeline)][String[]]$InputString,
16-
[Parameter(ParameterSetName='XML',ValueFromPipeline)][XML[]]$InputObject,
15+
#Reads XML "Strings" one by one
16+
[Parameter(ValueFromPipeline)][String[]]$InputObject,
1717

1818
#Choose the format that you want to convert the NMAPXML to. Valid options are: JSON or HashTable
1919
[ValidateSet('JSON','HashTable','PSObject','PoshNmap','Summary')]
20-
$OutFormat = 'JSON'
20+
$OutFormat = 'PoshNmap'
2121
)
2222

23-
process {
24-
#If strings were passed via pipeline, assume it is output from nmap XML which is multiple lines and coalesce them into one large document.
25-
$InputObjectBundle += $InputString
23+
begin {
24+
$xmlDocument = [Collections.ArrayList]@()
25+
$hostEntry = [Collections.ArrayList]@()
2626
}
2727

28-
end {
29-
try {
30-
[XML]$CombinedDocument = $InputObjectBundle
31-
} catch [InvalidCastException] {
32-
$exception = [System.Management.Automation.PSInvalidCastException]::New("The input provided is not valid XML. If you are piping from nmap, did you use 'nmap -oX -'?")
33-
throwUser $exception
34-
}
28+
process {
29+
#Unwrap $InputObject if it was passed as an array
30+
foreach ($nmapLineItem in $inputObject) {
31+
#If the output format is not PoshNmap, we will coalesce into a single document and process at the end, otherwise we will do it in real time for the pipeline
32+
if ($OutFormat -ne 'PoshNmap') {
33+
$xmlDocument += $nmapLineItem
34+
#If this is a host entry, start capturing a host buffer
35+
} elseif ($nmapLineItem -match '^<host') {
36+
$hostEntry += $nmapLineItem
37+
} elseif ($hostentry.count -ge 1) {
38+
if ($nmapLineItem -match '^</host>$') {
39+
$hostEntry += $nmapLineItem
40+
try {
41+
(ConvertFromXml ([xml]$hostEntry).host).host | FormatPoshNmapHost
42+
} finally {
43+
$hostEntry = [Collections.ArrayList]@()
44+
}
45+
} else {
46+
$hostEntry += $nmapLineItem
47+
}
48+
}
3549

36-
if ($CombinedDocument) {$inputObject = $CombinedDocument}
37-
$jsonResult = foreach ($nmapXmlItem in $InputObject) {
50+
#If we are making a host entry, keep adding lines until we hit a </host> entry and then process it
3851

39-
#Selecting NmapRun required for PS5.1 compatibility due to Newtonsoft.Json bug
40-
$nmapRunItem = ($nmapXmlItem).SelectSingleNode('nmaprun')
52+
}
53+
}
4154

42-
#Indented JSON is important as we will use a regex to clean up the @ elements
43-
$convertedJson = [JsonConvert]::SerializeXmlNode($nmapRunItem,'Indented')
55+
end {
56+
#If we don't have any post-processing, don't worry about it
57+
if (-not $xmlDocument) {continue}
4458

45-
#Remove @ symbols from xml attributes. There are no element/attribute collisions in the nmap xml (that we know of) so this should be OK.
46-
[Regex]$MatchConvertedAmpersand = '(?m)(?<=\s+\")(@)(?=.+\"\:)'
47-
$convertedJson = $convertedJson -replace $MatchConvertedAmpersand,''
48-
$convertedJson
59+
if ($xmlDocument -isnot [xml]) {
60+
try {
61+
$xmlDocument = [XML]$xmlDocument
62+
} catch [InvalidCastException] {
63+
$exception = [System.Management.Automation.PSInvalidCastException]::New("The input provided is not valid XML. If you are piping from nmap, did you use 'nmap -oX -'?")
64+
throwUser $exception
65+
}
66+
}
67+
68+
if (-not $xmlDocument.nmaprun) {
69+
throwUser "The provided document is not a valid NMAP XML document (doesn't have an nmaprun element)"
4970
}
5071

72+
$nmapRun = $xmlDocument.selectSingleNode('nmaprun')
73+
5174
switch ($OutFormat) {
5275
'JSON' {
53-
return $jsonResult
76+
ConvertFromXml $nmapRun -AsJSON
5477
}
5578
'PSObject' {
56-
return $jsonResult | ConvertFrom-Json
79+
(ConvertFromXml $nmapRun).nmaprun
5780
}
58-
'HashTable' {
59-
#TODO: PSCore Method, add as potential feature flag but for now use same method for both to avoid incompatibilities
60-
#$jsonResult | ConvertFrom-Json -AsHashtable
61-
return $jsonResult | ConvertFrom-Json | ConvertPSObjectToHashtable
81+
'Summary' {
82+
FormatNmapOutputSummary -nmaprun (ConvertFromXml $nmapRun).nmaprun
6283
}
63-
'PoshNmap' {
64-
return $jsonResult | ConvertFrom-Json | FormatNmapXml
84+
'HashTable' {
85+
(ConvertFromXml $nmapRun).nmaprun | ConvertPSObjectToHashtable
6586
}
66-
'Summary' {
67-
return $jsonResult | ConvertFrom-Json | FormatNmapXml -Summary
87+
Default {
88+
throwUser "Outformat $Outformat is not valid. This should not happen, file as an issue if you see this"
6889
}
6990
}
7091
}
71-
}
92+
}

0 commit comments

Comments
 (0)