mihari 5.6.0 → 5.6.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. checksums.yaml +4 -4
  2. data/docs/analyzers/binaryedge.md +1 -1
  3. data/docs/analyzers/censys.md +1 -1
  4. data/docs/analyzers/circl.md +1 -1
  5. data/docs/analyzers/crtsh.md +1 -1
  6. data/docs/analyzers/dnstwister.md +1 -1
  7. data/docs/analyzers/greynoise.md +1 -1
  8. data/docs/analyzers/hunterhow.md +1 -1
  9. data/docs/analyzers/index.md +29 -15
  10. data/docs/analyzers/onyphe.md +1 -1
  11. data/docs/analyzers/otx.md +2 -2
  12. data/docs/analyzers/passivetotal.md +2 -2
  13. data/docs/analyzers/pulsedive.md +2 -2
  14. data/docs/analyzers/securitytrails.md +2 -2
  15. data/docs/analyzers/shodan.md +1 -1
  16. data/docs/analyzers/urlscan.md +3 -3
  17. data/docs/analyzers/virustotal.md +2 -2
  18. data/docs/analyzers/virustotal_intelligence.md +4 -4
  19. data/docs/analyzers/zoomeye.md +5 -0
  20. data/docs/enrichers/google_public_dns.md +1 -1
  21. data/docs/enrichers/ipinfo.md +2 -2
  22. data/docs/enrichers/shodan.md +4 -4
  23. data/docs/enrichers/whois.md +1 -1
  24. data/frontend/package-lock.json +176 -179
  25. data/frontend/package.json +9 -9
  26. data/lib/mihari/{base.rb → actor.rb} +16 -2
  27. data/lib/mihari/analyzers/base.rb +5 -10
  28. data/lib/mihari/analyzers/censys.rb +1 -1
  29. data/lib/mihari/analyzers/circl.rb +1 -1
  30. data/lib/mihari/analyzers/crtsh.rb +1 -1
  31. data/lib/mihari/analyzers/dnstwister.rb +1 -1
  32. data/lib/mihari/analyzers/hunterhow.rb +1 -1
  33. data/lib/mihari/analyzers/otx.rb +1 -1
  34. data/lib/mihari/analyzers/passivetotal.rb +2 -2
  35. data/lib/mihari/analyzers/pulsedive.rb +2 -2
  36. data/lib/mihari/analyzers/securitytrails.rb +2 -2
  37. data/lib/mihari/analyzers/urlscan.rb +1 -1
  38. data/lib/mihari/analyzers/virustotal.rb +5 -5
  39. data/lib/mihari/analyzers/zoomeye.rb +3 -3
  40. data/lib/mihari/clients/base.rb +2 -2
  41. data/lib/mihari/clients/binaryedge.rb +3 -5
  42. data/lib/mihari/clients/censys.rb +3 -3
  43. data/lib/mihari/clients/circl.rb +5 -4
  44. data/lib/mihari/clients/crtsh.rb +5 -4
  45. data/lib/mihari/clients/dnstwister.rb +3 -2
  46. data/lib/mihari/clients/greynoise.rb +2 -2
  47. data/lib/mihari/clients/hunterhow.rb +2 -2
  48. data/lib/mihari/clients/misp.rb +1 -1
  49. data/lib/mihari/clients/onyphe.rb +2 -2
  50. data/lib/mihari/clients/otx.rb +4 -3
  51. data/lib/mihari/clients/passivetotal.rb +9 -8
  52. data/lib/mihari/clients/publsedive.rb +4 -3
  53. data/lib/mihari/clients/securitytrails.rb +8 -6
  54. data/lib/mihari/clients/shodan.rb +2 -2
  55. data/lib/mihari/clients/the_hive.rb +1 -1
  56. data/lib/mihari/clients/urlscan.rb +4 -4
  57. data/lib/mihari/clients/virustotal.rb +2 -2
  58. data/lib/mihari/clients/zoomeye.rb +2 -2
  59. data/lib/mihari/commands/rule.rb +2 -11
  60. data/lib/mihari/commands/search.rb +1 -1
  61. data/lib/mihari/emitters/base.rb +13 -24
  62. data/lib/mihari/emitters/database.rb +7 -9
  63. data/lib/mihari/emitters/misp.rb +14 -38
  64. data/lib/mihari/emitters/slack.rb +14 -11
  65. data/lib/mihari/emitters/the_hive.rb +16 -44
  66. data/lib/mihari/emitters/webhook.rb +31 -21
  67. data/lib/mihari/enrichers/base.rb +1 -6
  68. data/lib/mihari/enrichers/whois.rb +1 -1
  69. data/lib/mihari/models/alert.rb +75 -73
  70. data/lib/mihari/models/artifact.rb +182 -180
  71. data/lib/mihari/models/autonomous_system.rb +22 -20
  72. data/lib/mihari/models/cpe.rb +21 -19
  73. data/lib/mihari/models/dns.rb +24 -22
  74. data/lib/mihari/models/geolocation.rb +22 -20
  75. data/lib/mihari/models/port.rb +21 -19
  76. data/lib/mihari/models/reverse_dns.rb +21 -19
  77. data/lib/mihari/models/rule.rb +67 -65
  78. data/lib/mihari/models/tag.rb +5 -3
  79. data/lib/mihari/models/tagging.rb +5 -3
  80. data/lib/mihari/models/whois.rb +18 -16
  81. data/lib/mihari/rule.rb +352 -0
  82. data/lib/mihari/schemas/analyzer.rb +94 -87
  83. data/lib/mihari/schemas/emitter.rb +9 -5
  84. data/lib/mihari/schemas/enricher.rb +8 -4
  85. data/lib/mihari/schemas/mixins.rb +15 -0
  86. data/lib/mihari/schemas/rule.rb +3 -10
  87. data/lib/mihari/services/alert_builder.rb +1 -1
  88. data/lib/mihari/services/alert_proxy.rb +10 -6
  89. data/lib/mihari/services/alert_runner.rb +4 -4
  90. data/lib/mihari/services/rule_builder.rb +3 -3
  91. data/lib/mihari/services/rule_runner.rb +5 -5
  92. data/lib/mihari/structs/binaryedge.rb +1 -1
  93. data/lib/mihari/structs/censys.rb +6 -6
  94. data/lib/mihari/structs/config.rb +1 -1
  95. data/lib/mihari/structs/greynoise.rb +5 -5
  96. data/lib/mihari/structs/hunterhow.rb +3 -3
  97. data/lib/mihari/structs/onyphe.rb +5 -5
  98. data/lib/mihari/structs/shodan.rb +6 -6
  99. data/lib/mihari/structs/urlscan.rb +3 -3
  100. data/lib/mihari/structs/virustotal_intelligence.rb +3 -3
  101. data/lib/mihari/version.rb +1 -1
  102. data/lib/mihari/web/endpoints/alerts.rb +4 -4
  103. data/lib/mihari/web/endpoints/artifacts.rb +6 -6
  104. data/lib/mihari/web/endpoints/rules.rb +10 -17
  105. data/lib/mihari/web/endpoints/tags.rb +2 -2
  106. data/lib/mihari/web/public/assets/{index-9cc489e6.js → index-28d4c79d.js} +48 -48
  107. data/lib/mihari/web/public/index.html +1 -1
  108. data/lib/mihari.rb +6 -8
  109. data/mihari.gemspec +1 -2
  110. data/mkdocs.yml +0 -3
  111. data/requirements.txt +1 -1
  112. metadata +8 -22
  113. data/lib/mihari/analyzers/rule.rb +0 -232
  114. data/lib/mihari/services/rule_proxy.rb +0 -182
@@ -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
@@ -2,102 +2,109 @@
2
2
 
3
3
  module Mihari
4
4
  module Schemas
5
- AnalyzerAPIKeyPagination = Dry::Schema.Params do
6
- required(:analyzer).value(
7
- Types::String.enum(
8
- "binaryedge",
9
- "greynoise",
10
- "onyphe",
11
- "shodan",
12
- "urlscan",
13
- "virustotal_intelligence",
14
- "vt_intel"
15
- )
16
- )
17
- required(:query).value(:string)
18
- optional(:api_key).value(:string)
19
- optional(:options).hash(AnalyzerPaginationOptions)
20
- end
5
+ module Analyzers
6
+ extend Schemas::Mixins
21
7
 
22
- AnalyzerAPIKey = Dry::Schema.Params do
23
- required(:analyzer).value(
24
- Types::String.enum(
25
- "otx",
26
- "pulsedive",
27
- "securitytrails",
28
- "st",
29
- "virustotal",
30
- "vt"
31
- )
32
- )
33
- required(:query).value(:string)
34
- optional(:api_key).value(:string)
35
- optional(:options).hash(AnalyzerOptions)
36
- end
8
+ # Analyzer with API key and pagination
9
+ [
10
+ Mihari::Analyzers::BinaryEdge.class_keys,
11
+ Mihari::Analyzers::GreyNoise.class_keys,
12
+ Mihari::Analyzers::Onyphe.class_keys,
13
+ Mihari::Analyzers::Shodan.class_keys,
14
+ Mihari::Analyzers::Urlscan.class_keys,
15
+ Mihari::Analyzers::VirusTotalIntelligence.class_keys
16
+ ].each do |keys|
17
+ key = keys.first
18
+ const_set(key.upcase, Dry::Schema.Params do
19
+ required(:analyzer).value(Types::String.enum(*keys))
20
+ required(:query).value(:string)
21
+ optional(:api_key).value(:string)
22
+ optional(:options).hash(AnalyzerPaginationOptions)
23
+ end)
24
+ end
37
25
 
38
- DNSTwister = Dry::Schema.Params do
39
- required(:analyzer).value(Types::String.enum("dnstwister"))
40
- required(:query).value(:string)
41
- optional(:options).hash(AnalyzerOptions)
42
- end
26
+ # Analyzer with API key
27
+ [
28
+ Mihari::Analyzers::OTX.class_keys,
29
+ Mihari::Analyzers::Pulsedive.class_keys,
30
+ Mihari::Analyzers::VirusTotal.class_keys,
31
+ Mihari::Analyzers::SecurityTrails.class_keys
32
+ ].each do |keys|
33
+ key = keys.first
34
+ const_set(key.upcase, Dry::Schema.Params do
35
+ required(:analyzer).value(Types::String.enum(*keys))
36
+ required(:query).value(:string)
37
+ optional(:api_key).value(:string)
38
+ optional(:options).hash(AnalyzerOptions)
39
+ end)
40
+ end
43
41
 
44
- Censys = Dry::Schema.Params do
45
- required(:analyzer).value(Types::String.enum("censys"))
46
- required(:query).value(:string)
47
- optional(:id).value(:string)
48
- optional(:secret).value(:string)
49
- optional(:options).hash(AnalyzerPaginationOptions)
50
- end
42
+ DNSTwister = Dry::Schema.Params do
43
+ required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::DNSTwister.class_keys))
44
+ required(:query).value(:string)
45
+ optional(:options).hash(AnalyzerOptions)
46
+ end
51
47
 
52
- CIRCL = Dry::Schema.Params do
53
- required(:analyzer).value(Types::String.enum("circl"))
54
- required(:query).value(:string)
55
- optional(:username).value(:string)
56
- optional(:password).value(:string)
57
- optional(:options).hash(AnalyzerOptions)
58
- end
48
+ Censys = Dry::Schema.Params do
49
+ required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::Censys.class_keys))
50
+ required(:query).value(:string)
51
+ optional(:id).value(:string)
52
+ optional(:secret).value(:string)
53
+ optional(:options).hash(AnalyzerPaginationOptions)
54
+ end
59
55
 
60
- PassiveTotal = Dry::Schema.Params do
61
- required(:analyzer).value(Types::String.enum("passivetotal", "pt"))
62
- required(:query).value(:string)
63
- optional(:username).value(:string)
64
- optional(:api_key).value(:string)
65
- optional(:options).hash(AnalyzerOptions)
66
- end
56
+ CIRCL = Dry::Schema.Params do
57
+ required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::CIRCL.class_keys))
58
+ required(:query).value(:string)
59
+ optional(:username).value(:string)
60
+ optional(:password).value(:string)
61
+ optional(:options).hash(AnalyzerOptions)
62
+ end
67
63
 
68
- ZoomEye = Dry::Schema.Params do
69
- required(:analyzer).value(Types::String.enum("zoomeye"))
70
- required(:query).value(:string)
71
- required(:type).value(Types::String.enum("host", "web"))
72
- optional(:options).hash(AnalyzerPaginationOptions)
73
- end
64
+ PassiveTotal = Dry::Schema.Params do
65
+ required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::PassiveTotal.class_keys))
66
+ required(:query).value(:string)
67
+ optional(:username).value(:string)
68
+ optional(:api_key).value(:string)
69
+ optional(:options).hash(AnalyzerOptions)
70
+ end
74
71
 
75
- Crtsh = Dry::Schema.Params do
76
- required(:analyzer).value(Types::String.enum("crtsh"))
77
- required(:query).value(:string)
78
- optional(:exclude_expired).value(:bool).default(true)
79
- optional(:options).hash(AnalyzerOptions)
80
- end
72
+ ZoomEye = Dry::Schema.Params do
73
+ required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::ZoomEye.class_keys))
74
+ required(:query).value(:string)
75
+ required(:type).value(Types::String.enum("host", "web"))
76
+ optional(:options).hash(AnalyzerPaginationOptions)
77
+ end
81
78
 
82
- HunterHow = Dry::Schema.Params do
83
- required(:analyzer).value(Types::String.enum("hunterhow"))
84
- required(:query).value(:string)
85
- required(:start_time).value(:date)
86
- required(:end_time).value(:date)
87
- optional(:api_key).value(:string)
88
- optional(:options).hash(AnalyzerPaginationOptions)
89
- end
79
+ Crtsh = Dry::Schema.Params do
80
+ required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::Crtsh.class_keys))
81
+ required(:query).value(:string)
82
+ optional(:exclude_expired).value(:bool).default(true)
83
+ optional(:options).hash(AnalyzerOptions)
84
+ end
90
85
 
91
- Feed = Dry::Schema.Params do
92
- required(:analyzer).value(Types::String.enum("feed"))
93
- required(:query).value(:string)
94
- required(:selector).value(:string)
95
- optional(:method).value(Types::HTTPRequestMethods).default("GET")
96
- optional(:headers).value(:hash).default({})
97
- optional(:params).value(:hash)
98
- optional(:data).value(:hash)
99
- optional(:json).value(:hash)
100
- optional(:options).hash(AnalyzerOptions)
86
+ HunterHow = Dry::Schema.Params do
87
+ required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::HunterHow.class_keys))
88
+ required(:query).value(:string)
89
+ required(:start_time).value(:date)
90
+ required(:end_time).value(:date)
91
+ optional(:api_key).value(:string)
92
+ optional(:options).hash(AnalyzerPaginationOptions)
93
+ end
94
+
95
+ Feed = Dry::Schema.Params do
96
+ required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::Feed.class_keys))
97
+ required(:query).value(:string)
98
+ required(:selector).value(:string)
99
+ optional(:method).value(Types::HTTPRequestMethods).default("GET")
100
+ optional(:headers).value(:hash).default({})
101
+ optional(:params).value(:hash)
102
+ optional(:data).value(:hash)
103
+ optional(:json).value(:hash)
104
+ optional(:options).hash(AnalyzerOptions)
105
+ end
101
106
  end
107
+
108
+ Analyzer = Schemas::Analyzers.get_or_composition
102
109
  end
103
110
  end
@@ -3,20 +3,22 @@
3
3
  module Mihari
4
4
  module Schemas
5
5
  module Emitters
6
+ extend Schemas::Mixins
7
+
6
8
  Database = Dry::Schema.Params do
7
- required(:emitter).value(Types::String.enum("database"))
9
+ required(:emitter).value(Types::String.enum(*Mihari::Emitters::Database.class_keys))
8
10
  optional(:options).hash(Options)
9
11
  end
10
12
 
11
13
  MISP = Dry::Schema.Params do
12
- required(:emitter).value(Types::String.enum("misp"))
14
+ required(:emitter).value(Types::String.enum(*Mihari::Emitters::MISP.class_keys))
13
15
  optional(:url).value(:string)
14
16
  optional(:api_key).value(:string)
15
17
  optional(:options).hash(Options)
16
18
  end
17
19
 
18
20
  TheHive = Dry::Schema.Params do
19
- required(:emitter).value(Types::String.enum("thehive"))
21
+ required(:emitter).value(Types::String.enum(*Mihari::Emitters::TheHive.class_keys))
20
22
  optional(:url).value(:string)
21
23
  optional(:api_key).value(:string)
22
24
  optional(:api_version).value(Types::String.enum("v4", "v5")).default("v4")
@@ -24,14 +26,14 @@ module Mihari
24
26
  end
25
27
 
26
28
  Slack = Dry::Schema.Params do
27
- required(:emitter).value(Types::String.enum("slack"))
29
+ required(:emitter).value(Types::String.enum(*Mihari::Emitters::Slack.class_keys))
28
30
  optional(:webhook_url).value(:string)
29
31
  optional(:channel).value(:string)
30
32
  optional(:options).hash(Options)
31
33
  end
32
34
 
33
35
  Webhook = Dry::Schema.Params do
34
- required(:emitter).value(Types::String.enum("webhook"))
36
+ required(:emitter).value(Types::String.enum(*Mihari::Emitters::Webhook.class_keys))
35
37
  required(:url).value(:string)
36
38
  optional(:method).value(Types::HTTPRequestMethods).default("POST")
37
39
  optional(:headers).value(:hash).default({})
@@ -39,5 +41,7 @@ module Mihari
39
41
  optional(:options).hash(Options)
40
42
  end
41
43
  end
44
+
45
+ Emitter = Schemas::Emitters.get_or_composition
42
46
  end
43
47
  end
@@ -3,26 +3,30 @@
3
3
  module Mihari
4
4
  module Schemas
5
5
  module Enrichers
6
+ extend Schemas::Mixins
7
+
6
8
  IPInfo = Dry::Schema.Params do
7
- required(:enricher).value(Types::String.enum("ipinfo"))
9
+ required(:enricher).value(Types::String.enum(*Mihari::Enrichers::IPInfo.class_keys))
8
10
  optional(:api_key).value(:string)
9
11
  optional(:options).hash(Options)
10
12
  end
11
13
 
12
14
  Whois = Dry::Schema.Params do
13
- required(:enricher).value(Types::String.enum("whois"))
15
+ required(:enricher).value(Types::String.enum(*Mihari::Enrichers::Whois.class_keys))
14
16
  optional(:options).hash(Options)
15
17
  end
16
18
 
17
19
  Shodan = Dry::Schema.Params do
18
- required(:enricher).value(Types::String.enum("shodan"))
20
+ required(:enricher).value(Types::String.enum(*Mihari::Enrichers::Shodan.class_keys))
19
21
  optional(:options).hash(Options)
20
22
  end
21
23
 
22
24
  GooglePublicDNS = Dry::Schema.Params do
23
- required(:enricher).value(Types::String.enum("google_public_dns"))
25
+ required(:enricher).value(Types::String.enum(*Mihari::Enrichers::GooglePublicDNS.class_keys))
24
26
  optional(:options).hash(Options)
25
27
  end
26
28
  end
29
+
30
+ Enricher = Schemas::Enrichers.get_or_composition
27
31
  end
28
32
  end
@@ -0,0 +1,15 @@
1
+ module Mihari
2
+ module Schemas
3
+ module Mixins
4
+ def get_or_composition
5
+ schemas = constants.map { |sym| const_get sym }
6
+ return schemas.first if schemas.length <= 1
7
+
8
+ base, *others = schemas
9
+ others.each { |other| base = base.or(other) }
10
+
11
+ base
12
+ end
13
+ end
14
+ end
15
+ end
@@ -21,17 +21,10 @@ module Mihari
21
21
  optional(:created_on).value(:date)
22
22
  optional(:updated_on).value(:date)
23
23
 
24
- required(:queries).value(:array).each do
25
- AnalyzerAPIKey | AnalyzerAPIKeyPagination | Censys | CIRCL | PassiveTotal | ZoomEye | Crtsh | Feed | HunterHow | DNSTwister
26
- end
27
-
28
- optional(:emitters).value(:array).each do
29
- Emitters::Database | Emitters::MISP | Emitters::TheHive | Emitters::Slack | Emitters::Webhook
30
- end.default(DEFAULT_EMITTERS)
24
+ required(:queries).value(:array).each { Analyzer } # rubocop:disable Lint/Void
31
25
 
32
- optional(:enrichers).value(:array).each do
33
- Enrichers::Whois | Enrichers::IPInfo | Enrichers::Shodan | Enrichers::GooglePublicDNS
34
- end.default(DEFAULT_ENRICHERS)
26
+ optional(:emitters).value(:array).each { Emitter }.default(DEFAULT_EMITTERS) # rubocop:disable Lint/Void
27
+ optional(:enrichers).value(:array).each { Enricher }.default(DEFAULT_ENRICHERS) # rubocop:disable Lint/Void
35
28
 
36
29
  optional(:data_types).value(array[Types::DataTypes]).default(Mihari::Types::DataTypes.values)
37
30
  optional(:falsepositives).value(array[:string]).default([])
@@ -36,7 +36,7 @@ module Mihari
36
36
  end
37
37
 
38
38
  def result
39
- Try[StandardError] { AlertProxy.new(data) }.to_result
39
+ Try[StandardError] { AlertProxy.new(**data) }.to_result
40
40
  end
41
41
  end
42
42
  end