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.
- checksums.yaml +7 -0
- data/.env.example +3 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +6 -0
- data/CHANGELOG.md +1 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +93 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +38 -0
- data/Rakefile +38 -0
- data/bin/console +20 -0
- data/bin/setup +8 -0
- data/db/migrations/0001_create_geo_ip_cache.rb +35 -0
- data/lib/phisher_phinder.rb +28 -0
- data/lib/phisher_phinder/body_hyperlink.rb +47 -0
- data/lib/phisher_phinder/cached_geoip_client.rb +95 -0
- data/lib/phisher_phinder/expanded_data_processor.rb +61 -0
- data/lib/phisher_phinder/extended_ip.rb +16 -0
- data/lib/phisher_phinder/extended_ip_factory.rb +51 -0
- data/lib/phisher_phinder/geoip_ip_data.rb +6 -0
- data/lib/phisher_phinder/mail.rb +50 -0
- data/lib/phisher_phinder/mail_parser.rb +111 -0
- data/lib/phisher_phinder/mail_parser/body_parser.rb +94 -0
- data/lib/phisher_phinder/mail_parser/header_value_parser.rb +24 -0
- data/lib/phisher_phinder/mail_parser/received_headers/by_parser.rb +45 -0
- data/lib/phisher_phinder/mail_parser/received_headers/classifier.rb +27 -0
- data/lib/phisher_phinder/mail_parser/received_headers/for_parser.rb +23 -0
- data/lib/phisher_phinder/mail_parser/received_headers/from_parser.rb +40 -0
- data/lib/phisher_phinder/mail_parser/received_headers/parser.rb +74 -0
- data/lib/phisher_phinder/mail_parser/received_headers/starttls_parser.rb +24 -0
- data/lib/phisher_phinder/mail_parser/received_headers/timestamp_parser.rb +32 -0
- data/lib/phisher_phinder/simple_ip.rb +15 -0
- data/lib/phisher_phinder/version.rb +3 -0
- data/phisher_phinder.gemspec +32 -0
- 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,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: []
|