# frozen_string_literal: true
#
# ronin-recon - A micro-framework and tool for performing reconnaissance.
#
# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
#
# ronin-recon 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-recon 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-recon. If not, see .
#
require 'ronin/recon/exceptions'
require 'ronin/core/home'
require 'set'
module Ronin
module Recon
#
# Represents configuration for the recon engine.
#
class Config
#
# Represents the set of workers to use.
#
# @api private
#
class Workers
include Enumerable
# The set of worker IDs.
#
# @return [Set]
attr_reader :ids
#
# Initializes the workers.
#
# @param [Set, Array, Hash{String => Boolean}] workers
# The set of worker IDs.
#
# @raise [ArgumentError]
# The given workers argument was not a Set, Array, or Hash.
#
def initialize(workers)
case workers
when Set then @ids = workers.dup
when Array then @ids = workers.to_set
when Hash
@ids = DEFAULT.dup
workers.each do |worker_id,enabled|
if enabled then add(worker_id)
else delete(worker_id)
end
end
else
raise(ArgumentError,"workers value must be a Set, Array, or Hash: #{workers.inspect}")
end
end
# The default workers configuration.
DEFAULT = Set[
'dns/lookup',
'dns/mailservers',
'dns/nameservers',
'dns/reverse_lookup',
'dns/srv_enum',
'dns/subdomain_enum',
'dns/suffix_enum',
'net/ip_range_enum',
'net/port_scan',
'net/service_id',
'ssl/cert_grab',
'ssl/cert_enum',
# NOTE: disabled due to rate limiting issues
# 'ssl/cert_sh',
'web/dir_enum',
'web/email_addresses',
'web/spider'
]
#
# Initializes the default workers.
#
# @return [Workers]
#
def self.default
new(DEFAULT)
end
#
# Adds a worker to the workers.
#
# @param [String] worker_id
# The worker ID to add.
#
# @return [self]
#
# @api public
#
def add(worker_id)
@ids.add(worker_id)
return self
end
#
# Deletes a worker from the workers.
#
# @param [String] worker_id
# The worker ID to disable.
#
# @api public
#
def delete(worker_id)
@ids.delete(worker_id)
return self
end
#
# Determines if the worker is enabled in the workers.
#
# @param [String] worker_id
# The worker ID to search for.
#
# @return [Boolean]
#
# @api public
#
def include?(worker_id)
@ids.include?(worker_id)
end
#
# Enumerates over each worker in the set.
#
# @yield [worker_id]
# The given block will be passed each worker ID in the set.
#
# @yieldparam [String] worker_id
# A worker ID in the set.
#
# @return [Enumerator]
# If no block is given, an Enumerator will be returned.
#
def each(&block)
@ids.each(&block)
end
#
# Compares the workers to another object.
#
# @param [Object] other
# The other object.
#
# @return [Boolean]
#
def eql?(other)
self.class == other.class && @ids == other.ids
end
alias == eql?
end
# The workers to use.
#
# @return [Workers]
#
# @api public
attr_reader :workers
# Params for individual workers.
#
# @return [Hash{String => Hash{Symbol => Object}}]
#
# @api public
attr_reader :params
# Concurrency values for individual workers.
#
# @return [Hash{String => Integer}]
#
# @api public
attr_reader :concurrency
#
# Initializes the recon engine configuration.
#
# @param [Workers] workers
# The workers to use.
#
# @param [Hash{String => Hash{Symbol => Object}}] params
# The params for individual workers.
#
# @param [Hash{String => Hash{Symbol => Object}}] concurrency
# The concurrency values for individual workers.
#
def initialize(workers: Workers.default, params: {}, concurrency: {})
@workers = workers
@params = params
@concurrency = concurrency
end
#
# Validates the loaded configuration data.
#
# @param [Object] data
# The loaded configuration data.
#
# @raise [InvalidConfig]
# The configuration data is not a Hash, does not contain Symbol keys,
# or does not contain Hashes.
#
# @return [true]
# The configuration data is valid.
#
def self.validate(data)
unless data.kind_of?(Hash)
raise(InvalidConfig,"must contain a Hash: #{data.inspect}")
end
if (workers = data[:workers])
unless (workers.kind_of?(Hash) || workers.kind_of?(Array))
raise(InvalidConfig,"workers value must be a Hash or an Array: #{workers.inspect}")
end
end
if (params_value = data[:params])
unless params_value.kind_of?(Hash)
raise(InvalidConfig,"params value must be a Hash: #{params_value.inspect}")
end
params_value.each do |worker_id,params_hash|
unless worker_id.kind_of?(String)
raise(InvalidConfig,"worker ID must be a String: #{worker_id.inspect}")
end
unless params_hash.kind_of?(Hash)
raise(InvalidConfig,"params value for worker (#{worker_id.inspect}) must be a Hash: #{params_hash.inspect}")
end
params_hash.each_key do |param_key|
unless param_key.kind_of?(Symbol)
raise(InvalidConfig,"param key for worker (#{worker_id.inspect}) must be a Symbol: #{param_key.inspect}")
end
end
end
end
if (concurrency_value = data[:concurrency])
unless concurrency_value.kind_of?(Hash)
raise(InvalidConfig,"concurrency value must be a Hash: #{concurrency_value.inspect}")
end
concurrency_value.each do |worker_id,concurrency|
unless worker_id.kind_of?(String)
raise(InvalidConfig,"worker ID must be a String: #{worker_id.inspect}")
end
unless concurrency.kind_of?(Integer)
raise(InvalidConfig,"concurrency value for worker (#{worker_id.inspect}) must be an Integer: #{concurrency.inspect}")
end
end
end
return true
end
#
# Loads configuration from a YAML file.
#
# @param [String] path
# The path to the YAML configuration file.
#
# @raise [InvalidConfigFile]
# The configuration file contained invalid YAML.
#
def self.load(path)
yaml = YAML.load_file(path)
begin
validate(yaml)
rescue InvalidConfig => error
raise(InvalidConfigFile,"invalid config file (#{path.inspect}): #{error.message}")
end
workers = if (workers_value = yaml[:workers])
Workers.new(workers_value)
else
Workers.default
end
params = yaml.fetch(:params,{})
concurrency = yaml.fetch(:concurrency,{})
return new(workers: workers, params: params, concurrency: concurrency)
end
# The path to the `~/.config/ronin-recon/config.yml` file.
DEFAULT_PATH = File.join(Core::Home.config_dir('ronin-recon'),'config.yml')
#
# The default configuration to use.
#
# @return [Config]
#
# @api public
#
def self.default
if File.file?(DEFAULT_PATH)
load(DEFAULT_PATH)
else
new
end
end
#
# Overrides the workers.
#
# @param [Workers, Set, Array] new_workers
# The new workers value.
#
# @return [Workers]
# The new workers value.
#
# @raise [ArgumentError]
# An invalid workers value was given.
#
# @api public
#
def workers=(new_workers)
@workers = case new_workers
when Workers then new_workers
when Set, Array, Hash then Workers.new(new_workers)
else
raise(ArgumentError,"new workers value must be a #{Workers}, Set, Array, or Hash: #{new_workers.inspect}")
end
end
#
# Compares the configuration with another object.
#
# @param [Object] other
# The other object.
#
# @return [Boolean]
#
def eql?(other)
self.class == other.class &&
@workers == other.workers &&
@params == other.params &&
@concurrency == other.concurrency
end
alias == eql?
end
end
end