Skip to content

Commit

Permalink
Add an optional worker for the hunter.io API
Browse files Browse the repository at this point in the history
  • Loading branch information
AI-Mozi committed Aug 23, 2024
1 parent 9e8f021 commit 58aeba3
Show file tree
Hide file tree
Showing 2 changed files with 187 additions and 0 deletions.
113 changes: 113 additions & 0 deletions lib/ronin/recon/builtin/api/hunter_io.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# 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

register 'api/hunter_io'

summary "Queries the Domains https://api.hunter.io/domain-search"
description <<~DESC
Queries the Domains https://api.hunter.io/domain-search and returns
corresponding email addresses.
The hunter.io API key can be specified via the api/hunter_io.api_key
param or the HUNTER_IO_API_KEY env variables.
DESC

accepts Domain
outputs EmailAddress
intensity :passive
concurrency 1

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

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

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

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

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

emails = body.fetch(:data, {}).fetch(:emails, [])
emails.each do |email|
if (email_addr = email[:value])
yield EmailAddress.new(email_addr)
end
end
end

end
end
end
end
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

0 comments on commit 58aeba3

Please sign in to comment.