mihari 5.6.1 → 5.6.2

Sign up to get free protection for your applications and to get access to all the features.
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