mihari 5.2.2 → 5.2.3
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/lib/mihari/analyzers/binaryedge.rb +0 -1
- data/lib/mihari/analyzers/censys.rb +7 -2
- data/lib/mihari/analyzers/circl.rb +1 -1
- data/lib/mihari/analyzers/passivetotal.rb +1 -1
- data/lib/mihari/analyzers/rule.rb +63 -72
- data/lib/mihari/analyzers/virustotal_intelligence.rb +1 -2
- data/lib/mihari/clients/base.rb +1 -1
- data/lib/mihari/commands/database.rb +12 -11
- data/lib/mihari/commands/rule.rb +47 -45
- data/lib/mihari/commands/search.rb +66 -47
- data/lib/mihari/commands/version.rb +8 -6
- data/lib/mihari/commands/web.rb +26 -23
- data/lib/mihari/emitters/base.rb +14 -1
- data/lib/mihari/emitters/database.rb +3 -10
- data/lib/mihari/emitters/misp.rb +16 -5
- data/lib/mihari/emitters/slack.rb +13 -15
- data/lib/mihari/emitters/the_hive.rb +17 -19
- data/lib/mihari/emitters/webhook.rb +23 -23
- data/lib/mihari/enrichers/whois.rb +1 -0
- data/lib/mihari/feed/parser.rb +1 -0
- data/lib/mihari/feed/reader.rb +29 -14
- data/lib/mihari/mixins/configurable.rb +13 -4
- data/lib/mihari/structs/censys.rb +96 -82
- data/lib/mihari/structs/config.rb +23 -21
- data/lib/mihari/structs/google_public_dns.rb +27 -23
- data/lib/mihari/structs/greynoise.rb +44 -38
- data/lib/mihari/structs/onyphe.rb +34 -30
- data/lib/mihari/structs/shodan.rb +77 -69
- data/lib/mihari/structs/urlscan.rb +42 -36
- data/lib/mihari/structs/virustotal_intelligence.rb +57 -49
- data/lib/mihari/type_checker.rb +10 -8
- data/lib/mihari/version.rb +1 -1
- data/mihari.gemspec +3 -3
- metadata +8 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2a66fb2d71bcae401062277921a8ade6a3e9e9d961b193a80deacd3a8a934d4c
|
4
|
+
data.tar.gz: b4dcccfa58019f819241f8679b5d1ae846002f0a95307cd95856f2b6d04a6dd1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e215400c8dce2b864bc26a951ed8ea35757441e7d25bdba6c66632d6716991bad202170f9984d09b7a709f6c9aceb731e5b40396efe621ffc55810a10db45db2
|
7
|
+
data.tar.gz: 745522a9cefaed75e5b266a429dea7afc0efe34f26f8591af18e3bc27a0746659ed8d64f6aa64f6edc0f9195acbf26e4a7ee078c052bab760f985da779c4e6e4
|
@@ -37,7 +37,12 @@ module Mihari
|
|
37
37
|
response = client.search(query, cursor: cursor)
|
38
38
|
artifacts << response.result.to_artifacts
|
39
39
|
cursor = response.result.links.next
|
40
|
-
|
40
|
+
# NOTE: Censys's search API is unstable recently
|
41
|
+
# it may returns empty links or empty string cursors
|
42
|
+
# - Empty links: "links": {}
|
43
|
+
# - Empty cursors: "links": { "next": "", "prev": "" }
|
44
|
+
# So it needs to check both cases
|
45
|
+
break if cursor.nil? || cursor.empty?
|
41
46
|
|
42
47
|
# sleep #{interval} seconds to avoid the rate limitation (if it is set)
|
43
48
|
sleep interval
|
@@ -50,7 +55,7 @@ module Mihari
|
|
50
55
|
# @return [Boolean]
|
51
56
|
#
|
52
57
|
def configured?
|
53
|
-
configuration_keys
|
58
|
+
configuration_keys? || (id? && secret?)
|
54
59
|
end
|
55
60
|
|
56
61
|
private
|
@@ -54,12 +54,12 @@ module Mihari
|
|
54
54
|
end
|
55
55
|
|
56
56
|
#
|
57
|
-
# Returns a list of artifacts matched with queries
|
57
|
+
# Returns a list of artifacts matched with queries/analyzers
|
58
58
|
#
|
59
59
|
# @return [Array<Mihari::Artifact>]
|
60
60
|
#
|
61
61
|
def artifacts
|
62
|
-
|
62
|
+
analyzers.flat_map(&:normalized_artifacts)
|
63
63
|
end
|
64
64
|
|
65
65
|
#
|
@@ -111,26 +111,15 @@ module Mihari
|
|
111
111
|
# @return [Array<Mihari::Alert>]
|
112
112
|
#
|
113
113
|
def bulk_emit
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
#
|
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}"
|
114
|
+
return [] if enriched_artifacts.empty?
|
115
|
+
|
116
|
+
Parallel.map(valid_emitters) do |emitter|
|
117
|
+
emission = emitter.emit
|
118
|
+
Mihari.logger.info "Emission by #{emitter.class} is succeeded"
|
119
|
+
emission
|
120
|
+
rescue StandardError => e
|
121
|
+
Mihari.logger.info "Emission by #{emitter.class} is failed: #{e}"
|
122
|
+
end.compact
|
134
123
|
end
|
135
124
|
|
136
125
|
#
|
@@ -165,27 +154,50 @@ module Mihari
|
|
165
154
|
end
|
166
155
|
|
167
156
|
#
|
168
|
-
#
|
157
|
+
# Deep copied queries
|
169
158
|
#
|
170
|
-
# @return [Array<
|
159
|
+
# @return [Array<Hash>]
|
160
|
+
#
|
161
|
+
def queries
|
162
|
+
rule.queries.map(&:deep_dup)
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# Get analyzer class
|
167
|
+
#
|
168
|
+
# @param [String] analyzer_name
|
169
|
+
#
|
170
|
+
# @return [Class<Mihari::Analyzers::Base>] analyzer class
|
171
|
+
#
|
172
|
+
def get_analyzer_class(analyzer_name)
|
173
|
+
analyzer = ANALYZER_TO_CLASS[analyzer_name]
|
174
|
+
return analyzer if analyzer
|
175
|
+
|
176
|
+
raise ArgumentError, "#{analyzer_name} is not supported"
|
177
|
+
end
|
178
|
+
|
179
|
+
#
|
180
|
+
# @return [Array<Mihari::Analyzers::Base>] <description>
|
171
181
|
#
|
172
|
-
def
|
173
|
-
|
174
|
-
|
182
|
+
def analyzers
|
183
|
+
@analyzers ||= queries.map do |params|
|
184
|
+
analyzer_name = params[:analyzer]
|
185
|
+
klass = get_analyzer_class(analyzer_name)
|
175
186
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
187
|
+
# set interval in the top level
|
188
|
+
options = params[:options] || {}
|
189
|
+
interval = options[:interval]
|
190
|
+
params[:interval] = interval if interval
|
180
191
|
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
analyzer = klass.new(query, **params)
|
192
|
+
# set rule
|
193
|
+
params[:rule] = rule
|
194
|
+
query = params[:query]
|
185
195
|
|
186
|
-
|
187
|
-
|
188
|
-
|
196
|
+
analyzer = klass.new(query, **params)
|
197
|
+
raise ConfigurationError, "#{analyzer.source} is not configured correctly" unless analyzer.configured?
|
198
|
+
|
199
|
+
analyzer
|
200
|
+
end
|
189
201
|
end
|
190
202
|
|
191
203
|
#
|
@@ -203,54 +215,33 @@ module Mihari
|
|
203
215
|
end
|
204
216
|
|
205
217
|
#
|
206
|
-
#
|
218
|
+
# Deep copied emitters
|
207
219
|
#
|
208
|
-
# @return [Mihari::
|
220
|
+
# @return [Array<Mihari::Emitters::Base>]
|
209
221
|
#
|
210
|
-
def
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
klass = get_emitter_class(name)
|
215
|
-
emitter = klass.new(**params)
|
222
|
+
def emitters
|
223
|
+
rule.emitters.map(&:deep_dup).map do |params|
|
224
|
+
name = params[:emitter]
|
225
|
+
params.delete(:emitter)
|
216
226
|
|
217
|
-
|
227
|
+
klass = get_emitter_class(name)
|
228
|
+
klass.new(artifacts: enriched_artifacts, rule: rule, **params)
|
229
|
+
end
|
218
230
|
end
|
219
231
|
|
220
232
|
#
|
221
|
-
# @return [Array<Mihari::
|
233
|
+
# @return [Array<Mihari::Emitters::Base>]
|
222
234
|
#
|
223
235
|
def valid_emitters
|
224
|
-
@valid_emitters ||=
|
225
|
-
end
|
226
|
-
|
227
|
-
#
|
228
|
-
# Get analyzer class
|
229
|
-
#
|
230
|
-
# @param [String] analyzer_name
|
231
|
-
#
|
232
|
-
# @return [Class<Mihari::Analyzers::Base>] analyzer class
|
233
|
-
#
|
234
|
-
def get_analyzer_class(analyzer_name)
|
235
|
-
analyzer = ANALYZER_TO_CLASS[analyzer_name]
|
236
|
-
return analyzer if analyzer
|
237
|
-
|
238
|
-
raise ArgumentError, "#{analyzer_name} is not supported"
|
236
|
+
@valid_emitters ||= emitters.select(&:valid?)
|
239
237
|
end
|
240
238
|
|
241
239
|
#
|
242
240
|
# Validate configuration of analyzers
|
243
241
|
#
|
244
242
|
def validate_analyzer_configurations
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
klass = get_analyzer_class(analyzer_name)
|
249
|
-
klass_name = klass.to_s.split("::").last
|
250
|
-
|
251
|
-
instance = klass.new("dummy")
|
252
|
-
raise ConfigurationError, "#{klass_name} is not configured correctly" unless instance.configured?
|
253
|
-
end
|
243
|
+
# memoize analyzers & raise ConfigurationError if there is an analyzer which is not configured
|
244
|
+
analyzers
|
254
245
|
end
|
255
246
|
end
|
256
247
|
end
|
data/lib/mihari/clients/base.rb
CHANGED
@@ -3,18 +3,19 @@
|
|
3
3
|
module Mihari
|
4
4
|
module Commands
|
5
5
|
module Database
|
6
|
-
|
7
|
-
thor
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
6
|
+
class << self
|
7
|
+
def included(thor)
|
8
|
+
thor.class_eval do
|
9
|
+
desc "migrate", "Migrate DB schemas"
|
10
|
+
method_option :verbose, type: :boolean, default: true
|
11
|
+
#
|
12
|
+
# @param [String] direction
|
13
|
+
#
|
14
|
+
def migrate(direction = "up")
|
15
|
+
ActiveRecord::Migration.verbose = options["verbose"]
|
16
16
|
|
17
|
-
|
17
|
+
Mihari::Database.with_db_connection { Mihari::Database.migrate direction.to_sym }
|
18
|
+
end
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
data/lib/mihari/commands/rule.rb
CHANGED
@@ -5,60 +5,62 @@ require "pathname"
|
|
5
5
|
module Mihari
|
6
6
|
module Commands
|
7
7
|
module Rule
|
8
|
-
|
9
|
-
thor
|
10
|
-
|
11
|
-
|
12
|
-
# Validate format of a rule
|
13
|
-
#
|
14
|
-
# @param [String] path
|
15
|
-
#
|
16
|
-
def validate(path)
|
17
|
-
rule = Structs::Rule.from_path_or_id(path)
|
18
|
-
|
19
|
-
begin
|
20
|
-
rule.validate!
|
21
|
-
Mihari.logger.info "Valid format. The input is parsed as the following:"
|
22
|
-
Mihari.logger.info rule.data.to_yaml
|
23
|
-
rescue RuleValidationError
|
24
|
-
nil
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
desc "init [PATH]", "Initialize a new rule file"
|
29
|
-
#
|
30
|
-
# Initialize a new rule file
|
31
|
-
#
|
32
|
-
# @param [String] path
|
33
|
-
#
|
34
|
-
#
|
35
|
-
def init(path = "./rule.yml")
|
36
|
-
warning = "#{path} exists. Do you want to overwrite it? (y/n)"
|
37
|
-
return if Pathname(path).exist? && !(yes? warning)
|
38
|
-
|
39
|
-
initialize_rule path
|
40
|
-
|
41
|
-
Mihari.logger.info "A new rule is initialized: #{path}."
|
42
|
-
end
|
43
|
-
|
44
|
-
no_commands do
|
8
|
+
class << self
|
9
|
+
def included(thor)
|
10
|
+
thor.class_eval do
|
11
|
+
desc "validate [PATH]", "Validate a rule file"
|
45
12
|
#
|
46
|
-
#
|
13
|
+
# Validate format of a rule
|
47
14
|
#
|
48
|
-
|
49
|
-
|
15
|
+
# @param [String] path
|
16
|
+
#
|
17
|
+
def validate(path)
|
18
|
+
rule = Structs::Rule.from_path_or_id(path)
|
19
|
+
|
20
|
+
begin
|
21
|
+
rule.validate!
|
22
|
+
Mihari.logger.info "Valid format. The input is parsed as the following:"
|
23
|
+
Mihari.logger.info rule.data.to_yaml
|
24
|
+
rescue RuleValidationError
|
25
|
+
nil
|
26
|
+
end
|
50
27
|
end
|
51
28
|
|
29
|
+
desc "init [PATH]", "Initialize a new rule file"
|
52
30
|
#
|
53
|
-
#
|
31
|
+
# Initialize a new rule file
|
54
32
|
#
|
55
33
|
# @param [String] path
|
56
|
-
# @param [Dry::Files] files
|
57
34
|
#
|
58
|
-
# @return [nil]
|
59
35
|
#
|
60
|
-
def
|
61
|
-
|
36
|
+
def init(path = "./rule.yml")
|
37
|
+
warning = "#{path} exists. Do you want to overwrite it? (y/n)"
|
38
|
+
return if Pathname(path).exist? && !(yes? warning)
|
39
|
+
|
40
|
+
initialize_rule path
|
41
|
+
|
42
|
+
Mihari.logger.info "A new rule file has been initialized: #{path}."
|
43
|
+
end
|
44
|
+
|
45
|
+
no_commands do
|
46
|
+
#
|
47
|
+
# @return [Mihari::Structs::Rule]
|
48
|
+
#
|
49
|
+
def rule_template
|
50
|
+
Structs::Rule.from_path File.expand_path("../templates/rule.yml.erb", __dir__)
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Create a new rule
|
55
|
+
#
|
56
|
+
# @param [String] path
|
57
|
+
# @param [Dry::Files] files
|
58
|
+
#
|
59
|
+
# @return [nil]
|
60
|
+
#
|
61
|
+
def initialize_rule(path, files = Dry::Files.new)
|
62
|
+
files.write(path, rule_template.yaml)
|
63
|
+
end
|
62
64
|
end
|
63
65
|
end
|
64
66
|
end
|
@@ -3,64 +3,83 @@
|
|
3
3
|
module Mihari
|
4
4
|
module Commands
|
5
5
|
module Search
|
6
|
-
|
6
|
+
class << self
|
7
|
+
class RuleWrapper
|
8
|
+
include Mixins::ErrorNotification
|
9
|
+
|
10
|
+
# @return [Nihari::Structs::Rule]
|
11
|
+
attr_reader :rule
|
12
|
+
|
13
|
+
# @return [Boolean]
|
14
|
+
attr_reader :force_overwrite
|
15
|
+
|
16
|
+
def initialize(rule:, force_overwrite:)
|
17
|
+
@rule = rule
|
18
|
+
@force_overwrite = force_overwrite
|
19
|
+
end
|
20
|
+
|
21
|
+
def force_overwrite?
|
22
|
+
force_overwrite
|
23
|
+
end
|
7
24
|
|
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
25
|
#
|
15
|
-
# @
|
26
|
+
# @return [Boolean]
|
16
27
|
#
|
17
|
-
def
|
18
|
-
Mihari::
|
19
|
-
|
28
|
+
def diff?
|
29
|
+
model = Mihari::Rule.find(rule.id)
|
30
|
+
model.data != rule.data.deep_stringify_keys
|
31
|
+
rescue ActiveRecord::RecordNotFound
|
32
|
+
false
|
33
|
+
end
|
20
34
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
return
|
25
|
-
end
|
35
|
+
def update_or_create
|
36
|
+
rule.model.save
|
37
|
+
end
|
26
38
|
|
27
|
-
|
28
|
-
|
39
|
+
def run
|
40
|
+
with_error_notification do
|
41
|
+
alert = rule.analyzer.run
|
42
|
+
if alert
|
43
|
+
data = Mihari::Entities::Alert.represent(alert)
|
44
|
+
puts JSON.pretty_generate(data.as_json)
|
45
|
+
else
|
46
|
+
Mihari.logger.info "There is no new artifact found"
|
47
|
+
end
|
29
48
|
end
|
30
49
|
end
|
31
50
|
end
|
32
|
-
end
|
33
51
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
52
|
+
def included(thor)
|
53
|
+
thor.class_eval do
|
54
|
+
desc "search [PATH]", "Search by a rule"
|
55
|
+
method_option :force_overwrite, type: :boolean, aliases: "-f", desc: "Force an overwrite the rule"
|
56
|
+
#
|
57
|
+
# Search by a rule
|
58
|
+
#
|
59
|
+
# @param [String] path_or_id
|
60
|
+
#
|
61
|
+
def search(path_or_id)
|
62
|
+
Mihari::Database.with_db_connection do
|
63
|
+
rule = Structs::Rule.from_path_or_id path_or_id
|
42
64
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
# create a new rule
|
49
|
-
rule.model.save
|
50
|
-
end
|
51
|
-
end
|
65
|
+
begin
|
66
|
+
rule.validate!
|
67
|
+
rescue RuleValidationError
|
68
|
+
return
|
69
|
+
end
|
52
70
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
71
|
+
force_overwrite = options["force_overwrite"] || false
|
72
|
+
wrapper = RuleWrapper.new(rule: rule, force_overwrite: force_overwrite)
|
73
|
+
|
74
|
+
if wrapper.diff? && !force_overwrite
|
75
|
+
message = "There is diff in the rule (#{rule.id}). Are you sure you want to overwrite the rule? (y/n)"
|
76
|
+
return unless yes?(message)
|
77
|
+
end
|
78
|
+
|
79
|
+
wrapper.update_or_create
|
80
|
+
wrapper.run
|
81
|
+
end
|
82
|
+
end
|
64
83
|
end
|
65
84
|
end
|
66
85
|
end
|
@@ -3,13 +3,15 @@
|
|
3
3
|
module Mihari
|
4
4
|
module Commands
|
5
5
|
module Version
|
6
|
-
|
7
|
-
thor
|
8
|
-
|
6
|
+
class << self
|
7
|
+
def included(thor)
|
8
|
+
thor.class_eval do
|
9
|
+
map %w[--version -v] => :__print_version
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
desc "--version, -v", "Print the version"
|
12
|
+
def __print_version
|
13
|
+
puts Mihari::VERSION
|
14
|
+
end
|
13
15
|
end
|
14
16
|
end
|
15
17
|
end
|
data/lib/mihari/commands/web.rb
CHANGED
@@ -3,29 +3,32 @@
|
|
3
3
|
module Mihari
|
4
4
|
module Commands
|
5
5
|
module Web
|
6
|
-
|
7
|
-
thor
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
6
|
+
class << self
|
7
|
+
def included(thor)
|
8
|
+
thor.class_eval do
|
9
|
+
desc "web", "Launch the web app"
|
10
|
+
method_option :port, type: :numeric, default: 9292, desc: "Hostname to listen on"
|
11
|
+
method_option :host, type: :string, default: "localhost", desc: "Port to listen on"
|
12
|
+
method_option :threads, type: :string, default: "0:5", desc: "min:max threads to use"
|
13
|
+
method_option :verbose, type: :boolean, default: true, desc: "Report each request"
|
14
|
+
method_option :worker_timeout, type: :numeric, default: 60, desc: "Worker timeout value (in seconds)"
|
15
|
+
method_option :hide_config_values, type: :boolean, default: false,
|
16
|
+
desc: "Whether to hide config values or not"
|
17
|
+
method_option :open, type: :boolean, default: true, desc: "Whether to open the app in browser or not"
|
18
|
+
method_option :rack_env, type: :string, default: "production", desc: "Rack environment"
|
19
|
+
def web
|
20
|
+
Mihari.config.hide_config_values = options["hide_config_values"]
|
21
|
+
# set rack env as production
|
22
|
+
ENV["RACK_ENV"] ||= options["rack_env"]
|
23
|
+
Mihari::App.run!(
|
24
|
+
port: options["port"],
|
25
|
+
host: options["host"],
|
26
|
+
threads: options["threads"],
|
27
|
+
verbose: options["verbose"],
|
28
|
+
worker_timeout: options["worker_timeout"],
|
29
|
+
open: options["open"]
|
30
|
+
)
|
31
|
+
end
|
29
32
|
end
|
30
33
|
end
|
31
34
|
end
|
data/lib/mihari/emitters/base.rb
CHANGED
@@ -6,7 +6,20 @@ module Mihari
|
|
6
6
|
include Mixins::Configurable
|
7
7
|
include Mixins::Retriable
|
8
8
|
|
9
|
-
|
9
|
+
# @return [Array<Mihari::Artifact>]
|
10
|
+
attr_reader :artifacts
|
11
|
+
|
12
|
+
# @return [Mihari::Structs::Rule]
|
13
|
+
attr_reader :rule
|
14
|
+
|
15
|
+
#
|
16
|
+
# @param [Array<Mihari::Artifact>] artifacts
|
17
|
+
# @param [Mihari::Structs::Rule] rule
|
18
|
+
# @param [Hash] **_options
|
19
|
+
#
|
20
|
+
def initialize(artifacts:, rule:, **_options)
|
21
|
+
@artifacts = artifacts
|
22
|
+
@rule = rule
|
10
23
|
end
|
11
24
|
|
12
25
|
class << self
|