Skip to content

Commit 8bd112c

Browse files
authored
Feature: Add additional port, service, snmp formatting
* Bugfix: PS5 doesn't process snmp communities * Feature: Add SNMP Preset * Feature: Extensive Rework of Output Everything is PSObjects now Added better default toString output for several properties Added port Scripts formatting * Test: Update PoshNmap Output Test * Feature: Clean up entries * Feature: Add additional port, service, snmp formatting
1 parent 00fc76f commit 8bd112c

File tree

7 files changed

+144
-64
lines changed

7 files changed

+144
-64
lines changed

PoshNmap/Private/FormatNmapXml.ps1

Lines changed: 67 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The raw formatting is still available as the nmaprun property on the object, to
1515
[CmdletBinding()]
1616
param (
1717
#Nmaprun output from ConvertFrom-NmapXml. We use hashtable because it's the easiest to manipulate quickly
18-
[Parameter(ValueFromPipeline)][Hashtable]$InputNmapXml,
18+
[Parameter(ValueFromPipeline)]$InputNmapXml,
1919
#Return a summary of the scan rather than individual hosts
2020
[Switch]$Summary
2121
)
@@ -34,59 +34,82 @@ The raw formatting is still available as the nmaprun property on the object, to
3434
write-progress -Activity "Parsing NMAP Result" -Status "Processing Scan Entries" -CurrentOperation "Processing $i of $itotal" -PercentComplete (($i/$itotal)*100)
3535

3636
# Init variables, with $entry being the custom object for each <host>.
37-
$service = " " #service needs to be a single space.
37+
$service = $null
3838
$entry = [ordered]@{
3939
PSTypeName = 'PoshNmapHost'
40+
Hostname = $null
41+
Status = ($hostnode.status.state.Trim() | where length -ge 2)
42+
FQDNs = $hostnode.hostnames.hostname.name | select -Unique
43+
FDQN = $null
44+
IPv4 = $null
45+
IPv6 = $null
46+
MAC = $null
47+
#Arraylist used for performance as this can get large quickly
48+
Ports = New-Object Collections.ArrayList
49+
OpenPorts = $hostnode.ports | measure | % count
4050
}
41-
42-
# Extract state element of status
43-
$entry.Status = $hostnode.status.state.Trim()
44-
if ($entry.Status.length -lt 2) { $entry.Status = $null }
45-
46-
# Extract fully-qualified domain name(s), removing any duplicates.
47-
$entry.FQDNs = $hostnode.hostnames.hostname.name | select -Unique
4851
$entry.FQDN = $entry.FQDNs | select -first 1
52+
$entry.Hostname = $entry.FQDN -replace '^(\w+)\..*$','$1'
53+
FormatStringOut -InputObject $entry.Ports {$this.ports | measure | % count}
54+
55+
# Process each of the supplied address properties, extracting by type.
56+
foreach ($addressItem in $hostnode.address) {
57+
switch ($addressItem.addrtype) {
58+
"ipv4" { $entry.IPv4 += $addressItem.addr}
59+
"ipv6" { $entry.IPv6 += $addressItem.addr}
60+
"mac" { $entry.MAC += $addressItem.addr}
61+
}
62+
}
4963

50-
# Note that this code cheats, it only gets the hostname of the first FQDN if there are multiple FQDNs.
51-
if ($entry.FQDN -eq $null) { $entry.HostName = $null }
52-
elseif ($entry.FQDN -like "*.*") { $entry.HostName = $entry.FQDN.Substring(0,$entry.FQDN.IndexOf(".")) }
53-
else { $entry.HostName = $entry.FQDN }
64+
$hostnode.ports.port | foreach-object {
65+
$portResult = [pscustomobject][ordered]@{
66+
PSTypeName="PoshNmapPort"
67+
Protocol=$_.protocol
68+
Port=$_.portid
69+
Services=$_.service
70+
State=$_.state
71+
ScriptResult = @{}
72+
}
73+
$portResult | FormatStringOut -scriptblock {$this.protocol,$this.port -join ':'}
74+
$portResult.state | FormatStringOut -scriptblock {$this.state}
75+
$portResult.Services | FormatStringOut -scriptblock {($this.name,$this.product -join ':') + " ($($this.conf * 10)%)"}
76+
77+
#TODO: Refactor this now that I'm better at Powershell :)
78+
# 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.
79+
if ($_.state.state -like "open*" -and ($_.service.tunnel.length -gt 2 -or $_.service.product.length -gt 2 -or $_.service.proto.length -gt 2)) {
80+
81+
$entry.Services += ($_.protocol,$_.portid,$service -join ':')+
82+
':'+
83+
($_.service.product,$_.service.version,$_.service.tunnel,$_.service.proto,$_.service.rpcnum -join " ").Trim() +
84+
" <" +
85+
([Int] $_.service.conf * 10) + "%-confidence>$OutputDelimiter"
86+
}
5487

55-
# Process each of the <address> nodes, extracting by type.
56-
$hostnode.address | foreach-object {
57-
if ($_.addrtype -eq "ipv4") { $entry.IPv4 += $_.addr + " "}
58-
if ($_.addrtype -eq "ipv6") { $entry.IPv6 += $_.addr + " "}
59-
if ($_.addrtype -eq "mac") { $entry.MAC += $_.addr + " "}
60-
}
61-
if ($entry.IPv4 -eq $null) { $entry.IPv4 = $null } else { $entry.IPv4 = $entry.IPv4.Trim()}
62-
if ($entry.IPv6 -eq $null) { $entry.IPv6 = $null } else { $entry.IPv6 = $entry.IPv6.Trim()}
63-
if ($entry.MAC -eq $null) { $entry.MAC = $null } else { $entry.MAC = $entry.MAC.Trim()}
64-
65-
66-
# Process all ports from <ports><port>, and note that <port> does not contain an array if it only has one item in it.
67-
if ($hostnode.ports.port -eq $null) { $entry.Ports = $null ; $entry.Services = $null }
68-
else
69-
{
70-
$entry.Ports = @()
71-
72-
$hostnode.ports.port | foreach-object {
73-
if ($_.service.name -eq $null) { $service = "unknown" } else { $service = $_.service.name }
74-
$entry.Ports += [ordered]@{
75-
Protocol=$_.protocol
76-
Port=$_.portid
77-
Service=$service
78-
State=$_.state.state
88+
#Port Script Result Processing
89+
foreach ($scriptItem in $_.script) {
90+
$scriptResultEntry = [ordered]@{
91+
PSTypeName = 'PoshNmapScriptResult'
92+
id = $ScriptItem.id
93+
output = $ScriptItem.output
94+
table = [Collections.Arraylist]@()
7995
}
8096

81-
# 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.
82-
if ($_.state.state -like "open*" -and ($_.service.tunnel.length -gt 2 -or $_.service.product.length -gt 2 -or $_.service.proto.length -gt 2)) { $entry.Services += $_.protocol + ":" + $_.portid + ":" + $service + ":" + ($_.service.product + " " + $_.service.version + " " + $_.service.tunnel + " " + $_.service.proto + " " + $_.service.rpcnum).Trim() + " <" + ([Int] $_.service.conf * 10) + "%-confidence>$OutputDelimiter" }
97+
#Loop through the script elements and create a hashtable for them
98+
foreach ($tableitem in $scriptItem.table) {
99+
$scriptTable = @{
100+
PSTypeName = 'PoshNmapScriptTable'
101+
}
102+
foreach ($elemItem in $tableitem.elem) {
103+
$scriptTable[$elemItem.key] = $elemItem.'#text'
104+
}
105+
$scriptResultEntry.table += [PSCustomObject]$scriptTable
106+
}
107+
108+
$portResult.scriptResult[$scriptItem.id] = [pscustomobject]$scriptResultEntry
83109
}
84-
if ($entry.Services -eq $null) { $entry.Services = $null } else { $entry.Services = $entry.Services.Trim() }
85-
#Provide a nicer ToString Output
86-
$entry.Ports | Add-Member -MemberType ScriptMethod -Name ToString -Force -Value {$this.protocol,$this.port -join ':'}
87-
}
88110

89-
$entry.OpenPorts = $entry.ports.count
111+
$entry.Ports.Add($portResult) > $null
112+
}
90113

91114
# If there is 100% Accuracy OS, show it
92115
$CertainOS = $hostnode.os.osmatch | where {$_.accuracy -eq 100} | select -first 1
@@ -97,14 +120,6 @@ The raw formatting is still available as the nmaprun property on the object, to
97120
if (@($entry.OSGuesses).count -lt 1) { $entry.OS = $null }
98121

99122

100-
# Extract script output, first for port scripts, then for host scripts.
101-
$entry.Script = $null
102-
$hostnode.ports.port | foreach-object {
103-
if ($_.script -ne $null) {
104-
$entry.Script += "<PortScript id=""" + $_.script.id + """>$OutputDelimiter" + ($_.script.output -replace "`n","$OutputDelimiter") + "$OutputDelimiter</PortScript> $OutputDelimiter $OutputDelimiter"
105-
}
106-
}
107-
108123
if ($hostnode.hostscript -ne $null) {
109124
$hostnode.hostscript.script | foreach-object {
110125
$entry.Script += '<HostScript id="' + $_.id + '">' + $OutputDelimiter + ($_.output.replace("`n","$OutputDelimiter")) + "$OutputDelimiter</HostScript> $OutputDelimiter $OutputDelimiter"
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
function FormatStringOut () {
2+
<#
3+
.SYNOPSIS
4+
Change what is shown when the supplied object is cast to a string. Use $this to reference the supplied object
5+
.EXAMPLE
6+
[String](FormatStringOut (Get-Item .) {$this.name})
7+
#>
8+
[CmdletBinding()]
9+
param (
10+
[Parameter(Mandatory,ValueFromPipeline)]$inputObject,
11+
[Parameter(Mandatory,Position=0)][ScriptBlock]$scriptBlock
12+
)
13+
14+
process {
15+
$AddMemberParams = @{
16+
InputObject = $inputObject
17+
MemberType = 'ScriptMethod'
18+
Name = 'ToString'
19+
Force = $true
20+
Value = $scriptBlock
21+
}
22+
Add-Member @AddMemberParams
23+
}
24+
25+
}

PoshNmap/Public/ConvertFrom-NmapXML.ps1

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ Takes an NMAP run output and converts it into JSON
5656
return $jsonResult | ConvertFrom-Json
5757
}
5858
'HashTable' {
59-
#TODO: PSCore Method, add as potential feature flag
59+
#TODO: PSCore Method, add as potential feature flag but for now use same method for both to avoid incompatibilities
6060
#$jsonResult | ConvertFrom-Json -AsHashtable
6161
return $jsonResult | ConvertFrom-Json | ConvertPSObjectToHashtable
6262
}
6363
'PoshNmap' {
64-
return $jsonResult | ConvertFrom-Json | ConvertPSObjectToHashtable | FormatNmapXml
64+
return $jsonResult | ConvertFrom-Json | FormatNmapXml
6565
}
6666
'Summary' {
67-
return $jsonResult | ConvertFrom-Json | ConvertPSObjectToHashtable | FormatNmapXml -Summary
67+
return $jsonResult | ConvertFrom-Json | FormatNmapXml -Summary
6868
}
6969
}
7070
}

PoshNmap/Public/Get-NmapPresetArguments.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ function Get-NmapPresetArguments ($Preset) {
1010
Quick = '-T4 -F'
1111
QuickPlus = '-T4 --version-intensity 2 -sV -O -F'
1212
QuickTraceroute = '-T4 -sn -traceroute'
13+
Snmp = '-T4 -sU -p U:161'
1314
}
1415

1516
#The call operator wants this as an array

PoshNmap/Public/Invoke-Nmap.ps1

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,15 @@ function Invoke-Nmap {
5050
#Show all results, not just online hosts
5151
[Switch]$All,
5252

53-
#Perform an SNMP community scan
53+
#Perform an SNMP community scan. This is also done automatically with the "snmp" preset
5454
[Switch]$Snmp,
5555

5656
#A list of SNMP communities to scan. Defaults to public and private
5757
[String[]]
5858
[Parameter()]
59-
$snmpCommunityList = @("private","public")
59+
$snmpCommunityList = @("public","private")
6060
)
61+
6162
if ($ArgumentList) {$ArgumentList = $ArgumentList.split(' ')}
6263

6364
if ($Preset -and ($PSCmdlet.ParameterSetName -ne 'Custom')) {
@@ -69,9 +70,11 @@ function Invoke-Nmap {
6970
}
7071
}
7172

73+
if ($Preset -eq 'snmp') {$snmp = $true}
7274
if ($snmp) {
7375
$snmpCommunityFile = [io.path]::GetTempFileName()
74-
$snmpCommunityList > $snmpCommunityFile
76+
#Special file format required
77+
($snmpCommunityList -join "`n") + "`n" | Set-Content -NoNewLine -Encoding ASCII -Path $snmpCommunityFile -Force
7578
$argumentList += '--script','snmp-brute','--script-args',"snmp-brute.communitiesdb=$snmpCommunityFile"
7679
}
7780

@@ -92,7 +95,6 @@ function Invoke-Nmap {
9295
if ($snmp -and (Test-Path $snmpCommunityFile)) {Remove-Item $snmpCommunityFile -Force -ErrorAction SilentlyContinue}
9396
}
9497

95-
9698
if (-not $nmapResult) {throwUser "NMAP did not produce any output. Please review any errors that are present above this warning."}
9799
switch ($OutFormat) {
98100
'XML' {

Tests/Invoke-Nmap.tests.ps1

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,30 @@ import-module $moduleManifestFile
2828

2929
Describe "Invoke-Nmap" {
3030
$SCRIPT:nmapResult = "Test"
31-
$Mocks = (join-path $PSScriptRoot "Mocks")
31+
#TODO: Figure out a better way to do this than a global variable, maybe get-variable -scope "up one"
32+
$GLOBAL:NmapPesterTestMockDir = (join-path $PSScriptRoot "Mocks")
3233

3334
Mock -Modulename PoshNmap InvokeNmapExe {
34-
Get-Content -Raw "$Mocks\asusrouter.nmapxml"
35-
}.GetNewClosure() #GetNewClosure "Freezes" the mock. We use this to expand the variable present inside: https://stackoverflow.com/questions/49681015/access-external-variable-from-with-in-mock-script-block-pester
35+
Get-Content -Raw "$NmapPesterTestMockDir\asusrouter.nmapxml"
36+
}
37+
Mock -Modulename PoshNmap InvokeNmapExe -parameterFilter {$argumentlist -match 'snmp-brute'} {
38+
Get-Content -Raw "$NmapPesterTestMockDir\snmpresult.nmapxml"
39+
}
3640

3741
It "Output: PSCustomObject by default" {
3842
$SCRIPT:nmapResult = Invoke-Nmap
3943
$nmapResult | Should -BeOfType [PSCustomObject]
4044
}
4145

4246
It "Output: PoshNmap Output Data Sanity Check" {
43-
(Invoke-Nmap -OutFormat PSObject).nmaprun.host.ports.port | where portid -match '445' | % protocol | should -be 'tcp'
47+
(Invoke-Nmap).nmapresult.ports.port | where portid -match '445' | % protocol | should -be 'tcp'
4448
}
4549

4650
It "Output: XML Data Sanity Check" {
4751
(Invoke-Nmap -OutFormat PSObject).nmaprun.host.ports.port | where portid -match '445' | % protocol | should -be 'tcp'
4852
}
49-
}
53+
54+
It "Output: SNMP Table output is correct" {
55+
(Invoke-Nmap -snmp).ports.scriptresult.'snmp-brute'.table | where password -match 'public' | Should -Not -BeNullOrEmpty
56+
}
57+
}

Tests/Mocks/snmpresult.nmapxml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE nmaprun>
3+
<?xml-stylesheet href="file:///C:/Program Files (x86)/Nmap/nmap.xsl" type="text/xsl"?>
4+
<!-- Nmap 7.70 scan initiated Thu Mar 21 10:00:08 2019 as: &quot;C:\\Program Files (x86)\\Nmap\\nmap.exe&quot; -T4 -sU -p 161 -&#45;script snmp-brute -oX snmpresult.nmapxml 192.168.111.1 -->
5+
<nmaprun scanner="nmap" args="&quot;C:\\Program Files (x86)\\Nmap\\nmap.exe&quot; -T4 -sU -p 161 -&#45;script snmp-brute -oX snmpresult.nmapxml 192.168.111.1" start="1553187608" startstr="Thu Mar 21 10:00:08 2019" version="7.70" xmloutputversion="1.04">
6+
<scaninfo type="udp" protocol="udp" numservices="1" services="161"/>
7+
<verbose level="0"/>
8+
<debugging level="0"/>
9+
<host starttime="1553187608" endtime="1553187612"><status state="up" reason="arp-response" reason_ttl="0"/>
10+
<address addr="192.168.111.1" addrtype="ipv4"/>
11+
<address addr="AC:9E:17:A7:17:88" addrtype="mac" vendor="Asustek Computer"/>
12+
<hostnames>
13+
<hostname name="router.asus.com" type="PTR"/>
14+
</hostnames>
15+
<ports><port protocol="udp" portid="161"><state state="open" reason="udp-response" reason_ttl="64"/><service name="snmp" method="table" conf="3"/><script id="snmp-brute" output="&#xa; public - Valid credentials&#xa; private - Valid credentials"><table>
16+
<elem key="password">public</elem>
17+
<elem key="state">Valid credentials</elem>
18+
</table>
19+
<table>
20+
<elem key="password">private</elem>
21+
<elem key="state">Valid credentials</elem>
22+
</table>
23+
</script></port>
24+
</ports>
25+
<times srtt="125" rttvar="4000" to="100000"/>
26+
</host>
27+
<runstats><finished time="1553187612" timestr="Thu Mar 21 10:00:12 2019" elapsed="4.95" summary="Nmap done at Thu Mar 21 10:00:12 2019; 1 IP address (1 host up) scanned in 4.95 seconds" exit="success"/><hosts up="1" down="0" total="1"/>
28+
</runstats>
29+
</nmaprun>

0 commit comments

Comments
 (0)