mihari 7.2.0 → 7.3.1

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.
@@ -6,6 +6,27 @@ module Mihari
6
6
  # Rule model
7
7
  #
8
8
  class Rule < ActiveRecord::Base
9
+ # @!attribute [rw] id
10
+ # @return [String]
11
+
12
+ # @!attribute [rw] title
13
+ # @return [String]
14
+
15
+ # @!attribute [rw] description
16
+ # @return [String]
17
+
18
+ # @!attribute [rw] data
19
+ # @return [Hash]
20
+
21
+ # @!attribute [rw] created_at
22
+ # @return [DateTime]
23
+
24
+ # @!attribute [rw] updated_at
25
+ # @return [DateTime]
26
+
27
+ # @!attribute [r] alerts
28
+ # @return [Array<Mihari::Models::Alert>]
29
+
9
30
  has_many :alerts, dependent: :destroy
10
31
  has_many :taggings, dependent: :destroy
11
32
  has_many :tags, through: :taggings
data/lib/mihari/rule.rb CHANGED
@@ -33,9 +33,9 @@ module Mihari
33
33
  # @return [Boolean]
34
34
  #
35
35
  def errors?
36
- return false if @errors.nil?
36
+ return false if errors.nil?
37
37
 
38
- !@errors.empty?
38
+ !errors.empty?
39
39
  end
40
40
 
41
41
  def [](key)
@@ -163,9 +163,7 @@ module Mihari
163
163
  # @return [Array<Mihari::Models::Artifact>]
164
164
  #
165
165
  def unique_artifacts
166
- normalized_artifacts.select do |artifact|
167
- artifact.unique?(base_time: base_time, artifact_ttl: artifact_ttl)
168
- end
166
+ normalized_artifacts.select { |artifact| artifact.unique?(base_time: base_time, artifact_ttl: artifact_ttl) }
169
167
  end
170
168
 
171
169
  #
@@ -175,8 +173,10 @@ module Mihari
175
173
  #
176
174
  def enriched_artifacts
177
175
  @enriched_artifacts ||= Parallel.map(unique_artifacts) do |artifact|
178
- enrichers.each { |enricher| artifact.enrich_by_enricher enricher }
179
- artifact
176
+ artifact.tap do |tapped|
177
+ # NOTE: To apply changes correctly, enrichers should be applied to an artifact serially
178
+ enrichers.each { |enricher| enricher.result(tapped) }
179
+ end
180
180
  end
181
181
  end
182
182
 
@@ -188,7 +188,10 @@ module Mihari
188
188
  def bulk_emit
189
189
  return [] if enriched_artifacts.empty?
190
190
 
191
- Parallel.map(emitters) { |emitter| emitter.result(enriched_artifacts).value_or nil }.compact
191
+ [].tap do |out|
192
+ out << serial_emitters.map { |emitter| emitter.result(enriched_artifacts).value_or(nil) }
193
+ out << Parallel.map(parallel_emitters) { |emitter| emitter.result(enriched_artifacts).value_or(nil) }
194
+ end.flatten.compact
192
195
  end
193
196
 
194
197
  #
@@ -289,11 +292,11 @@ module Mihari
289
292
  #
290
293
  # @return [Boolean]
291
294
  #
292
- def falsepositive?(value)
293
- return true if falsepositives.include?(value)
295
+ def falsepositive?(artifact)
296
+ return true if falsepositives.include?(artifact)
294
297
 
295
298
  regexps = falsepositives.select { |fp| fp.is_a?(Regexp) }
296
- regexps.any? { |fp| fp.match?(value) }
299
+ regexps.any? { |fp| fp.match?(artifact) }
297
300
  end
298
301
 
299
302
  #
@@ -332,9 +335,10 @@ module Mihari
332
335
 
333
336
  # @return [Array<Dry::Monads::Result::Success<Array<Mihari::Models::Artifact>>, Dry::Monads::Result::Failure>]
334
337
  def analyzer_results
335
- parallel_results = Parallel.map(parallel_analyzers, &:result)
336
- serial_results = serial_analyzers.map(&:result)
337
- parallel_results + serial_results
338
+ [].tap do |out|
339
+ out << Parallel.map(parallel_analyzers, &:result)
340
+ out << serial_analyzers.map(&:result)
341
+ end.flatten
338
342
  end
339
343
 
340
344
  #
@@ -365,6 +369,14 @@ module Mihari
365
369
  end
366
370
  end
367
371
 
372
+ def parallel_emitters
373
+ emitters.select(&:parallel?)
374
+ end
375
+
376
+ def serial_emitters
377
+ emitters.reject(&:parallel?)
378
+ end
379
+
368
380
  #
369
381
  # Get enricher class
370
382
  #
@@ -3,9 +3,9 @@
3
3
  module Mihari
4
4
  module Schemas
5
5
  Alert = Dry::Schema.Params do
6
- required(:rule_id).value(:string)
7
- required(:artifacts).value(array[:string])
8
- optional(:source).value(:string)
6
+ required(:rule_id).filled(:string)
7
+ required(:artifacts).array { filled(:string) }
8
+ optional(:source).filled(:string)
9
9
  end
10
10
 
11
11
  #
@@ -20,8 +20,8 @@ module Mihari
20
20
  key = keys.first
21
21
  const_set(key.upcase, Dry::Schema.Params do
22
22
  required(:analyzer).value(Types::String.enum(*keys))
23
- required(:query).value(:string)
24
- optional(:api_key).value(:string)
23
+ required(:query).filled(:string)
24
+ optional(:api_key).filled(:string)
25
25
  optional(:options).hash(AnalyzerPaginationOptions)
26
26
  end)
27
27
  end
@@ -36,60 +36,60 @@ module Mihari
36
36
  key = keys.first
37
37
  const_set(key.upcase, Dry::Schema.Params do
38
38
  required(:analyzer).value(Types::String.enum(*keys))
39
- required(:query).value(:string)
40
- optional(:api_key).value(:string)
39
+ required(:query).filled(:string)
40
+ optional(:api_key).filled(:string)
41
41
  optional(:options).hash(AnalyzerOptions)
42
42
  end)
43
43
  end
44
44
 
45
45
  DNSTwister = Dry::Schema.Params do
46
46
  required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::DNSTwister.keys))
47
- required(:query).value(:string)
47
+ required(:query).filled(:string)
48
48
  optional(:options).hash(AnalyzerOptions)
49
49
  end
50
50
 
51
51
  Censys = Dry::Schema.Params do
52
52
  required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::Censys.keys))
53
- required(:query).value(:string)
54
- optional(:id).value(:string)
55
- optional(:secret).value(:string)
53
+ required(:query).filled(:string)
54
+ optional(:id).filled(:string)
55
+ optional(:secret).filled(:string)
56
56
  optional(:options).hash(AnalyzerPaginationOptions)
57
57
  end
58
58
 
59
59
  CIRCL = Dry::Schema.Params do
60
60
  required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::CIRCL.keys))
61
- required(:query).value(:string)
62
- optional(:username).value(:string)
63
- optional(:password).value(:string)
61
+ required(:query).filled(:string)
62
+ optional(:username).filled(:string)
63
+ optional(:password).filled(:string)
64
64
  optional(:options).hash(AnalyzerOptions)
65
65
  end
66
66
 
67
67
  Fofa = Dry::Schema.Params do
68
68
  required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::Fofa.keys))
69
- required(:query).value(:string)
70
- optional(:api_key).value(:string)
71
- optional(:email).value(:string)
69
+ required(:query).filled(:string)
70
+ optional(:api_key).filled(:string)
71
+ optional(:email).filled(:string)
72
72
  optional(:options).hash(AnalyzerPaginationOptions)
73
73
  end
74
74
 
75
75
  PassiveTotal = Dry::Schema.Params do
76
76
  required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::PassiveTotal.keys))
77
- required(:query).value(:string)
78
- optional(:username).value(:string)
79
- optional(:api_key).value(:string)
77
+ required(:query).filled(:string)
78
+ optional(:username).filled(:string)
79
+ optional(:api_key).filled(:string)
80
80
  optional(:options).hash(AnalyzerOptions)
81
81
  end
82
82
 
83
83
  ZoomEye = Dry::Schema.Params do
84
84
  required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::ZoomEye.keys))
85
- required(:query).value(:string)
85
+ required(:query).filled(:string)
86
86
  required(:type).value(Types::String.enum("host", "web"))
87
87
  optional(:options).hash(AnalyzerPaginationOptions)
88
88
  end
89
89
 
90
90
  Crtsh = Dry::Schema.Params do
91
91
  required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::Crtsh.keys))
92
- required(:query).value(:string)
92
+ required(:query).filled(:string)
93
93
  optional(:exclude_expired).value(:bool).default(true)
94
94
  optional(:match).value(Types::String.enum("=", "ILIKE", "LIKE", "single", "any", "FTS")).default(nil)
95
95
  optional(:options).hash(AnalyzerOptions)
@@ -97,26 +97,26 @@ module Mihari
97
97
 
98
98
  HunterHow = Dry::Schema.Params do
99
99
  required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::HunterHow.keys))
100
- required(:query).value(:string)
100
+ required(:query).filled(:string)
101
101
  required(:start_time).value(:date)
102
102
  required(:end_time).value(:date)
103
- optional(:api_key).value(:string)
103
+ optional(:api_key).filled(:string)
104
104
  optional(:options).hash(AnalyzerPaginationOptions)
105
105
  end
106
106
 
107
107
  Feed = Dry::Schema.Params do
108
108
  required(:analyzer).value(Types::String.enum(*Mihari::Analyzers::Feed.keys))
109
- required(:query).value(:string)
110
- required(:selector).value(:string)
109
+ required(:query).filled(:string)
110
+ required(:selector).filled(:string)
111
111
  optional(:method).value(Types::HTTPRequestMethods).default("GET")
112
- optional(:headers).value(:hash).default({})
113
- optional(:params).value(:hash)
114
- optional(:form).value(:hash)
115
- optional(:json).value(:hash)
112
+ optional(:headers).filled(:hash)
113
+ optional(:params).filled(:hash)
114
+ optional(:form).filled(:hash)
115
+ optional(:json).filled(:hash)
116
116
  optional(:options).hash(AnalyzerOptions)
117
117
  end
118
118
  end
119
119
 
120
- Analyzer = Schemas::Analyzers.get_or_composition
120
+ Analyzer = Schemas::Analyzers.compose_by_or
121
121
  end
122
122
  end
@@ -9,7 +9,7 @@ module Mihari
9
9
  module Orrable
10
10
  extend ActiveSupport::Concern
11
11
 
12
- def get_or_composition
12
+ def compose_by_or
13
13
  schemas = constants.map { |sym| const_get sym }
14
14
  return schemas.first if schemas.length <= 1
15
15
 
@@ -10,40 +10,40 @@ module Mihari
10
10
 
11
11
  Database = Dry::Schema.Params do
12
12
  required(:emitter).value(Types::String.enum(*Mihari::Emitters::Database.keys))
13
- optional(:options).hash(Options)
13
+ optional(:options).hash(EmitterOptions)
14
14
  end
15
15
 
16
16
  MISP = Dry::Schema.Params do
17
17
  required(:emitter).value(Types::String.enum(*Mihari::Emitters::MISP.keys))
18
- optional(:url).value(:string)
19
- optional(:api_key).value(:string)
20
- optional(:options).hash(Options)
18
+ optional(:url).filled(:string)
19
+ optional(:api_key).filled(:string)
20
+ optional(:options).hash(EmitterOptions)
21
21
  end
22
22
 
23
23
  TheHive = Dry::Schema.Params do
24
24
  required(:emitter).value(Types::String.enum(*Mihari::Emitters::TheHive.keys))
25
- optional(:url).value(:string)
26
- optional(:api_key).value(:string)
27
- optional(:options).hash(Options)
25
+ optional(:url).filled(:string)
26
+ optional(:api_key).filled(:string)
27
+ optional(:options).hash(EmitterOptions)
28
28
  end
29
29
 
30
30
  Slack = Dry::Schema.Params do
31
31
  required(:emitter).value(Types::String.enum(*Mihari::Emitters::Slack.keys))
32
- optional(:webhook_url).value(:string)
33
- optional(:channel).value(:string)
34
- optional(:options).hash(Options)
32
+ optional(:webhook_url).filled(:string)
33
+ optional(:channel).filled(:string)
34
+ optional(:options).hash(EmitterOptions)
35
35
  end
36
36
 
37
37
  Webhook = Dry::Schema.Params do
38
38
  required(:emitter).value(Types::String.enum(*Mihari::Emitters::Webhook.keys))
39
- required(:url).value(:string)
39
+ required(:url).filled(:string)
40
40
  optional(:method).value(Types::HTTPRequestMethods).default("POST")
41
- optional(:headers).value(:hash).default({})
42
- optional(:template).value(:string)
43
- optional(:options).hash(Options)
41
+ optional(:headers).filled(:hash)
42
+ optional(:template).filled(:string)
43
+ optional(:options).hash(EmitterOptions)
44
44
  end
45
45
  end
46
46
 
47
- Emitter = Schemas::Emitters.get_or_composition
47
+ Emitter = Schemas::Emitters.compose_by_or
48
48
  end
49
49
  end
@@ -29,6 +29,6 @@ module Mihari
29
29
  end
30
30
  end
31
31
 
32
- Enricher = Schemas::Enrichers.get_or_composition
32
+ Enricher = Schemas::Enrichers.compose_by_or
33
33
  end
34
34
  end
@@ -8,9 +8,9 @@ module Dry
8
8
  # (see https://github.com/dry-rb/dry-schema/issues/70)
9
9
  #
10
10
  class DSL
11
- def default(value)
11
+ def default(artifact)
12
12
  schema_dsl.before(:rule_applier) do |result|
13
- result.update(name => value) if result.output && !result[name]
13
+ result.update(name => artifact) if result.output && !result[name]
14
14
  end
15
15
  end
16
16
  end
@@ -3,27 +3,29 @@
3
3
  module Mihari
4
4
  module Schemas
5
5
  Options = Dry::Schema.Params do
6
- optional(:retry_times).value(:integer).default(Mihari.config.retry_times)
7
- optional(:retry_interval).value(:integer).default(Mihari.config.retry_interval)
8
- optional(:retry_exponential_backoff).value(:bool).default(Mihari.config.retry_exponential_backoff)
6
+ optional(:retry_times).value(:integer)
7
+ optional(:retry_interval).value(:integer)
8
+ optional(:retry_exponential_backoff).value(:bool)
9
9
  optional(:timeout).value(:integer)
10
10
  end
11
11
 
12
- IgnoreErrorOptions = Dry::Schema.Params do
13
- optional(:ignore_error).value(:bool).default(Mihari.config.ignore_error)
14
- end
15
-
16
12
  ParallelOptions = Dry::Schema.Params do
17
- optional(:parallel).value(:bool).default(Mihari.config.parallel)
13
+ optional(:parallel).value(:bool)
18
14
  end
19
15
 
20
- AnalyzerOptions = Options | IgnoreErrorOptions | ParallelOptions
16
+ IgnoreErrorOptions = Dry::Schema.Params do
17
+ optional(:ignore_error).value(:bool)
18
+ end
21
19
 
22
20
  PaginationOptions = Dry::Schema.Params do
23
- optional(:pagination_interval).value(:integer).default(Mihari.config.pagination_interval)
24
- optional(:pagination_limit).value(:integer).default(Mihari.config.pagination_limit)
21
+ optional(:pagination_interval).value(:integer)
22
+ optional(:pagination_limit).value(:integer)
25
23
  end
26
24
 
27
- AnalyzerPaginationOptions = AnalyzerOptions | PaginationOptions
25
+ AnalyzerOptions = Options & IgnoreErrorOptions & ParallelOptions
26
+
27
+ AnalyzerPaginationOptions = AnalyzerOptions & PaginationOptions
28
+
29
+ EmitterOptions = Options & ParallelOptions
28
30
  end
29
31
  end
@@ -7,27 +7,27 @@ require "mihari/schemas/enricher"
7
7
  module Mihari
8
8
  module Schemas
9
9
  Rule = Dry::Schema.Params do
10
- required(:id).value(:string)
11
- required(:title).value(:string)
12
- required(:description).value(:string)
10
+ required(:id).filled(:string)
11
+ required(:title).filled(:string)
12
+ required(:description).filled(:string)
13
13
 
14
- optional(:tags).value(array[:string]).default([])
14
+ optional(:author).filled(:string)
15
+ optional(:status).filled(:string)
15
16
 
16
- optional(:author).value(:string)
17
- optional(:references).value(array[:string])
18
- optional(:related).value(array[:string])
19
- optional(:status).value(:string)
17
+ optional(:tags).array { filled(:string) }.default([])
18
+ optional(:references).array { filled(:string) }
19
+ optional(:related).array { filled(:string) }
20
20
 
21
21
  optional(:created_on).value(:date)
22
22
  optional(:updated_on).value(:date)
23
23
 
24
- required(:queries).value(:array).each { Analyzer } # rubocop:disable Lint/Void
24
+ required(:queries).array { Analyzer }
25
+ optional(:emitters).array { Emitter }.default(DEFAULT_EMITTERS)
26
+ optional(:enrichers).array { Enricher }.default(DEFAULT_ENRICHERS)
25
27
 
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
28
+ optional(:data_types).filled(array[Types::DataTypes]).default(Mihari::Types::DataTypes.values)
28
29
 
29
- optional(:data_types).value(array[Types::DataTypes]).default(Mihari::Types::DataTypes.values)
30
- optional(:falsepositives).value(array[:string]).default([])
30
+ optional(:falsepositives).array { filled(:string) }.default([])
31
31
 
32
32
  optional(:artifact_ttl).value(:integer)
33
33
  end
@@ -42,7 +42,7 @@ module Mihari
42
42
 
43
43
  rule(:falsepositives) do
44
44
  value.each do |v|
45
- key.failure("#{v} is not a valid format.") unless valid_falsepositive?(v)
45
+ key.failure("#{v} is not a valid format") unless valid_falsepositive?(v)
46
46
  end
47
47
  end
48
48
 
@@ -20,158 +20,5 @@ module Mihari
20
20
  Rule.from_yaml ERB.new(File.read(path_or_id)).result
21
21
  end
22
22
  end
23
-
24
- #
25
- # Autonomous system builder
26
- #
27
- class AutonomousSystemBuilder < Service
28
- #
29
- # @param [String] ip
30
- # @param [Mihari::Enrichers::MMDB] enricher
31
- #
32
- # @return [Mihari::Models::AutonomousSystem, nil]
33
- #
34
- def call(ip, enricher: Enrichers::MMDB.new)
35
- enricher.result(ip).fmap do |res|
36
- Models::AutonomousSystem.new(number: res.asn) if res.asn
37
- end.value_or nil
38
- end
39
- end
40
-
41
- #
42
- # CPE builder
43
- #
44
- class CPEBuilder < Service
45
- #
46
- # Build CPEs
47
- #
48
- # @param [String] ip
49
- # @param [Mihari::Enrichers::Shodan] enricher
50
- #
51
- # @return [Array<Mihari::Models::CPE>]
52
- #
53
- def call(ip, enricher: Enrichers::Shodan.new)
54
- enricher.result(ip).fmap do |res|
55
- (res&.cpes || []).map { |cpe| Models::CPE.new(name: cpe) }
56
- end.value_or []
57
- end
58
- end
59
-
60
- #
61
- # DNS record builder
62
- #
63
- class DnsRecordBuilder < Service
64
- #
65
- # Build DNS records
66
- #
67
- # @param [String] domain
68
- # @param [Mihari::Enrichers::Shodan] enricher
69
- #
70
- # @return [Array<Mihari::Models::DnsRecord>]
71
- #
72
- def call(domain, enricher: Enrichers::GooglePublicDNS.new)
73
- enricher.result(domain).fmap do |res|
74
- res.answers.map { |answer| Models::DnsRecord.new(resource: answer.resource_type, value: answer.data) }
75
- end.value_or []
76
- end
77
- end
78
-
79
- #
80
- # Geolocation builder
81
- #
82
- class GeolocationBuilder < Service
83
- #
84
- # Build Geolocation
85
- #
86
- # @param [String] ip
87
- # @param [Mihari::Enrichers::MMDB] enricher
88
- #
89
- # @return [Mihari::Models::Geolocation, nil]
90
- #
91
- def call(ip, enricher: Enrichers::MMDB.new)
92
- enricher.result(ip).fmap do |res|
93
- if res.country_code
94
- Models::Geolocation.new(
95
- country: NormalizeCountry(res.country_code, to: :short),
96
- country_code: res.country_code
97
- )
98
- end
99
- end.value_or nil
100
- end
101
- end
102
-
103
- #
104
- # Port builder
105
- #
106
- class PortBuilder < Service
107
- #
108
- # Build ports
109
- #
110
- # @param [String] ip
111
- # @param [Mihari::Enrichers::Shodan] enricher
112
- #
113
- # @return [Array<Mihari::Models::Port>]
114
- #
115
- def call(ip, enricher: Enrichers::Shodan.new)
116
- enricher.result(ip).fmap do |res|
117
- (res&.ports || []).map { |port| Models::Port.new(number: port) }
118
- end.value_or []
119
- end
120
- end
121
-
122
- #
123
- # Reverse DNS name builder
124
- #
125
- class ReverseDnsNameBuilder < Service
126
- #
127
- # Build reverse DNS names
128
- #
129
- # @param [String] ip
130
- # @param [Mihari::Enrichers::Shodan] enricher
131
- #
132
- # @return [Array<Mihari::Models::ReverseDnsName>]
133
- #
134
- def call(ip, enricher: Enrichers::Shodan.new)
135
- enricher.result(ip).fmap do |res|
136
- (res&.hostnames || []).map { |name| Models::ReverseDnsName.new(name: name) }
137
- end.value_or []
138
- end
139
- end
140
-
141
- #
142
- # Vulnerability builder
143
- #
144
- class VulnerabilityBuilder < Service
145
- #
146
- # Build vulnerabilities
147
- #
148
- # @param [String] ip
149
- # @param [Mihari::Enrichers::Shodan] enricher
150
- #
151
- # @return [Array<Mihari::Models::Vulnerability>]
152
- #
153
- def call(ip, enricher: Enrichers::Shodan.new)
154
- enricher.result(ip).fmap do |res|
155
- (res&.vulns || []).map { |name| Models::Vulnerability.new(name: name) }
156
- end.value_or []
157
- end
158
- end
159
-
160
- #
161
- # Whois record builder
162
- #
163
- class WhoisRecordBuilder < Service
164
- #
165
- # Build whois record
166
- #
167
- # @param [String] domain
168
- # @param [Mihari::Enrichers::Whois] enricher
169
- #
170
- # @return [Mihari::Models::WhoisRecord, nil]
171
- #
172
- def call(domain, enricher: Enrichers::Whois.new)
173
- enricher.result(domain).value_or nil
174
- end
175
- end
176
23
  end
177
24
  end
@@ -19,7 +19,7 @@ module Mihari
19
19
 
20
20
  raise UnenrichableError.new, "#{artifact.id} is already enriched or unenrichable" unless artifact.enrichable?
21
21
 
22
- artifact.enrich_all
22
+ artifact.enrich
23
23
  artifact.save
24
24
  end
25
25
  end
@@ -51,7 +51,7 @@ module Mihari
51
51
  # @return [Mihari::Structs::MMDB::Response]
52
52
  #
53
53
  def call(ip)
54
- Mihari::Enrichers::MMDB.new.call ip
54
+ Clients::MMDB.new.query ip
55
55
  end
56
56
  end
57
57
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "7.2.0"
4
+ VERSION = "7.3.1"
5
5
  end