diff --git a/lib/ronin/recon/cli/commands/run.rb b/lib/ronin/recon/cli/commands/run.rb index ce26a92..5a7c594 100644 --- a/lib/ronin/recon/cli/commands/run.rb +++ b/lib/ronin/recon/cli/commands/run.rb @@ -43,6 +43,13 @@ module Commands # ## Options # # -D, --debug Enable debugging output + # -C, --config-file FILE Loads the configuration file + # -w, --worker WORKER Explicitly uses a worker + # -e, --enable WORKER Enables a worker + # -d, --disable WORKER Disables a worker + # --worker-file FILE Loads a worker from a file + # -p, --param WORKER.NAME=VALUE Sets a param for a worker + # -c, --concurrency WORKER=NUM Sets the concurrency of a worker # --max-depth NUM The maximum recon depth (Default: 3) # -o, --output FILE The output file to write results to # -I, --ignore VALUE The values to ignore in result @@ -64,6 +71,76 @@ class Run < Command usage '[options] {IP | IP-range | DOMAIN | HOST | WILDCARD | WEBSITE} ...' + option :config_file, short: '-C', + value: { + type: String, + usage: 'FILE' + }, + desc: 'Loads the configuration file' + + option :worker, short: '-w', + value: { + type: String, + usage: 'WORKER' + }, + desc: 'Explicitly uses a worker' do |worker| + @only_workers << worker + end + + option :enable, short: '-e', + value: { + type: String, + usage: 'WORKER' + }, + desc: 'Enables a worker' do |worker| + @enable_workers << worker + end + + option :disable, short: '-d', + value: { + type: String, + usage: 'WORKER' + }, + desc: 'Disables a worker' do |worker| + @disable_workers << worker + end + + option :worker_file, value: { + type: String, + usage: 'FILE' + }, + desc: 'Loads a worker from a file' do |path| + @worker_files << path + end + + option :param, short: '-p', + value: { + type: /\A[^\.\=\s]+\.[^=\s]+=.+\z/, + usage: 'WORKER.NAME=VALUE' + }, + desc: 'Sets a param for a worker' do |str| + prefix, value = str.split('=',2) + worker, name = prefix.split('.',2) + + @worker_params[worker][name.to_sym] = value + end + + option :concurrency, short: '-c', + value: { + type: /\A[^\.\=\s]+=\d+\z/, + usage: 'WORKER=NUM' + }, + desc: 'Sets the concurrency of a worker' do |str| + worker, concurrency = str.split('=',2) + + @worker_concurrency[worker] = concurrency.to_i + end + + option :intensity, value: { + type: [:passive, :active, :aggressive] + }, + desc: 'Filter workers by intensity' + option :max_depth, value: { type: Integer, usage: 'NUM', @@ -107,6 +184,46 @@ class Run < Command man_page 'ronin-recon-run.1' + # Explicit set of workers to only use. + # + # @return [Set] + attr_reader :only_workers + + # Additional set of workers to enable. + # + # @return [Set] + attr_reader :enable_workers + + # Additional set of workers to disable. + # + # @return [Set] + attr_reader :disable_workers + + # Additional set of worker files to load. + # + # @return [Set] + attr_reader :worker_files + + # The loaded configuration for the {Engine}. + # + # @return [Config] + attr_reader :config + + # The loaded workers for the {Engine}. + # + # @return [Workers] + attr_reader :workers + + # The params for the workers. + # + # @return [Hash{String => Hash{String => String}}] + attr_reader :worker_params + + # The concurrency for the workers. + # + # @return [Hash{String => Integer}] + attr_reader :worker_concurrency + # The values that are out of scope. # # @return [Array] @@ -121,6 +238,14 @@ class Run < Command def initialize(**kwargs) super(**kwargs) + @only_workers = Set.new + @enable_workers = Set.new + @disable_workers = Set.new + @worker_files = Set.new + + @worker_params = Hash.new { |hash,key| hash[key] = {} } + @worker_concurrency = {} + @ignore = [] end @@ -131,6 +256,9 @@ def initialize(**kwargs) # The initial recon values. # def run(*values) + load_config + load_workers + values = values.map { |value| parse_value(value) } output_file = if options[:output] && options[:output_format] @@ -144,7 +272,10 @@ def run(*values) end begin - Engine.run(values, max_depth: options[:max_depth], ignore: @ignore) do |engine| + Engine.run(values, config: @config, + workers: @workers, + max_depth: options[:max_depth], + ignore: @ignore) do |engine| engine.on(:value) do |value,parent| print_value(value,parent) end @@ -198,6 +329,64 @@ def parse_value(value) exit(-1) end + # + # Loads the recon configuration file from either + # the `--config-file` option or `~/.config/ronin-recon/config.yml`. + # + def load_config + @config = if (path = options[:config_file]) + Config.load(path) + else + Config.default + end + + unless @only_workers.empty? + @config.workers = @only_workers + end + + @enable_workers.each do |worker_id| + @config.workers.add(worker_id) + end + + @disable_workers.each do |worker_id| + @config.workers.delete(worker_id) + end + + @worker_params.each do |worker,params| + if @config.params.has_key?(params) + @config.params[worker].merge!(params) + else + @config.params[worker] = params + end + end + + @worker_concurrency.each do |worker,concurrency| + @config.concurrency[worker] = concurrency + end + end + + # + # Loads the worker classes from the {Config#workers}, as well as + # additional workers loaded by `--load-worker`. + # + # @note + # If the `--intensity` option is given, then the workers will be + # filtered by intensity. + # + def load_workers + @workers = Workers.load(@config.workers) + + unless @worker_files.empty? + @worker_files.each do |path| + @workers.load_file(path) + end + end + + if (level = options[:intensity]) + @workers = @workers.intensity(level) + end + end + # # Imports a discovered value into ronin-db. # diff --git a/man/ronin-recon-run.1.md b/man/ronin-recon-run.1.md index e1dff96..707d1c7 100644 --- a/man/ronin-recon-run.1.md +++ b/man/ronin-recon-run.1.md @@ -37,6 +37,28 @@ Runs the recon engine with one or more initial values. `-D`, `--debug` : Enables debugging output. +`-C`, `--config-file` *FILE* +: Loads the `ronin-recon` configuration file. If not specified, then + `~/.config/ronin-recon/config.yml` will be loaded instead. + +`-w`, `--worker` *WORKER* +: Explicitly uses the specified worker instead of the default set of workers. + +`-e`, `--enable` *WORKER* +: Enables the worker in addition to the default set of workers. + +`-d`, `--disable` *WORKER* +: Disables the worker from the default set of workers. + +`--worker-file` *FILE* +: Loads a custom worker from the specified `.rb` file. + +`-p`, `--param` *WORKER*`.`*NAME*`=`*VALUE* +: Sets a param value for the given worker. + +`-c`, `--concurrency` *WORKER*`=`*NUM* +: Overrides the concurrency for the given worker. + `--max-depth` *NUM* : The maximum recon depth. Defaults to depth of `3` if the option is not specified. diff --git a/spec/cli/commands/run_spec.rb b/spec/cli/commands/run_spec.rb index fe4eb3e..61322a5 100644 --- a/spec/cli/commands/run_spec.rb +++ b/spec/cli/commands/run_spec.rb @@ -7,6 +7,30 @@ include_examples "man_page" describe "#initialize" do + it "must initialize #only_workers to an empty Set" do + expect(subject.only_workers).to eq(Set.new) + end + + it "must initialize #enable_workers to an empty Set" do + expect(subject.enable_workers).to eq(Set.new) + end + + it "must initialize #disable_workers to an empty Set" do + expect(subject.disable_workers).to eq(Set.new) + end + + it "must initialize #worker_files to an empty Set" do + expect(subject.worker_files).to eq(Set.new) + end + + it "must initialize #worker_params to an empty Hash" do + expect(subject.worker_params).to eq({}) + end + + it "must initialize #worker_concurrency to an empty Hash" do + expect(subject.worker_concurrency).to eq({}) + end + it "must initialize #ignore to an empty Array" do expect(subject.ignore).to eq([]) end @@ -15,6 +39,104 @@ describe "options" do before { subject.option_parser.parse(argv) } + context "when the '--worker WORKER' option is given" do + let(:worker1) { 'dns/lookup' } + let(:worker2) { 'dns/reverse_lookup' } + let(:argv) { ['--worker', worker1, '--worker', worker2] } + + it "must append the WORKER values to #only_workers" do + expect(subject.only_workers).to eq(Set[worker1, worker2]) + end + end + + context "when the '--enable WORKER' option is given" do + let(:worker1) { 'dns/lookup' } + let(:worker2) { 'dns/reverse_lookup' } + let(:argv) { ['--enable', worker1, '--enable', worker2] } + + it "must append the WORKER values to #enable_workers" do + expect(subject.enable_workers).to eq(Set[worker1, worker2]) + end + end + + context "when the '--disable WORKER' option is given" do + let(:worker1) { 'dns/lookup' } + let(:worker2) { 'dns/reverse_lookup' } + let(:argv) { ['--disable', worker1, '--disable', worker2] } + + it "must append the WORKER values to #disable_workers" do + expect(subject.disable_workers).to eq(Set[worker1, worker2]) + end + end + + context "when the '--worker-file FILE' option is given" do + let(:file1) { 'path/to/worker1.rb' } + let(:file2) { 'path/to/worker2.rb' } + let(:argv) { ['--worker-file', file1, '--worker-file', file2] } + + it "must append the WORKER values to #worker_files" do + expect(subject.worker_files).to eq(Set[file1, file2]) + end + end + + context "when the '--param WORKER.NAME=VALUE' option is given" do + let(:worker1) { 'test/worker1' } + let(:name1) { :foo } + let(:value1) { 'a' } + let(:name2) { :bar } + let(:value2) { 'b' } + let(:worker2) { 'test/worker2' } + let(:name3) { :baz } + let(:value3) { 'x' } + let(:name4) { :qux } + let(:value4) { 'y' } + let(:argv) do + [ + '--param', "#{worker1}.#{name1}=#{value1}", + '--param', "#{worker1}.#{name2}=#{value2}", + '--param', "#{worker2}.#{name3}=#{value3}", + '--param', "#{worker2}.#{name4}=#{value4}" + ] + end + + it "must parse and populate #worker_params with the params grouped by worker ID" do + expect(subject.worker_params).to eq( + { + worker1 => { + name1 => value1, + name2 => value2 + }, + worker2 => { + name3 => value3, + name4 => value4 + } + } + ) + end + end + + context "when the '--concurrency WORKER=NUM' option is given" do + let(:worker1) { 'test/worker1' } + let(:concurrency1) { 42 } + let(:worker2) { 'test/worker2' } + let(:concurrency2) { 10 } + let(:argv) do + [ + '--concurrency', "#{worker1}=#{concurrency1}", + '--concurrency', "#{worker2}=#{concurrency2}" + ] + end + + it "must parse and populate #worker_concurrency grouped by worker ID" do + expect(subject.worker_concurrency).to eq( + { + worker1 => concurrency1, + worker2 => concurrency2 + } + ) + end + end + context "when the '--output' option is given" do let(:path) { 'path/to/output.json' } let(:argv) { ['--output', path] } @@ -48,6 +170,168 @@ end end + let(:fixtures_dir) { File.join(__dir__,'..','..','fixtures') } + + describe "#load_config" do + context "when the '--config-file FILE' option has not been given" do + before { subject.load_config } + + it "must set #config using Ronin::Recon::Config.default" do + expect(subject.config).to eq(Ronin::Recon::Config.default) + end + end + + context "when the '--config-file FILE' option has been given" do + let(:config_file) { File.join(fixtures_dir,'config.yml') } + + before do + subject.options[:config_file] = config_file + + subject.load_config + end + + it "must set #config using Ronin::Recon::Config.load" do + expect(subject.config).to eq(Ronin::Recon::Config.load(config_file)) + end + end + + context "when the '--worker WORKER' option has been given" do + let(:worker1) { 'dns/lookup' } + let(:worker2) { 'dns/reverse_lookup' } + + before do + subject.only_workers << worker1 << worker2 + + subject.load_config + end + + it "must override #config.workers with the workers" do + expect(subject.config.workers.workers).to eq(Set[worker1, worker2]) + end + end + + context "when the '--enable WORKER' option has been given" do + let(:worker1) { 'test/worker1' } + let(:worker2) { 'test/worker2' } + + before do + subject.enable_workers << worker1 << worker2 + + subject.load_config + end + + it "must add the workers to #config.workers" do + expect(subject.config.workers.workers).to include(worker1) + expect(subject.config.workers.workers).to include(worker2) + end + end + + context "when the '--disable WORKER' option has been given" do + let(:worker1) { 'dns/lookup' } + let(:worker2) { 'dns/reverse_lookup' } + + before do + subject.disable_workers << worker1 << worker2 + + subject.load_config + end + + it "must remove the workers from #config.workers" do + expect(subject.config.workers.workers).to_not include(worker1) + expect(subject.config.workers.workers).to_not include(worker2) + end + end + + context "when the '--params WORKER.NAME=VALUE' option has been given" do + let(:worker1) { 'test/worker1' } + let(:name1) { :foo } + let(:value1) { 'a' } + let(:name2) { :bar } + let(:value2) { 'b' } + let(:worker2) { 'test/worker2' } + let(:name3) { :baz } + let(:value3) { 'x' } + let(:name4) { :qux } + let(:value4) { 'y' } + + before do + subject.worker_params[worker1][name1] = value1 + subject.worker_params[worker1][name2] = value2 + subject.worker_params[worker2][name3] = value3 + subject.worker_params[worker2][name4] = value4 + + subject.load_config + end + + it "must populate #config.params with the params" do + expect(subject.config.params[worker1][name1]).to eq(value1) + expect(subject.config.params[worker1][name2]).to eq(value2) + expect(subject.config.params[worker2][name3]).to eq(value3) + expect(subject.config.params[worker2][name4]).to eq(value4) + end + end + + context "when the '--concurrency WORKER=VALUE' option has been given" do + let(:worker1) { 'test/worker1' } + let(:concurrency1) { 42 } + let(:worker2) { 'test/worker2' } + let(:concurrency2) { 10 } + + before do + subject.worker_concurrency[worker1] = concurrency1 + subject.worker_concurrency[worker2] = concurrency2 + + subject.load_config + end + + it "must populate #config.concurrency with the concurrencies" do + expect(subject.config.concurrency[worker1]).to eq(concurrency1) + expect(subject.config.concurrency[worker2]).to eq(concurrency2) + end + end + end + + describe "#load_workers" do + it "must load the workers in #config.workers and set #workers" do + subject.load_config + subject.load_workers + + expect(subject.workers).to eq( + Ronin::Recon::Workers.load(subject.config.workers) + ) + end + + context "when the '--worker-file FILE' option has been given" do + let(:file) { File.join(fixtures_dir,'test_worker.rb') } + + before do + subject.worker_files << file + + subject.load_config + subject.load_workers + end + + it "must load the worker from the given FILE" do + expect(subject.workers).to include(Ronin::Recon::TestWorker) + end + end + + context "when the '--intensity LEVEL' option has been given" do + let(:intensity) { :passive } + + before do + subject.options[:intensity] = intensity + + subject.load_config + subject.load_workers + end + + it "must filter #workers by the intensity level" do + expect(subject.workers.map(&:intensity)).to all(eq(intensity)) + end + end + end + describe "#parse_value" do context "when given a valid value string" do let(:string) { 'www.example.com' }