mihari 7.2.0 → 7.3.1

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