Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an optional worker for the hunter.io API #167

Merged
merged 2 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions lib/ronin/recon/builtin/api/hunter_io.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# frozen_string_literal: true
#
# ronin-recon - A micro-framework and tool for performing reconnaissance.
#
# Copyright (c) 2023-2024 Hal Brodigan ([email protected])
#
# 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 <https://www.gnu.org/licenses/>.
#

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
postmodern marked this conversation as resolved.
Show resolved Hide resolved

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
postmodern marked this conversation as resolved.
Show resolved Hide resolved

accepts Domain
outputs EmailAddress
intensity :passive
concurrency 1

param :api_key, String, required: true,
default: ENV['HUNTER_IO_API_KEY'],
desc: 'The API key for hunter.io'

# The HTTP client for `https://api.hunter.io`.
#
# @return [Async::HTTP::Client]
#
# @api private
attr_reader :client

#
# Initializes the `api/hunter` worker.
#
# @param [Hash{Symbol => Object}] kwargs
# Additional keyword arguments.
#
# @api private
#
def initialize(**kwargs)
super(**kwargs)

@client = Async::HTTP::Client.new(
Async::HTTP::Endpoint.for('https', 'api.hunter.io')
)
end

#
# Returns email addresses corresponding to domain."
#
# @param [Values::Domain] domain
# The domain value to gather email addresses for.
#
# @yield [email]
# For each email address found through the API, a EmailAddress
# value will be yielded.
#
# @yieldparam [Values::EmailAddress] email_address
# The emial addresses found.
#
def process(domain)
path = "/v2/domain-search?domain=#{domain}&api_key=#{params[:api_key]}"
response = @client.get(path)
body = begin
JSON.parse(response.read, symbolize_names: true)
ensure
response.close
end

emails = body.fetch(:data, {}).fetch(:emails, [])

emails.each do |email|
if (email_addr = email[:value])
yield EmailAddress.new(email_addr)
end
end
end

end
end
end
end
74 changes: 74 additions & 0 deletions spec/builtin/api/hunter_io_spec.rb
Original file line number Diff line number Diff line change
@@ -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\":\"[email protected]\"},{\"value\":\"[email protected]\"}]}}"
end
let(:expected) do
%w[
[email protected]
[email protected]
]
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