# frozen_string_literal: true
#
# ronin-masscan - A Ruby library and CLI for working with masscan.
#
# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
#
# ronin-masscan 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-masscan 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-masscan. If not, see .
#
require 'ronin/masscan/cli/port_list'
module Ronin
module Masscan
class CLI
#
# Mixin which adds nmap target filtering options to commands.
#
module FilteringOptions
#
# Adds filtering options to the command class including
# {FilteringOptions}.
#
# @param [Class] command
# The command class including {FilteringOptions}.
#
def self.included(command)
command.option :protocol, short: '-P',
value: {
type: [:tcp, :udp]
},
desc: 'Filters the targets by protocol' do |proto|
@protocols << proto
end
command.option :ip, value: {
type: String,
usage: 'IP'
},
desc: 'Filters the targets by IP' do |ip|
@ips << ip
end
command.option :ip_range, value: {
type: String,
usage: 'CIDR'
},
desc: 'Filters the targets by IP range' do |ip_range|
@ip_ranges << IPAddr.new(ip_range)
end
command.option :ports, short: '-p',
value: {
type: /\A(?:\d+|\d+-\d+)(?:,(?:\d+|\d+-\d+))*\z/,
usage: '{PORT | PORT1-PORT2},...'
},
desc: 'Filter targets by port number' do |ports|
@ports << PortList.parse(ports)
end
command.option :with_app_protocol, value: {
type: /\A[a-z][a-z0-9_-]*\z/,
usage: 'APP_PROTOCOL[,...]'
},
desc: 'Filters targets with the app protocol' do |app_protocol|
@with_app_protocols << app_protocol.to_sym
end
command.option :with_payload, value: {
type: String,
usage: 'STRING'
},
desc: 'Filters targets containing the payload' do |string|
@with_payloads << string
end
command.option :with_payload_regex, value: {
type: Regexp,
usage: '/REGEX/'
},
desc: 'Filters targets with the matching payload' do |regexp|
@with_payloads << regexp
end
end
# The protocols to filter the targets by.
#
# @return [Set<:tcp, :udp>]
attr_reader :protocols
# The IPs to filter the targets by.
#
# @return [Set]
attr_reader :ips
# The IP ranges to filter the targets by.
#
# @return [Set]
attr_reader :ip_ranges
# The ports to filter the targets by.
#
# @return [Set]
attr_reader :ports
# The app protocols to filter the targets by.
#
# @return [Set]
attr_reader :with_app_protocols
# The payload Strings or Regexps to filter the targets by.
#
# @return [Set]
attr_reader :with_payloads
#
# Initializes the command.
#
# @param [Hash{Symbol => String}] kwargs
# Additional keywords for the command.
#
def initialize(**kwargs)
super(**kwargs)
@protocols = Set.new
@ips = Set.new
@ip_ranges = Set.new
@ports = Set.new
# Masscan::Banner filtering options
@with_app_protocols = Set.new
@with_payloads = Set.new
end
#
# Filters the masscan records.
#
# @param [::Masscan::OutputFile] output_file
# The parsed nmap xml data to filter.
#
# @return [Enumerator::Lazy]
# A lazy enumerator of the filtered records.
#
def filter_records(output_file)
records = output_file.each.lazy
unless @protocols.empty?
records = filter_records_by_protocol(records)
end
unless @ips.empty?
records = filter_records_by_ip(records)
end
unless @ip_ranges.empty?
records = filter_records_by_ip_range(records)
end
unless @ports.empty?
records = filter_records_by_port(records)
end
unless @with_app_protocols.empty?
records = filter_records_by_app_protocol(records)
end
unless @with_payloads.empty?
records = filter_records_by_payload(records)
end
return records
end
#
# Filter `Masscan::Status` records.
#
# @param [Enumerator::Lazy] records
# The records to filter.
#
# @return [Enumerator::Lazy]
# The filtered records.
#
def filter_status_records(records)
records.filter do |record|
record.kind_of?(::Masscan::Status)
end
end
#
# Filter `Masscan::Banner` records.
#
# @param [Enumerator::Lazy] records
# The records to filter.
#
# @return [Enumerator::Lazy]
# The filtered records.
#
def filter_banner_records(records)
records.filter do |record|
record.kind_of?(::Masscan::Banner)
end
end
#
# Filters the records by protocol
#
# @param [Enumerator::Lazy] records
# The records to filter.
#
# @return [Enumerator::Lazy]
# A lazy enumerator of the filtered records.
#
def filter_records_by_protocol(records)
records.filter do |record|
@protocols.include?(record.protocol)
end
end
#
# Filters the records by IP address.
#
# @param [Enumerator::Lazy] records
# The records to filter.
#
# @return [Enumerator::Lazy]
# A lazy enumerator of the filtered records.
#
def filter_records_by_ip(records)
records.filter do |record|
@ips.include?(record.ip)
end
end
#
# Filters the records by an IP rangeo.
#
# @param [Enumerator::Lazy] records
# The records to filter.
#
# @return [Enumerator::Lazy]
# A lazy enumerator of the filtered records.
#
def filter_records_by_ip_range(records)
records.filter do |record|
@ip_ranges.any? do |ip_range|
ip_range.include?(record.ip)
end
end
end
#
# Filters the records by port number.
#
# @param [Enumerator::Lazy] records
# The records to filter.
#
# @return [Enumerator::Lazy]
# A lazy enumerator of the filtered records.
#
def filter_records_by_port(records)
records.filter do |record|
@ports.include?(record.port)
end
end
#
# Filters the records by app-protocol IDs.
#
# @param [Enumerator::Lazy] records
# The records to filter.
#
# @return [Enumerator::Lazy]
# A lazy enumerator of the filtered records.
#
def filter_records_by_app_protocol(records)
records.filter do |record|
record.kind_of?(::Masscan::Banner) &&
@with_app_protocols.include?(record.app_protocol)
end
end
#
# Filters the records by payload contents.
#
# @param [Enumerator::Lazy] records
# The records to filter.
#
# @return [Enumerator::Lazy]
# A lazy enumerator of the filtered records.
#
def filter_records_by_payload(records)
regexp = Regexp.union(@with_payloads.to_a)
records.filter do |record|
record.kind_of?(::Masscan::Banner) &&
record.payload =~ regexp
end
end
end
end
end
end