lib/ronin/nmap/importer.rb
# frozen_string_literal: true
#
# ronin-nmap - A Ruby library for automating nmap and importing nmap scans.
#
# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
#
# ronin-nmap is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ronin-nmap is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ronin-nmap. If not, see <https://www.gnu.org/licenses/>.
#
require 'ronin/db'
require 'nmap/xml'
module Ronin
module Nmap
#
# Handles importing a parsed nmap XML file into [ronin-db].
#
# [ronin-db]: https://github.com/ronin-rb/ronin-db#readme
#
# ## Examples
#
# require 'ronin/nmap/importer'
#
# Ronin::DB.connect
# Ronin::Nmap::Importer.import_file('scan.xml') do |record|
# puts "Imported #{record.inspect}!"
# end
#
module Importer
#
# Parses the nmap XML file and imports it's contents into the database.
#
# @param [String] path
# The path to the nmap XML file to parse and import.
#
# @yield [imported]
# If a block is given, it will be passed the imported database records.
#
# @yieldparam [Ronin::DB::IPAddress,
# Ronin::DB::MACAddress,
# Ronin::DB::HostName,
# Ronin::DB::Port,
# Ronin::DB::Service,
# Ronin::DB::OpenPort] imported
# An imported IP address, MAC address, host name, or open port.
#
# @return [Array<Ronin::DB::IPAddress>]
# If no block was given, then an Array of imported IP addresses will
# be returned.
#
def self.import_file(path,&block)
import(::Nmap::XML.open(path),&block)
end
#
# Imports the parsed nmap XML into the database.
#
# @param [::Nmap::XML] xml
# The parsed nmap XML document.
#
# @yield [imported]
# If a block is given, it will be passed the imported database records.
#
# @yieldparam [Ronin::DB::IPAddress,
# Ronin::DB::MACAddress,
# Ronin::DB::HostName,
# Ronin::DB::Port,
# Ronin::DB::Service,
# Ronin::DB::OpenPort] imported
# An imported IP address, MAC address, host name, or open port.
#
# @return [Array<Ronin::DB::IPAddress>]
# If no block was given, then an Array of imported IP addresses will
# be returned.
#
def self.import(xml,&block)
return enum_for(__method__,xml).to_a unless block
xml.each_up_host do |host|
import_host(host,&block)
end
end
#
# Imports an nmap host into the database.
#
# @yield [imported]
# If a block is given, it will be passed the imported database records.
#
# @yieldparam [Ronin::DB::IPAddress,
# Ronin::DB::MACAddress,
# Ronin::DB::HostName,
# Ronin::DB::Port,
# Ronin::DB::Service,
# Ronin::DB::OpenPort] imported
# An imported IP address, MAC address, host name, or open port.
#
# @param [::Nmap::XML::Host] host
#
# @return [Array<Ronin::DB::IPAddress>]
#
def self.import_host(host,&block)
imported_ip_addresses, = import_addresses(host,&block)
imported_hostnames = import_hostnames(host,&block)
# associate any imported host names with the imported IP addresses
imported_hostnames.each do |imported_hostname|
imported_ip_addresses.each do |imported_ip_address|
DB::HostNameIPAddress.transaction do
DB::HostNameIPAddress.find_or_create_by(
host_name: imported_hostname,
ip_address: imported_ip_address
)
end
end
end
host.each_open_port do |port|
imported_port, imported_service = import_port(port,&block)
# associate the imported port with the imported IP addresses
imported_ip_addresses.each do |imported_ip_address|
import_open_port(imported_ip_address,
imported_port,
imported_service,
&block)
end
end
return imported_ip_addresses
end
#
# Creates or updates an open port association between the imported IP
# address, imported port, and imported service.
#
# @param [Ronin::DB::IPAddress] imported_ip_address
#
# @param [Ronin::DB::Port] imported_port
#
# @param [Ronin::DB::Service] imported_service
#
# @return [Ronin::DB::OpenPort]
#
def self.import_open_port(imported_ip_address,
imported_port,
imported_service)
imported_open_port = DB::OpenPort.transaction do
DB::OpenPort.find_or_create_by(
ip_address: imported_ip_address,
port: imported_port,
service: imported_service
)
end
yield imported_open_port if block_given?
return imported_open_port
end
#
# Imports the host names for the scanned nmap host.
#
# @param [::Nmap::XML::Host] host
#
# @yield [imported]
# If a block is given, it will be passed the imported database records.
#
# @yieldparam [Ronin::DB::HostName] imported
# An imported host name.
#
# @return [Array<Ronin::DB::HostName>]
#
def self.import_hostnames(host)
host.each_hostname.map do |hostname|
imported_host_name = import_hostname(hostname)
yield imported_host_name if block_given?
imported_host_name
end
end
#
# Imports a hostname into the database.
#
# @param [::Nmap::XML::HostName] hostname
# The nmap XML hostname object to import.
#
# @return [Ronin::DB::HostName]
# The imported host name.
#
def self.import_hostname(hostname)
DB::HostName.transaction do
DB::HostName.find_or_import(hostname.name)
end
end
#
# Imports the addresses for a host.
#
# @param [::Nmap::XML::Host] host
#
# @yield [imported]
# If a block is given, it will be passed the imported database records.
#
# @yieldparam [Ronin::DB::IPAddress, Ronin::DB::MACAddress] imported
# An imported IP address or MAC address.
#
# @return [(Array<Ronin::DB::IPAddress>, Array<Ronin::DB::MACAddress>)]
# The imported IP addresses and MAC addresses.
#
def self.import_addresses(host,&block)
imported_ip_addresses = []
imported_mac_addresses = []
host.each_address do |address|
case (imported_address = import_address(address,&block))
when DB::IPAddress
imported_ip_addresses << imported_address
when DB::MACAddress
imported_mac_addresses << imported_address
end
end
# associate any imported MAC addresses with the imported IP addresses
imported_mac_addresses.each do |imported_mac_address|
imported_ip_addresses.each do |imported_ip_address|
DB::IPAddressMACAddress.transaction do
DB::IPAddressMACAddress.find_or_create_by(
mac_address: imported_mac_address,
ip_address: imported_ip_address
)
end
end
end
return imported_ip_addresses, imported_mac_addresses
end
#
# Imports and IP address or a MAC address into the database.
#
# @param [::Nmap::XML::Address] address
# The nmap XML address object.
#
# @yield [imported]
# If a block is given, it will be passed the imported address.
#
# @yieldparam [Ronin::DB::IPAddress, Ronin::DB::MACAddress] imported
# The imported IP address or MAC address record.
#
# @return [Ronin::DB::IPAddress, Ronin::DB::MACAddress]
# The imported IP address or MAC address.
#
def self.import_address(address,&block)
case address.type
when :ipv4, :ipv6 then import_ip_address(address,&block)
when :mac then import_mac_address(address,&block)
end
end
# Mapping of nmap XML IP address types to IP versions.
IP_VERSIONS = {
ipv4: 4,
ipv6: 6
}
#
# Imports an IP address into the database.
#
# @param [::Nmap::XML::Address] address
# The nmap XML IP address object.
#
# @yield [imported]
# If a block is given, it will be passed the imported IP address.
#
# @yieldparam [Ronin::DB::IPAddress] imported
# The imported IP address record.
#
# @return [Ronin::DB::IPAddress]
# The imported IP address.
#
def self.import_ip_address(address,&block)
imported_ip_address = DB::IPAddress.transaction do
DB::IPAddress.find_or_create_by(
version: IP_VERSIONS.fetch(address.type),
address: address.addr
)
end
yield imported_ip_address if block_given?
return imported_ip_address
end
#
# Imports an MAC address into the database.
#
# @param [::Nmap::XML::Address] address
# The nmap XML MAC address object.
#
# @yield [imported]
# If a block is given, it will be passed the imported MAC address
# reocrd.
#
# @yieldparam [Ronin::DB::MACAddress] imported
# The imported MAC address record.
#
# @return [Ronin::DB::MACAddress]
# The imported MAC address.
#
def self.import_mac_address(address,&block)
imported_mac_address = DB::MACAddress.transaction do
DB::MACAddress.find_or_import(address.addr)
end
yield imported_mac_address if block_given?
return imported_mac_address
end
#
# Import an nmap port.
#
# @param [::Nmap::XML::Port] port
# The nmap port.
#
# @yield [imported]
# If a block is given, it will be passed the imported database records.
#
# @yieldparam [Ronin::DB::Port, Ronin::DB::Service] imported
# An imported port or service.
#
# @return [Ronin::DB::Port, (Ronin::DB::Port, Ronin::DB::Service)]
# The imported port and optionally the imported service.
#
def self.import_port(port)
imported_port = DB::Port.transaction do
DB::Port.find_or_create_by(
protocol: port.protocol,
number: port.number
)
end
imported_service = if (service = port.service)
DB::Service.transaction do
DB::Service.find_or_create_by(
name: service.name
)
end
end
if block_given?
yield imported_port
yield imported_service if imported_service
end
return imported_port, imported_service
end
end
end
end