mihari 5.4.0 → 5.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/package-lock.json +145 -146
  3. data/frontend/package.json +8 -8
  4. data/frontend/src/swagger.yaml +306 -272
  5. data/lib/mihari/analyzers/binaryedge.rb +4 -4
  6. data/lib/mihari/analyzers/censys.rb +2 -2
  7. data/lib/mihari/analyzers/circl.rb +2 -2
  8. data/lib/mihari/analyzers/greynoise.rb +2 -2
  9. data/lib/mihari/analyzers/hunterhow.rb +5 -5
  10. data/lib/mihari/analyzers/onyphe.rb +4 -4
  11. data/lib/mihari/analyzers/otx.rb +2 -2
  12. data/lib/mihari/analyzers/passivetotal.rb +2 -2
  13. data/lib/mihari/analyzers/pulsedive.rb +2 -2
  14. data/lib/mihari/analyzers/rule.rb +13 -12
  15. data/lib/mihari/analyzers/securitytrails.rb +2 -2
  16. data/lib/mihari/analyzers/shodan.rb +4 -4
  17. data/lib/mihari/analyzers/urlscan.rb +2 -2
  18. data/lib/mihari/analyzers/virustotal.rb +2 -2
  19. data/lib/mihari/analyzers/virustotal_intelligence.rb +2 -2
  20. data/lib/mihari/analyzers/zoomeye.rb +4 -4
  21. data/lib/mihari/cli/alert.rb +11 -0
  22. data/lib/mihari/cli/main.rb +6 -1
  23. data/lib/mihari/commands/alert.rb +42 -0
  24. data/lib/mihari/commands/rule.rb +2 -2
  25. data/lib/mihari/commands/search.rb +20 -59
  26. data/lib/mihari/config.rb +2 -2
  27. data/lib/mihari/emitters/base.rb +1 -1
  28. data/lib/mihari/emitters/database.rb +2 -2
  29. data/lib/mihari/errors.rb +23 -2
  30. data/lib/mihari/http.rb +7 -1
  31. data/lib/mihari/schemas/alert.rb +14 -0
  32. data/lib/mihari/services/alert_proxy.rb +106 -0
  33. data/lib/mihari/services/alert_runner.rb +22 -0
  34. data/lib/mihari/services/{rule.rb → rule_proxy.rb} +10 -6
  35. data/lib/mihari/services/rule_runner.rb +49 -0
  36. data/lib/mihari/version.rb +1 -1
  37. data/lib/mihari/web/endpoints/alerts.rb +22 -0
  38. data/lib/mihari/web/endpoints/rules.rb +8 -8
  39. data/lib/mihari/web/public/assets/{index-61dc587c.js → index-4d7eda9f.js} +1 -1
  40. data/lib/mihari/web/public/index.html +1 -1
  41. data/lib/mihari/web/public/redoc-static.html +29 -27
  42. data/lib/mihari.rb +6 -1
  43. data/mihari.gemspec +2 -3
  44. metadata +14 -23
  45. data/Steepfile +0 -31
@@ -30,6 +30,10 @@ module Mihari
30
30
  end.flatten
31
31
  end
32
32
 
33
+ def configuration_keys
34
+ %w[binaryedge_api_key]
35
+ end
36
+
33
37
  private
34
38
 
35
39
  PAGE_SIZE = 20
@@ -69,10 +73,6 @@ module Mihari
69
73
  responses
70
74
  end
71
75
 
72
- def configuration_keys
73
- %w[binaryedge_api_key]
74
- end
75
-
76
76
  #
77
77
  #
78
78
  # @return [Mihari::Clients::BinaryEdge]
@@ -55,8 +55,6 @@ module Mihari
55
55
  configuration_keys? || (id? && secret?)
56
56
  end
57
57
 
58
- private
59
-
60
58
  #
61
59
  # @return [Array<String>]
62
60
  #
@@ -64,6 +62,8 @@ module Mihari
64
62
  %w[censys_id censys_secret]
65
63
  end
66
64
 
65
+ private
66
+
67
67
  #
68
68
  # @return [Mihari::Clients::Censys]
69
69
  #
@@ -44,12 +44,12 @@ module Mihari
44
44
  configuration_keys? || (username? && password?)
45
45
  end
46
46
 
47
- private
48
-
49
47
  def configuration_keys
50
48
  %w[circl_passive_password circl_passive_username]
51
49
  end
52
50
 
51
+ private
52
+
53
53
  def client
54
54
  @client ||= Clients::CIRCL.new(username: username, password: password)
55
55
  end
@@ -23,12 +23,12 @@ module Mihari
23
23
  client.gnql_search(query, size: PAGE_SIZE).to_artifacts
24
24
  end
25
25
 
26
- private
27
-
28
26
  def configuration_keys
29
27
  %w[greynoise_api_key]
30
28
  end
31
29
 
30
+ private
31
+
32
32
  def client
33
33
  @client ||= Clients::GreyNoise.new(api_key: api_key)
34
34
  end
@@ -3,9 +3,6 @@
3
3
  module Mihari
4
4
  module Analyzers
5
5
  class HunterHow < Base
6
- # @return [Integer]
7
- PAGE_SIZE = 100
8
-
9
6
  # @return [String, nil]
10
7
  attr_reader :api_key
11
8
 
@@ -54,12 +51,15 @@ module Mihari
54
51
  artifacts.flatten
55
52
  end
56
53
 
57
- private
58
-
59
54
  def configuration_keys
60
55
  %w[hunterhow_api_key]
61
56
  end
62
57
 
58
+ private
59
+
60
+ # @return [Integer]
61
+ PAGE_SIZE = 100
62
+
63
63
  def client
64
64
  @client ||= Clients::HunterHow.new(api_key: api_key)
65
65
  end
@@ -26,14 +26,14 @@ module Mihari
26
26
  responses.map(&:to_artifacts).flatten
27
27
  end
28
28
 
29
- private
30
-
31
- PAGE_SIZE = 10
32
-
33
29
  def configuration_keys
34
30
  %w[onyphe_api_key]
35
31
  end
36
32
 
33
+ private
34
+
35
+ PAGE_SIZE = 10
36
+
37
37
  def client
38
38
  @client ||= Clients::Onyphe.new(api_key: api_key)
39
39
  end
@@ -35,12 +35,12 @@ module Mihari
35
35
  end
36
36
  end
37
37
 
38
- private
39
-
40
38
  def configuration_keys
41
39
  %w[otx_api_key]
42
40
  end
43
41
 
42
+ private
43
+
44
44
  def client
45
45
  @client ||= Mihari::Clients::OTX.new(api_key: api_key)
46
46
  end
@@ -46,12 +46,12 @@ module Mihari
46
46
  configuration_keys? || (username? && api_key?)
47
47
  end
48
48
 
49
- private
50
-
51
49
  def configuration_keys
52
50
  %w[passivetotal_username passivetotal_api_key]
53
51
  end
54
52
 
53
+ private
54
+
55
55
  def client
56
56
  @client ||= Clients::PassiveTotal.new(username: username, api_key: api_key)
57
57
  end
@@ -40,12 +40,12 @@ module Mihari
40
40
  end
41
41
  end
42
42
 
43
- private
44
-
45
43
  def configuration_keys
46
44
  %w[pulsedive_api_key]
47
45
  end
48
46
 
47
+ private
48
+
49
49
  def client
50
50
  @client ||= Clients::PulseDive.new(api_key: api_key)
51
51
  end
@@ -55,12 +55,15 @@ module Mihari
55
55
  end
56
56
 
57
57
  #
58
- # Returns a list of artifacts matched with queries/analyzers
58
+ # Returns a list of artifacts matched with queries/analyzers (with the rule ID)
59
59
  #
60
60
  # @return [Array<Mihari::Artifact>]
61
61
  #
62
62
  def artifacts
63
- analyzers.flat_map(&:normalized_artifacts)
63
+ analyzers.flat_map(&:normalized_artifacts).map do |artifact|
64
+ artifact.rule_id = rule.id
65
+ artifact
66
+ end
64
67
  end
65
68
 
66
69
  #
@@ -73,14 +76,9 @@ module Mihari
73
76
  # @return [Array<Mihari::Artifact>]
74
77
  #
75
78
  def normalized_artifacts
76
- @normalized_artifacts ||= artifacts.uniq(&:data).select(&:valid?).select do |artifact|
77
- rule.data_types.include? artifact.data_type
78
- end.reject do |artifact|
79
- falsepositive? artifact.data
80
- end.map do |artifact|
81
- artifact.rule_id = rule.id
82
- artifact
83
- end
79
+ valid_artifacts = artifacts.uniq(&:data).select(&:valid?)
80
+ date_type_allowed_artifacts = valid_artifacts.select { |artifact| rule.data_types.include? artifact.data_type }
81
+ date_type_allowed_artifacts.reject { |artifact| falsepositive? artifact.data }
84
82
  end
85
83
 
86
84
  #
@@ -89,7 +87,7 @@ module Mihari
89
87
  # @return [Array<Mihari::Artifact>]
90
88
  #
91
89
  def unique_artifacts
92
- @unique_artifacts ||= normalized_artifacts.select do |artifact|
90
+ normalized_artifacts.select do |artifact|
93
91
  artifact.unique?(base_time: base_time, artifact_lifetime: rule.artifact_lifetime)
94
92
  end
95
93
  end
@@ -217,7 +215,10 @@ module Mihari
217
215
  #
218
216
  def validate_analyzer_configurations
219
217
  analyzers.map do |analyzer|
220
- raise ConfigurationError, "#{analyzer.source} is not configured correctly" unless analyzer.configured?
218
+ next if analyzer.configured?
219
+
220
+ message = "#{analyzer.source} is not configured correctly. #{analyzer.configuration_keys.join(", ")} is/are missing."
221
+ raise ConfigurationError, message
221
222
  end
222
223
  end
223
224
  end
@@ -40,12 +40,12 @@ module Mihari
40
40
  end
41
41
  end
42
42
 
43
- private
44
-
45
43
  def configuration_keys
46
44
  %w[securitytrails_api_key]
47
45
  end
48
46
 
47
+ private
48
+
49
49
  def client
50
50
  @client ||= Clients::SecurityTrails.new(api_key: api_key)
51
51
  end
@@ -24,14 +24,14 @@ module Mihari
24
24
  results.map(&:to_artifacts).flatten.uniq(&:data)
25
25
  end
26
26
 
27
- private
28
-
29
- PAGE_SIZE = 100
30
-
31
27
  def configuration_keys
32
28
  %w[shodan_api_key]
33
29
  end
34
30
 
31
+ private
32
+
33
+ PAGE_SIZE = 100
34
+
35
35
  def client
36
36
  @client ||= Clients::Shodan.new(api_key: api_key)
37
37
  end
@@ -39,12 +39,12 @@ module Mihari
39
39
  end
40
40
  end
41
41
 
42
- private
43
-
44
42
  def configuration_keys
45
43
  %w[urlscan_api_key]
46
44
  end
47
45
 
46
+ private
47
+
48
48
  def client
49
49
  @client ||= Clients::UrlScan.new(api_key: api_key)
50
50
  end
@@ -35,12 +35,12 @@ module Mihari
35
35
  end
36
36
  end
37
37
 
38
- private
39
-
40
38
  def configuration_keys
41
39
  %w[virustotal_api_key]
42
40
  end
43
41
 
42
+ private
43
+
44
44
  def client
45
45
  @client = Clients::VirusTotal.new(api_key: api_key)
46
46
  end
@@ -21,12 +21,12 @@ module Mihari
21
21
  search_with_cursor.map(&:to_artifacts).flatten
22
22
  end
23
23
 
24
- private
25
-
26
24
  def configuration_keys
27
25
  %w[virustotal_api_key]
28
26
  end
29
27
 
28
+ private
29
+
30
30
  #
31
31
  # VT API
32
32
  #
@@ -33,6 +33,10 @@ module Mihari
33
33
  end
34
34
  end
35
35
 
36
+ def configuration_keys
37
+ %w[zoomeye_api_key]
38
+ end
39
+
36
40
  private
37
41
 
38
42
  PAGE_SIZE = 10
@@ -46,10 +50,6 @@ module Mihari
46
50
  %w[host web].include? type
47
51
  end
48
52
 
49
- def configuration_keys
50
- %w[zoomeye_api_key]
51
- end
52
-
53
53
  def client
54
54
  @client ||= Clients::ZoomEye.new(api_key: api_key)
55
55
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mihari/commands/alert"
4
+
5
+ module Mihari
6
+ module CLI
7
+ class Alert < Base
8
+ include Mihari::Commands::Alert
9
+ end
10
+ end
11
+ end
@@ -3,14 +3,16 @@
3
3
  require "thor"
4
4
 
5
5
  # Commands
6
+ require "mihari/commands/alert"
7
+ require "mihari/commands/database"
6
8
  require "mihari/commands/search"
7
9
  require "mihari/commands/version"
8
10
  require "mihari/commands/web"
9
- require "mihari/commands/database"
10
11
 
11
12
  # CLIs
12
13
  require "mihari/cli/base"
13
14
 
15
+ require "mihari/cli/alert"
14
16
  require "mihari/cli/database"
15
17
  require "mihari/cli/rule"
16
18
 
@@ -26,6 +28,9 @@ module Mihari
26
28
 
27
29
  desc "rule", "Sub commands for rule"
28
30
  subcommand "rule", Rule
31
+
32
+ desc "alert", "Sub commands for alert"
33
+ subcommand "alert", Alert
29
34
  end
30
35
  end
31
36
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Commands
5
+ module Alert
6
+ class << self
7
+ def included(thor)
8
+ thor.class_eval do
9
+ desc "add [PATH]", "Add an alert"
10
+ #
11
+ # @param [String] path
12
+ #
13
+ def add(path)
14
+ Mihari::Database.with_db_connection do
15
+ proxy = Mihari::Services::AlertProxy.from_path(path)
16
+ proxy.validate!
17
+
18
+ runner = Mihari::Services::AlertRunner.new(proxy)
19
+
20
+ begin
21
+ alert = runner.run
22
+ rescue ActiveRecord::RecordNotFound => e
23
+ # if there is a ActiveRecord::RecordNotFound, output that error without the stack trace
24
+ Mihari.logger.error e.to_s
25
+ return
26
+ end
27
+
28
+ if alert.nil?
29
+ Mihari.logger.info "There is no new artifact found"
30
+ return
31
+ end
32
+
33
+ data = Mihari::Entities::Alert.represent(alert)
34
+ puts JSON.pretty_generate(data.as_json)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -15,7 +15,7 @@ module Mihari
15
15
  # @param [String] path
16
16
  #
17
17
  def validate(path)
18
- rule = Services::Rule.from_path_or_id(path)
18
+ rule = Services::RuleProxy.from_path_or_id(path)
19
19
 
20
20
  begin
21
21
  rule.validate!
@@ -47,7 +47,7 @@ module Mihari
47
47
  # @return [Mihari::Services::Rule]
48
48
  #
49
49
  def rule_template
50
- Services::Rule.from_path File.expand_path("../templates/rule.yml.erb", __dir__)
50
+ Services::RuleProxy.from_path File.expand_path("../templates/rule.yml.erb", __dir__)
51
51
  end
52
52
 
53
53
  #
@@ -4,60 +4,6 @@ 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
24
-
25
- #
26
- # @return [Boolean]
27
- #
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
34
-
35
- def update_or_create
36
- rule.to_model.save
37
- end
38
-
39
- def run
40
- begin
41
- analyzer = rule.to_analyzer
42
- rescue ConfigurationError => e
43
- # if there is a configuration error, output that error without the stack trace
44
- Mihari.logger.error e.to_s
45
- return
46
- end
47
-
48
- with_error_notification do
49
- alert = analyzer.run
50
- if alert.nil?
51
- Mihari.logger.info "There is no new artifact found"
52
- return
53
- end
54
-
55
- data = Mihari::Entities::Alert.represent(alert)
56
- puts JSON.pretty_generate(data.as_json)
57
- end
58
- end
59
- end
60
-
61
7
  def included(thor)
62
8
  thor.class_eval do
63
9
  desc "search [PATH]", "Search by a rule"
@@ -69,7 +15,7 @@ module Mihari
69
15
  #
70
16
  def search(path_or_id)
71
17
  Mihari::Database.with_db_connection do
72
- rule = Services::Rule.from_path_or_id path_or_id
18
+ rule = Services::RuleProxy.from_path_or_id path_or_id
73
19
 
74
20
  begin
75
21
  rule.validate!
@@ -78,15 +24,30 @@ module Mihari
78
24
  end
79
25
 
80
26
  force_overwrite = options["force_overwrite"] || false
81
- wrapper = RuleWrapper.new(rule, force_overwrite: force_overwrite)
27
+ runner = Services::RuleRunner.new(rule, force_overwrite: force_overwrite)
82
28
 
83
- if wrapper.diff? && !force_overwrite
29
+ if runner.diff? && !force_overwrite
84
30
  message = "There is diff in the rule (#{rule.id}). Are you sure you want to overwrite the rule? (y/n)"
85
31
  return unless yes?(message)
86
32
  end
87
33
 
88
- wrapper.update_or_create
89
- wrapper.run
34
+ runner.update_or_create
35
+
36
+ begin
37
+ alert = runner.run
38
+ rescue ConfigurationError => e
39
+ # if there is a configuration error, output that error without the stack trace
40
+ Mihari.logger.error e.to_s
41
+ return
42
+ end
43
+
44
+ if alert.nil?
45
+ Mihari.logger.info "There is no new artifact found"
46
+ return
47
+ end
48
+
49
+ data = Mihari::Entities::Alert.represent(alert)
50
+ puts JSON.pretty_generate(data.as_json)
90
51
  end
91
52
  end
92
53
  end
data/lib/mihari/config.rb CHANGED
@@ -82,7 +82,7 @@ module Mihari
82
82
  attr_reader :sentry_dsn
83
83
 
84
84
  # @return [Boolean]
85
- attr_reader :hide_config_values
85
+ attr_accessor :hide_config_values
86
86
 
87
87
  # @return [Integer]
88
88
  attr_reader :retry_interval
@@ -146,7 +146,7 @@ module Mihari
146
146
  @retry_times = ENV.fetch("RETRY_TIMES", 3).to_i
147
147
  @retry_interval = ENV.fetch("RETRY_INTERVAL", 5).to_i
148
148
 
149
- @pagination_limit = ENV.fetch("PAGINATION_LIMIT", 1000).to_i
149
+ @pagination_limit = ENV.fetch("PAGINATION_LIMIT", 100).to_i
150
150
  end
151
151
  end
152
152
  end
@@ -14,7 +14,7 @@ module Mihari
14
14
 
15
15
  #
16
16
  # @param [Array<Mihari::Artifact>] artifacts
17
- # @param [Mihari::Services::Rule] rule
17
+ # @param [Mihari::Services::RuleProxy] rule
18
18
  # @param [Hash] **_options
19
19
  #
20
20
  def initialize(artifacts:, rule:, **_options)
@@ -10,10 +10,10 @@ module Mihari
10
10
  #
11
11
  # Create an alert
12
12
  #
13
- # @return [Mihari::Alert]
13
+ # @return [Mihari::Alert, nil]
14
14
  #
15
15
  def emit
16
- return if artifacts.empty?
16
+ return nil if artifacts.empty?
17
17
 
18
18
  tags = rule.tags.filter_map { |name| Tag.find_or_create_by(name: name) }.uniq
19
19
  taggings = tags.map { |tag| Tagging.new(tag_id: tag.id) }
data/lib/mihari/errors.rb CHANGED
@@ -15,17 +15,38 @@ module Mihari
15
15
 
16
16
  class RuleValidationError < Error; end
17
17
 
18
+ class AlertValidationError < Error; end
19
+
18
20
  class YAMLSyntaxError < Error; end
19
21
 
20
22
  class ConfigurationError < Error; end
21
23
 
24
+ # errors for HTTP interactions
22
25
  class HTTPError < Error; end
23
26
 
24
- class StatusCodeError < HTTPError; end
25
-
26
27
  class NetworkError < HTTPError; end
27
28
 
28
29
  class TimeoutError < HTTPError; end
29
30
 
30
31
  class SSLError < HTTPError; end
32
+
33
+ class StatusCodeError < HTTPError
34
+ # @return [Integer]
35
+ attr_reader :status_code
36
+
37
+ # @return [String, nil]
38
+ attr_reader :body
39
+
40
+ #
41
+ # @param [String] msg
42
+ # @param [Integer] status_code
43
+ # @param [String, nil] body
44
+ #
45
+ def initialize(msg, status_code, body)
46
+ super(msg)
47
+
48
+ @status_code = status_code
49
+ @body = body
50
+ end
51
+ end
31
52
  end
data/lib/mihari/http.rb CHANGED
@@ -94,7 +94,13 @@ module Mihari
94
94
  Net::HTTP.start(url.host, url.port, https_options) do |http|
95
95
  res = http.request(req)
96
96
 
97
- raise StatusCodeError, "Unsuccessful response code returned: #{res.code}" unless res.is_a?(Net::HTTPSuccess)
97
+ unless res.is_a?(Net::HTTPSuccess)
98
+ raise StatusCodeError.new(
99
+ "Unsuccessful response code returned: #{res.code}",
100
+ res.code.to_i,
101
+ res.body
102
+ )
103
+ end
98
104
 
99
105
  res
100
106
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Schemas
5
+ Alert = Dry::Schema.Params do
6
+ required(:rule_id).value(:string)
7
+ required(:artifacts).value(array[:string])
8
+ end
9
+
10
+ class AlertContract < Dry::Validation::Contract
11
+ params(Alert)
12
+ end
13
+ end
14
+ end