mihari 5.2.2 → 5.2.3

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