Last active
June 20, 2019 17:56
-
-
Save mbrownnycnyc/5fca3b5e0fbbf46d9586ca816a2ca74a to your computer and use it in GitHub Desktop.
Part of SANS SEC505 class tools. see https://cyber-defense.sans.org/blog/2009/06/11/powershell-script-to-parse-nmap-xml-output and https://gist.github.com/mbrownnycnyc/a69345cd8f034192b9ba2947ab708470
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #################################################################################### | |
| #.Synopsis | |
| # very slightly modified version of Parse-Nmap.ps1. Parse XML output files of the nmap port scanner (www.nmap.org). | |
| # | |
| #.Description | |
| # Parse XML output files 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 OutputDelimiter | |
| # The delimiter for the strings in the OS, Ports and Services properties. | |
| # Default is a newline. Change it when you want single-line output. | |
| # | |
| #.Parameter RunStatsOnly | |
| # Only displays general scan information from each XML output file, such | |
| # as scan start/stop time, elapsed time, command-line arguments, etc. | |
| # | |
| #.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 | |
| # .\parse-nmap.ps1 -path $files | |
| # | |
| #.Example | |
| # .\parse-nmap.ps1 -path scanfile.xml -runstatsonly | |
| # | |
| #.Example | |
| # .\parse-nmap.ps1 scanfile.xml -OutputDelimiter " " | |
| # | |
| #Requires -Version 2 | |
| # | |
| #.Notes | |
| # Author: Enclave Consulting LLC, Jason Fossen (http://www.sans.org/sec505) | |
| # Version: 4.6 | |
| # Updated: 27.Feb.2016 | |
| # 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. | |
| #################################################################################### | |
| function parse-nmap | |
| { | |
| param ( | |
| $Path, | |
| [String] $OutputDelimiter = "`n", | |
| [Switch] $RunStatsOnly | |
| ) | |
| if ($Path -match '/\?|/help|--h|--help') | |
| { | |
| $MyInvocation = (Get-Variable -Name MyInvocation -Scope Script).Value | |
| get-help -full ($MyInvocation.MyCommand.Path) | |
| 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,ScanTypes,TcpPorts,UdpPorts,IpProtocols,SctpPorts,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 | |
| $xmldoc.nmaprun.scaninfo | foreach { | |
| $stat.ScanTypes += $_.type + " " | |
| $services = $_.services #Seems unnecessary, but solves a problem. | |
| if ($services -ne $null -and $services.contains("-")) | |
| { | |
| #In the original XML, ranges of ports are summarized, e.g., "500-522", | |
| #but the script will list each port separately for easier searching. | |
| $array = $($services.replace("-","..")).Split(",") | |
| $temp = @($array | where { $_ -notlike "*..*" }) | |
| $array | where { $_ -like "*..*" } | foreach { invoke-expression "$_" } | foreach { $temp += $_ } | |
| $temp = [Int32[]] $temp | sort | |
| $services = [String]::Join(",",$temp) | |
| } | |
| switch ($_.protocol) | |
| { | |
| "tcp" { $stat.TcpPorts = $services ; break } | |
| "udp" { $stat.UdpPorts = $services ; break } | |
| "ip" { $stat.IpProtocols = $services ; break } | |
| "sctp" { $stat.SctpPorts = $services ; break } | |
| } | |
| } | |
| $stat.ScanTypes = $($stat.ScanTypes).Trim() | |
| $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. | |
| } | |
| # Not doing just -RunStats, so process hosts from XML file. | |
| ForEach ($file in $Path) | |
| { | |
| Write-Verbose -Message ("[" + (get-date).ToLongTimeString() + "] Starting $file" ) | |
| $StartTime = get-date | |
| $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. | |
| foreach ($hostnode in $xmldoc.nmaprun.host) | |
| { | |
| # Init some variables, with $entry being the custom object for each <host>. | |
| $service = " " #service needs to be a single space. | |
| $entry = ($entry = " " | select-object HostName, FQDN, Status, IPv4, IPv6, MAC, Ports, Services, OS, Script) | |
| # Extract state element of status: | |
| if ($hostnode.Status -ne $null -and $hostnode.Status.length -ne 0) { $entry.Status = $hostnode.status.state.Trim() } | |
| if ($entry.Status.length -lt 2) { $entry.Status = "<no-status>" } | |
| # Extract computer names provided by user or through PTR record, but avoid duplicates and allow multiple names. | |
| # Note that $hostnode.hostnames can be empty, and the formatting of one versus multiple names is different. | |
| # The crazy foreach-ing here is to deal with backwards compatibility issues... | |
| $tempFQDN = $tempHostName = "" | |
| ForEach ($hostname in $hostnode.hostnames) | |
| { | |
| ForEach ($hname in $hostname.hostname) | |
| { | |
| ForEach ($namer in $hname.name) | |
| { | |
| if ($namer -ne $null -and $namer.length -ne 0 -and $namer.IndexOf(".") -ne -1) | |
| { | |
| #Only append to temp variable if it would be unique. | |
| if($tempFQDN.IndexOf($namer.tolower()) -eq -1) | |
| { $tempFQDN = $tempFQDN + " " + $namer.tolower() } | |
| } | |
| elseif ($namer -ne $null -and $namer.length -ne 0) | |
| { | |
| #Only append to temp variable if it would be unique. | |
| if($tempHostName.IndexOf($namer.tolower()) -eq -1) | |
| { $tempHostName = $tempHostName + " " + $namer.tolower() } | |
| } | |
| } | |
| } | |
| } | |
| $tempFQDN = $tempFQDN.Trim() | |
| $tempHostName = $tempHostName.Trim() | |
| if ($tempHostName.Length -eq 0 -and $tempFQDN.Length -eq 0) { $tempHostName = "<no-hostname>" } | |
| #Extract hostname from the first (and only the first) FQDN, if FQDN present. | |
| if ($tempFQDN.Length -ne 0 -and $tempHostName.Length -eq 0) | |
| { $tempHostName = $tempFQDN.Substring(0,$tempFQDN.IndexOf(".")) } | |
| if ($tempFQDN.Length -eq 0) { $tempFQDN = "<no-fullname>" } | |
| $entry.FQDN = $tempFQDN | |
| $entry.HostName = $tempHostName #This can be different than FQDN because PTR might not equal user-supplied hostname. | |
| # Process each of the <address> nodes, extracting by type. | |
| ForEach ($addr in $hostnode.address) | |
| { | |
| if ($addr.addrtype -eq "ipv4") { $entry.IPv4 += $addr.addr + " "} | |
| if ($addr.addrtype -eq "ipv6") { $entry.IPv6 += $addr.addr + " "} | |
| if ($addr.addrtype -eq "mac") { $entry.MAC += $addr.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. | |
| # This could be parsed out into separate properties, but that would be overkill. We still want to be able to use | |
| # simple regex patterns to do our filtering afterwards, and it's helpful to have the output look similar to | |
| # the console output of nmap by itself for easier first-time comprehension. | |
| if ($hostnode.ports.port -eq $null) { $entry.Ports = "<no-ports>" ; $entry.Services = "<no-services>" } | |
| else | |
| { | |
| ForEach ($porto in $hostnode.ports.port) | |
| { | |
| if ($porto.service.name -eq $null) { $service = "unknown" } else { $service = $porto.service.name } | |
| $entry.Ports += $porto.state.state + ":" + $porto.protocol + ":" + $porto.portid + ":" + $service + $OutputDelimiter | |
| # 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. | |
| if ($porto.state.state -like "open*" -and ($porto.service.tunnel.length -gt 2 -or $porto.service.product.length -gt 2 -or $porto.service.proto.length -gt 2)) { $entry.Services += $porto.protocol + ":" + $porto.portid + ":" + $service + ":" + ($porto.service.product + " " + $porto.service.version + " " + $porto.service.tunnel + " " + $porto.service.proto + " " + $porto.service.rpcnum).Trim() + " <" + ([Int] $porto.service.conf * 10) + "%-confidence>$OutputDelimiter" } | |
| } | |
| $entry.Ports = $entry.Ports.Trim() | |
| if ($entry.Services -eq $null) { $entry.Services = "<no-services>" } else { $entry.Services = $entry.Services.Trim() } | |
| if ($entry.Services -ne $null) { $entry.Services = $entry.Services.Trim() } | |
| } | |
| # Extract fingerprinted OS type and percent of accuracy. | |
| ForEach ($osm in $hostnode.os.osmatch) {$entry.OS += $osm.name + " <" + ([String] $osm.accuracy) + "%-accuracy>$OutputDelimiter"} | |
| ForEach ($osc in $hostnode.os.osclass) {$entry.OS += $osc.type + " " + $osc.vendor + " " + $osc.osfamily + " " + $osc.osgen + " <" + ([String] $osc.accuracy) + "%-accuracy>$OutputDelimiter"} | |
| if ($entry.OS -ne $null -and $entry.OS.length -gt 0) | |
| { | |
| $entry.OS = $entry.OS.Replace(" "," ") | |
| $entry.OS = $entry.OS.Replace("<%-accuracy>","") #Sometimes no osmatch. | |
| $entry.OS = $entry.OS.Trim() | |
| } | |
| if ($entry.OS.length -lt 16) { $entry.OS = "<no-os>" } | |
| # Extract script output, first for port scripts, then for host scripts. | |
| ForEach ($pp in $hostnode.ports.port) | |
| { | |
| if ($pp.script -ne $null) { | |
| $entry.Script += "<PortScript id=""" + $pp.script.id + """>$OutputDelimiter" + ($pp.script.output -replace "`n","$OutputDelimiter") + "$OutputDelimiter</PortScript> $OutputDelimiter $OutputDelimiter" | |
| } | |
| } | |
| if ($hostnode.hostscript -ne $null) { | |
| ForEach ($scr in $hostnode.hostscript.script) | |
| { | |
| $entry.Script += '<HostScript id="' + $scr.id + '">' + $OutputDelimiter + ($scr.output.replace("`n","$OutputDelimiter")) + "$OutputDelimiter</HostScript> $OutputDelimiter $OutputDelimiter" | |
| } | |
| } | |
| if ($entry.Script -eq $null) { $entry.Script = "<no-script>" } | |
| # Emit custom object from script. | |
| $i++ #Progress counter... | |
| $entry | |
| } | |
| Write-Verbose -Message ( "[" + (get-date).ToLongTimeString() + "] Finished $file, processed $i entries." ) | |
| Write-Verbose -Message ('Total Run Time: ' + ( [MATH]::Round( ((Get-date) - $StartTime).TotalSeconds, 3 )) + ' seconds') | |
| Write-Verbose -Message ('Entries/Second: ' + ( [MATH]::Round( ($i / $((Get-date) - $StartTime).TotalSeconds), 3 ) ) ) | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment