mihari 5.1.1 → 5.1.2

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.gitmodules +0 -3
  3. data/.rubocop.yml +6 -0
  4. data/README.md +0 -1
  5. data/lib/mihari/analyzers/base.rb +32 -27
  6. data/lib/mihari/analyzers/binaryedge.rb +8 -2
  7. data/lib/mihari/analyzers/censys.rb +7 -49
  8. data/lib/mihari/analyzers/circl.rb +5 -2
  9. data/lib/mihari/analyzers/crtsh.rb +6 -0
  10. data/lib/mihari/analyzers/dnstwister.rb +4 -2
  11. data/lib/mihari/analyzers/feed.rb +21 -0
  12. data/lib/mihari/analyzers/greynoise.rb +5 -28
  13. data/lib/mihari/analyzers/onyphe.rb +8 -33
  14. data/lib/mihari/analyzers/otx.rb +3 -0
  15. data/lib/mihari/analyzers/passivetotal.rb +3 -0
  16. data/lib/mihari/analyzers/pulsedive.rb +3 -0
  17. data/lib/mihari/analyzers/rule.rb +0 -1
  18. data/lib/mihari/analyzers/securitytrails.rb +8 -10
  19. data/lib/mihari/analyzers/shodan.rb +13 -81
  20. data/lib/mihari/analyzers/urlscan.rb +9 -0
  21. data/lib/mihari/analyzers/virustotal.rb +4 -0
  22. data/lib/mihari/analyzers/virustotal_intelligence.rb +8 -2
  23. data/lib/mihari/analyzers/zoomeye.rb +9 -0
  24. data/lib/mihari/clients/binaryedge.rb +5 -0
  25. data/lib/mihari/clients/censys.rb +4 -4
  26. data/lib/mihari/clients/circl.rb +3 -3
  27. data/lib/mihari/clients/greynoise.rb +6 -1
  28. data/lib/mihari/clients/misp.rb +6 -1
  29. data/lib/mihari/clients/onyphe.rb +13 -1
  30. data/lib/mihari/clients/otx.rb +20 -0
  31. data/lib/mihari/clients/passivetotal.rb +6 -2
  32. data/lib/mihari/clients/publsedive.rb +18 -1
  33. data/lib/mihari/clients/securitytrails.rb +94 -0
  34. data/lib/mihari/clients/shodan.rb +14 -3
  35. data/lib/mihari/clients/the_hive.rb +6 -1
  36. data/lib/mihari/clients/urlscan.rb +3 -1
  37. data/lib/mihari/clients/virustotal.rb +9 -3
  38. data/lib/mihari/clients/zoomeye.rb +7 -1
  39. data/lib/mihari/commands/database.rb +1 -6
  40. data/lib/mihari/commands/searcher.rb +1 -2
  41. data/lib/mihari/database.rb +9 -0
  42. data/lib/mihari/structs/censys.rb +62 -0
  43. data/lib/mihari/structs/greynoise.rb +43 -0
  44. data/lib/mihari/structs/onyphe.rb +45 -0
  45. data/lib/mihari/structs/shodan.rb +83 -0
  46. data/lib/mihari/version.rb +1 -1
  47. data/lib/mihari/web/middleware/connection_adapter.rb +1 -3
  48. data/lib/mihari/web/public/assets/{index-63900d73.js → index-7d0fb8c4.js} +2 -2
  49. data/lib/mihari/web/public/index.html +1 -1
  50. data/lib/mihari/web/public/redoc-static.html +2 -2
  51. data/lib/mihari.rb +1 -3
  52. data/mihari.gemspec +2 -3
  53. metadata +9 -25
  54. data/lib/mihari/analyzers/dnpedia.rb +0 -33
  55. data/lib/mihari/clients/dnpedia.rb +0 -64
  56. data/lib/mihari/mixins/database.rb +0 -16
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e19317302956178dc4302543d81a0920018aa384595f91c69dd70110086d575a
4
- data.tar.gz: 80d6314c2df13a4a28ec71a0d4b358e74ab0ee9778d818658a997c1fd821f062
3
+ metadata.gz: 5415ee0f5bb820e8073383e4771589bbbaa39fc313af3aa434c5754f34bb9056
4
+ data.tar.gz: a4da4ce859fa718c900572b865518110f92649698a76ca64c55e38ebad2f9856
5
5
  SHA512:
6
- metadata.gz: b54bffc456bc114a2a8b52e5acbcc3f30d7571124fc9431fbda209bb57910eabe7950d86c50ef750c41d5df604a5aa08c9affb25d434db8a4f53d85ba8ae4921
7
- data.tar.gz: 4bce535d8d6d2573102b0197984854b84628a08ab37855c2a7f7fb1574b701e6b4ca816d3b23b3719d7cde0b7de89753af3b386d43af39e422c7c06b1d65448c
6
+ metadata.gz: c0b95e672f17d5ba0035624b035da278c863dc0b1a0860e3393cd8f001125964270233da7bf5ffb08c9057889b4a51698829de059089ed4d31a0f4fd7d0aa3e8
7
+ data.tar.gz: 30cbba0170104212e9244dfeac820a1a223468bae940719c242778d5bffd95b147f482548aebcc2386713fe43cc0c1b97bf2d71d08d318ad62a4b1a707f3e605
data/.gitmodules CHANGED
@@ -1,3 +0,0 @@
1
- [submodule "vendor/rbs/gem_rbs_collection"]
2
- path = vendor/rbs/gem_rbs_collection
3
- url = https://github.com/ruby/gem_rbs_collection.git
data/.rubocop.yml CHANGED
@@ -2,3 +2,9 @@ Style/HashSyntax:
2
2
  EnforcedShorthandSyntax: either
3
3
  Style/StringLiterals:
4
4
  EnforcedStyle: double_quotes
5
+ Metrics/BlockLength:
6
+ Exclude:
7
+ - "spec/**/*"
8
+ - "*.gemspec"
9
+ Metrics/ClassLength:
10
+ Enabled: false
data/README.md CHANGED
@@ -45,7 +45,6 @@ Mihari supports the following services by default.
45
45
  - [Censys](http://censys.io)
46
46
  - [CIRCL passive DNS](https://www.circl.lu/services/passive-dns/) / [passive SSL](https://www.circl.lu/services/passive-ssl/)
47
47
  - [crt.sh](https://crt.sh/)
48
- - [DN Pedia](https://dnpedia.com/)
49
48
  - [dnstwister](https://dnstwister.report/)
50
49
  - [GreyNoise](https://www.greynoise.io/)
51
50
  - [Onyphe](https://onyphe.io)
@@ -7,7 +7,6 @@ module Mihari
7
7
 
8
8
  option :rule, default: proc {}
9
9
 
10
- include Mixins::AutonomousSystem
11
10
  include Mixins::Configurable
12
11
  include Mixins::Retriable
13
12
 
@@ -20,6 +19,15 @@ module Mihari
20
19
  @base_time = Time.now.utc
21
20
  end
22
21
 
22
+ #
23
+ # Load/overwrite rule
24
+ #
25
+ # @param [String] path_or_id
26
+ #
27
+ def load_rule(path_or_id)
28
+ @rule = Structs::Rule.from_path_or_id path_or_id
29
+ end
30
+
23
31
  # @return [Array<String>, Array<Mihari::Artifact>]
24
32
  def artifacts
25
33
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
@@ -30,35 +38,41 @@ module Mihari
30
38
  self.class.to_s.split("::").last.to_s
31
39
  end
32
40
 
41
+ # @return [String]
42
+ def class_name
43
+ self.class.to_s.split("::").last
44
+ end
45
+
33
46
  #
34
47
  # Set artifacts & run emitters in parallel
35
48
  #
36
49
  # @return [Mihari::Alert, nil]
37
50
  #
38
51
  def run
39
- unless configured?
40
- class_name = self.class.to_s.split("::").last
41
- raise ConfigurationError, "#{class_name} is not configured correctly"
42
- end
43
-
44
- set_enriched_artifacts
45
-
46
- responses = Parallel.map(valid_emitters) do |emitter|
47
- run_emitter emitter
48
- end
52
+ raise ConfigurationError, "#{class_name} is not configured correctly" unless configured?
49
53
 
54
+ alert_or_something = bulk_emit
50
55
  # returns Mihari::Alert created by the database emitter
51
- responses.find { |res| res.is_a?(Mihari::Alert) }
56
+ alert_or_something.find { |res| res.is_a?(Mihari::Alert) }
52
57
  end
53
58
 
54
59
  #
55
- # Run emitter
60
+ # Bulk emit
61
+ #
62
+ # @return [Array<Mihari::Alert>]
63
+ #
64
+ def bulk_emit
65
+ Parallel.map(valid_emitters) { |emitter| emit emitter }.compact
66
+ end
67
+
68
+ #
69
+ # Emit an alert
56
70
  #
57
71
  # @param [Mihari::Emitters::Base] emitter
58
72
  #
59
73
  # @return [Mihari::Alert, nil]
60
74
  #
61
- def run_emitter(emitter)
75
+ def emit(emitter)
62
76
  return if enriched_artifacts.empty?
63
77
 
64
78
  alert_or_something = emitter.run(artifacts: enriched_artifacts, rule: rule)
@@ -80,6 +94,7 @@ module Mihari
80
94
  #
81
95
  # Normalize artifacts
82
96
  # - Convert data (string) into an artifact
97
+ # - Set rule ID
83
98
  # - Reject an invalid artifact
84
99
  # - Uniquefy artifacts by data
85
100
  #
@@ -89,17 +104,16 @@ module Mihari
89
104
  @normalized_artifacts ||= artifacts.compact.sort.map do |artifact|
90
105
  # No need to set data_type manually
91
106
  # It is set automatically in #initialize
92
- artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact, source: source)
93
- end.select(&:valid?).uniq(&:data).map do |artifact|
107
+ artifact = artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact, source: source)
94
108
  artifact.rule_id = rule&.id
95
109
  artifact
96
- end
110
+ end.select(&:valid?).uniq(&:data)
97
111
  end
98
112
 
99
113
  private
100
114
 
101
115
  #
102
- # Uniquefy artifacts
116
+ # Uniquefy artifacts (assure rule level uniqueness)
103
117
  #
104
118
  # @return [Array<Mihari::Artifact>]
105
119
  #
@@ -121,15 +135,6 @@ module Mihari
121
135
  end
122
136
  end
123
137
 
124
- #
125
- # Set enriched artifacts
126
- #
127
- # @return [nil]
128
- #
129
- def set_enriched_artifacts
130
- retry_on_error { enriched_artifacts }
131
- end
132
-
133
138
  #
134
139
  # Select valid emitters
135
140
  #
@@ -10,6 +10,12 @@ module Mihari
10
10
  # @return [String, nil]
11
11
  attr_reader :api_key
12
12
 
13
+ # @return [String]
14
+ attr_reader :query
15
+
16
+ # @return [Integer]
17
+ attr_reader :interval
18
+
13
19
  def initialize(*args, **kwargs)
14
20
  super(*args, **kwargs)
15
21
 
@@ -41,7 +47,7 @@ module Mihari
41
47
  #
42
48
  # @return [Hash]
43
49
  #
44
- def search_with_page(query, page: 1)
50
+ def search_with_page(page: 1)
45
51
  client.search(query, page: page)
46
52
  rescue UnsuccessfulStatusCodeError => e
47
53
  raise RetryableError, e if e.message.include?("Request time limit exceeded")
@@ -57,7 +63,7 @@ module Mihari
57
63
  def search
58
64
  responses = []
59
65
  (1..500).each do |page|
60
- res = search_with_page(query, page: page)
66
+ res = search_with_page(page: page)
61
67
  total = res["total"].to_i
62
68
 
63
69
  responses << res
@@ -13,6 +13,12 @@ module Mihari
13
13
  # @return [String, nil]
14
14
  attr_reader :secret
15
15
 
16
+ # @return [Integer]
17
+ attr_reader :interval
18
+
19
+ # @return [String]
20
+ attr_reader :query
21
+
16
22
  def initialize(*args, **kwargs)
17
23
  super(*args, **kwargs)
18
24
 
@@ -41,10 +47,7 @@ module Mihari
41
47
  cursor = nil
42
48
  loop do
43
49
  response = client.search(query, cursor: cursor)
44
- response = Structs::Censys::Response.from_dynamic!(response)
45
-
46
- artifacts << response_to_artifacts(response)
47
-
50
+ artifacts << response.result.to_artifacts(source)
48
51
  cursor = response.result.links.next
49
52
  break if cursor == ""
50
53
 
@@ -55,51 +58,6 @@ module Mihari
55
58
  artifacts.flatten.uniq(&:data)
56
59
  end
57
60
 
58
- #
59
- # Extract IPv4s from Censys search API response
60
- #
61
- # @param [Structs::Censys::Response] response
62
- #
63
- # @return [Array<String>]
64
- #
65
- def response_to_artifacts(response)
66
- response.result.hits.map { |hit| build_artifact(hit) }
67
- end
68
-
69
- #
70
- # Build an artifact from a Shodan search API response
71
- #
72
- # @param [Structs::Censys::Hit] hit
73
- #
74
- # @return [Artifact]
75
- #
76
- def build_artifact(hit)
77
- as = AutonomousSystem.new(asn: normalize_asn(hit.autonomous_system.asn))
78
-
79
- # sometimes Censys overlooks country
80
- # then set geolocation as nil
81
- geolocation = nil
82
- unless hit.location.country.nil?
83
- geolocation = Geolocation.new(
84
- country: hit.location.country,
85
- country_code: hit.location.country_code
86
- )
87
- end
88
-
89
- ports = hit.services.map(&:port).map do |port|
90
- Port.new(port: port)
91
- end
92
-
93
- Artifact.new(
94
- data: hit.ip,
95
- source: source,
96
- metadata: hit.metadata,
97
- autonomous_system: as,
98
- geolocation: geolocation,
99
- ports: ports
100
- )
101
- end
102
-
103
61
  def configuration_keys
104
62
  %w[censys_id censys_secret]
105
63
  end
@@ -16,6 +16,9 @@ module Mihari
16
16
  # @return [String, nil]
17
17
  attr_reader :password
18
18
 
19
+ # @return [String]
20
+ attr_reader :query
21
+
19
22
  def initialize(*args, **kwargs)
20
23
  super
21
24
 
@@ -66,7 +69,7 @@ module Mihari
66
69
  # @return [Array<String>]
67
70
  #
68
71
  def passive_dns_search
69
- results = client.dns_query(@query)
72
+ results = client.dns_query(query)
70
73
  results.filter_map do |result|
71
74
  type = result["rrtype"]
72
75
  (type == "A") ? result["rdata"] : nil
@@ -79,7 +82,7 @@ module Mihari
79
82
  # @return [Array<String>]
80
83
  #
81
84
  def passive_ssl_search
82
- result = client.ssl_cquery(@query)
85
+ result = client.ssl_cquery(query)
83
86
  seen = result["seen"] || []
84
87
  seen.uniq
85
88
  end
@@ -7,6 +7,12 @@ module Mihari
7
7
 
8
8
  option :exclude_expired, default: proc { true }
9
9
 
10
+ # @return [Boolean]
11
+ attr_reader :exclude_expired
12
+
13
+ # @return [String]
14
+ attr_reader :query
15
+
10
16
  def artifacts
11
17
  results = search
12
18
  results.map do |result|
@@ -7,10 +7,12 @@ module Mihari
7
7
 
8
8
  param :query
9
9
 
10
- option :tags, default: proc { [] }
11
-
10
+ # @return [String]
12
11
  attr_reader :type
13
12
 
13
+ # @return [String]
14
+ attr_reader :query
15
+
14
16
  def initialize(*args, **kwargs)
15
17
  super
16
18
 
@@ -16,6 +16,27 @@ module Mihari
16
16
 
17
17
  option :selector, default: proc { "" }
18
18
 
19
+ # @return [Hash, nil]
20
+ attr_reader :data
21
+
22
+ # @return [Hash, nil]
23
+ attr_reader :json
24
+
25
+ # @return [Hash, nil]
26
+ attr_reader :params
27
+
28
+ # @return [Hash, nil]
29
+ attr_reader :headers
30
+
31
+ # @return [String]
32
+ attr_reader :method
33
+
34
+ # @return [String]
35
+ attr_reader :selector
36
+
37
+ # @return [String]
38
+ attr_reader :query
39
+
19
40
  def artifacts
20
41
  Mihari::Feed::Parser.new(results).parse selector
21
42
  end
@@ -8,6 +8,9 @@ module Mihari
8
8
  # @return [String, nil]
9
9
  attr_reader :api_key
10
10
 
11
+ # @return [String]
12
+ attr_reader :query
13
+
11
14
  def initialize(*args, **kwargs)
12
15
  super(*args, **kwargs)
13
16
 
@@ -15,10 +18,8 @@ module Mihari
15
18
  end
16
19
 
17
20
  def artifacts
18
- res = Structs::GreyNoise::Response.from_dynamic!(search)
19
- res.data.map do |datum|
20
- build_artifact datum
21
- end
21
+ res = search
22
+ res.to_artifacts
22
23
  end
23
24
 
24
25
  private
@@ -41,30 +42,6 @@ module Mihari
41
42
  def search
42
43
  client.gnql_search(query, size: PAGE_SIZE)
43
44
  end
44
-
45
- #
46
- # Build an artifact from a GreyNoise search API response
47
- #
48
- # @param [Structs::GreyNoise::Datum] datum
49
- #
50
- # @return [Artifact]
51
- #
52
- def build_artifact(datum)
53
- as = AutonomousSystem.new(asn: normalize_asn(datum.metadata.asn))
54
-
55
- geolocation = Geolocation.new(
56
- country: datum.metadata.country,
57
- country_code: datum.metadata.country_code
58
- )
59
-
60
- Artifact.new(
61
- data: datum.ip,
62
- source: source,
63
- metadata: datum.metadata_,
64
- autonomous_system: as,
65
- geolocation: geolocation
66
- )
67
- end
68
45
  end
69
46
  end
70
47
  end
@@ -12,6 +12,12 @@ module Mihari
12
12
  # @return [String, nil]
13
13
  attr_reader :api_key
14
14
 
15
+ # @return [String]
16
+ attr_reader :query
17
+
18
+ # @return [Integer]
19
+ attr_reader :interval
20
+
15
21
  def initialize(*args, **kwargs)
16
22
  super(*args, **kwargs)
17
23
 
@@ -22,10 +28,7 @@ module Mihari
22
28
  responses = search
23
29
  return [] unless responses
24
30
 
25
- results = responses.map(&:results).flatten
26
- results.map do |result|
27
- build_artifact result
28
- end
31
+ responses.map { |response| response.to_artifacts(source) }.flatten
29
32
  end
30
33
 
31
34
  private
@@ -49,8 +52,7 @@ module Mihari
49
52
  # @return [Structs::Onyphe::Response]
50
53
  #
51
54
  def search_with_page(query, page: 1)
52
- res = client.datascan(query, page: page)
53
- Structs::Onyphe::Response.from_dynamic!(res)
55
+ client.datascan(query, page: page)
54
56
  end
55
57
 
56
58
  #
@@ -72,33 +74,6 @@ module Mihari
72
74
  end
73
75
  responses
74
76
  end
75
-
76
- #
77
- # Build an artifact from an Onyphe search API result
78
- #
79
- # @param [Structs::Onyphe::Result] result
80
- #
81
- # @return [Artifact]
82
- #
83
- def build_artifact(result)
84
- as = AutonomousSystem.new(asn: normalize_asn(result.asn))
85
-
86
- geolocation = nil
87
- unless result.country_code.nil?
88
- geolocation = Geolocation.new(
89
- country: NormalizeCountry(result.country_code, to: :short),
90
- country_code: result.country_code
91
- )
92
- end
93
-
94
- Artifact.new(
95
- data: result.ip,
96
- source: source,
97
- metadata: result.metadata,
98
- autonomous_system: as,
99
- geolocation: geolocation
100
- )
101
- end
102
77
  end
103
78
  end
104
79
  end
@@ -13,6 +13,9 @@ module Mihari
13
13
  # @return [String, nil]
14
14
  attr_reader :api_key
15
15
 
16
+ # @return [String]
17
+ attr_reader :query
18
+
16
19
  def initialize(*args, **kwargs)
17
20
  super
18
21
 
@@ -16,6 +16,9 @@ module Mihari
16
16
  # @return [String, nil]
17
17
  attr_reader :api_key
18
18
 
19
+ # @return [String]
20
+ attr_reader :query
21
+
19
22
  def initialize(*args, **kwargs)
20
23
  super(*args, **kwargs)
21
24
 
@@ -13,6 +13,9 @@ module Mihari
13
13
  # @return [String, nil]
14
14
  attr_reader :api_key
15
15
 
16
+ # @return [Integer]
17
+ attr_reader :query
18
+
16
19
  def initialize(*args, **kwargs)
17
20
  super
18
21
 
@@ -7,7 +7,6 @@ module Mihari
7
7
  "censys" => Censys,
8
8
  "circl" => CIRCL,
9
9
  "crtsh" => Crtsh,
10
- "dnpedia" => DNPedia,
11
10
  "dnstwister" => DNSTwister,
12
11
  "feed" => Feed,
13
12
  "greynoise" => GreyNoise,
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "securitytrails"
4
-
5
3
  module Mihari
6
4
  module Analyzers
7
5
  class SecurityTrails < Base
@@ -15,6 +13,9 @@ module Mihari
15
13
  # @return [String, nil]
16
14
  attr_reader :api_key
17
15
 
16
+ # @return [String]
17
+ attr_reader :query
18
+
18
19
  def initialize(*args, **kwargs)
19
20
  super
20
21
 
@@ -34,8 +35,8 @@ module Mihari
34
35
  %w[securitytrails_api_key]
35
36
  end
36
37
 
37
- def api
38
- @api ||= ::SecurityTrails::API.new(api_key)
38
+ def client
39
+ @client ||= Clients::SecurityTrails.new(api_key: api_key)
39
40
  end
40
41
 
41
42
  #
@@ -71,8 +72,7 @@ module Mihari
71
72
  # @return [Array<String>]
72
73
  #
73
74
  def domain_search
74
- result = api.history.get_all_dns_history(query, type: "a")
75
- records = result["records"] || []
75
+ records = client.get_all_dns_history(query, type: "a")
76
76
  records.map do |record|
77
77
  (record["values"] || []).map { |value| value["ip"] }
78
78
  end.flatten.compact.uniq
@@ -84,8 +84,7 @@ module Mihari
84
84
  # @return [Array<Mihari::Artifact>]
85
85
  #
86
86
  def ip_search
87
- result = api.domains.search(filter: { ipv4: query })
88
- records = result["records"] || []
87
+ records = client.search_by_ip(query)
89
88
  records.filter_map do |record|
90
89
  data = record["hostname"]
91
90
  Artifact.new(data: data, source: source, metadata: record)
@@ -98,8 +97,7 @@ module Mihari
98
97
  # @return [Array<String>]
99
98
  #
100
99
  def mail_search
101
- result = api.domains.search(filter: { whois_email: query })
102
- records = result["records"] || []
100
+ records = client.search_by_mail(query)
103
101
  records.filter_map do |record|
104
102
  data = record["hostname"]
105
103
  Artifact.new(data: data, source: source, metadata: record)