mihari 4.6.1 → 4.7.2

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mihari/analyzers/clients/otx.rb +36 -0
  3. data/lib/mihari/analyzers/otx.rb +19 -11
  4. data/lib/mihari/analyzers/rule.rb +17 -1
  5. data/lib/mihari/commands/init.rb +25 -2
  6. data/lib/mihari/commands/search.rb +2 -7
  7. data/lib/mihari/commands/validator.rb +10 -5
  8. data/lib/mihari/constants.rb +2 -0
  9. data/lib/mihari/enrichers/google_public_dns.rb +36 -0
  10. data/lib/mihari/enrichers/whois.rb +126 -0
  11. data/lib/mihari/errors.rb +2 -0
  12. data/lib/mihari/http.rb +2 -2
  13. data/lib/mihari/models/alert.rb +6 -1
  14. data/lib/mihari/models/artifact.rb +30 -0
  15. data/lib/mihari/models/dns.rb +5 -21
  16. data/lib/mihari/models/geolocation.rb +2 -4
  17. data/lib/mihari/models/port.rb +1 -1
  18. data/lib/mihari/models/rule.rb +7 -2
  19. data/lib/mihari/models/whois.rb +1 -96
  20. data/lib/mihari/schemas/enricher.rb +9 -0
  21. data/lib/mihari/schemas/rule.rb +6 -0
  22. data/lib/mihari/structs/filters.rb +71 -0
  23. data/lib/mihari/structs/google_public_dns.rb +42 -0
  24. data/lib/mihari/structs/ipinfo.rb +4 -4
  25. data/lib/mihari/structs/rule.rb +187 -137
  26. data/lib/mihari/types.rb +7 -0
  27. data/lib/mihari/version.rb +1 -1
  28. data/lib/mihari/web/endpoints/alerts.rb +1 -1
  29. data/lib/mihari/web/endpoints/rules.rb +13 -5
  30. data/lib/mihari/web/public/index.html +1 -1
  31. data/lib/mihari/web/public/redoc-static.html +796 -763
  32. data/lib/mihari/web/public/static/css/chunk-vendors.5013d549.css +7 -0
  33. data/lib/mihari/web/public/static/js/app.3ac3bd7a.js +2 -0
  34. data/lib/mihari/web/public/static/js/app.3ac3bd7a.js.map +1 -0
  35. data/lib/mihari/web/public/static/js/{chunk-vendors.dde2116c.js → chunk-vendors.37b7208e.js} +6 -6
  36. data/lib/mihari/web/public/static/js/chunk-vendors.37b7208e.js.map +1 -0
  37. data/lib/mihari.rb +4 -2
  38. data/mihari.gemspec +8 -9
  39. data/sig/lib/mihari/cli/base.rbs +0 -2
  40. data/sig/lib/mihari/enrichers/google_public_dns.rbs +18 -0
  41. data/sig/lib/mihari/models/alert.rbs +3 -3
  42. data/sig/lib/mihari/models/rule.rbs +2 -2
  43. data/sig/lib/mihari/structs/filters.rbs +40 -0
  44. data/sig/lib/mihari/structs/google_public_dns.rbs +21 -0
  45. data/sig/lib/mihari/structs/ipinfo.rbs +2 -2
  46. data/sig/lib/mihari/structs/rule.rbs +36 -43
  47. metadata +32 -45
  48. data/lib/mihari/mixins/rule.rb +0 -84
  49. data/lib/mihari/structs/alert.rb +0 -44
  50. data/lib/mihari/web/public/static/css/chunk-vendors.06251949.css +0 -7
  51. data/lib/mihari/web/public/static/js/app-legacy.9d5c9c3d.js +0 -2
  52. data/lib/mihari/web/public/static/js/app-legacy.9d5c9c3d.js.map +0 -1
  53. data/lib/mihari/web/public/static/js/app.823b5af7.js +0 -2
  54. data/lib/mihari/web/public/static/js/app.823b5af7.js.map +0 -1
  55. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.b110c129.js +0 -25
  56. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.b110c129.js.map +0 -1
  57. data/lib/mihari/web/public/static/js/chunk-vendors.dde2116c.js.map +0 -1
  58. data/sig/lib/mihari/mixins/rule.rbs +0 -36
  59. data/sig/lib/mihari/structs/alert.rbs +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 491cb9d0477c4101251c1f3e5705328008a196920efb41c239048247937f21f4
4
- data.tar.gz: 14bcfd11b8871a759e70f6658cf2773dadc6afe7ffde44dbbffaef99961b8e00
3
+ metadata.gz: 7eeecbacefc194f5d2e9356fe8f17884c358b45bb3b7ccbd83a47aa3567ae1f6
4
+ data.tar.gz: d99abc5ef4368bf1a23a48cf0f15a64a33d3539313a624d489887e86a8cc564c
5
5
  SHA512:
6
- metadata.gz: d833915545629ade609ec994e6139387fd66247fa1c48fdbe4e3eab01679fb42820e3b07cb95fdbb8cff092753394f1dfa9aaa4459259837c2c0d7d5f2f855ed
7
- data.tar.gz: dcbb112faa805b27157ec72d0f3544031c7e7789e855eb082a69a76216d0fcd644013ade95aea28ae138ebfb3bc8eb90d8fb922c83999d292c12b78d15acfe5c
6
+ metadata.gz: e0347f1c03a949d7e3c85cb9a615247c41bea2a0ac8288f0e3c7a8f9bd5d93ad8f952c99f3b595bf19997fe10e32490898b58a858f63f402a6082344f68cdb87
7
+ data.tar.gz: a507cfb86593c602ba46ef59f279a17ed1a34e433c8e34898a872048306733d26fb1a6546f1ae96605d524ccd80c3c3a0487ab4b4ad66219c0e3a63215ce8c6e
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Analyzers
5
+ module Clients
6
+ class OTX
7
+ attr_reader :api_key
8
+
9
+ def initialize(api_key)
10
+ @api_key = api_key
11
+ end
12
+
13
+ def query_by_ip(ip)
14
+ get "https://otx.alienvault.com/api/v1/indicators/IPv4/#{ip}/passive_dns"
15
+ end
16
+
17
+ def query_by_domain(domain)
18
+ get "https://otx.alienvault.com/api/v1/indicators/domain/#{domain}/passive_dns"
19
+ end
20
+
21
+ private
22
+
23
+ def headers
24
+ { "x-otx-api-key": api_key }
25
+ end
26
+
27
+ def get(url)
28
+ res = HTTP.get(url, headers: headers)
29
+ JSON.parse(res.body.to_s)
30
+ rescue HTTPError
31
+ nil
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "otx_ruby"
3
+ require "mihari/analyzers/clients/otx"
4
4
 
5
5
  module Mihari
6
6
  module Analyzers
@@ -34,12 +34,8 @@ module Mihari
34
34
  %w[otx_api_key]
35
35
  end
36
36
 
37
- def domain_client
38
- @domain_client ||= ::OTX::Domain.new(api_key)
39
- end
40
-
41
- def ip_client
42
- @ip_client ||= ::OTX::IP.new(api_key)
37
+ def client
38
+ @client ||= Mihari::Analyzers::Clients::OTX.new(api_key)
43
39
  end
44
40
 
45
41
  #
@@ -73,9 +69,15 @@ module Mihari
73
69
  # @return [Array<String>]
74
70
  #
75
71
  def domain_search
76
- records = domain_client.get_passive_dns(query)
72
+ res = client.query_by_domain(query)
73
+ return [] if res.nil?
74
+
75
+ records = res["passive_dns"] || []
77
76
  records.filter_map do |record|
78
- record.address if record.record_type == "A"
77
+ record_type = record["record_type"]
78
+ address = record["address"]
79
+
80
+ address if record_type == "A"
79
81
  end.uniq
80
82
  end
81
83
 
@@ -85,9 +87,15 @@ module Mihari
85
87
  # @return [Array<String>]
86
88
  #
87
89
  def ip_search
88
- records = ip_client.get_passive_dns(query)
90
+ res = client.query_by_ip(query)
91
+ return [] if res.nil?
92
+
93
+ records = res["passive_dns"] || []
89
94
  records.filter_map do |record|
90
- record.hostname if record.record_type == "A"
95
+ record_type = record["record_type"]
96
+ hostname = record["hostname"]
97
+
98
+ hostname if record_type == "A"
91
99
  end.uniq
92
100
  end
93
101
  end
@@ -39,7 +39,6 @@ module Mihari
39
39
 
40
40
  class Rule < Base
41
41
  include Mixins::DisallowedDataValue
42
- include Mixins::Rule
43
42
 
44
43
  option :title
45
44
  option :description
@@ -51,6 +50,7 @@ module Mihari
51
50
  option :disallowed_data_values, default: proc { [] }
52
51
 
53
52
  option :emitters, optional: true
53
+ option :enrichers, optional: true
54
54
 
55
55
  attr_reader :source
56
56
 
@@ -60,6 +60,7 @@ module Mihari
60
60
  @source = id
61
61
 
62
62
  @emitters = emitters || DEFAULT_EMITTERS
63
+ @enrichers = enrichers || DEFAULT_ENRICHERS
63
64
 
64
65
  validate_analyzer_configurations
65
66
  end
@@ -112,6 +113,21 @@ module Mihari
112
113
  end
113
114
  end
114
115
 
116
+ #
117
+ # Enriched artifacts
118
+ #
119
+ # @return [Array<Mihari::Artifact>]
120
+ #
121
+ def enriched_artifacts
122
+ @enriched_artifacts ||= Parallel.map(unique_artifacts) do |artifact|
123
+ enrichers.each do |enricher|
124
+ artifact.enrich_by_enricher(enricher[:enricher])
125
+ end
126
+
127
+ artifact
128
+ end
129
+ end
130
+
115
131
  #
116
132
  # Normalized disallowed data values
117
133
  #
@@ -3,8 +3,6 @@
3
3
  module Mihari
4
4
  module Commands
5
5
  module Initialization
6
- include Mixins::Rule
7
-
8
6
  def self.included(thor)
9
7
  thor.class_eval do
10
8
  desc "rule", "Create a rule file"
@@ -21,6 +19,31 @@ module Mihari
21
19
 
22
20
  Mihari.logger.info "The rule file is initialized as #{filename}."
23
21
  end
22
+
23
+ no_commands do
24
+ #
25
+ # Returns a template for rule
26
+ #
27
+ # @return [String] A template for rule
28
+ #
29
+ def rule_template
30
+ rule = Structs::Rule.from_path_or_id File.expand_path("../templates/rule.yml.erb", __dir__)
31
+ rule.yaml
32
+ end
33
+
34
+ #
35
+ # Create (blank) rule file
36
+ #
37
+ # @param [String] filename
38
+ # @param [Dry::Files] files
39
+ # @param [String] template
40
+ #
41
+ # @return [nil]
42
+ #
43
+ def initialize_rule_yaml(filename, files = Dry::Files.new, template: rule_template)
44
+ files.write(filename, template)
45
+ end
46
+ end
24
47
  end
25
48
  end
26
49
  end
@@ -4,7 +4,6 @@ module Mihari
4
4
  module Commands
5
5
  module Search
6
6
  include Mixins::Database
7
- include Mixins::Rule
8
7
  include Mixins::ErrorNotification
9
8
 
10
9
  def self.included(thor)
@@ -12,14 +11,10 @@ module Mihari
12
11
  desc "search [RULE]", "Search by a rule"
13
12
  method_option :yes, type: :boolean, aliases: "-y", desc: "yes to overwrite the rule in the database"
14
13
  def search_by_rule(path_or_id)
15
- rule = load_rule(path_or_id)
14
+ rule = Structs::Rule.from_path_or_id path_or_id
16
15
 
17
16
  # validate
18
- begin
19
- validate_rule! rule
20
- rescue RuleValidationError => e
21
- raise e
22
- end
17
+ rule.validate!
23
18
 
24
19
  # check update
25
20
  id = rule.id
@@ -3,16 +3,21 @@
3
3
  module Mihari
4
4
  module Commands
5
5
  module Validator
6
- include Mixins::Rule
7
-
8
6
  def self.included(thor)
9
7
  thor.class_eval do
10
- desc "rule [PATH]", "Validate format of a rule file"
8
+ desc "rule [PATH]", "Validate rule file format"
9
+ #
10
+ # Validate format of a rule
11
+ #
12
+ # @param [String] path
13
+ #
14
+ # @return [nil]
15
+ #
11
16
  def rule(path)
12
- rule = load_rule(path)
17
+ rule = Structs::Rule.from_path_or_id(path)
13
18
 
14
19
  begin
15
- validate_rule! rule
20
+ rule.validate!
16
21
  Mihari.logger.info "Valid format. The input is parsed as the following:\n#{rule.data.to_yaml}"
17
22
  rescue RuleValidationError
18
23
  nil
@@ -4,4 +4,6 @@ module Mihari
4
4
  ALLOWED_DATA_TYPES = ["hash", "ip", "domain", "url", "mail"].freeze
5
5
 
6
6
  DEFAULT_EMITTERS = ["database", "misp", "slack", "the_hive", "webhook"].map { |name| { emitter: name } }.freeze
7
+
8
+ DEFAULT_ENRICHERS = ["whois", "ipinfo", "shodan", "google_public_dns"].map { |name| { enricher: name } }.freeze
7
9
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/https"
4
+
5
+ module Mihari
6
+ module Enrichers
7
+ class GooglePublicDNS < Base
8
+ # @return [Boolean]
9
+ def valid?
10
+ true
11
+ end
12
+
13
+ class << self
14
+ #
15
+ # Query Google Public DNS
16
+ #
17
+ # @param [String] name
18
+ # @param [String] resource_type
19
+ #
20
+ # @return [Mihari::Structs::Shodan::GooglePublicDNS::Response, nil]
21
+ #
22
+ def query(name, resource_type)
23
+ url = "https://dns.google/resolve"
24
+ params = { name: name, type: resource_type }
25
+ res = HTTP.get(url, params: params)
26
+
27
+ data = JSON.parse(res.body.to_s)
28
+
29
+ Structs::GooglePublicDNS::Response.from_dynamic! data
30
+ rescue HTTPError
31
+ nil
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "whois-parser"
4
+
5
+ module Mihari
6
+ module Enrichers
7
+ class Whois < Base
8
+ @memo = {}
9
+
10
+ # @return [Boolean]
11
+ def valid?
12
+ true
13
+ end
14
+
15
+ class << self
16
+ #
17
+ # Query IAIA Whois API
18
+ #
19
+ # @param [String] name
20
+ #
21
+ # @return [Mihari::WhoisRecord, nil]
22
+ #
23
+ def query(domain)
24
+ domain = PublicSuffix.domain(domain)
25
+
26
+ # check memo
27
+ if @memo.key?(domain)
28
+ whois_record = @memo[domain]
29
+ # return clone of the record
30
+ return whois_record.dup
31
+ end
32
+
33
+ record = ::Whois.whois(domain)
34
+ parser = record.parser
35
+
36
+ return nil if parser.available?
37
+
38
+ whois_record = WhoisRecord.new(
39
+ domain: domain,
40
+ created_on: get_created_on(parser),
41
+ updated_on: get_updated_on(parser),
42
+ expires_on: get_expires_on(parser),
43
+ registrar: get_registrar(parser),
44
+ contacts: get_contacts(parser)
45
+ )
46
+ # set memo
47
+ @memo[domain] = whois_record
48
+ whois_record
49
+ rescue ::Whois::Error, ::Whois::ParserError, Timeout::Error
50
+ nil
51
+ end
52
+
53
+ def reset_cache
54
+ @memo = {}
55
+ end
56
+
57
+ private
58
+
59
+ #
60
+ # Get created_on
61
+ #
62
+ # @param [::Whois::Parser:] parser
63
+ #
64
+ # @return [Date, nil]
65
+ #
66
+ def get_created_on(parser)
67
+ parser.created_on
68
+ rescue ::Whois::AttributeNotImplemented
69
+ nil
70
+ end
71
+
72
+ #
73
+ # Get updated_on
74
+ #
75
+ # @param [::Whois::Parser:] parser
76
+ #
77
+ # @return [Date, nil]
78
+ #
79
+ def get_updated_on(parser)
80
+ parser.updated_on
81
+ rescue ::Whois::AttributeNotImplemented
82
+ nil
83
+ end
84
+
85
+ #
86
+ # Get expires_on
87
+ #
88
+ # @param [::Whois::Parser:] parser
89
+ #
90
+ # @return [Date, nil]
91
+ #
92
+ def get_expires_on(parser)
93
+ parser.expires_on
94
+ rescue ::Whois::AttributeNotImplemented
95
+ nil
96
+ end
97
+
98
+ #
99
+ # Get registrar
100
+ #
101
+ # @param [::Whois::Parser:] parser
102
+ #
103
+ # @return [Hash, nil]
104
+ #
105
+ def get_registrar(parser)
106
+ parser.registrar&.to_h
107
+ rescue ::Whois::AttributeNotImplemented
108
+ nil
109
+ end
110
+
111
+ #
112
+ # Get contacts
113
+ #
114
+ # @param [::Whois::Parser:] parser
115
+ #
116
+ # @return [Array[Hash], nil]
117
+ #
118
+ def get_contacts(parser)
119
+ parser.contacts.map(&:to_h)
120
+ rescue ::Whois::AttributeNotImplemented
121
+ nil
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
data/lib/mihari/errors.rb CHANGED
@@ -15,6 +15,8 @@ module Mihari
15
15
 
16
16
  class RuleValidationError < Error; end
17
17
 
18
+ class YAMLSyntaxError < Error; end
19
+
18
20
  class ConfigurationError < Error; end
19
21
 
20
22
  class HTTPError < Error; end
data/lib/mihari/http.rb CHANGED
@@ -44,8 +44,8 @@ module Mihari
44
44
  end
45
45
 
46
46
  class << self
47
- def get(uri, headers: {}, payload: {})
48
- client = new(uri, headers: headers, payload: payload)
47
+ def get(uri, headers: {}, params: {})
48
+ client = new(uri, headers: headers, payload: params)
49
49
  client.get
50
50
  end
51
51
 
@@ -12,7 +12,7 @@ module Mihari
12
12
  #
13
13
  # Search alerts
14
14
  #
15
- # @param [Structs::Alert::SearchFilterWithPagination] filter
15
+ # @param [Structs::Filters::Alert::SearchFilterWithPagination] filter
16
16
  #
17
17
  # @return [Array<Alert>]
18
18
  #
@@ -58,6 +58,11 @@ module Mihari
58
58
 
59
59
  private
60
60
 
61
+ #
62
+ # @param [Structs::Filters::Alert::SearchFilter] filter
63
+ #
64
+ # @return [Mihari::Alert]
65
+ #
61
66
  def build_relation(filter)
62
67
  artifact_ids = []
63
68
  artifact = Artifact.includes(:autonomous_system, :dns_records, :reverse_dns_names)
@@ -135,6 +135,36 @@ module Mihari
135
135
  enrich_cpes
136
136
  end
137
137
 
138
+ ENRICH_METHODS_BY_ENRICHER = {
139
+ whois: [
140
+ :enrich_whois
141
+ ],
142
+ ipinfo: [
143
+ :enrich_autonomous_system,
144
+ :enrich_geolocation
145
+ ],
146
+ shodan: [
147
+ :enrich_ports,
148
+ :enrich_cpes,
149
+ :enrich_reverse_dns
150
+ ],
151
+ google_public_dns: [
152
+ :enrich_dns
153
+ ]
154
+ }.freeze
155
+
156
+ #
157
+ # Enrich by name of enricher
158
+ #
159
+ # @param [String] enricher
160
+ #
161
+ def enrich_by_enricher(enricher)
162
+ methods = ENRICH_METHODS_BY_ENRICHER[enricher.downcase.to_sym] || []
163
+ methods.each do |method|
164
+ send(method) if respond_to?(method)
165
+ end
166
+ end
167
+
138
168
  private
139
169
 
140
170
  def normalize_as_domain(url_or_domain)
@@ -13,14 +13,7 @@ module Mihari
13
13
  # @return [Array<Mihari::DnsRecord>]
14
14
  #
15
15
  def build_by_domain(domain)
16
- resource_types = [
17
- Resolv::DNS::Resource::IN::A,
18
- Resolv::DNS::Resource::IN::AAAA,
19
- Resolv::DNS::Resource::IN::CNAME,
20
- Resolv::DNS::Resource::IN::TXT,
21
- Resolv::DNS::Resource::IN::NS
22
- ]
23
-
16
+ resource_types = %w[A AAAA CNAME TXT NS]
24
17
  resource_types.map do |resource_type|
25
18
  get_values domain, resource_type
26
19
  rescue Resolv::ResolvError
@@ -31,20 +24,11 @@ module Mihari
31
24
  private
32
25
 
33
26
  def get_values(domain, resource_type)
34
- resources = Resolv::DNS.new.getresources(domain, resource_type)
35
- resource_name = resource_type.to_s.split("::").last
27
+ response = Enrichers::GooglePublicDNS.query(domain, resource_type)
28
+ answers = response.answers || []
36
29
 
37
- resources.filter_map do |resource|
38
- # A, AAAA
39
- if resource.respond_to?(:address)
40
- new(resource: resource_name, value: resource.address.to_s)
41
- # CNAME, NS
42
- elsif resource.respond_to?(:name)
43
- new(resource: resource_name, value: resource.name.to_s)
44
- # TXT
45
- elsif resource.respond_to?(:data)
46
- new(resource: resource_name, value: resource.data.to_s)
47
- end
30
+ answers.filter_map do |answer|
31
+ new(resource: answer.resource_type, value: answer.data)
48
32
  end
49
33
  end
50
34
  end
@@ -17,11 +17,9 @@ module Mihari
17
17
  def build_by_ip(ip)
18
18
  res = Enrichers::IPInfo.query(ip)
19
19
 
20
- unless res.nil?
21
- return new(country: NormalizeCountry(res.country_code, to: :short), country_code: res.country_code)
22
- end
20
+ return nil if res&.country_code.nil?
23
21
 
24
- nil
22
+ new(country: NormalizeCountry(res.country_code, to: :short), country_code: res.country_code)
25
23
  end
26
24
  end
27
25
  end
@@ -14,7 +14,7 @@ module Mihari
14
14
  #
15
15
  def build_by_ip(ip)
16
16
  res = Enrichers::Shodan.query(ip)
17
- return if res.nil?
17
+ return [] if res.nil?
18
18
 
19
19
  res.ports.map { |port| new(port: port) }
20
20
  end
@@ -28,7 +28,7 @@ module Mihari
28
28
  #
29
29
  # Search rules
30
30
  #
31
- # @param [Structs::Rule::SearchFilterWithPagination] filter
31
+ # @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
32
32
  #
33
33
  # @return [Array<Rule>]
34
34
  #
@@ -51,7 +51,7 @@ module Mihari
51
51
  #
52
52
  # Count alerts
53
53
  #
54
- # @param [Structs::Rule::SearchFilterWithPagination] filter
54
+ # @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
55
55
  #
56
56
  # @return [Integer]
57
57
  #
@@ -62,6 +62,11 @@ module Mihari
62
62
 
63
63
  private
64
64
 
65
+ #
66
+ # @param [Structs::Filters::Rule::SearchFilter] filter
67
+ #
68
+ # @return [Mihari::Rule]
69
+ #
65
70
  def build_relation(filter)
66
71
  relation = self
67
72
  relation = relation.includes(alerts: :tags)