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..95dddd91 100644
--- a/README.md
+++ b/README.md
@@ -74,11 +74,12 @@ Arguments:
Commands:
completion
+ config
help
irb
new
run
- test
+ run-worker, test
worker
workers
```
@@ -87,22 +88,27 @@ List all available recon workers:
```shell
$ ronin-recon workers
- api/crt_sh
- dns/lookup
- dns/mailservers
- dns/nameservers
- dns/reverse_lookup
- dns/srv_enum
- dns/subdomain_enum
- dns/suffix_enum
- net/cert_enum
- net/cert_grab
- net/ip_range_enum
- net/port_scan
- net/service_id
- web/dir_enum
- web/email_addresses
- web/spider
+ api/built_with
+ api/crt_sh
+ api/hunter_io
+ api/security_trails
+ api/zoom_eye
+ dns/lookup [enabled]
+ dns/mailservers [enabled]
+ dns/nameservers [enabled]
+ dns/reverse_lookup [enabled]
+ dns/srv_enum [enabled]
+ dns/subdomain_enum [enabled]
+ dns/suffix_enum [enabled]
+ net/ip_range_enum [enabled]
+ net/port_scan [enabled]
+ net/service_id [enabled]
+ ssl/cert_enum [enabled]
+ ssl/cert_grab [enabled]
+ web/dir_enum [enabled]
+ web/email_addresses [enabled]
+ web/screenshot
+ web/spider [enabled]
```
Print info about a specific recon worker:
@@ -215,25 +221,41 @@ Save the recon results to a PDF image:
$ ronin-recon run -o output.pdf example.com
```
+Enable an optional worker by default:
+
+```shell
+$ ronin-recon config enable api/hunter_io
+```
+
+Set the default concurrency for a worker:
+
+```shell
+$ ronin-recon config set --concurrency web/spider=4
+```
+
+Set the API key for a worker:
+
+```shell
+$ ronin-recon config set --param api/hunter_io.api_key=...
+```
+
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,11 +335,12 @@ 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
* [ronin-web-spider] ~> 0.2
+* [ronin-web-browser] ~> 0.1
## Install
@@ -394,3 +417,4 @@ along with ronin-recon. If not, see .
[ronin-masscan]: https://github.com/ronin-rb/ronin-masscan#readme
[ronin-nmap]: https://github.com/ronin-rb/ronin-nmap#readme
[ronin-web-spider]: https://github.com/ronin-rb/ronin-web-spider#readme
+[ronin-web-browser]: https://github.com/ronin-rb/ronin-web-browser#readme
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..ed8804d0 100644
--- a/gemspec.yml
+++ b/gemspec.yml
@@ -27,11 +27,18 @@ generated_files:
- data/completions/ronin-recon
- man/ronin-recon.1
- man/ronin-recon-completion.1
+ - man/ronin-recon-config.1
+ - man/ronin-recon-config-disable.1
+ - man/ronin-recon-config-enable.1
+ - man/ronin-recon-config-get.1
+ - man/ronin-recon-config-list.1
+ - man/ronin-recon-config-set.1
+ - man/ronin-recon-config-unset.1
- man/ronin-recon-irb.1
- 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,11 +53,12 @@ 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
ronin-web-spider: ~> 0.2
+ ronin-web-browser: ~> 0.1
development_dependencies:
bundler: ~> 2.0
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/builtin/web/dir_enum.rb b/lib/ronin/recon/builtin/web/dir_enum.rb
index df26910f..bb04b98b 100644
--- a/lib/ronin/recon/builtin/web/dir_enum.rb
+++ b/lib/ronin/recon/builtin/web/dir_enum.rb
@@ -99,9 +99,17 @@ def process(website)
headers = response.headers.to_h
if VALID_STATUS_CODES.include?(status)
+ get_response = http.get(path)
+ body = begin
+ get_response.read
+ ensure
+ get_response.close
+ end
+
yield URL.new(
"#{base_url}#{path}", status: status,
- headers: headers
+ headers: headers,
+ body: body
)
end
rescue Errno::ECONNREFUSED,
diff --git a/lib/ronin/recon/builtin/web/screenshot.rb b/lib/ronin/recon/builtin/web/screenshot.rb
new file mode 100644
index 00000000..9bb554d7
--- /dev/null
+++ b/lib/ronin/recon/builtin/web/screenshot.rb
@@ -0,0 +1,107 @@
+# 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/web/browser'
+
+module Ronin
+ module Recon
+ module Web
+ #
+ # A recon worker that takes a screenshot of a page.
+ #
+ # @since 0.2.0
+ #
+ class Screenshot < Worker
+
+ register 'web/screenshot'
+
+ summary 'Visits a website and takes a screenshot of it'
+ description <<~DESC
+ Visits a website and takes a screenshot of it.
+ DESC
+
+ accepts URL
+ outputs nil
+ concurrency 1
+
+ param :output_dir, String, required: true,
+ desc: 'The directory you want to save the screenshot to.'
+
+ param :format, Enum[:png, :jpg], required: true,
+ default: :png,
+ desc: 'The screenshot format.'
+
+ # The Web::Browser instance
+ #
+ # @return [Web::Browser]
+ #
+ # @api private
+ attr_reader :browser
+
+ #
+ # Initializes the `web/screenshot` worker.
+ #
+ # @param [Hash{Symbol => Object}] kwargs
+ # Additional keyword arguments.
+ #
+ # @api private
+ #
+ def initialize(**kwargs)
+ super(**kwargs)
+
+ @browser = Ronin::Web::Browser.new
+ end
+
+ #
+ # Visits a website and takes a screenshot of it.
+ #
+ # @param [Values::URL] url
+ # The URL of the website you want to screenshot.
+ #
+ def process(url)
+ browser.go_to(url)
+
+ path = path_for(browser.page.url)
+ FileUtils.mkdir_p(File.dirname(path))
+
+ browser.screenshot(path: path)
+ end
+
+ #
+ # Generates the file path for a given URL.
+ #
+ # @param [String] url
+ # The given url.
+ #
+ # @return [String]
+ # The relative file path that represents the URL.
+ #
+ def path_for(url)
+ page_url = URI(url)
+ path = File.join(params[:output_dir], page_url.host, page_url.request_uri)
+ path << 'index' if path.end_with?('/')
+ path << ".#{params[:format]}"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ronin/recon/builtin/web/spider.rb b/lib/ronin/recon/builtin/web/spider.rb
index f613af53..b27b7778 100644
--- a/lib/ronin/recon/builtin/web/spider.rb
+++ b/lib/ronin/recon/builtin/web/spider.rb
@@ -45,6 +45,30 @@ class Spider < WebWorker
accepts Website
outputs URL
+ param :limit, Integer, desc: 'The maximum number of pages to visit.'
+
+ param :max_depth, Integer, desc: 'The maximum link depth to follow.'
+
+ param :delay, Integer, desc: 'The number of seconds to pause between each request.'
+
+ param :open_timeout, Integer, desc: 'Optional open connection timeout.'
+
+ param :read_timeout, Integer, desc: 'Optional read timeout.'
+
+ param :ssl_timeout, Integer, desc: 'Optional SSL connection timeout.'
+
+ param :continue_timeout, Integer, desc: 'Optional continue timeout.'
+
+ param :keep_alive_timeout, Integer, desc: 'Optional `Keep-Alive` timeout.'
+
+ param :proxy, URI, desc: 'The proxy information to use.'
+
+ param :referer, String, desc: 'The `Referer` URL to send with each request.'
+
+ param :user_agent, String, desc: 'The `User-Agent` string to send with each requests.'
+
+ param :robots, Boolean, desc: 'Specifies whether `robots.txt` should be honored.'
+
#
# Spiders a website and yields every spidered URL.
#
@@ -60,7 +84,7 @@ class Spider < WebWorker
def process(website)
base_uri = website.to_uri
- Ronin::Web::Spider.site(base_uri) do |agent|
+ Ronin::Web::Spider.site(base_uri, **agent_kwargs) do |agent|
agent.every_page do |page|
if VALID_STATUS_CODES.include?(page.code)
yield URL.new(page.url, status: page.code,
@@ -91,6 +115,13 @@ def process(website)
end
end
+ #
+ # Returns Hash based on the params
+ #
+ def agent_kwargs
+ params.slice(:limit, :max_depth, :delay, :open_timeout, :read_timeout, :ssl_timeout, :continue_timeout, :keep_alive_timeout, :proxy, :referer, :user_agent, :robots)
+ 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/config.rb b/lib/ronin/recon/cli/commands/config.rb
new file mode 100644
index 00000000..8523e195
--- /dev/null
+++ b/lib/ronin/recon/cli/commands/config.rb
@@ -0,0 +1,92 @@
+# 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 '../command'
+
+require 'command_kit/commands/auto_load'
+
+module Ronin
+ module Recon
+ class CLI
+ module Commands
+ #
+ # Get and set ronin-recon configuration.
+ #
+ # ## Usage
+ #
+ # ronin-recon config [options] [COMMAND [ARGS...]]
+ #
+ # ## Options
+ #
+ # -h, --help Print help information
+ #
+ # ## Arguments
+ #
+ # [COMMAND] The command name to run
+ # [ARGS ...] Additional arguments for the command
+ #
+ # ## Commands
+ #
+ # disable
+ # enable
+ # get
+ # help
+ # list
+ # set
+ # unset
+ #
+ # ## Examples
+ #
+ # ronin-recon config list
+ # ronin-recon config enable api/hunter_io
+ # ronin-recon config disable api/hunter_io
+ # ronin-recon config set --param api/hunter_io.api_key=...
+ # ronin-recon config set --concurrency web/spider=10
+ # ronin-recon config unset --param web/spider.proxy
+ # ronin-recon config unset --concurrency web/spider
+ #
+ # @since 0.2.0
+ #
+ class Config < Command
+
+ include CommandKit::Commands::AutoLoad.new(
+ dir: "#{__dir__}/config",
+ namespace: "#{self}"
+ )
+
+ examples [
+ 'list',
+ 'enable api/hunter_io',
+ 'disable api/hunter_io',
+ 'set --param api/hunter_io.api_key=...',
+ 'set --concurrency web/spider=10',
+ 'unset --param web/spider.proxy',
+ 'unset --concurrency web/spider'
+ ]
+
+ description 'Get and set ronin-recon configuration'
+
+ man_page 'ronin-recon-config.1'
+
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ronin/recon/cli/commands/config/disable.rb b/lib/ronin/recon/cli/commands/config/disable.rb
new file mode 100644
index 00000000..2d4e4912
--- /dev/null
+++ b/lib/ronin/recon/cli/commands/config/disable.rb
@@ -0,0 +1,74 @@
+# 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 '../../config_command'
+
+module Ronin
+ module Recon
+ class CLI
+ module Commands
+ class Config < Command
+ #
+ # Disables a worker in the configuration file.
+ #
+ # ## Usage
+ #
+ # ronin-recon config disable [options] WORKER
+ #
+ # ## Options
+ #
+ # -C, --config-file FILE Loads the configuration file
+ # -h, --help Print help information
+ #
+ # ## Arguments
+ #
+ # WORKER The worker ID to disable
+ #
+ # @since 0.2.0
+ #
+ class Disable < ConfigCommand
+
+ argument :worker, required: true,
+ desc: 'The worker ID to disable'
+
+ description "Disables a worker in the configuration file"
+
+ man_page 'ronin-recon-config-disable.1'
+
+ #
+ # Runs the `ronin-recon config disable` command.
+ #
+ # @param [String] worker
+ # The worker ID to disable.
+ #
+ def run(worker)
+ load_config
+
+ @config.workers.delete(worker)
+
+ save_config
+ end
+
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ronin/recon/cli/commands/config/enable.rb b/lib/ronin/recon/cli/commands/config/enable.rb
new file mode 100644
index 00000000..afa33106
--- /dev/null
+++ b/lib/ronin/recon/cli/commands/config/enable.rb
@@ -0,0 +1,73 @@
+# 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 '../../config_command'
+
+module Ronin
+ module Recon
+ class CLI
+ module Commands
+ class Config < Command
+ #
+ # Enables a worker in the configuration file.
+ #
+ # ## Usage
+ #
+ # ronin-recon config enable [options] WORKER
+ #
+ # ## Options
+ #
+ # -C, --config-file FILE Loads the configuration file
+ # -h, --help Print help information
+ #
+ # ## Arguments
+ #
+ # WORKER The worker ID to enable
+ #
+ # @since 0.2.0
+ #
+ class Enable < ConfigCommand
+
+ argument :worker, required: true,
+ desc: 'The worker ID to enable'
+
+ description "Enables a worker in the configuration file"
+
+ man_page 'ronin-recon-config-enable.1'
+
+ #
+ # Runs the `ronin-recon config enable` command.
+ #
+ # @param [String] worker
+ #
+ def run(worker)
+ load_config
+
+ @config.workers.add(worker)
+
+ save_config
+ end
+
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ronin/recon/cli/commands/config/get.rb b/lib/ronin/recon/cli/commands/config/get.rb
new file mode 100644
index 00000000..57eeea37
--- /dev/null
+++ b/lib/ronin/recon/cli/commands/config/get.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 '../../config_command'
+
+module Ronin
+ module Recon
+ class CLI
+ module Commands
+ class Config < Command
+ #
+ # Gets the concurrency or a param for a worker.
+ #
+ # ## Usage
+ #
+ # ronin-recon config get [options] {--concurrency WORKER | --param WORKER.NAME}
+ #
+ # ## Options
+ #
+ # -C, --config-file FILE Loads the configuration file
+ # -c, --concurrency WORKER Gets the concurrency of a worker
+ # -p, --param WORKER.PARAM Gets a param for a worker
+ # -h, --help Print help information
+ #
+ # @since 0.2.0
+ #
+ class Get < ConfigCommand
+
+ usage '[options] {--concurrency WORKER | --param WORKER.NAME}'
+
+ option :concurrency, short: '-c',
+ value: {
+ type: String,
+ usage: 'WORKER'
+ },
+ desc: 'Gets the concurrency for the worker' do |worker|
+ @mode = :concurrency
+ @worker = worker
+ end
+
+ option :param, short: '-p',
+ value: {
+ type: /\A([^\.\=\s]+)\.([^=\s]+)\z/,
+ usage: 'WORKER.PARAM'
+ },
+ desc: 'Gets the param for the worker' do |str,worker,param|
+ @mode = :param
+ @worker = worker
+ @param = param.to_sym
+ end
+
+ description 'Gets the concurrency or a param for a worker'
+
+ man_page 'ronin-recon-config-get.1'
+
+ # Specifies whether to unset the worker's concurrency or param.
+ #
+ # @return [:concurrency, :param, nil]
+ attr_reader :mode
+
+ # The worker name.
+ #
+ # @return [String, nil]
+ attr_reader :worker
+
+ # The param name to unset.
+ #
+ # @return [Symbol, nil]
+ attr_reader :param
+
+ #
+ # Runs the `ronin-recon config set` command.
+ #
+ def run
+ load_config
+
+ case @mode
+ when :concurrency
+ if (concurrency = @config.concurrency[@worker])
+ puts concurrency
+ end
+ when :param
+ if (params = @config.params[@worker]) &&
+ (value = params[@param])
+ puts value
+ end
+ else
+ print_error "--concurrency or --param options must be given"
+ exit(-1)
+ end
+ end
+
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ronin/recon/cli/commands/config/list.rb b/lib/ronin/recon/cli/commands/config/list.rb
new file mode 100644
index 00000000..0a574df1
--- /dev/null
+++ b/lib/ronin/recon/cli/commands/config/list.rb
@@ -0,0 +1,85 @@
+# 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 '../../config_command'
+
+module Ronin
+ module Recon
+ class CLI
+ module Commands
+ class Config < Command
+ #
+ # Lists the values in the configuration file.
+ #
+ # ## Usage
+ #
+ # ronin-recon config list [options]
+ #
+ # ## Options
+ #
+ # -C, --config-file FILE Loads the configuration file
+ # -h, --help Print help information
+ #
+ # @since 0.2.0
+ #
+ class List < ConfigCommand
+
+ description "Lists the values in the configuration file"
+
+ man_page 'ronin-recon-config-list.1'
+
+ #
+ # Runs the `ronin-recon config list` command.
+ #
+ def run
+ load_config
+
+ puts "Workers:"
+ @config.workers.each do |worker_id|
+ puts " * #{worker_id}"
+ end
+
+ unless @config.concurrency.empty?
+ puts
+ puts "Concurrency:"
+ @config.concurrency.each do |worker_id,concurrency|
+ puts " * #{worker_id}=#{concurrency}"
+ end
+ end
+
+ unless @config.params.empty?
+ puts
+ puts "Params:"
+ @config.params.each do |worker_id,params|
+ puts " * #{worker_id}"
+
+ params.each do |name,value|
+ puts " * #{name}=#{value}"
+ end
+ end
+ end
+ end
+
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ronin/recon/cli/commands/config/set.rb b/lib/ronin/recon/cli/commands/config/set.rb
new file mode 100644
index 00000000..305df21a
--- /dev/null
+++ b/lib/ronin/recon/cli/commands/config/set.rb
@@ -0,0 +1,120 @@
+# 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 '../../config_command'
+
+module Ronin
+ module Recon
+ class CLI
+ module Commands
+ class Config < Command
+ #
+ # Sets the concurrency or a param for a worker.
+ #
+ # ## Usage
+ #
+ # ronin-recon config set [options]
+ #
+ # ## Options
+ #
+ # -C, --config-file FILE Loads the configuration file
+ # -c, --concurrency WORKER=NUM Sets the concurrency of a worker
+ # -p, --param WORKER.NAME=VALUE Sets a param for a worker
+ # -h, --help Print help information
+ #
+ # @since 0.2.0
+ #
+ class Set < ConfigCommand
+
+ option :concurrency, short: '-c',
+ value: {
+ type: /\A([^\.\=\s]+)=(\d+)\z/,
+ usage: 'WORKER=NUM'
+ },
+ desc: 'Gets the concurrency for the worker' do |str,worker,concurrency|
+ @mode = :concurrency
+ @worker = worker
+ @concurrency = concurrency.to_i
+ end
+
+ option :param, short: '-p',
+ value: {
+ type: /\A([^\.\=\s]+)\.([^=\s]+)=(.+)\z/,
+ usage: 'WORKER.NAME=VALUE'
+ },
+ desc: 'Gets the param for the worker' do |str,worker,param_name,param_value|
+ @mode = :param
+ @worker = worker
+ @param_name = param_name.to_sym
+ @param_value = param_value
+ end
+
+ description "Sets the concurrency or a param for a worker"
+
+ man_page 'ronin-recon-config-set.1'
+
+ # The worker name.
+ #
+ # @return [String, nil]
+ attr_reader :worker
+
+ # The concurrency value to set.
+ #
+ # @return [Integer, nil]
+ attr_reader :concurrency
+
+ # The param name to set.
+ #
+ # @return [Symbol, nil]
+ attr_reader :param_name
+
+ # The param value to set.
+ #
+ # @return [String, nil]
+ attr_reader :param_value
+
+ #
+ # Runs the `ronin-recon config set` command.
+ #
+ def run
+ load_config
+
+ if @concurrency
+ @config.concurrency[@worker] = @concurrency
+ elsif @param_name
+ if (params = @config.params[@worker])
+ params[@param_name] = @param_value
+ else
+ @config.params[@worker] = {@param_name => @param_value}
+ end
+ else
+ print_error "--concurrency or --param options must be given"
+ exit(-1)
+ end
+
+ save_config
+ end
+
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ronin/recon/cli/commands/config/unset.rb b/lib/ronin/recon/cli/commands/config/unset.rb
new file mode 100644
index 00000000..79846543
--- /dev/null
+++ b/lib/ronin/recon/cli/commands/config/unset.rb
@@ -0,0 +1,114 @@
+# 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 '../../config_command'
+
+module Ronin
+ module Recon
+ class CLI
+ module Commands
+ class Config < Command
+ #
+ # Unsets the concurrency or params for a worker.
+ #
+ # ## Usage
+ #
+ # ronin-recon config unset [options] {--concurrency WORKER | --param WORKER.NAME}
+ #
+ # ## Options
+ #
+ # -C, --config-file FILE Loads the configuration file
+ # -c, --concurrency WORKER Gets the concurrency of a worker
+ # -p, --param WORKER.PARAM Gets a param for a worker
+ # -h, --help Print help information
+ #
+ # @since 0.2.0
+ #
+ class Unset < ConfigCommand
+
+ usage '[options] {--concurrency WORKER | --param WORKER.NAME}'
+
+ option :concurrency, short: '-c',
+ value: {
+ type: String,
+ usage: 'WORKER'
+ },
+ desc: 'Gets the concurrency for the worker' do |worker|
+ @mode = :concurrency
+ @worker = worker
+ end
+
+ option :param, short: '-p',
+ value: {
+ type: /\A([^\.\=\s]+)\.([^=\s]+)\z/,
+ usage: 'WORKER.PARAM'
+ },
+ desc: 'Gets the param for the worker' do |str,worker,param|
+ @mode = :param
+ @worker = worker
+ @param = param.to_sym
+ end
+
+ description 'Unsets the concurrency or params for a worker'
+
+ man_page 'ronin-recon-config-unset.1'
+
+ # Specifies whether to unset the worker's concurrency or param.
+ #
+ # @return [:concurrency, :param, nil]
+ attr_reader :mode
+
+ # The worker name.
+ #
+ # @return [String, nil]
+ attr_reader :worker
+
+ # The param name to unset.
+ #
+ # @return [String, nil]
+ attr_reader :param
+
+ #
+ # Runs the `ronin-recon config unset` command.
+ #
+ def run
+ load_config
+
+ case @mode
+ when :concurrency
+ @config.concurrency.delete(@worker)
+ when :param
+ if (params = @config.params[@worker])
+ params.delete(@param)
+ end
+ else
+ print_error "--concurrency or --param options must be given"
+ exit(-1)
+ end
+
+ save_config
+ end
+
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ronin/recon/cli/commands/run.rb b/lib/ronin/recon/cli/commands/run.rb
index 21dc32c5..8551b3d2 100644
--- a/lib/ronin/recon/cli/commands/run.rb
+++ b/lib/ronin/recon/cli/commands/run.rb
@@ -19,6 +19,7 @@
#
require_relative '../command'
+require_relative '../config_file_option'
require_relative '../debug_option'
require_relative '../printing'
require_relative '../../value/parser'
@@ -55,6 +56,7 @@ module Commands
# -I, --ignore VALUE The values to ignore in result
# -F txt|list|csv|json|ndjson|dot|svg|png|pdf,
# --output-format The output format
+ # --output-dir DIR Writes data to an output directory
# --import Imports each newly discovered value into the Ronin database
# -h, --help Print help information
#
@@ -65,19 +67,13 @@ module Commands
class Run < Command
include DebugOption
+ include ConfigFileOption
include Printing
include Core::CLI::Logging
include DB::CLI::DatabaseOptions
usage '[options] {IP | IP-range | DOMAIN | HOST | WILDCARD | WEBSITE} ...'
- option :config_file, short: '-C',
- value: {
- type: String,
- usage: 'FILE'
- },
- desc: 'Loads the configuration file'
-
option :worker, short: '-w',
value: {
type: String,
@@ -115,24 +111,19 @@ class Run < Command
option :param, short: '-p',
value: {
- type: /\A[^\.\=\s]+\.[^=\s]+=.+\z/,
+ type: /\A([^\.\=\s]+)\.([^=\s]+)=(.+)\z/,
usage: 'WORKER.NAME=VALUE'
},
- desc: 'Sets a param for a worker' do |str|
- prefix, value = str.split('=',2)
- worker, name = prefix.split('.',2)
-
- @worker_params[worker][name.to_sym] = value
+ desc: 'Sets a param for a worker' do |str,worker,param_name,param_value|
+ @worker_params[worker][param_name.to_sym] = param_value
end
option :concurrency, short: '-c',
value: {
- type: /\A[^\.\=\s]+=\d+\z/,
+ type: /\A([^\.\=\s]+)=(\d+)\z/,
usage: 'WORKER=NUM'
},
- desc: 'Sets the concurrency of a worker' do |str|
- worker, concurrency = str.split('=',2)
-
+ desc: 'Sets the concurrency of a worker' do |str,worker,concurrency|
@worker_concurrency[worker] = concurrency.to_i
end
@@ -154,8 +145,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',
@@ -164,6 +154,14 @@ class Run < Command
},
desc: 'The output format'
+ option :output_dir, value: {
+ type: String,
+ usage: 'DIR'
+ },
+ desc: 'Writes data to an output directory' do |path|
+ @outputs << [path, OutputFormats::Dir]
+ end
+
option :import, desc: 'Imports each newly discovered value into the Ronin database'
option :ignore, short: '-I',
@@ -204,11 +202,6 @@ class Run < Command
# @return [Set]
attr_reader :worker_files
- # The loaded configuration for the {Engine}.
- #
- # @return [Config]
- attr_reader :config
-
# The loaded workers for the {Engine}.
#
# @return [Workers]
@@ -229,6 +222,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 +244,8 @@ def initialize(**kwargs)
@worker_params = Hash.new { |hash,key| hash[key] = {} }
@worker_concurrency = {}
- @ignore = []
+ @ignore = []
+ @outputs = []
end
#
@@ -261,9 +260,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 +279,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 +307,7 @@ def run(*values)
end
end
ensure
- output_file.close if options[:output]
+ output_files&.each(&:close)
end
end
@@ -334,11 +333,7 @@ def parse_value(value)
# the `--config-file` option or `~/.config/ronin-recon/config.yml`.
#
def load_config
- @config = if (path = options[:config_file])
- Config.load(path)
- else
- Config.default
- end
+ super()
unless @only_workers.empty?
@config.workers = @only_workers
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/cli/commands/workers.rb b/lib/ronin/recon/cli/commands/workers.rb
index a8ca4489..e8cb3649 100644
--- a/lib/ronin/recon/cli/commands/workers.rb
+++ b/lib/ronin/recon/cli/commands/workers.rb
@@ -20,6 +20,9 @@
require_relative '../command'
require_relative '../../registry'
+require_relative '../../config'
+
+require 'command_kit/printing/tables'
module Ronin
module Recon
@@ -42,6 +45,8 @@ module Commands
#
class Workers < Command
+ include CommandKit::Printing::Tables
+
usage '[options] [DIR]'
argument :dir, required: false,
@@ -58,19 +63,23 @@ class Workers < Command
# The optional recon worker directory to list.
#
def run(dir=nil)
- files = if dir
- dir = "#{dir}/" unless dir.end_with?('/')
+ config = Ronin::Recon::Config.default
- Ronin::Recon.list_files.select do |file|
- file.start_with?(dir)
+ workers = if dir
+ dir = "#{dir}/" unless dir.end_with?('/')
+
+ Ronin::Recon.list_files.select do |file|
+ file.start_with?(dir)
+ end
+ else
+ Ronin::Recon.list_files
end
- else
- Ronin::Recon.list_files
- end
- files.each do |file|
- puts " #{file}"
+ columns = workers.map do |worker|
+ [worker, ('[enabled]' if config.workers.include?(worker))]
end
+
+ print_table(columns, padding: 2)
end
end
diff --git a/lib/ronin/recon/cli/config_command.rb b/lib/ronin/recon/cli/config_command.rb
new file mode 100644
index 00000000..88c1bfce
--- /dev/null
+++ b/lib/ronin/recon/cli/config_command.rb
@@ -0,0 +1,51 @@
+# 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 'command'
+require_relative 'config_file_option'
+
+module Ronin
+ module Recon
+ class CLI
+ #
+ # Base class for all `ronin-recon config` sub-commands.
+ #
+ # @since 0.2.0
+ #
+ class ConfigCommand < Command
+
+ include ConfigFileOption
+
+ #
+ # Saves the configuration back out to either the `--config-file`
+ # path or `~/.config/ronin-recon/config.yml`.
+ #
+ def save_config
+ if (config_file = options[:config_file])
+ @config.save(config_file)
+ else
+ @config.save
+ end
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/ronin/recon/cli/config_file_option.rb b/lib/ronin/recon/cli/config_file_option.rb
new file mode 100644
index 00000000..06975bc5
--- /dev/null
+++ b/lib/ronin/recon/cli/config_file_option.rb
@@ -0,0 +1,83 @@
+# 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 '../config'
+
+module Ronin
+ module Recon
+ class CLI
+ #
+ # Defines the `-C, --config-file` option.
+ #
+ # @since 0.2.0
+ #
+ module ConfigFileOption
+ #
+ # Defines the `-C, --config-file` option on the command class that
+ # included {ConfigFileOption}.
+ #
+ # @param [Class] command
+ # The command class that included {ConfigFileOption}.
+ #
+ def self.included(command)
+ command.option :config_file, short: '-C',
+ value: {
+ type: String,
+ usage: 'FILE'
+ },
+ desc: 'Loads the configuration file'
+ end
+
+ # The loaded configuration for the {Engine}.
+ #
+ # @return [Config]
+ attr_reader :config
+
+ #
+ # Loads the recon configuration file from either
+ # the `--config-file` option or `~/.config/ronin-recon/config.yml`.
+ #
+ # @note
+ # * If the `--config-file` path is missing an error will be printed
+ # and the command will exit with -1.
+ # * If the config file is invalid an error will be printed and the
+ # command will exit with -2.
+ #
+ def load_config
+ @config = begin
+ if (path = options[:config_file])
+ unless File.file?(path)
+ print_error("no such file or directory: #{path}")
+ exit(-1)
+ end
+
+ Config.load(path)
+ else
+ Config.default
+ end
+ rescue InvalidConfigFile => error
+ print_error(error.message)
+ exit(-2)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ronin/recon/config.rb b/lib/ronin/recon/config.rb
index bef15e35..88ec0393 100644
--- a/lib/ronin/recon/config.rb
+++ b/lib/ronin/recon/config.rb
@@ -22,6 +22,7 @@
require 'ronin/core/home'
require 'set'
+require 'fileutils'
module Ronin
module Recon
@@ -172,6 +173,30 @@ def eql?(other)
alias == eql?
+ #
+ # Converts workers to a YAML Hash.
+ #
+ # @return [Hash{String => Boolean}]
+ # The hash of worker IDs and whether to enable/disable them.
+ #
+ # @since 0.2.0
+ #
+ def to_h
+ hash = {}
+
+ # enable the workers in the set.
+ (@ids - DEFAULT).each do |worker_id|
+ hash[worker_id] = true
+ end
+
+ # disable the default workers that are not in the set.
+ (DEFAULT - @ids).each do |worker_id|
+ hash[worker_id] = false
+ end
+
+ return hash
+ end
+
end
# The workers to use.
@@ -366,6 +391,39 @@ def eql?(other)
alias == eql?
+ #
+ # Converts config into YAML string.
+ #
+ # @params [Hash{Symbol => Object}] options
+ # Additional options for Object#to_yaml.
+ #
+ # @return [String]
+ #
+ # @since 0.2.0
+ #
+ def to_yaml(options={})
+ data = {}
+
+ data[:params] = @params unless @params.empty?
+ data[:concurrency] = @concurrency unless @concurrency.empty?
+ data[:workers] = @workers.to_h if @workers
+
+ data.to_yaml(options)
+ end
+
+ #
+ # Writes config converted to YAML into a file.
+ #
+ # @param [String] path
+ # The output file path.
+ #
+ # @since 0.2.0
+ #
+ def save(path=DEFAULT_PATH)
+ FileUtils.mkdir_p(File.dirname(path))
+ File.write(path,to_yaml)
+ 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/values/web_socket.rb b/lib/ronin/recon/values/web_socket.rb
new file mode 100644
index 00000000..bdbfa8d9
--- /dev/null
+++ b/lib/ronin/recon/values/web_socket.rb
@@ -0,0 +1,240 @@
+# 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 '../value'
+
+require 'uri'
+
+module Ronin
+ module Recon
+ module Values
+ #
+ # Represents a WebSocket.
+ #
+ # @api public
+ #
+ # @since 0.2.0
+ #
+ class WebSocket < Value
+
+ # The parsed URI.
+ #
+ # @return [URI::WS, URI::WSS]
+ attr_reader :uri
+
+ #
+ # Initializes the WebSocket value.
+ #
+ # @param [URI::WS, URI::WSS, String] url
+ #
+ def initialize(url)
+ @uri = URI(url)
+ end
+
+ #
+ # Indicates whether the WebSocket uses `ws://` or `wss://`.
+ #
+ # @return ['ws', 'wss']
+ #
+ def scheme
+ @uri.scheme
+ end
+
+ #
+ # The WebSocket's hostname.
+ #
+ # @return [String]
+ #
+ def host
+ @uri.host
+ end
+
+ #
+ # The WebSocket's port number.
+ #
+ # @return [Integer]
+ #
+ def port
+ @uri.port
+ end
+
+ #
+ # The WebSocket's path.
+ #
+ # @return [String]
+ #
+ def path
+ @uri.path
+ end
+
+ #
+ # The WebSocket's query
+ #
+ # @return [String]
+ #
+ def query
+ @uri.query
+ end
+
+ #
+ # Initializes the 'ws://' WebSocket.
+ #
+ # @param [String] host
+ # The WebSocket's host.
+ #
+ # @param [Integer] port
+ # The WebSocket's port.
+ #
+ # @param [String] path
+ # The WebSocket's path.
+ #
+ # @param [String] query
+ # The WebSocket's query.
+ #
+ def self.ws(host,port=80,path=nil,query=nil)
+ new(URI::WS.build(host: host, port: port, path: path, query: query))
+ end
+
+ #
+ # Initializes the 'wss://' WebSocket.
+ #
+ # @param [String] host
+ # The WebSocket's host.
+ #
+ # @param [Integer] port
+ # The WebSocket's port.
+ #
+ # @param [String] path
+ # The WebSocket's path.
+ #
+ # @param [String] query
+ # The WebSocket's query.
+ #
+ def self.wss(host,port=443,path=nil,query=nil)
+ new(URI::WSS.build(host: host, port: port, path: path, query: query))
+ end
+
+ #
+ # Compares the WebSocket to another value.
+ #
+ # @param [Value] other
+ #
+ # @return [Boolean]
+ #
+ def eql?(other)
+ self.class == other.class &&
+ scheme == other.scheme &&
+ host == other.host &&
+ port == other.port &&
+ path == other.path &&
+ query == other.query
+ end
+
+ #
+ # Case equality method used for fuzzy matching.
+ #
+ # @param [Value] other
+ # The other value to compare.
+ #
+ # @return [Boolean]
+ # Imdicates whether the other value same as {WebSocket}
+ #
+ def ===(other)
+ case other
+ when WebSocket
+ eql?(other)
+ else
+ false
+ end
+ end
+
+ #
+ # The "hash" value of the WebSocket.
+ #
+ # @return [Integer]
+ # The hash value of {#scheme}, {#host}, {#port}, {#path} and {#query}.
+ #
+ def hash
+ [self.class, scheme, host, port, path, query].hash
+ end
+
+ # Mapping of {#scheme} values to URI classes.
+ #
+ # @api private
+ URI_CLASSES = {
+ 'wss' => URI::WSS,
+ 'ws' => URI::WS
+ }
+
+ #
+ # Converts the WebSocket to URI.
+ #
+ # @return [URI::WS, URI::WSS]
+ # The URI object for the website.
+ #
+ def to_uri
+ @uri
+ end
+
+ #
+ # Converts the WebSocket to a String.
+ #
+ # @return [String]
+ # The base URL value for the WebSocket.
+ #
+ def to_s
+ @uri.to_s
+ end
+
+ #
+ # Coerces the WebSocket value into JSON.
+ #
+ # @return [Hash{Symbol => Object}]
+ # The Ruby Hash that will be converted into JSON.
+ #
+ def as_json
+ {
+ type: :web_socket,
+ scheme: scheme,
+ host: host,
+ port: port,
+ path: path,
+ query: query
+ }
+ end
+
+ #
+ # Returns the type or kind of recon value.
+ #
+ # @return [:web_socket]
+ #
+ # @note
+ # This is used internally to map a recon value class to a printable
+ # type.
+ #
+ # @api private
+ #
+ def self.value_type
+ :web_socket
+ end
+ end
+ end
+ end
+end
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-config-disable.1.md b/man/ronin-recon-config-disable.1.md
new file mode 100644
index 00000000..b9ac58a4
--- /dev/null
+++ b/man/ronin-recon-config-disable.1.md
@@ -0,0 +1,40 @@
+# ronin-recon-config-disable 1 "2024-09-03" Ronin Recon "User Manuals"
+
+## NAME
+
+ronin-recon-config-disable - Disables a worker in the configuration file
+
+## SYNOPSIS
+
+`ronin-recon config disable` [*options*] *WORKER*
+
+## DESCRIPTION
+
+Disables a worker in the configuration file. This will prevent the worker from
+running by default when `ronin-recon run` is ran.
+
+## ARGUMENTS
+
+*WORKER*
+: The worker ID to disable.
+
+## OPTIONS
+
+`-C`, `--config-file` *FILE*
+: Loads the configuration file from another file.
+
+`-h`, `--help`
+: Print help information
+
+## FILES
+
+`~/.config/ronin-recon/config.yml`
+: The path to the default configuration file for `ronin-recon`.
+
+## AUTHOR
+
+Postmodern
+
+## SEE ALSO
+
+[ronin-recon-config-enable](ronin-recon-config-enable.1.md) [ronin-recon-config-list](ronin-recon-config-list.1.md)
diff --git a/man/ronin-recon-config-enable.1.md b/man/ronin-recon-config-enable.1.md
new file mode 100644
index 00000000..a3728439
--- /dev/null
+++ b/man/ronin-recon-config-enable.1.md
@@ -0,0 +1,40 @@
+# ronin-recon-config-enable 1 "2024-09-03" Ronin Recon "User Manuals"
+
+## NAME
+
+ronin-recon-config-enable - Enables a worker in the configuration file
+
+## SYNOPSIS
+
+`ronin-recon config enable` [*options*] *WORKER*
+
+## DESCRIPTION
+
+Enables a worker in the configuration file. This will cause the worker to
+automatically run by default when `ronin-recon run` is ran.
+
+## ARGUMENTS
+
+*WORKER*
+: The worker ID to enable.
+
+## OPTIONS
+
+`-C`, `--config-file` *FILE*
+: Loads the configuration file from another file.
+
+`-h`, `--help`
+: Print help information
+
+## FILES
+
+`~/.config/ronin-recon/config.yml`
+: The path to the default configuration file for `ronin-recon`.
+
+## AUTHOR
+
+Postmodern
+
+## SEE ALSO
+
+[ronin-recon-config-disable](ronin-recon-config-disable.1.md) [ronin-recon-config-list](ronin-recon-config-list.1.md)
diff --git a/man/ronin-recon-config-get.1.md b/man/ronin-recon-config-get.1.md
new file mode 100644
index 00000000..6bf8d56f
--- /dev/null
+++ b/man/ronin-recon-config-get.1.md
@@ -0,0 +1,40 @@
+# ronin-recon-config-get 1 "2024-09-03" Ronin Recon "User Manuals"
+
+## NAME
+
+ronin-recon-config-get - Gets the concurrency or a param for a worker
+
+## SYNOPSIS
+
+`ronin-recon config get` [*options*] {`--concurrency` *WORKER* \| `--param` *WORKER*`.`*NAME*}
+
+## DESCRIPTION
+
+Gets the concurrency setting for a *WORKER* or a param value for the *WORKER*.
+
+## OPTIONS
+
+`-C`, `--config-file` *FILE*
+: Loads the configuration file from another file.
+
+`-c`, `--concurrency` *WORKER*
+: Gets the concurrency of the *WORKER*.
+
+`-p`, `--param` *WORKER*`.`*PARAM*
+: Get the param value for the *PARAM* and *WORKER*.
+
+`-h`, `--help`
+: Print help information
+
+## FILES
+
+`~/.config/ronin-recon/config.yml`
+: The path to the default configuration file for `ronin-recon`.
+
+## AUTHOR
+
+Postmodern
+
+## SEE ALSO
+
+[ronin-recon-config-list](ronin-recon-config-list.1.md) [ronin-recon-config-set](ronin-recon-config-set.1.md) [ronin-recon-config-unset](ronin-recon-config-unset.1.md)
diff --git a/man/ronin-recon-config-list.1.md b/man/ronin-recon-config-list.1.md
new file mode 100644
index 00000000..fd08e0e8
--- /dev/null
+++ b/man/ronin-recon-config-list.1.md
@@ -0,0 +1,35 @@
+# ronin-recon-config-list 1 "2024-09-03" Ronin Recon "User Manuals"
+
+## NAME
+
+ronin-recon-config-list - Lists the values in the configuration file
+
+## SYNOPSIS
+
+`ronin-recon config list` [*options*]
+
+## DESCRIPTION
+
+Prints the enabled workers, concurrency settings, and params for each worker,
+set in the configuration file.
+
+## OPTIONS
+
+`-C`, `--config-file` *FILE*
+: Loads the configuration file from another file.
+
+`-h`, `--help`
+: Print help information
+
+## FILES
+
+`~/.config/ronin-recon/config.yml`
+: The path to the default configuration file for `ronin-recon`.
+
+## AUTHOR
+
+Postmodern
+
+## SEE ALSO
+
+[ronin-recon-config-disable](ronin-recon-config-disable.1.md) [ronin-recon-config-enable](ronin-recon-config-enable.1.md) [ronin-recon-config-get](ronin-recon-config-get.1.md) [ronin-recon-config-set](ronin-recon-config-set.1.md) [ronin-recon-config-unset](ronin-recon-config-unset.1.md)
diff --git a/man/ronin-recon-config-set.1.md b/man/ronin-recon-config-set.1.md
new file mode 100644
index 00000000..f04561cc
--- /dev/null
+++ b/man/ronin-recon-config-set.1.md
@@ -0,0 +1,40 @@
+# ronin-recon-config-set 1 "2024-09-03" Ronin Recon "User Manuals"
+
+## NAME
+
+ronin-recon-config-set - Sets the concurrency or a param for a worker
+
+## SYNOPSIS
+
+`ronin-recon config set` [*options*] {`--concurrency` *WORKER*`=`*NUM* \| `--param` *WORKER*`.`*NAME*`=`*VALUE*}
+
+## DESCRIPTION
+
+Sets the concurrency setting for a *WORKER* or a param value for the *WORKER*.
+
+## OPTIONS
+
+`-C`, `--config-file` *FILE*
+: Loads the configuration file from another file.
+
+`-c`, `--concurrency` *WORKER*`=`*NUM*
+: Sets the concurrency for the *WORKER*.
+
+`-p`, `--param` *WORKER*`.`*PARAM*`=`*VALUE*
+: Set the param value for the *PARAM* and *WORKER*.
+
+`-h`, `--help`
+: Print help information
+
+## FILES
+
+`~/.config/ronin-recon/config.yml`
+: The path to the default configuration file for `ronin-recon`.
+
+## AUTHOR
+
+Postmodern
+
+## SEE ALSO
+
+[ronin-recon-config-list](ronin-recon-config-list.1.md) [ronin-recon-config-get](ronin-recon-config-get.1.md) [ronin-recon-config-unset](ronin-recon-config-unset.1.md)
diff --git a/man/ronin-recon-config-unset.1.md b/man/ronin-recon-config-unset.1.md
new file mode 100644
index 00000000..3bbd6e47
--- /dev/null
+++ b/man/ronin-recon-config-unset.1.md
@@ -0,0 +1,42 @@
+# ronin-recon-config-unset 1 "2024-09-03" Ronin Recon "User Manuals"
+
+## NAME
+
+ronin-recon-config-unset - Unsets the concurrency or params for a worker
+
+## SYNOPSIS
+
+`ronin-recon config unset` [*options*] {`--concurrency` *WORKER* \| `--param` *WORKER*`.`*NAME*}
+
+## DESCRIPTION
+
+Unsets the concurrency setting for a *WORKER* or a param value for the *WORKER*.
+This will cause the *WORKER* to revert to using the default concurrency value
+or the default param value.
+
+## OPTIONS
+
+`-C`, `--config-file` *FILE*
+: Loads the configuration file from another file.
+
+`-c`, `--concurrency` *WORKER*
+: Unsets the concurrency of the *WORKER*.
+
+`-p`, `--param` *WORKER*`.`*PARAM*
+: Unsets the param value for the *PARAM* and *WORKER*.
+
+`-h`, `--help`
+: Print help information
+
+## FILES
+
+`~/.config/ronin-recon/config.yml`
+: The path to the default configuration file for `ronin-recon`.
+
+## AUTHOR
+
+Postmodern
+
+## SEE ALSO
+
+[ronin-recon-config-list](ronin-recon-config-list.1.md) [ronin-recon-config-get](ronin-recon-config-get.1.md) [ronin-recon-config-set](ronin-recon-config-set.1.md)
diff --git a/man/ronin-recon-config.1.md b/man/ronin-recon-config.1.md
new file mode 100644
index 00000000..ff516f4b
--- /dev/null
+++ b/man/ronin-recon-config.1.md
@@ -0,0 +1,55 @@
+# ronin-recon-config 1 "2024-09-03" Ronin Recon "User Manuals"
+
+## NAME
+
+ronin-recon-config - Get and set ronin-recon configuration
+
+## SYNOPSIS
+
+`ronin-recon config` [*options*] [*COMMAND* [...]]
+
+## DESCRIPTION
+
+Runs a `ronin-recon config` *COMMAND* that can get or set `ronin-recon`
+configuration settings.
+
+## ARGUMENTS
+
+*COMMAND*
+: The `ronin-recon config` command to execute.
+
+## OPTIONS
+
+`-V`, `--version`
+: Prints the `ronin-recon` version and exits.
+
+`-h`, `--help`
+: Print help information
+
+## COMMANDS
+
+*disable*
+: Disables a worker in the configuration file.
+
+*enable*
+: Enables a worker in the configuration file.
+
+*get*
+: Gets the concurrency or a param for a worker.
+
+*list*
+: Lists the values in the configuration file.
+
+*set*
+: Sets the concurrency or a param for a worker.
+
+*unset*
+: Unsets the concurrency or params for a worker.
+
+## AUTHOR
+
+Postmodern
+
+## SEE ALSO
+
+[ronin-recon-config-disable](ronin-recon-config-disable.1.md) [ronin-recon-config-enable](ronin-recon-config-enable.1.md) [ronin-recon-config-get](ronin-recon-config-get.1.md) [ronin-recon-config-list](ronin-recon-config-list.1.md) [ronin-recon-config-set](ronin-recon-config-set.1.md) [ronin-recon-config-unset](ronin-recon-config-unset.1.md)
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..8a9e544b 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
@@ -70,6 +70,9 @@ Runs the recon engine with one or more initial values.
: The output format. If not specified, the output format will be inferred from
the `--output` *FILE* extension.
+`--output-dir` *DIR*
+: Writes the output to a directory of separate files for each value type.
+
`--import`
: Imports each newly discovered value into the Ronin database.
@@ -112,4 +115,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..ff7030e0 100644
--- a/man/ronin-recon.1.md
+++ b/man/ronin-recon.1.md
@@ -30,6 +30,9 @@ Runs a `ronin-recon` *COMMAND*.
*completion*
: Manages the shell completion rules for `ronin-recon`.
+*config*
+: Get and set ronin-recon configuration.
+
*help*
: Lists available commands or shows help about a specific command.
@@ -42,8 +45,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.
@@ -57,4 +60,4 @@ Postmodern
## SEE ALSO
-[ronin-recon-completion](ronin-recon-completion.1.md) [ronin-recon-new](ronin-recon-new.1.md) [ronin-recon-test](ronin-recon-test.1.md) [ronin-recon-worker](ronin-recon-worker.1.md) [ronin-recon-workers](ronin-recon-workers.1.md)
+[ronin-recon-completion](ronin-recon-completion.1.md) [ronin-recon-config](ronin-recon-config.1.md) [ronin-recon-new](ronin-recon-new.1.md) [ronin-recon-test](ronin-recon-test.1.md) [ronin-recon-worker](ronin-recon-worker.1.md) [ronin-recon-workers](ronin-recon-workers.1.md)
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/builtin/web/dir_enum_spec.rb b/spec/builtin/web/dir_enum_spec.rb
index 60ce6214..95694e2d 100644
--- a/spec/builtin/web/dir_enum_spec.rb
+++ b/spec/builtin/web/dir_enum_spec.rb
@@ -57,19 +57,19 @@ class App < Sinatra::Base
set :port, 80
get '/' do
- halt 200
+ [200, ['Body for index']]
end
get '/admin' do
- halt 200
+ [200, ['Body for /admin']]
end
get '/downloads' do
- halt 200
+ [200, ['Body for /downloads']]
end
get '/secret' do
- halt 200
+ [200, ['Body for /secret']]
end
end
end
@@ -79,6 +79,7 @@ class App < Sinatra::Base
before do
stub_request(:head, /#{Regexp.escape(host)}/).to_rack(app)
+ stub_request(:get, /#{Regexp.escape(host)}/).to_rack(app)
end
let(:fixtures_dir) { File.join(__dir__,'fixtures') }
@@ -114,17 +115,17 @@ class App < Sinatra::Base
expect(yielded_values[0].uri).to eq(URI('http://example.com/admin'))
expect(yielded_values[0].status).to eq(200)
expect(yielded_values[0].headers).to eq(expected_headers)
- expect(yielded_values[0].body).to be(nil)
+ expect(yielded_values[0].body).to eq('Body for /admin')
expect(yielded_values[1]).to be_kind_of(Ronin::Recon::Values::URL)
expect(yielded_values[1].uri).to eq(URI('http://example.com/downloads'))
expect(yielded_values[1].status).to eq(200)
expect(yielded_values[1].headers).to eq(expected_headers)
- expect(yielded_values[1].body).to be(nil)
+ expect(yielded_values[1].body).to eq('Body for /downloads')
expect(yielded_values[2]).to be_kind_of(Ronin::Recon::Values::URL)
expect(yielded_values[2].uri).to eq(URI('http://example.com/secret'))
- expect(yielded_values[2].status).to eq(200)
+ expect(yielded_values[2].status).to be(200)
expect(yielded_values[2].headers).to eq(expected_headers)
- expect(yielded_values[2].body).to be(nil)
+ expect(yielded_values[2].body).to eq('Body for /secret')
end
# Valid HTTP status codes
diff --git a/spec/builtin/web/screenshot_spec.rb b/spec/builtin/web/screenshot_spec.rb
new file mode 100644
index 00000000..a3932d65
--- /dev/null
+++ b/spec/builtin/web/screenshot_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+require 'ronin/recon/builtin/web/screenshot'
+
+require 'webmock/rspec'
+
+describe Ronin::Recon::Web::Screenshot do
+ let(:dir) { Dir.mktmpdir('test-ronin-recon-web-screenshot') }
+
+ subject { described_class.new(params: { output_dir: dir }) }
+
+ before do
+ WebMock.disable_net_connect!(allow_localhost: true)
+ end
+
+ describe "#process" do
+ let(:url) { Ronin::Recon::Values::URL.new('https://www.example.com') }
+ let(:path) { File.join(dir,"www.example.com","index.png") }
+
+ before do
+ stub_request(:get, 'https://www.example.com')
+ .to_return(status: 200, body: "")
+ end
+
+ it "must visit a website and take a screenshot of it" do
+ skip "GitHub Actions causes the Ferrum process to timeout sometimes" if ENV['CI']
+ subject.process(url)
+
+ expect(File.exist?(path)).to be(true)
+ end
+ end
+
+ describe "#path_for" do
+ context "when url ends with '/'" do
+ let(:url) { 'https://www.example.com/' }
+ let(:expected_path) { File.join(dir,'www.example.com','index.png') }
+
+ it "must add 'index' to the returned path" do
+ expect(subject.path_for(url)).to eq(expected_path)
+ end
+ end
+
+ context "when url does not ends with '/'" do
+ let(:url) { 'https://www.example.com/foo/bar.php' }
+ let(:expected_path) { File.join(dir, 'www.example.com','foo','bar.php.png') }
+
+ it "must return path" do
+ expect(subject.path_for(url)).to eq(expected_path)
+ end
+ end
+ end
+end
diff --git a/spec/builtin/web/spider_spec.rb b/spec/builtin/web/spider_spec.rb
index 6cebb33c..215cc638 100644
--- a/spec/builtin/web/spider_spec.rb
+++ b/spec/builtin/web/spider_spec.rb
@@ -463,4 +463,41 @@ class AppWithURLsInJavaScript < Sinatra::Base
end
end
end
+
+ describe "#agent_kwargs" do
+ context "when there are params" do
+ let(:params_hash) do
+ {
+ limit: 10,
+ max_depth: 2,
+ delay: 5,
+ open_timeout: 10,
+ read_timeout: 10,
+ ssl_timeout: 10,
+ continue_timeout: 10,
+ keep_alive_timeout: 10,
+ proxy: URI('http://proxy.example.com:8080'),
+ referer: 'http://example.com/',
+ user_agent: 'Test User Agent',
+ robots: true
+ }
+ end
+
+ subject { described_class.new(params: params_hash) }
+
+ it "must return a Hash with the params" do
+ expect(subject.agent_kwargs).to eq(params_hash)
+ end
+ end
+
+ context "when there are no params" do
+ let(:params_hash) { {} }
+
+ subject { described_class.new(params: params_hash) }
+
+ it "must return an empty Hash" do
+ expect(subject.agent_kwargs).to eq(params_hash)
+ end
+ end
+ end
end
diff --git a/spec/cli/commands/config/disable_spec.rb b/spec/cli/commands/config/disable_spec.rb
new file mode 100644
index 00000000..3489e96d
--- /dev/null
+++ b/spec/cli/commands/config/disable_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require 'ronin/recon/cli/commands/config/disable'
+
+require_relative '../man_page_example'
+
+require 'tempfile'
+
+describe Ronin::Recon::CLI::Commands::Config::Disable do
+ include_examples "man_page"
+
+ describe "#run" do
+ let(:tempfile) { Tempfile.new(['ronin-recon-config-','.yml']) }
+ let(:config_file) { tempfile.path }
+
+ let(:worker) { 'dns/reverse_lookup' }
+
+ before do
+ config = Ronin::Recon::Config.new
+ config.save(config_file)
+ end
+
+ before do
+ subject.options[:config_file] = config_file
+ end
+
+ it "must disable the worker in the config file" do
+ subject.run(worker)
+
+ config = Ronin::Recon::Config.load(config_file)
+ expect(config.workers).to_not include(worker)
+ end
+ end
+end
diff --git a/spec/cli/commands/config/enable_spec.rb b/spec/cli/commands/config/enable_spec.rb
new file mode 100644
index 00000000..63baf58a
--- /dev/null
+++ b/spec/cli/commands/config/enable_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+require 'ronin/recon/cli/commands/config/enable'
+
+require_relative '../man_page_example'
+
+require 'tempfile'
+
+describe Ronin::Recon::CLI::Commands::Config::Enable do
+ include_examples "man_page"
+
+ describe "#run" do
+ let(:tempfile) { Tempfile.new(['ronin-recon-config-','.yml']) }
+ let(:config_file) { tempfile.path }
+
+ let(:worker) { 'test/worker1' }
+
+ before do
+ config = Ronin::Recon::Config.new
+ config.workers.add(worker)
+ config.save(config_file)
+ end
+
+ before do
+ subject.options[:config_file] = config_file
+ end
+
+ it "must enable the worker in the config file" do
+ subject.run(worker)
+
+ config = Ronin::Recon::Config.load(config_file)
+ expect(config.workers).to include(worker)
+ end
+ end
+end
diff --git a/spec/cli/commands/config/get_spec.rb b/spec/cli/commands/config/get_spec.rb
new file mode 100644
index 00000000..012256df
--- /dev/null
+++ b/spec/cli/commands/config/get_spec.rb
@@ -0,0 +1,118 @@
+require 'spec_helper'
+require 'ronin/recon/cli/commands/config/get'
+
+require_relative '../man_page_example'
+
+require 'tempfile'
+
+describe Ronin::Recon::CLI::Commands::Config::Get do
+ include_examples "man_page"
+
+ describe "#run" do
+ let(:tempfile) { Tempfile.new(['ronin-recon-config-','.yml']) }
+ let(:config_file) { tempfile.path }
+
+ let(:worker) { 'test/worker1' }
+
+ context "when the --concurrency option is given" do
+ let(:concurrency) { 10 }
+
+ before do
+ config = Ronin::Recon::Config.new(concurrency: {worker => concurrency})
+ config.save(config_file)
+ end
+
+ before do
+ subject.option_parser.parse(
+ [
+ '--config-file', config_file,
+ '--concurrency', worker
+ ]
+ )
+ end
+
+ it "must print the concurrency value for the worker to stdout" do
+ expect {
+ subject.run
+ }.to output("#{concurrency}#{$/}").to_stdout
+ end
+
+ context "but the concurrency is not set" do
+ before do
+ subject.option_parser.parse(
+ [
+ '--config-file', config_file,
+ '--concurrency', "test/does_not_exist"
+ ]
+ )
+ end
+
+ it "must not print anything" do
+ expect {
+ subject.run
+ }.to_not output.to_stdout
+ end
+ end
+ end
+
+ context "when the --param option is given" do
+ let(:param_name) { :foo }
+ let(:param_value) { 'bar' }
+
+ before do
+ config = Ronin::Recon::Config.new(
+ params: {
+ worker => {
+ param_name => param_value
+ }
+ }
+ )
+ config.save(config_file)
+ end
+
+ before do
+ subject.option_parser.parse(
+ [
+ '--config-file', config_file,
+ '--param', "#{worker}.#{param_name}"
+ ]
+ )
+ end
+
+ it "must print the param valeu for the worker to stdout" do
+ expect {
+ subject.run
+ }.to output("#{param_value}#{$/}").to_stdout
+ end
+
+ context "but the param is not set" do
+ before do
+ subject.option_parser.parse(
+ [
+ '--config-file', config_file,
+ '--param', "#{worker}.does_not_exist"
+ ]
+ )
+ end
+
+ it "must not print anything" do
+ expect {
+ subject.run
+ }.to_not output.to_stdout
+ end
+ end
+ end
+
+ context "when neither the --concurrency or --param options are given" do
+ it "must print an error and exit with 1" do
+ expect(subject).to receive(:print_error).with("--concurrency or --param options must be given")
+
+ expect {
+ subject.run
+ }.to raise_error(SystemExit) do |error|
+ expect(error.status).to eq(-1)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/cli/commands/config/list_spec.rb b/spec/cli/commands/config/list_spec.rb
new file mode 100644
index 00000000..c56646e9
--- /dev/null
+++ b/spec/cli/commands/config/list_spec.rb
@@ -0,0 +1,166 @@
+require 'spec_helper'
+require 'ronin/recon/cli/commands/config/list'
+
+require_relative '../man_page_example'
+
+require 'tempfile'
+
+describe Ronin::Recon::CLI::Commands::Config::List do
+ include_examples "man_page"
+
+ describe "#run" do
+ let(:tempfile) { Tempfile.new(['ronin-recon-config-','.yml']) }
+ let(:config_file) { tempfile.path }
+
+ before do
+ subject.options[:config_file] = config_file
+ end
+
+ context "when the config file is empty" do
+ before do
+ config = Ronin::Recon::Config.new
+ config.save(config_file)
+ end
+
+ it "must print the default workers" do
+ expect {
+ subject.run
+ }.to output(
+ [
+ "Workers:",
+ *Ronin::Recon::Config::Workers::DEFAULT.map { |worker|
+ " * #{worker}"
+ },
+ ''
+ ].join($/)
+ ).to_stdout
+ end
+ end
+
+ context "when the config file has workers enabled or disalbed" do
+ let(:enabled_worker1) { 'test/worker1' }
+ let(:enabled_worker2) { 'test/worker2' }
+ let(:enabled_worker3) { 'test/worker3' }
+ let(:disabled_worker1) { 'dns/reverse_lookup' }
+ let(:disabled_worker2) { 'dns/subdomain_enum' }
+
+ before do
+ config = Ronin::Recon::Config.new
+ config.workers.add(enabled_worker1)
+ config.workers.add(enabled_worker2)
+ config.workers.add(enabled_worker3)
+ config.workers.delete(disabled_worker1)
+ config.workers.delete(disabled_worker2)
+ config.save(config_file)
+ end
+
+ it "must print the enabled workers in addition to the default workers that were not disabled" do
+ config = Ronin::Recon::Config.load(config_file)
+
+ expect {
+ subject.run
+ }.to output(
+ [
+ "Workers:",
+ *config.workers.map { |worker|
+ " * #{worker}"
+ },
+ ''
+ ].join($/)
+ ).to_stdout
+ end
+ end
+
+ context "when the config file has concurrency values set" do
+ let(:worker1) { 'test/worker1' }
+ let(:worker2) { 'test/worker2' }
+ let(:worker3) { 'test/worker3' }
+
+ let(:concurrency1) { 2 }
+ let(:concurrency2) { 10 }
+ let(:concurrency3) { 42 }
+
+ before do
+ config = Ronin::Recon::Config.new(
+ concurrency: {
+ worker1 => concurrency1,
+ worker2 => concurrency2,
+ worker3 => concurrency3
+ }
+ )
+ config.save(config_file)
+ end
+
+ it "must print the concurrency values for the workers" do
+ config = Ronin::Recon::Config.load(config_file)
+
+ expect {
+ subject.run
+ }.to output(
+ [
+ "Workers:",
+ *config.workers.map { |worker|
+ " * #{worker}"
+ },
+ '',
+ 'Concurrency:',
+ " * #{worker1}=#{concurrency1}",
+ " * #{worker2}=#{concurrency2}",
+ " * #{worker3}=#{concurrency3}",
+ ''
+ ].join($/)
+ ).to_stdout
+ end
+ end
+
+ context "when the config file has param values set" do
+ let(:worker1) { 'test/worker1' }
+ let(:worker2) { 'test/worker2' }
+
+ let(:param_name1) { :foo }
+ let(:param_value1) { true }
+ let(:param_name2) { :bar }
+ let(:param_value2) { 42 }
+ let(:param_name3) { :baz }
+ let(:param_value3) { 'xyz' }
+
+ before do
+ config = Ronin::Recon::Config.new(
+ params: {
+ worker1 => {
+ param_name1 => param_value1,
+ param_name2 => param_value2
+ },
+ worker2 => {
+ param_name3 => param_value3
+ }
+ }
+ )
+ config.save(config_file)
+ end
+
+ it "must print the concurrency values for the workers" do
+ config = Ronin::Recon::Config.load(config_file)
+
+ expect {
+ subject.run
+ }.to output(
+ [
+ "Workers:",
+ *config.workers.map { |worker|
+ " * #{worker}"
+ },
+ '',
+ 'Params:',
+ " * #{worker1}",
+ " * #{param_name1}=#{param_value1}",
+ " * #{param_name2}=#{param_value2}",
+ " * #{worker2}",
+ " * #{param_name3}=#{param_value3}",
+ ''
+ ].join($/)
+ ).to_stdout
+ end
+ end
+ end
+end
diff --git a/spec/cli/commands/config/set_spec.rb b/spec/cli/commands/config/set_spec.rb
new file mode 100644
index 00000000..5f5217cb
--- /dev/null
+++ b/spec/cli/commands/config/set_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+require 'ronin/recon/cli/commands/config/set'
+
+require_relative '../man_page_example'
+
+require 'tempfile'
+
+describe Ronin::Recon::CLI::Commands::Config::Set do
+ include_examples "man_page"
+
+ describe "#run" do
+ let(:tempfile) { Tempfile.new(['ronin-recon-config-','.yml']) }
+ let(:config_file) { tempfile.path }
+
+ let(:worker) { 'test/worker1' }
+
+ context "when the --concurrency option is given" do
+ let(:concurrency) { 10 }
+
+ before do
+ config = Ronin::Recon::Config.new
+ config.save(config_file)
+ end
+
+ before do
+ subject.option_parser.parse(
+ [
+ '--config-file', config_file,
+ '--concurrency', "#{worker}=#{concurrency}"
+ ]
+ )
+ end
+
+ it "must print the concurrency value for the worker to stdout" do
+ subject.run
+
+ config = Ronin::Recon::Config.load(config_file)
+ expect(config.concurrency[worker]).to eq(concurrency)
+ end
+ end
+
+ context "when the --param option is given" do
+ let(:param_name) { :foo }
+ let(:param_value) { 'bar' }
+
+ before do
+ config = Ronin::Recon::Config.new
+ config.save(config_file)
+ end
+
+ before do
+ subject.option_parser.parse(
+ [
+ '--config-file', config_file,
+ '--param', "#{worker}.#{param_name}=#{param_value}"
+ ]
+ )
+ end
+
+ it "must print the param valeu for the worker to stdout" do
+ subject.run
+
+ config = Ronin::Recon::Config.load(config_file)
+ expect(config.params[worker][param_name]).to eq(param_value)
+ end
+ end
+
+ context "when neither the --concurrency or --param options are given" do
+ it "must print an error and exit with 1" do
+ expect(subject).to receive(:print_error).with("--concurrency or --param options must be given")
+
+ expect {
+ subject.run
+ }.to raise_error(SystemExit) do |error|
+ expect(error.status).to eq(-1)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/cli/commands/config/unset_spec.rb b/spec/cli/commands/config/unset_spec.rb
new file mode 100644
index 00000000..7b6bd385
--- /dev/null
+++ b/spec/cli/commands/config/unset_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+require 'ronin/recon/cli/commands/config/unset'
+
+require_relative '../man_page_example'
+
+require 'tempfile'
+
+describe Ronin::Recon::CLI::Commands::Config::Unset do
+ include_examples "man_page"
+
+ describe "#run" do
+ let(:tempfile) { Tempfile.new(['ronin-recon-config-','.yml']) }
+ let(:config_file) { tempfile.path }
+
+ let(:worker) { 'test/worker1' }
+
+ context "when the --concurrency option is given" do
+ let(:concurrency) { 10 }
+
+ before do
+ config = Ronin::Recon::Config.new(concurrency: {worker => concurrency})
+ config.save(config_file)
+ end
+
+ before do
+ subject.option_parser.parse(
+ [
+ '--config-file', config_file,
+ '--concurrency', worker
+ ]
+ )
+ end
+
+ it "must print the concurrency value for the worker to stdout" do
+ subject.run
+
+ config = Ronin::Recon::Config.load(config_file)
+ expect(config.concurrency).to_not have_key(worker)
+ end
+ end
+
+ context "when the --param option is given" do
+ let(:param_name) { :foo }
+ let(:param_value) { 'bar' }
+
+ before do
+ config = Ronin::Recon::Config.new(
+ params: {
+ worker => {
+ param_name => param_value
+ }
+ }
+ )
+ config.save(config_file)
+ end
+
+ before do
+ subject.option_parser.parse(
+ [
+ '--config-file', config_file,
+ '--param', "#{worker}.#{param_name}"
+ ]
+ )
+ end
+
+ it "must print the param valeu for the worker to stdout" do
+ subject.run
+
+ config = Ronin::Recon::Config.load(config_file)
+ expect(config.params[worker]).to_not have_key(param_name)
+ end
+ end
+
+ context "when neither the --concurrency or --param options are given" do
+ it "must print an error and exit with 1" do
+ expect(subject).to receive(:print_error).with("--concurrency or --param options must be given")
+
+ expect {
+ subject.run
+ }.to raise_error(SystemExit) do |error|
+ expect(error.status).to eq(-1)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/cli/commands/config_spec.rb b/spec/cli/commands/config_spec.rb
new file mode 100644
index 00000000..16b68132
--- /dev/null
+++ b/spec/cli/commands/config_spec.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+require 'ronin/recon/cli/commands/config'
+
+require_relative 'man_page_example'
+
+describe Ronin::Recon::CLI::Commands::Config do
+ include_examples "man_page"
+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..e2770589 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
@@ -160,6 +162,17 @@
end
end
+ context "when the '--output-dir' option is given" do
+ let(:path) { 'path/to/dir' }
+ let(:argv) { ['--output-dir', path] }
+
+ it "must add the output directory path to #outputs" do
+ expect(subject.outputs).to eq([
+ [path, Ronin::Recon::OutputFormats::Dir]
+ ])
+ end
+ end
+
context "when the '--ignore' option is given" do
let(:value1) { Ronin::Recon::Values::Host.new('staging.example.com') }
let(:value2) { Ronin::Recon::Values::Host.new('dev.example.com') }
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/cli/commands/workers_spec.rb b/spec/cli/commands/workers_spec.rb
index e3242605..2985d1ff 100644
--- a/spec/cli/commands/workers_spec.rb
+++ b/spec/cli/commands/workers_spec.rb
@@ -1,15 +1,39 @@
require 'spec_helper'
require 'ronin/recon/cli/commands/workers'
+require 'ronin/recon/config'
describe Ronin::Recon::CLI::Commands::Workers do
describe "#run" do
+ let(:config) { Ronin::Recon::Config.default }
+
it "must list all worker IDs" do
expect {
subject.run
}.to output(
- Ronin::Recon.list_files.map { |id|
- " #{id}"
- }.join($/) + $/
+ [
+ " api/built_with ",
+ " api/crt_sh ",
+ " api/hunter_io ",
+ " api/security_trails ",
+ " api/zoom_eye ",
+ " dns/lookup [enabled] ",
+ " dns/mailservers [enabled] ",
+ " dns/nameservers [enabled] ",
+ " dns/reverse_lookup [enabled] ",
+ " dns/srv_enum [enabled] ",
+ " dns/subdomain_enum [enabled] ",
+ " dns/suffix_enum [enabled] ",
+ " net/ip_range_enum [enabled] ",
+ " net/port_scan [enabled] ",
+ " net/service_id [enabled] ",
+ " ssl/cert_enum [enabled] ",
+ " ssl/cert_grab [enabled] ",
+ " web/dir_enum [enabled] ",
+ " web/email_addresses [enabled] ",
+ " web/screenshot ",
+ " web/spider [enabled] ",
+ ""
+ ].join($/)
).to_stdout
end
@@ -20,11 +44,16 @@
expect {
subject.run(dir)
}.to output(
- Ronin::Recon.list_files.select { |id|
- id.start_with?("#{dir}/")
- }.map { |id|
- " #{id}"
- }.join($/) + $/
+ [
+ " dns/lookup [enabled] ",
+ " dns/mailservers [enabled] ",
+ " dns/nameservers [enabled] ",
+ " dns/reverse_lookup [enabled] ",
+ " dns/srv_enum [enabled] ",
+ " dns/subdomain_enum [enabled] ",
+ " dns/suffix_enum [enabled] ",
+ ""
+ ].join($/)
).to_stdout
end
end
diff --git a/spec/cli/config_file_option_spec.rb b/spec/cli/config_file_option_spec.rb
new file mode 100644
index 00000000..e88e0c12
--- /dev/null
+++ b/spec/cli/config_file_option_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+require 'ronin/recon/cli/config_file_option'
+require 'ronin/recon/cli/command'
+
+describe Ronin::Recon::CLI::ConfigFileOption do
+ module TestConfigFileOption
+ class TestCommand < Ronin::Recon::CLI::Command
+
+ include Ronin::Recon::CLI::ConfigFileOption
+
+ end
+ end
+
+ let(:test_command) { TestConfigFileOption::TestCommand }
+ subject { test_command.new }
+
+ describe ".included" do
+ subject { test_command }
+
+ it "must define a '-C, --config-file' option" do
+ expect(subject.options[:config_file]).to_not be(nil)
+ expect(subject.options[:config_file].short).to eq('-C')
+ expect(subject.options[:config_file].value).to_not be(nil)
+ expect(subject.options[:config_file].value.type).to be(String)
+ expect(subject.options[:config_file].desc).to eq('Loads the configuration file')
+ end
+ end
+
+ describe "#load_config" do
+ let(:fixtures) { File.join(__dir__,'..','fixtures') }
+
+ context "when the '-C, --config-file' option is not given" do
+ before { subject.load_config }
+
+ it "must set @config to `Config.default`" do
+ expect(subject.config).to eq(Ronin::Recon::Config.default)
+ end
+ end
+
+ context "when the '-C, --config-file' option is given" do
+ context "and the config file exists and is valid" do
+ let(:config_file) { File.join(fixtures,'config.yml') }
+
+ before do
+ subject.options[:config_file] = config_file
+
+ subject.load_config
+ end
+
+ it "must load the config file and set @config" do
+ expect(subject.config).to eq(
+ Ronin::Recon::Config.load(config_file)
+ )
+ end
+ end
+
+ context "but the config file does not exist" do
+ let(:config_file) { 'does/not/exist.yml' }
+
+ before do
+ subject.options[:config_file] = config_file
+ end
+
+ it "must print an error and exit with -1" do
+ expect(subject).to receive(:print_error).with("no such file or directory: #{config_file}")
+
+ expect {
+ subject.load_config
+ }.to raise_error(SystemExit) do |error|
+ expect(error.status).to eq(-1)
+ end
+ end
+ end
+
+ context "but the config file is invalid" do
+ let(:fixtures_dir) { File.join(__dir__,'..','fixtures') }
+ let(:config_file) { File.join(fixtures_dir,'config','does_not_contain_a_hash.yml') }
+ let(:yaml) { YAML.load_file(config_file) }
+
+ before do
+ subject.options[:config_file] = config_file
+ end
+
+ it "must print an error and exit with -2" do
+ expect(subject).to receive(:print_error).with("invalid config file (#{config_file.inspect}): must contain a Hash: #{yaml.inspect}")
+
+ expect {
+ subject.load_config
+ }.to raise_error(SystemExit) do |error|
+ expect(error.status).to eq(-2)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/config_spec.rb b/spec/config_spec.rb
index f4e147e9..fdcc7c34 100644
--- a/spec/config_spec.rb
+++ b/spec/config_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
require 'ronin/recon/config'
+require 'tmpdir'
+
describe Ronin::Recon::Config do
describe described_class::Workers do
describe "#initialize" do
@@ -168,6 +170,92 @@
end
end
end
+
+ describe "#to_h" do
+ context "when initialized the default set of workers" do
+ subject { described_class.default }
+
+ it "must return an empty Hash" do
+ expect(subject.to_h).to eq({})
+ end
+ end
+
+ context "when given a Set" do
+ let(:set) { Set['dns/lookup', 'web/dir_enum', 'web/new_worker'] }
+
+ subject { described_class.new(set) }
+
+ it "must return a Hash with the worker IDs from the default, that are not in the array, disabled and the worker IDs that are not in the default set, but in the array, enabled" do
+ expect(subject.to_h).to eq(
+ {
+ 'dns/mailservers' => false,
+ 'dns/nameservers' => false,
+ 'dns/reverse_lookup' => false,
+ 'dns/srv_enum' => false,
+ 'dns/subdomain_enum' => false,
+ 'dns/suffix_enum' => false,
+ 'net/ip_range_enum' => false,
+ 'net/port_scan' => false,
+ 'net/service_id' => false,
+ 'ssl/cert_enum' => false,
+ 'ssl/cert_grab' => false,
+ 'web/email_addresses' => false,
+ 'web/new_worker' => true,
+ 'web/spider' => false
+ }
+ )
+ end
+ end
+
+ context "when given an Array" do
+ let(:array) { %w[dns/lookup web/dir_enum web/new_worker] }
+
+ subject { described_class.new(array) }
+
+ it "must return a Hash with the worker IDs from the default, that are not in the array, disabled and the worker IDs that are not in the default set, but in the array, enabled" do
+ expect(subject.to_h).to eq(
+ {
+ 'dns/mailservers' => false,
+ 'dns/nameservers' => false,
+ 'dns/reverse_lookup' => false,
+ 'dns/srv_enum' => false,
+ 'dns/subdomain_enum' => false,
+ 'dns/suffix_enum' => false,
+ 'net/ip_range_enum' => false,
+ 'net/port_scan' => false,
+ 'net/service_id' => false,
+ 'ssl/cert_enum' => false,
+ 'ssl/cert_grab' => false,
+ 'web/email_addresses' => false,
+ 'web/new_worker' => true,
+ 'web/spider' => false
+ }
+ )
+ end
+ end
+
+ context "when initialized with a Hash" do
+ let(:hash) do
+ {
+ 'dns/lookup' => true,
+ 'web/dir_enum' => true,
+ 'net/port_scan' => false,
+ 'web/new_worker' => true
+ }
+ end
+
+ subject { described_class.new(hash) }
+
+ it "must return a Hash with the worker IDs that are disabled and enabled, which are not already in the default set" do
+ expect(subject.to_h).to eq(
+ {
+ 'net/port_scan' => false,
+ 'web/new_worker' => true
+ }
+ )
+ end
+ end
+ end
end
describe "#initialize" do
@@ -759,4 +847,37 @@
end
end
end
+
+ describe "#to_yaml" do
+ let(:expected_yml) { File.join(fixtures_dir,'with_params_and_workers.yml') }
+
+ subject { described_class.load(expected_yml) }
+
+ it "must convert Config into YAML string" do
+ expect(subject.to_yaml).to eq(File.read(expected_yml))
+ end
+ end
+
+ describe "#save" do
+ subject { described_class.default }
+
+ let(:tempdir) { Dir.mktmpdir('test-ronin-recon-config-save') }
+ let(:path) { File.join(tempdir, 'test-config.yml') }
+
+ it "must write Config converted to YAML into a file" do
+ subject.save(path)
+
+ expect(File.read(path)).to eq(subject.to_yaml)
+ end
+
+ context "when the parent directory does not exist yet" do
+ let(:path) { File.join(tempdir,'does','not','exist','test-config.yml') }
+
+ it "must create the parent directory" do
+ subject.save(path)
+
+ expect(File.directory?(File.dirname(path))).to be(true)
+ end
+ end
+ end
end
diff --git a/spec/fixtures/config/with_params_and_workers.yml b/spec/fixtures/config/with_params_and_workers.yml
new file mode 100644
index 00000000..64462999
--- /dev/null
+++ b/spec/fixtures/config/with_params_and_workers.yml
@@ -0,0 +1,9 @@
+---
+:params:
+ test/worker1:
+ :foo: a
+ :bar: b
+ test/worker2:
+ :foo: x
+ :bar: "y"
+:workers: {}
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/values/web_socket_spec.rb b/spec/values/web_socket_spec.rb
new file mode 100644
index 00000000..488ce115
--- /dev/null
+++ b/spec/values/web_socket_spec.rb
@@ -0,0 +1,316 @@
+require 'spec_helper'
+require 'ronin/recon/values/web_socket'
+
+describe Ronin::Recon::Values::WebSocket do
+ let(:scheme) { 'ws' }
+ let(:host) { 'example.com' }
+ let(:port) { 80 }
+ let(:path) { '/path' }
+ let(:query) { 'foo=bar' }
+ let(:url) { "#{scheme}://#{host}:#{port}#{path}?#{query}" }
+
+ subject { described_class.new(url) }
+
+ describe "#initialize" do
+ it "must set #scheme" do
+ expect(subject.scheme).to eq(scheme)
+ end
+
+ it "must set #host" do
+ expect(subject.host).to eq(host)
+ end
+
+ it "must set #port" do
+ expect(subject.port).to eq(port)
+ end
+
+ it "must set #path" do
+ expect(subject.path).to eq(path)
+ end
+
+ it "must set #query" do
+ expect(subject.query).to eq(query)
+ end
+ end
+
+ describe ".wss" do
+ subject { described_class.wss(host,port,path,query) }
+
+ it "must create a WebSocket object with #scheme of 'wss'" do
+ expect(subject.scheme).to eq('wss')
+ end
+
+ it "must set #host" do
+ expect(subject.host).to eq(host)
+ end
+
+ it "must set #port" do
+ expect(subject.port).to eq(port)
+ end
+
+ it "must set #path" do
+ expect(subject.path).to eq(path)
+ end
+
+ it "must set #query" do
+ expect(subject.query).to eq(query)
+ end
+ end
+
+ describe ".ws" do
+ subject { described_class.ws(host,port,path,query) }
+
+ it "must create a WebSocket object with #scheme of 'ws'" do
+ expect(subject.scheme).to eq('ws')
+ end
+
+ it "must set #host" do
+ expect(subject.host).to eq(host)
+ end
+
+ it "must set #port" do
+ expect(subject.port).to eq(port)
+ end
+
+ it "must set #path" do
+ expect(subject.path).to eq(path)
+ end
+
+ it "must set #query" do
+ expect(subject.query).to eq(query)
+ end
+ end
+
+ describe "#eql?" do
+ context "when given an WebSocket object" do
+ context "and the other WebSocket object has the same #scheme, #host, #port, #path and #query" do
+ let(:other) { described_class.new(url) }
+
+ it "must return true" do
+ expect(subject.eql?(other)).to be(true)
+ end
+ end
+
+ context "but the other WebSocket object has a different #scheme" do
+ let(:other_url) { "wss://#{host}:#{port}#{path}?#{query}" }
+ let(:other) { described_class.new(other_url) }
+
+ it "must return false" do
+ expect(subject.eql?(other)).to be(false)
+ end
+ end
+
+ context "but the other WebSocket object has a different #host" do
+ let(:other_url) { "#{scheme}://other.com:#{port}#{path}?#{query}" }
+ let(:other) { described_class.new(other_url) }
+
+ it "must return false" do
+ expect(subject.eql?(other)).to be(false)
+ end
+ end
+
+ context "but the other WebSocket object has a different #port" do
+ let(:other_url) { "#{scheme}://#{host}:8000#{path}?#{query}" }
+ let(:other) { described_class.new(other_url) }
+
+ it "must return false" do
+ expect(subject.eql?(other)).to be(false)
+ end
+ end
+
+ context "but the other WebSocket object has a different #path" do
+ let(:other_url) { "#{scheme}://#{host}:#{port}/not_path?#{query}" }
+ let(:other) { described_class.new(other_url) }
+
+ it "must return false" do
+ expect(subject.eql?(other)).to be(false)
+ end
+ end
+
+ context "but the other WebSocket object has a different #query" do
+ let(:other_url) { "#{scheme}://#{host}:#{port}#{path}?different=query" }
+ let(:other) { described_class.new(other_url) }
+
+ it "must return false" do
+ expect(subject.eql?(other)).to be(false)
+ end
+ end
+ end
+
+ context "when given a non-WebSocket object" do
+ let(:other) { Object.new }
+
+ it "must return false" do
+ expect(subject.eql?(other)).to be(false)
+ end
+ end
+ end
+
+ describe "#===" do
+ context "when given an WebSocket object" do
+ context "and the other WebSocket object has the same #scheme, #host, #port, #path and #query" do
+ let(:other) { described_class.new(url) }
+
+ it "must return true" do
+ expect(subject === other).to be(true)
+ end
+ end
+
+ context "but the other WebSocket object has a different #scheme" do
+ let(:other_url) { "wss://#{host}:#{port}#{path}?#{query}" }
+ let(:other) { described_class.new(other_url) }
+
+ it "must return false" do
+ expect(subject === other).to be(false)
+ end
+ end
+
+ context "but the other WebSocket object has a different #host" do
+ let(:other_url) { "#{scheme}://other.com:#{port}#{path}?#{query}" }
+ let(:other) { described_class.new(other_url) }
+
+ it "must return false" do
+ expect(subject === other).to be(false)
+ end
+ end
+
+ context "but the other WebSocket object has a different #port" do
+ let(:other_url) { "#{scheme}://#{host}:8000#{path}?#{query}" }
+ let(:other) { described_class.new(other_url) }
+
+ it "must return false" do
+ expect(subject === other).to be(false)
+ end
+ end
+
+ context "but the other WebSocket object has a different #path" do
+ let(:other_url) { "#{scheme}://#{host}:#{port}/not_path?#{query}" }
+ let(:other) { described_class.new(other_url) }
+
+ it "must return false" do
+ expect(subject === other).to be(false)
+ end
+ end
+
+ context "but the other WebSocket object has a different #query" do
+ let(:other_url) { "#{scheme}://#{host}:#{port}#{path}?different=query" }
+ let(:other) { described_class.new(other_url) }
+
+ it "must return false" do
+ expect(subject === other).to be(false)
+ end
+ end
+ end
+
+ context "when given a non-WebSocket object" do
+ let(:other) { Object.new }
+
+ it "must return false" do
+ expect(subject === other).to be(false)
+ end
+ end
+ end
+
+ describe "#hash" do
+ it "must return the #hash of an Array containing the class and the #scheme, #host, and #port" do
+ expect(subject.hash).to eq([described_class, scheme, host, port, path, query].hash)
+ end
+ end
+
+ describe "#to_uri" do
+ context "when the #scheme is wss" do
+ let(:scheme) { 'wss' }
+
+ it "must return URI object with 'wss' scheme" do
+ uri = subject.to_uri
+
+ expect(uri.scheme).to eq('wss')
+ expect(uri.host).to eq(host)
+ expect(uri.port).to eq(port)
+ expect(uri.path).to eq(path)
+ expect(uri.query).to eq(query)
+ end
+ end
+
+ context "when the #scheme is ws" do
+ let(:scheme) { 'ws' }
+
+ it "must return URI object with 'ws' scheme" do
+ uri = subject.to_uri
+
+ expect(uri.scheme).to eq('ws')
+ expect(uri.host).to eq(host)
+ expect(uri.port).to eq(port)
+ expect(uri.path).to eq(path)
+ expect(uri.query).to eq(query)
+ end
+ end
+ end
+
+ describe "#to_s" do
+ it "must return a String URL for the WebSocket" do
+ expect(subject.to_s).to eq("#{scheme}://#{host}#{path}?#{query}")
+ end
+
+ context "when the #scheme is 'ws'" do
+ let(:scheme) { 'ws' }
+
+ context "and the #port is 80" do
+ let(:port) { 80 }
+
+ it "must omit the port from the URL String" do
+ expect(subject.to_s).to eq("#{scheme}://#{host}#{path}?#{query}")
+ end
+ end
+
+ context "but the #port is not 80" do
+ let(:port) { 8000 }
+
+ it "must omit the port from the URL String" do
+ expect(subject.to_s).to eq("#{scheme}://#{host}:#{port}#{path}?#{query}")
+ end
+ end
+ end
+
+ context "when the #scheme is 'wss'" do
+ let(:scheme) { 'wss' }
+
+ context "and the #port is 443" do
+ let(:port) { 443 }
+
+ it "must omit the port from the URL String" do
+ expect(subject.to_s).to eq("#{scheme}://#{host}#{path}?#{query}")
+ end
+ end
+
+ context "but the #port is not 443" do
+ let(:port) { 9000 }
+
+ it "must omit the port from the URL String" do
+ expect(subject.to_s).to eq("#{scheme}://#{host}:#{port}#{path}?#{query}")
+ end
+ end
+ end
+ end
+
+ describe "#as_json" do
+ it "must return a Hash containing the type: and scheme:, host:, and port: attributes" do
+ expect(subject.as_json).to eq(
+ {
+ type: :web_socket,
+ scheme: scheme,
+ host: host,
+ port: port,
+ path: path,
+ query: query
+ }
+ )
+ end
+ end
+
+ describe ".value_type" do
+ it "must return :web_socket" do
+ expect(described_class.value_type).to eq(:web_socket)
+ 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