mihari 4.5.0 → 4.5.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mihari/analyzers/censys.rb +6 -1
  3. data/lib/mihari/analyzers/rule.rb +6 -3
  4. data/lib/mihari/analyzers/shodan.rb +39 -5
  5. data/lib/mihari/enrichers/ipinfo.rb +1 -1
  6. data/lib/mihari/enrichers/shodan.rb +1 -1
  7. data/lib/mihari/errors.rb +10 -2
  8. data/lib/mihari/http.rb +7 -1
  9. data/lib/mihari/schemas/analyzer.rb +2 -2
  10. data/lib/mihari/schemas/emitter.rb +1 -1
  11. data/lib/mihari/structs/censys.rb +14 -1
  12. data/lib/mihari/structs/rule.rb +3 -0
  13. data/lib/mihari/structs/shodan.rb +2 -0
  14. data/lib/mihari/types.rb +2 -4
  15. data/lib/mihari/version.rb +1 -1
  16. data/lib/mihari/web/app.rb +7 -1
  17. data/lib/mihari/web/endpoints/rules.rb +2 -1
  18. data/lib/mihari/web/public/index.html +1 -1
  19. data/lib/mihari/web/public/redoc-static.html +317 -314
  20. data/lib/mihari/web/public/static/css/{chunk-vendors.da2a7bfc.css → chunk-vendors.06251949.css} +2 -2
  21. data/lib/mihari/web/public/static/fonts/{fa-brands-400.f7223235.ttf → fa-brands-400.7fa789ab.ttf} +0 -0
  22. data/lib/mihari/web/public/static/fonts/fa-brands-400.859fc388.woff2 +0 -0
  23. data/lib/mihari/web/public/static/fonts/fa-regular-400.2ffd018f.woff2 +0 -0
  24. data/lib/mihari/web/public/static/fonts/{fa-regular-400.a7fde52b.ttf → fa-regular-400.da02cb7e.ttf} +0 -0
  25. data/lib/mihari/web/public/static/fonts/{fa-solid-900.5b03221c.ttf → fa-solid-900.3a463ec3.ttf} +0 -0
  26. data/lib/mihari/web/public/static/fonts/fa-solid-900.40ddefd7.woff2 +0 -0
  27. data/lib/mihari/web/public/static/fonts/{fa-v4compatibility.42932bea.ttf → fa-v4compatibility.924588dc.ttf} +0 -0
  28. data/lib/mihari/web/public/static/js/app-legacy.9d5c9c3d.js +2 -0
  29. data/lib/mihari/web/public/static/js/app-legacy.9d5c9c3d.js.map +1 -0
  30. data/lib/mihari/web/public/static/js/app.823b5af7.js +2 -0
  31. data/lib/mihari/web/public/static/js/app.823b5af7.js.map +1 -0
  32. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.b110c129.js +25 -0
  33. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.b110c129.js.map +1 -0
  34. data/lib/mihari/web/public/static/js/chunk-vendors.dde2116c.js +31 -0
  35. data/lib/mihari/web/public/static/js/chunk-vendors.dde2116c.js.map +1 -0
  36. data/mihari.gemspec +7 -8
  37. data/sig/lib/mihari/structs/censys.rbs +8 -0
  38. data/sig/lib/mihari/structs/shodan.rbs +2 -0
  39. metadata +32 -46
  40. data/lib/mihari/web/public/static/fonts/fa-brands-400.edf40f86.woff2 +0 -0
  41. data/lib/mihari/web/public/static/fonts/fa-regular-400.3665ebc7.woff2 +0 -0
  42. data/lib/mihari/web/public/static/fonts/fa-solid-900.0d2abd43.woff2 +0 -0
  43. data/lib/mihari/web/public/static/js/app-legacy.c3595dce.js +0 -2
  44. data/lib/mihari/web/public/static/js/app-legacy.c3595dce.js.map +0 -1
  45. data/lib/mihari/web/public/static/js/app.afd5025f.js +0 -2
  46. data/lib/mihari/web/public/static/js/app.afd5025f.js.map +0 -1
  47. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.d6b76c57.js +0 -25
  48. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.d6b76c57.js.map +0 -1
  49. data/lib/mihari/web/public/static/js/chunk-vendors.3bdbaffb.js +0 -31
  50. data/lib/mihari/web/public/static/js/chunk-vendors.3bdbaffb.js.map +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dfd4bafdc33911546dce3477184426c581b9e3a40a762f32d1944870589ffcc9
4
- data.tar.gz: 8765422118bcc6fe914439b416e6362551c9de0492397ce90c2c5bf7487d982a
3
+ metadata.gz: 02c4e60250ffc8e40e4d0c08fd3721101ab943abdfeb1f73ec267cc345bc0dd2
4
+ data.tar.gz: 5e65063ca7b9cc8c5b9c6d137abe6820abb6b179c3fb8582d1381d583eb27280
5
5
  SHA512:
6
- metadata.gz: a0788f87ab0ef4838243e25ca71496408187add826900d0428af6f4a8e7f81093e81944d809425b2c716c778c35b725f757b656cec046adc2294b4a07d764b4f
7
- data.tar.gz: 3c0c001e91aaec0090daeb117a7595a84172a355d527af0dab7f81dfee557b271697a0fbcf75c8a44d5e33b6fb1ff1f8b82e0c057dff91e85b4fb4a0a30d69ca
6
+ metadata.gz: 142cc9ec86582d37e7e5bd472f1a4f085582441e2204d406fa6ff9d2e94331cbc27513a410c00174950750e066ff9c53b100bd4cfe23039e4cb2be7d8f69ed33
7
+ data.tar.gz: 49a02b02e298b94d8d847d74ea3389a3e873e72e17fdb93d1e5392992be05db73b37f6cea05ead35b04573400b6956e5b34d323008efb679fba60dcf53d6b372
@@ -88,12 +88,17 @@ module Mihari
88
88
  )
89
89
  end
90
90
 
91
+ ports = hit.services.map(&:port).map do |port|
92
+ Port.new(port: port)
93
+ end
94
+
91
95
  Artifact.new(
92
96
  data: hit.ip,
93
97
  source: source,
94
98
  metadata: hit.metadata,
95
99
  autonomous_system: as,
96
- geolocation: geolocation
100
+ geolocation: geolocation,
101
+ ports: ports
97
102
  )
98
103
  end
99
104
 
@@ -127,9 +127,12 @@ module Mihari
127
127
  # @return [Boolean]
128
128
  #
129
129
  def disallowed_data_value?(value)
130
- normalized_disallowed_data_values.any? do |disallowed_data_value|
131
- return value == disallowed_data_value if disallowed_data_value.is_a?(String)
132
- return disallowed_data_value.match?(value) if disallowed_data_value.is_a?(Regexp)
130
+ return true if normalized_disallowed_data_values.include?(value)
131
+
132
+ normalized_disallowed_data_values.select do |disallowed_data_value|
133
+ disallowed_data_value.is_a?(Regexp)
134
+ end.any? do |disallowed_data_value|
135
+ disallowed_data_value.match?(value)
133
136
  end
134
137
  end
135
138
 
@@ -23,10 +23,10 @@ module Mihari
23
23
  return [] unless results || results.empty?
24
24
 
25
25
  results = results.map { |result| Structs::Shodan::Result.from_dynamic!(result) }
26
- results.map do |result|
27
- matches = result.matches || []
28
- matches.map { |match| build_artifact(match, matches) }
29
- end.flatten.uniq(&:data)
26
+ matches = results.map { |result| result.matches || [] }.flatten
27
+
28
+ uniq_matches = matches.uniq(&:ip_str)
29
+ uniq_matches.map { |match| build_artifact(match, matches) }
30
30
  end
31
31
 
32
32
  private
@@ -94,6 +94,30 @@ module Mihari
94
94
  matches.select { |match| match.ip_str == ip }.map(&:metadata)
95
95
  end
96
96
 
97
+ #
98
+ # Collect ports from matches
99
+ #
100
+ # @param [Array<Structs::Shodan::Match>] matches
101
+ # @param [String] ip
102
+ #
103
+ # @return [Array<String>]
104
+ #
105
+ def collect_ports_by_ip(matches, ip)
106
+ matches.select { |match| match.ip_str == ip }.map(&:port)
107
+ end
108
+
109
+ #
110
+ # Collect hostnames from matches
111
+ #
112
+ # @param [Array<Structs::Shodan::Match>] matches
113
+ # @param [String] ip
114
+ #
115
+ # @return [Array<String>]
116
+ #
117
+ def collect_hostnames_by_ip(matches, ip)
118
+ matches.select { |match| match.ip_str == ip }.map(&:hostnames).flatten.uniq
119
+ end
120
+
97
121
  #
98
122
  # Build an artifact from a Shodan search API response
99
123
  #
@@ -116,12 +140,22 @@ module Mihari
116
140
 
117
141
  metadata = collect_metadata_by_ip(matches, match.ip_str)
118
142
 
143
+ ports = collect_ports_by_ip(matches, match.ip_str).map do |port|
144
+ Port.new(port: port)
145
+ end
146
+
147
+ reverse_dns_names = collect_hostnames_by_ip(matches, match.ip_str).map do |name|
148
+ ReverseDnsName.new(name: name)
149
+ end
150
+
119
151
  Artifact.new(
120
152
  data: match.ip_str,
121
153
  source: source,
122
154
  metadata: metadata,
123
155
  autonomous_system: as,
124
- geolocation: geolocation
156
+ geolocation: geolocation,
157
+ ports: ports,
158
+ reverse_dns_names: reverse_dns_names
125
159
  )
126
160
  end
127
161
  end
@@ -39,7 +39,7 @@ module Mihari
39
39
  data = JSON.parse(res.body.to_s)
40
40
 
41
41
  Structs::IPInfo::Response.from_dynamic! data
42
- rescue HttpError
42
+ rescue HTTPError
43
43
  nil
44
44
  end
45
45
  end
@@ -26,7 +26,7 @@ module Mihari
26
26
  data = JSON.parse(res.body.to_s)
27
27
 
28
28
  Structs::Shodan::InternetDBResponse.from_dynamic! data
29
- rescue HttpError
29
+ rescue HTTPError
30
30
  nil
31
31
  end
32
32
  memoize :query
data/lib/mihari/errors.rb CHANGED
@@ -11,11 +11,19 @@ module Mihari
11
11
 
12
12
  class FileNotFoundError < Error; end
13
13
 
14
- class HttpError < Error; end
15
-
16
14
  class FeedParseError < Error; end
17
15
 
18
16
  class RuleValidationError < Error; end
19
17
 
20
18
  class ConfigurationError < Error; end
19
+
20
+ class HTTPError < Error; end
21
+
22
+ class UnsuccessfulStatusCodeError < HTTPError; end
23
+
24
+ class NetworkError < HTTPError; end
25
+
26
+ class TimeoutError < HTTPError; end
27
+
28
+ class SSLError < HTTPError; end
21
29
  end
data/lib/mihari/http.rb CHANGED
@@ -90,11 +90,17 @@ module Mihari
90
90
 
91
91
  unless res.is_a?(Net::HTTPSuccess)
92
92
  code = res.code.to_i
93
- raise HttpError, "Unsuccessful response code returned: #{code}"
93
+ raise UnsuccessfulStatusCodeError, "Unsuccessful response code returned: #{code}"
94
94
  end
95
95
 
96
96
  res
97
97
  end
98
+ rescue Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, EOFError, SocketError, Net::ProtocolError => e
99
+ raise NetworkError, e
100
+ rescue Timeout::Error => e
101
+ raise TimeoutError, e
102
+ rescue OpenSSL::SSL::SSLError => e
103
+ raise SSLError, e
98
104
  end
99
105
  end
100
106
  end
@@ -89,10 +89,10 @@ module Mihari
89
89
  required(:analyzer).value(Types::String.enum("feed"))
90
90
  required(:query).value(:string)
91
91
  required(:selector).value(:string)
92
- optional(:http_request_method).value(Types::HttpRequestMethods).default("GET")
92
+ optional(:http_request_method).value(Types::HTTPRequestMethods).default("GET")
93
93
  optional(:http_request_headers).value(:hash).default({})
94
94
  optional(:http_request_payload).value(:hash).default({})
95
- optional(:http_request_payload_type).value(Types::HttpRequestPayloadTypes)
95
+ optional(:http_request_payload_type).value(Types::HTTPRequestPayloadTypes)
96
96
  optional(:options).hash(AnalyzerOptions)
97
97
  end
98
98
  end
@@ -27,7 +27,7 @@ module Mihari
27
27
  HTTP = Dry::Schema.Params do
28
28
  required(:emitter).value(Types::String.enum("http"))
29
29
  required(:url).value(:string)
30
- optional(:http_request_method).value(Types::HttpRequestMethods).default("POST")
30
+ optional(:http_request_method).value(Types::HTTPRequestMethods).default("POST")
31
31
  optional(:http_request_headers).value(:hash).default({})
32
32
  optional(:template).value(:string)
33
33
  end
@@ -27,11 +27,23 @@ module Mihari
27
27
  end
28
28
  end
29
29
 
30
+ class Service < Dry::Struct
31
+ attribute :port, Types::Integer
32
+
33
+ def self.from_dynamic!(d)
34
+ d = Types::Hash[d]
35
+ new(
36
+ port: d.fetch("port")
37
+ )
38
+ end
39
+ end
40
+
30
41
  class Hit < Dry::Struct
31
42
  attribute :ip, Types::String
32
43
  attribute :location, Location
33
44
  attribute :autonomous_system, AutonomousSystem
34
45
  attribute :metadata, Types::Hash
46
+ attribute :services, Types.Array(Service)
35
47
 
36
48
  def self.from_dynamic!(d)
37
49
  d = Types::Hash[d]
@@ -39,7 +51,8 @@ module Mihari
39
51
  ip: d.fetch("ip"),
40
52
  location: Location.from_dynamic!(d.fetch("location")),
41
53
  autonomous_system: AutonomousSystem.from_dynamic!(d.fetch("autonomous_system")),
42
- metadata: d
54
+ metadata: d,
55
+ services: d.fetch("services", []).map { |x| Service.from_dynamic!(x) }
43
56
  )
44
57
  end
45
58
  end
@@ -36,6 +36,9 @@ module Mihari
36
36
  # @return [Array, nil]
37
37
  attr_reader :errors
38
38
 
39
+ # @return [String]
40
+ attr_writer :id
41
+
39
42
  def initialize(data, yaml)
40
43
  @data = data.deep_symbolize_keys
41
44
  @yaml = yaml
@@ -22,6 +22,7 @@ module Mihari
22
22
  attribute :location, Location
23
23
  attribute :domains, Types.Array(Types::String)
24
24
  attribute :ip_str, Types::String
25
+ attribute :port, Types::Integer
25
26
  attribute :metadata, Types::Hash
26
27
 
27
28
  def self.from_dynamic!(d)
@@ -40,6 +41,7 @@ module Mihari
40
41
  location: Location.from_dynamic!(d.fetch("location")),
41
42
  domains: d.fetch("domains"),
42
43
  ip_str: d.fetch("ip_str"),
44
+ port: d.fetch("port"),
43
45
  metadata: d
44
46
  )
45
47
  end
data/lib/mihari/types.rb CHANGED
@@ -14,10 +14,8 @@ module Mihari
14
14
 
15
15
  DataTypes = Types::String.enum(*ALLOWED_DATA_TYPES)
16
16
 
17
- UrlscanDataTypes = Types::String.enum("ip", "domain", "url")
18
-
19
- HttpRequestMethods = Types::String.enum("GET", "POST")
20
- HttpRequestPayloadTypes = Types::String.enum("application/json", "application/x-www-form-urlencoded")
17
+ HTTPRequestMethods = Types::String.enum("GET", "POST")
18
+ HTTPRequestPayloadTypes = Types::String.enum("application/json", "application/x-www-form-urlencoded")
21
19
 
22
20
  EmitterTypes = Types::String.enum(
23
21
  "database",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "4.5.0"
4
+ VERSION = "4.5.3"
5
5
  end
@@ -42,9 +42,15 @@ module Mihari
42
42
  end.to_app
43
43
  end
44
44
 
45
- def run!(port: 9292, host: "localhost", threads: "0:5", verbose: false)
45
+ def run!(port: 9292, host: "localhost", threads: "1:1", verbose: false)
46
46
  url = "http://#{host}:#{port}"
47
47
 
48
+ # set maximum number of threads to use as PARALLEL_PROCESSOR_COUNT (if it is not set)
49
+ # ref. https://github.com/grosser/parallel#tips
50
+ # TODO: is this the best way?
51
+ _min_thread, max_thread = threads.split(":")
52
+ ENV["PARALLEL_PROCESSOR_COUNT"] = max_thread if ENV["PARALLEL_PROCESSOR_COUNT"].nil?
53
+
48
54
  Rack::Handler::Puma.run(instance, Port: port, Host: host, Threads: threads, Verbose: verbose) do |_launcher|
49
55
  Launchy.open(url) if ENV["RACK_ENV"] != "development"
50
56
  end
@@ -145,6 +145,7 @@ module Mihari
145
145
  end
146
146
 
147
147
  rule = Structs::Rule::Rule.from_yaml(yaml)
148
+ rule.id = id
148
149
 
149
150
  begin
150
151
  rule.validate!
@@ -159,7 +160,7 @@ module Mihari
159
160
  model = rule.to_model
160
161
  model.save
161
162
  rescue ActiveRecord::RecordNotUnique
162
- error!({ message: "ID:#{rule.id} is already registered" }, 400)
163
+ error!({ message: "ID:#{model.id} is already registered" }, 400)
163
164
  end
164
165
 
165
166
  status 201
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="icon" href="/static/favicon.ico"/><title>Mihari</title><script defer="defer" type="module" src="/static/js/chunk-vendors.3bdbaffb.js"></script><script defer="defer" type="module" src="/static/js/app.afd5025f.js"></script><link href="/static/css/chunk-vendors.da2a7bfc.css" rel="stylesheet"><link href="/static/css/app.2a5d3d21.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.d6b76c57.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.c3595dce.js" nomodule></script></head><body><noscript><strong>We're sorry but Mihari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="icon" href="/static/favicon.ico"/><title>Mihari</title><script defer="defer" type="module" src="/static/js/chunk-vendors.dde2116c.js"></script><script defer="defer" type="module" src="/static/js/app.823b5af7.js"></script><link href="/static/css/chunk-vendors.06251949.css" rel="stylesheet"><link href="/static/css/app.2a5d3d21.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.b110c129.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.9d5c9c3d.js" nomodule></script></head><body><noscript><strong>We're sorry but Mihari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>