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 3d79441c..32f3d4a7 100644
--- a/README.md
+++ b/README.md
@@ -78,7 +78,7 @@ Commands:
irb
new
run
- test
+ run-worker, test
worker
workers
```
@@ -219,21 +219,19 @@ 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."
```
-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
$ cd my-repo/
$ mkdir recon
$ ronin-recon new recon/my_recon.rb \
- --name MyRecon \
- --authors You \
- --description "This is my payload."
+ --author You \
+ --description "This is my recon worker."
$ vim recon/my_recon.rb
$ git add recon/my_recon.rb
$ git commit
@@ -313,7 +311,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/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..5534f807 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
@@ -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
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..f6baaa49
--- /dev/null
+++ b/lib/ronin/recon/builtin/api/built_with.rb
@@ -0,0 +1,129 @@
+# 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
+
+ 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
+
+ if (emails = result.dig(:Meta, :Emails))
+ emails.each do |email|
+ yield EmailAddress.new(email) if email_addresses.add?(email)
+ end
+ end
+ end
+ 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
new file mode 100755
index 00000000..234188f8
--- /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
+
+ if (emails = body.dig(:data, :emails))
+ emails.each do |email|
+ if (email_addr = email[:value])
+ yield EmailAddress.new(email_addr)
+ end
+ end
+ end
+ end
+
+ end
+ end
+ end
+end
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..debaad1c
--- /dev/null
+++ b/lib/ronin/recon/builtin/api/security_trails.rb
@@ -0,0 +1,112 @@
+# 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://securitytrails.com`.
+ #
+ # @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
+ full_domains = Set.new
+
+ if (subdomains = body.fetch(:subdomains))
+ 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
+end
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..30e43367
--- /dev/null
+++ b/lib/ronin/recon/builtin/api/zoom_eye.rb
@@ -0,0 +1,121 @@
+# 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
+
+ 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
+
+ end
+ end
+ end
+end
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/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/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/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/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/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/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
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/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/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
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
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
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
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/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
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' }
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/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
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
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