diff --git a/Gemfile b/Gemfile index 85a4ea0f..8da80b3f 100644 --- a/Gemfile +++ b/Gemfile @@ -15,8 +15,8 @@ end # gem 'ronin-support', '~> 1.1', github: 'ronin-rb/ronin-support', # branch: 'main' -# gem 'ronin-core', '~> 0.2', github: 'ronin-rb/ronin-core', -# branch: 'main' +gem 'ronin-core', '~> 0.3', github: 'ronin-rb/ronin-core', + branch: '0.3.0' # gem 'ronin-repos', '~> 0.1', github: 'ronin-rb/ronin-repos', # branch: 'main' diff --git a/README.md b/README.md index 3d79441c..32f3d4a7 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Commands: irb new run - test + run-worker, test worker workers ``` @@ -219,21 +219,19 @@ Generate a boilerplate recon worker file, with some custom information: ```shell $ ronin-recon new example_worker.rb \ - --name Example \ - --authors Postmodern \ + --author Postmodern \ --description "This is an example." ``` -Generate a ronin repository of your own payloads (or exploits): +Generate a ronin repository of your own recon workers: ```shell $ ronin-repos new my-repo $ cd my-repo/ $ mkdir recon $ ronin-recon new recon/my_recon.rb \ - --name MyRecon \ - --authors You \ - --description "This is my payload." + --author You \ + --description "This is my recon worker." $ vim recon/my_recon.rb $ git add recon/my_recon.rb $ git commit @@ -313,7 +311,7 @@ end * [async-http] ~> 0.60 * [wordlist] ~> 1.0, >= 1.0.3 * [ronin-support] ~> 1.1 -* [ronin-core] ~> 0.2 +* [ronin-core] ~> 0.3 * [ronin-db] ~> 0.2 * [ronin-repos] ~> 0.1 * [ronin-nmap] ~> 0.1 diff --git a/data/templates/worker.rb.erb b/data/templates/worker.rb.erb index 56220ea7..9691cc01 100644 --- a/data/templates/worker.rb.erb +++ b/data/templates/worker.rb.erb @@ -1,4 +1,4 @@ -#!/usr/bin/env -S ronin-recon test -f +#!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/<%= @worker_type[:file] -%>' diff --git a/gemspec.yml b/gemspec.yml index 4cba8628..5534f807 100644 --- a/gemspec.yml +++ b/gemspec.yml @@ -31,7 +31,7 @@ generated_files: - man/ronin-recon-new.1 - man/ronin-recon-workers.1 - man/ronin-recon-worker.1 - - man/ronin-recon-test.1 + - man/ronin-recon-run-worker.1 - man/ronin-recon-run.1 - data/wordlists/subdomains-1000.txt.gz - data/wordlists/raft-small-directories.txt.gz @@ -46,7 +46,7 @@ dependencies: wordlist: ~> 1.0, >= 1.0.3 # Ronin dependencies: ronin-support: ~> 1.1 - ronin-core: ~> 0.2 + ronin-core: ~> 0.3 ronin-db: ~> 0.2 ronin-repos: ~> 0.1 ronin-nmap: ~> 0.1 diff --git a/lib/ronin/recon/builtin/api/built_with.rb b/lib/ronin/recon/builtin/api/built_with.rb new file mode 100644 index 00000000..f6baaa49 --- /dev/null +++ b/lib/ronin/recon/builtin/api/built_with.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true +# +# ronin-recon - A micro-framework and tool for performing reconnaissance. +# +# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com) +# +# ronin-recon is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-recon is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-recon. If not, see . +# + +require_relative '../../worker' + +require 'async/http/internet/instance' +require 'set' + +module Ronin + module Recon + module API + # + # A recon worker that queries https://api.builtwith.com and return + # informations for given domain + # + # ## Environment Variables + # + # * `BUILT_WITH_API_KEY` - Specifies the API key used for authorization. + # + class BuiltWith < Worker + + register 'api/built_with' + + summary "Queries the domain informations from https://api.builtwith.com" + description <<~DESC + Queriest the domain informations from https://api.builtwith.com. + + The BuiltWith API key can be specified via the api/built_with.api_key + param or the BUILT_WITH_API_KEY environment variables. + DESC + + accepts Domain + outputs Domain, EmailAddress + intensity :passive + concurrency 1 + + param :api_key, String, required: true, + default: ENV['BUILT_WITH_API_KEY'], + desc: 'The API key for BuiltWith' + + # The HTTP client for `https://api.builtwith.com` + # + # @return [Async::HTTP::Client] + # + # @api private + attr_reader :client + + # + # Initializes the `api/built_with` worker. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments. + # + # @api private + # + def initialize(**kwargs) + super(**kwargs) + + @client = Async::HTTP::Client.new( + Async::HTTP::Endpoint.for('https', 'api.builtwith.com') + ) + end + + # + # Returns all informations queried for given domain + # + # @param [Values::Domain] domain + # The domain value to gather informations for. + # + # @yield [Value] value + # The found value will be yielded + # + # @yieldparam [Values::Domain, Values::EmailAddress] + # The found domains or email addresses + # + def process(domain) + path = "/v21/api.json?KEY=#{params[:api_key]}&LOOKUP=#{domain}" + response = client.get(path) + body = begin + JSON.parse(response.read, symbolize_names: true) + ensure + response.close + end + + domains = Set.new + email_addresses = Set.new + + if (results = body[:Results]) + results.each do |result| + if (paths = result.dig(:Result, :Paths)) + paths.each do |result_path| + if (sub_domain = result_path[:SubDomain]) + new_domain = "#{sub_domain}.#{domain}" + + yield Domain.new(new_domain) if domains.add?(new_domain) + end + end + end + + if (emails = result.dig(:Meta, :Emails)) + emails.each do |email| + yield EmailAddress.new(email) if email_addresses.add?(email) + end + end + end + end + end + + end + end + end +end diff --git a/lib/ronin/recon/builtin/api/hunter_io.rb b/lib/ronin/recon/builtin/api/hunter_io.rb new file mode 100755 index 00000000..234188f8 --- /dev/null +++ b/lib/ronin/recon/builtin/api/hunter_io.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true +# +# ronin-recon - A micro-framework and tool for performing reconnaissance. +# +# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com) +# +# ronin-recon is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-recon is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-recon. If not, see . +# + +require_relative '../../worker' + +require 'async/http/internet' + +module Ronin + module Recon + module API + # + # A recon worker that queries https://api.hunter.io/domain-search + # and returns corresponding email addresses. + # + # ## Environment Variables + # + # * `HUNTER_IO_API_KEY` - Specifies the API key used for authorization. + # + class HunterIO < Worker + + register 'api/hunter_io' + + summary "Queries the Domains https://api.hunter.io/domain-search" + description <<~DESC + Queries the Domains https://api.hunter.io/domain-search and returns + corresponding email addresses. + + The hunter.io API key can be specified via the api/hunter_io.api_key + param or the HUNTER_IO_API_KEY env variables. + DESC + + accepts Domain + outputs EmailAddress + intensity :passive + concurrency 1 + + param :api_key, String, required: true, + default: ENV['HUNTER_IO_API_KEY'], + desc: 'The API key for hunter.io' + + # The HTTP client for `https://api.hunter.io`. + # + # @return [Async::HTTP::Client] + # + # @api private + attr_reader :client + + # + # Initializes the `api/hunter` worker. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments. + # + # @api private + # + def initialize(**kwargs) + super(**kwargs) + + @client = Async::HTTP::Client.new( + Async::HTTP::Endpoint.for('https', 'api.hunter.io') + ) + end + + # + # Returns email addresses corresponding to domain." + # + # @param [Values::Domain] domain + # The domain value to gather email addresses for. + # + # @yield [email] + # For each email address found through the API, a EmailAddress + # value will be yielded. + # + # @yieldparam [Values::EmailAddress] email_address + # The emial addresses found. + # + def process(domain) + path = "/v2/domain-search?domain=#{domain}&api_key=#{params[:api_key]}" + response = @client.get(path) + body = begin + JSON.parse(response.read, symbolize_names: true) + ensure + response.close + end + + if (emails = body.dig(:data, :emails)) + emails.each do |email| + if (email_addr = email[:value]) + yield EmailAddress.new(email_addr) + end + end + end + end + + end + end + end +end diff --git a/lib/ronin/recon/builtin/api/security_trails.rb b/lib/ronin/recon/builtin/api/security_trails.rb new file mode 100755 index 00000000..debaad1c --- /dev/null +++ b/lib/ronin/recon/builtin/api/security_trails.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true +# +# ronin-recon - A micro-framework and tool for performing reconnaissance. +# +# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com) +# +# ronin-recon is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-recon is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-recon. If not, see . +# + +require 'ronin/recon/worker' +require 'ronin/support/text/patterns/network' + +require 'async/http/internet/instance' +require 'set' + +module Ronin + module Recon + module API + # + # A recon worker that queries https://securitytrails.com and returns subdomains + # for a given domain. + # + class SecurityTrails < Worker + + register 'api/security_trails' + + author "Nicolò Rebughini", email: "nicolo.rebughini@gmail.com" + summary "Queries the Domains https://securitytrails.com API" + description <<~DESC + Queries the Domains https://securitytrails.com API and returns the subdomains + of the domain. + DESC + + accepts Domain + outputs Host + intensity :passive + concurrency 1 + + param :api_key, String, required: true, + default: ENV['SECURITYTRAILS_API_KEY'], + desc: 'The API key for SecurityTrails' + + # The HTTP client for `https://securitytrails.com`. + # + # @return [Async::HTTP::Client] + # + # @api private + attr_reader :client + + # + # Initializes the `api/security_trails` worker. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments. + # + # @api private + # + def initialize(**kwargs) + super(**kwargs) + + @client = Async::HTTP::Client.new( + Async::HTTP::Endpoint.for('https','api.securitytrails.com') + ) + end + + # + # Returns host from each domains certificate. + # + # @param [Values::Domain] domain + # The domain value to gather subdomains for. + # + # @yield [host] + # For each subdmomain found through the API, a Domain + # value will be yielded. + # + # @yieldparam [Values::Host] subdomain + # The host found. + # + def process(domain) + path = "/v1/domain/#{domain}/subdomains?children_only=false&include_inactive=false" + response = @client.get(path, { 'APIKEY' => params[:api_key] }) + body = begin + JSON.parse(response.read, symbolize_names: true) + ensure + response.close + end + full_domains = Set.new + + if (subdomains = body.fetch(:subdomains)) + subdomains.each do |subdomain| + full_domain = "#{subdomain}.#{domain}" + + yield Host.new(full_domain) if full_domains.add?(full_domain) + end + end + end + + end + end + end +end diff --git a/lib/ronin/recon/builtin/api/zoom_eye.rb b/lib/ronin/recon/builtin/api/zoom_eye.rb new file mode 100755 index 00000000..30e43367 --- /dev/null +++ b/lib/ronin/recon/builtin/api/zoom_eye.rb @@ -0,0 +1,121 @@ +# frozen_string_literal: true +# +# ronin-recon - A micro-framework and tool for performing reconnaissance. +# +# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com) +# +# ronin-recon is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-recon is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-recon. If not, see . +# + +require_relative '../../worker' + +require 'async/http/internet' + +module Ronin + module Recon + module API + # + # A recon worker that queries https://api.zoomeye.hk/domain/search + # and returns subdomain and ip addresses for a given domain + # + # ## Environment Variables + # + # * `ZOOM_EYE_API_KEY` - Specifies the API key used for authorization. + # + class ZoomEye < Worker + + register 'api/zoom_eye' + + summary "Queries the Domains https://api.zoomeye.hk API" + description <<~DESC + Queries the Domains https://api.zoomeye.hk API and returns subdomains + and ip addresses of the domain. + + The ZoomEye API key can be specified via the api/zoom_eye.api_key + param or the ZOOM_EYE_API_KEY env variables. + DESC + + accepts Domain + outputs Domain, IP + intensity :passive + concurrency 1 + + param :api_key, String, required: true, + default: ENV['ZOOM_EYE_API_KEY'], + desc: 'The API key for ZoomEye' + + # The HTTP client for `https://api.zoomeye.hk`. + # + # @return [Async::HTTP::Client] + # + # @api private + attr_reader :client + + # + # Initialize the `api/zoom_eye` worker. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments. + # + # @api private + # + def initialize(**kwargs) + super(**kwargs) + + @client = Async::HTTP::Client.new( + Async::HTTP::Endpoint.for('https', 'api.zoomeye.hk') + ) + end + + # + # Returns associated domain names and ip addresses + # + # @param [Values::Domain] domain + # The domain value to gather subdomains and ip_addresses for. + # + # @yield [value] + # For each subdomain found through the API, a Domain + # and optionaly IP will be yielded. + # + # @yieldparam [Values::Domain, Values::IP] value + # The domain or ip found. + # + def process(domain) + path = "/domain/search?q=#{domain}&type=1" + response = @client.get(path, { 'API-KEY' => params[:api_key] }) + body = begin + JSON.parse(response.read, symbolize_names: true) + ensure + response.close + end + + if (list = body[:list]) + list.each do |record| + if (subdomain = record[:name]) + yield Domain.new(subdomain) + end + + if (ip_addresses = record[:ip]) + ip_addresses.each do |ip_addr| + yield IP.new(ip_addr) + end + end + end + end + end + + end + end + end +end diff --git a/lib/ronin/recon/cli.rb b/lib/ronin/recon/cli.rb index 51964352..2e8815f8 100644 --- a/lib/ronin/recon/cli.rb +++ b/lib/ronin/recon/cli.rb @@ -46,6 +46,8 @@ class CLI command_name 'ronin-recon' version Ronin::Recon::VERSION + command_aliases['test'] = 'run-worker' + end end end diff --git a/lib/ronin/recon/cli/commands/run.rb b/lib/ronin/recon/cli/commands/run.rb index 21dc32c5..28d581bc 100644 --- a/lib/ronin/recon/cli/commands/run.rb +++ b/lib/ronin/recon/cli/commands/run.rb @@ -154,8 +154,7 @@ class Run < Command usage: 'FILE' }, desc: 'The output file to write results to' do |path| - options[:output] = path - options[:output_format] ||= OutputFormats.infer_from(path) + @outputs << [path, options[:output_format] || OutputFormats.infer_from(path)] end option :output_format, short: '-F', @@ -229,6 +228,11 @@ class Run < Command # @return [Array] attr_reader :ignore + # The output files and formats + # + # @return [Array<(String, Class)>] + attr_reader :outputs + # # Initializes the `ronin-recon run` command. # @@ -246,7 +250,8 @@ def initialize(**kwargs) @worker_params = Hash.new { |hash,key| hash[key] = {} } @worker_concurrency = {} - @ignore = [] + @ignore = [] + @outputs = [] end # @@ -261,9 +266,9 @@ def run(*values) values = values.map { |value| parse_value(value) } - output_file = if options[:output] && options[:output_format] - options[:output_format].open(options[:output]) - end + output_files = outputs.filter_map do |output, output_format| + output_format.open(output) + end if options[:import] require 'ronin/db' @@ -280,7 +285,7 @@ def run(*values) print_value(value,parent) end - if output_file + output_files.each do |output_file| engine.on(:value) do |value| output_file << value end @@ -308,7 +313,7 @@ def run(*values) end end ensure - output_file.close if options[:output] + output_files&.each(&:close) end end diff --git a/lib/ronin/recon/cli/commands/test.rb b/lib/ronin/recon/cli/commands/run_worker.rb similarity index 88% rename from lib/ronin/recon/cli/commands/test.rb rename to lib/ronin/recon/cli/commands/run_worker.rb index 26d01344..55faaa23 100644 --- a/lib/ronin/recon/cli/commands/test.rb +++ b/lib/ronin/recon/cli/commands/run_worker.rb @@ -31,11 +31,11 @@ module Recon class CLI module Commands # - # Loads an individual worker and tests it. + # Loads an individual worker and runs it. # # ## Usage # - # ronin-recon test [options] {--file FILE | NAME} {IP | IP-range | DOMAIN | HOST | WILDCARD | WEBSITE} + # ronin-recon run-worker [options] {--file FILE | NAME} {IP | IP-range | DOMAIN | HOST | WILDCARD | WEBSITE} # # ## Options # @@ -47,7 +47,9 @@ module Commands # # IP|IP-range|DOMAIN|HOST|WILDCARD|WEBSITE An initial recon value. # - class Test < WorkerCommand + # @since 0.2.0 + # + class RunWorker < WorkerCommand include DebugOption include Printing @@ -60,12 +62,12 @@ class Test < WorkerCommand usage: 'IP|IP-range|DOMAIN|HOST|WILDCARD|WEBSITE', desc: 'The initial recon value' - description 'Loads an individual worker and tests it' + description 'Loads an individual worker and runs it' - man_page 'ronin-recon-test.1' + man_page 'ronin-recon-run-worker.1' # - # Runs the `ronin-recon test` command. + # Runs the `ronin-recon run-worker` command. # # @param [String, nil] name # The optional worker name to load and print metadata for. diff --git a/lib/ronin/recon/cli/commands/worker.rb b/lib/ronin/recon/cli/commands/worker.rb index 60d1b000..728f2a82 100644 --- a/lib/ronin/recon/cli/commands/worker.rb +++ b/lib/ronin/recon/cli/commands/worker.rb @@ -95,12 +95,14 @@ def print_worker(worker) end puts - puts 'Outputs:' - puts - indent do - print_list(worker.outputs.map(&method(:value_class_name))) + if (outputs = worker.outputs) + puts 'Outputs:' + puts + indent do + print_list(outputs.map(&method(:value_class_name))) + end + puts end - puts puts "Intensity: #{worker.intensity}" diff --git a/lib/ronin/recon/output_formats.rb b/lib/ronin/recon/output_formats.rb index 823910e4..c8ef06f7 100644 --- a/lib/ronin/recon/output_formats.rb +++ b/lib/ronin/recon/output_formats.rb @@ -23,6 +23,8 @@ require_relative 'output_formats/svg' require_relative 'output_formats/png' require_relative 'output_formats/pdf' +require_relative 'output_formats/archive' +require_relative 'output_formats/git_archive' require 'ronin/core/output_formats' @@ -35,15 +37,17 @@ module Recon module OutputFormats include Core::OutputFormats - register :txt, '.txt', TXT - register :csv, '.csv', CSV - register :json, '.json', JSON - register :ndjson, '.ndjson', NDJSON - register :dir, '', Dir - register :dot, '.dot', Dot - register :svg, '.svg', SVG - register :png, '.png', PNG - register :pdf, '.pdf', PDF + register :txt, '.txt', TXT + register :csv, '.csv', CSV + register :json, '.json', JSON + register :ndjson, '.ndjson', NDJSON + register :dir, '', Dir + register :dot, '.dot', Dot + register :svg, '.svg', SVG + register :png, '.png', PNG + register :pdf, '.pdf', PDF + register :web_archive, '', Archive + register :web_git_archive, '', GitArchive end end end diff --git a/lib/ronin/recon/output_formats/archive.rb b/lib/ronin/recon/output_formats/archive.rb new file mode 100644 index 00000000..f4cbe881 --- /dev/null +++ b/lib/ronin/recon/output_formats/archive.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true +# +# ronin-recon - A micro-framework and tool for performing reconnaissance. +# +# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com) +# +# ronin-recon is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-recon is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-recon. If not, see . +# + +require 'ronin/web/spider/archive' + +module Ronin + module Recon + module OutputFormats + # + # Represents a web archive directory. + # + class Archive + + # + # Initializes new archive. + # + # @param [String] root + # The path to the root directory. + # + def initialize(root) + @archive = Ronin::Web::Spider::Archive.new(root) + end + + # + # Writes a new URL to it's specific file. + # + # @param [Value] value + # The value to write. + # + def <<(value) + if Values::URL === value + @archive.write(value.uri, value.body) + end + end + + end + end + end +end diff --git a/lib/ronin/recon/output_formats/git_archive.rb b/lib/ronin/recon/output_formats/git_archive.rb new file mode 100644 index 00000000..02d78575 --- /dev/null +++ b/lib/ronin/recon/output_formats/git_archive.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true +# +# ronin-recon - A micro-framework and tool for performing reconnaissance. +# +# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com) +# +# ronin-recon is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-recon is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-recon. If not, see . +# + +require 'ronin/web/spider/git_archive' + +module Ronin + module Recon + module OutputFormats + # + # Represents a web archive directory that is backed by Git. + # + class GitArchive + + # + # Initializes new Git repository. + # + # @param [String] root + # The path to the root directory. + # + def initialize(root) + @git_archive = Ronin::Web::Spider::GitArchive.new(root) + @git_archive.init unless @git_archive.git? + end + + # + # Writes a new URL to it's specific file in Git archive. + # + # @param [Value] value + # The value to write. + # + def <<(value) + if Values::URL === value + @git_archive.write(value.uri, value.body) + end + end + + end + end + end +end diff --git a/lib/ronin/recon/values/url.rb b/lib/ronin/recon/values/url.rb index 9eb8f054..d8230230 100644 --- a/lib/ronin/recon/values/url.rb +++ b/lib/ronin/recon/values/url.rb @@ -196,6 +196,27 @@ def as_json return hash end + # + # Case equality method used for fuzzy matching. + # + # @param [URL, Value] other + # The other value to compare. + # + # @return [Boolean] + # Indicates whether the other value is an URL with + # the same uri. + # + # @since 0.3.0 + # + def ===(other) + case other + when URL + @uri == other.uri + else + false + end + end + # # Returns the type or kind of recon value. # diff --git a/lib/ronin/recon/version.rb b/lib/ronin/recon/version.rb index b3840134..54c5f952 100644 --- a/lib/ronin/recon/version.rb +++ b/lib/ronin/recon/version.rb @@ -21,6 +21,6 @@ module Ronin module Recon # ronin-recon version - VERSION = '0.1.0' + VERSION = '0.2.0' end end diff --git a/lib/ronin/recon/worker.rb b/lib/ronin/recon/worker.rb index 59c8603f..4340065f 100644 --- a/lib/ronin/recon/worker.rb +++ b/lib/ronin/recon/worker.rb @@ -305,12 +305,9 @@ def self.accepts(*value_classes) # @param [Array>] value_classes # The optional new value class(es) to outputs. # - # @return [Array>] + # @return [Array>, nil] # the value class which the recon worker outputs. # - # @raise [NotImplementedError] - # No value class was defined for the recon worker. - # # @example define that the recon worker outputs Host values: # outputs Host # @@ -320,8 +317,6 @@ def self.outputs(*value_classes) else @outputs || if superclass < Worker superclass.outputs - else - raise(NotImplementedError,"#{self} did not set outputs") end end end diff --git a/man/ronin-recon-irb.1.md b/man/ronin-recon-irb.1.md index 7dd572c4..2c704d15 100644 --- a/man/ronin-recon-irb.1.md +++ b/man/ronin-recon-irb.1.md @@ -23,4 +23,4 @@ Postmodern ## SEE ALSO -[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-worker](ronin-recon-worker.1.md) [ronin-recon-run](ronin-recon-run.1.md) [ronin-recon-test](ronin-recon-test.1.md) +[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-worker](ronin-recon-worker.1.md) [ronin-recon-run](ronin-recon-run.1.md) [ronin-recon-run-worker](ronin-recon-run-worker.1.md) diff --git a/man/ronin-recon-test.1.md b/man/ronin-recon-run-worker.1.md similarity index 69% rename from man/ronin-recon-test.1.md rename to man/ronin-recon-run-worker.1.md index 377a4d08..76ee04d0 100644 --- a/man/ronin-recon-test.1.md +++ b/man/ronin-recon-run-worker.1.md @@ -1,16 +1,16 @@ -# ronin-recon-test 1 "2023-05-01" Ronin "User Manuals" +# ronin-recon-run-worker 1 "2023-05-01" Ronin "User Manuals" ## NAME -ronin-recon-test - Loads an individual worker and tests it +ronin-recon-run-worker - Loads an individual worker and runs it ## SYNOPSIS -`ronin-recon test` [*options*] {`--file` *FILE* \| *NAME*} {*IP* \| *IP-range* \| *DOMAIN* \| *HOST* \| *WILDCARD* \| *WEBSITE*} +`ronin-recon run-worker` [*options*] {`--file` *FILE* \| *NAME*} {*IP* \| *IP-range* \| *DOMAIN* \| *HOST* \| *WILDCARD* \| *WEBSITE*} ## DESCRIPTION -Loads an individual worker and tests it with an input value.. +Loads an individual worker and runs it with an input value.. ## ARGUMENTS @@ -52,4 +52,4 @@ Postmodern ## SEE ALSO -[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-run](ronin-recon-run.1.md) \ No newline at end of file +[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-run](ronin-recon-run.1.md) diff --git a/man/ronin-recon-run.1.md b/man/ronin-recon-run.1.md index e0dc595b..91da395e 100644 --- a/man/ronin-recon-run.1.md +++ b/man/ronin-recon-run.1.md @@ -1,4 +1,4 @@ -# ronin-recon-test 1 "2023-05-01" Ronin "User Manuals" +# ronin-recon-run-worker 1 "2023-05-01" Ronin "User Manuals" ## NAME @@ -112,4 +112,4 @@ Postmodern ## SEE ALSO -[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-worker](ronin-recon-worker.1.md) [ronin-recon-test](ronin-recon-test.1.md) +[ronin-recon-workers](ronin-recon-workers.1.md) [ronin-recon-worker](ronin-recon-worker.1.md) [ronin-recon-run-worker](ronin-recon-run-worker.1.md) diff --git a/man/ronin-recon.1.md b/man/ronin-recon.1.md index e7429cc8..fa7410db 100644 --- a/man/ronin-recon.1.md +++ b/man/ronin-recon.1.md @@ -42,8 +42,8 @@ Runs a `ronin-recon` *COMMAND*. *run* : Runs the recon engine with one or more initial values. -*test* -: Loads an individual worker and tests it. +*run-worker*, *test* +: Loads an individual worker and runs it. *worker* : Prints information about a recon worker. diff --git a/spec/builtin/api/built_with_spec.rb b/spec/builtin/api/built_with_spec.rb new file mode 100644 index 00000000..2eb235d5 --- /dev/null +++ b/spec/builtin/api/built_with_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' +require 'ronin/recon/builtin/api/built_with' +require 'webmock/rspec' + +describe Ronin::Recon::API::BuiltWith do + let(:api_key) { 'my-test-api-key' } + + subject { described_class.new(params: { api_key: api_key }) } + + it "must set concurrency to 1" do + expect(described_class.concurrency).to eq(1) + end + + describe "#process" do + context "for domain with subdomains" do + let(:domain) { Ronin::Recon::Values::Domain.new("example.com") } + let(:response_json) do + "{\"Results\":[{\"Result\":{\"Paths\":[{\"Domain\":\"example.com\",\"SubDomain\":\"api\"},{\"Domain\":\"example.com\",\"SubDomain\":\"test\"}]}}]}" + end + let(:expected) do + %w[ + api.example.com + test.example.com + ] + end + + before do + stub_request(:get, "https://api.builtwith.com/v21/api.json?KEY=#{api_key}&LOOKUP=#{domain}") + .to_return(status: 200, body: response_json) + end + + it "must yield Values::Domain for each subdomain" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to_not be_empty + expect(yielded_values).to all(be_kind_of(Ronin::Recon::Values::Domain)) + expect(yielded_values.map(&:name)).to eq(expected) + end + end + + context "for email addresses found on the lookup website" do + let(:domain) { Ronin::Recon::Values::Domain.new("example.com") } + let(:response_json) do + "{\"Results\":[{\"Meta\":{\"Emails\":[\"email@example.com\",\"test@example.com\"]}}]}" + end + let(:expected) do + %w[ + email@example.com + test@example.com + ] + end + + before do + stub_request(:get, "https://api.builtwith.com/v21/api.json?KEY=#{api_key}&LOOKUP=#{domain}") + .to_return(status: 200, body: response_json) + end + + it "must yield Values::EmailAddress for each email address" do + yielded_values = [] + + Async do + subject.process(domain) do |email| + yielded_values << email + end + end + + expect(yielded_values).to_not be_empty + expect(yielded_values).to all(be_kind_of(Ronin::Recon::Values::EmailAddress)) + expect(yielded_values.map(&:address)).to eq(expected) + end + end + end +end diff --git a/spec/builtin/api/hunter_io_spec.rb b/spec/builtin/api/hunter_io_spec.rb new file mode 100644 index 00000000..3b9458d6 --- /dev/null +++ b/spec/builtin/api/hunter_io_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' +require 'ronin/recon/builtin/api/hunter_io' + +require 'webmock/rspec' + +describe Ronin::Recon::API::HunterIO do + let(:api_key) { 'my-test-api-key' } + + subject { described_class.new(params: { api_key: api_key }) } + + describe "#initialize" do + it "must initialize #client for 'https://api.hunter.io'" do + expect(subject.client).to be_kind_of(Async::HTTP::Client) + end + end + + describe "#process" do + context "for domain with corresponding email addresses" do + let(:domain) { Ronin::Recon::Values::Domain.new("example.com") } + let(:response_json) do + "{\"data\":{\"emails\":[{\"value\":\"foo@example.com\"},{\"value\":\"bar@example.com\"}]}}" + end + let(:expected) do + %w[ + foo@example.com + bar@example.com + ] + end + + before do + stub_request(:get, "https://api.hunter.io/v2/domain-search?domain=#{domain}&api_key=#{api_key}") + .to_return(status: 200, body: response_json) + end + + it "must yield Values::EmailAddress for each subdomain" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to_not be_empty + expect(yielded_values).to all(be_kind_of(Ronin::Recon::Values::EmailAddress)) + expect(yielded_values.map(&:address)).to eq(expected) + end + end + + context "for domain with no email addresses" do + let(:domain) { Ronin::Recon::Values::Domain.new("invalid.com") } + let(:response_json) do + "{\"data\":{\"emails\":[]}}" + end + + before do + stub_request(:get, "https://api.hunter.io/v2/domain-search?domain=#{domain}&api_key=#{api_key}") + .to_return(status: 200, body: response_json) + end + + it "must not yield anything" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to be_empty + end + end + end +end diff --git a/spec/builtin/api/security_trails_spec.rb b/spec/builtin/api/security_trails_spec.rb new file mode 100644 index 00000000..6406414d --- /dev/null +++ b/spec/builtin/api/security_trails_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' +require 'ronin/recon/builtin/api/security_trails' + +require 'webmock/rspec' + +describe Ronin::Recon::API::SecurityTrails do + subject do + described_class.new(params: { api_key: 'my-test-api-key'}) + end + + it "must set concurrency to 1" do + expect(described_class.concurrency).to eq(1) + end + + describe "#initialize" do + it "must initialize #client for 'https://api.securitytrails.com'" do + expect(subject.client).to be_kind_of(Async::HTTP::Client) + # BUG: https://github.com/bblimke/webmock/issues/1060 + # expect(subject.client.endpoint).to be_kind_of(Async::HTTP::Endpoint) + # expect(subject.client.endpoint.scheme).to eq('https') + # expect(subject.client.endpoint.hostname).to eq('api.securitytrails.com') + # expect(subject.client.endpoint.port).to eq(443) + end + end + + describe "#process" do + context "for domain with subdomains" do + let(:domain) { Ronin::Recon::Values::Domain.new("example.com") } + let(:response_json) do + "{\"endpoint\":\"/v1/domain/example.com/subdomains\",\"meta\":{\"limit_reached\":true},\"subdomain_count\":3,\"subdomains\":[\"api\",\"test\",\"proxy\"]}" + end + let(:expected) do + %w[ + api.example.com + test.example.com + proxy.example.com + ] + end + + before do + stub_request(:get, "https://api.securitytrails.com/v1/domain/#{domain.name}/subdomains?children_only=false&include_inactive=false") + .with(headers: {APIKEY: 'my-test-api-key'}) + .to_return(status: 200, body: response_json) + end + + it "must yield Values::Domain for each subdomain" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to_not be_empty + expect(yielded_values).to all(be_kind_of(Ronin::Recon::Values::Host)) + expect(yielded_values.map(&:name)).to eq(expected) + end + end + + context "for domain with no subdomains" do + let(:domain) { Ronin::Recon::Values::Domain.new("invalid.com") } + let(:response_json) do + "{\"endpoint\":\"/v1/domain/invalid.com/subdomains\",\"count\":null,\"subdomains\":[]}" + end + + before do + stub_request(:get, "https://api.securitytrails.com/v1/domain/#{domain.name}/subdomains?children_only=false&include_inactive=false") + .with(headers: {APIKEY: 'my-test-api-key'}) + .to_return(status: 200, body: response_json) + end + + it "must not yield anything" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to be_empty + end + end + end +end diff --git a/spec/builtin/api/zoom_eye_spec.rb b/spec/builtin/api/zoom_eye_spec.rb new file mode 100644 index 00000000..f44dc952 --- /dev/null +++ b/spec/builtin/api/zoom_eye_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' +require 'ronin/recon/builtin/api/zoom_eye' + +require 'webmock/rspec' + +describe Ronin::Recon::API::ZoomEye do + let(:api_key) { 'my-test-api-key' } + + subject { described_class.new(params: { api_key: api_key }) } + + describe "#initialize" do + it "must initialize #client for 'https://api.zoomeye.hk'" do + expect(subject.client).to be_kind_of(Async::HTTP::Client) + end + end + + describe "#process" do + context "for domain with subdomains and ip_addresses" do + let(:domain) { Ronin::Recon::Values::Domain.new("example.com") } + let(:response_json) do + "{\"status\":200,\"total\":183386,\"list\":[{\"name\":\"api.example.com\",\"ip\":[\"1.1.1.1\"]},{\"name\":\"test.example.com\",\"ip\":[\"2.2.2.2\"]}]}" + end + let(:expected) do + [ + Ronin::Recon::Values::Domain.new('api.example.com'), + Ronin::Recon::Values::Domain.new('test.example.com'), + Ronin::Recon::Values::IP.new('1.1.1.1'), + Ronin::Recon::Values::IP.new('2.2.2.2') + ] + end + + before do + stub_request(:get, "https://api.zoomeye.hk/domain/search?q=#{domain}&type=1") + .with(headers: { "API-KEY" => 'my-test-api-key' }) + .to_return(status: 200, body: response_json) + end + + it "must yield Values::Domain and Values::IP for each subdomain" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to_not be_empty + expect(yielded_values).to match_array(expected) + end + end + + context "for domain with no subdomains" do + let(:domain) { Ronin::Recon::Values::Domain.new("invalid.com") } + let(:response_json) do + "{\"status\":200,\"total\":183386,\"list\":[]}" + end + + before do + stub_request(:get, "https://api.zoomeye.hk/domain/search?q=#{domain}&type=1") + .with(headers: { "API-KEY" => 'my-test-api-key' }) + .to_return(status: 200, body: response_json) + end + + it "must not yield anything" do + yielded_values = [] + + Async do + subject.process(domain) do |subdomain| + yielded_values << subdomain + end + end + + expect(yielded_values).to be_empty + end + end + end +end diff --git a/spec/cli/commands/new_spec.rb b/spec/cli/commands/new_spec.rb index a95ee133..8f8fa5ff 100644 --- a/spec/cli/commands/new_spec.rb +++ b/spec/cli/commands/new_spec.rb @@ -78,7 +78,7 @@ it "must generate a new file containing a new Ronin::Recon::Worker class" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -131,7 +131,7 @@ def process(value) it "must add a boilerplate `author` metadata attribute" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -173,7 +173,7 @@ def process(value) it "must override the author name in the `author ...` metadata attribute with the '--author' name" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -214,7 +214,7 @@ def process(value) it "must override the author email in the `author ...` metadata attribute with the '--author-email' email" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -257,7 +257,7 @@ def process(value) it "must fill in the `summary ...` metadata attribute with the '--summary' text" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -299,7 +299,7 @@ def process(value) it "must fill in the `description ...` metadata attribute with the '--description' text" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -344,7 +344,7 @@ def process(value) it "must fill in the `references [...]` metadata attribute containing the '--reference' URLs" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -391,7 +391,7 @@ def process(value) it "must set the `accepts ...` metadata attribute in the worker class with the '--accepts' value classes" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -438,7 +438,7 @@ def process(value) it "must set the `outputs ...` metadata attribute in the worker class with the '--outputs' value classes" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' @@ -480,7 +480,7 @@ def process(value) it "must add the `intensity :level` metadata attribute to the worker class using the '--intensity' level" do expect(File.read(path)).to eq( <<~RUBY - #!/usr/bin/env -S ronin-recon test -f + #!/usr/bin/env -S ronin-recon run-worker -f require 'ronin/recon/worker' diff --git a/spec/cli/commands/run_spec.rb b/spec/cli/commands/run_spec.rb index 1f3a7358..4febdc8f 100644 --- a/spec/cli/commands/run_spec.rb +++ b/spec/cli/commands/run_spec.rb @@ -147,7 +147,9 @@ end it "must set the :output_format option using the path's file extension" do - expect(subject.options[:output_format]).to be(Ronin::Core::OutputFormats::JSON) + expect(subject.outputs.size).to eq(1) + expect(subject.outputs[0][0]).to eq(path) + expect(subject.outputs[0][1]).to be(Ronin::Core::OutputFormats::JSON) end context "but the '--output-format' has already been specified" do diff --git a/spec/cli/commands/test_spec.rb b/spec/cli/commands/run_worker_spec.rb similarity index 96% rename from spec/cli/commands/test_spec.rb rename to spec/cli/commands/run_worker_spec.rb index fc699534..39a5d8ce 100644 --- a/spec/cli/commands/test_spec.rb +++ b/spec/cli/commands/run_worker_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -require 'ronin/recon/cli/commands/test' +require 'ronin/recon/cli/commands/run_worker' require 'fixtures/test_worker' -describe Ronin::Recon::CLI::Commands::Test do +describe Ronin::Recon::CLI::Commands::RunWorker do describe "#run" do let(:name) { 'test_worker' } let(:value) { 'example.com' } diff --git a/spec/cli/commands/worker_spec.rb b/spec/cli/commands/worker_spec.rb index 4d0ffb1a..12d8bbdf 100644 --- a/spec/cli/commands/worker_spec.rb +++ b/spec/cli/commands/worker_spec.rb @@ -131,5 +131,45 @@ OUTPUT ).to_stdout end + + context "when the worker class does not define `outputs`" do + module TestWorkerCommand + class WorkerWithoutOutputs < Ronin::Recon::Worker + + id 'worker_without_outputs' + summary 'Test worker without `outputs`' + description <<~DESC + Test printing a worker without an `outputs`. + DESC + + accepts URL + intensity :passive + + end + end + + let(:worker_class) { TestWorkerCommand::WorkerWithoutOutputs } + + it "must omit the 'Outputs:' line and list" do + expect { + subject.print_worker(worker_class) + }.to output( + <<~OUTPUT + [ worker_without_outputs ] + + Summary: Test worker without `outputs` + Description: + + Test printing a worker without an `outputs`. + + Accepts: + + * URL + + Intensity: passive + OUTPUT + ).to_stdout + end + end end end diff --git a/spec/output_formats/archive_spec.rb b/spec/output_formats/archive_spec.rb new file mode 100644 index 00000000..86947862 --- /dev/null +++ b/spec/output_formats/archive_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'ronin/recon/output_formats/archive' +require 'ronin/recon/values/url' +require 'ronin/recon/values/domain' +require 'tmpdir' + +describe Ronin::Recon::OutputFormats::Archive do + subject { described_class.new(path) } + + let(:path) { Dir.mktmpdir('ronin-recon-output-archive') } + + describe "#<<" do + context "for Values::URL" do + let(:value) { Ronin::Recon::Values::URL.new('https://www.example.com/foo.html') } + let(:expected_path) { File.join(path,value.path) } + + it "must create a new file with webpage" do + subject << value + + expect(File.exist?(expected_path)).to be(true) + end + end + + context "for other values" do + let(:value) { Ronin::Recon::Values::Domain.new('example.com') } + + it "must not create any files" do + subject << value + + expect(Dir.glob("#{path}/*")).to be_empty + end + end + end +end diff --git a/spec/output_formats/git_archive_spec.rb b/spec/output_formats/git_archive_spec.rb new file mode 100644 index 00000000..1ec39674 --- /dev/null +++ b/spec/output_formats/git_archive_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'ronin/recon/output_formats/git_archive' +require 'ronin/recon/values/url' +require 'ronin/recon/values/domain' +require 'tmpdir' + +describe Ronin::Recon::OutputFormats::GitArchive do + subject { described_class.new(path) } + + let(:path) { Dir.mktmpdir('ronin-recon-output-git-archive') } + + describe "#<<" do + context "for Values::URL" do + let(:value) { Ronin::Recon::Values::URL.new('https://www.example.com/foo.html') } + let(:expected_path) { File.join(path,value.path) } + + it "must create a new file with webpage" do + subject << value + + expect(File.exist?(expected_path)).to be(true) + end + end + + context "for other values" do + let(:value) { Ronin::Recon::Values::Domain.new('example.com') } + + it "must not create any files" do + subject << value + + expect(Dir.glob("#{path}/*")).to be_empty + end + end + end +end diff --git a/spec/values/url_spec.rb b/spec/values/url_spec.rb index 0930fead..3eb39e5e 100644 --- a/spec/values/url_spec.rb +++ b/spec/values/url_spec.rb @@ -1,5 +1,6 @@ require 'spec_helper' require 'ronin/recon/values/url' +require 'ronin/recon/values/domain' describe Ronin::Recon::Values::URL do let(:url) { 'https://www.example.com/index.html' } @@ -234,4 +235,34 @@ expect(subject.value_type).to be(:url) end end + + describe "#===" do + let(:url) { 'https://www.foo.example.com/index.html' } + + context "when given an URL object" do + context "and it has the same uri as the other URL value" do + let(:other) { described_class.new(url) } + + it "must return true" do + expect(subject === other).to be(true) + end + end + + context "but it has diffferent uri than the other URL value" do + let(:other) { described_class.new('https://www.example.net/index.html') } + + it "must return false" do + expect(subject === other).to be(false) + end + end + end + + context "when given non-URL object" do + let(:other) { Ronin::Recon::Values::Domain.new('example.com') } + + it "must return false" do + expect(subject === other).to be(false) + end + end + end end diff --git a/spec/worker_spec.rb b/spec/worker_spec.rb index bc05de13..c8ebbaf7 100644 --- a/spec/worker_spec.rb +++ b/spec/worker_spec.rb @@ -156,10 +156,8 @@ class WorkerWithoutOutputs < Ronin::Recon::Worker subject { TestWorkers::WorkerWithoutOutputs } - it "must raise a NotImplementedError excpetion when called" do - expect { - subject.outputs - }.to raise_error(NotImplementedError,"#{subject} did not set outputs") + it "must return nil" do + expect(subject.outputs).to be(nil) end context "but the Worker class inherits from another worker class" do