From 68d807129edb7e68e8e18d3735180b9c5d9765a8 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Fri, 26 Jul 2024 11:44:19 -0700 Subject: [PATCH 01/16] Version bump to 0.2.0. --- lib/ronin/recon/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ronin/recon/version.rb b/lib/ronin/recon/version.rb index b3840134..54c5f952 100644 --- a/lib/ronin/recon/version.rb +++ b/lib/ronin/recon/version.rb @@ -21,6 +21,6 @@ module Ronin module Recon # ronin-recon version - VERSION = '0.1.0' + VERSION = '0.2.0' end end From c1a64c6dbf8320ee1687f35e3b3c9a4b667cf14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Rebughini?= Date: Sun, 11 Aug 2024 07:28:49 +0200 Subject: [PATCH 02/16] Add a SecurityTrails API worker (#152) This worker will accept a domain as input and, given a valid API key, will return a list of subdomain hosts by querying the Security Trails API. --------- Co-authored-by: Postmodern --- .../recon/builtin/api/security_trails.rb | 111 ++++++++++++++++++ spec/builtin/api/security_trails_spec.rb | 86 ++++++++++++++ 2 files changed, 197 insertions(+) create mode 100755 lib/ronin/recon/builtin/api/security_trails.rb create mode 100644 spec/builtin/api/security_trails_spec.rb diff --git a/lib/ronin/recon/builtin/api/security_trails.rb b/lib/ronin/recon/builtin/api/security_trails.rb new file mode 100755 index 00000000..6b2228fa --- /dev/null +++ b/lib/ronin/recon/builtin/api/security_trails.rb @@ -0,0 +1,111 @@ +# 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/worker' +require 'ronin/support/text/patterns/network' + +require 'async/http/internet/instance' +require 'set' + +module Ronin + module Recon + module API + # + # A recon worker that queries https://securitytrails.com and returns subdomains + # for a given domain. + # + class SecurityTrails < Worker + + register 'api/security_trails' + + author "Nicolò Rebughini", email: "nicolo.rebughini@gmail.com" + summary "Queries the Domains https://securitytrails.com API" + description <<~DESC + Queries the Domains https://securitytrails.com API and returns the subdomains + of the domain. + DESC + + accepts Domain + outputs Host + intensity :passive + concurrency 1 + + param :api_key, String, required: true, + default: ENV['SECURITYTRAILS_API_KEY'], + desc: 'The API key for SecurityTrails' + + # The HTTP client for `https://crt.sh`. + # + # @return [Async::HTTP::Client] + # + # @api private + attr_reader :client + + # + # Initializes the `api/security_trails` worker. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments. + # + # @api private + # + def initialize(**kwargs) + super(**kwargs) + + @client = Async::HTTP::Client.new( + Async::HTTP::Endpoint.for('https','api.securitytrails.com') + ) + end + + # + # Returns host from each domains certificate. + # + # @param [Values::Domain] domain + # The domain value to gather subdomains for. + # + # @yield [host] + # For each subdmomain found through the API, a Domain + # value will be yielded. + # + # @yieldparam [Values::Host] subdomain + # The host found. + # + def process(domain) + path = "/v1/domain/#{domain}/subdomains?children_only=false&include_inactive=false" + response = @client.get(path, { 'APIKEY' => params[:api_key] }) + body = begin + JSON.parse(response.read, symbolize_names: true) + ensure + response.close + end + subdomains = body.fetch(:subdomains, []) + full_domains = Set.new + + subdomains.each do |subdomain| + full_domain = "#{subdomain}.#{domain}" + + yield Host.new(full_domain) if full_domains.add?(full_domain) + end + end + + end + end + end +end diff --git a/spec/builtin/api/security_trails_spec.rb b/spec/builtin/api/security_trails_spec.rb new file mode 100644 index 00000000..6406414d --- /dev/null +++ b/spec/builtin/api/security_trails_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' +require 'ronin/recon/builtin/api/security_trails' + +require 'webmock/rspec' + +describe Ronin::Recon::API::SecurityTrails do + subject do + described_class.new(params: { api_key: 'my-test-api-key'}) + end + + it "must set concurrency to 1" do + expect(described_class.concurrency).to eq(1) + end + + describe "#initialize" do + it "must initialize #client for 'https://api.securitytrails.com'" do + expect(subject.client).to be_kind_of(Async::HTTP::Client) + # BUG: https://github.com/bblimke/webmock/issues/1060 + # expect(subject.client.endpoint).to be_kind_of(Async::HTTP::Endpoint) + # expect(subject.client.endpoint.scheme).to eq('https') + # expect(subject.client.endpoint.hostname).to eq('api.securitytrails.com') + # expect(subject.client.endpoint.port).to eq(443) + end + end + + describe "#process" do + context "for domain with subdomains" do + let(:domain) { Ronin::Recon::Values::Domain.new("example.com") } + let(:response_json) do + "{\"endpoint\":\"/v1/domain/example.com/subdomains\",\"meta\":{\"limit_reached\":true},\"subdomain_count\":3,\"subdomains\":[\"api\",\"test\",\"proxy\"]}" + end + let(:expected) do + %w[ + api.example.com + test.example.com + proxy.example.com + ] + end + + before do + stub_request(:get, "https://api.securitytrails.com/v1/domain/#{domain.name}/subdomains?children_only=false&include_inactive=false") + .with(headers: {APIKEY: 'my-test-api-key'}) + .to_return(status: 200, body: response_json) + end + + it "must yield Values::Domain for each subdomain" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to_not be_empty + expect(yielded_values).to all(be_kind_of(Ronin::Recon::Values::Host)) + expect(yielded_values.map(&:name)).to eq(expected) + end + end + + context "for domain with no subdomains" do + let(:domain) { Ronin::Recon::Values::Domain.new("invalid.com") } + let(:response_json) do + "{\"endpoint\":\"/v1/domain/invalid.com/subdomains\",\"count\":null,\"subdomains\":[]}" + end + + before do + stub_request(:get, "https://api.securitytrails.com/v1/domain/#{domain.name}/subdomains?children_only=false&include_inactive=false") + .with(headers: {APIKEY: 'my-test-api-key'}) + .to_return(status: 200, body: response_json) + end + + it "must not yield anything" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to be_empty + end + end + end +end From c56ddf07a77fd81603746054525d7196ee6e7008 Mon Sep 17 00:00:00 2001 From: AI-Mozi Date: Sat, 17 Aug 2024 15:24:41 +0200 Subject: [PATCH 03/16] Fix att_reader description for SecurityTrails --- lib/ronin/recon/builtin/api/security_trails.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/ronin/recon/builtin/api/security_trails.rb b/lib/ronin/recon/builtin/api/security_trails.rb index 6b2228fa..88a57876 100755 --- a/lib/ronin/recon/builtin/api/security_trails.rb +++ b/lib/ronin/recon/builtin/api/security_trails.rb @@ -51,11 +51,13 @@ class SecurityTrails < Worker default: ENV['SECURITYTRAILS_API_KEY'], desc: 'The API key for SecurityTrails' - # The HTTP client for `https://crt.sh`. + # + # The HTTP client for `https://securitytrails.com`. # # @return [Async::HTTP::Client] # # @api private + # attr_reader :client # From 4e5aa00b78f5dbaa4c66ac4c7ff665317d747b92 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Sun, 18 Aug 2024 15:53:48 -0700 Subject: [PATCH 04/16] Allow `Worker` classes to omit defining `outputs` values (closes #158). * Some workers will need to accept values, process/save them, but not yield any Values themselves (ex: `web/wordlist` and `web/screenshot`). --- lib/ronin/recon/cli/commands/worker.rb | 12 ++++---- lib/ronin/recon/worker.rb | 7 +---- spec/cli/commands/worker_spec.rb | 40 ++++++++++++++++++++++++++ spec/worker_spec.rb | 6 ++-- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/lib/ronin/recon/cli/commands/worker.rb b/lib/ronin/recon/cli/commands/worker.rb index 60d1b000..728f2a82 100644 --- a/lib/ronin/recon/cli/commands/worker.rb +++ b/lib/ronin/recon/cli/commands/worker.rb @@ -95,12 +95,14 @@ def print_worker(worker) end puts - puts 'Outputs:' - puts - indent do - print_list(worker.outputs.map(&method(:value_class_name))) + if (outputs = worker.outputs) + puts 'Outputs:' + puts + indent do + print_list(outputs.map(&method(:value_class_name))) + end + puts end - puts puts "Intensity: #{worker.intensity}" diff --git a/lib/ronin/recon/worker.rb b/lib/ronin/recon/worker.rb index 59c8603f..4340065f 100644 --- a/lib/ronin/recon/worker.rb +++ b/lib/ronin/recon/worker.rb @@ -305,12 +305,9 @@ def self.accepts(*value_classes) # @param [Array>] value_classes # The optional new value class(es) to outputs. # - # @return [Array>] + # @return [Array>, nil] # the value class which the recon worker outputs. # - # @raise [NotImplementedError] - # No value class was defined for the recon worker. - # # @example define that the recon worker outputs Host values: # outputs Host # @@ -320,8 +317,6 @@ def self.outputs(*value_classes) else @outputs || if superclass < Worker superclass.outputs - else - raise(NotImplementedError,"#{self} did not set outputs") end end end diff --git a/spec/cli/commands/worker_spec.rb b/spec/cli/commands/worker_spec.rb index 4d0ffb1a..12d8bbdf 100644 --- a/spec/cli/commands/worker_spec.rb +++ b/spec/cli/commands/worker_spec.rb @@ -131,5 +131,45 @@ OUTPUT ).to_stdout end + + context "when the worker class does not define `outputs`" do + module TestWorkerCommand + class WorkerWithoutOutputs < Ronin::Recon::Worker + + id 'worker_without_outputs' + summary 'Test worker without `outputs`' + description <<~DESC + Test printing a worker without an `outputs`. + DESC + + accepts URL + intensity :passive + + end + end + + let(:worker_class) { TestWorkerCommand::WorkerWithoutOutputs } + + it "must omit the 'Outputs:' line and list" do + expect { + subject.print_worker(worker_class) + }.to output( + <<~OUTPUT + [ worker_without_outputs ] + + Summary: Test worker without `outputs` + Description: + + Test printing a worker without an `outputs`. + + Accepts: + + * URL + + Intensity: passive + OUTPUT + ).to_stdout + end + end end end diff --git a/spec/worker_spec.rb b/spec/worker_spec.rb index bc05de13..c8ebbaf7 100644 --- a/spec/worker_spec.rb +++ b/spec/worker_spec.rb @@ -156,10 +156,8 @@ class WorkerWithoutOutputs < Ronin::Recon::Worker subject { TestWorkers::WorkerWithoutOutputs } - it "must raise a NotImplementedError excpetion when called" do - expect { - subject.outputs - }.to raise_error(NotImplementedError,"#{subject} did not set outputs") + it "must return nil" do + expect(subject.outputs).to be(nil) end context "but the Worker class inherits from another worker class" do From 9e8f021bbc66bd17c4c4ae4449800bacaf7ba91b Mon Sep 17 00:00:00 2001 From: Postmodern Date: Mon, 19 Aug 2024 15:34:27 -0700 Subject: [PATCH 05/16] Renamed `ronin-recon test` to `ronin-recon run-worker` (closes #165). * Left behind an alias for `ronin-recon test` to `ronin-recon run-worker`. --- README.md | 2 +- data/templates/worker.rb.erb | 2 +- gemspec.yml | 2 +- lib/ronin/recon/cli.rb | 2 ++ .../cli/commands/{test.rb => run_worker.rb} | 14 +++++++------ man/ronin-recon-irb.1.md | 2 +- ...-test.1.md => ronin-recon-run-worker.1.md} | 10 +++++----- man/ronin-recon-run.1.md | 4 ++-- man/ronin-recon.1.md | 4 ++-- spec/cli/commands/new_spec.rb | 20 +++++++++---------- .../{test_spec.rb => run_worker_spec.rb} | 4 ++-- 11 files changed, 35 insertions(+), 31 deletions(-) rename lib/ronin/recon/cli/commands/{test.rb => run_worker.rb} (88%) rename man/{ronin-recon-test.1.md => ronin-recon-run-worker.1.md} (69%) rename spec/cli/commands/{test_spec.rb => run_worker_spec.rb} (96%) diff --git a/README.md b/README.md index 3d79441c..7ca8d316 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Commands: irb new run - test + run-worker, test worker workers ``` diff --git a/data/templates/worker.rb.erb b/data/templates/worker.rb.erb index 56220ea7..9691cc01 100644 --- a/data/templates/worker.rb.erb +++ b/data/templates/worker.rb.erb @@ -1,4 +1,4 @@ -#!/usr/bin/env -S ronin-recon test -f +#!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/<%= @worker_type[:file] -%>' diff --git a/gemspec.yml b/gemspec.yml index 4cba8628..9ad8aefb 100644 --- a/gemspec.yml +++ b/gemspec.yml @@ -31,7 +31,7 @@ generated_files: - man/ronin-recon-new.1 - man/ronin-recon-workers.1 - man/ronin-recon-worker.1 - - man/ronin-recon-test.1 + - man/ronin-recon-run-worker.1 - man/ronin-recon-run.1 - data/wordlists/subdomains-1000.txt.gz - data/wordlists/raft-small-directories.txt.gz diff --git a/lib/ronin/recon/cli.rb b/lib/ronin/recon/cli.rb index 51964352..2e8815f8 100644 --- a/lib/ronin/recon/cli.rb +++ b/lib/ronin/recon/cli.rb @@ -46,6 +46,8 @@ class CLI command_name 'ronin-recon' version Ronin::Recon::VERSION + command_aliases['test'] = 'run-worker' + end end end diff --git a/lib/ronin/recon/cli/commands/test.rb b/lib/ronin/recon/cli/commands/run_worker.rb similarity index 88% rename from lib/ronin/recon/cli/commands/test.rb rename to lib/ronin/recon/cli/commands/run_worker.rb index 26d01344..55faaa23 100644 --- a/lib/ronin/recon/cli/commands/test.rb +++ b/lib/ronin/recon/cli/commands/run_worker.rb @@ -31,11 +31,11 @@ module Recon class CLI module Commands # - # Loads an individual worker and tests it. + # Loads an individual worker and runs it. # # ## Usage # - # ronin-recon test [options] {--file FILE | NAME} {IP | IP-range | DOMAIN | HOST | WILDCARD | WEBSITE} + # ronin-recon run-worker [options] {--file FILE | NAME} {IP | IP-range | DOMAIN | HOST | WILDCARD | WEBSITE} # # ## Options # @@ -47,7 +47,9 @@ module Commands # # IP|IP-range|DOMAIN|HOST|WILDCARD|WEBSITE An initial recon value. # - class Test < WorkerCommand + # @since 0.2.0 + # + class RunWorker < WorkerCommand include DebugOption include Printing @@ -60,12 +62,12 @@ class Test < WorkerCommand usage: 'IP|IP-range|DOMAIN|HOST|WILDCARD|WEBSITE', desc: 'The initial recon value' - description 'Loads an individual worker and tests it' + description 'Loads an individual worker and runs it' - man_page 'ronin-recon-test.1' + man_page 'ronin-recon-run-worker.1' # - # Runs the `ronin-recon test` command. + # Runs the `ronin-recon run-worker` command. # # @param [String, nil] name # The optional worker name to load and print metadata for. diff --git a/man/ronin-recon-irb.1.md b/man/ronin-recon-irb.1.md index 7dd572c4..2c704d15 100644 --- a/man/ronin-recon-irb.1.md +++ b/man/ronin-recon-irb.1.md @@ -23,4 +23,4 @@ Postmodern ## SEE ALSO -[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-worker](ronin-recon-worker.1.md) [ronin-recon-run](ronin-recon-run.1.md) [ronin-recon-test](ronin-recon-test.1.md) +[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-worker](ronin-recon-worker.1.md) [ronin-recon-run](ronin-recon-run.1.md) [ronin-recon-run-worker](ronin-recon-run-worker.1.md) diff --git a/man/ronin-recon-test.1.md b/man/ronin-recon-run-worker.1.md similarity index 69% rename from man/ronin-recon-test.1.md rename to man/ronin-recon-run-worker.1.md index 377a4d08..76ee04d0 100644 --- a/man/ronin-recon-test.1.md +++ b/man/ronin-recon-run-worker.1.md @@ -1,16 +1,16 @@ -# ronin-recon-test 1 "2023-05-01" Ronin "User Manuals" +# ronin-recon-run-worker 1 "2023-05-01" Ronin "User Manuals" ## NAME -ronin-recon-test - Loads an individual worker and tests it +ronin-recon-run-worker - Loads an individual worker and runs it ## SYNOPSIS -`ronin-recon test` [*options*] {`--file` *FILE* \| *NAME*} {*IP* \| *IP-range* \| *DOMAIN* \| *HOST* \| *WILDCARD* \| *WEBSITE*} +`ronin-recon run-worker` [*options*] {`--file` *FILE* \| *NAME*} {*IP* \| *IP-range* \| *DOMAIN* \| *HOST* \| *WILDCARD* \| *WEBSITE*} ## DESCRIPTION -Loads an individual worker and tests it with an input value.. +Loads an individual worker and runs it with an input value.. ## ARGUMENTS @@ -52,4 +52,4 @@ Postmodern ## SEE ALSO -[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-run](ronin-recon-run.1.md) \ No newline at end of file +[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-run](ronin-recon-run.1.md) diff --git a/man/ronin-recon-run.1.md b/man/ronin-recon-run.1.md index e0dc595b..91da395e 100644 --- a/man/ronin-recon-run.1.md +++ b/man/ronin-recon-run.1.md @@ -1,4 +1,4 @@ -# ronin-recon-test 1 "2023-05-01" Ronin "User Manuals" +# ronin-recon-run-worker 1 "2023-05-01" Ronin "User Manuals" ## NAME @@ -112,4 +112,4 @@ Postmodern ## SEE ALSO -[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-worker](ronin-recon-worker.1.md) [ronin-recon-test](ronin-recon-test.1.md) +[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-worker](ronin-recon-worker.1.md) [ronin-recon-run-worker](ronin-recon-run-worker.1.md) diff --git a/man/ronin-recon.1.md b/man/ronin-recon.1.md index e7429cc8..fa7410db 100644 --- a/man/ronin-recon.1.md +++ b/man/ronin-recon.1.md @@ -42,8 +42,8 @@ Runs a `ronin-recon` *COMMAND*. *run* : Runs the recon engine with one or more initial values. -*test* -: Loads an individual worker and tests it. +*run-worker*, *test* +: Loads an individual worker and runs it. *worker* : Prints information about a recon worker. diff --git a/spec/cli/commands/new_spec.rb b/spec/cli/commands/new_spec.rb index a95ee133..8f8fa5ff 100644 --- a/spec/cli/commands/new_spec.rb +++ b/spec/cli/commands/new_spec.rb @@ -78,7 +78,7 @@ it "must generate a new file containing a new Ronin::Recon::Worker class" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -131,7 +131,7 @@ def process(value) it "must add a boilerplate `author` metadata attribute" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -173,7 +173,7 @@ def process(value) it "must override the author name in the `author ...` metadata attribute with the '--author' name" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -214,7 +214,7 @@ def process(value) it "must override the author email in the `author ...` metadata attribute with the '--author-email' email" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -257,7 +257,7 @@ def process(value) it "must fill in the `summary ...` metadata attribute with the '--summary' text" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -299,7 +299,7 @@ def process(value) it "must fill in the `description ...` metadata attribute with the '--description' text" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -344,7 +344,7 @@ def process(value) it "must fill in the `references [...]` metadata attribute containing the '--reference' URLs" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -391,7 +391,7 @@ def process(value) it "must set the `accepts ...` metadata attribute in the worker class with the '--accepts' value classes" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -438,7 +438,7 @@ def process(value) it "must set the `outputs ...` metadata attribute in the worker class with the '--outputs' value classes" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -480,7 +480,7 @@ def process(value) it "must add the `intensity :level` metadata attribute to the worker class using the '--intensity' level" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' diff --git a/spec/cli/commands/test_spec.rb b/spec/cli/commands/run_worker_spec.rb similarity index 96% rename from spec/cli/commands/test_spec.rb rename to spec/cli/commands/run_worker_spec.rb index fc699534..39a5d8ce 100644 --- a/spec/cli/commands/test_spec.rb +++ b/spec/cli/commands/run_worker_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -require 'ronin/recon/cli/commands/test' +require 'ronin/recon/cli/commands/run_worker' require 'fixtures/test_worker' -describe Ronin::Recon::CLI::Commands::Test do +describe Ronin::Recon::CLI::Commands::RunWorker do describe "#run" do let(:name) { 'test_worker' } let(:value) { 'example.com' } From 682ab41f9e1d2ed1ddf182f0397f22f36ba6b9c5 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Wed, 21 Aug 2024 16:22:16 -0700 Subject: [PATCH 06/16] Upgraded to `ronin-core` ~> 0.3 for `command_kit` ~> 0.6 (closes #132). --- Gemfile | 4 ++-- README.md | 2 +- gemspec.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 85a4ea0f..8da80b3f 100644 --- a/Gemfile +++ b/Gemfile @@ -15,8 +15,8 @@ end # gem 'ronin-support', '~> 1.1', github: 'ronin-rb/ronin-support', # branch: 'main' -# gem 'ronin-core', '~> 0.2', github: 'ronin-rb/ronin-core', -# branch: 'main' +gem 'ronin-core', '~> 0.3', github: 'ronin-rb/ronin-core', + branch: '0.3.0' # gem 'ronin-repos', '~> 0.1', github: 'ronin-rb/ronin-repos', # branch: 'main' diff --git a/README.md b/README.md index 7ca8d316..3a11d7a2 100644 --- a/README.md +++ b/README.md @@ -313,7 +313,7 @@ end * [async-http] ~> 0.60 * [wordlist] ~> 1.0, >= 1.0.3 * [ronin-support] ~> 1.1 -* [ronin-core] ~> 0.2 +* [ronin-core] ~> 0.3 * [ronin-db] ~> 0.2 * [ronin-repos] ~> 0.1 * [ronin-nmap] ~> 0.1 diff --git a/gemspec.yml b/gemspec.yml index 9ad8aefb..5534f807 100644 --- a/gemspec.yml +++ b/gemspec.yml @@ -46,7 +46,7 @@ dependencies: wordlist: ~> 1.0, >= 1.0.3 # Ronin dependencies: ronin-support: ~> 1.1 - ronin-core: ~> 0.2 + ronin-core: ~> 0.3 ronin-db: ~> 0.2 ronin-repos: ~> 0.1 ronin-nmap: ~> 0.1 From 770ae497bd487c10427fb90fa9d38958678d3d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Bieniek?= Date: Sun, 25 Aug 2024 06:18:33 +0200 Subject: [PATCH 07/16] Add an optional worker for the `BuiltWith` API (#163) --------- Co-authored-by: Postmodern --- lib/ronin/recon/builtin/api/built_with.rb | 128 ++++++++++++++++++++++ spec/builtin/api/built_with_spec.rb | 79 +++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 lib/ronin/recon/builtin/api/built_with.rb create mode 100644 spec/builtin/api/built_with_spec.rb diff --git a/lib/ronin/recon/builtin/api/built_with.rb b/lib/ronin/recon/builtin/api/built_with.rb new file mode 100644 index 00000000..e98d4314 --- /dev/null +++ b/lib/ronin/recon/builtin/api/built_with.rb @@ -0,0 +1,128 @@ +# 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_relative '../../worker' + +require 'async/http/internet/instance' +require 'set' + +module Ronin + module Recon + module API + # + # A recon worker that queries https://api.builtwith.com and return + # informations for given domain + # + # ## Environment Variables + # + # * `BUILT_WITH_API_KEY` - Specifies the API key used for authorization. + # + class BuiltWith < Worker + + register 'api/built_with' + + summary "Queries the domain informations from https://api.builtwith.com" + description <<~DESC + Queriest the domain informations from https://api.builtwith.com. + + The BuiltWith API key can be specified via the api/built_with.api_key + param or the BUILT_WITH_API_KEY environment variables. + DESC + + accepts Domain + outputs Domain, EmailAddress + intensity :passive + concurrency 1 + + param :api_key, String, required: true, + default: ENV['BUILT_WITH_API_KEY'], + desc: 'The API key for BuiltWith' + + # + # The HTTP client for `https://api.builtwith.com` + # + # @return [Async::HTTP::Client] + # + # @api private + # + attr_reader :client + + # + # Initializes the `api/built_with` worker. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments. + # + # @api private + # + def initialize(**kwargs) + super(**kwargs) + + @client = Async::HTTP::Client.new( + Async::HTTP::Endpoint.for('https', 'api.builtwith.com') + ) + end + + # + # Returns all informations queried for given domain + # + # @param [Values::Domain] domain + # The domain value to gather informations for. + # + # @yield [Value] value + # The found value will be yielded + # + # @yieldparam [Values::Domain, Values::EmailAddress] + # The found domains or email addresses + # + def process(domain) + path = "/v21/api.json?KEY=#{params[:api_key]}&LOOKUP=#{domain}" + response = client.get(path) + body = begin + JSON.parse(response.read, symbolize_names: true) + ensure + response.close + end + + domains = Set.new + email_addresses = Set.new + + body.fetch(:Results, []).each do |results| + paths = results.fetch(:Result, {}).fetch(:Paths, []) + + paths.each do |result_path| + if (sub_domain = result_path[:SubDomain]) + new_domain = "#{sub_domain}.#{domain}" + + yield Domain.new(new_domain) if domains.add?(new_domain) + end + end + + emails = results.fetch(:Meta, {}).fetch(:Emails, []) + + emails.each do |email| + yield EmailAddress.new(email) if email_addresses.add?(email) + end + end + end + end + end + end +end diff --git a/spec/builtin/api/built_with_spec.rb b/spec/builtin/api/built_with_spec.rb new file mode 100644 index 00000000..2eb235d5 --- /dev/null +++ b/spec/builtin/api/built_with_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' +require 'ronin/recon/builtin/api/built_with' +require 'webmock/rspec' + +describe Ronin::Recon::API::BuiltWith do + let(:api_key) { 'my-test-api-key' } + + subject { described_class.new(params: { api_key: api_key }) } + + it "must set concurrency to 1" do + expect(described_class.concurrency).to eq(1) + end + + describe "#process" do + context "for domain with subdomains" do + let(:domain) { Ronin::Recon::Values::Domain.new("example.com") } + let(:response_json) do + "{\"Results\":[{\"Result\":{\"Paths\":[{\"Domain\":\"example.com\",\"SubDomain\":\"api\"},{\"Domain\":\"example.com\",\"SubDomain\":\"test\"}]}}]}" + end + let(:expected) do + %w[ + api.example.com + test.example.com + ] + end + + before do + stub_request(:get, "https://api.builtwith.com/v21/api.json?KEY=#{api_key}&LOOKUP=#{domain}") + .to_return(status: 200, body: response_json) + end + + it "must yield Values::Domain for each subdomain" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to_not be_empty + expect(yielded_values).to all(be_kind_of(Ronin::Recon::Values::Domain)) + expect(yielded_values.map(&:name)).to eq(expected) + end + end + + context "for email addresses found on the lookup website" do + let(:domain) { Ronin::Recon::Values::Domain.new("example.com") } + let(:response_json) do + "{\"Results\":[{\"Meta\":{\"Emails\":[\"email@example.com\",\"test@example.com\"]}}]}" + end + let(:expected) do + %w[ + email@example.com + test@example.com + ] + end + + before do + stub_request(:get, "https://api.builtwith.com/v21/api.json?KEY=#{api_key}&LOOKUP=#{domain}") + .to_return(status: 200, body: response_json) + end + + it "must yield Values::EmailAddress for each email address" do + yielded_values = [] + + Async do + subject.process(domain) do |email| + yielded_values << email + end + end + + expect(yielded_values).to_not be_empty + expect(yielded_values).to all(be_kind_of(Ronin::Recon::Values::EmailAddress)) + expect(yielded_values.map(&:address)).to eq(expected) + end + end + end +end From 858a1466a703e921581acc7fdfb7a9e0b20ce208 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Wed, 21 Aug 2024 05:13:54 -0700 Subject: [PATCH 08/16] Fixed `ronin-recon new` example commands. --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3a11d7a2..0c30565f 100644 --- a/README.md +++ b/README.md @@ -219,8 +219,7 @@ Generate a boilerplate recon worker file, with some custom information: ```shell $ ronin-recon new example_worker.rb \ - --name Example \ - --authors Postmodern \ + --author Postmodern \ --description "This is an example." ``` @@ -231,8 +230,7 @@ $ ronin-repos new my-repo $ cd my-repo/ $ mkdir recon $ ronin-recon new recon/my_recon.rb \ - --name MyRecon \ - --authors You \ + --author You \ --description "This is my payload." $ vim recon/my_recon.rb $ git add recon/my_recon.rb From 8fdfd4fffa397c2accb0b51bf5e932a2c97e9a76 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Wed, 21 Aug 2024 05:21:09 -0700 Subject: [PATCH 09/16] Fixed copy/pasted README examples. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0c30565f..32f3d4a7 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ $ ronin-recon new example_worker.rb \ --description "This is an example." ``` -Generate a ronin repository of your own payloads (or exploits): +Generate a ronin repository of your own recon workers: ```shell $ ronin-repos new my-repo @@ -231,7 +231,7 @@ $ cd my-repo/ $ mkdir recon $ ronin-recon new recon/my_recon.rb \ --author You \ - --description "This is my payload." + --description "This is my recon worker." $ vim recon/my_recon.rb $ git add recon/my_recon.rb $ git commit From 3f50e4f62ebf681c5f2861b5d69f58cb535ea7fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Bieniek?= Date: Sun, 25 Aug 2024 06:23:25 +0200 Subject: [PATCH 10/16] Add an optional worker for the `ZoomEye` API (#166) --------- Co-authored-by: Postmodern --- lib/ronin/recon/builtin/api/zoom_eye.rb | 122 ++++++++++++++++++++++++ spec/builtin/api/zoom_eye_spec.rb | 77 +++++++++++++++ 2 files changed, 199 insertions(+) create mode 100755 lib/ronin/recon/builtin/api/zoom_eye.rb create mode 100644 spec/builtin/api/zoom_eye_spec.rb diff --git a/lib/ronin/recon/builtin/api/zoom_eye.rb b/lib/ronin/recon/builtin/api/zoom_eye.rb new file mode 100755 index 00000000..87815226 --- /dev/null +++ b/lib/ronin/recon/builtin/api/zoom_eye.rb @@ -0,0 +1,122 @@ +# 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_relative '../../worker' + +require 'async/http/internet' + +module Ronin + module Recon + module API + # + # A recon worker that queries https://api.zoomeye.hk/domain/search + # and returns subdomain and ip addresses for a given domain + # + # ## Environment Variables + # + # * `ZOOM_EYE_API_KEY` - Specifies the API key used for authorization. + # + class ZoomEye < Worker + + register 'api/zoom_eye' + + summary "Queries the Domains https://api.zoomeye.hk API" + description <<~DESC + Queries the Domains https://api.zoomeye.hk API and returns subdomains + and ip addresses of the domain. + + The ZoomEye API key can be specified via the api/zoom_eye.api_key + param or the ZOOM_EYE_API_KEY env variables. + DESC + + accepts Domain + outputs Domain, IP + intensity :passive + concurrency 1 + + param :api_key, String, required: true, + default: ENV['ZOOM_EYE_API_KEY'], + desc: 'The API key for ZoomEye' + + # The HTTP client for `https://api.zoomeye.hk`. + # + # @return [Async::HTTP::Client] + # + # @api private + # + attr_reader :client + + # + # Initialize the `api/zoom_eye` worker. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments. + # + # @api private + # + def initialize(**kwargs) + super(**kwargs) + + @client = Async::HTTP::Client.new( + Async::HTTP::Endpoint.for('https', 'api.zoomeye.hk') + ) + end + + # + # Returns associated domain names and ip addresses + # + # @param [Values::Domain] domain + # The domain value to gather subdomains and ip_addresses for. + # + # @yield [value] + # For each subdomain found through the API, a Domain + # and optionaly IP will be yielded. + # + # @yieldparam [Values::Domain, Values::IP] value + # The domain or ip found. + # + def process(domain) + path = "/domain/search?q=#{domain}&type=1" + response = @client.get(path, { 'API-KEY' => params[:api_key] }) + body = begin + JSON.parse(response.read, symbolize_names: true) + ensure + response.close + end + + list = body.fetch(:list, []) + + list.each do |record| + if (subdomain = record[:name]) + yield Domain.new(subdomain) + end + + ip_addresses = record.fetch(:ip, []) + + ip_addresses.each do |ip_addr| + yield IP.new(ip_addr) + end + end + end + + end + end + end +end diff --git a/spec/builtin/api/zoom_eye_spec.rb b/spec/builtin/api/zoom_eye_spec.rb new file mode 100644 index 00000000..f44dc952 --- /dev/null +++ b/spec/builtin/api/zoom_eye_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' +require 'ronin/recon/builtin/api/zoom_eye' + +require 'webmock/rspec' + +describe Ronin::Recon::API::ZoomEye do + let(:api_key) { 'my-test-api-key' } + + subject { described_class.new(params: { api_key: api_key }) } + + describe "#initialize" do + it "must initialize #client for 'https://api.zoomeye.hk'" do + expect(subject.client).to be_kind_of(Async::HTTP::Client) + end + end + + describe "#process" do + context "for domain with subdomains and ip_addresses" do + let(:domain) { Ronin::Recon::Values::Domain.new("example.com") } + let(:response_json) do + "{\"status\":200,\"total\":183386,\"list\":[{\"name\":\"api.example.com\",\"ip\":[\"1.1.1.1\"]},{\"name\":\"test.example.com\",\"ip\":[\"2.2.2.2\"]}]}" + end + let(:expected) do + [ + Ronin::Recon::Values::Domain.new('api.example.com'), + Ronin::Recon::Values::Domain.new('test.example.com'), + Ronin::Recon::Values::IP.new('1.1.1.1'), + Ronin::Recon::Values::IP.new('2.2.2.2') + ] + end + + before do + stub_request(:get, "https://api.zoomeye.hk/domain/search?q=#{domain}&type=1") + .with(headers: { "API-KEY" => 'my-test-api-key' }) + .to_return(status: 200, body: response_json) + end + + it "must yield Values::Domain and Values::IP for each subdomain" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to_not be_empty + expect(yielded_values).to match_array(expected) + end + end + + context "for domain with no subdomains" do + let(:domain) { Ronin::Recon::Values::Domain.new("invalid.com") } + let(:response_json) do + "{\"status\":200,\"total\":183386,\"list\":[]}" + end + + before do + stub_request(:get, "https://api.zoomeye.hk/domain/search?q=#{domain}&type=1") + .with(headers: { "API-KEY" => 'my-test-api-key' }) + .to_return(status: 200, body: response_json) + end + + it "must not yield anything" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to be_empty + end + end + end +end From 434e18bebc9026aa56e1efe89c056fc8481783c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Bieniek?= Date: Sun, 25 Aug 2024 06:26:32 +0200 Subject: [PATCH 11/16] Add an optional worker for the `hunter.io` API (#167) --------- Co-authored-by: Postmodern --- lib/ronin/recon/builtin/api/hunter_io.rb | 115 +++++++++++++++++++++++ spec/builtin/api/hunter_io_spec.rb | 74 +++++++++++++++ 2 files changed, 189 insertions(+) create mode 100755 lib/ronin/recon/builtin/api/hunter_io.rb create mode 100644 spec/builtin/api/hunter_io_spec.rb diff --git a/lib/ronin/recon/builtin/api/hunter_io.rb b/lib/ronin/recon/builtin/api/hunter_io.rb new file mode 100755 index 00000000..d6b6eb53 --- /dev/null +++ b/lib/ronin/recon/builtin/api/hunter_io.rb @@ -0,0 +1,115 @@ +# 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_relative '../../worker' + +require 'async/http/internet' + +module Ronin + module Recon + module API + # + # A recon worker that queries https://api.hunter.io/domain-search + # and returns corresponding email addresses. + # + # ## Environment Variables + # + # * `HUNTER_IO_API_KEY` - Specifies the API key used for authorization. + # + class HunterIO < Worker + + register 'api/hunter_io' + + summary "Queries the Domains https://api.hunter.io/domain-search" + description <<~DESC + Queries the Domains https://api.hunter.io/domain-search and returns + corresponding email addresses. + + The hunter.io API key can be specified via the api/hunter_io.api_key + param or the HUNTER_IO_API_KEY env variables. + DESC + + accepts Domain + outputs EmailAddress + intensity :passive + concurrency 1 + + param :api_key, String, required: true, + default: ENV['HUNTER_IO_API_KEY'], + desc: 'The API key for hunter.io' + + # The HTTP client for `https://api.hunter.io`. + # + # @return [Async::HTTP::Client] + # + # @api private + attr_reader :client + + # + # Initializes the `api/hunter` worker. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments. + # + # @api private + # + def initialize(**kwargs) + super(**kwargs) + + @client = Async::HTTP::Client.new( + Async::HTTP::Endpoint.for('https', 'api.hunter.io') + ) + end + + # + # Returns email addresses corresponding to domain." + # + # @param [Values::Domain] domain + # The domain value to gather email addresses for. + # + # @yield [email] + # For each email address found through the API, a EmailAddress + # value will be yielded. + # + # @yieldparam [Values::EmailAddress] email_address + # The emial addresses found. + # + def process(domain) + path = "/v2/domain-search?domain=#{domain}&api_key=#{params[:api_key]}" + response = @client.get(path) + body = begin + JSON.parse(response.read, symbolize_names: true) + ensure + response.close + end + + emails = body.fetch(:data, {}).fetch(:emails, []) + + emails.each do |email| + if (email_addr = email[:value]) + yield EmailAddress.new(email_addr) + end + end + end + + end + end + end +end diff --git a/spec/builtin/api/hunter_io_spec.rb b/spec/builtin/api/hunter_io_spec.rb new file mode 100644 index 00000000..3b9458d6 --- /dev/null +++ b/spec/builtin/api/hunter_io_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' +require 'ronin/recon/builtin/api/hunter_io' + +require 'webmock/rspec' + +describe Ronin::Recon::API::HunterIO do + let(:api_key) { 'my-test-api-key' } + + subject { described_class.new(params: { api_key: api_key }) } + + describe "#initialize" do + it "must initialize #client for 'https://api.hunter.io'" do + expect(subject.client).to be_kind_of(Async::HTTP::Client) + end + end + + describe "#process" do + context "for domain with corresponding email addresses" do + let(:domain) { Ronin::Recon::Values::Domain.new("example.com") } + let(:response_json) do + "{\"data\":{\"emails\":[{\"value\":\"foo@example.com\"},{\"value\":\"bar@example.com\"}]}}" + end + let(:expected) do + %w[ + foo@example.com + bar@example.com + ] + end + + before do + stub_request(:get, "https://api.hunter.io/v2/domain-search?domain=#{domain}&api_key=#{api_key}") + .to_return(status: 200, body: response_json) + end + + it "must yield Values::EmailAddress for each subdomain" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to_not be_empty + expect(yielded_values).to all(be_kind_of(Ronin::Recon::Values::EmailAddress)) + expect(yielded_values.map(&:address)).to eq(expected) + end + end + + context "for domain with no email addresses" do + let(:domain) { Ronin::Recon::Values::Domain.new("invalid.com") } + let(:response_json) do + "{\"data\":{\"emails\":[]}}" + end + + before do + stub_request(:get, "https://api.hunter.io/v2/domain-search?domain=#{domain}&api_key=#{api_key}") + .to_return(status: 200, body: response_json) + end + + it "must not yield anything" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to be_empty + end + end + end +end From d382c892f3e317c81bae17beb15115a159baabf2 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Sun, 25 Aug 2024 00:11:47 -0700 Subject: [PATCH 12/16] Avoid creating empty Hashes and Arrays. --- lib/ronin/recon/builtin/api/built_with.rb | 28 ++++++++++--------- lib/ronin/recon/builtin/api/hunter_io.rb | 10 +++---- .../recon/builtin/api/security_trails.rb | 9 +++--- lib/ronin/recon/builtin/api/zoom_eye.rb | 22 +++++++-------- 4 files changed, 36 insertions(+), 33 deletions(-) diff --git a/lib/ronin/recon/builtin/api/built_with.rb b/lib/ronin/recon/builtin/api/built_with.rb index e98d4314..74d28176 100644 --- a/lib/ronin/recon/builtin/api/built_with.rb +++ b/lib/ronin/recon/builtin/api/built_with.rb @@ -104,21 +104,23 @@ def process(domain) domains = Set.new email_addresses = Set.new - body.fetch(:Results, []).each do |results| - paths = results.fetch(:Result, {}).fetch(:Paths, []) - - paths.each do |result_path| - if (sub_domain = result_path[:SubDomain]) - new_domain = "#{sub_domain}.#{domain}" - - yield Domain.new(new_domain) if domains.add?(new_domain) + if (results = body[:Results]) + results.each do |result| + if (paths = result.dig(:Result, :Paths)) + paths.each do |result_path| + if (sub_domain = result_path[:SubDomain]) + new_domain = "#{sub_domain}.#{domain}" + + yield Domain.new(new_domain) if domains.add?(new_domain) + end + end end - end - emails = results.fetch(:Meta, {}).fetch(:Emails, []) - - emails.each do |email| - yield EmailAddress.new(email) if email_addresses.add?(email) + if (emails = result.dig(:Meta, :Emails)) + emails.each do |email| + yield EmailAddress.new(email) if email_addresses.add?(email) + end + end end end end diff --git a/lib/ronin/recon/builtin/api/hunter_io.rb b/lib/ronin/recon/builtin/api/hunter_io.rb index d6b6eb53..2812369c 100755 --- a/lib/ronin/recon/builtin/api/hunter_io.rb +++ b/lib/ronin/recon/builtin/api/hunter_io.rb @@ -100,11 +100,11 @@ def process(domain) response.close end - emails = body.fetch(:data, {}).fetch(:emails, []) - - emails.each do |email| - if (email_addr = email[:value]) - yield EmailAddress.new(email_addr) + if (emails = body.dig(:data, :emails)) + emails.each do |email| + if (email_addr = email[:value]) + yield EmailAddress.new(email_addr) + end end end end diff --git a/lib/ronin/recon/builtin/api/security_trails.rb b/lib/ronin/recon/builtin/api/security_trails.rb index 88a57876..5863a5f9 100755 --- a/lib/ronin/recon/builtin/api/security_trails.rb +++ b/lib/ronin/recon/builtin/api/security_trails.rb @@ -97,13 +97,14 @@ def process(domain) ensure response.close end - subdomains = body.fetch(:subdomains, []) full_domains = Set.new - subdomains.each do |subdomain| - full_domain = "#{subdomain}.#{domain}" + if (subdomains = body.fetch(:subdomains)) + subdomains.each do |subdomain| + full_domain = "#{subdomain}.#{domain}" - yield Host.new(full_domain) if full_domains.add?(full_domain) + yield Host.new(full_domain) if full_domains.add?(full_domain) + end end end diff --git a/lib/ronin/recon/builtin/api/zoom_eye.rb b/lib/ronin/recon/builtin/api/zoom_eye.rb index 87815226..aca3397b 100755 --- a/lib/ronin/recon/builtin/api/zoom_eye.rb +++ b/lib/ronin/recon/builtin/api/zoom_eye.rb @@ -101,17 +101,17 @@ def process(domain) response.close end - list = body.fetch(:list, []) - - list.each do |record| - if (subdomain = record[:name]) - yield Domain.new(subdomain) - end - - ip_addresses = record.fetch(:ip, []) - - ip_addresses.each do |ip_addr| - yield IP.new(ip_addr) + if (list = body[:list]) + list.each do |record| + if (subdomain = record[:name]) + yield Domain.new(subdomain) + end + + if (ip_addresses = record[:ip]) + ip_addresses.each do |ip_addr| + yield IP.new(ip_addr) + end + end end end end From 23380109d35b1ad129cd3b69c1f6126941132582 Mon Sep 17 00:00:00 2001 From: Postmodern Date: Sun, 25 Aug 2024 00:14:18 -0700 Subject: [PATCH 13/16] Stylistic fixes. --- lib/ronin/recon/builtin/api/built_with.rb | 3 +-- lib/ronin/recon/builtin/api/hunter_io.rb | 8 ++++---- lib/ronin/recon/builtin/api/security_trails.rb | 2 -- lib/ronin/recon/builtin/api/zoom_eye.rb | 1 - 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/ronin/recon/builtin/api/built_with.rb b/lib/ronin/recon/builtin/api/built_with.rb index 74d28176..f6baaa49 100644 --- a/lib/ronin/recon/builtin/api/built_with.rb +++ b/lib/ronin/recon/builtin/api/built_with.rb @@ -55,13 +55,11 @@ class BuiltWith < Worker default: ENV['BUILT_WITH_API_KEY'], desc: 'The API key for BuiltWith' - # # The HTTP client for `https://api.builtwith.com` # # @return [Async::HTTP::Client] # # @api private - # attr_reader :client # @@ -124,6 +122,7 @@ def process(domain) end end end + end end end diff --git a/lib/ronin/recon/builtin/api/hunter_io.rb b/lib/ronin/recon/builtin/api/hunter_io.rb index 2812369c..234188f8 100755 --- a/lib/ronin/recon/builtin/api/hunter_io.rb +++ b/lib/ronin/recon/builtin/api/hunter_io.rb @@ -95,10 +95,10 @@ def process(domain) path = "/v2/domain-search?domain=#{domain}&api_key=#{params[:api_key]}" response = @client.get(path) body = begin - JSON.parse(response.read, symbolize_names: true) - ensure - response.close - end + JSON.parse(response.read, symbolize_names: true) + ensure + response.close + end if (emails = body.dig(:data, :emails)) emails.each do |email| diff --git a/lib/ronin/recon/builtin/api/security_trails.rb b/lib/ronin/recon/builtin/api/security_trails.rb index 5863a5f9..debaad1c 100755 --- a/lib/ronin/recon/builtin/api/security_trails.rb +++ b/lib/ronin/recon/builtin/api/security_trails.rb @@ -51,13 +51,11 @@ class SecurityTrails < Worker default: ENV['SECURITYTRAILS_API_KEY'], desc: 'The API key for SecurityTrails' - # # The HTTP client for `https://securitytrails.com`. # # @return [Async::HTTP::Client] # # @api private - # attr_reader :client # diff --git a/lib/ronin/recon/builtin/api/zoom_eye.rb b/lib/ronin/recon/builtin/api/zoom_eye.rb index aca3397b..30e43367 100755 --- a/lib/ronin/recon/builtin/api/zoom_eye.rb +++ b/lib/ronin/recon/builtin/api/zoom_eye.rb @@ -60,7 +60,6 @@ class ZoomEye < Worker # @return [Async::HTTP::Client] # # @api private - # attr_reader :client # From fc4ba5b71d47ac96f793a81166bc9590ddcbb2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=82osz=20Bieniek?= Date: Wed, 28 Aug 2024 23:13:33 +0200 Subject: [PATCH 14/16] Add a `Values::URL#===` method (#164) --------- Co-authored-by: Postmodern --- lib/ronin/recon/values/url.rb | 21 +++++++++++++++++++++ spec/values/url_spec.rb | 31 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/lib/ronin/recon/values/url.rb b/lib/ronin/recon/values/url.rb index 9eb8f054..d8230230 100644 --- a/lib/ronin/recon/values/url.rb +++ b/lib/ronin/recon/values/url.rb @@ -196,6 +196,27 @@ def as_json return hash end + # + # Case equality method used for fuzzy matching. + # + # @param [URL, Value] other + # The other value to compare. + # + # @return [Boolean] + # Indicates whether the other value is an URL with + # the same uri. + # + # @since 0.3.0 + # + def ===(other) + case other + when URL + @uri == other.uri + else + false + end + end + # # Returns the type or kind of recon value. # diff --git a/spec/values/url_spec.rb b/spec/values/url_spec.rb index 0930fead..3eb39e5e 100644 --- a/spec/values/url_spec.rb +++ b/spec/values/url_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' require 'ronin/recon/values/url' +require 'ronin/recon/values/domain' describe Ronin::Recon::Values::URL do let(:url) { 'https://www.example.com/index.html' } @@ -234,4 +235,34 @@ expect(subject.value_type).to be(:url) end end + + describe "#===" do + let(:url) { 'https://www.foo.example.com/index.html' } + + context "when given an URL object" do + context "and it has the same uri as the other URL value" do + let(:other) { described_class.new(url) } + + it "must return true" do + expect(subject === other).to be(true) + end + end + + context "but it has diffferent uri than the other URL value" do + let(:other) { described_class.new('https://www.example.net/index.html') } + + it "must return false" do + expect(subject === other).to be(false) + end + end + end + + context "when given non-URL object" do + let(:other) { Ronin::Recon::Values::Domain.new('example.com') } + + it "must return false" do + expect(subject === other).to be(false) + end + end + end end From b63b58346b4dbcf5a41a64800913c8b06857d777 Mon Sep 17 00:00:00 2001 From: AI-Mozi Date: Sun, 18 Aug 2024 16:44:11 +0200 Subject: [PATCH 15/16] Add support for multiple output files --- lib/ronin/recon/cli/commands/run.rb | 21 +++++++++++++-------- spec/cli/commands/run_spec.rb | 4 +++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/ronin/recon/cli/commands/run.rb b/lib/ronin/recon/cli/commands/run.rb index 21dc32c5..28d581bc 100644 --- a/lib/ronin/recon/cli/commands/run.rb +++ b/lib/ronin/recon/cli/commands/run.rb @@ -154,8 +154,7 @@ class Run < Command usage: 'FILE' }, desc: 'The output file to write results to' do |path| - options[:output] = path - options[:output_format] ||= OutputFormats.infer_from(path) + @outputs << [path, options[:output_format] || OutputFormats.infer_from(path)] end option :output_format, short: '-F', @@ -229,6 +228,11 @@ class Run < Command # @return [Array] attr_reader :ignore + # The output files and formats + # + # @return [Array<(String, Class)>] + attr_reader :outputs + # # Initializes the `ronin-recon run` command. # @@ -246,7 +250,8 @@ def initialize(**kwargs) @worker_params = Hash.new { |hash,key| hash[key] = {} } @worker_concurrency = {} - @ignore = [] + @ignore = [] + @outputs = [] end # @@ -261,9 +266,9 @@ def run(*values) values = values.map { |value| parse_value(value) } - output_file = if options[:output] && options[:output_format] - options[:output_format].open(options[:output]) - end + output_files = outputs.filter_map do |output, output_format| + output_format.open(output) + end if options[:import] require 'ronin/db' @@ -280,7 +285,7 @@ def run(*values) print_value(value,parent) end - if output_file + output_files.each do |output_file| engine.on(:value) do |value| output_file << value end @@ -308,7 +313,7 @@ def run(*values) end end ensure - output_file.close if options[:output] + output_files&.each(&:close) end end diff --git a/spec/cli/commands/run_spec.rb b/spec/cli/commands/run_spec.rb index 1f3a7358..4febdc8f 100644 --- a/spec/cli/commands/run_spec.rb +++ b/spec/cli/commands/run_spec.rb @@ -147,7 +147,9 @@ end it "must set the :output_format option using the path's file extension" do - expect(subject.options[:output_format]).to be(Ronin::Core::OutputFormats::JSON) + expect(subject.outputs.size).to eq(1) + expect(subject.outputs[0][0]).to eq(path) + expect(subject.outputs[0][1]).to be(Ronin::Core::OutputFormats::JSON) end context "but the '--output-format' has already been specified" do From 8d627999ccd7f6e6f50995c25fa336effc113231 Mon Sep 17 00:00:00 2001 From: AI-Mozi Date: Fri, 30 Aug 2024 15:58:03 +0200 Subject: [PATCH 16/16] Add `Archive` and `GitArchive` output formats --- lib/ronin/recon/output_formats.rb | 22 ++++--- lib/ronin/recon/output_formats/archive.rb | 56 ++++++++++++++++++ lib/ronin/recon/output_formats/git_archive.rb | 57 +++++++++++++++++++ spec/output_formats/archive_spec.rb | 34 +++++++++++ spec/output_formats/git_archive_spec.rb | 34 +++++++++++ 5 files changed, 194 insertions(+), 9 deletions(-) create mode 100644 lib/ronin/recon/output_formats/archive.rb create mode 100644 lib/ronin/recon/output_formats/git_archive.rb create mode 100644 spec/output_formats/archive_spec.rb create mode 100644 spec/output_formats/git_archive_spec.rb diff --git a/lib/ronin/recon/output_formats.rb b/lib/ronin/recon/output_formats.rb index 823910e4..c8ef06f7 100644 --- a/lib/ronin/recon/output_formats.rb +++ b/lib/ronin/recon/output_formats.rb @@ -23,6 +23,8 @@ require_relative 'output_formats/svg' require_relative 'output_formats/png' require_relative 'output_formats/pdf' +require_relative 'output_formats/archive' +require_relative 'output_formats/git_archive' require 'ronin/core/output_formats' @@ -35,15 +37,17 @@ module Recon module OutputFormats include Core::OutputFormats - register :txt, '.txt', TXT - register :csv, '.csv', CSV - register :json, '.json', JSON - register :ndjson, '.ndjson', NDJSON - register :dir, '', Dir - register :dot, '.dot', Dot - register :svg, '.svg', SVG - register :png, '.png', PNG - register :pdf, '.pdf', PDF + register :txt, '.txt', TXT + register :csv, '.csv', CSV + register :json, '.json', JSON + register :ndjson, '.ndjson', NDJSON + register :dir, '', Dir + register :dot, '.dot', Dot + register :svg, '.svg', SVG + register :png, '.png', PNG + register :pdf, '.pdf', PDF + register :web_archive, '', Archive + register :web_git_archive, '', GitArchive end end end diff --git a/lib/ronin/recon/output_formats/archive.rb b/lib/ronin/recon/output_formats/archive.rb new file mode 100644 index 00000000..f4cbe881 --- /dev/null +++ b/lib/ronin/recon/output_formats/archive.rb @@ -0,0 +1,56 @@ +# 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/web/spider/archive' + +module Ronin + module Recon + module OutputFormats + # + # Represents a web archive directory. + # + class Archive + + # + # Initializes new archive. + # + # @param [String] root + # The path to the root directory. + # + def initialize(root) + @archive = Ronin::Web::Spider::Archive.new(root) + end + + # + # Writes a new URL to it's specific file. + # + # @param [Value] value + # The value to write. + # + def <<(value) + if Values::URL === value + @archive.write(value.uri, value.body) + end + end + + end + end + end +end diff --git a/lib/ronin/recon/output_formats/git_archive.rb b/lib/ronin/recon/output_formats/git_archive.rb new file mode 100644 index 00000000..02d78575 --- /dev/null +++ b/lib/ronin/recon/output_formats/git_archive.rb @@ -0,0 +1,57 @@ +# 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/web/spider/git_archive' + +module Ronin + module Recon + module OutputFormats + # + # Represents a web archive directory that is backed by Git. + # + class GitArchive + + # + # Initializes new Git repository. + # + # @param [String] root + # The path to the root directory. + # + def initialize(root) + @git_archive = Ronin::Web::Spider::GitArchive.new(root) + @git_archive.init unless @git_archive.git? + end + + # + # Writes a new URL to it's specific file in Git archive. + # + # @param [Value] value + # The value to write. + # + def <<(value) + if Values::URL === value + @git_archive.write(value.uri, value.body) + end + end + + end + end + end +end diff --git a/spec/output_formats/archive_spec.rb b/spec/output_formats/archive_spec.rb new file mode 100644 index 00000000..86947862 --- /dev/null +++ b/spec/output_formats/archive_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'ronin/recon/output_formats/archive' +require 'ronin/recon/values/url' +require 'ronin/recon/values/domain' +require 'tmpdir' + +describe Ronin::Recon::OutputFormats::Archive do + subject { described_class.new(path) } + + let(:path) { Dir.mktmpdir('ronin-recon-output-archive') } + + describe "#<<" do + context "for Values::URL" do + let(:value) { Ronin::Recon::Values::URL.new('https://www.example.com/foo.html') } + let(:expected_path) { File.join(path,value.path) } + + it "must create a new file with webpage" do + subject << value + + expect(File.exist?(expected_path)).to be(true) + end + end + + context "for other values" do + let(:value) { Ronin::Recon::Values::Domain.new('example.com') } + + it "must not create any files" do + subject << value + + expect(Dir.glob("#{path}/*")).to be_empty + end + end + end +end diff --git a/spec/output_formats/git_archive_spec.rb b/spec/output_formats/git_archive_spec.rb new file mode 100644 index 00000000..1ec39674 --- /dev/null +++ b/spec/output_formats/git_archive_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'ronin/recon/output_formats/git_archive' +require 'ronin/recon/values/url' +require 'ronin/recon/values/domain' +require 'tmpdir' + +describe Ronin::Recon::OutputFormats::GitArchive do + subject { described_class.new(path) } + + let(:path) { Dir.mktmpdir('ronin-recon-output-git-archive') } + + describe "#<<" do + context "for Values::URL" do + let(:value) { Ronin::Recon::Values::URL.new('https://www.example.com/foo.html') } + let(:expected_path) { File.join(path,value.path) } + + it "must create a new file with webpage" do + subject << value + + expect(File.exist?(expected_path)).to be(true) + end + end + + context "for other values" do + let(:value) { Ronin::Recon::Values::Domain.new('example.com') } + + it "must not create any files" do + subject << value + + expect(Dir.glob("#{path}/*")).to be_empty + end + end + end +end