lib/nmap/xml.rb
# frozen_string_literal: true
require_relative 'xml/scanner'
require_relative 'xml/scan_task'
require_relative 'xml/scan'
require_relative 'xml/host'
require_relative 'xml/run_stat'
require_relative 'xml/prescript'
require_relative 'xml/postscript'
require 'nokogiri'
module Nmap
#
# Represents an Nmap XML file.
#
class XML
include Enumerable
# The parsed XML document.
#
# @return [Nokogiri::XML]
#
# @api private
attr_reader :doc
# Path of the Nmap XML scan file
#
# @return [String, nil]
attr_reader :path
#
# Creates a new XML object.
#
# @param [Nokogiri::XML::Document] doc
# The path to the Nmap XML scan file or Nokogiri::XML::Document.
#
# @param [String, nil] path
# The optional path the XML was loaded from.
#
# @yield [xml]
# If a block is given, it will be passed the new XML object.
#
# @yieldparam [XML] xml
# The newly created XML object.
#
def initialize(doc, path: nil)
@doc = doc
@path = File.expand_path(path) if path
yield self if block_given?
end
#
# Creates a new XML object from XML text.
#
# @param [String] text
# XML text of the scan file
#
# @yield [xml]
# If a block is given, it will be passed the new XML object.
#
# @yieldparam [XML] xml
# The newly created XML object.
#
# @since 0.8.0
#
def self.parse(text,&block)
new(Nokogiri::XML(text),&block)
end
#
# Creates a new XML object from the file.
#
# @param [String] path
# The path to the XML file.
#
# @yield [xml]
# If a block is given, it will be passed the new XML object.
#
# @yieldparam [XML] xml
# The newly created XML object.
#
# @since 0.7.0
#
def self.open(path,&block)
path = File.expand_path(path)
doc = Nokogiri::XML(File.open(path))
new(doc, path: path, &block)
end
#
# Parses the scanner information.
#
# @return [Scanner]
# The scanner that was used and generated the scan file.
#
def scanner
@scanner ||= Scanner.new(
@doc.root['scanner'],
@doc.root['version'],
@doc.root['args'],
Time.at(@doc.root['start'].to_i)
)
end
#
# Parses the XML scan file version.
#
# @return [String]
# The version of the XML scan file.
#
def version
@version ||= @doc.root['xmloutputversion']
end
#
# Parses the scan information.
#
# @return [Array<Scan>]
# The scan information.
#
def scan_info
@doc.xpath('/nmaprun/scaninfo').map do |scaninfo|
Scan.new(
scaninfo['type'].to_sym,
scaninfo['protocol'].to_sym,
scaninfo['services'].split(',').map { |ports|
if ports.include?('-')
Range.new(*(ports.split('-',2)))
else
ports.to_i
end
}
)
end
end
#
# Parses the essential runstats information.
#
# @yield [run_stat]
# The given block will be passed each runstat.
#
# @yieldparam [RunStat] run_stat
# A runstat.
#
# @return [Enumerator]
# If no block is given, an enumerator will be returned.
#
# @since 0.7.0
#
def each_run_stat
return enum_for(__method__) unless block_given?
@doc.xpath('/nmaprun/runstats/finished').each do |run_stat|
yield RunStat.new(
Time.at(run_stat['time'].to_i),
run_stat['elapsed'],
run_stat['summary'],
run_stat['exit']
)
end
return self
end
#
# Parses the essential runstats information.
#
# @return [Array<RunStat>]
# The runstats.
#
# @since 0.7.0
#
def run_stats
each_run_stat.to_a
end
#
# Parses the verbose level.
#
# @return [Integer]
# The verbose level.
#
def verbose
@verbose ||= @doc.at('verbose/@level').inner_text.to_i
end
#
# Parses the debugging level.
#
# @return [Integer]
# The debugging level.
#
def debugging
@debugging ||= @doc.at('debugging/@level').inner_text.to_i
end
#
# Parses the tasks of the scan.
#
# @yield [task]
# The given block will be passed each scan task.
#
# @yieldparam [ScanTask] task
# A task from the scan.
#
# @return [Enumerator]
# If no block is given, an enumerator will be returned.
#
# @since 0.7.0
#
def each_task
return enum_for(__method__) unless block_given?
@doc.xpath('/nmaprun/taskbegin').each do |task_begin|
task_end = task_begin.xpath('following-sibling::taskend').first
yield ScanTask.new(
task_begin['task'],
Time.at(task_begin['time'].to_i),
Time.at(task_end['time'].to_i),
task_end['extrainfo']
)
end
return self
end
#
# Parses the tasks of the scan.
#
# @return [Array<ScanTask>]
# The tasks of the scan.
#
# @since 0.1.2
#
def tasks
each_task.to_a
end
#
# Finds the task with the given name.
#
# @param [String] name
# The task name to search for.
#
# @return [ScanTask, nil]
# The scan task with the matching name or `nil`.
#
# @since 0.10.0
#
def task(name)
each_task.find { |scan_task| scan_task.name == name }
end
#
# The NSE scripts ran before the scan.
#
# @return [Prescript]
# Contains the script output and data.
#
# @since 0.9.0
#
def prescript
@prescript ||= if (prescript = @doc.at('prescript'))
Prescript.new(prescript)
end
end
#
# The NSE scripts ran after the scan.
#
# @return [Postscript]
# Contains the script output and data.
#
# @since 0.9.0
#
def postscript
@postscript ||= if (postscript = @doc.at('postscript'))
Postscript.new(postscript)
end
end
#
# Parses the hosts in the scan.
#
# @yield [host]
# Each host will be passed to a given block.
#
# @yieldparam [Host] host
# A host in the scan.
#
# @return [XML, Enumerator]
# The XML object. If no block was given, an enumerator object will
# be returned.
#
def each_host
return enum_for(__method__) unless block_given?
@doc.xpath('/nmaprun/host').each do |host|
yield Host.new(host)
end
return self
end
#
# Parses the hosts in the scan.
#
# @return [Array<Host>]
# The hosts in the scan.
#
def hosts
each_host.to_a
end
#
# Returns the first host.
#
# @return [Host]
#
# @since 0.8.0
#
def host
each_host.first
end
#
# Parses the hosts that were found to be down during the scan.
#
# @yield [host]
# Each host will be passed to a given block.
#
# @yieldparam [Host] host
# A down host in the scan.
#
# @return [XML, Enumerator]
# The XML parser. If no block was given, an enumerator object will
# be returned.
#
# @since 0.8.0
#
def each_down_host
return enum_for(__method__) unless block_given?
@doc.xpath("/nmaprun/host[status[@state='down']]").each do |host|
yield Host.new(host)
end
return self
end
#
# Parses the hosts found to be down during the scan.
#
# @return [Array<Host>]
# The down hosts in the scan.
#
# @since 0.8.0
#
def down_hosts
each_down_host.to_a
end
#
# Returns the first host found to be down during the scan.
#
# @return [Host]
#
# @since 0.8.0
#
def down_host
each_down_host.first
end
#
# Parses the hosts that were found to be up during the scan.
#
# @yield [host]
# Each host will be passed to a given block.
#
# @yieldparam [Host] host
# A host in the scan.
#
# @return [XML, Enumerator]
# The XML parser. If no block was given, an enumerator object will
# be returned.
#
def each_up_host
return enum_for(__method__) unless block_given?
@doc.xpath("/nmaprun/host[status[@state='up']]").each do |host|
yield Host.new(host)
end
return self
end
#
# Parses the hosts found to be up during the scan.
#
# @return [Array<Host>]
# The hosts in the scan.
#
def up_hosts
each_up_host.to_a
end
#
# Returns the first host found to be up during the scan.
#
# @return [Host]
#
# @since 0.8.0
#
def up_host
each_up_host.first
end
#
# Parses the hosts that were found to be up during the scan.
#
# @see each_up_host
#
def each(&block)
each_up_host(&block)
end
#
# Converts the XML parser to a String.
#
# @return [String]
# The path of the XML file or the raw XML.
#
def to_s
if @path then @path.to_s
else @doc.to_s
end
end
end
end