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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77de9f96ff14a64b2ab7a492fff36ed1515df7def7b18b5d81bac6785a5b75c0
4
- data.tar.gz: 9e99189740e4e3b6ee6af97cc09fbd2af1f5395c047df925f528721f2c68595d
3
+ metadata.gz: 2a66fb2d71bcae401062277921a8ade6a3e9e9d961b193a80deacd3a8a934d4c
4
+ data.tar.gz: b4dcccfa58019f819241f8679b5d1ae846002f0a95307cd95856f2b6d04a6dd1
5
5
  SHA512:
6
- metadata.gz: 59171eef265ace4aab9295b9e3866233138194afc0ee691022d5c6ec9f4ca6b53400a01109f9a745cdf54d70cd8c74a1fd39ff319624bf3f3ff04d2ae409ca26
7
- data.tar.gz: a886b191dcab85164e2f7e47d2b9445121254a8b94248ce1f6e54fff31b8e0a13afc6a8dc5013f04cfaac2340957344ba03b3441d09b0018e46bb7a564af4b03
6
+ metadata.gz: e215400c8dce2b864bc26a951ed8ea35757441e7d25bdba6c66632d6716991bad202170f9984d09b7a709f6c9aceb731e5b40396efe621ffc55810a10db45db2
7
+ data.tar.gz: 745522a9cefaed75e5b266a429dea7afc0efe34f26f8591af18e3bc27a0746659ed8d64f6aa64f6edc0f9195acbf26e4a7ee078c052bab760f985da779c4e6e4
@@ -42,7 +42,6 @@ module Mihari
42
42
  #
43
43
  # Search with pagination
44
44
  #
45
- # @param [String] query
46
45
  # @param [Integer] page
47
46
  #
48
47
  # @return [Hash]
@@ -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
- break if cursor.nil?
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.all? { |key| Mihari.config.send(key) } || (id? && secret?)
58
+ configuration_keys? || (id? && secret?)
54
59
  end
55
60
 
56
61
  private
@@ -41,7 +41,7 @@ module Mihari
41
41
  end
42
42
 
43
43
  def configured?
44
- configuration_keys.all? { |key| Mihari.config.send(key) } || (username? && password?)
44
+ configuration_keys? || (username? && password?)
45
45
  end
46
46
 
47
47
  private
@@ -43,7 +43,7 @@ module Mihari
43
43
  end
44
44
 
45
45
  def configured?
46
- configuration_keys.all? { |key| Mihari.config.send(key) } || (username? && api_key?)
46
+ configuration_keys? || (username? && api_key?)
47
47
  end
48
48
 
49
49
  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
- rule.queries.map { |params| run_query(params.deep_dup) }.flatten
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
- Parallel.map(valid_emitters) { |emitter| emit emitter }.compact
115
- end
116
-
117
- #
118
- # Emit an alert
119
- #
120
- # @param [Mihari::Emitters::Base] emitter
121
- #
122
- # @return [Mihari::Alert, nil]
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
- # @param [Hash] params
157
+ # Deep copied queries
169
158
  #
170
- # @return [Array<Mihari::Artifact>]
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 run_query(params)
173
- analyzer_name = params[:analyzer]
174
- klass = get_analyzer_class(analyzer_name)
182
+ def analyzers
183
+ @analyzers ||= queries.map do |params|
184
+ analyzer_name = params[:analyzer]
185
+ klass = get_analyzer_class(analyzer_name)
175
186
 
176
- # set interval in the top level
177
- options = params[:options] || {}
178
- interval = options[:interval]
179
- params[:interval] = interval if interval
187
+ # set interval in the top level
188
+ options = params[:options] || {}
189
+ interval = options[:interval]
190
+ params[:interval] = interval if interval
180
191
 
181
- # set rule
182
- params[:rule] = rule
183
- query = params[:query]
184
- analyzer = klass.new(query, **params)
192
+ # set rule
193
+ params[:rule] = rule
194
+ query = params[:query]
185
195
 
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
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
- # @param [Hash] params
218
+ # Deep copied emitters
207
219
  #
208
- # @return [Mihari::Emitter:Base]
220
+ # @return [Array<Mihari::Emitters::Base>]
209
221
  #
210
- def validate_emitter(params)
211
- name = params[:emitter]
212
- params.delete(:emitter)
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
- emitter.valid? ? emitter : nil
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::Emitter::Base>]
233
+ # @return [Array<Mihari::Emitters::Base>]
222
234
  #
223
235
  def valid_emitters
224
- @valid_emitters ||= rule.emitters.filter_map { |params| validate_emitter(params.deep_dup) }
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
- rule.queries.each do |params|
246
- analyzer_name = params[:analyzer]
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
@@ -25,8 +25,7 @@ module Mihari
25
25
  end
26
26
 
27
27
  def artifacts
28
- responses = search_with_cursor
29
- responses.map(&:to_artifacts).flatten
28
+ search_with_cursor.map(&:to_artifacts).flatten
30
29
  end
31
30
 
32
31
  private
@@ -31,7 +31,7 @@ module Mihari
31
31
 
32
32
  #
33
33
  # @param [String] path
34
- # @param [Hashk, nil] params
34
+ # @param [Hash, nil] params
35
35
  #
36
36
  # @return [String] <description>
37
37
  #
@@ -3,18 +3,19 @@
3
3
  module Mihari
4
4
  module Commands
5
5
  module Database
6
- def self.included(thor)
7
- thor.class_eval do
8
- desc "migrate", "Migrate DB schemas"
9
- method_option :verbose, type: :boolean, default: true
10
- #
11
- # @param [String] direction
12
- #
13
- def migrate(direction = "up")
14
- verbose = options["verbose"]
15
- ActiveRecord::Migration.verbose = verbose
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
- Mihari::Database.with_db_connection { Mihari::Database.migrate(direction.to_sym) }
17
+ Mihari::Database.with_db_connection { Mihari::Database.migrate direction.to_sym }
18
+ end
18
19
  end
19
20
  end
20
21
  end
@@ -5,60 +5,62 @@ require "pathname"
5
5
  module Mihari
6
6
  module Commands
7
7
  module Rule
8
- def self.included(thor)
9
- thor.class_eval do
10
- desc "validate [PATH]", "Validate a rule file"
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
- # @return [Mihari::Structs::Rule]
13
+ # Validate format of a rule
47
14
  #
48
- def rule_template
49
- Structs::Rule.from_path File.expand_path("../templates/rule.yml.erb", __dir__)
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
- # Create a new rule
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 initialize_rule(path, files = Dry::Files.new)
61
- files.write(path, rule_template.yaml)
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
- include Mixins::ErrorNotification
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
- # @param [String] path_or_id
26
+ # @return [Boolean]
16
27
  #
17
- def search(path_or_id)
18
- Mihari::Database.with_db_connection do
19
- rule = Structs::Rule.from_path_or_id path_or_id
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
- begin
22
- rule.validate!
23
- rescue RuleValidationError
24
- return
25
- end
35
+ def update_or_create
36
+ rule.model.save
37
+ end
26
38
 
27
- update_rule rule
28
- run_rule rule
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
- # @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
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
- 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
65
+ begin
66
+ rule.validate!
67
+ rescue RuleValidationError
68
+ return
69
+ end
52
70
 
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"
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
- def self.included(thor)
7
- thor.class_eval do
8
- map %w[--version -v] => :__print_version
6
+ class << self
7
+ def included(thor)
8
+ thor.class_eval do
9
+ map %w[--version -v] => :__print_version
9
10
 
10
- desc "--version, -v", "Print the version"
11
- def __print_version
12
- puts Mihari::VERSION
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
@@ -3,29 +3,32 @@
3
3
  module Mihari
4
4
  module Commands
5
5
  module Web
6
- def self.included(thor)
7
- thor.class_eval do
8
- desc "web", "Launch the web app"
9
- method_option :port, type: :numeric, default: 9292, desc: "Hostname to listen on"
10
- method_option :host, type: :string, default: "localhost", desc: "Port to listen on"
11
- method_option :threads, type: :string, default: "0:5", desc: "min:max threads to use"
12
- method_option :verbose, type: :boolean, default: true, desc: "Report each request"
13
- method_option :worker_timeout, type: :numeric, default: 60, desc: "Worker timeout value (in seconds)"
14
- method_option :hide_config_values, type: :boolean, default: false,
15
- desc: "Whether to hide config values or not"
16
- method_option :open, type: :boolean, default: true, desc: "Whether to open the app in browser or not"
17
- def web
18
- Mihari.config.hide_config_values = options["hide_config_values"]
19
- # set rack env as production
20
- ENV["RACK_ENV"] ||= "production"
21
- Mihari::App.run!(
22
- port: options["port"],
23
- host: options["host"],
24
- threads: options["threads"],
25
- verbose: options["verbose"],
26
- worker_timeout: options["worker_timeout"],
27
- open: options["open"]
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
@@ -6,7 +6,20 @@ module Mihari
6
6
  include Mixins::Configurable
7
7
  include Mixins::Retriable
8
8
 
9
- def initialize(*)
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