mihari 2.4.0 → 3.0.0

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 (106) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/.overcommit.yml +12 -0
  4. data/README.md +1 -9
  5. data/exe/mihari +1 -1
  6. data/lib/mihari.rb +88 -15
  7. data/lib/mihari/analyzers/base.rb +49 -8
  8. data/lib/mihari/analyzers/basic.rb +1 -2
  9. data/lib/mihari/analyzers/binaryedge.rb +7 -13
  10. data/lib/mihari/analyzers/censys.rb +26 -63
  11. data/lib/mihari/analyzers/circl.rb +20 -17
  12. data/lib/mihari/analyzers/crtsh.rb +6 -13
  13. data/lib/mihari/analyzers/dnpedia.rb +6 -12
  14. data/lib/mihari/analyzers/dnstwister.rb +13 -10
  15. data/lib/mihari/analyzers/onyphe.rb +6 -12
  16. data/lib/mihari/analyzers/otx.rb +22 -19
  17. data/lib/mihari/analyzers/passivetotal.rb +22 -21
  18. data/lib/mihari/analyzers/pulsedive.rb +16 -13
  19. data/lib/mihari/analyzers/rule.rb +99 -0
  20. data/lib/mihari/analyzers/securitytrails.rb +22 -19
  21. data/lib/mihari/analyzers/shodan.rb +7 -13
  22. data/lib/mihari/analyzers/spyse.rb +12 -19
  23. data/lib/mihari/analyzers/urlscan.rb +22 -27
  24. data/lib/mihari/analyzers/virustotal.rb +25 -22
  25. data/lib/mihari/analyzers/zoomeye.rb +14 -20
  26. data/lib/mihari/cli/analyzer.rb +44 -0
  27. data/lib/mihari/cli/base.rb +27 -0
  28. data/lib/mihari/cli/init.rb +13 -0
  29. data/lib/mihari/cli/main.rb +30 -0
  30. data/lib/mihari/cli/mixins/utils.rb +88 -0
  31. data/lib/mihari/cli/validator.rb +11 -0
  32. data/lib/mihari/commands/binaryedge.rb +1 -1
  33. data/lib/mihari/commands/censys.rb +1 -1
  34. data/lib/mihari/commands/circl.rb +2 -2
  35. data/lib/mihari/commands/crtsh.rb +1 -1
  36. data/lib/mihari/commands/dnpedia.rb +1 -1
  37. data/lib/mihari/commands/dnstwister.rb +2 -2
  38. data/lib/mihari/commands/init.rb +46 -0
  39. data/lib/mihari/commands/json.rb +1 -1
  40. data/lib/mihari/commands/onyphe.rb +1 -1
  41. data/lib/mihari/commands/otx.rb +2 -2
  42. data/lib/mihari/commands/passivetotal.rb +2 -2
  43. data/lib/mihari/commands/pulsedive.rb +2 -2
  44. data/lib/mihari/commands/search.rb +77 -0
  45. data/lib/mihari/commands/securitytrails.rb +2 -2
  46. data/lib/mihari/commands/shodan.rb +1 -1
  47. data/lib/mihari/commands/spyse.rb +1 -1
  48. data/lib/mihari/commands/urlscan.rb +2 -2
  49. data/lib/mihari/commands/validator.rb +38 -0
  50. data/lib/mihari/commands/virustotal.rb +2 -2
  51. data/lib/mihari/commands/zoomeye.rb +1 -1
  52. data/lib/mihari/constraints.rb +5 -0
  53. data/lib/mihari/database.rb +13 -2
  54. data/lib/mihari/emitters/base.rb +2 -2
  55. data/lib/mihari/emitters/database.rb +1 -1
  56. data/lib/mihari/emitters/misp.rb +1 -1
  57. data/lib/mihari/emitters/slack.rb +5 -6
  58. data/lib/mihari/emitters/the_hive.rb +1 -1
  59. data/lib/mihari/emitters/webhook.rb +2 -9
  60. data/lib/mihari/mixins/configurable.rb +38 -0
  61. data/lib/mihari/mixins/configuration.rb +85 -0
  62. data/lib/mihari/mixins/hash.rb +20 -0
  63. data/lib/mihari/mixins/refang.rb +21 -0
  64. data/lib/mihari/mixins/retriable.rb +27 -0
  65. data/lib/mihari/mixins/rule.rb +79 -0
  66. data/lib/mihari/models/alert.rb +28 -1
  67. data/lib/mihari/models/artifact.rb +10 -0
  68. data/lib/mihari/notifiers/base.rb +9 -1
  69. data/lib/mihari/notifiers/exception_notifier.rb +50 -0
  70. data/lib/mihari/notifiers/slack.rb +29 -0
  71. data/lib/mihari/schemas/configuration.rb +42 -0
  72. data/lib/mihari/schemas/macros.rb +17 -0
  73. data/lib/mihari/schemas/rule.rb +72 -0
  74. data/lib/mihari/serializers/artifact.rb +1 -1
  75. data/lib/mihari/status.rb +14 -0
  76. data/lib/mihari/templates/rule.yml.erb +19 -0
  77. data/lib/mihari/type_checker.rb +8 -3
  78. data/lib/mihari/version.rb +1 -1
  79. data/lib/mihari/web/controllers/base_controller.rb +1 -1
  80. data/lib/mihari/web/public/index.html +1 -21
  81. data/lib/mihari/web/public/redoc-static.html +2 -2
  82. data/lib/mihari/web/public/static/js/app.ab213f7c.js +12 -0
  83. data/lib/mihari/web/public/static/js/app.ab213f7c.js.map +1 -0
  84. data/mihari.gemspec +12 -5
  85. metadata +123 -50
  86. data/.rubocop.yml +0 -161
  87. data/lib/mihari/analyzers/free_text.rb +0 -48
  88. data/lib/mihari/analyzers/http_hash.rb +0 -100
  89. data/lib/mihari/analyzers/passive_dns.rb +0 -59
  90. data/lib/mihari/analyzers/passive_ssl.rb +0 -55
  91. data/lib/mihari/analyzers/reverse_whois.rb +0 -55
  92. data/lib/mihari/analyzers/securitytrails_domain_feed.rb +0 -59
  93. data/lib/mihari/analyzers/ssh_fingerprint.rb +0 -58
  94. data/lib/mihari/cli.rb +0 -126
  95. data/lib/mihari/commands/config.rb +0 -27
  96. data/lib/mihari/commands/free_text.rb +0 -21
  97. data/lib/mihari/commands/http_hash.rb +0 -25
  98. data/lib/mihari/commands/passive_dns.rb +0 -21
  99. data/lib/mihari/commands/passive_ssl.rb +0 -21
  100. data/lib/mihari/commands/reverse_whois.rb +0 -21
  101. data/lib/mihari/commands/securitytrails_domain_feed.rb +0 -23
  102. data/lib/mihari/commands/ssh_fingerprint.rb +0 -21
  103. data/lib/mihari/config.rb +0 -85
  104. data/lib/mihari/configurable.rb +0 -21
  105. data/lib/mihari/html.rb +0 -43
  106. data/lib/mihari/retriable.rb +0 -17
@@ -5,26 +5,29 @@ require "passive_circl"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class CIRCL < Base
8
- attr_reader :title, :description, :tags
8
+ include Mixins::Refang
9
9
 
10
- def initialize(query, title: nil, description: nil, tags: [])
11
- super()
10
+ param :query
11
+ option :title, default: proc { "CIRCL passive DNS/SSL search" }
12
+ option :description, default: proc { "query = #{query}" }
13
+ option :tags, default: proc { [] }
12
14
 
13
- @query = query
14
- @type = TypeChecker.type(query)
15
+ attr_reader :type
16
+
17
+ def initialize(*args, **kwargs)
18
+ super
15
19
 
16
- @title = title || "CIRCL passive lookup"
17
- @description = description || "query = #{query}"
18
- @tags = tags
20
+ @query = refang(query)
21
+ @type = TypeChecker.type(query)
19
22
  end
20
23
 
21
24
  def artifacts
22
- lookup || []
25
+ search || []
23
26
  end
24
27
 
25
28
  private
26
29
 
27
- def config_keys
30
+ def configuration_keys
28
31
  %w[circl_passive_password circl_passive_username]
29
32
  end
30
33
 
@@ -32,26 +35,26 @@ module Mihari
32
35
  @api ||= ::PassiveCIRCL::API.new(username: Mihari.config.circl_passive_username, password: Mihari.config.circl_passive_password)
33
36
  end
34
37
 
35
- def lookup
38
+ def search
36
39
  case @type
37
40
  when "domain"
38
- passive_dns_lookup
41
+ passive_dns_search
39
42
  when "hash"
40
- passive_ssl_lookup
43
+ passive_ssl_search
41
44
  else
42
45
  raise InvalidInputError, "#{@query}(type: #{@type || "unknown"}) is not supported."
43
46
  end
44
47
  end
45
48
 
46
- def passive_dns_lookup
49
+ def passive_dns_search
47
50
  results = api.dns.query(@query)
48
- results.map do |result|
51
+ results.filter_map do |result|
49
52
  type = result["rrtype"]
50
53
  type == "A" ? result["rdata"] : nil
51
- end.compact.uniq
54
+ end.uniq
52
55
  end
53
56
 
54
- def passive_ssl_lookup
57
+ def passive_ssl_search
55
58
  result = api.ssl.cquery(@query)
56
59
  seen = result["seen"] || []
57
60
  seen.uniq
@@ -5,22 +5,15 @@ require "crtsh"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class Crtsh < Base
8
- attr_reader :title, :description, :query, :tags, :exclude_expired
9
-
10
- def initialize(query, title: nil, description: nil, tags: [], exclude_expired: nil)
11
- super()
12
-
13
- @query = query
14
- @title = title || "crt.sh lookup"
15
- @description = description || "query = #{query}"
16
- @tags = tags
17
-
18
- @exclude_expired = exclude_expired.nil? ? true : exclude_expired
19
- end
8
+ param :query
9
+ option :title, default: proc { "crt.sh search" }
10
+ option :description, default: proc { "query = #{query}" }
11
+ option :tags, default: proc { [] }
12
+ option :exclude_expired, default: proc { true }
20
13
 
21
14
  def artifacts
22
15
  results = search
23
- name_values = results.map { |result| result["name_value"] }.compact
16
+ name_values = results.filter_map { |result| result["name_value"] }
24
17
  name_values.map(&:lines).flatten.uniq.map(&:chomp)
25
18
  end
26
19
 
@@ -5,19 +5,13 @@ require "dnpedia"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class DNPedia < Base
8
- attr_reader :query, :title, :description, :tags
9
-
10
- def initialize(query, title: nil, description: nil, tags: [])
11
- super()
12
-
13
- @query = query
14
- @title = title || "DNPedia domain lookup"
15
- @description = description || "query = #{query}"
16
- @tags = tags
17
- end
8
+ param :query
9
+ option :title, default: proc { "DNPedia domain search" }
10
+ option :description, default: proc { "query = #{query}" }
11
+ option :tags, default: proc { [] }
18
12
 
19
13
  def artifacts
20
- lookup || []
14
+ search || []
21
15
  end
22
16
 
23
17
  private
@@ -26,7 +20,7 @@ module Mihari
26
20
  @api ||= ::DNPedia::API.new
27
21
  end
28
22
 
29
- def lookup
23
+ def search
30
24
  res = api.search(query)
31
25
  rows = res["rows"] || []
32
26
  rows.map do |row|
@@ -7,21 +7,24 @@ require "parallel"
7
7
  module Mihari
8
8
  module Analyzers
9
9
  class DNSTwister < Base
10
- attr_reader :query, :type, :title, :description, :tags
10
+ include Mixins::Refang
11
11
 
12
- def initialize(query, title: nil, description: nil, tags: [])
13
- super()
12
+ param :query
13
+ option :title, default: proc { "dnstwister domain search" }
14
+ option :description, default: proc { "query = #{query}" }
15
+ option :tags, default: proc { [] }
14
16
 
15
- @query = query
16
- @type = TypeChecker.type(query)
17
+ attr_reader :type
18
+
19
+ def initialize(*args, **kwargs)
20
+ super
17
21
 
18
- @title = title || "dnstwister domain lookup"
19
- @description = description || "query = #{query}"
20
- @tags = tags
22
+ @query = refang(query)
23
+ @type = TypeChecker.type(query)
21
24
  end
22
25
 
23
26
  def artifacts
24
- lookup || []
27
+ search || []
25
28
  end
26
29
 
27
30
  private
@@ -41,7 +44,7 @@ module Mihari
41
44
  false
42
45
  end
43
46
 
44
- def lookup
47
+ def search
45
48
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
46
49
 
47
50
  res = api.fuzz(query)
@@ -5,16 +5,10 @@ require "onyphe"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class Onyphe < Base
8
- attr_reader :title, :description, :query, :tags
9
-
10
- def initialize(query, title: nil, description: nil, tags: [])
11
- super()
12
-
13
- @query = query
14
- @title = title || "Onyphe lookup"
15
- @description = description || "query = #{query}"
16
- @tags = tags
17
- end
8
+ param :query
9
+ option :title, default: proc { "Onyphe search" }
10
+ option :description, default: proc { "query = #{query}" }
11
+ option :tags, default: proc { [] }
18
12
 
19
13
  def artifacts
20
14
  results = search
@@ -24,14 +18,14 @@ module Mihari
24
18
  result["results"]
25
19
  end.flatten.compact
26
20
 
27
- flat_results.map { |result| result["ip"] }.compact.uniq
21
+ flat_results.filter_map { |result| result["ip"] }.uniq
28
22
  end
29
23
 
30
24
  private
31
25
 
32
26
  PAGE_SIZE = 10
33
27
 
34
- def config_keys
28
+ def configuration_keys
35
29
  %w[onyphe_api_key]
36
30
  end
37
31
 
@@ -5,26 +5,29 @@ require "otx_ruby"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class OTX < Base
8
- attr_reader :query, :type, :title, :description, :tags
8
+ include Mixins::Refang
9
9
 
10
- def initialize(query, title: nil, description: nil, tags: [])
11
- super()
10
+ param :query
11
+ option :title, default: proc { "OTX search" }
12
+ option :description, default: proc { "query = #{query}" }
13
+ option :tags, default: proc { [] }
12
14
 
13
- @query = query
14
- @type = TypeChecker.type(query)
15
+ attr_reader :type
16
+
17
+ def initialize(*args, **kwargs)
18
+ super
15
19
 
16
- @title = title || "OTX lookup"
17
- @description = description || "query = #{query}"
18
- @tags = tags
20
+ @query = refang(query)
21
+ @type = TypeChecker.type(query)
19
22
  end
20
23
 
21
24
  def artifacts
22
- lookup || []
25
+ search || []
23
26
  end
24
27
 
25
28
  private
26
29
 
27
- def config_keys
30
+ def configuration_keys
28
31
  %w[otx_api_key]
29
32
  end
30
33
 
@@ -40,29 +43,29 @@ module Mihari
40
43
  %w[ip domain].include? type
41
44
  end
42
45
 
43
- def lookup
46
+ def search
44
47
  case type
45
48
  when "domain"
46
- domain_lookup
49
+ domain_search
47
50
  when "ip"
48
- ip_lookup
51
+ ip_search
49
52
  else
50
53
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
51
54
  end
52
55
  end
53
56
 
54
- def domain_lookup
57
+ def domain_search
55
58
  records = domain_client.get_passive_dns(query)
56
- records.map do |record|
59
+ records.filter_map do |record|
57
60
  record.address if record.record_type == "A"
58
- end.compact.uniq
61
+ end.uniq
59
62
  end
60
63
 
61
- def ip_lookup
64
+ def ip_search
62
65
  records = ip_client.get_passive_dns(query)
63
- records.map do |record|
66
+ records.filter_map do |record|
64
67
  record.hostname if record.record_type == "A"
65
- end.compact.uniq
68
+ end.uniq
66
69
  end
67
70
  end
68
71
  end
@@ -5,26 +5,29 @@ require "passivetotal"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class PassiveTotal < Base
8
- attr_reader :query, :type, :title, :description, :tags
8
+ include Mixins::Refang
9
9
 
10
- def initialize(query, title: nil, description: nil, tags: [])
11
- super()
10
+ param :query
11
+ option :title, default: proc { "PassiveTotal search" }
12
+ option :description, default: proc { "query = #{query}" }
13
+ option :tags, default: proc { [] }
12
14
 
13
- @query = query
14
- @type = TypeChecker.type(query)
15
+ attr_reader :type
16
+
17
+ def initialize(*args, **kwargs)
18
+ super
15
19
 
16
- @title = title || "PassiveTotal lookup"
17
- @description = description || "query = #{query}"
18
- @tags = tags
20
+ @query = refang(query)
21
+ @type = TypeChecker.type(query)
19
22
  end
20
23
 
21
24
  def artifacts
22
- lookup || []
25
+ search || []
23
26
  end
24
27
 
25
28
  private
26
29
 
27
- def config_keys
30
+ def configuration_keys
28
31
  %w[passivetotal_username passivetotal_api_key]
29
32
  end
30
33
 
@@ -33,30 +36,28 @@ module Mihari
33
36
  end
34
37
 
35
38
  def valid_type?
36
- %w[ip domain mail].include? type
39
+ %w[ip domain mail hash].include? type
37
40
  end
38
41
 
39
- def lookup
42
+ def search
40
43
  case type
41
- when "domain"
42
- passive_dns_lookup
43
- when "ip"
44
- passive_dns_lookup
44
+ when "domain", "ip"
45
+ passive_dns_search
45
46
  when "mail"
46
- reverse_whois_lookup
47
+ reverse_whois_search
47
48
  when "hash"
48
- ssl_lookup
49
+ ssl_search
49
50
  else
50
51
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
51
52
  end
52
53
  end
53
54
 
54
- def passive_dns_lookup
55
+ def passive_dns_search
55
56
  res = api.dns.passive_unique(query)
56
57
  res["results"] || []
57
58
  end
58
59
 
59
- def reverse_whois_lookup
60
+ def reverse_whois_search
60
61
  res = api.whois.search(query: query, field: "email")
61
62
  results = res["results"] || []
62
63
  results.map do |result|
@@ -64,7 +65,7 @@ module Mihari
64
65
  end.flatten.compact.uniq
65
66
  end
66
67
 
67
- def ssl_lookup
68
+ def ssl_search
68
69
  res = api.ssl.history(query)
69
70
  results = res["results"] || []
70
71
  results.map do |result|
@@ -5,26 +5,29 @@ require "pulsedive"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class Pulsedive < Base
8
- attr_reader :query, :type, :title, :description, :tags
8
+ include Mixins::Refang
9
9
 
10
- def initialize(query, title: nil, description: nil, tags: [])
11
- super()
10
+ param :query
11
+ option :title, default: proc { "Pulsedive search" }
12
+ option :description, default: proc { "query = #{query}" }
13
+ option :tags, default: proc { [] }
12
14
 
13
- @query = query
14
- @type = TypeChecker.type(query)
15
+ attr_reader :type
16
+
17
+ def initialize(*args, **kwargs)
18
+ super
15
19
 
16
- @title = title || "Pulsedive lookup"
17
- @description = description || "query = #{query}"
18
- @tags = tags
20
+ @query = refang(query)
21
+ @type = TypeChecker.type(query)
19
22
  end
20
23
 
21
24
  def artifacts
22
- lookup || []
25
+ search || []
23
26
  end
24
27
 
25
28
  private
26
29
 
27
- def config_keys
30
+ def configuration_keys
28
31
  %w[pulsedive_api_key]
29
32
  end
30
33
 
@@ -36,16 +39,16 @@ module Mihari
36
39
  %w[ip domain].include? type
37
40
  end
38
41
 
39
- def lookup
42
+ def search
40
43
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
41
44
 
42
45
  indicator = api.indicator.get_by_value(query)
43
46
  iid = indicator["iid"]
44
47
 
45
48
  properties = api.indicator.get_properties_by_id(iid)
46
- (properties["dns"] || []).map do |property|
49
+ (properties["dns"] || []).filter_map do |property|
47
50
  property["value"] if ["A", "PTR"].include?(property["name"])
48
- end.compact
51
+ end
49
52
  end
50
53
  end
51
54
  end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uuidtools"
4
+
5
+ NIL = nil
6
+
7
+ module Mihari
8
+ module Analyzers
9
+ class Rule < Base
10
+ option :title
11
+ option :description
12
+ option :queries
13
+
14
+ option :id, default: proc {}
15
+ option :tags, default: proc { [] }
16
+ option :allowed_data_types, default: proc { ALLOWED_DATA_TYPES }
17
+
18
+ attr_reader :source
19
+
20
+ def initialize(**kwargs)
21
+ super(**kwargs)
22
+
23
+ @source = id || UUIDTools::UUID.md5_create(UUIDTools::UUID_URL_NAMESPACE, title + description).to_s
24
+ end
25
+
26
+ ANALYZER_TO_CLASS = {
27
+ "binaryedge" => BinaryEdge,
28
+ "censys" => Censys,
29
+ "circl" => CIRCL,
30
+ "crtsh" => Crtsh,
31
+ "dnpedia" => DNPedia,
32
+ "dnstwister" => DNSTwister,
33
+ "onyphe" => Onyphe,
34
+ "otx" => OTX,
35
+ "passivetotal" => PassiveTotal,
36
+ "pulsedive" => Pulsedive,
37
+ "securitytrails" => SecurityTrails,
38
+ "shodan" => Shodan,
39
+ "spyse" => Spyse,
40
+ "urlscan" => Urlscan,
41
+ "virustotal" => VirusTotal,
42
+ "zoomeye" => ZoomEye
43
+ }.freeze
44
+
45
+ #
46
+ # Returns a list of artifacts matched with queries
47
+ #
48
+ # @return [Array<Mihari::Artifact>]
49
+ #
50
+ def artifacts
51
+ artifacts = []
52
+
53
+ queries.each do |params|
54
+ analyzer_name = params[:analyzer]
55
+ klass = get_analyzer_class(analyzer_name)
56
+
57
+ query = params[:query]
58
+ analyzer = klass.new(query, **params)
59
+
60
+ # Use #normalized_artifacts method to get atrifacts as Array<Mihari::Artifact>
61
+ # So Mihari::Artifact object has "source" attribute (e.g. "Shodan")
62
+ artifacts << analyzer.normalized_artifacts
63
+ end
64
+
65
+ artifacts.flatten
66
+ end
67
+
68
+ #
69
+ # Normalize artifacts
70
+ # - Uniquefy artifacts by #uniq(&:data)
71
+ # - Reject an invalid artifact (for just in case)
72
+ # - Select artifacts with allowed data types
73
+ #
74
+ # @return [Array<Mihari::Artifact>]
75
+ #
76
+ def normalized_artifacts
77
+ @normalized_artifacts ||= artifacts.uniq(&:data).select(&:valid?).select do |artifact|
78
+ allowed_data_types.include? artifact.data_type
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ #
85
+ # Get analyzer class
86
+ #
87
+ # @param [String] analyzer_name
88
+ #
89
+ # @return [Class<Mihari::Analyzers::Base>] analyzer class
90
+ #
91
+ def get_analyzer_class(analyzer_name)
92
+ analyzer = ANALYZER_TO_CLASS[analyzer_name]
93
+ return analyzer if analyzer
94
+
95
+ raise ArgumentError, "#{analyzer_name} is not supported"
96
+ end
97
+ end
98
+ end
99
+ end