phisher_phinder 0.1.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 (39) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +3 -0
  3. data/.gitignore +18 -0
  4. data/.rspec +3 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +6 -0
  8. data/CHANGELOG.md +1 -0
  9. data/Gemfile +14 -0
  10. data/Gemfile.lock +93 -0
  11. data/LICENSE +21 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +38 -0
  14. data/Rakefile +38 -0
  15. data/bin/console +20 -0
  16. data/bin/setup +8 -0
  17. data/db/migrations/0001_create_geo_ip_cache.rb +35 -0
  18. data/lib/phisher_phinder.rb +28 -0
  19. data/lib/phisher_phinder/body_hyperlink.rb +47 -0
  20. data/lib/phisher_phinder/cached_geoip_client.rb +95 -0
  21. data/lib/phisher_phinder/expanded_data_processor.rb +61 -0
  22. data/lib/phisher_phinder/extended_ip.rb +16 -0
  23. data/lib/phisher_phinder/extended_ip_factory.rb +51 -0
  24. data/lib/phisher_phinder/geoip_ip_data.rb +6 -0
  25. data/lib/phisher_phinder/mail.rb +50 -0
  26. data/lib/phisher_phinder/mail_parser.rb +111 -0
  27. data/lib/phisher_phinder/mail_parser/body_parser.rb +94 -0
  28. data/lib/phisher_phinder/mail_parser/header_value_parser.rb +24 -0
  29. data/lib/phisher_phinder/mail_parser/received_headers/by_parser.rb +45 -0
  30. data/lib/phisher_phinder/mail_parser/received_headers/classifier.rb +27 -0
  31. data/lib/phisher_phinder/mail_parser/received_headers/for_parser.rb +23 -0
  32. data/lib/phisher_phinder/mail_parser/received_headers/from_parser.rb +40 -0
  33. data/lib/phisher_phinder/mail_parser/received_headers/parser.rb +74 -0
  34. data/lib/phisher_phinder/mail_parser/received_headers/starttls_parser.rb +24 -0
  35. data/lib/phisher_phinder/mail_parser/received_headers/timestamp_parser.rb +32 -0
  36. data/lib/phisher_phinder/simple_ip.rb +15 -0
  37. data/lib/phisher_phinder/version.rb +3 -0
  38. data/phisher_phinder.gemspec +32 -0
  39. metadata +112 -0
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ module MailParser
5
+ module ReceivedHeaders
6
+ class Classifier
7
+ def classify(header_data)
8
+ {partial: !complete?(header_data)}
9
+ end
10
+
11
+ private
12
+
13
+ def complete?(header_data)
14
+ (
15
+ header_data[:advertised_sender] &&
16
+ header_data[:recipient] &&
17
+ header_data[:recipient_mailbox] &&
18
+ (
19
+ (header_data[:protocol] == 'ESMTPS' && header_data[:starttls]) ||
20
+ (header_data[:protocol] != 'ESMTPS' && !header_data[:starttls])
21
+ )
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ module MailParser
5
+ module ReceivedHeaders
6
+ class ForParser
7
+ def parse(component)
8
+ component =~ /\Afor\s(\S+)\z/
9
+
10
+ {
11
+ recipient_mailbox: strip_angle_brackets($1)
12
+ }
13
+ end
14
+
15
+ private
16
+
17
+ def strip_angle_brackets(email_address_string)
18
+ email_address_string =~ /\<([^>]+)\>/ ? $1 : email_address_string
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ module MailParser
5
+ module ReceivedHeaders
6
+ class FromParser
7
+ def initialize(extended_ip_factory)
8
+ @extended_ip_factory = extended_ip_factory
9
+ end
10
+
11
+ def parse(component)
12
+ return {advertised_sender: nil, helo: nil, sender: nil} unless component
13
+
14
+ patterns = [
15
+ /from\s(?<advertised_sender>[\S]+)\s\(HELO\s(?<helo>[^)]+)\)\s\(\[(?<sender_ip>[^\]]+)\]\)/,
16
+ /from\s(?<advertised_sender>[\S]+)\s\((?<sender_host>\S+?)\.?\s\[(?<sender_ip>[^\]]+)\]\)/,
17
+ /from\s(?<advertised_sender>\S+)\s\((?<sender_host>\S+?)\.?\s(?<sender_ip>\S+?)\)/,
18
+ /from\s(?<advertised_sender>\S+)\s\(\[(?<sender_ip>[^\]]+)\]\)/,
19
+ /from\s(?<advertised_sender>\S+)\s\((?<sender_ip>[^)]+)\)/,
20
+ /\(from\s(?<advertised_sender>[^)]+)\)/,
21
+ /from\s(?<advertised_sender>\S+)/,
22
+ ]
23
+
24
+ matches = patterns.inject(nil) do |memo, pattern|
25
+ memo || component.match(pattern)
26
+ end
27
+
28
+ {
29
+ advertised_sender: matches[:advertised_sender],
30
+ helo: matches.names.include?('helo') ? matches[:helo] : nil,
31
+ sender: {
32
+ host: matches.names.include?('sender_host') ? matches[:sender_host] : nil,
33
+ ip: matches.names.include?('sender_ip') ? @extended_ip_factory.build(matches[:sender_ip]) : nil
34
+ }
35
+ }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ module MailParser
5
+ module ReceivedHeaders
6
+ class Parser
7
+ def initialize(by_parser:, for_parser:, from_parser:, starttls_parser:, timestamp_parser:, classifier:)
8
+ @parsers = {
9
+ by: by_parser,
10
+ for: for_parser,
11
+ from: from_parser,
12
+ starttls: starttls_parser,
13
+ time: timestamp_parser,
14
+ }
15
+ @classifier = classifier
16
+ end
17
+
18
+ def parse(header)
19
+ require 'strscan'
20
+
21
+ header_value, timestamp = header.split(';')
22
+
23
+ parsers = [@by_parser, @for_parser, @from_parser, @starttls_parser, @timestamp_parser]
24
+ components = extract_value_components(header_value).merge(time: timestamp)
25
+
26
+ output = @parsers.inject({}) do |memo, (component_name, parser)|
27
+ memo.merge(parser.parse(components[component_name]))
28
+ end
29
+
30
+ output.merge(@classifier.classify(output))
31
+ end
32
+
33
+ private
34
+
35
+ def extract_value_components(header_value)
36
+ require 'strscan'
37
+
38
+ scanner = StringScanner.new(header_value)
39
+
40
+ output = {}
41
+
42
+ if scanner.check(/\(from\s+[^)]+\)/)
43
+ from_part = scanner.scan(/\(from\s+[^)]+\)/)
44
+ elsif scanner.check(/from\s.+?\(HELO\s[^)]+\)\s\(\[[^\]]+\]\)/)
45
+ from_part = scanner.scan(/from.+?(?=by)/)
46
+ elsif scanner.check(/from\s[^)(]+\sby/)
47
+ from_part = scanner.scan(/from.+?(?=by)/)
48
+ else
49
+ from_part = scanner.scan(/from\s.+?\([^)]+?\)/)
50
+ end
51
+
52
+ if scanner.check(/.+\S+\s+by/)
53
+ starttls_part = scanner.scan(/.+\s(?=by)/)
54
+ by_part = scanner.scan(/\s?by.+?\sid\s[\S]+\s?/)
55
+ for_part = scanner.scan(/for\s+\S+/)
56
+ elsif scanner.check(/\s?by.+?\s(id|ID)\s[\S]+\s?/)
57
+ by_part = scanner.scan(/\s?by.+?\s(id|ID)\s[\S]+\s?/) unless scanner.eos?
58
+ for_part = scanner.scan(/for\s+\S+/) unless scanner.eos?
59
+ starttls_part = scanner.rest unless scanner.eos?
60
+ elsif scanner.check(/by.+(?!\sid)/)
61
+ by_part = scanner.scan(/.+/)
62
+ end
63
+
64
+ {
65
+ by: by_part,
66
+ for: for_part,
67
+ from: from_part,
68
+ starttls: starttls_part
69
+ }
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ module MailParser
5
+ module ReceivedHeaders
6
+ class StarttlsParser
7
+ def parse(component)
8
+ return {starttls: nil} unless component
9
+
10
+ patterns = [
11
+ /\(version=(?<version>\S+)\scipher=(?<cipher>\S+)\sbits=(?<bits>\S+)\)/,
12
+ /using\s(?<version>\S+)\swith cipher\s(?<cipher>\S+)\s\((?<bits>.+?) bits\)/
13
+ ]
14
+
15
+ matches = patterns.inject(nil) do |memo, pattern|
16
+ memo || component.match(pattern)
17
+ end
18
+
19
+ {starttls: {version: matches[:version], cipher: matches[:cipher], bits: matches[:bits]}}
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PhisherPhinder
4
+ module MailParser
5
+ module ReceivedHeaders
6
+ class TimestampParser
7
+ def parse(timestamp)
8
+ return {time: nil} unless timestamp
9
+
10
+ require 'time'
11
+
12
+ date_formats = [
13
+ "%a, %d %b %Y %H:%M:%S %z (%Z)",
14
+ "%a, %d %b %Y %H:%M:%S %z",
15
+ "%d %b %Y %H:%M:%S %z"
16
+ ]
17
+
18
+ parsed_time = date_formats.inject(nil) do |parsed_time, pattern|
19
+ begin
20
+ parsed_time || Time.strptime(timestamp.strip, pattern)
21
+ rescue ArgumentError
22
+ end
23
+ end
24
+
25
+ raise "Could not match `#{timestamp}` with the available patterns" unless parsed_time
26
+
27
+ {time: parsed_time}
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal
2
+
3
+ module PhisherPhinder
4
+ class SimpleIp
5
+ attr_reader :ip_address
6
+
7
+ def initialize(ip_address:)
8
+ @ip_address = ip_address
9
+ end
10
+
11
+ def ==(other)
12
+ other.is_a?(SimpleIp) && ip_address == other.ip_address
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ module PhisherPhinder
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,32 @@
1
+ require_relative 'lib/phisher_phinder/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "phisher_phinder"
5
+ spec.version = PhisherPhinder::VERSION
6
+ spec.authors = ["Rory McKinley"]
7
+ spec.email = ["rorymckinley@gmail.com"]
8
+
9
+ spec.summary = %q{A gem for dissecting phishing emails}
10
+ spec.description = %q{A collection of tools to dissect and report on phishing emails}
11
+ spec.homepage = "https://capefox.co"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/rorymckinley/phisher_phinder"
19
+ spec.metadata["changelog_uri"] = "https://github.com/rorymckinley/phisher_phinder/blob/master/CHANGELOG.md"
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "pry"
31
+ spec.add_development_dependency "rspec", "3.9.0"
32
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: phisher_phinder
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rory McKinley
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-09-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 3.9.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 3.9.0
41
+ description: A collection of tools to dissect and report on phishing emails
42
+ email:
43
+ - rorymckinley@gmail.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - ".env.example"
49
+ - ".gitignore"
50
+ - ".rspec"
51
+ - ".ruby-gemset"
52
+ - ".ruby-version"
53
+ - ".travis.yml"
54
+ - CHANGELOG.md
55
+ - Gemfile
56
+ - Gemfile.lock
57
+ - LICENSE
58
+ - LICENSE.txt
59
+ - README.md
60
+ - Rakefile
61
+ - bin/console
62
+ - bin/setup
63
+ - db/migrations/0001_create_geo_ip_cache.rb
64
+ - lib/phisher_phinder.rb
65
+ - lib/phisher_phinder/body_hyperlink.rb
66
+ - lib/phisher_phinder/cached_geoip_client.rb
67
+ - lib/phisher_phinder/expanded_data_processor.rb
68
+ - lib/phisher_phinder/extended_ip.rb
69
+ - lib/phisher_phinder/extended_ip_factory.rb
70
+ - lib/phisher_phinder/geoip_ip_data.rb
71
+ - lib/phisher_phinder/mail.rb
72
+ - lib/phisher_phinder/mail_parser.rb
73
+ - lib/phisher_phinder/mail_parser/body_parser.rb
74
+ - lib/phisher_phinder/mail_parser/header_value_parser.rb
75
+ - lib/phisher_phinder/mail_parser/received_headers/by_parser.rb
76
+ - lib/phisher_phinder/mail_parser/received_headers/classifier.rb
77
+ - lib/phisher_phinder/mail_parser/received_headers/for_parser.rb
78
+ - lib/phisher_phinder/mail_parser/received_headers/from_parser.rb
79
+ - lib/phisher_phinder/mail_parser/received_headers/parser.rb
80
+ - lib/phisher_phinder/mail_parser/received_headers/starttls_parser.rb
81
+ - lib/phisher_phinder/mail_parser/received_headers/timestamp_parser.rb
82
+ - lib/phisher_phinder/simple_ip.rb
83
+ - lib/phisher_phinder/version.rb
84
+ - phisher_phinder.gemspec
85
+ homepage: https://capefox.co
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ allowed_push_host: https://rubygems.org
90
+ homepage_uri: https://capefox.co
91
+ source_code_uri: https://github.com/rorymckinley/phisher_phinder
92
+ changelog_uri: https://github.com/rorymckinley/phisher_phinder/blob/master/CHANGELOG.md
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: 2.3.0
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubygems_version: 3.1.2
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: A gem for dissecting phishing emails
112
+ test_files: []