mihari 5.2.1 → 5.2.2
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/lib/mihari/analyzers/base.rb +20 -115
- data/lib/mihari/analyzers/censys.rb +20 -2
- data/lib/mihari/analyzers/onyphe.rb +1 -1
- data/lib/mihari/analyzers/rule.rb +116 -60
- data/lib/mihari/analyzers/shodan.rb +1 -1
- data/lib/mihari/analyzers/urlscan.rb +6 -9
- data/lib/mihari/analyzers/virustotal_intelligence.rb +1 -5
- data/lib/mihari/cli/main.rb +2 -2
- data/lib/mihari/commands/search.rb +69 -0
- data/lib/mihari/mixins/error_notification.rb +0 -2
- data/lib/mihari/models/artifact.rb +1 -1
- data/lib/mihari/schemas/rule.rb +2 -17
- data/lib/mihari/structs/censys.rb +167 -11
- data/lib/mihari/structs/config.rb +28 -0
- data/lib/mihari/structs/google_public_dns.rb +39 -1
- data/lib/mihari/structs/greynoise.rb +93 -6
- data/lib/mihari/structs/ipinfo.rb +40 -0
- data/lib/mihari/structs/onyphe.rb +88 -6
- data/lib/mihari/structs/rule.rb +4 -2
- data/lib/mihari/structs/shodan.rb +138 -4
- data/lib/mihari/structs/urlscan.rb +98 -1
- data/lib/mihari/structs/virustotal_intelligence.rb +96 -1
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari.rb +1 -0
- data/mihari.gemspec +8 -7
- metadata +29 -30
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -43
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -15
- data/.github/workflows/test.yml +0 -90
- data/config/pre_commit.yml +0 -3
- data/docker/Dockerfile +0 -14
- data/examples/ipinfo_hosted_domains.rb +0 -45
- data/images/Tines-Full_Logo-Tines_Black.png +0 -0
- data/images/alert.png +0 -0
- data/images/logo.png +0 -0
- data/images/misp.png +0 -0
- data/images/overview.jpg +0 -0
- data/images/slack.png +0 -0
- data/images/tines.png +0 -0
- data/images/web_alerts.png +0 -0
- data/images/web_config.png +0 -0
- data/lib/mihari/commands/searcher.rb +0 -61
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77de9f96ff14a64b2ab7a492fff36ed1515df7def7b18b5d81bac6785a5b75c0
|
4
|
+
data.tar.gz: 9e99189740e4e3b6ee6af97cc09fbd2af1f5395c047df925f528721f2c68595d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 59171eef265ace4aab9295b9e3866233138194afc0ee691022d5c6ec9f4ca6b53400a01109f9a745cdf54d70cd8c74a1fd39ff319624bf3f3ff04d2ae409ca26
|
7
|
+
data.tar.gz: a886b191dcab85164e2f7e47d2b9445121254a8b94248ce1f6e54fff31b8e0a13afc6a8dc5013f04cfaac2340957344ba03b3441d09b0018e46bb7a564af4b03
|
data/.rubocop.yml
CHANGED
@@ -5,32 +5,34 @@ module Mihari
|
|
5
5
|
class Base
|
6
6
|
extend Dry::Initializer
|
7
7
|
|
8
|
-
option :rule, default: proc {}
|
9
|
-
|
10
8
|
include Mixins::Configurable
|
11
9
|
include Mixins::Retriable
|
12
10
|
|
13
|
-
# @return [Mihari::
|
14
|
-
|
15
|
-
|
16
|
-
def initialize(*args, **kwargs)
|
17
|
-
super(*args, **kwargs)
|
18
|
-
|
19
|
-
@base_time = Time.now.utc
|
11
|
+
# @return [Array<String>, Array<Mihari::Artifact>]
|
12
|
+
def artifacts
|
13
|
+
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
20
14
|
end
|
21
15
|
|
22
16
|
#
|
23
|
-
#
|
17
|
+
# Normalize artifacts
|
18
|
+
# - Convert data (string) into an artifact
|
19
|
+
# - Reject an invalid artifact
|
24
20
|
#
|
25
|
-
# @
|
21
|
+
# @return [Array<Mihari::Artifact>]
|
26
22
|
#
|
27
|
-
def
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
23
|
+
def normalized_artifacts
|
24
|
+
retry_on_error do
|
25
|
+
@normalized_artifacts ||= artifacts.compact.sort.map do |artifact|
|
26
|
+
# No need to set data_type manually
|
27
|
+
# It is set automatically in #initialize
|
28
|
+
artifact = artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact)
|
29
|
+
artifact
|
30
|
+
end.select(&:valid?).uniq(&:data).map do |artifact|
|
31
|
+
# set source
|
32
|
+
artifact.source = source
|
33
|
+
artifact
|
34
|
+
end
|
35
|
+
end
|
34
36
|
end
|
35
37
|
|
36
38
|
# @return [String]
|
@@ -43,109 +45,12 @@ module Mihari
|
|
43
45
|
self.class.to_s.split("::").last
|
44
46
|
end
|
45
47
|
|
46
|
-
#
|
47
|
-
# Set artifacts & run emitters in parallel
|
48
|
-
#
|
49
|
-
# @return [Mihari::Alert, nil]
|
50
|
-
#
|
51
|
-
def run
|
52
|
-
raise ConfigurationError, "#{class_name} is not configured correctly" unless configured?
|
53
|
-
|
54
|
-
alert_or_something = bulk_emit
|
55
|
-
# returns Mihari::Alert created by the database emitter
|
56
|
-
alert_or_something.find { |res| res.is_a?(Mihari::Alert) }
|
57
|
-
end
|
58
|
-
|
59
|
-
#
|
60
|
-
# Bulk emit
|
61
|
-
#
|
62
|
-
# @return [Array<Mihari::Alert>]
|
63
|
-
#
|
64
|
-
def bulk_emit
|
65
|
-
Parallel.map(valid_emitters) { |emitter| emit emitter }.compact
|
66
|
-
end
|
67
|
-
|
68
|
-
#
|
69
|
-
# Emit an alert
|
70
|
-
#
|
71
|
-
# @param [Mihari::Emitters::Base] emitter
|
72
|
-
#
|
73
|
-
# @return [Mihari::Alert, nil]
|
74
|
-
#
|
75
|
-
def emit(emitter)
|
76
|
-
return if enriched_artifacts.empty?
|
77
|
-
|
78
|
-
alert_or_something = emitter.run(artifacts: enriched_artifacts, rule: rule)
|
79
|
-
|
80
|
-
Mihari.logger.info "Emission by #{emitter.class} is succedded"
|
81
|
-
|
82
|
-
alert_or_something
|
83
|
-
rescue StandardError => e
|
84
|
-
Mihari.logger.info "Emission by #{emitter.class} is failed: #{e}"
|
85
|
-
end
|
86
|
-
|
87
48
|
class << self
|
88
49
|
def inherited(child)
|
89
50
|
super
|
90
51
|
Mihari.analyzers << child
|
91
52
|
end
|
92
53
|
end
|
93
|
-
|
94
|
-
#
|
95
|
-
# Normalize artifacts
|
96
|
-
# - Convert data (string) into an artifact
|
97
|
-
# - Set rule ID
|
98
|
-
# - Reject an invalid artifact
|
99
|
-
# - Uniquefy artifacts by data
|
100
|
-
#
|
101
|
-
# @return [Array<Mihari::Artifact>]
|
102
|
-
#
|
103
|
-
def normalized_artifacts
|
104
|
-
@normalized_artifacts ||= artifacts.compact.sort.map do |artifact|
|
105
|
-
# No need to set data_type manually
|
106
|
-
# It is set automatically in #initialize
|
107
|
-
artifact = artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact, source: source)
|
108
|
-
artifact.rule_id = rule&.id
|
109
|
-
artifact
|
110
|
-
end.select(&:valid?).uniq(&:data)
|
111
|
-
end
|
112
|
-
|
113
|
-
private
|
114
|
-
|
115
|
-
#
|
116
|
-
# Uniquefy artifacts (assure rule level uniqueness)
|
117
|
-
#
|
118
|
-
# @return [Array<Mihari::Artifact>]
|
119
|
-
#
|
120
|
-
def unique_artifacts
|
121
|
-
@unique_artifacts ||= normalized_artifacts.select do |artifact|
|
122
|
-
artifact.unique?(base_time: @base_time, artifact_lifetime: rule&.artifact_lifetime)
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
#
|
127
|
-
# Enriched artifacts
|
128
|
-
#
|
129
|
-
# @return [Array<Mihari::Artifact>]
|
130
|
-
#
|
131
|
-
def enriched_artifacts
|
132
|
-
@enriched_artifacts ||= Parallel.map(unique_artifacts) do |artifact|
|
133
|
-
artifact.enrich_all
|
134
|
-
artifact
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
#
|
139
|
-
# Select valid emitters
|
140
|
-
#
|
141
|
-
# @return [Array<Mihari::Emitters::Base>]
|
142
|
-
#
|
143
|
-
def valid_emitters
|
144
|
-
@valid_emitters ||= Mihari.emitters.filter_map do |klass|
|
145
|
-
emitter = klass.new
|
146
|
-
emitter.valid? ? emitter : nil
|
147
|
-
end.compact
|
148
|
-
end
|
149
54
|
end
|
150
55
|
end
|
151
56
|
end
|
@@ -26,15 +26,18 @@ module Mihari
|
|
26
26
|
@secret = kwargs[:secret] || Mihari.config.censys_secret
|
27
27
|
end
|
28
28
|
|
29
|
+
#
|
30
|
+
# @return [Array<Mihari::Artifact>]
|
31
|
+
#
|
29
32
|
def artifacts
|
30
33
|
artifacts = []
|
31
34
|
|
32
35
|
cursor = nil
|
33
36
|
loop do
|
34
37
|
response = client.search(query, cursor: cursor)
|
35
|
-
artifacts << response.result.to_artifacts
|
38
|
+
artifacts << response.result.to_artifacts
|
36
39
|
cursor = response.result.links.next
|
37
|
-
break if cursor
|
40
|
+
break if cursor.nil?
|
38
41
|
|
39
42
|
# sleep #{interval} seconds to avoid the rate limitation (if it is set)
|
40
43
|
sleep interval
|
@@ -43,24 +46,39 @@ module Mihari
|
|
43
46
|
artifacts.flatten.uniq(&:data)
|
44
47
|
end
|
45
48
|
|
49
|
+
#
|
50
|
+
# @return [Boolean]
|
51
|
+
#
|
46
52
|
def configured?
|
47
53
|
configuration_keys.all? { |key| Mihari.config.send(key) } || (id? && secret?)
|
48
54
|
end
|
49
55
|
|
50
56
|
private
|
51
57
|
|
58
|
+
#
|
59
|
+
# @return [Array<String>]
|
60
|
+
#
|
52
61
|
def configuration_keys
|
53
62
|
%w[censys_id censys_secret]
|
54
63
|
end
|
55
64
|
|
65
|
+
#
|
66
|
+
# @return [Mihari::Clients::Censys]
|
67
|
+
#
|
56
68
|
def client
|
57
69
|
@client ||= Clients::Censys.new(id: id, secret: secret)
|
58
70
|
end
|
59
71
|
|
72
|
+
#
|
73
|
+
# @return [Boolean]
|
74
|
+
#
|
60
75
|
def id?
|
61
76
|
!id.nil?
|
62
77
|
end
|
63
78
|
|
79
|
+
#
|
80
|
+
# @return [Boolean]
|
81
|
+
#
|
64
82
|
def secret?
|
65
83
|
!secret.nil?
|
66
84
|
end
|
@@ -34,14 +34,21 @@ module Mihari
|
|
34
34
|
"webhook" => Emitters::Webhook
|
35
35
|
}.freeze
|
36
36
|
|
37
|
-
|
38
|
-
attr_reader :rule
|
39
|
-
|
40
|
-
class Rule < Base
|
37
|
+
class Rule
|
41
38
|
include Mixins::FalsePositive
|
42
39
|
|
43
|
-
|
44
|
-
|
40
|
+
# @return [Mihari::Structs::Rule]
|
41
|
+
attr_reader :rule
|
42
|
+
|
43
|
+
# @return [Time]
|
44
|
+
attr_reader :base_time
|
45
|
+
|
46
|
+
#
|
47
|
+
# @param [Mihari::Structs::Rule] rule
|
48
|
+
#
|
49
|
+
def initialize(rule:)
|
50
|
+
@rule = rule
|
51
|
+
@base_time = Time.now.utc
|
45
52
|
|
46
53
|
validate_analyzer_configurations
|
47
54
|
end
|
@@ -52,41 +59,15 @@ module Mihari
|
|
52
59
|
# @return [Array<Mihari::Artifact>]
|
53
60
|
#
|
54
61
|
def artifacts
|
55
|
-
|
56
|
-
|
57
|
-
rule.queries.each do |original_params|
|
58
|
-
parmas = original_params.deep_dup
|
59
|
-
|
60
|
-
analyzer_name = parmas[:analyzer]
|
61
|
-
klass = get_analyzer_class(analyzer_name)
|
62
|
-
|
63
|
-
query = parmas[:query]
|
64
|
-
|
65
|
-
# set interval in the top level
|
66
|
-
options = parmas[:options] || {}
|
67
|
-
interval = options[:interval]
|
68
|
-
|
69
|
-
parmas[:interval] = interval if interval
|
70
|
-
|
71
|
-
# set rule
|
72
|
-
parmas[:rule] = rule
|
73
|
-
|
74
|
-
analyzer = klass.new(query, **parmas)
|
75
|
-
|
76
|
-
# Use #normalized_artifacts method to get atrifacts as Array<Mihari::Artifact>
|
77
|
-
# So Mihari::Artifact object has "source" attribute (e.g. "Shodan")
|
78
|
-
artifacts << analyzer.normalized_artifacts
|
79
|
-
end
|
80
|
-
|
81
|
-
artifacts.flatten
|
62
|
+
rule.queries.map { |params| run_query(params.deep_dup) }.flatten
|
82
63
|
end
|
83
64
|
|
84
65
|
#
|
85
66
|
# Normalize artifacts
|
86
|
-
# -
|
87
|
-
# - Reject an invalid artifact (for just in case)
|
67
|
+
# - Reject invalid artifacts (for just in case)
|
88
68
|
# - Select artifacts with allowed data types
|
89
|
-
# - Reject artifacts with
|
69
|
+
# - Reject artifacts with false positive values
|
70
|
+
# - Set rule ID
|
90
71
|
#
|
91
72
|
# @return [Array<Mihari::Artifact>]
|
92
73
|
#
|
@@ -95,6 +76,20 @@ module Mihari
|
|
95
76
|
rule.data_types.include? artifact.data_type
|
96
77
|
end.reject do |artifact|
|
97
78
|
falsepositive? artifact.data
|
79
|
+
end.map do |artifact|
|
80
|
+
artifact.rule_id = rule.id
|
81
|
+
artifact
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
# Uniquify artifacts (assure rule level uniqueness)
|
87
|
+
#
|
88
|
+
# @return [Array<Mihari::Artifact>]
|
89
|
+
#
|
90
|
+
def unique_artifacts
|
91
|
+
@unique_artifacts ||= normalized_artifacts.select do |artifact|
|
92
|
+
artifact.unique?(base_time: base_time, artifact_lifetime: rule.artifact_lifetime)
|
98
93
|
end
|
99
94
|
end
|
100
95
|
|
@@ -105,39 +100,93 @@ module Mihari
|
|
105
100
|
#
|
106
101
|
def enriched_artifacts
|
107
102
|
@enriched_artifacts ||= Parallel.map(unique_artifacts) do |artifact|
|
108
|
-
rule.enrichers.each
|
109
|
-
artifact.enrich_by_enricher(enricher[:enricher])
|
110
|
-
end
|
111
|
-
|
103
|
+
rule.enrichers.each { |enricher| artifact.enrich_by_enricher enricher[:enricher] }
|
112
104
|
artifact
|
113
105
|
end
|
114
106
|
end
|
115
107
|
|
116
108
|
#
|
117
|
-
#
|
109
|
+
# Bulk emit
|
110
|
+
#
|
111
|
+
# @return [Array<Mihari::Alert>]
|
112
|
+
#
|
113
|
+
def bulk_emit
|
114
|
+
Parallel.map(valid_emitters) { |emitter| emit emitter }.compact
|
115
|
+
end
|
116
|
+
|
117
|
+
#
|
118
|
+
# Emit an alert
|
119
|
+
#
|
120
|
+
# @param [Mihari::Emitters::Base] emitter
|
118
121
|
#
|
119
|
-
# @return [
|
122
|
+
# @return [Mihari::Alert, nil]
|
120
123
|
#
|
121
|
-
def
|
122
|
-
|
124
|
+
def emit(emitter)
|
125
|
+
return if enriched_artifacts.empty?
|
126
|
+
|
127
|
+
alert_or_something = emitter.run(artifacts: enriched_artifacts, rule: rule)
|
128
|
+
|
129
|
+
Mihari.logger.info "Emission by #{emitter.class} is succeeded"
|
130
|
+
|
131
|
+
alert_or_something
|
132
|
+
rescue StandardError => e
|
133
|
+
Mihari.logger.info "Emission by #{emitter.class} is failed: #{e}"
|
123
134
|
end
|
124
135
|
|
136
|
+
#
|
137
|
+
# Set artifacts & run emitters in parallel
|
138
|
+
#
|
139
|
+
# @return [Mihari::Alert, nil]
|
140
|
+
#
|
141
|
+
def run
|
142
|
+
# memoize enriched artifacts
|
143
|
+
enriched_artifacts
|
144
|
+
|
145
|
+
alert_or_something = bulk_emit
|
146
|
+
# returns Mihari::Alert created by the database emitter
|
147
|
+
alert_or_something.find { |res| res.is_a?(Mihari::Alert) }
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
125
152
|
#
|
126
153
|
# Check whether a value is a falsepositive value or not
|
127
154
|
#
|
128
155
|
# @return [Boolean]
|
129
156
|
#
|
130
157
|
def falsepositive?(value)
|
131
|
-
return true if
|
158
|
+
return true if rule.falsepositives.include?(value)
|
132
159
|
|
133
|
-
|
160
|
+
rule.falsepositives.select do |falsepositive|
|
134
161
|
falsepositive.is_a?(Regexp)
|
135
162
|
end.any? do |falseposistive|
|
136
163
|
falseposistive.match?(value)
|
137
164
|
end
|
138
165
|
end
|
139
166
|
|
140
|
-
|
167
|
+
#
|
168
|
+
# @param [Hash] params
|
169
|
+
#
|
170
|
+
# @return [Array<Mihari::Artifact>]
|
171
|
+
#
|
172
|
+
def run_query(params)
|
173
|
+
analyzer_name = params[:analyzer]
|
174
|
+
klass = get_analyzer_class(analyzer_name)
|
175
|
+
|
176
|
+
# set interval in the top level
|
177
|
+
options = params[:options] || {}
|
178
|
+
interval = options[:interval]
|
179
|
+
params[:interval] = interval if interval
|
180
|
+
|
181
|
+
# set rule
|
182
|
+
params[:rule] = rule
|
183
|
+
query = params[:query]
|
184
|
+
analyzer = klass.new(query, **params)
|
185
|
+
|
186
|
+
# Use #normalized_artifacts method to get artifacts as Array<Mihari::Artifact>
|
187
|
+
# So Mihari::Artifact object has "source" attribute (e.g. "Shodan")
|
188
|
+
analyzer.normalized_artifacts
|
189
|
+
end
|
141
190
|
|
142
191
|
#
|
143
192
|
# Get emitter class
|
@@ -153,18 +202,26 @@ module Mihari
|
|
153
202
|
raise ArgumentError, "#{emitter_name} is not supported"
|
154
203
|
end
|
155
204
|
|
156
|
-
|
157
|
-
|
158
|
-
|
205
|
+
#
|
206
|
+
# @param [Hash] params
|
207
|
+
#
|
208
|
+
# @return [Mihari::Emitter:Base]
|
209
|
+
#
|
210
|
+
def validate_emitter(params)
|
211
|
+
name = params[:emitter]
|
212
|
+
params.delete(:emitter)
|
159
213
|
|
160
|
-
|
161
|
-
|
214
|
+
klass = get_emitter_class(name)
|
215
|
+
emitter = klass.new(**params)
|
162
216
|
|
163
|
-
|
164
|
-
|
217
|
+
emitter.valid? ? emitter : nil
|
218
|
+
end
|
165
219
|
|
166
|
-
|
167
|
-
|
220
|
+
#
|
221
|
+
# @return [Array<Mihari::Emitter::Base>]
|
222
|
+
#
|
223
|
+
def valid_emitters
|
224
|
+
@valid_emitters ||= rule.emitters.filter_map { |params| validate_emitter(params.deep_dup) }
|
168
225
|
end
|
169
226
|
|
170
227
|
#
|
@@ -187,13 +244,12 @@ module Mihari
|
|
187
244
|
def validate_analyzer_configurations
|
188
245
|
rule.queries.each do |params|
|
189
246
|
analyzer_name = params[:analyzer]
|
247
|
+
|
190
248
|
klass = get_analyzer_class(analyzer_name)
|
249
|
+
klass_name = klass.to_s.split("::").last
|
191
250
|
|
192
251
|
instance = klass.new("dummy")
|
193
|
-
unless instance.configured?
|
194
|
-
klass_name = klass.to_s.split("::").last
|
195
|
-
raise ConfigurationError, "#{klass_name} is not configured correctly"
|
196
|
-
end
|
252
|
+
raise ConfigurationError, "#{klass_name} is not configured correctly" unless instance.configured?
|
197
253
|
end
|
198
254
|
end
|
199
255
|
end
|
@@ -36,15 +36,12 @@ module Mihari
|
|
36
36
|
|
37
37
|
def artifacts
|
38
38
|
responses = search
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
data.nil? ? nil : Artifact.new(data: data, source: source, metadata: result)
|
46
|
-
end
|
47
|
-
end.flatten
|
39
|
+
# @type [Array<Mihari::Artifact>]
|
40
|
+
artifacts = responses.map { |res| res.to_artifacts }.flatten
|
41
|
+
|
42
|
+
artifacts.select do |artifact|
|
43
|
+
allowed_data_types.include? artifact.data_type
|
44
|
+
end
|
48
45
|
end
|
49
46
|
|
50
47
|
private
|
@@ -26,11 +26,7 @@ module Mihari
|
|
26
26
|
|
27
27
|
def artifacts
|
28
28
|
responses = search_with_cursor
|
29
|
-
responses.map
|
30
|
-
response.data.map do |datum|
|
31
|
-
Artifact.new(data: datum.value, source: source, metadata: datum.metadata)
|
32
|
-
end
|
33
|
-
end.flatten
|
29
|
+
responses.map(&:to_artifacts).flatten
|
34
30
|
end
|
35
31
|
|
36
32
|
private
|
data/lib/mihari/cli/main.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
require "thor"
|
4
4
|
|
5
5
|
# Commands
|
6
|
-
require "mihari/commands/
|
6
|
+
require "mihari/commands/search"
|
7
7
|
require "mihari/commands/version"
|
8
8
|
require "mihari/commands/web"
|
9
9
|
require "mihari/commands/database"
|
@@ -17,7 +17,7 @@ require "mihari/cli/rule"
|
|
17
17
|
module Mihari
|
18
18
|
module CLI
|
19
19
|
class Main < Base
|
20
|
-
include Mihari::Commands::
|
20
|
+
include Mihari::Commands::Search
|
21
21
|
include Mihari::Commands::Version
|
22
22
|
include Mihari::Commands::Web
|
23
23
|
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Commands
|
5
|
+
module Search
|
6
|
+
include Mixins::ErrorNotification
|
7
|
+
|
8
|
+
def self.included(thor)
|
9
|
+
thor.class_eval do
|
10
|
+
desc "search [PATH]", "Search by a rule"
|
11
|
+
method_option :force_overwrite, type: :boolean, aliases: "-f", desc: "Force an overwrite the rule"
|
12
|
+
#
|
13
|
+
# Search by a rule
|
14
|
+
#
|
15
|
+
# @param [String] path_or_id
|
16
|
+
#
|
17
|
+
def search(path_or_id)
|
18
|
+
Mihari::Database.with_db_connection do
|
19
|
+
rule = Structs::Rule.from_path_or_id path_or_id
|
20
|
+
|
21
|
+
begin
|
22
|
+
rule.validate!
|
23
|
+
rescue RuleValidationError
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
update_rule rule
|
28
|
+
run_rule rule
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param [Mihari::Structs::Rule] rule
|
35
|
+
#
|
36
|
+
def update_rule(rule)
|
37
|
+
force_overwrite = options["force_overwrite"] || false
|
38
|
+
begin
|
39
|
+
rule_model = Mihari::Rule.find(rule.id)
|
40
|
+
has_change = rule_model.data != rule.data.deep_stringify_keys
|
41
|
+
has_change_and_not_force_overwrite = has_change & !force_overwrite
|
42
|
+
|
43
|
+
confirm_message = "This operation will overwrite the rule in the database (Rule ID: #{rule.id}). Are you sure you want to update the rule? (y/n)"
|
44
|
+
return if has_change_and_not_force_overwrite && !yes?(confirm_message)
|
45
|
+
# update the rule
|
46
|
+
rule.model.save
|
47
|
+
rescue ActiveRecord::RecordNotFound
|
48
|
+
# create a new rule
|
49
|
+
rule.model.save
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# @param [Mihari::Structs::Rule] rule
|
55
|
+
#
|
56
|
+
def run_rule(rule)
|
57
|
+
with_error_notification do
|
58
|
+
alert = rule.analyzer.run
|
59
|
+
if alert
|
60
|
+
data = Mihari::Entities::Alert.represent(alert)
|
61
|
+
puts JSON.pretty_generate(data.as_json)
|
62
|
+
else
|
63
|
+
Mihari.logger.info "There is no new alert created in the database"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -60,7 +60,7 @@ module Mihari
|
|
60
60
|
).order(created_at: :desc).first
|
61
61
|
return true if artifact.nil?
|
62
62
|
|
63
|
-
# check
|
63
|
+
# check whether the artifact is decayed or not
|
64
64
|
return false if artifact_lifetime.nil?
|
65
65
|
|
66
66
|
# use the current UTC time if base_time is not given (for testing)
|
data/lib/mihari/schemas/rule.rb
CHANGED
@@ -25,30 +25,15 @@ module Mihari
|
|
25
25
|
AnalyzerWithoutAPIKey | AnalyzerWithAPIKey | Censys | CIRCL | PassiveTotal | ZoomEye | Urlscan | Crtsh | Feed
|
26
26
|
end
|
27
27
|
|
28
|
-
optional(:emitters).value(:array).each { Database | MISP | TheHive | Slack | Webhook }
|
28
|
+
optional(:emitters).value(:array).each { Database | MISP | TheHive | Slack | Webhook }.default(DEFAULT_EMITTERS)
|
29
29
|
|
30
|
-
optional(:enrichers).value(:array).each(Enricher)
|
30
|
+
optional(:enrichers).value(:array).each(Enricher).default(DEFAULT_ENRICHERS)
|
31
31
|
|
32
32
|
optional(:data_types).value(array[Types::DataTypes]).default(DEFAULT_DATA_TYPES)
|
33
33
|
optional(:falsepositives).value(array[:string]).default([])
|
34
34
|
|
35
35
|
optional(:artifact_lifetime).value(:integer)
|
36
36
|
optional(:artifact_ttl).value(:integer)
|
37
|
-
|
38
|
-
before(:key_coercer) do |result|
|
39
|
-
# it looks like that dry-schema v1.9.1 has an issue with setting an array of schemas as a default value
|
40
|
-
# e.g. optional(:emitters).value(:array).each { Emitter | HTTP }.default(DEFAULT_EMITTERS) does not work well
|
41
|
-
# so let's do a dirty hack...
|
42
|
-
h = result.to_h
|
43
|
-
|
44
|
-
emitters = h[:emitters]
|
45
|
-
h[:emitters] = emitters || DEFAULT_EMITTERS
|
46
|
-
|
47
|
-
enrichers = h[:enrichers]
|
48
|
-
h[:enrichers] = enrichers || DEFAULT_ENRICHERS
|
49
|
-
|
50
|
-
h
|
51
|
-
end
|
52
37
|
end
|
53
38
|
|
54
39
|
class RuleContract < Dry::Validation::Contract
|