# encoding: ascii
# api: powershell
# title: 
# description: 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/  (this is where the latest version will be maintained, it’s been edited a few times already since being posted here).
# version: 3.0
# type: function
# author: Jason Fossen 
# license: CC0
# function: parse-nmap
# x-poshcode-id: 1219
# x-archived: 2009-07-22T10:23:11
#
#
Param ($Path, [Switch] $RunStatsOnly, [Switch] $ShowProgress)

function parse-nmap 
{
	####################################################################################
	#.Synopsis 
	#    Parse XML output file(s) of the nmap port scanner (www.nmap.org). 
	#
	#.Description 
	#    Parse XML output file(s) of the nmap port scanner (www.nmap.org) and  
	#    emit custom objects with properties containing the scan data. The 
	#    script can accept either piped or parameter input.  The script can be
	#    safely dot-sourced without error as is. 
	#
	#.Parameter Path  
	#    Either 1) a string with or without wildcards to one or more XML output
	#    files, or 2) one or more FileInfo objects representing XML output files.
	#
	#.Parameter RunStatsOnly
	#    Only displays general scan information from each XML output file, such
	#    as scan start/stop time, elapsed time, command-line arguments, etc.
	#
	#.Parameter ShowProgress
	#    Prints progress information to StdErr while processing host entries.    
	#
	#.Example 
	#    dir *.xml | .\parse-nmap.ps1
	#
	#.Example 
	#	 .\parse-nmap.ps1 -path onefile.xml
	#    .\parse-nmap.ps1 -path *files.xml 
	#
	#.Example 
	#    $files = dir *some.xml,others*.xml,there?.xml 
	#    .\parse-nmap.ps1 -path $files    
	#
	#.Example 
	#    .\parse-nmap.ps1 -path scanfile.xml -runstatsonly
	#
	#Requires -Version 1.0 
	#
	#.Notes 
	#  Author: Jason Fossen (http://blogs.sans.org/windows-security/)  
	# Version: 3.0
	# Updated: 18.Jul.2009
	#   Other: Once PowerShell 2.0 has been out a few months, the script will be
	#          rewritten as an advanced function and otherwise spruced up...
	#   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.
	####################################################################################

	param ($Path, [Switch] $RunStatsOnly, [Switch] $ShowProgress)
	
	if ($Path -match "/\?|/help|-h|-help|--h|--help") 
	{ 
		"`nPurpose: Process nmap XML output files (www.nmap.org)."
		"Example: dir *.xml | .\parse-nmap.ps1 `n"
		exit 
	}

	if ($Path -eq $null) {$Path = @(); $input | foreach { $Path += $_ } } 
	if (($Path -ne $null) -and ($Path.gettype().name -eq "String")) {$Path = dir $path} #To support wildcards in $path.  
	$1970 = [DateTime] "01 Jan 1970 01:00:00 GMT"

	if ($RunStatsOnly)
	{
		ForEach ($file in $Path) 
		{
			$xmldoc = new-object System.XML.XMLdocument
			$xmldoc.Load($file)
			$stat = ($stat = " " | select-object FilePath,FileName,Scanner,Profile,ProfileName,Hint,ScanName,Arguments,Options,NmapVersion,XmlOutputVersion,StartTime,FinishedTime,ElapsedSeconds,ScanType,Protocol,NumberOfServices,Services,VerboseLevel,DebuggingLevel,HostsUp,HostsDown,HostsTotal)
			$stat.FilePath = $file.fullname
			$stat.FileName = $file.name
			$stat.Scanner = $xmldoc.nmaprun.scanner
			$stat.Profile = $xmldoc.nmaprun.profile
			$stat.ProfileName = $xmldoc.nmaprun.profile_name
			$stat.Hint = $xmldoc.nmaprun.hint
			$stat.ScanName = $xmldoc.nmaprun.scan_name
			$stat.Arguments = $xmldoc.nmaprun.args
			$stat.Options = $xmldoc.nmaprun.options
			$stat.NmapVersion = $xmldoc.nmaprun.version
			$stat.XmlOutputVersion = $xmldoc.nmaprun.xmloutputversion
			$stat.StartTime = $1970.AddSeconds($xmldoc.nmaprun.start) 	
			$stat.FinishedTime = $1970.AddSeconds($xmldoc.nmaprun.runstats.finished.time)
			$stat.ElapsedSeconds = $xmldoc.nmaprun.runstats.finished.elapsed
			$stat.ScanType = $xmldoc.nmaprun.scaninfo.type	
			$stat.Protocol = $xmldoc.nmaprun.scaninfo.protocol		
			$stat.NumberOfServices = $xmldoc.nmaprun.scaninfo.numservices

			#In the original XML, ranges of ports are summarized, e.g., "500-522", but the script will list each port separately.
			$stat.Services = $xmldoc.nmaprun.scaninfo.services   #A long comma-delimitted string which includes ",50-88," ranges.
			$stat.Services = $($stat.Services.replace("-","..")).Split(",") 
			$temp  = $stat.Services | where { $_ -notlike "*..*"} | foreach { [Int] $_ } 
			$stat.Services | where { $_ -like "*..*" } | foreach { invoke-expression "$_" } | foreach { $temp += [Int] $_ } 
			$stat.Services = $temp | sort #Services now contains a sorted Int32 array of all ports, range operators removed. 

			$stat.VerboseLevel = $xmldoc.nmaprun.verbose.level
			$stat.DebuggingLevel = $xmldoc.nmaprun.debugging.level		
			$stat.HostsUp = $xmldoc.nmaprun.runstats.hosts.up
			$stat.HostsDown = $xmldoc.nmaprun.runstats.hosts.down		
			$stat.HostsTotal = $xmldoc.nmaprun.runstats.hosts.total
			$stat 			
		}
		return #Don't process hosts.  
	}
	
	ForEach ($file in $Path) {
		If ($ShowProgress) { [Console]::Error.WriteLine("[" + (get-date).ToLongTimeString() + "] Starting $file" ) }

		$xmldoc = new-object System.XML.XMLdocument
		$xmldoc.Load($file)
		
		# Process each of the <host> nodes from the nmap report.
		$i = 0  #Counter for <host> nodes processed.
		$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.
			$i++  #Progress counter...
			$entry
		}

		If ($ShowProgress) { [Console]::Error.WriteLine("[" + (get-date).ToLongTimeString() + "] Finished $file, processed $i entries." ) }
	}
}



if ($path -eq $null) { $path = @(); $input | foreach { $Path += $_ } } #Piping issues...

if ($showprogress) {parse-nmap -path $Path -showprogress}
elseif ($runstatsonly) {parse-nmap -path $Path -runstatsonly}
else { parse-nmap -path $Path }
