mihari 6.1.0 → 6.3.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mihari/actor.rb +3 -5
  3. data/lib/mihari/analyzers/base.rb +7 -3
  4. data/lib/mihari/analyzers/circl.rb +1 -1
  5. data/lib/mihari/analyzers/dnstwister.rb +1 -1
  6. data/lib/mihari/analyzers/otx.rb +1 -1
  7. data/lib/mihari/analyzers/passivetotal.rb +1 -1
  8. data/lib/mihari/analyzers/pulsedive.rb +1 -1
  9. data/lib/mihari/analyzers/securitytrails.rb +1 -1
  10. data/lib/mihari/analyzers/virustotal.rb +1 -1
  11. data/lib/mihari/clients/google_public_dns.rb +31 -0
  12. data/lib/mihari/config.rb +5 -1
  13. data/lib/mihari/{type_checker.rb → data_type.rb} +32 -37
  14. data/lib/mihari/database.rb +1 -3
  15. data/lib/mihari/enrichers/google_public_dns.rb +4 -21
  16. data/lib/mihari/entities/artifact.rb +8 -0
  17. data/lib/mihari/models/alert.rb +4 -27
  18. data/lib/mihari/models/artifact.rb +65 -3
  19. data/lib/mihari/models/dns.rb +3 -8
  20. data/lib/mihari/models/rule.rb +2 -5
  21. data/lib/mihari/rule.rb +20 -7
  22. data/lib/mihari/schemas/options.rb +5 -1
  23. data/lib/mihari/structs/filters.rb +53 -9
  24. data/lib/mihari/structs/google_public_dns.rb +4 -8
  25. data/lib/mihari/version.rb +1 -1
  26. data/lib/mihari/web/endpoints/alerts.rb +2 -10
  27. data/lib/mihari/web/endpoints/artifacts.rb +64 -0
  28. data/lib/mihari/web/endpoints/exports.rb +0 -0
  29. data/lib/mihari/web/endpoints/rules.rb +1 -8
  30. data/lib/mihari/web/public/assets/index-81613_nX.js +1763 -0
  31. data/lib/mihari/web/public/assets/index-Wv6xUrTI.css +1 -0
  32. data/lib/mihari/web/public/index.html +2 -3
  33. data/lib/mihari/web/public/redoc-static.html +20 -16
  34. data/lib/mihari.rb +2 -1
  35. data/mihari.gemspec +8 -7
  36. data/requirements.txt +1 -1
  37. metadata +30 -56
  38. data/lib/mihari/web/public/assets/index-216d49d1.js +0 -1750
  39. data/lib/mihari/web/public/assets/index-4c8509ee.css +0 -1
  40. /data/lib/mihari/web/public/assets/{mode-yaml-24faa242.js → mode-yaml-BC4MIiYj.js} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dc2d39ad14f53cf88ed2c68bb2e2668e9a43f36444099401ef9fbe1347948644
4
- data.tar.gz: 2d08fed6ec6f82da36f72a2fb05ae1ede82ba65d6bc9bcdb7c60b8f7e244c71a
3
+ metadata.gz: f027c5c24759f4e09d5dad277df4cad58a1705478eba1f3b9b6b354a896a29a5
4
+ data.tar.gz: a908c432c313bab4f4af70b0b32fb1f2e65c9f40926dc3099c4869330080005c
5
5
  SHA512:
6
- metadata.gz: 684c4106554f9c11bc7ff7f8633b32c5fa24b4127001415e6d14bd4de73e51a04084eb802a29cac109d8b56c71bc7068c8657f4d6ec7cd53e0519ef353aa859f
7
- data.tar.gz: 4a4c3183ca85d47b54f7a98837059a9fae585c2d079fe005336520c62290bc317aa61880695f923a2ad0ad7ba5cf18e3e888990a9394d82b4ac41939f34b3403
6
+ metadata.gz: 8110df87f854b6e06ea8477c94c6563b2450dae4585738f256da9c94203d14c6fb3ce11e775a470dae04d4d3934549ae71068e6b3f3d3eca48ea3689d87a4cd4
7
+ data.tar.gz: 375855d7dba59d13ff894472f5f6d5ccc82f36de73aeb12c4144c05863c64283f2ff451dd4531643ba3ddbb218181937d57da1a42bb28970f78170e3fae5dfbf
data/lib/mihari/actor.rb CHANGED
@@ -65,11 +65,9 @@ module Mihari
65
65
 
66
66
  def result(...)
67
67
  Try[StandardError] do
68
- retry_on_error(
69
- times: retry_times,
70
- interval: retry_interval,
71
- exponential_backoff: retry_exponential_backoff
72
- ) { call(...) }
68
+ retry_on_error(times: retry_times, interval: retry_interval, exponential_backoff: retry_exponential_backoff) do
69
+ call(...)
70
+ end
73
71
  end.to_result
74
72
  end
75
73
 
@@ -37,10 +37,14 @@ module Mihari
37
37
  # @return [Boolean]
38
38
  #
39
39
  def ignore_error?
40
- ignore_error = options[:ignore_error]
41
- return ignore_error unless ignore_error.nil?
40
+ options[:ignore_error] || Mihari.config.ignore_error
41
+ end
42
42
 
43
- Mihari.config.ignore_error
43
+ #
44
+ # @return [Boolean]
45
+ #
46
+ def parallel?
47
+ options[:parallel] || Mihari.config.parallel
44
48
  end
45
49
 
46
50
  # @return [Array<String>, Array<Mihari::Models::Artifact>]
@@ -26,7 +26,7 @@ module Mihari
26
26
  def initialize(query, options: nil, username: nil, password: nil)
27
27
  super(refang(query), options: options)
28
28
 
29
- @type = TypeChecker.type(query)
29
+ @type = DataType.type(query)
30
30
 
31
31
  @username = username || Mihari.config.circl_passive_username
32
32
  @password = password || Mihari.config.circl_passive_password
@@ -18,7 +18,7 @@ module Mihari
18
18
  def initialize(query, options: nil)
19
19
  super(refang(query), options: options)
20
20
 
21
- @type = TypeChecker.type(query)
21
+ @type = DataType.type(query)
22
22
  end
23
23
 
24
24
  def artifacts
@@ -22,7 +22,7 @@ module Mihari
22
22
  def initialize(query, options: nil, api_key: nil)
23
23
  super(refang(query), options: options)
24
24
 
25
- @type = TypeChecker.type(query)
25
+ @type = DataType.type(query)
26
26
 
27
27
  @api_key = api_key || Mihari.config.otx_api_key
28
28
  end
@@ -26,7 +26,7 @@ module Mihari
26
26
  def initialize(query, options: nil, api_key: nil, username: nil)
27
27
  super(refang(query), options: options)
28
28
 
29
- @type = TypeChecker.type(query)
29
+ @type = DataType.type(query)
30
30
 
31
31
  @username = username || Mihari.config.passivetotal_username
32
32
  @api_key = api_key || Mihari.config.passivetotal_api_key
@@ -22,7 +22,7 @@ module Mihari
22
22
  def initialize(query, options: nil, api_key: nil)
23
23
  super(refang(query), options: options)
24
24
 
25
- @type = TypeChecker.type(query)
25
+ @type = DataType.type(query)
26
26
 
27
27
  @api_key = api_key || Mihari.config.pulsedive_api_key
28
28
  end
@@ -25,7 +25,7 @@ module Mihari
25
25
  def initialize(query, options: nil, api_key: nil)
26
26
  super(refang(query), options: options)
27
27
 
28
- @type = TypeChecker.type(query)
28
+ @type = DataType.type(query)
29
29
 
30
30
  @api_key = api_key || Mihari.config.securitytrails_api_key
31
31
  end
@@ -22,7 +22,7 @@ module Mihari
22
22
  def initialize(query, options: nil, api_key: nil)
23
23
  super(refang(query), options: options)
24
24
 
25
- @type = TypeChecker.type(query)
25
+ @type = DataType.type(query)
26
26
 
27
27
  @api_key = api_key || Mihari.config.virustotal_api_key
28
28
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Clients
5
+ #
6
+ # Google Public DNS enricher
7
+ #
8
+ class GooglePublicDNS < Base
9
+ #
10
+ # @param [String] base_url
11
+ # @param [Hash] headers
12
+ # @param [Integer, nil] timeout
13
+ #
14
+ def initialize(base_url = "https://dns.google", headers: {}, timeout: nil)
15
+ super(base_url, headers: headers, timeout: timeout)
16
+ end
17
+
18
+ #
19
+ # Query Google Public DNS by resource type
20
+ #
21
+ # @param [String] name
22
+ #
23
+ # @return [Mihari::Structs::GooglePublicDNS::Response, nil]
24
+ #
25
+ def query_all(name)
26
+ Structs::GooglePublicDNS::Response.from_dynamic! get_json("/resolve",
27
+ params: { name: name, type: "ALL" })
28
+ end
29
+ end
30
+ end
31
+ end
data/lib/mihari/config.rb CHANGED
@@ -42,6 +42,7 @@ module Mihari
42
42
  ignore_error: false,
43
43
  pagination_interval: 0,
44
44
  pagination_limit: 100,
45
+ parallel: false,
45
46
  retry_exponential_backoff: true,
46
47
  retry_interval: 5,
47
48
  retry_times: 3,
@@ -62,7 +63,7 @@ module Mihari
62
63
  # @return [String, nil]
63
64
 
64
65
  # @!attribute [r] database_url
65
- # @return [String, nil]
66
+ # @return [URI, nil]
66
67
 
67
68
  # @!attribute [r] fofa_api_key
68
69
  # @return [String, nil]
@@ -151,6 +152,9 @@ module Mihari
151
152
  # @!attribute [r] pagination_limit
152
153
  # @return [Integer]
153
154
 
155
+ # @!attribute [r] parallel
156
+ # @return [Boolean]
157
+
154
158
  # @!attribute [r] ignore_error
155
159
  # @return [Boolean]
156
160
 
@@ -2,9 +2,11 @@
2
2
 
3
3
  module Mihari
4
4
  #
5
- # Artifact type checker
5
+ # (Artifact) Data Type
6
6
  #
7
- class TypeChecker
7
+ class DataType
8
+ include Dry::Monads[:try]
9
+
8
10
  # @return [String]
9
11
  attr_reader :data
10
12
 
@@ -24,26 +26,25 @@ module Mihari
24
26
 
25
27
  # @return [Boolean]
26
28
  def ip?
27
- IPAddr.new data
28
- true
29
- rescue IPAddr::InvalidAddressError => _e
30
- false
29
+ Try[IPAddr::InvalidAddressError] do
30
+ IPAddr.new(data).to_s == data
31
+ end.to_result.value_or(false)
31
32
  end
32
33
 
33
34
  # @return [Boolean]
34
35
  def domain?
35
- uri = Addressable::URI.parse("http://#{data}")
36
- uri.host == data && PublicSuffix.valid?(uri.host)
37
- rescue Addressable::URI::InvalidURIError => _e
38
- false
36
+ Try[Addressable::URI::InvalidURIError] do
37
+ uri = Addressable::URI.parse("http://#{data}")
38
+ uri.host == data && PublicSuffix.valid?(uri.host)
39
+ end.to_result.value_or(false)
39
40
  end
40
41
 
41
42
  # @return [Boolean]
42
43
  def url?
43
- uri = Addressable::URI.parse(data)
44
- uri.scheme && uri.host && uri.path && PublicSuffix.valid?(uri.host)
45
- rescue Addressable::URI::InvalidURIError => _e
46
- false
44
+ Try[Addressable::URI::InvalidURIError] do
45
+ uri = Addressable::URI.parse(data)
46
+ uri.scheme && uri.host && uri.path && PublicSuffix.valid?(uri.host)
47
+ end.to_result.value_or(false)
47
48
  end
48
49
 
49
50
  # @return [Boolean]
@@ -53,38 +54,20 @@ module Mihari
53
54
 
54
55
  # @return [String, nil]
55
56
  def type
56
- return "hash" if hash?
57
- return "ip" if ip?
58
- return "domain" if domain?
59
- return "url" if url?
57
+ found = %i[hash? ip? domain? url? mail?].find { |method| send(method) if respond_to?(method) }
58
+ return nil if found.nil?
60
59
 
61
- "mail" if mail?
60
+ found[...-1].to_s
62
61
  end
63
62
 
64
63
  # @return [String, nil]
65
64
  def detailed_type
66
- return "md5" if md5?
67
- return "sha1" if sha1?
68
- return "sha256" if sha256?
69
- return "sha512" if sha512?
65
+ found = %i[md5? sha1? sha256? sha512?].find { |method| send(method) if respond_to?(method) }
66
+ return found[...-1].to_s unless found.nil?
70
67
 
71
68
  type
72
69
  end
73
70
 
74
- class << self
75
- # @return [String, nil]
76
- def type(data)
77
- new(data).type
78
- end
79
-
80
- # @return [String, nil]
81
- def detailed_type(data)
82
- new(data).detailed_type
83
- end
84
- end
85
-
86
- private
87
-
88
71
  # @return [Boolean]
89
72
  def md5?
90
73
  data.match?(/^[A-Fa-f0-9]{32}$/)
@@ -104,5 +87,17 @@ module Mihari
104
87
  def sha512?
105
88
  data.match?(/^[A-Fa-f0-9]{128}$/)
106
89
  end
90
+
91
+ class << self
92
+ # @return [String, nil]
93
+ def type(data)
94
+ new(data).type
95
+ end
96
+
97
+ # @return [String, nil]
98
+ def detailed_type(data)
99
+ new(data).detailed_type
100
+ end
101
+ end
107
102
  end
108
103
  end
@@ -154,7 +154,7 @@ module Mihari
154
154
 
155
155
  case adapter
156
156
  when "postgresql", "mysql2"
157
- ActiveRecord::Base.establish_connection(Mihari.config.database_url.to_s)
157
+ ActiveRecord::Base.establish_connection Mihari.config.database_url.to_s
158
158
  else
159
159
  ActiveRecord::Base.establish_connection(
160
160
  adapter: adapter,
@@ -162,8 +162,6 @@ module Mihari
162
162
  )
163
163
  end
164
164
  ActiveRecord::Base.logger = Logger.new($stdout) if development_env?
165
- rescue StandardError => e
166
- Mihari.logger.error e
167
165
  end
168
166
 
169
167
  #
@@ -11,27 +11,10 @@ module Mihari
11
11
  #
12
12
  # @param [String] name
13
13
  #
14
- # @return [Array<Mihari::Structs::GooglePublicDNS::Response>]
14
+ # @return [Mihari::Structs::GooglePublicDNS::Response]
15
15
  #
16
16
  def call(name)
17
- %w[A AAAA CNAME TXT NS].filter_map { |resource_type| query_by_type(name, resource_type) }
18
- end
19
-
20
- #
21
- # Query Google Public DNS by resource type
22
- #
23
- # @param [String] name
24
- # @param [String] resource_type
25
- #
26
- # @return [Mihari::Structs::GooglePublicDNS::Response, nil]
27
- #
28
- def query_by_type(name, resource_type)
29
- url = "https://dns.google/resolve"
30
- params = { name: name, type: resource_type }
31
- res = http.get(url, params: params)
32
- Structs::GooglePublicDNS::Response.from_dynamic! JSON.parse(res.body.to_s)
33
- rescue HTTPError
34
- nil
17
+ client.query_all name
35
18
  end
36
19
 
37
20
  class << self
@@ -45,8 +28,8 @@ module Mihari
45
28
 
46
29
  private
47
30
 
48
- def http
49
- HTTP::Factory.build timeout: timeout
31
+ def client
32
+ Clients::GooglePublicDNS.new
50
33
  end
51
34
  end
52
35
  end
@@ -42,5 +42,13 @@ module Mihari
42
42
  status.ports.empty? ? nil : status.ports
43
43
  end
44
44
  end
45
+
46
+ class ArtifactsWithPagination < Grape::Entity
47
+ expose :artifacts, using: Entities::Artifact,
48
+ documentation: { type: Entities::Artifact, is_array: true, required: true }
49
+ expose :total, documentation: { type: Integer, required: true }
50
+ expose :current_page, documentation: { type: Integer, required: true }, as: :currentPage
51
+ expose :page_size, documentation: { type: Integer, required: true }, as: :pageSize
52
+ end
45
53
  end
46
54
  end
@@ -30,8 +30,7 @@ module Mihari
30
30
  offset = (page - 1) * limit
31
31
 
32
32
  relation = build_relation(filter.without_pagination)
33
- alert_ids = relation.limit(limit).offset(offset).order(id: :desc).pluck(:id).uniq
34
- eager_load(:artifacts, :tags).where(id: [alert_ids]).order(id: :desc)
33
+ relation.limit(limit).offset(offset).order(id: :desc)
35
34
  end
36
35
 
37
36
  #
@@ -48,39 +47,17 @@ module Mihari
48
47
 
49
48
  private
50
49
 
51
- #
52
- # @param [Mihari::Structs::Filters::Alert::SearchFilter] filter
53
- #
54
- # @return [Array<Integer>]
55
- #
56
- def get_artifact_ids_by_filter(filter)
57
- artifact_ids = []
58
-
59
- if filter.artifact_data
60
- artifact = Artifact.where(data: filter.artifact_data)
61
- artifact_ids = artifact.pluck(:id)
62
- # set invalid ID if nothing is matched with the filters
63
- artifact_ids = [-1] if artifact_ids.empty?
64
- end
65
-
66
- artifact_ids
67
- end
68
-
69
50
  #
70
51
  # @param [Mihari::Structs::Filters::Alert::SearchFilter] filter
71
52
  #
72
53
  # @return [Mihari::Models::Alert]
73
54
  #
74
55
  def build_relation(filter)
75
- artifact_ids = get_artifact_ids_by_filter(filter)
76
-
77
- relation = includes(:artifacts, :tags)
78
-
79
- relation = relation.where(artifacts: { id: artifact_ids }) unless artifact_ids.empty?
80
- relation = relation.where(tags: { name: filter.tag_name }) if filter.tag_name
56
+ relation = eager_load(:artifacts, :tags)
81
57
 
58
+ relation = relation.where(artifacts: { data: filter.artifact }) if filter.artifact
59
+ relation = relation.where(tags: { name: filter.tag }) if filter.tag
82
60
  relation = relation.where(rule_id: filter.rule_id) if filter.rule_id
83
-
84
61
  relation = relation.where("alerts.created_at >= ?", filter.from_at) if filter.from_at
85
62
  relation = relation.where("alerts.created_at <= ?", filter.to_at) if filter.to_at
86
63
 
@@ -46,7 +46,7 @@ module Mihari
46
46
 
47
47
  super(*args, **kwargs)
48
48
 
49
- self.data_type = TypeChecker.type(data)
49
+ self.data_type = DataType.type(data)
50
50
 
51
51
  @tags = []
52
52
  @rule_id = ""
@@ -77,6 +77,18 @@ module Mihari
77
77
  artifact.created_at < decayed_at
78
78
  end
79
79
 
80
+ #
81
+ # Count artifacts
82
+ #
83
+ # @param [Mihari::Structs::Filters::Artifact::SearchFilter] filter
84
+ #
85
+ # @return [Integer]
86
+ #
87
+ def count(filter)
88
+ relation = build_relation(filter)
89
+ relation.distinct("artifact.id").count
90
+ end
91
+
80
92
  #
81
93
  # Enrich whois record
82
94
  #
@@ -105,7 +117,7 @@ module Mihari
105
117
  # @param [Mihari::Enrichers::Shodan] enricher
106
118
  #
107
119
  def enrich_reverse_dns(enricher = Enrichers::Shodan.new)
108
- return unless can_enrich_revese_dns?
120
+ return unless can_enrich_reverse_dns?
109
121
 
110
122
  self.reverse_dns_names = ReverseDnsName.build_by_ip(data, enricher: enricher)
111
123
  end
@@ -195,6 +207,56 @@ module Mihari
195
207
  methods.each { |method| send(method, enricher) if respond_to?(method) }
196
208
  end
197
209
 
210
+ class << self
211
+ #
212
+ # Search artifacts
213
+ #
214
+ # @param [Mihari::Structs::Filters::Artifact::SearchFilterWithPagination] filter
215
+ #
216
+ # @return [Array<Artifact>]
217
+ #
218
+ def search(filter)
219
+ limit = filter.limit.to_i
220
+ raise ArgumentError, "limit should be bigger than zero" unless limit.positive?
221
+
222
+ page = filter.page.to_i
223
+ raise ArgumentError, "page should be bigger than zero" unless page.positive?
224
+
225
+ offset = (page - 1) * limit
226
+
227
+ relation = build_relation(filter.without_pagination)
228
+ relation.limit(limit).offset(offset).order(id: :desc)
229
+ end
230
+
231
+ #
232
+ # Count artifacts
233
+ #
234
+ # @param [Mihari::Structs::Filters::Artifact::SearchFilter] filter
235
+ #
236
+ # @return [Integer]
237
+ #
238
+ def count(filter)
239
+ relation = build_relation(filter)
240
+ relation.distinct("artifacts.id").count
241
+ end
242
+
243
+ #
244
+ # @param [Mihari::Structs::Filters::Artifact::SearchFilter] filter
245
+ #
246
+ # @return [Mihari::Models::Artifact]
247
+ #
248
+ def build_relation(filter)
249
+ relation = eager_load(alert: :tags)
250
+
251
+ relation = relation.where(alert: { rule_id: filter.rule_id }) if filter.rule_id
252
+ relation = relation.where(alert: { tags: { name: filter.tag } }) if filter.tag
253
+ relation = relation.where("artifacts.created_at >= ?", filter.from_at) if filter.from_at
254
+ relation = relation.where("artifacts.created_at <= ?", filter.to_at) if filter.to_at
255
+
256
+ relation
257
+ end
258
+ end
259
+
198
260
  private
199
261
 
200
262
  def ipinfo
@@ -219,7 +281,7 @@ module Mihari
219
281
  %w[domain url].include?(data_type) && dns_records.empty?
220
282
  end
221
283
 
222
- def can_enrich_revese_dns?
284
+ def can_enrich_reverse_dns?
223
285
  data_type == "ip" && reverse_dns_names.empty?
224
286
  end
225
287
 
@@ -20,16 +20,11 @@ module Mihari
20
20
  # @return [Array<Mihari::Models::DnsRecord>]
21
21
  #
22
22
  def build_by_domain(domain, enricher: Enrichers::GooglePublicDNS.new)
23
- result = enricher.result(domain).bind do |responses|
23
+ enricher.result(domain).bind do |res|
24
24
  Success(
25
- responses.map do |res|
26
- res.answers.map do |answer|
27
- new(resource: answer.resource_type, value: answer.data)
28
- end
29
- end.flatten
25
+ res.answers.map { |answer| new(resource: answer.resource_type, value: answer.data) }
30
26
  )
31
- end
32
- result.value_or []
27
+ end.value_or([])
33
28
  end
34
29
  end
35
30
  end
@@ -40,10 +40,7 @@ module Mihari
40
40
  offset = (page - 1) * limit
41
41
 
42
42
  relation = build_relation(filter.without_pagination)
43
-
44
- # TODO: improve queires
45
- rule_ids = relation.limit(limit).offset(offset).order(created_at: :desc).pluck(:id).uniq
46
- where(id: [rule_ids]).order(created_at: :desc)
43
+ relation.limit(limit).offset(offset).order(created_at: :desc)
47
44
  end
48
45
 
49
46
  #
@@ -68,7 +65,7 @@ module Mihari
68
65
  def build_relation(filter)
69
66
  relation = includes(alerts: :tags)
70
67
 
71
- relation = relation.where(alerts: { tags: { name: filter.tag_name } }) if filter.tag_name
68
+ relation = relation.where(alerts: { tags: { name: filter.tag } }) if filter.tag
72
69
 
73
70
  relation = relation.where("rules.title LIKE ?", "%#{filter.title}%") if filter.title
74
71
  relation = relation.where("rules.description LIKE ?", "%#{filter.description}%") if filter.description
data/lib/mihari/rule.rb CHANGED
@@ -110,9 +110,7 @@ module Mihari
110
110
  # @return [Array<Mihari::Models::Artifact>]
111
111
  #
112
112
  def artifacts
113
- analyzers.flat_map do |analyzer|
114
- # @type [Dry::Monads::Result::Success<Array<Mihari::Models::Artifact>>, Dry::Monads::Result::Failure]
115
- result = analyzer.result
113
+ analyzer_results.flat_map do |result|
116
114
  case result
117
115
  when Success
118
116
  artifacts = result.value!
@@ -123,7 +121,7 @@ module Mihari
123
121
  else
124
122
  raise result.failure unless analyzer.ignore_error?
125
123
  end
126
- end.compact
124
+ end
127
125
  end
128
126
 
129
127
  #
@@ -292,15 +290,30 @@ module Mihari
292
290
  # @return [Array<Mihari::Analyzers::Base>]
293
291
  #
294
292
  def analyzers
295
- @analyzers ||= queries.map do |query_params|
296
- analyzer_name = query_params[:analyzer]
293
+ @analyzers ||= queries.map do |params|
294
+ analyzer_name = params[:analyzer]
297
295
  klass = get_analyzer_class(analyzer_name)
298
- analyzer = klass.from_query(query_params)
296
+ analyzer = klass.from_query(params)
299
297
  analyzer.validate_configuration!
300
298
  analyzer
301
299
  end
302
300
  end
303
301
 
302
+ def parallel_analyzers
303
+ analyzers.select(&:parallel?)
304
+ end
305
+
306
+ def serial_analyzers
307
+ analyzers.reject(&:parallel?)
308
+ end
309
+
310
+ # @return [Array<Dry::Monads::Result::Success<Array<Mihari::Models::Artifact>>, Dry::Monads::Result::Failure>]
311
+ def analyzer_results
312
+ parallel_results = Parallel.map(parallel_analyzers) { |analyzer| analyzer.result }
313
+ serial_results = serial_analyzers.map(&:result)
314
+ parallel_results + serial_results
315
+ end
316
+
304
317
  #
305
318
  # Get emitter class
306
319
  #
@@ -15,7 +15,11 @@ module Mihari
15
15
  optional(:ignore_error).value(:bool).default(Mihari.config.ignore_error)
16
16
  end
17
17
 
18
- AnalyzerOptions = Options | IgnoreErrorOptions
18
+ ParallelOptions = Dry::Schema.Params do
19
+ optional(:parallel).value(:bool).default(Mihari.config.parallel)
20
+ end
21
+
22
+ AnalyzerOptions = Options | IgnoreErrorOptions | ParallelOptions
19
23
 
20
24
  PaginationOptions = Dry::Schema.Params do
21
25
  optional(:pagination_interval).value(:integer).default(Mihari.config.pagination_interval)