class RogueOne::Detector

Constants

GOOGLE_PUBLIC_DNS

Attributes

custom_list[R]
default_list[R]
max_concurrency[R]
record_type[R]
target[R]
verbose[R]

Public Class Methods

new(target:, custom_list: nil, default_list: "alexa", record_type: "A", threshold: nil, verbose: false) click to toggle source
# File lib/rogue_one/detector.rb, line 23
def initialize(target:,
               custom_list: nil,
               default_list: "alexa",
               record_type: "A",
               threshold: nil,
               verbose: false)
  @target = target

  @custom_list = custom_list
  @default_list = default_list
  @record_type = record_type.upcase.to_sym
  @threshold = threshold
  @verbose = verbose

  @max_concurrency = Etc.nprocessors * 2
  @memo = {}
  @verbose_memo = nil
end

Public Instance Methods

report() click to toggle source
# File lib/rogue_one/detector.rb, line 42
def report
  inspect

  {
    verdict: verdict,
    landing_pages: landing_pages,
    results: results,
    meta: meta
  }.compact
end

Private Instance Methods

bulk_resolve(resolver, domains) click to toggle source
# File lib/rogue_one/detector.rb, line 147
def bulk_resolve(resolver, domains)
  results = []

  Async do
    barrier = Async::Barrier.new
    semaphore = Async::Semaphore.new(max_concurrency, parent: barrier)

    domains.each do |domain|
      semaphore.async do
        addresses = []
        begin
          addresses = resolver.addresses_for(domain, dns_resource_by_record_type, { retries: 1 }).map(&:to_s)
        rescue Async::DNS::ResolutionFailure
          # do nothing
        end
        results << [domain, addresses]
      end
    end
  end
  results.to_h
end
custom_domains() click to toggle source
# File lib/rogue_one/detector.rb, line 126
def custom_domains
  read_domains custom_list
end
dns_resource_by_record_type() click to toggle source
# File lib/rogue_one/detector.rb, line 177
def dns_resource_by_record_type
  @dns_resource_by_record_type ||= dns_resources.dig(record_type)
end
dns_resources() click to toggle source
# File lib/rogue_one/detector.rb, line 181
def dns_resources
  {
    A: Resolv::DNS::Resource::IN::A,
    AAAA: Resolv::DNS::Resource::IN::AAAA,
  }
end
domains() click to toggle source
# File lib/rogue_one/detector.rb, line 122
def domains
  @domains ||= custom_list ? custom_domains : top_100_domains
end
inspect() click to toggle source
# File lib/rogue_one/detector.rb, line 99
def inspect
  return unless @memo.empty?

  # read domains outside of the async blocks
  load_domains

  normal_resolutions = bulk_resolve(normal_resolver, domains)
  resolutions = bulk_resolve(target_resolver, domains)

  results = resolutions.map do |domain, addresses|
    normal_addresses = normal_resolutions.dig(domain) || []
    address = (addresses || []).first
    [domain, address] if address && !normal_addresses.include?(address)
  end.compact.to_h

  @memo = results.values.group_by(&:itself).map { |k, v| [k, v.length] }.to_h
  @verbose_memo = results if verbose
end
landing_pages() click to toggle source
# File lib/rogue_one/detector.rb, line 76
def landing_pages
  @memo.map do |ip, count|
    count > threshold ? ip : nil
  end.compact.sort
end
load_domains() click to toggle source
# File lib/rogue_one/detector.rb, line 118
def load_domains
  domains
end
meta() click to toggle source
# File lib/rogue_one/detector.rb, line 67
def meta
  return nil unless verbose

  {
    record_type: record_type,
    threshold: threshold,
  }
end
normal_resolver() click to toggle source
# File lib/rogue_one/detector.rb, line 169
def normal_resolver
  Async::DNS::Resolver.new([[:udp, GOOGLE_PUBLIC_DNS, 53], [:tcp, GOOGLE_PUBLIC_DNS, 53]])
end
occurrences() click to toggle source
# File lib/rogue_one/detector.rb, line 95
def occurrences
  @memo.sort_by{ |_, v| -v }.to_h
end
read_domains(path) click to toggle source
# File lib/rogue_one/detector.rb, line 139
def read_domains(path)
  list = DomainList.new(path)
  return list.domains if list.valid?

  raise ArgumentError, "Inputted an invalid list. #{path} is not eixst." unless list.exists?
  raise ArgumentError, "Inputted an invalid list. Please input a list as an YAML file." unless list.valid_format?
end
resolutions() click to toggle source
# File lib/rogue_one/detector.rb, line 91
def resolutions
  (@verbose_memo || {}).sort_by { |_, v| v }.to_h
end
results() click to toggle source
# File lib/rogue_one/detector.rb, line 82
def results
  return nil unless verbose

  {
    resolutions: resolutions,
    occurrences: occurrences
  }
end
rogue_one?() click to toggle source
# File lib/rogue_one/detector.rb, line 59
def rogue_one?
  !landing_pages.empty?
end
target_resolver() click to toggle source
# File lib/rogue_one/detector.rb, line 173
def target_resolver
  Async::DNS::Resolver.new([[:udp, target, 53], [:tcp, target, 53]])
end
threshold() click to toggle source
# File lib/rogue_one/detector.rb, line 63
def threshold
  @threshold ||= (domains.length.to_f / 10.0).ceil
end
top_100_domains() click to toggle source
# File lib/rogue_one/detector.rb, line 130
def top_100_domains
  case default_list
  when "alexa"
    read_domains File.expand_path("./data/alexa_100.yml", __dir__)
  when "fortune"
    read_domains File.expand_path("./data/fortune_100.yml", __dir__)
  end
end
verdict() click to toggle source
# File lib/rogue_one/detector.rb, line 55
def verdict
  rogue_one? ? "rogue one" : "benign one"
end