phisher_phinder 0.1.0 → 0.2.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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.env.example +2 -1
  3. data/.gitignore +3 -0
  4. data/Gemfile +0 -11
  5. data/Gemfile.lock +45 -13
  6. data/README.md +108 -2
  7. data/exe/phisher_phinder +61 -0
  8. data/lib/phisher_phinder.rb +11 -2
  9. data/lib/phisher_phinder/command.rb +20 -0
  10. data/lib/phisher_phinder/display.rb +64 -0
  11. data/lib/phisher_phinder/extended_ip.rb +4 -0
  12. data/lib/phisher_phinder/extended_ip_factory.rb +4 -2
  13. data/lib/phisher_phinder/geoip_ip_data.rb +9 -2
  14. data/lib/phisher_phinder/mail.rb +10 -3
  15. data/lib/phisher_phinder/mail_parser.rb +43 -30
  16. data/lib/phisher_phinder/mail_parser/authentication_headers/auth_results_parser.rb +150 -0
  17. data/lib/phisher_phinder/mail_parser/authentication_headers/parser.rb +25 -0
  18. data/lib/phisher_phinder/mail_parser/authentication_headers/received_spf_parser.rb +222 -0
  19. data/lib/phisher_phinder/mail_parser/body/block_classifier.rb +106 -0
  20. data/lib/phisher_phinder/mail_parser/body/block_parser.rb +37 -0
  21. data/lib/phisher_phinder/mail_parser/body_parser.rb +26 -31
  22. data/lib/phisher_phinder/mail_parser/header_value_parser.rb +25 -10
  23. data/lib/phisher_phinder/mail_parser/received_headers/by_parser.rb +35 -5
  24. data/lib/phisher_phinder/mail_parser/received_headers/for_parser.rb +25 -5
  25. data/lib/phisher_phinder/mail_parser/received_headers/from_parser.rb +50 -6
  26. data/lib/phisher_phinder/mail_parser/received_headers/parser.rb +50 -29
  27. data/lib/phisher_phinder/mail_parser/received_headers/starttls_parser.rb +8 -1
  28. data/lib/phisher_phinder/null_lookup_client.rb +9 -0
  29. data/lib/phisher_phinder/null_response.rb +12 -0
  30. data/lib/phisher_phinder/sender_extractor.rb +74 -0
  31. data/lib/phisher_phinder/simple_ip.rb +4 -0
  32. data/lib/phisher_phinder/tracing_report.rb +47 -0
  33. data/lib/phisher_phinder/version.rb +1 -1
  34. data/phisher_phinder.gemspec +15 -1
  35. metadata +208 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee817dd4cbc016b47f7710714c4be4c5b07a81baa89b47d46c00ae9112fc04e0
4
- data.tar.gz: 7fb874d90fbb94f310ccec195ee6901f77c193f92615cdb5d9c8f5764dfaac94
3
+ metadata.gz: c8063750bef71fc06792e7c10440f99e979891612540b06abf4c66f2f368022d
4
+ data.tar.gz: a94cd0e2438359bc626609216a91633deb589121ac92a2567c2a90625c0e30ae
5
5
  SHA512:
6
- metadata.gz: c4d8660b672488b32ce4cb4ba1e3b7652e414b54afff9d2817a8461d570f7db5aac391424287953a4653ef3b00513dfcffa6666dc740aea823231bc4d6b5f685
7
- data.tar.gz: 9e35efd983eb5961fac93c05036bf09e59919ecd1ce6ecd1c338d026e5c3489276f69edd135d2d81e47870daf3422df5ef07f8de570c3d5dfa0f14111c6cdce6
6
+ metadata.gz: 8cfc5618456ab2db9304232da36e6a5ae3e989edd1ba0993f5bd1f07feb27cfb4d1921052176630e002aa7847f0faee646f5f6690cdd650c34fd64ed843d0188
7
+ data.tar.gz: bb5ef0c6f6f601a8c7d42ec2a7a8c8de976ccc9a86e438c56cedf0e4853908edb9efb3dfecfae515d300e0f2671cf321c3eaebe0105dad874a52e9be2fff7050
@@ -1,3 +1,4 @@
1
- DATABASE_URL=sqlite3://example.sqlite3
1
+ DATABASE_URL=sqlite://example.sqlite3
2
2
  MAXMIND_USER_ID=12345
3
3
  MAXMIND_LICENCE_KEY=abcdef
4
+ LINE_ENDING_TYPE=dos # or unix
data/.gitignore CHANGED
@@ -10,9 +10,12 @@
10
10
  # rspec failure tracking
11
11
  .rspec_status
12
12
 
13
+ *.gem
14
+
13
15
  *.sqlite3
14
16
 
15
17
  *.env
16
18
  *.env.test
17
19
 
18
20
  test_files
21
+ icebox
data/Gemfile CHANGED
@@ -1,14 +1,3 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
-
5
- gem 'dotenv', '~> 2.7.5'
6
- gem 'maxmind-geoip2', '~> 0.4.0'
7
- gem 'nokogiri', '~> 1.10.10'
8
- gem 'sequel', '~> 5.33'
9
- gem 'sqlite3', '~> 1.4.2'
10
-
11
- gem "rake", "~> 12.0"
12
- gem "rspec", "~> 3.0"
13
- gem 'database_cleaner-sequel', '1.8.0'
14
- gem 'webmock', '~> 3.8.3'
@@ -1,14 +1,32 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- phisher_phinder (0.1.0)
4
+ phisher_phinder (0.2.0)
5
+ dotenv (~> 2.7.5)
6
+ maxmind-geoip2 (~> 0.4.0)
7
+ nokogiri (~> 1.11.0)
8
+ sequel (~> 5.33)
9
+ sqlite3 (~> 1.4.2)
10
+ terminal-table (~> 2.0.0)
11
+ whois (~> 5.0.1)
12
+ whois-parser (~> 1.2.0)
5
13
 
6
14
  GEM
7
15
  remote: https://rubygems.org/
8
16
  specs:
17
+ activesupport (6.1.0)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (>= 1.6, < 2)
20
+ minitest (>= 5.1)
21
+ tzinfo (~> 2.0)
22
+ zeitwerk (~> 2.3)
9
23
  addressable (2.7.0)
10
24
  public_suffix (>= 2.0.2, < 5.0)
25
+ bundler-audit (0.7.0.1)
26
+ bundler (>= 1.2.0, < 3)
27
+ thor (>= 0.18, < 2)
11
28
  coderay (1.1.2)
29
+ concurrent-ruby (1.1.7)
12
30
  connection_pool (2.2.3)
13
31
  crack (0.4.3)
14
32
  safe_yaml (~> 1.0.0)
@@ -19,8 +37,8 @@ GEM
19
37
  diff-lcs (1.3)
20
38
  domain_name (0.5.20190701)
21
39
  unf (>= 0.0.5, < 1.0.0)
22
- dotenv (2.7.5)
23
- ffi (1.13.1)
40
+ dotenv (2.7.6)
41
+ ffi (1.14.2)
24
42
  ffi-compiler (1.0.1)
25
43
  ffi (>= 1.0.0)
26
44
  rake
@@ -33,21 +51,26 @@ GEM
33
51
  http-cookie (1.0.3)
34
52
  domain_name (~> 0.5)
35
53
  http-form_data (2.3.0)
36
- http-parser (1.2.1)
37
- ffi-compiler (>= 1.0, < 2.0)
54
+ http-parser (1.2.2)
55
+ ffi-compiler
56
+ i18n (1.8.7)
57
+ concurrent-ruby (~> 1.0)
38
58
  maxmind-db (1.1.1)
39
59
  maxmind-geoip2 (0.4.0)
40
60
  connection_pool (~> 2.2)
41
61
  http (~> 4.3)
42
62
  maxmind-db (~> 1.1)
43
63
  method_source (1.0.0)
44
- mini_portile2 (2.4.0)
45
- nokogiri (1.10.10)
46
- mini_portile2 (~> 2.4.0)
64
+ mini_portile2 (2.5.0)
65
+ minitest (5.14.2)
66
+ nokogiri (1.11.0)
67
+ mini_portile2 (~> 2.5.0)
68
+ racc (~> 1.4)
47
69
  pry (0.13.1)
48
70
  coderay (~> 1.1)
49
71
  method_source (~> 1.0)
50
72
  public_suffix (4.0.5)
73
+ racc (1.5.2)
51
74
  rake (12.3.3)
52
75
  rspec (3.9.0)
53
76
  rspec-core (~> 3.9.0)
@@ -65,28 +88,37 @@ GEM
65
88
  safe_yaml (1.0.5)
66
89
  sequel (5.33.0)
67
90
  sqlite3 (1.4.2)
91
+ terminal-table (2.0.0)
92
+ unicode-display_width (~> 1.1, >= 1.1.1)
93
+ thor (1.0.1)
94
+ timecop (0.9.2)
95
+ tzinfo (2.0.4)
96
+ concurrent-ruby (~> 1.0)
68
97
  unf (0.1.4)
69
98
  unf_ext
70
99
  unf_ext (0.0.7.7)
100
+ unicode-display_width (1.7.0)
71
101
  webmock (3.8.3)
72
102
  addressable (>= 2.3.6)
73
103
  crack (>= 0.3.2)
74
104
  hashdiff (>= 0.4.0, < 2.0.0)
105
+ whois (5.0.1)
106
+ whois-parser (1.2.0)
107
+ activesupport (>= 4)
108
+ whois (>= 4.0.7)
109
+ zeitwerk (2.4.2)
75
110
 
76
111
  PLATFORMS
77
112
  ruby
78
113
 
79
114
  DEPENDENCIES
115
+ bundler-audit (~> 0.7.0.1)
80
116
  database_cleaner-sequel (= 1.8.0)
81
- dotenv (~> 2.7.5)
82
- maxmind-geoip2 (~> 0.4.0)
83
- nokogiri (~> 1.10.10)
84
117
  phisher_phinder!
85
118
  pry
86
119
  rake (~> 12.0)
87
120
  rspec (~> 3.0)
88
- sequel (~> 5.33)
89
- sqlite3 (~> 1.4.2)
121
+ timecop (~> 0.9.2)
90
122
  webmock (~> 3.8.3)
91
123
 
92
124
  BUNDLED WITH
data/README.md CHANGED
@@ -1,9 +1,22 @@
1
1
  # PhisherPhinder
2
2
 
3
- A simple gem to explore phishing emails
3
+ PhisherPhinder is a small utility that extracts data from an email that can be useful when trying to determine the
4
+ source of a phishing email.
5
+
6
+ As of version 0.2.0 this functionality is very limited. There is a good chance that you will encounter sharp edges - if
7
+ you do, feel free to file a GH issue and I will try to fix this.
8
+
9
+ ## Caution
10
+
11
+ **When downloading an email for parsing, DO NOT CLICK on any links contained within the email. Rather download the
12
+ message source as text. As an example, here are the instructions for doing it using
13
+ [GMail](https://www.lifewire.com/how-to-view-the-source-of-a-message-in-gmail-1172105 'Gmail Download Instructions')**
4
14
 
5
15
  ## Installation
6
16
 
17
+ Note: Currently, Windows support is not guaranteed. If you would like to test it on Windows and tell me what does not
18
+ work, I will try my best to address these issues.
19
+
7
20
  Add this line to your application's Gemfile:
8
21
 
9
22
  ```ruby
@@ -20,9 +33,102 @@ Or install it yourself as:
20
33
 
21
34
  ## Usage
22
35
 
23
- TODO: Write usage instructions here
36
+ At it's most simple - download the email as text to a location on th emahcine running PhihserPhinder
37
+ (`/path/to/content.eml`), then:
38
+
39
+ ```ruby
40
+ phisher_phinder /path/to/content.eml
41
+ ```
42
+
43
+ By default, PhisherPhinder will assume that the file uses the dos-style line endings (`\r\n`), but you can specify the
44
+ line ending type:
45
+
46
+ ```bash
47
+ phisher_phinder -l unix /path/to/content.eml # unix line endings \n
48
+ phisher_phinder -l dos /path/to/content.eml # windows line endings \r\n
49
+ ```
50
+
51
+ PhisherPhinder can lookup IPV4 address data from the MaxMind GeoIP service which requires a Maxmind GeoIP account.
52
+ Lookup results are cached locally in a database (SQLITE will suffice). The URL for the database needs to be provided as
53
+ an environment variable:
54
+
55
+ ```bash
56
+ DATABASE_URL=sqlite://development.sqlite3 phisher_phinder -a foo -k bar -g /path/to/content.eml
57
+ ```
58
+
59
+ Note: As of version 0.2.0, the GeoIP data is not used for much and the functionality may be removed in future releases.
60
+
61
+ To see help instructions for the CLI usage:
62
+
63
+ ```bash
64
+ phisher_phinder -h
65
+ ```
66
+
67
+ ```
68
+ Usage: phisher_phinder [options] /path/to/email/contents
69
+ -a, --account_id ACCOUNT_ID GeoIP account id
70
+ -k, --license_key LICENSE_KEY GeoIP license key
71
+ -g, --geoip Enable lookup of GeoIP data for IP addresses (requires `DATABASE_URL` env variable to be defined)
72
+ -l, --line-ending TYPE Select line ending type for file
73
+ -h, --help Prints help text
74
+
75
+ ```
76
+
77
+ ## Output
78
+
79
+ The output of PhisherPhinder will produce something similar to the below:
80
+
81
+ ```
82
+ +-------------+---------------------------------------------+
83
+ | Origin |
84
+ +-------------+---------------------------------------------+
85
+ | From | "Email Security Gateway" <test@test.zzz> |
86
+ | Message ID | <ed1770$bbsq7@test.zzz> |
87
+ | Return Path | <test@test.zzz> |
88
+ +-------------+---------------------------------------------+
89
+
90
+
91
+ +-----------+---------------+------------------+
92
+ | SPF |
93
+ +-----------+---------------+------------------+
94
+ | SPF Pass? | Sender Host | From Address |
95
+ +-----------+---------------+------------------+
96
+ | Yes | 10.0.0.1 | test@test.zzz |
97
+ +-----------+---------------+------------------+
98
+
99
+
100
+ +---------------+------------------------------+------------------------------+--------------------------+
101
+ | Trace |
102
+ +---------------+------------------------------+------------------------------+--------------------------+
103
+ | Sender IP | Sender Host | Advertised Sender | Recipient |
104
+ +---------------+------------------------------+------------------------------+--------------------------+
105
+ | 10.0.0.1 | host1.test.zzz | dodgyname.test.zzz | mx.google.com |
106
+ | 10.0.0.2 | | othersdodgyname.text.zzz | host1.test.zzz |
107
+ +---------------+------------------------------+------------------------------+--------------------------+
108
+
109
+ ```
110
+
111
+ The `Origin` section contains data relating to the email origin.
112
+
113
+ With regard to the `SPF` and `Trace` sections, they are based on the assumption that the most recent SPF details
114
+ provided in the headers can be trusted as they have been provided by the host of the recipient email and can, hopefully,
115
+ be trusted.
116
+
117
+ The `Trace` secion shows a subset of the `Received` headers from the original (advertised, but not necessarily actual)
118
+ origin (the last entry in the table) to the last external server to process the email before the recipient's mail host
119
+ received the email.
120
+
121
+ ## Dependencies
122
+ 1. [Maxmind GeoIP2 User Account](https://dev.maxmind.com/geoip/geoip2/web-services/) - pay as you go
123
+ 2. [Database Cleaner](https://github.com/DatabaseCleaner/database_cleaner#safeguards) - whitelist for database urls
24
124
 
25
125
  ## Development
126
+ .env.example contains dependent environment variables for the .env.test file. You will need to change: DATABASE_URL=sqlite://test.sqlite3.
127
+ Prior to running the specs, run the 0001 migration to create the geo_ip_cache table.
128
+
129
+ ```
130
+ # bundle exec rake db:migrate
131
+ ```
26
132
 
27
133
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
28
134
 
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'optparse'
4
+ require 'phisher_phinder'
5
+
6
+ options = {
7
+ line_ending: "\r\n",
8
+ geoip_lookup: false,
9
+ geoip_settings: {
10
+ account_id: nil,
11
+ license_key: nil
12
+ }
13
+ }
14
+
15
+ OptionParser.new do |opts|
16
+ opts.banner = 'Usage: phisher_phinder [options] /path/to/email/contents'
17
+ line_endings = {
18
+ 'windows' => "\r\n",
19
+ 'dos' => "\r\n",
20
+ 'unix' => "\n",
21
+ }
22
+
23
+ def geoip_credentials?(opts)
24
+ opts[:geoip_settings][:account_id] && opts[:geoip_settings][:license_key]
25
+ end
26
+
27
+ opts.on('-a ACCOUNT_ID', '--account_id ACCOUNT_ID', 'GeoIP account id') do |account_id|
28
+ options[:geoip_settings][:account_id] = account_id
29
+ end
30
+
31
+ opts.on('-k LICENSE_KEY', '--license_key LICENSE_KEY', 'GeoIP license key') do |license_key|
32
+ options[:geoip_settings][:license_key] = license_key
33
+ end
34
+
35
+ opts.on(
36
+ '-g',
37
+ '--geoip',
38
+ 'Enable lookup of GeoIP data for IP addresses (requires `DATABASE_URL` env variable to be defined)'
39
+ ) do |geoip|
40
+ raise 'Please provide the GeoIP account id and license key' unless geoip_credentials?(options)
41
+ raise 'Please set the DATABASE_URL ENV variable' unless ENV['DATABASE_URL']
42
+ options[:geoip_lookup] = geoip
43
+ end
44
+
45
+ opts.on('-l TYPE', '--line-ending TYPE', line_endings, 'Select line ending type for file') do |ending|
46
+ options[:line_ending] = ending
47
+ end
48
+
49
+ opts.on('-h', '--help', 'Prints help text') do
50
+ puts opts
51
+ exit
52
+ end
53
+ end.parse!
54
+
55
+ file_contents = IO.read(ARGV.last)
56
+
57
+ command = PhisherPhinder::Command.new
58
+
59
+ PhisherPhinder::Display.new.display_report(
60
+ command.report(file_contents, **options)
61
+ )
@@ -2,11 +2,13 @@ require "phisher_phinder/version"
2
2
 
3
3
  require 'maxmind/geoip2'
4
4
 
5
- require 'sequel'
6
- Sequel::Model.plugin :timestamps, update_on_create: true
5
+ require_relative './phisher_phinder/command'
6
+ require_relative './phisher_phinder/display'
7
7
 
8
8
  require_relative './phisher_phinder/body_hyperlink'
9
9
  require_relative './phisher_phinder/cached_geoip_client'
10
+ require_relative './phisher_phinder/null_lookup_client'
11
+ require_relative './phisher_phinder/null_response'
10
12
  require_relative './phisher_phinder/geoip_ip_data'
11
13
  require_relative './phisher_phinder/expanded_data_processor'
12
14
  require_relative './phisher_phinder/extended_ip'
@@ -14,6 +16,11 @@ require_relative './phisher_phinder/extended_ip_factory'
14
16
  require_relative './phisher_phinder/mail_parser'
15
17
  require_relative './phisher_phinder/mail'
16
18
  require_relative './phisher_phinder/simple_ip'
19
+ require_relative './phisher_phinder/mail_parser/authentication_headers/parser'
20
+ require_relative './phisher_phinder/mail_parser/authentication_headers/auth_results_parser'
21
+ require_relative './phisher_phinder/mail_parser/authentication_headers/received_spf_parser'
22
+ require_relative './phisher_phinder/mail_parser/body/block_classifier'
23
+ require_relative './phisher_phinder/mail_parser/body/block_parser'
17
24
  require_relative './phisher_phinder/mail_parser/received_headers/parser'
18
25
  require_relative './phisher_phinder/mail_parser/received_headers/by_parser'
19
26
  require_relative './phisher_phinder/mail_parser/received_headers/classifier'
@@ -21,6 +28,8 @@ require_relative './phisher_phinder/mail_parser/received_headers/for_parser'
21
28
  require_relative './phisher_phinder/mail_parser/received_headers/from_parser'
22
29
  require_relative './phisher_phinder/mail_parser/received_headers/starttls_parser'
23
30
  require_relative './phisher_phinder/mail_parser/received_headers/timestamp_parser'
31
+ require_relative './phisher_phinder/sender_extractor'
32
+ require_relative './phisher_phinder/tracing_report'
24
33
 
25
34
  module PhisherPhinder
26
35
  class Error < StandardError; end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ class Command
5
+ def report(contents, line_ending:, geoip_lookup:, geoip_settings: {})
6
+ lookup_client = if geoip_lookup
7
+ PhisherPhinder::CachedGeoipClient.new(
8
+ MaxMind::GeoIP2::Client.new(**geoip_settings),
9
+ Time.now - 86400
10
+ )
11
+ else
12
+ PhisherPhinder::NullLookupClient.new
13
+ end
14
+ ip_factory = PhisherPhinder::ExtendedIpFactory.new(geoip_client: lookup_client)
15
+ mail_parser = PhisherPhinder::MailParser::Parser.new(ip_factory, line_ending)
16
+ tracing_report = PhisherPhinder::TracingReport.new(mail_parser.parse(contents))
17
+ tracing_report.report
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ require 'terminal-table'
3
+
4
+ module PhisherPhinder
5
+ class Display
6
+ def display_report(input_data)
7
+ origin_table = Terminal::Table.new(
8
+ title: 'Origin',
9
+ rows: format_origin_data(input_data)
10
+ )
11
+
12
+ puts origin_table
13
+
14
+ puts "\n\n"
15
+
16
+ spf_table = Terminal::Table.new(
17
+ headings: ['SPF Pass?', 'Sender Host', 'From Address'],
18
+ title: 'SPF',
19
+ rows: [
20
+ [
21
+ input_data[:authentication][:spf][:success] ? 'Yes' : 'No',
22
+ input_data[:authentication][:spf][:ip],
23
+ input_data[:authentication][:spf][:from_address]
24
+ ]
25
+ ]
26
+ )
27
+
28
+ puts spf_table
29
+
30
+ puts "\n\n"
31
+
32
+ data = input_data[:tracing].map do |entry|
33
+ [
34
+ entry[:sender][:ip],
35
+ entry[:sender][:host],
36
+ entry[:advertised_sender] || entry[:helo],
37
+ entry[:recipient]
38
+ ]
39
+ end
40
+
41
+ trace_table = Terminal::Table.new(
42
+ headings: ['Sender IP', 'Sender Host', 'Advertised Sender', 'Recipient'],
43
+ title: 'Trace',
44
+ rows: data
45
+ )
46
+
47
+ puts trace_table
48
+ end
49
+
50
+ private
51
+
52
+ def format_origin_data(input_data)
53
+ types = [
54
+ ['From', :from],
55
+ ['Message ID', :message_id],
56
+ ['Return Path', :return_path],
57
+ ]
58
+
59
+ types.inject([]) do |output, (description, type)|
60
+ output << [description, input_data[:origin][type].join(', ')]
61
+ end
62
+ end
63
+ end
64
+ end