Skip to content

Commit

Permalink
Add a SecurityTrails API worker
Browse files Browse the repository at this point in the history
This worker will accept a domain as input and, given a valid API key,
will return a list of subdomain hosts by querying the Security Trails API.
  • Loading branch information
nirebu committed Aug 11, 2024
1 parent 349ab88 commit dbcf49e
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 0 deletions.
110 changes: 110 additions & 0 deletions lib/ronin/recon/builtin/api/security_trails.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# 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 <https://www.gnu.org/licenses/>.
#

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://crt.sh`.
#
# @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
subdomains = body.fetch(:subdomains, [])
full_domains = Set.new

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
86 changes: 86 additions & 0 deletions spec/builtin/api/security_trails_spec.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit dbcf49e

Please sign in to comment.