phisher_phinder 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +15 -7
- data/bin/console +2 -2
- data/lib/phisher_phinder.rb +4 -1
- data/lib/phisher_phinder/command.rb +9 -4
- data/lib/phisher_phinder/display.rb +20 -0
- data/lib/phisher_phinder/host_information_finder.rb +50 -0
- data/lib/phisher_phinder/host_response_policy.rb +17 -0
- data/lib/phisher_phinder/link_explorer.rb +40 -0
- data/lib/phisher_phinder/link_host.rb +20 -0
- data/lib/phisher_phinder/mail_parser.rb +2 -10
- data/lib/phisher_phinder/mail_parser/body_parser.rb +21 -72
- data/lib/phisher_phinder/tracing_report.rb +25 -5
- data/lib/phisher_phinder/version.rb +1 -1
- data/phisher_phinder.gemspec +2 -0
- metadata +34 -3
- data/lib/phisher_phinder/contact_finder.rb +0 -37
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: bf9bd21584b11a5fe4ceff942aa6b7631fef9177682b775d9c0a0508a99c0643
         | 
| 4 | 
            +
              data.tar.gz: b10859f0c80054eeeff83288dde7eb25ef5bdf8a32d83cd04cfff181a39864a2
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: dcf8371fbe75476a791fd12c94be9e2c1fac881785b856a62a9aba67fdca39cdef6956aba9f5b772882f0b58f93ff810734f94f6c8abb2aff89dc0b12364f2c7
         | 
| 7 | 
            +
              data.tar.gz: b44b1edcb3c1dc0732478d512f75ef599b2fa9b74db3ecc981c0125f5547703b983ddd9dddb205c745688631f79f0722548021da1bd42106ec1f6282bccae88c
         | 
    
        data/Gemfile.lock
    CHANGED
    
    | @@ -3,6 +3,8 @@ PATH | |
| 3 3 | 
             
              specs:
         | 
| 4 4 | 
             
                phisher_phinder (0.3.0)
         | 
| 5 5 | 
             
                  dotenv (~> 2.7.5)
         | 
| 6 | 
            +
                  excon (~> 0.78.1)
         | 
| 7 | 
            +
                  mail
         | 
| 6 8 | 
             
                  maxmind-geoip2 (~> 0.4.0)
         | 
| 7 9 | 
             
                  nokogiri (~> 1.11.0)
         | 
| 8 10 | 
             
                  sequel (~> 5.33)
         | 
| @@ -14,7 +16,7 @@ PATH | |
| 14 16 | 
             
            GEM
         | 
| 15 17 | 
             
              remote: https://rubygems.org/
         | 
| 16 18 | 
             
              specs:
         | 
| 17 | 
            -
                activesupport (6.1. | 
| 19 | 
            +
                activesupport (6.1.1)
         | 
| 18 20 | 
             
                  concurrent-ruby (~> 1.0, >= 1.0.2)
         | 
| 19 21 | 
             
                  i18n (>= 1.6, < 2)
         | 
| 20 22 | 
             
                  minitest (>= 5.1)
         | 
| @@ -26,7 +28,7 @@ GEM | |
| 26 28 | 
             
                  bundler (>= 1.2.0, < 3)
         | 
| 27 29 | 
             
                  thor (>= 0.18, < 2)
         | 
| 28 30 | 
             
                coderay (1.1.2)
         | 
| 29 | 
            -
                concurrent-ruby (1.1. | 
| 31 | 
            +
                concurrent-ruby (1.1.8)
         | 
| 30 32 | 
             
                connection_pool (2.2.3)
         | 
| 31 33 | 
             
                crack (0.4.3)
         | 
| 32 34 | 
             
                  safe_yaml (~> 1.0.0)
         | 
| @@ -38,6 +40,7 @@ GEM | |
| 38 40 | 
             
                domain_name (0.5.20190701)
         | 
| 39 41 | 
             
                  unf (>= 0.0.5, < 1.0.0)
         | 
| 40 42 | 
             
                dotenv (2.7.6)
         | 
| 43 | 
            +
                excon (0.78.1)
         | 
| 41 44 | 
             
                ffi (1.14.2)
         | 
| 42 45 | 
             
                ffi-compiler (1.0.1)
         | 
| 43 46 | 
             
                  ffi (>= 1.0.0)
         | 
| @@ -51,18 +54,23 @@ GEM | |
| 51 54 | 
             
                http-cookie (1.0.3)
         | 
| 52 55 | 
             
                  domain_name (~> 0.5)
         | 
| 53 56 | 
             
                http-form_data (2.3.0)
         | 
| 54 | 
            -
                http-parser (1.2. | 
| 55 | 
            -
                  ffi-compiler
         | 
| 56 | 
            -
                i18n (1.8. | 
| 57 | 
            +
                http-parser (1.2.3)
         | 
| 58 | 
            +
                  ffi-compiler (>= 1.0, < 2.0)
         | 
| 59 | 
            +
                i18n (1.8.8)
         | 
| 57 60 | 
             
                  concurrent-ruby (~> 1.0)
         | 
| 61 | 
            +
                mail (2.7.1)
         | 
| 62 | 
            +
                  mini_mime (>= 0.1.1)
         | 
| 58 63 | 
             
                maxmind-db (1.1.1)
         | 
| 59 64 | 
             
                maxmind-geoip2 (0.4.0)
         | 
| 60 65 | 
             
                  connection_pool (~> 2.2)
         | 
| 61 66 | 
             
                  http (~> 4.3)
         | 
| 62 67 | 
             
                  maxmind-db (~> 1.1)
         | 
| 63 68 | 
             
                method_source (1.0.0)
         | 
| 64 | 
            -
                 | 
| 65 | 
            -
                 | 
| 69 | 
            +
                mini_mime (1.0.2)
         | 
| 70 | 
            +
                mini_portile2 (2.5.0)
         | 
| 71 | 
            +
                minitest (5.14.3)
         | 
| 72 | 
            +
                nokogiri (1.11.1)
         | 
| 73 | 
            +
                  mini_portile2 (~> 2.5.0)
         | 
| 66 74 | 
             
                  racc (~> 1.4)
         | 
| 67 75 | 
             
                pry (0.13.1)
         | 
| 68 76 | 
             
                  coderay (~> 1.1)
         | 
    
        data/bin/console
    CHANGED
    
    
    
        data/lib/phisher_phinder.rb
    CHANGED
    
    | @@ -8,7 +8,10 @@ require_relative './phisher_phinder/display' | |
| 8 8 |  | 
| 9 9 | 
             
            require_relative './phisher_phinder/body_hyperlink'
         | 
| 10 10 | 
             
            require_relative './phisher_phinder/cached_geoip_client'
         | 
| 11 | 
            -
            require_relative './phisher_phinder/ | 
| 11 | 
            +
            require_relative './phisher_phinder/host_information_finder'
         | 
| 12 | 
            +
            require_relative './phisher_phinder/host_response_policy'
         | 
| 13 | 
            +
            require_relative './phisher_phinder/link_explorer'
         | 
| 14 | 
            +
            require_relative './phisher_phinder/link_host'
         | 
| 12 15 | 
             
            require_relative './phisher_phinder/whois_email_extractor'
         | 
| 13 16 | 
             
            require_relative './phisher_phinder/null_lookup_client'
         | 
| 14 17 | 
             
            require_relative './phisher_phinder/null_response'
         | 
| @@ -14,11 +14,16 @@ module PhisherPhinder | |
| 14 14 | 
             
                  ip_factory = PhisherPhinder::ExtendedIpFactory.new(geoip_client: lookup_client)
         | 
| 15 15 | 
             
                  mail_parser = PhisherPhinder::MailParser::Parser.new(ip_factory, line_ending)
         | 
| 16 16 | 
             
                  whois_client = Whois::Client.new
         | 
| 17 | 
            +
                  host_information_finder = PhisherPhinder::HostInformationFinder.new(
         | 
| 18 | 
            +
                    whois_client: whois_client,
         | 
| 19 | 
            +
                    extractor: PhisherPhinder::WhoisEmailExtractor.new
         | 
| 20 | 
            +
                  )
         | 
| 17 21 | 
             
                  tracing_report = PhisherPhinder::TracingReport.new(
         | 
| 18 | 
            -
                    mail_parser.parse(contents),
         | 
| 19 | 
            -
                     | 
| 20 | 
            -
             | 
| 21 | 
            -
                       | 
| 22 | 
            +
                    mail:  mail_parser.parse(contents),
         | 
| 23 | 
            +
                    host_information_finder: host_information_finder,
         | 
| 24 | 
            +
                    link_explorer: PhisherPhinder::LinkExplorer.new(
         | 
| 25 | 
            +
                      host_information_finder: host_information_finder,
         | 
| 26 | 
            +
                      host_response_policy: PhisherPhinder::HostResponsePolicy.new
         | 
| 22 27 | 
             
                    )
         | 
| 23 28 | 
             
                  )
         | 
| 24 29 | 
             
                  tracing_report.report
         | 
| @@ -47,6 +47,22 @@ module PhisherPhinder | |
| 47 47 | 
             
                  )
         | 
| 48 48 |  | 
| 49 49 | 
             
                  puts trace_table
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  puts "\n\n"
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  puts 'Body Content'
         | 
| 54 | 
            +
                  puts "\n"
         | 
| 55 | 
            +
                  puts "Linked URLs"
         | 
| 56 | 
            +
                  input_data[:content][:linked_urls].each do |link_set|
         | 
| 57 | 
            +
                    link_set.each_with_index do |link_host, tab_count|
         | 
| 58 | 
            +
                      puts "#{"\t"*tab_count}" +
         | 
| 59 | 
            +
                        "#{link_host.url.to_s} " +
         | 
| 60 | 
            +
                        "(#{display_creation_date(link_host)}) " +
         | 
| 61 | 
            +
                        "[#{display_email_addresses(link_host.host_information[:abuse_contacts])}]" +
         | 
| 62 | 
            +
                        "\n"
         | 
| 63 | 
            +
                    end
         | 
| 64 | 
            +
                    puts "\n"
         | 
| 65 | 
            +
                  end
         | 
| 50 66 | 
             
                end
         | 
| 51 67 |  | 
| 52 68 | 
             
                private
         | 
| @@ -66,5 +82,9 @@ module PhisherPhinder | |
| 66 82 | 
             
                def display_email_addresses(email_addresses)
         | 
| 67 83 | 
             
                  email_addresses.map { |address| address.gsub(/[,<>]/, '') }.join(', ')
         | 
| 68 84 | 
             
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                def display_creation_date(link_host)
         | 
| 87 | 
            +
                  (date = link_host.host_information[:creation_date]) ? date.strftime('%Y-%m-%d %H:%M:%S') : nil
         | 
| 88 | 
            +
                end
         | 
| 69 89 | 
             
              end
         | 
| 70 90 | 
             
            end
         | 
| @@ -0,0 +1,50 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module PhisherPhinder
         | 
| 4 | 
            +
              class HostInformationFinder
         | 
| 5 | 
            +
                def initialize(whois_client:, extractor:)
         | 
| 6 | 
            +
                  @whois_client = whois_client
         | 
| 7 | 
            +
                  @extractor = extractor
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def information_for(address)
         | 
| 11 | 
            +
                  most_relevant_record = nil
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  begin
         | 
| 14 | 
            +
                    case address
         | 
| 15 | 
            +
                    when ExtendedIp
         | 
| 16 | 
            +
                      most_relevant_record = @whois_client.lookup(address.ip_address.to_s)
         | 
| 17 | 
            +
                    when String
         | 
| 18 | 
            +
                      hostname_parts = address.split('.')
         | 
| 19 | 
            +
                      until most_relevant_record do
         | 
| 20 | 
            +
                        address = hostname_parts.join('.')
         | 
| 21 | 
            +
                        whois_record = @whois_client.lookup(address)
         | 
| 22 | 
            +
                        if whois_record.parser.available?
         | 
| 23 | 
            +
                          hostname_parts = hostname_parts[1..-1]
         | 
| 24 | 
            +
                        else
         | 
| 25 | 
            +
                          most_relevant_record = whois_record
         | 
| 26 | 
            +
                        end
         | 
| 27 | 
            +
                      end
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                  rescue Whois::ServerNotFound
         | 
| 30 | 
            +
                  rescue Whois::AttributeNotImplemented
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  {
         | 
| 34 | 
            +
                    abuse_contacts: most_relevant_record ? @extractor.abuse_contact_emails(most_relevant_record.content) : [],
         | 
| 35 | 
            +
                    creation_date: creation_date(most_relevant_record)
         | 
| 36 | 
            +
                  }
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                private
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                def creation_date(record)
         | 
| 42 | 
            +
                  return nil unless record
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                  begin
         | 
| 45 | 
            +
                    record.parser.created_on
         | 
| 46 | 
            +
                  rescue Whois::AttributeNotImplemented, Whois::AttributeNotSupported
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module PhisherPhinder
         | 
| 4 | 
            +
              class HostResponsePolicy
         | 
| 5 | 
            +
                def next_url(url, response)
         | 
| 6 | 
            +
                  location_header = response.headers['Location']
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  if [301, 302, 303, 307, 308].include?(response.status) && location_header && !location_header.empty?
         | 
| 9 | 
            +
                    if response.headers['Location'] =~ %r{\A/}
         | 
| 10 | 
            +
                      url.merge(response.headers['Location'])
         | 
| 11 | 
            +
                    else
         | 
| 12 | 
            +
                      URI.parse(response.headers['Location'])
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,40 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require 'excon'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module PhisherPhinder
         | 
| 5 | 
            +
              class LinkExplorer
         | 
| 6 | 
            +
                def initialize(host_information_finder:, host_response_policy:)
         | 
| 7 | 
            +
                  @host_information_finder = host_information_finder
         | 
| 8 | 
            +
                  @host_response_policy = host_response_policy
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def explore(hyperlink)
         | 
| 12 | 
            +
                  if hyperlink.type == :url
         | 
| 13 | 
            +
                    chain_terminated = false
         | 
| 14 | 
            +
                    url = hyperlink.href
         | 
| 15 | 
            +
                    output = []
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    until chain_terminated do
         | 
| 18 | 
            +
                      result = Excon.get(url.to_s)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                      output << LinkHost.new(
         | 
| 21 | 
            +
                        url: url,
         | 
| 22 | 
            +
                        body: result.body,
         | 
| 23 | 
            +
                        headers: result.headers,
         | 
| 24 | 
            +
                        status_code: result.status,
         | 
| 25 | 
            +
                        host_information: @host_information_finder.information_for("#{url.scheme}://#{url.host}"),
         | 
| 26 | 
            +
                      )
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                      unless url = @host_response_policy.next_url(url, result)
         | 
| 29 | 
            +
                        chain_terminated = true
         | 
| 30 | 
            +
                      end
         | 
| 31 | 
            +
                    end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    output
         | 
| 34 | 
            +
                  else
         | 
| 35 | 
            +
                    hyperlink.href =~ /mailto:(.+)/
         | 
| 36 | 
            +
                    ($1.split(';').map { |address| address.strip }).uniq
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
            end
         | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            # frozen-string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module PhisherPhinder
         | 
| 4 | 
            +
              class LinkHost
         | 
| 5 | 
            +
                attr_accessor :url, :body, :status_code, :headers, :host_information
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(url:, body:, status_code:, headers:, host_information:)
         | 
| 8 | 
            +
                  @url = url
         | 
| 9 | 
            +
                  @body = body
         | 
| 10 | 
            +
                  @status_code = status_code
         | 
| 11 | 
            +
                  @headers = headers
         | 
| 12 | 
            +
                  @host_information = host_information
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def ==(other)
         | 
| 16 | 
            +
                  url == other.url && body == other.body && status_code == other.status_code && headers == other.headers &&
         | 
| 17 | 
            +
                    host_information == other.host_information
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
            end
         | 
| @@ -13,13 +13,13 @@ module PhisherPhinder | |
| 13 13 | 
             
                  def parse(contents)
         | 
| 14 14 | 
             
                    original_headers, original_body = separate(contents)
         | 
| 15 15 | 
             
                    headers = extract_headers(original_headers)
         | 
| 16 | 
            -
                    Mail.new(
         | 
| 16 | 
            +
                    PhisherPhinder::Mail.new(
         | 
| 17 17 | 
             
                      original_email: contents,
         | 
| 18 18 | 
             
                      original_headers: original_headers,
         | 
| 19 19 | 
             
                      original_body: original_body,
         | 
| 20 20 | 
             
                      headers: headers,
         | 
| 21 21 | 
             
                      tracing_headers: generate_tracing_headers(headers),
         | 
| 22 | 
            -
                      body:  | 
| 22 | 
            +
                      body: MailParser::BodyParser.new.parse(contents),
         | 
| 23 23 | 
             
                      authentication_headers: generate_authentication_headers(headers)
         | 
| 24 24 | 
             
                    )
         | 
| 25 25 | 
             
                  end
         | 
| @@ -86,14 +86,6 @@ module PhisherPhinder | |
| 86 86 | 
             
                    values.sort { |a,b| b[:sequence] <=> a[:sequence] }
         | 
| 87 87 | 
             
                  end
         | 
| 88 88 |  | 
| 89 | 
            -
                  def parse_body(original_body, headers)
         | 
| 90 | 
            -
                    MailParser::BodyParser.new(@line_ending).parse(
         | 
| 91 | 
            -
                      body_contents: original_body,
         | 
| 92 | 
            -
                      content_type: content_type_data(headers),
         | 
| 93 | 
            -
                      content_transfer_encoding: content_transfer_encoding_data(headers),
         | 
| 94 | 
            -
                    )
         | 
| 95 | 
            -
                  end
         | 
| 96 | 
            -
             | 
| 97 89 | 
             
                  def valid_base64_decoded(text)
         | 
| 98 90 | 
             
                    if Base64.strict_encode64(Base64.decode64(text)) == text.gsub(/#{@line_ending}/, '')
         | 
| 99 91 | 
             
                      Base64.decode64(text)
         | 
| @@ -1,87 +1,36 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require 'mail'
         | 
| 2 3 |  | 
| 3 4 | 
             
            module PhisherPhinder
         | 
| 4 5 | 
             
              module MailParser
         | 
| 5 6 | 
             
                class BodyParser
         | 
| 6 | 
            -
                  def  | 
| 7 | 
            -
                     | 
| 8 | 
            -
             | 
| 9 | 
            -
             | 
| 10 | 
            -
                  def parse(body_contents:, content_type:, content_transfer_encoding:)
         | 
| 11 | 
            -
                    if multipart_alternative?(content_type)
         | 
| 12 | 
            -
                      parse_multipart_alternative(content_type, body_contents)
         | 
| 13 | 
            -
                    else
         | 
| 14 | 
            -
                      classifier = Body::BlockClassifier.new(@line_end)
         | 
| 15 | 
            -
                      parser = Body::BlockParser.new(@line_end)
         | 
| 16 | 
            -
             | 
| 17 | 
            -
                      classification = classifier.classify_headers(
         | 
| 18 | 
            -
                        content_type: content_type, content_transfer_encoding: content_transfer_encoding
         | 
| 19 | 
            -
                      ).merge(content: body_contents)
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                      contents = parser.parse(classification)
         | 
| 22 | 
            -
             | 
| 23 | 
            -
                      if classification[:content_type] == :html
         | 
| 24 | 
            -
                        {
         | 
| 25 | 
            -
                          html: contents,
         | 
| 26 | 
            -
                          text: nil
         | 
| 27 | 
            -
                        }
         | 
| 28 | 
            -
                      else
         | 
| 29 | 
            -
                        {
         | 
| 30 | 
            -
                          html: nil,
         | 
| 31 | 
            -
                          text: contents
         | 
| 32 | 
            -
                        }
         | 
| 33 | 
            -
                      end
         | 
| 34 | 
            -
                    end
         | 
| 7 | 
            +
                  def parse(contents)
         | 
| 8 | 
            +
                    mail = ::Mail.new(contents)
         | 
| 9 | 
            +
                    aggregate_body_parts(mail)
         | 
| 35 10 | 
             
                  end
         | 
| 36 11 |  | 
| 37 12 | 
             
                  private
         | 
| 38 13 |  | 
| 39 | 
            -
                  def  | 
| 40 | 
            -
                     | 
| 41 | 
            -
             | 
| 42 | 
            -
             | 
| 43 | 
            -
             | 
| 44 | 
            -
                     | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
             | 
| 48 | 
            -
             | 
| 49 | 
            -
                  def multipart_alternative?(content_type)
         | 
| 50 | 
            -
                    content_type =~ /\Amultipart\/alternative/
         | 
| 51 | 
            -
                  end
         | 
| 52 | 
            -
             | 
| 53 | 
            -
                  def parse_multipart_alternative(content_type, contents)
         | 
| 54 | 
            -
                    base_boundary = content_type.split(';').last.strip.gsub(/boundary=/, '').gsub(/"/, '')
         | 
| 55 | 
            -
                    start_boundary = '--' + base_boundary + @line_end
         | 
| 56 | 
            -
                    end_boundary = '--' + base_boundary + '--'
         | 
| 57 | 
            -
             | 
| 58 | 
            -
                    raw_blocks = contents.split(start_boundary)
         | 
| 59 | 
            -
                    trimmed_blocks = strip_epilogue(strip_prologue(raw_blocks), end_boundary)
         | 
| 60 | 
            -
             | 
| 61 | 
            -
                    categorise_blocks(trimmed_blocks).inject({html: '', text: ''}) do |memo, block|
         | 
| 62 | 
            -
                      memo.merge(block[:html] ? {html: memo[:html] + block[:contents]} : {text: memo[:text] + block[:contents]})
         | 
| 14 | 
            +
                  def aggregate_body_parts(mail)
         | 
| 15 | 
            +
                    accumulator = {text: [], html: []}
         | 
| 16 | 
            +
                    if mail.body.parts.any?
         | 
| 17 | 
            +
                      collapse_content(mail.body, accumulator)
         | 
| 18 | 
            +
                      accumulator.inject({}) { |accum, (type, parts)| accum.merge(type => parts.join) }
         | 
| 19 | 
            +
                    else
         | 
| 20 | 
            +
                      {
         | 
| 21 | 
            +
                        text: mail.body.decoded,
         | 
| 22 | 
            +
                        html: nil
         | 
| 23 | 
            +
                      }
         | 
| 63 24 | 
             
                    end
         | 
| 64 25 | 
             
                  end
         | 
| 65 26 |  | 
| 66 | 
            -
                  def  | 
| 67 | 
            -
                     | 
| 68 | 
            -
             | 
| 69 | 
            -
             | 
| 70 | 
            -
             | 
| 71 | 
            -
                     | 
| 72 | 
            -
             | 
| 73 | 
            -
             | 
| 74 | 
            -
                  def categorise_blocks(blocks)
         | 
| 75 | 
            -
                    classifier = Body::BlockClassifier.new(@line_end)
         | 
| 76 | 
            -
                    parser = Body::BlockParser.new(@line_end)
         | 
| 77 | 
            -
                    blocks.map do |block|
         | 
| 78 | 
            -
                      classification = classifier.classify_block(block)
         | 
| 79 | 
            -
                      contents = parser.parse(classification)
         | 
| 80 | 
            -
             | 
| 81 | 
            -
                      {
         | 
| 82 | 
            -
                        html: classification[:content_type] == :html,
         | 
| 83 | 
            -
                        contents: contents
         | 
| 84 | 
            -
                      }
         | 
| 27 | 
            +
                  def collapse_content(part, accumulator)
         | 
| 28 | 
            +
                    if part.parts.any?
         | 
| 29 | 
            +
                      part.parts.each { |p| collapse_content(p, accumulator) }
         | 
| 30 | 
            +
                    elsif part.content_type =~ %r{text/plain}
         | 
| 31 | 
            +
                      accumulator[:text] << part.decoded
         | 
| 32 | 
            +
                    elsif part.content_type =~ %r{text/html}
         | 
| 33 | 
            +
                      accumulator[:html] << part.decoded
         | 
| 85 34 | 
             
                    end
         | 
| 86 35 | 
             
                  end
         | 
| 87 36 | 
             
                end
         | 
| @@ -2,9 +2,10 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module PhisherPhinder
         | 
| 4 4 | 
             
              class TracingReport
         | 
| 5 | 
            -
                def initialize(mail | 
| 5 | 
            +
                def initialize(mail:, host_information_finder:, link_explorer:)
         | 
| 6 6 | 
             
                  @mail = mail
         | 
| 7 | 
            -
                  @ | 
| 7 | 
            +
                  @host_information_finder = host_information_finder
         | 
| 8 | 
            +
                  @link_explorer = link_explorer
         | 
| 8 9 | 
             
                end
         | 
| 9 10 |  | 
| 10 11 | 
             
                def report
         | 
| @@ -19,7 +20,8 @@ module PhisherPhinder | |
| 19 20 | 
             
                      }
         | 
| 20 21 | 
             
                    },
         | 
| 21 22 | 
             
                    origin: extract_origin_headers(@mail.headers),
         | 
| 22 | 
            -
                    tracing: extract_tracing_headers(@mail.tracing_headers, latest_spf_entry)
         | 
| 23 | 
            +
                    tracing: extract_tracing_headers(@mail.tracing_headers, latest_spf_entry),
         | 
| 24 | 
            +
                    content: explore_hyperlinks(@mail.hypertext_links)
         | 
| 23 25 | 
             
                  }
         | 
| 24 26 | 
             
                end
         | 
| 25 27 |  | 
| @@ -38,8 +40,16 @@ module PhisherPhinder | |
| 38 40 | 
             
                  received_headers[:received][start..-1].map do |h|
         | 
| 39 41 | 
             
                    h.merge(
         | 
| 40 42 | 
             
                      sender_contact_details: {
         | 
| 41 | 
            -
                        host: { | 
| 42 | 
            -
             | 
| 43 | 
            +
                        host: {
         | 
| 44 | 
            +
                          email: @host_information_finder.information_for(
         | 
| 45 | 
            +
                            h[:sender][:host]
         | 
| 46 | 
            +
                          )[:abuse_contacts]
         | 
| 47 | 
            +
                        },
         | 
| 48 | 
            +
                        ip: {
         | 
| 49 | 
            +
                          email: @host_information_finder.information_for(
         | 
| 50 | 
            +
                            h[:sender][:ip]
         | 
| 51 | 
            +
                          )[:abuse_contacts]
         | 
| 52 | 
            +
                        },
         | 
| 43 53 | 
             
                      }
         | 
| 44 54 | 
             
                    )
         | 
| 45 55 | 
             
                  end
         | 
| @@ -51,5 +61,15 @@ module PhisherPhinder | |
| 51 61 | 
             
                    output.merge(header_type => entries.map { |h| h[:data] })
         | 
| 52 62 | 
             
                  end
         | 
| 53 63 | 
             
                end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                def explore_hyperlinks(hyperlinks)
         | 
| 66 | 
            +
                  url_hyperlinks = (hyperlinks.select{ |link| link.type == :url }).uniq { |link| link.href }
         | 
| 67 | 
            +
                  email_hyperlinks = hyperlinks.select { |link| link.type == :email_address }
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  {
         | 
| 70 | 
            +
                    linked_urls: url_hyperlinks.map { |hyperlink| @link_explorer.explore(hyperlink) },
         | 
| 71 | 
            +
                    linked_email_addresses: (email_hyperlinks.map { |hyperlink| @link_explorer.explore(hyperlink) }).flatten.uniq
         | 
| 72 | 
            +
                  }
         | 
| 73 | 
            +
                end
         | 
| 54 74 | 
             
              end
         | 
| 55 75 | 
             
            end
         | 
    
        data/phisher_phinder.gemspec
    CHANGED
    
    | @@ -28,6 +28,8 @@ Gem::Specification.new do |spec| | |
| 28 28 | 
             
              spec.require_paths = ["lib"]
         | 
| 29 29 |  | 
| 30 30 | 
             
              spec.add_dependency "dotenv", "~> 2.7.5"
         | 
| 31 | 
            +
              spec.add_dependency "excon", "~> 0.78.1"
         | 
| 32 | 
            +
              spec.add_dependency "mail"
         | 
| 31 33 | 
             
              spec.add_dependency "maxmind-geoip2", "~> 0.4.0"
         | 
| 32 34 | 
             
              spec.add_dependency "nokogiri", "~> 1.11.0"
         | 
| 33 35 | 
             
              spec.add_dependency "sequel", "~> 5.33"
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: phisher_phinder
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 0.4.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Rory McKinley
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2021- | 
| 11 | 
            +
            date: 2021-02-13 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: dotenv
         | 
| @@ -24,6 +24,34 @@ dependencies: | |
| 24 24 | 
             
                - - "~>"
         | 
| 25 25 | 
             
                  - !ruby/object:Gem::Version
         | 
| 26 26 | 
             
                    version: 2.7.5
         | 
| 27 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 28 | 
            +
              name: excon
         | 
| 29 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 30 | 
            +
                requirements:
         | 
| 31 | 
            +
                - - "~>"
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: 0.78.1
         | 
| 34 | 
            +
              type: :runtime
         | 
| 35 | 
            +
              prerelease: false
         | 
| 36 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 37 | 
            +
                requirements:
         | 
| 38 | 
            +
                - - "~>"
         | 
| 39 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 40 | 
            +
                    version: 0.78.1
         | 
| 41 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            +
              name: mail
         | 
| 43 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            +
                requirements:
         | 
| 45 | 
            +
                - - ">="
         | 
| 46 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            +
                    version: '0'
         | 
| 48 | 
            +
              type: :runtime
         | 
| 49 | 
            +
              prerelease: false
         | 
| 50 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - ">="
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 27 55 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 28 56 | 
             
              name: maxmind-geoip2
         | 
| 29 57 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -249,12 +277,15 @@ files: | |
| 249 277 | 
             
            - lib/phisher_phinder/body_hyperlink.rb
         | 
| 250 278 | 
             
            - lib/phisher_phinder/cached_geoip_client.rb
         | 
| 251 279 | 
             
            - lib/phisher_phinder/command.rb
         | 
| 252 | 
            -
            - lib/phisher_phinder/contact_finder.rb
         | 
| 253 280 | 
             
            - lib/phisher_phinder/display.rb
         | 
| 254 281 | 
             
            - lib/phisher_phinder/expanded_data_processor.rb
         | 
| 255 282 | 
             
            - lib/phisher_phinder/extended_ip.rb
         | 
| 256 283 | 
             
            - lib/phisher_phinder/extended_ip_factory.rb
         | 
| 257 284 | 
             
            - lib/phisher_phinder/geoip_ip_data.rb
         | 
| 285 | 
            +
            - lib/phisher_phinder/host_information_finder.rb
         | 
| 286 | 
            +
            - lib/phisher_phinder/host_response_policy.rb
         | 
| 287 | 
            +
            - lib/phisher_phinder/link_explorer.rb
         | 
| 288 | 
            +
            - lib/phisher_phinder/link_host.rb
         | 
| 258 289 | 
             
            - lib/phisher_phinder/mail.rb
         | 
| 259 290 | 
             
            - lib/phisher_phinder/mail_parser.rb
         | 
| 260 291 | 
             
            - lib/phisher_phinder/mail_parser/authentication_headers/auth_results_parser.rb
         | 
| @@ -1,37 +0,0 @@ | |
| 1 | 
            -
            # frozen_string_literal: true
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            module PhisherPhinder
         | 
| 4 | 
            -
              class ContactFinder
         | 
| 5 | 
            -
                def initialize(whois_client:, extractor:)
         | 
| 6 | 
            -
                  @whois_client = whois_client
         | 
| 7 | 
            -
                  @extractor = extractor
         | 
| 8 | 
            -
                end
         | 
| 9 | 
            -
             | 
| 10 | 
            -
                def contacts_for(address)
         | 
| 11 | 
            -
                  whois_content = nil
         | 
| 12 | 
            -
                  begin
         | 
| 13 | 
            -
                    whois_content = case address
         | 
| 14 | 
            -
                                    when ExtendedIp
         | 
| 15 | 
            -
                                      @whois_client.lookup(address.ip_address.to_s).content
         | 
| 16 | 
            -
                                    when String
         | 
| 17 | 
            -
                                      hostname_parts = address.split('.')
         | 
| 18 | 
            -
                                      whois_content = nil
         | 
| 19 | 
            -
                                      until whois_content do
         | 
| 20 | 
            -
                                        address = hostname_parts.join('.')
         | 
| 21 | 
            -
                                        whois_record = @whois_client.lookup(address)
         | 
| 22 | 
            -
                                        if whois_record.parser.available?
         | 
| 23 | 
            -
                                          hostname_parts = hostname_parts[1..-1]
         | 
| 24 | 
            -
                                        else
         | 
| 25 | 
            -
                                          whois_content = whois_record.content
         | 
| 26 | 
            -
                                        end
         | 
| 27 | 
            -
                                      end
         | 
| 28 | 
            -
                                      whois_content
         | 
| 29 | 
            -
                                    end
         | 
| 30 | 
            -
                  rescue Whois::ServerNotFound
         | 
| 31 | 
            -
                  rescue Whois::AttributeNotImplemented
         | 
| 32 | 
            -
                  end
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                  whois_content ? @extractor.abuse_contact_emails(whois_content) : []
         | 
| 35 | 
            -
                end
         | 
| 36 | 
            -
              end
         | 
| 37 | 
            -
            end
         |