mihari 5.6.1 → 5.6.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/package-lock.json +173 -176
  3. data/frontend/package.json +9 -9
  4. data/lib/mihari/{base.rb → actor.rb} +16 -2
  5. data/lib/mihari/analyzers/base.rb +5 -10
  6. data/lib/mihari/analyzers/censys.rb +1 -1
  7. data/lib/mihari/analyzers/hunterhow.rb +1 -1
  8. data/lib/mihari/analyzers/passivetotal.rb +1 -1
  9. data/lib/mihari/analyzers/pulsedive.rb +1 -1
  10. data/lib/mihari/analyzers/securitytrails.rb +1 -1
  11. data/lib/mihari/analyzers/urlscan.rb +1 -1
  12. data/lib/mihari/analyzers/virustotal.rb +5 -5
  13. data/lib/mihari/analyzers/zoomeye.rb +3 -3
  14. data/lib/mihari/clients/crtsh.rb +2 -2
  15. data/lib/mihari/clients/passivetotal.rb +4 -4
  16. data/lib/mihari/clients/securitytrails.rb +3 -3
  17. data/lib/mihari/commands/rule.rb +2 -11
  18. data/lib/mihari/commands/search.rb +1 -1
  19. data/lib/mihari/emitters/base.rb +13 -24
  20. data/lib/mihari/emitters/database.rb +7 -9
  21. data/lib/mihari/emitters/misp.rb +14 -38
  22. data/lib/mihari/emitters/slack.rb +14 -11
  23. data/lib/mihari/emitters/the_hive.rb +16 -44
  24. data/lib/mihari/emitters/webhook.rb +31 -21
  25. data/lib/mihari/enrichers/base.rb +1 -6
  26. data/lib/mihari/enrichers/whois.rb +1 -1
  27. data/lib/mihari/models/alert.rb +75 -73
  28. data/lib/mihari/models/artifact.rb +182 -180
  29. data/lib/mihari/models/autonomous_system.rb +22 -20
  30. data/lib/mihari/models/cpe.rb +21 -19
  31. data/lib/mihari/models/dns.rb +24 -22
  32. data/lib/mihari/models/geolocation.rb +22 -20
  33. data/lib/mihari/models/port.rb +21 -19
  34. data/lib/mihari/models/reverse_dns.rb +21 -19
  35. data/lib/mihari/models/rule.rb +67 -65
  36. data/lib/mihari/models/tag.rb +5 -3
  37. data/lib/mihari/models/tagging.rb +5 -3
  38. data/lib/mihari/models/whois.rb +18 -16
  39. data/lib/mihari/rule.rb +352 -0
  40. data/lib/mihari/schemas/analyzer.rb +94 -87
  41. data/lib/mihari/schemas/emitter.rb +9 -5
  42. data/lib/mihari/schemas/enricher.rb +8 -4
  43. data/lib/mihari/schemas/mixins.rb +15 -0
  44. data/lib/mihari/schemas/rule.rb +3 -10
  45. data/lib/mihari/services/alert_builder.rb +1 -1
  46. data/lib/mihari/services/alert_proxy.rb +10 -6
  47. data/lib/mihari/services/alert_runner.rb +4 -4
  48. data/lib/mihari/services/rule_builder.rb +3 -3
  49. data/lib/mihari/services/rule_runner.rb +5 -5
  50. data/lib/mihari/structs/binaryedge.rb +1 -1
  51. data/lib/mihari/structs/censys.rb +6 -6
  52. data/lib/mihari/structs/config.rb +1 -1
  53. data/lib/mihari/structs/greynoise.rb +5 -5
  54. data/lib/mihari/structs/hunterhow.rb +3 -3
  55. data/lib/mihari/structs/onyphe.rb +5 -5
  56. data/lib/mihari/structs/shodan.rb +6 -6
  57. data/lib/mihari/structs/urlscan.rb +3 -3
  58. data/lib/mihari/structs/virustotal_intelligence.rb +3 -3
  59. data/lib/mihari/version.rb +1 -1
  60. data/lib/mihari/web/endpoints/alerts.rb +4 -4
  61. data/lib/mihari/web/endpoints/artifacts.rb +6 -6
  62. data/lib/mihari/web/endpoints/rules.rb +10 -17
  63. data/lib/mihari/web/endpoints/tags.rb +2 -2
  64. data/lib/mihari/web/public/assets/{index-9cc489e6.js → index-28d4c79d.js} +48 -48
  65. data/lib/mihari/web/public/index.html +1 -1
  66. data/lib/mihari.rb +6 -8
  67. data/mihari.gemspec +1 -2
  68. data/requirements.txt +1 -1
  69. metadata +8 -22
  70. data/lib/mihari/analyzers/rule.rb +0 -232
  71. data/lib/mihari/services/rule_proxy.rb +0 -182
@@ -1,29 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- class ReverseDnsName < ActiveRecord::Base
5
- belongs_to :artifact
4
+ module Models
5
+ class ReverseDnsName < ActiveRecord::Base
6
+ belongs_to :artifact
6
7
 
7
- class << self
8
- include Dry::Monads[:result]
8
+ class << self
9
+ include Dry::Monads[:result]
9
10
 
10
- #
11
- # Build reverse DNS names
12
- #
13
- # @param [String] ip
14
- # @param [Mihari::Enrichers::Shodan] enricher
15
- #
16
- # @return [Array<Mihari::ReverseDnsName>]
17
- #
18
- def build_by_ip(ip, enricher: Enrichers::Shodan.new)
19
- result = enricher.query_result(ip).bind do |res|
20
- if res.nil?
21
- Success []
22
- else
23
- Success(res.hostnames.map { |name| new(name: name) })
11
+ #
12
+ # Build reverse DNS names
13
+ #
14
+ # @param [String] ip
15
+ # @param [Mihari::Enrichers::Shodan] enricher
16
+ #
17
+ # @return [Array<Mihari::Models::ReverseDnsName>]
18
+ #
19
+ def build_by_ip(ip, enricher: Enrichers::Shodan.new)
20
+ result = enricher.query_result(ip).bind do |res|
21
+ if res.nil?
22
+ Success []
23
+ else
24
+ Success(res.hostnames.map { |name| new(name: name) })
25
+ end
24
26
  end
27
+ result.value_or []
25
28
  end
26
- result.value_or []
27
29
  end
28
30
  end
29
31
  end
@@ -3,77 +3,79 @@
3
3
  require "yaml"
4
4
 
5
5
  module Mihari
6
- class Rule < ActiveRecord::Base
7
- has_many :alerts, dependent: :destroy
6
+ module Models
7
+ class Rule < ActiveRecord::Base
8
+ has_many :alerts, dependent: :destroy
8
9
 
9
- def symbolized_data
10
- @symbolized_data ||= data.deep_symbolize_keys
11
- end
12
-
13
- def yaml
14
- data.to_yaml
15
- end
16
-
17
- def tags
18
- (data["tags"] || []).map { |tag| { name: tag } }
19
- end
20
-
21
- class << self
22
- #
23
- # Search rules
24
- #
25
- # @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
26
- #
27
- # @return [Array<Rule>]
28
- #
29
- def search(filter)
30
- limit = filter.limit.to_i
31
- raise ArgumentError, "limit should be bigger than zero" unless limit.positive?
32
-
33
- page = filter.page.to_i
34
- raise ArgumentError, "page should be bigger than zero" unless page.positive?
35
-
36
- offset = (page - 1) * limit
37
-
38
- relation = build_relation(filter.without_pagination)
39
-
40
- # TODO: improve queires
41
- rule_ids = relation.limit(limit).offset(offset).order(created_at: :desc).pluck(:id).uniq
42
- where(id: [rule_ids]).order(created_at: :desc)
10
+ def symbolized_data
11
+ @symbolized_data ||= data.deep_symbolize_keys
43
12
  end
44
13
 
45
- #
46
- # Count alerts
47
- #
48
- # @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
49
- #
50
- # @return [Integer]
51
- #
52
- def count(filter)
53
- relation = build_relation(filter)
54
- relation.distinct("rules.id").count
14
+ def yaml
15
+ data.to_yaml
55
16
  end
56
17
 
57
- private
58
-
59
- #
60
- # @param [Structs::Filters::Rule::SearchFilter] filter
61
- #
62
- # @return [Mihari::Rule]
63
- #
64
- def build_relation(filter)
65
- relation = self
66
- relation = relation.includes(alerts: :tags)
67
-
68
- relation = relation.where(alerts: { tags: { name: filter.tag_name } }) if filter.tag_name
69
-
70
- relation = relation.where("rules.title LIKE ?", "%#{filter.title}%") if filter.title
71
- relation = relation.where("rules.description LIKE ?", "%#{filter.description}%") if filter.description
72
-
73
- relation = relation.where("rules.created_at >= ?", filter.from_at) if filter.from_at
74
- relation = relation.where("rules.created_at <= ?", filter.to_at) if filter.to_at
18
+ def tags
19
+ (data["tags"] || []).map { |tag| { name: tag } }
20
+ end
75
21
 
76
- relation
22
+ class << self
23
+ #
24
+ # Search rules
25
+ #
26
+ # @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
27
+ #
28
+ # @return [Array<Rule>]
29
+ #
30
+ def search(filter)
31
+ limit = filter.limit.to_i
32
+ raise ArgumentError, "limit should be bigger than zero" unless limit.positive?
33
+
34
+ page = filter.page.to_i
35
+ raise ArgumentError, "page should be bigger than zero" unless page.positive?
36
+
37
+ offset = (page - 1) * limit
38
+
39
+ relation = build_relation(filter.without_pagination)
40
+
41
+ # TODO: improve queires
42
+ rule_ids = relation.limit(limit).offset(offset).order(created_at: :desc).pluck(:id).uniq
43
+ where(id: [rule_ids]).order(created_at: :desc)
44
+ end
45
+
46
+ #
47
+ # Count alerts
48
+ #
49
+ # @param [Structs::Filters::Rule::SearchFilterWithPagination] filter
50
+ #
51
+ # @return [Integer]
52
+ #
53
+ def count(filter)
54
+ relation = build_relation(filter)
55
+ relation.distinct("rules.id").count
56
+ end
57
+
58
+ private
59
+
60
+ #
61
+ # @param [Structs::Filters::Rule::SearchFilter] filter
62
+ #
63
+ # @return [Mihari::Models::Rule]
64
+ #
65
+ def build_relation(filter)
66
+ relation = self
67
+ relation = relation.includes(alerts: :tags)
68
+
69
+ relation = relation.where(alerts: { tags: { name: filter.tag_name } }) if filter.tag_name
70
+
71
+ relation = relation.where("rules.title LIKE ?", "%#{filter.title}%") if filter.title
72
+ relation = relation.where("rules.description LIKE ?", "%#{filter.description}%") if filter.description
73
+
74
+ relation = relation.where("rules.created_at >= ?", filter.from_at) if filter.from_at
75
+ relation = relation.where("rules.created_at <= ?", filter.to_at) if filter.to_at
76
+
77
+ relation
78
+ end
77
79
  end
78
80
  end
79
81
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- class Tag < ActiveRecord::Base
5
- has_many :taggings, dependent: :destroy
6
- has_many :tags, through: :taggings
4
+ module Models
5
+ class Tag < ActiveRecord::Base
6
+ has_many :taggings, dependent: :destroy
7
+ has_many :tags, through: :taggings
8
+ end
7
9
  end
8
10
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- class Tagging < ActiveRecord::Base
5
- belongs_to :alert
6
- belongs_to :tag
4
+ module Models
5
+ class Tagging < ActiveRecord::Base
6
+ belongs_to :alert
7
+ belongs_to :tag
8
+ end
7
9
  end
8
10
  end
@@ -1,25 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- class WhoisRecord < ActiveRecord::Base
5
- belongs_to :artifact
4
+ module Models
5
+ class WhoisRecord < ActiveRecord::Base
6
+ belongs_to :artifact
6
7
 
7
- @memo = {}
8
+ @memo = {}
8
9
 
9
- class << self
10
- include Dry::Monads[:result]
10
+ class << self
11
+ include Dry::Monads[:result]
11
12
 
12
- #
13
- # Build whois record
14
- #
15
- # @param [String] domain
16
- # @param [Mihari::Enrichers::Whois] enricher
17
- #
18
- # @return [WhoisRecord, nil]
19
- #
20
- def build_by_domain(domain, enricher: Enrichers::Whois.new)
21
- result = enricher.query_result(domain)
22
- result.value_or nil
13
+ #
14
+ # Build whois record
15
+ #
16
+ # @param [String] domain
17
+ # @param [Mihari::Enrichers::Whois] enricher
18
+ #
19
+ # @return [WhoisRecord, nil]
20
+ #
21
+ def build_by_domain(domain, enricher: Enrichers::Whois.new)
22
+ result = enricher.query_result(domain)
23
+ result.value_or nil
24
+ end
23
25
  end
24
26
  end
25
27
  end
@@ -0,0 +1,352 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ class Rule
5
+ include Mixins::FalsePositive
6
+
7
+ # @return [Hash]
8
+ attr_reader :data
9
+
10
+ # @return [Array, nil]
11
+ attr_reader :errors
12
+
13
+ # @return [Time]
14
+ attr_reader :base_time
15
+
16
+ #
17
+ # Initialize
18
+ #
19
+ # @param [Hash] data
20
+ #
21
+ def initialize(**data)
22
+ @data = data.deep_symbolize_keys
23
+ @errors = nil
24
+ @base_time = Time.now.utc
25
+
26
+ validate!
27
+ end
28
+
29
+ #
30
+ # @return [Boolean]
31
+ #
32
+ def errors?
33
+ return false if @errors.nil?
34
+
35
+ !@errors.empty?
36
+ end
37
+
38
+ def [](key)
39
+ data key.to_sym
40
+ end
41
+
42
+ #
43
+ # @return [String]
44
+ #
45
+ def id
46
+ data[:id]
47
+ end
48
+
49
+ #
50
+ # @return [String]
51
+ #
52
+ def title
53
+ data[:title]
54
+ end
55
+
56
+ #
57
+ # @return [String]
58
+ #
59
+ def description
60
+ data[:description]
61
+ end
62
+
63
+ #
64
+ # @return [String]
65
+ #
66
+ def yaml
67
+ data.deep_stringify_keys.to_yaml
68
+ end
69
+
70
+ #
71
+ # @return [Array<Hash>]
72
+ #
73
+ def queries
74
+ data[:queries]
75
+ end
76
+
77
+ #
78
+ # @return [Array<String>]
79
+ #
80
+ def data_types
81
+ data[:data_types]
82
+ end
83
+
84
+ #
85
+ # @return [Array<String>]
86
+ #
87
+ def tags
88
+ data[:tags]
89
+ end
90
+
91
+ #
92
+ # @return [Array<String, RegExp>]
93
+ #
94
+ def falsepositives
95
+ @falsepositives ||= data[:falsepositives].map { |fp| normalize_falsepositive fp }
96
+ end
97
+
98
+ #
99
+ # @return [Integer, nil]
100
+ #
101
+ def artifact_lifetime
102
+ data[:artifact_lifetime] || data[:artifact_ttl]
103
+ end
104
+
105
+ #
106
+ # Returns a list of artifacts matched with queries/analyzers (with the rule ID)
107
+ #
108
+ # @return [Array<Mihari::Models::Artifact>]
109
+ #
110
+ def artifacts
111
+ analyzers.flat_map do |analyzer|
112
+ result = analyzer.result
113
+
114
+ raise result.failure if result.failure? && !analyzer.ignore_error?
115
+
116
+ artifacts = result.value!
117
+ artifacts.map do |artifact|
118
+ artifact.rule_id = id
119
+ artifact
120
+ end
121
+ end
122
+ end
123
+
124
+ #
125
+ # Normalize artifacts
126
+ # - Reject invalid artifacts (for just in case)
127
+ # - Select artifacts with allowed data types
128
+ # - Reject artifacts with false positive values
129
+ # - Set rule ID
130
+ #
131
+ # @return [Array<Mihari::Models::Artifact>]
132
+ #
133
+ def normalized_artifacts
134
+ valid_artifacts = artifacts.uniq(&:data).select(&:valid?)
135
+ date_type_allowed_artifacts = valid_artifacts.select { |artifact| data_types.include? artifact.data_type }
136
+ date_type_allowed_artifacts.reject { |artifact| falsepositive? artifact.data }
137
+ end
138
+
139
+ #
140
+ # Uniquify artifacts (assure rule level uniqueness)
141
+ #
142
+ # @return [Array<Mihari::Models::Artifact>]
143
+ #
144
+ def unique_artifacts
145
+ normalized_artifacts.select do |artifact|
146
+ artifact.unique?(base_time: base_time, artifact_lifetime: artifact_lifetime)
147
+ end
148
+ end
149
+
150
+ #
151
+ # Enriched artifacts
152
+ #
153
+ # @return [Array<Mihari::Models::Artifact>]
154
+ #
155
+ def enriched_artifacts
156
+ @enriched_artifacts ||= Parallel.map(unique_artifacts) do |artifact|
157
+ enrichers.each { |enricher| artifact.enrich_by_enricher enricher }
158
+ artifact
159
+ end
160
+ end
161
+
162
+ #
163
+ # Bulk emit
164
+ #
165
+ # @return [Array<Mihari::Models::Alert>]
166
+ #
167
+ def bulk_emit
168
+ return [] if enriched_artifacts.empty?
169
+
170
+ # NOTE: separate parallel execution and logging
171
+ # because the logger does not work along with Parallel
172
+ results = Parallel.map(emitters) { |emitter| emitter.emit_result(enriched_artifacts) }
173
+ results.zip(emitters).map do |result_and_emitter|
174
+ result, emitter = result_and_emitter
175
+ Mihari.logger.info "Emission by #{emitter.class} is failed: #{result.failure}" if result.failure?
176
+ Mihari.logger.info "Emission by #{emitter.class} is succeeded" if result.success?
177
+ result.value_or nil
178
+ end.compact
179
+ end
180
+
181
+ #
182
+ # Set artifacts & run emitters in parallel
183
+ #
184
+ # @return [Mihari::Models::Alert, nil]
185
+ #
186
+ def run
187
+ # Validate analyzers & emitters before using them
188
+ analyzers
189
+ emitters
190
+
191
+ alert_or_something = bulk_emit
192
+ # Return Mihari::Models::Alert created by the database emitter
193
+ alert_or_something.find { |res| res.is_a?(Mihari::Models::Alert) }
194
+ end
195
+
196
+ #
197
+ # @return [Mihari::Models::Rule]
198
+ #
199
+ def model
200
+ rule = Mihari::Models::Rule.find(id)
201
+
202
+ rule.title = title
203
+ rule.description = description
204
+ rule.data = data
205
+
206
+ rule
207
+ rescue ActiveRecord::RecordNotFound
208
+ Mihari::Models::Rule.new(
209
+ id: id,
210
+ title: title,
211
+ description: description,
212
+ data: data
213
+ )
214
+ end
215
+
216
+ class << self
217
+ #
218
+ # Load rule from YAML string
219
+ #
220
+ # @param [String] yaml
221
+ #
222
+ # @return [Mihari::Services::Rule]
223
+ #
224
+ def from_yaml(yaml)
225
+ data = YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol])
226
+ new(**data)
227
+ end
228
+
229
+ #
230
+ # @param [Mihari::Models::Rule] model
231
+ #
232
+ # @return [Mihari::Services::Rule]
233
+ #
234
+ def from_model(model)
235
+ new(**model.data)
236
+ end
237
+ end
238
+
239
+ private
240
+
241
+ #
242
+ # Check whether a value is a falsepositive value or not
243
+ #
244
+ # @return [Boolean]
245
+ #
246
+ def falsepositive?(value)
247
+ return true if falsepositives.include?(value)
248
+
249
+ regexps = falsepositives.select { |fp| fp.is_a?(Regexp) }
250
+ regexps.any? { |fp| fp.match?(value) }
251
+ end
252
+
253
+ #
254
+ # Get analyzer class
255
+ #
256
+ # @param [String] key
257
+ #
258
+ # @return [Class<Mihari::Analyzers::Base>] analyzer class
259
+ #
260
+ def get_analyzer_class(key)
261
+ raise ArgumentError, "#{key} is not supported" unless Mihari.analyzer_to_class.key?(key)
262
+
263
+ Mihari.analyzer_to_class[key]
264
+ end
265
+
266
+ #
267
+ # @return [Array<Mihari::Analyzers::Base>]
268
+ #
269
+ def analyzers
270
+ @analyzers ||= queries.map do |query_params|
271
+ analyzer_name = query_params[:analyzer]
272
+ klass = get_analyzer_class(analyzer_name)
273
+ klass.from_query(query_params)
274
+ end.map do |analyzer|
275
+ analyzer.validate_configuration!
276
+ analyzer
277
+ end
278
+ end
279
+
280
+ #
281
+ # Get emitter class
282
+ #
283
+ # @param [String] key
284
+ #
285
+ # @return [Class<Mihari::Emitters::Base>] emitter class
286
+ #
287
+ def get_emitter_class(key)
288
+ raise ArgumentError, "#{key} is not supported" unless Mihari.emitter_to_class.key?(key)
289
+
290
+ Mihari.emitter_to_class[key]
291
+ end
292
+
293
+ #
294
+ # @return [Array<Mihari::Emitters::Base>]
295
+ #
296
+ def emitters
297
+ @emitters ||= data[:emitters].map(&:deep_dup).map do |params|
298
+ name = params[:emitter]
299
+ options = params[:options]
300
+
301
+ %i[emitter options].each { |key| params.delete key }
302
+
303
+ klass = get_emitter_class(name)
304
+ klass.new(rule: self, options: options, **params)
305
+ end.map do |emitter|
306
+ emitter.validate_configuration!
307
+ emitter
308
+ end
309
+ end
310
+
311
+ #
312
+ # Get enricher class
313
+ #
314
+ # @param [String] key
315
+ #
316
+ # @return [Class<Mihari::Enrichers::Base>] enricher class
317
+ #
318
+ def get_enricher_class(key)
319
+ raise ArgumentError, "#{key} is not supported" unless Mihari.enricher_to_class.key?(key)
320
+
321
+ Mihari.enricher_to_class[key]
322
+ end
323
+
324
+ #
325
+ # @return [Array<Mihari::Enrichers::Base>] enrichers
326
+ #
327
+ def enrichers
328
+ @enrichers ||= data[:enrichers].map(&:deep_dup).map do |params|
329
+ name = params[:enricher]
330
+ options = params[:options]
331
+
332
+ %i[enricher options].each { |key| params.delete key }
333
+
334
+ klass = get_enricher_class(name)
335
+ klass.new(options: options, **params)
336
+ end
337
+ end
338
+
339
+ #
340
+ # Validate the data format
341
+ #
342
+ def validate!
343
+ contract = Schemas::RuleContract.new
344
+ result = contract.call(data)
345
+
346
+ @data = result.to_h
347
+ @errors = result.errors
348
+
349
+ raise ValidationError.new("Validation failed", errors) if errors?
350
+ end
351
+ end
352
+ end