# encoding: ascii
# api: powershell
# title: Parse nmap XML output
# description: A PowerShell script into which one or more nmap XML output file objects can be piped, then the script emits synthetic objects representing port-scanned hosts from the XML file(s). Get Windows and Linux versions of the nmap scanner for free from http://nmap.org. Get a sample nmap XML file to play with and see some examples of using the script at https://blogs.sans.org/windows-security/2009/06/11/powershell-script-to-parse-nmap-xml-output/
# version: 2.0
# type: script
# author: Jason Fossen
# license: CC0
# x-poshcode-id: 1179
# x-archived: 2017-04-30T13:01:40
# x-published: 2010-06-27T07:04:00
#
#
####################################################################################
# Script: parse-nmap.ps1
# Purpose: Parse the XML output file of the nmap port scanner and emit custom
# objects with properties containing data from XML file.
# Example: dir *.xml | .\parse-nmap.ps1
# Author: Jason Fossen (www.EnclaveConsulting.com)
# Version: 2.0
# Updated: 15.Jun.2009
# Notes: Pipe one or more file objects into script. Don't pipe file contents.
# Script is slow, e.g., a 20MB XML log with 65k entries requires 103 seconds
# to process on a 2.16GHz T2600 Intel CPU (about 630 entries per second).
# LEGAL: PUBLIC DOMAIN. SCRIPT PROVIDED "AS IS" WITH NO WARRANTIES OR GUARANTEES OF
# ANY KIND, INCLUDING BUT NOT LIMITED TO MERCHANTABILITY AND/OR FITNESS FOR
# A PARTICULAR PURPOSE. ALL RISKS OF DAMAGE REMAINS WITH THE USER, EVEN IF
# THE AUTHOR, SUPPLIER OR DISTRIBUTOR HAS BEEN ADVISED OF THE POSSIBILITY OF
# ANY SUCH DAMAGE. IF YOUR STATE DOES NOT PERMIT THE COMPLETE LIMITATION OF
# LIABILITY, THEN DELETE THIS FILE SINCE YOU ARE NOW PROHIBITED TO HAVE IT.
####################################################################################
if ($args -ne $null) {
"`nThis script takes no arguments, please pipe one or more files into it."
"Example: dir *.xml | .\parse-nmap.ps1 | export-csv -path c:\file.csv`n"
exit
}
# Set $ShowProgress to $false if you do not want progress info sent to StdErr.
$ShowProgress = $true
ForEach ($file in $input) {
If ($ShowProgress) { [Console]::Error.WriteLine("[" + (get-date).ToLongTimeString() + "] Starting $file" ) }
$xmldoc = new-object System.XML.XMLdocument
$xmldoc.Load($file)
$i = 1 #Counter for <host> nodes processed.
# Process each of the <host> nodes from the nmap report.
$xmldoc.nmaprun.host | foreach-object {
$hostnode = $_ # $hostnode is a <host> node in the XML.
# Init variables, with $entry being the custom object for each <host>.
$service = " " #service needs to be a single space.
$entry = ($entry = " " | select-object FQDN, HostName, Status, IPv4, IPv6, MAC, Ports, OS)
# Extract state element of status:
$entry.Status = $hostnode.status.state.Trim()
if ($entry.Status.length -eq 0) { $entry.Status = "<no-status>" }
# Extract fully-qualified domain name(s).
$hostnode.hostnames | foreach-object { $entry.FQDN += $_.hostname.name + " " }
$entry.FQDN = $entry.FQDN.Trim()
if ($entry.FQDN.Length -eq 0) { $entry.FQDN = "<no-fullname>" }
# Note that this code cheats, it only gets the hostname of the first FQDN if there are multiple FQDNs.
if ($entry.FQDN.Contains(".")) { $entry.HostName = $entry.FQDN.Substring(0,$entry.FQDN.IndexOf(".")) }
elseif ($entry.FQDN -eq "<no-fullname>") { $entry.HostName = "<no-hostname>" }
else { $entry.HostName = $entry.FQDN }
# Process each of the <address> nodes, extracting by type.
$hostnode.address | foreach-object {
if ($_.addrtype -eq "ipv4") { $entry.IPv4 += $_.addr + " "}
if ($_.addrtype -eq "ipv6") { $entry.IPv6 += $_.addr + " "}
if ($_.addrtype -eq "mac") { $entry.MAC += $_.addr + " "}
}
if ($entry.IPv4 -eq $null) { $entry.IPv4 = "<no-ipv4>" } else { $entry.IPv4 = $entry.IPv4.Trim()}
if ($entry.IPv6 -eq $null) { $entry.IPv6 = "<no-ipv6>" } else { $entry.IPv6 = $entry.IPv6.Trim()}
if ($entry.MAC -eq $null) { $entry.MAC = "<no-mac>" } else { $entry.MAC = $entry.MAC.Trim() }
# Process all ports from <ports><port>, and note that <port> does not contain an array if it only has one item in it.
if ($hostnode.ports.port -eq $null) { $entry.Ports = "<no-ports>" }
else
{
$hostnode.ports.port | foreach-object {
If ($_.service.name -eq $null) { $service = "unknown" } else { $service = $_.service.name }
$entry.Ports += $_.state.state + ":" + $_.protocol + ":" + $_.portid + ":" + $service + " "
}
$entry.Ports = $entry.Ports.Trim()
}
# Extract fingerprinted OS type and percent of accuracy.
$hostnode.os.osmatch | foreach-object {$entry.OS += $_.name + " <" + ([String] $_.accuracy) + "%-accuracy> "}
$entry.OS = $entry.OS.Trim()
if ($entry.OS -eq "<%-accuracy>") { $entry.OS = "<no-os>" }
# Emit custom object from script.
$entry
$i++ #Progress counter...
}
If ($ShowProgress) { [Console]::Error.WriteLine("[" + (get-date).ToLongTimeString() + "] Finished $file, processed $i entries." ) }
}