mihari 5.4.1 → 5.4.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) 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/base.rb +0 -4
  6. data/lib/mihari/analyzers/binaryedge.rb +4 -44
  7. data/lib/mihari/analyzers/censys.rb +4 -20
  8. data/lib/mihari/analyzers/circl.rb +2 -26
  9. data/lib/mihari/analyzers/crtsh.rb +2 -17
  10. data/lib/mihari/analyzers/dnstwister.rb +1 -3
  11. data/lib/mihari/analyzers/greynoise.rb +5 -4
  12. data/lib/mihari/analyzers/hunterhow.rb +8 -23
  13. data/lib/mihari/analyzers/onyphe.rb +5 -39
  14. data/lib/mihari/analyzers/otx.rb +2 -38
  15. data/lib/mihari/analyzers/passivetotal.rb +3 -41
  16. data/lib/mihari/analyzers/securitytrails.rb +3 -41
  17. data/lib/mihari/analyzers/shodan.rb +7 -39
  18. data/lib/mihari/analyzers/urlscan.rb +2 -38
  19. data/lib/mihari/analyzers/virustotal_intelligence.rb +2 -25
  20. data/lib/mihari/analyzers/zoomeye.rb +17 -83
  21. data/lib/mihari/cli/alert.rb +11 -0
  22. data/lib/mihari/cli/main.rb +6 -1
  23. data/lib/mihari/clients/base.rb +9 -1
  24. data/lib/mihari/clients/binaryedge.rb +27 -2
  25. data/lib/mihari/clients/censys.rb +32 -2
  26. data/lib/mihari/clients/circl.rb +28 -1
  27. data/lib/mihari/clients/crtsh.rb +9 -2
  28. data/lib/mihari/clients/dnstwister.rb +4 -2
  29. data/lib/mihari/clients/greynoise.rb +31 -4
  30. data/lib/mihari/clients/hunterhow.rb +41 -3
  31. data/lib/mihari/clients/onyphe.rb +25 -3
  32. data/lib/mihari/clients/otx.rb +40 -0
  33. data/lib/mihari/clients/passivetotal.rb +33 -15
  34. data/lib/mihari/clients/securitytrails.rb +44 -0
  35. data/lib/mihari/clients/shodan.rb +30 -2
  36. data/lib/mihari/clients/urlscan.rb +32 -6
  37. data/lib/mihari/clients/virustotal.rb +29 -4
  38. data/lib/mihari/clients/zoomeye.rb +53 -2
  39. data/lib/mihari/commands/alert.rb +42 -0
  40. data/lib/mihari/commands/rule.rb +2 -2
  41. data/lib/mihari/commands/search.rb +20 -59
  42. data/lib/mihari/commands/web.rb +1 -1
  43. data/lib/mihari/config.rb +2 -2
  44. data/lib/mihari/emitters/base.rb +1 -1
  45. data/lib/mihari/emitters/database.rb +2 -2
  46. data/lib/mihari/errors.rb +23 -2
  47. data/lib/mihari/http.rb +7 -1
  48. data/lib/mihari/schemas/alert.rb +14 -0
  49. data/lib/mihari/services/alert_proxy.rb +106 -0
  50. data/lib/mihari/services/alert_runner.rb +22 -0
  51. data/lib/mihari/services/{rule.rb → rule_proxy.rb} +10 -6
  52. data/lib/mihari/services/rule_runner.rb +49 -0
  53. data/lib/mihari/structs/censys.rb +11 -11
  54. data/lib/mihari/structs/greynoise.rb +17 -8
  55. data/lib/mihari/structs/onyphe.rb +7 -7
  56. data/lib/mihari/structs/shodan.rb +5 -5
  57. data/lib/mihari/structs/urlscan.rb +3 -3
  58. data/lib/mihari/structs/virustotal_intelligence.rb +3 -3
  59. data/lib/mihari/version.rb +1 -1
  60. data/lib/mihari/web/endpoints/alerts.rb +22 -0
  61. data/lib/mihari/web/endpoints/rules.rb +8 -8
  62. data/lib/mihari/web/public/assets/{index-61dc587c.js → index-4d7eda9f.js} +1 -1
  63. data/lib/mihari/web/public/index.html +1 -1
  64. data/lib/mihari/web/public/redoc-static.html +29 -27
  65. data/lib/mihari.rb +6 -1
  66. data/mihari.gemspec +9 -10
  67. metadata +28 -37
  68. data/Steepfile +0 -31
@@ -15,6 +15,50 @@ module Mihari
15
15
  super(base_url, headers: headers)
16
16
  end
17
17
 
18
+ #
19
+ # Domain search
20
+ #
21
+ # @param [String] query
22
+ #
23
+ # @return [Array<String>]
24
+ #
25
+ def domain_search(query)
26
+ records = get_all_dns_history(query, type: "a")
27
+ records.map do |record|
28
+ (record["values"] || []).map { |value| value["ip"] }
29
+ end.flatten.compact.uniq
30
+ end
31
+
32
+ #
33
+ # IP search
34
+ #
35
+ # @param [String] query
36
+ #
37
+ # @return [Array<Mihari::Artifact>]
38
+ #
39
+ def ip_search(query)
40
+ records = search_by_ip(query)
41
+ records.filter_map do |record|
42
+ data = record["hostname"]
43
+ Artifact.new(data: data, metadata: record)
44
+ end
45
+ end
46
+
47
+ #
48
+ # Mail search
49
+ #
50
+ # @param [String] query
51
+ #
52
+ # @return [Array<String>]
53
+ #
54
+ def mail_search(query)
55
+ records = search_by_mail(query)
56
+ records.filter_map do |record|
57
+ data = record["hostname"]
58
+ Artifact.new(data: data, metadata: record)
59
+ end
60
+ end
61
+
18
62
  #
19
63
  # @param [String] mail
20
64
  #
@@ -3,6 +3,8 @@
3
3
  module Mihari
4
4
  module Clients
5
5
  class Shodan < Base
6
+ PAGE_SIZE = 100
7
+
6
8
  # @return [String]
7
9
  attr_reader :api_key
8
10
 
@@ -10,11 +12,12 @@ module Mihari
10
12
  # @param [String] base_url
11
13
  # @param [String, nil] api_key
12
14
  # @param [Hash] headers
15
+ # @param [Integer, nil] interval
13
16
  #
14
- def initialize(base_url = "https://api.shodan.io", api_key:, headers: {})
17
+ def initialize(base_url = "https://api.shodan.io", api_key:, headers: {}, interval: nil)
15
18
  raise(ArgumentError, "'api_key' argument is required") unless api_key
16
19
 
17
- super(base_url, headers: headers)
20
+ super(base_url, headers: headers, interval: interval)
18
21
 
19
22
  @api_key = api_key
20
23
  end
@@ -36,6 +39,31 @@ module Mihari
36
39
  res = get("/shodan/host/search", params: params)
37
40
  Structs::Shodan::Result.from_dynamic! JSON.parse(res.body.to_s)
38
41
  end
42
+
43
+ #
44
+ # @param [String] query
45
+ # @param [Boolean] minify
46
+ # @param [Integer] pagination_limit
47
+ #
48
+ # @return [Enumerable<Structs::Shodan::Result>]
49
+ #
50
+ def search_with_pagination(query, minify: true, pagination_limit: Mihari.config.pagination_limit)
51
+ Enumerator.new do |y|
52
+ (1..pagination_limit).each do |page|
53
+ res = search(query, page: page, minify: minify)
54
+
55
+ y.yield res
56
+
57
+ break if res.total <= page * PAGE_SIZE
58
+
59
+ sleep_interval
60
+ rescue JSON::ParserError
61
+ # ignore JSON::ParserError
62
+ # ref. https://github.com/ninoseki/mihari/issues/197
63
+ next
64
+ end
65
+ end
66
+ end
39
67
  end
40
68
  end
41
69
  end
@@ -7,26 +7,52 @@ module Mihari
7
7
  # @param [String] base_url
8
8
  # @param [String, nil] api_key
9
9
  # @param [Hash] headers
10
+ # @param [Interval, nil] interval
10
11
  #
11
- def initialize(base_url = "https://urlscan.io", api_key:, headers: {})
12
+ def initialize(base_url = "https://urlscan.io", api_key:, headers: {}, interval: nil)
12
13
  raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
13
14
 
14
15
  headers["api-key"] = api_key
15
16
 
16
- super(base_url, headers: headers)
17
+ super(base_url, headers: headers, interval: interval)
17
18
  end
18
19
 
19
20
  #
20
21
  # @param [String] q
21
- # @param [Integer] size
22
+ # @param [Integer, nil] size
22
23
  # @param [String, nil] search_after
23
24
  #
24
- # @return [Hash]
25
+ # @return [Structs::Urlscan::Response]
25
26
  #
26
- def search(q, size: 100, search_after: nil)
27
+ def search(q, size: nil, search_after: nil)
27
28
  params = { q: q, size: size, search_after: search_after }.compact
28
29
  res = get("/api/v1/search/", params: params)
29
- JSON.parse res.body.to_s
30
+ Structs::Urlscan::Response.from_dynamic! JSON.parse(res.body.to_s)
31
+ end
32
+
33
+ #
34
+ # @param [String] q
35
+ # @param [Integer, nil] size
36
+ # @param [Integer] pagination_limit
37
+ #
38
+ # @return [Enumerable<Structs::Urlscan::Response>]
39
+ #
40
+ def search_with_pagination(q, size: nil, pagination_limit: Mihari.config.pagination_limit)
41
+ search_after = nil
42
+
43
+ Enumerator.new do |y|
44
+ pagination_limit.times do
45
+ res = search(q, size: size, search_after: search_after)
46
+
47
+ y.yield res
48
+
49
+ break unless res.has_more
50
+
51
+ search_after = res.results.last.sort.join(",")
52
+
53
+ sleep_interval
54
+ end
55
+ end
30
56
  end
31
57
  end
32
58
  end
@@ -7,13 +7,14 @@ module Mihari
7
7
  # @param [String] base_url
8
8
  # @param [String, nil] api_key
9
9
  # @param [Hash] headers
10
+ # @param [Integer, nil] interval
10
11
  #
11
- def initialize(base_url = "https://www.virustotal.com", api_key:, headers: {})
12
+ def initialize(base_url = "https://www.virustotal.com", api_key:, headers: {}, interval: nil)
12
13
  raise(ArgumentError, "'api_key' argument is required") if api_key.nil?
13
14
 
14
15
  headers["x-apikey"] = api_key
15
16
 
16
- super(base_url, headers: headers)
17
+ super(base_url, headers: headers, interval: interval)
17
18
  end
18
19
 
19
20
  #
@@ -38,11 +39,35 @@ module Mihari
38
39
  # @param [String] query
39
40
  # @param [String, nil] cursor
40
41
  #
41
- # @return [Hash]
42
+ # @return [Structs::VirusTotalIntelligence::Response]
42
43
  #
43
44
  def intel_search(query, cursor: nil)
44
45
  params = { query: query, cursor: cursor }.compact
45
- _get("/api/v3/intelligence/search", params: params)
46
+ res = _get("/api/v3/intelligence/search", params: params)
47
+ Structs::VirusTotalIntelligence::Response.from_dynamic! res
48
+ end
49
+
50
+ #
51
+ # @param [String] query
52
+ # @param [Integer] pagination_limit
53
+ #
54
+ # @return [Enumerable<Structs::VirusTotalIntelligence::Response>]
55
+ #
56
+ def intel_search_with_pagination(query, pagination_limit: Mihari.config.pagination_limit)
57
+ cursor = nil
58
+
59
+ Enumerator.new do |y|
60
+ pagination_limit.times do
61
+ res = intel_search(query, cursor: cursor)
62
+
63
+ y.yield res
64
+
65
+ cursor = res.meta.cursor
66
+ break if cursor.nil?
67
+
68
+ sleep_interval
69
+ end
70
+ end
46
71
  end
47
72
 
48
73
  private
@@ -3,18 +3,21 @@
3
3
  module Mihari
4
4
  module Clients
5
5
  class ZoomEye < Base
6
+ PAGE_SIZE = 10
7
+
6
8
  attr_reader :api_key
7
9
 
8
10
  #
9
11
  # @param [String] base_url
10
12
  # @param [String, nil] api_key
11
13
  # @param [Hash] headers
14
+ # @param [Integer, nil] interval
12
15
  #
13
- def initialize(base_url = "https://api.zoomeye.org", api_key:, headers: {})
16
+ def initialize(base_url = "https://api.zoomeye.org", api_key:, headers: {}, interval: nil)
14
17
  raise(ArgumentError, "'api_key' argument is required") unless api_key
15
18
 
16
19
  headers["api-key"] = api_key
17
- super(base_url, headers: headers)
20
+ super(base_url, headers: headers, interval: interval)
18
21
  end
19
22
 
20
23
  #
@@ -36,6 +39,30 @@ module Mihari
36
39
  _get("/host/search", params: params)
37
40
  end
38
41
 
42
+ #
43
+ # @param [String] query
44
+ # @param [String, nil] facets
45
+ # @param [Integer] pagination_limit
46
+ #
47
+ # @return [Enumerable<Hash>]
48
+ #
49
+ def host_search_with_pagination(query, facets: nil, pagination_limit: Mihari.config.pagination_limit)
50
+ Enumerator.new do |y|
51
+ (1..pagination_limit).each do |page|
52
+ res = host_search(query, facets: facets, page: page)
53
+
54
+ break if res.nil?
55
+
56
+ y.yield res
57
+
58
+ total = res["total"].to_i
59
+ break if total <= page * PAGE_SIZE
60
+
61
+ sleep_interval
62
+ end
63
+ end
64
+ end
65
+
39
66
  #
40
67
  # Search the Web technologies
41
68
  #
@@ -55,6 +82,30 @@ module Mihari
55
82
  _get("/web/search", params: params)
56
83
  end
57
84
 
85
+ #
86
+ # @param [String] query
87
+ # @param [String, nil] facets
88
+ # @param [Integer] pagination_limit
89
+ #
90
+ # @return [Enumerable<Hash>]
91
+ #
92
+ def web_search_with_pagination(query, facets: nil, pagination_limit: Mihari.config.pagination_limit)
93
+ Enumerator.new do |y|
94
+ (1..pagination_limit).each do |page|
95
+ res = web_search(query, facets: facets, page: page)
96
+
97
+ break if res.nil?
98
+
99
+ y.yield res
100
+
101
+ total = res["total"].to_i
102
+ break if total <= page * PAGE_SIZE
103
+
104
+ sleep_interval
105
+ end
106
+ end
107
+ end
108
+
58
109
  private
59
110
 
60
111
  #
@@ -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
@@ -12,7 +12,7 @@ module Mihari
12
12
  method_option :threads, type: :string, default: "0:5", desc: "min:max threads to use"
13
13
  method_option :verbose, type: :boolean, default: true, desc: "Report each request"
14
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,
15
+ method_option :hide_config_values, type: :boolean, default: true,
16
16
  desc: "Whether to hide config values or not"
17
17
  method_option :open, type: :boolean, default: true, desc: "Whether to open the app in browser or not"
18
18
  method_option :rack_env, type: :string, default: "production", desc: "Rack environment"
data/lib/mihari/config.rb CHANGED
@@ -141,12 +141,12 @@ module Mihari
141
141
 
142
142
  @sentry_dsn = ENV.fetch("SENTRY_DSN", nil)
143
143
 
144
- @hide_config_values = ENV.fetch("HIDE_CONFIG_VALUES", false)
144
+ @hide_config_values = ENV.fetch("HIDE_CONFIG_VALUES", true)
145
145
 
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