mihari 5.4.2 → 5.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/package-lock.json +2399 -1504
  3. data/frontend/package.json +22 -22
  4. data/lib/mihari/analyzers/base.rb +25 -14
  5. data/lib/mihari/analyzers/binaryedge.rb +2 -48
  6. data/lib/mihari/analyzers/censys.rb +4 -20
  7. data/lib/mihari/analyzers/circl.rb +3 -27
  8. data/lib/mihari/analyzers/crtsh.rb +2 -17
  9. data/lib/mihari/analyzers/dnstwister.rb +2 -4
  10. data/lib/mihari/analyzers/greynoise.rb +5 -4
  11. data/lib/mihari/analyzers/hunterhow.rb +8 -23
  12. data/lib/mihari/analyzers/onyphe.rb +5 -39
  13. data/lib/mihari/analyzers/otx.rb +3 -39
  14. data/lib/mihari/analyzers/passivetotal.rb +4 -42
  15. data/lib/mihari/analyzers/pulsedive.rb +1 -1
  16. data/lib/mihari/analyzers/rule.rb +18 -13
  17. data/lib/mihari/analyzers/securitytrails.rb +4 -42
  18. data/lib/mihari/analyzers/shodan.rb +7 -39
  19. data/lib/mihari/analyzers/urlscan.rb +3 -39
  20. data/lib/mihari/analyzers/virustotal.rb +1 -1
  21. data/lib/mihari/analyzers/virustotal_intelligence.rb +2 -25
  22. data/lib/mihari/analyzers/zoomeye.rb +18 -84
  23. data/lib/mihari/clients/base.rb +9 -1
  24. data/lib/mihari/clients/binaryedge.rb +26 -4
  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 +7 -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/publsedive.rb +1 -1
  35. data/lib/mihari/clients/securitytrails.rb +44 -0
  36. data/lib/mihari/clients/shodan.rb +31 -3
  37. data/lib/mihari/clients/urlscan.rb +32 -6
  38. data/lib/mihari/clients/virustotal.rb +29 -4
  39. data/lib/mihari/clients/zoomeye.rb +53 -2
  40. data/lib/mihari/commands/alert.rb +42 -13
  41. data/lib/mihari/commands/rule.rb +11 -7
  42. data/lib/mihari/commands/search.rb +54 -22
  43. data/lib/mihari/commands/web.rb +1 -1
  44. data/lib/mihari/config.rb +6 -1
  45. data/lib/mihari/emitters/base.rb +9 -3
  46. data/lib/mihari/emitters/slack.rb +1 -1
  47. data/lib/mihari/enrichers/base.rb +13 -0
  48. data/lib/mihari/enrichers/google_public_dns.rb +16 -1
  49. data/lib/mihari/enrichers/ipinfo.rb +9 -13
  50. data/lib/mihari/enrichers/shodan.rb +1 -2
  51. data/lib/mihari/enrichers/whois.rb +2 -2
  52. data/lib/mihari/errors.rb +16 -10
  53. data/lib/mihari/feed/parser.rb +2 -2
  54. data/lib/mihari/models/artifact.rb +1 -1
  55. data/lib/mihari/models/autonomous_system.rb +11 -5
  56. data/lib/mihari/models/cpe.rb +10 -4
  57. data/lib/mihari/models/dns.rb +11 -16
  58. data/lib/mihari/models/geolocation.rb +11 -5
  59. data/lib/mihari/models/port.rb +10 -4
  60. data/lib/mihari/models/reverse_dns.rb +10 -4
  61. data/lib/mihari/models/whois.rb +4 -1
  62. data/lib/mihari/schemas/analyzer.rb +1 -0
  63. data/lib/mihari/services/alert_builder.rb +43 -0
  64. data/lib/mihari/services/alert_proxy.rb +7 -25
  65. data/lib/mihari/services/alert_runner.rb +9 -0
  66. data/lib/mihari/services/rule_builder.rb +47 -0
  67. data/lib/mihari/services/rule_proxy.rb +5 -61
  68. data/lib/mihari/services/rule_runner.rb +9 -4
  69. data/lib/mihari/structs/binaryedge.rb +89 -0
  70. data/lib/mihari/structs/censys.rb +11 -11
  71. data/lib/mihari/structs/greynoise.rb +17 -8
  72. data/lib/mihari/structs/onyphe.rb +7 -7
  73. data/lib/mihari/structs/shodan.rb +7 -6
  74. data/lib/mihari/structs/urlscan.rb +4 -6
  75. data/lib/mihari/structs/virustotal_intelligence.rb +4 -6
  76. data/lib/mihari/type_checker.rb +1 -1
  77. data/lib/mihari/version.rb +1 -1
  78. data/lib/mihari/web/endpoints/alerts.rb +33 -15
  79. data/lib/mihari/web/endpoints/artifacts.rb +53 -25
  80. data/lib/mihari/web/endpoints/configs.rb +2 -2
  81. data/lib/mihari/web/endpoints/ip_addresses.rb +3 -5
  82. data/lib/mihari/web/endpoints/rules.rb +97 -71
  83. data/lib/mihari/web/endpoints/tags.rb +15 -5
  84. data/lib/mihari/web/public/assets/index-ef33a6cd.js +1738 -0
  85. data/lib/mihari/web/public/index.html +1 -1
  86. data/lib/mihari/web/public/redoc-static.html +419 -382
  87. data/lib/mihari.rb +4 -0
  88. data/mihari.gemspec +10 -9
  89. metadata +38 -21
  90. data/lib/mihari/web/public/assets/index-4d7eda9f.js +0 -1738
@@ -60,9 +60,16 @@ module Mihari
60
60
  # @return [Array<Mihari::Artifact>]
61
61
  #
62
62
  def artifacts
63
- analyzers.flat_map(&:normalized_artifacts).map do |artifact|
64
- artifact.rule_id = rule.id
65
- artifact
63
+ analyzers.flat_map do |analyzer|
64
+ result = analyzer.result
65
+
66
+ raise result.failure if result.failure? && !analyzer.ignore_error?
67
+
68
+ artifacts = result.value!
69
+ artifacts.map do |artifact|
70
+ artifact.rule_id = rule.id
71
+ artifact
72
+ end
66
73
  end
67
74
  end
68
75
 
@@ -113,11 +120,12 @@ module Mihari
113
120
  return [] if enriched_artifacts.empty?
114
121
 
115
122
  Parallel.map(valid_emitters) do |emitter|
116
- emission = emitter.emit
117
- Mihari.logger.info "Emission by #{emitter.class} is succeeded"
118
- emission
119
- rescue StandardError => e
120
- Mihari.logger.info "Emission by #{emitter.class} is failed: #{e}"
123
+ result = emitter.result
124
+
125
+ Mihari.logger.info "Emission by #{emitter.class} is failed: #{result.failure}" if result.failure?
126
+ Mihari.logger.info "Emission by #{emitter.class} is succeeded" if result.success?
127
+
128
+ result.value_or nil
121
129
  end.compact
122
130
  end
123
131
 
@@ -127,9 +135,6 @@ module Mihari
127
135
  # @return [Mihari::Alert, nil]
128
136
  #
129
137
  def run
130
- # memoize enriched artifacts
131
- enriched_artifacts
132
-
133
138
  alert_or_something = bulk_emit
134
139
  # returns Mihari::Alert created by the database emitter
135
140
  alert_or_something.find { |res| res.is_a?(Mihari::Alert) }
@@ -167,7 +172,7 @@ module Mihari
167
172
  # @return [Array<Mihari::Analyzers::Base>]
168
173
  #
169
174
  def analyzers
170
- @analyzers ||= rule.queries.map do |query_params|
175
+ rule.queries.map do |query_params|
171
176
  analyzer_name = query_params[:analyzer]
172
177
  klass = get_analyzer_class(analyzer_name)
173
178
  klass.from_query(query_params)
@@ -207,7 +212,7 @@ module Mihari
207
212
  # @return [Array<Mihari::Emitters::Base>]
208
213
  #
209
214
  def valid_emitters
210
- @valid_emitters ||= emitters.select(&:valid?)
215
+ emitters.select(&:valid?)
211
216
  end
212
217
 
213
218
  #
@@ -30,13 +30,13 @@ module Mihari
30
30
  def artifacts
31
31
  case type
32
32
  when "domain"
33
- domain_search
33
+ client.domain_search query
34
34
  when "ip"
35
- ip_search
35
+ client.ip_search query
36
36
  when "mail"
37
- mail_search
37
+ client.mail_search query
38
38
  else
39
- raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
39
+ raise ValueError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
40
40
  end
41
41
  end
42
42
 
@@ -58,44 +58,6 @@ module Mihari
58
58
  def valid_type?
59
59
  %w[ip domain mail].include? type
60
60
  end
61
-
62
- #
63
- # Domain search
64
- #
65
- # @return [Array<String>]
66
- #
67
- def domain_search
68
- records = client.get_all_dns_history(query, type: "a")
69
- records.map do |record|
70
- (record["values"] || []).map { |value| value["ip"] }
71
- end.flatten.compact.uniq
72
- end
73
-
74
- #
75
- # IP search
76
- #
77
- # @return [Array<Mihari::Artifact>]
78
- #
79
- def ip_search
80
- records = client.search_by_ip(query)
81
- records.filter_map do |record|
82
- data = record["hostname"]
83
- Artifact.new(data: data, source: source, metadata: record)
84
- end
85
- end
86
-
87
- #
88
- # Mail search
89
- #
90
- # @return [Array<String>]
91
- #
92
- def mail_search
93
- records = client.search_by_mail(query)
94
- records.filter_map do |record|
95
- data = record["hostname"]
96
- Artifact.new(data: data, source: source, metadata: record)
97
- end
98
- end
99
61
  end
100
62
  end
101
63
  end
@@ -18,10 +18,10 @@ module Mihari
18
18
  end
19
19
 
20
20
  def artifacts
21
- results = search
22
- return [] if results.empty?
23
-
24
- results.map(&:to_artifacts).flatten.uniq(&:data)
21
+ client.search_with_pagination(
22
+ query,
23
+ pagination_limit: pagination_limit
24
+ ).map(&:artifacts).flatten.uniq(&:data)
25
25
  end
26
26
 
27
27
  def configuration_keys
@@ -30,43 +30,11 @@ module Mihari
30
30
 
31
31
  private
32
32
 
33
- PAGE_SIZE = 100
34
-
35
- def client
36
- @client ||= Clients::Shodan.new(api_key: api_key)
37
- end
38
-
39
- #
40
- # Search with pagination
41
- #
42
- # @param [Integer] page
43
- #
44
- # @return [Structs::Shodan::Result]
45
- #
46
- def search_with_page(page: 1)
47
- client.search(query, page: page)
48
- end
49
-
50
33
  #
51
- # Search
34
+ # @return [Clients::Shodan]
52
35
  #
53
- # @return [Array<Structs::Shodan::Result>]
54
- #
55
- def search
56
- responses = []
57
- (1..pagination_limit).each do |page|
58
- res = search_with_page(page: page)
59
- responses << res
60
- break if res.total <= page * PAGE_SIZE
61
-
62
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
63
- sleep_interval
64
- rescue JSON::ParserError
65
- # ignore JSON::ParserError
66
- # ref. https://github.com/ninoseki/mihari/issues/197
67
- next
68
- end
69
- responses
36
+ def client
37
+ @client ||= Clients::Shodan.new(api_key: api_key, interval: interval)
70
38
  end
71
39
  end
72
40
  end
@@ -4,7 +4,6 @@ module Mihari
4
4
  module Analyzers
5
5
  class Urlscan < Base
6
6
  SUPPORTED_DATA_TYPES = %w[url domain ip].freeze
7
- SIZE = 1000
8
7
 
9
8
  # @return [String, nil]
10
9
  attr_reader :api_key
@@ -26,13 +25,12 @@ module Mihari
26
25
 
27
26
  return if valid_allowed_data_types?
28
27
 
29
- raise InvalidInputError, "allowed_data_types should be any of url, domain and ip."
28
+ raise ValueError, "allowed_data_types should be any of url, domain and ip."
30
29
  end
31
30
 
32
31
  def artifacts
33
- responses = search
34
32
  # @type [Array<Mihari::Artifact>]
35
- artifacts = responses.map { |res| res.to_artifacts }.flatten
33
+ artifacts = client.search_with_pagination(query, pagination_limit: pagination_limit).map(&:artifacts).flatten
36
34
 
37
35
  artifacts.select do |artifact|
38
36
  allowed_data_types.include? artifact.data_type
@@ -46,41 +44,7 @@ module Mihari
46
44
  private
47
45
 
48
46
  def client
49
- @client ||= Clients::UrlScan.new(api_key: api_key)
50
- end
51
-
52
- #
53
- # Search with search_after option
54
- #
55
- # @return [Structs::Urlscan::Response]
56
- #
57
- def search_with_search_after(search_after: nil)
58
- res = client.search(query, size: SIZE, search_after: search_after)
59
- Structs::Urlscan::Response.from_dynamic! res
60
- end
61
-
62
- #
63
- # Search
64
- #
65
- # @return [Array<Structs::Urlscan::Response>]
66
- #
67
- def search
68
- responses = []
69
-
70
- search_after = nil
71
- pagination_limit.times do
72
- res = search_with_search_after(search_after: search_after)
73
- responses << res
74
-
75
- break if res.results.length < SIZE
76
-
77
- search_after = res.results.last.sort.join(",")
78
-
79
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
80
- sleep_interval
81
- end
82
-
83
- responses
47
+ @client ||= Clients::UrlScan.new(api_key: api_key, interval: interval)
84
48
  end
85
49
 
86
50
  #
@@ -31,7 +31,7 @@ module Mihari
31
31
  when "ip"
32
32
  ip_search
33
33
  else
34
- raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
34
+ raise ValueError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
35
35
  end
36
36
  end
37
37
 
@@ -18,7 +18,7 @@ module Mihari
18
18
  end
19
19
 
20
20
  def artifacts
21
- search_with_cursor.map(&:to_artifacts).flatten
21
+ client.intel_search_with_pagination(query, pagination_limit: pagination_limit).map(&:artifacts).flatten
22
22
  end
23
23
 
24
24
  def configuration_keys
@@ -33,30 +33,7 @@ module Mihari
33
33
  # @return [::VirusTotal::API]
34
34
  #
35
35
  def client
36
- @client = Clients::VirusTotal.new(api_key: api_key)
37
- end
38
-
39
- #
40
- # Search with cursor
41
- #
42
- # @return [Array<Structs::VirusTotalIntelligence::Response>]
43
- #
44
- def search_with_cursor
45
- cursor = nil
46
- responses = []
47
-
48
- pagination_limit.times do
49
- response = Structs::VirusTotalIntelligence::Response.from_dynamic!(client.intel_search(query,
50
- cursor: cursor))
51
- responses << response
52
- break if response.meta.cursor.nil?
53
-
54
- cursor = response.meta.cursor
55
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
56
- sleep_interval
57
- end
58
-
59
- responses
36
+ @client = Clients::VirusTotal.new(api_key: api_key, interval: interval)
60
37
  end
61
38
  end
62
39
  end
@@ -25,11 +25,15 @@ module Mihari
25
25
  def artifacts
26
26
  case type
27
27
  when "host"
28
- host_search
28
+ client.host_search_with_pagination(query).map do |res|
29
+ convert(res)
30
+ end.flatten
29
31
  when "web"
30
- web_search
32
+ client.web_search_with_pagination(query).map do |res|
33
+ convert(res)
34
+ end.flatten
31
35
  else
32
- raise InvalidInputError, "#{type} type is not supported." unless valid_type?
36
+ raise ValueError, "#{type} type is not supported." unless valid_type?
33
37
  end
34
38
  end
35
39
 
@@ -39,8 +43,6 @@ module Mihari
39
43
 
40
44
  private
41
45
 
42
- PAGE_SIZE = 10
43
-
44
46
  #
45
47
  # Check whether a type is valid or not
46
48
  #
@@ -51,95 +53,27 @@ module Mihari
51
53
  end
52
54
 
53
55
  def client
54
- @client ||= Clients::ZoomEye.new(api_key: api_key)
56
+ @client ||= Clients::ZoomEye.new(api_key: api_key, interval: interval)
55
57
  end
56
58
 
57
59
  #
58
60
  # Convert responses into an array of String
59
61
  #
60
- # @param [Array<Hash>] responses
62
+ # @param [Hash] response
61
63
  #
62
64
  # @return [Array<Mihari::Artifact>]
63
65
  #
64
- def convert_responses(responses)
65
- responses.map do |res|
66
- matches = res["matches"] || []
67
- matches.map do |match|
68
- data = match["ip"]
66
+ def convert(res)
67
+ matches = res["matches"] || []
68
+ matches.map do |match|
69
+ data = match["ip"]
69
70
 
70
- if data.is_a?(Array)
71
- data.map { |d| Artifact.new(data: d, source: source, metadata: match) }
72
- else
73
- Artifact.new(data: data, source: source, metadata: match)
74
- end
71
+ if data.is_a?(Array)
72
+ data.map { |d| Artifact.new(data: d, source: source, metadata: match) }
73
+ else
74
+ Artifact.new(data: data, source: source, metadata: match)
75
75
  end
76
- end.flatten.compact.uniq
77
- end
78
-
79
- #
80
- # Host search
81
- #
82
- # @param [String] query
83
- # @param [Integer] page
84
- #
85
- # @return [Hash, nil]
86
- #
87
- def _host_search(query, page: 1)
88
- client.host_search(query, page: page)
89
- end
90
-
91
- #
92
- # Host search
93
- #
94
- # @return [Array<String>]
95
- #
96
- def host_search
97
- responses = []
98
- (1..pagination_limit).each do |page|
99
- res = _host_search(query, page: page)
100
- break unless res
101
-
102
- total = res["total"].to_i
103
- responses << res
104
- break if total <= page * PAGE_SIZE
105
-
106
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
107
- sleep_interval
108
- end
109
- convert_responses responses.compact
110
- end
111
-
112
- #
113
- # Web search
114
- #
115
- # @param [String] query
116
- # @param [Integer] page
117
- #
118
- # @return [Hash, nil]
119
- #
120
- def _web_search(query, page: 1)
121
- client.web_search(query, page: page)
122
- end
123
-
124
- #
125
- # Web search
126
- #
127
- # @return [Array<String>]
128
- #
129
- def web_search
130
- responses = []
131
- (1..pagination_limit).each do |page|
132
- res = _web_search(query, page: page)
133
- break unless res
134
-
135
- total = res["total"].to_i
136
- responses << res
137
- break if total <= page * PAGE_SIZE
138
-
139
- # sleep #{interval} seconds to avoid the rate limitation (if it is set)
140
- sleep_interval
141
- end
142
- convert_responses responses.compact
76
+ end.flatten
143
77
  end
144
78
  end
145
79
  end
@@ -9,17 +9,25 @@ module Mihari
9
9
  # @return [Hash]
10
10
  attr_reader :headers
11
11
 
12
+ # @return [Integer, nil]
13
+ attr_reader :interval
14
+
12
15
  #
13
16
  # @param [String] base_url
14
17
  # @param [Hash] headers
15
18
  #
16
- def initialize(base_url, headers: {})
19
+ def initialize(base_url, headers: {}, interval: nil)
17
20
  @base_url = base_url
18
21
  @headers = headers || {}
22
+ @interval = interval
19
23
  end
20
24
 
21
25
  private
22
26
 
27
+ def sleep_interval
28
+ sleep(interval) if interval
29
+ end
30
+
23
31
  #
24
32
  # @param [String] path
25
33
  #
@@ -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://api.binaryedge.io/v2", api_key:, headers: {})
12
+ def initialize(base_url = "https://api.binaryedge.io/v2", api_key:, headers: {}, interval: nil)
12
13
  raise(ArgumentError, "'api_key' argument is required") unless api_key
13
14
 
14
15
  headers["x-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
  #
@@ -21,7 +22,7 @@ module Mihari
21
22
  # @param [Integer] page Default 1, Maximum: 500
22
23
  # @param [Integer, nil] only_ips If selected, only output IP addresses, ports and protocols.
23
24
  #
24
- # @return [Hash]
25
+ # @return [Structs::BinaryEdge::Response]
25
26
  #
26
27
  def search(query, page: 1, only_ips: nil)
27
28
  params = {
@@ -31,7 +32,28 @@ module Mihari
31
32
  }.compact
32
33
 
33
34
  res = get("/query/search", params: params)
34
- JSON.parse(res.body.to_s)
35
+ Structs::BinaryEdge::Response.from_dynamic! JSON.parse(res.body.to_s)
36
+ end
37
+
38
+ #
39
+ # @param [String] query
40
+ # @param [Integer, nil] only_ips
41
+ # @param [Integer] pagination_limit
42
+ #
43
+ # @return [Enumerable<Structs::BinaryEdge::Response.>]
44
+ #
45
+ def search_with_pagination(query, only_ips: nil, pagination_limit: Mihari.config.pagination_limit)
46
+ Enumerator.new do |y|
47
+ (1..pagination_limit).each do |page|
48
+ res = search(query, page: page, only_ips: only_ips)
49
+
50
+ y.yield res
51
+
52
+ break if res.events.length < res.pagesize
53
+
54
+ sleep_interval
55
+ end
56
+ end
35
57
  end
36
58
  end
37
59
  end
@@ -10,14 +10,15 @@ module Mihari
10
10
  # @param [String, nil] id
11
11
  # @param [String, nil] secret
12
12
  # @param [Hash] headers
13
+ # @param [Integer, nil] interval
13
14
  #
14
- def initialize(base_url = "https://search.censys.io", id:, secret:, headers: {})
15
+ def initialize(base_url = "https://search.censys.io", id:, secret:, headers: {}, interval: nil)
15
16
  raise(ArgumentError, "'id' argument is required") if id.nil?
16
17
  raise(ArgumentError, "'secret' argument is required") if secret.nil?
17
18
 
18
19
  headers["authorization"] = "Basic #{Base64.strict_encode64("#{id}:#{secret}")}"
19
20
 
20
- super(base_url, headers: headers)
21
+ super(base_url, headers: headers, interval: interval)
21
22
  end
22
23
 
23
24
  #
@@ -37,6 +38,35 @@ module Mihari
37
38
  res = get("/api/v2/hosts/search", params: params)
38
39
  Structs::Censys::Response.from_dynamic! JSON.parse(res.body.to_s)
39
40
  end
41
+
42
+ #
43
+ # @param [String] query
44
+ # @param [Integer, nil] per_page
45
+ # @param [Integer] pagination_limit
46
+ #
47
+ # @return [Enumerable<Structs::Censys::Response>]
48
+ #
49
+ def search_with_pagination(query, per_page: nil, pagination_limit: Mihari.config.pagination_limit)
50
+ cursor = nil
51
+
52
+ Enumerator.new do |y|
53
+ pagination_limit.times do
54
+ res = search(query, per_page: per_page, cursor: cursor)
55
+
56
+ y.yield res
57
+
58
+ cursor = res.result.links.next
59
+ # NOTE: Censys's search API is unstable recently
60
+ # it may returns empty links or empty string cursors
61
+ # - Empty links: "links": {}
62
+ # - Empty cursors: "links": { "next": "", "prev": "" }
63
+ # So it needs to check both cases
64
+ break if cursor.nil? || cursor.empty?
65
+
66
+ sleep_interval
67
+ end
68
+ end
69
+ end
40
70
  end
41
71
  end
42
72
  end
@@ -20,6 +20,34 @@ module Mihari
20
20
  super(base_url, headers: headers)
21
21
  end
22
22
 
23
+ #
24
+ # Passive DNS search
25
+ #
26
+ # @param [String] query
27
+ #
28
+ # @return [Array<String>]
29
+ #
30
+ def passive_dns_search(query)
31
+ results = dns_query(query)
32
+ results.filter_map do |result|
33
+ type = result["rrtype"]
34
+ (type == "A") ? result["rdata"] : nil
35
+ end.uniq
36
+ end
37
+
38
+ #
39
+ # Passive SSL search
40
+ #
41
+ # @param [String] query
42
+ #
43
+ # @return [Array<String>]
44
+ #
45
+ def passive_ssl_search(query)
46
+ result = ssl_cquery(query)
47
+ seen = result["seen"] || []
48
+ seen.uniq
49
+ end
50
+
23
51
  #
24
52
  # @param [String] query
25
53
  #
@@ -40,7 +68,6 @@ module Mihari
40
68
 
41
69
  private
42
70
 
43
- #
44
71
  #
45
72
  # @param [String] path
46
73
  # @param [Hash] params
@@ -18,13 +18,18 @@ module Mihari
18
18
  # @param [String, nil] match "=", "ILIKE", "LIKE", "single", "any" or nil
19
19
  # @param [String, nil] exclude "expired" or nil
20
20
  #
21
- # @return [Array<Hash>]
21
+ # @return [Array<Mihari::Artifact>]
22
22
  #
23
23
  def search(identity, match: nil, exclude: nil)
24
24
  params = { identity: identity, match: match, exclude: exclude, output: "json" }.compact
25
25
 
26
26
  res = get("/", params: params)
27
- JSON.parse(res.body.to_s)
27
+ parsed = JSON.parse(res.body.to_s)
28
+
29
+ parsed.map do |result|
30
+ values = result["name_value"].to_s.lines.map(&:chomp)
31
+ values.map { |value| Artifact.new(data: value, metadata: result) }
32
+ end.flatten
28
33
  end
29
34
  end
30
35
  end
@@ -16,11 +16,13 @@ module Mihari
16
16
  #
17
17
  # @param [String] domain
18
18
  #
19
- # @return [Hash]
19
+ # @return [Array<String>]
20
20
  #
21
21
  def fuzz(domain)
22
22
  res = get("/api/fuzz/#{to_hex(domain)}")
23
- JSON.parse(res.body.to_s)
23
+ res = JSON.parse(res.body.to_s)
24
+ fuzzy_domains = res["fuzzy_domains"] || []
25
+ fuzzy_domains.map { |d| d["domain"] }
24
26
  end
25
27
 
26
28
  private