mihari 6.1.0 → 6.3.0

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