mihari 2.3.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (108) 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/docker/Dockerfile +1 -1
  6. data/exe/mihari +1 -1
  7. data/lib/mihari.rb +89 -15
  8. data/lib/mihari/analyzers/base.rb +49 -8
  9. data/lib/mihari/analyzers/basic.rb +1 -2
  10. data/lib/mihari/analyzers/binaryedge.rb +7 -13
  11. data/lib/mihari/analyzers/censys.rb +26 -63
  12. data/lib/mihari/analyzers/circl.rb +20 -17
  13. data/lib/mihari/analyzers/crtsh.rb +6 -13
  14. data/lib/mihari/analyzers/dnpedia.rb +6 -12
  15. data/lib/mihari/analyzers/dnstwister.rb +13 -10
  16. data/lib/mihari/analyzers/onyphe.rb +6 -12
  17. data/lib/mihari/analyzers/otx.rb +22 -19
  18. data/lib/mihari/analyzers/passivetotal.rb +22 -21
  19. data/lib/mihari/analyzers/pulsedive.rb +16 -13
  20. data/lib/mihari/analyzers/rule.rb +97 -0
  21. data/lib/mihari/analyzers/securitytrails.rb +22 -19
  22. data/lib/mihari/analyzers/shodan.rb +7 -13
  23. data/lib/mihari/analyzers/spyse.rb +12 -19
  24. data/lib/mihari/analyzers/urlscan.rb +22 -27
  25. data/lib/mihari/analyzers/virustotal.rb +25 -22
  26. data/lib/mihari/analyzers/zoomeye.rb +14 -20
  27. data/lib/mihari/cli/analyzer.rb +44 -0
  28. data/lib/mihari/cli/base.rb +27 -0
  29. data/lib/mihari/cli/init.rb +13 -0
  30. data/lib/mihari/cli/main.rb +30 -0
  31. data/lib/mihari/cli/mixins/utils.rb +88 -0
  32. data/lib/mihari/cli/validator.rb +11 -0
  33. data/lib/mihari/commands/binaryedge.rb +1 -1
  34. data/lib/mihari/commands/censys.rb +1 -1
  35. data/lib/mihari/commands/circl.rb +2 -2
  36. data/lib/mihari/commands/crtsh.rb +1 -1
  37. data/lib/mihari/commands/dnpedia.rb +1 -1
  38. data/lib/mihari/commands/dnstwister.rb +2 -2
  39. data/lib/mihari/commands/init.rb +46 -0
  40. data/lib/mihari/commands/json.rb +1 -1
  41. data/lib/mihari/commands/onyphe.rb +1 -1
  42. data/lib/mihari/commands/otx.rb +2 -2
  43. data/lib/mihari/commands/passivetotal.rb +2 -2
  44. data/lib/mihari/commands/pulsedive.rb +2 -2
  45. data/lib/mihari/commands/search.rb +77 -0
  46. data/lib/mihari/commands/securitytrails.rb +2 -2
  47. data/lib/mihari/commands/shodan.rb +1 -1
  48. data/lib/mihari/commands/spyse.rb +1 -1
  49. data/lib/mihari/commands/urlscan.rb +2 -2
  50. data/lib/mihari/commands/validator.rb +38 -0
  51. data/lib/mihari/commands/virustotal.rb +2 -2
  52. data/lib/mihari/commands/zoomeye.rb +1 -1
  53. data/lib/mihari/constraints.rb +5 -0
  54. data/lib/mihari/database.rb +13 -2
  55. data/lib/mihari/emitters/base.rb +2 -2
  56. data/lib/mihari/emitters/database.rb +1 -1
  57. data/lib/mihari/emitters/misp.rb +3 -1
  58. data/lib/mihari/emitters/slack.rb +6 -10
  59. data/lib/mihari/emitters/the_hive.rb +1 -1
  60. data/lib/mihari/emitters/webhook.rb +53 -0
  61. data/lib/mihari/mixins/configurable.rb +38 -0
  62. data/lib/mihari/mixins/configuration.rb +90 -0
  63. data/lib/mihari/mixins/hash.rb +20 -0
  64. data/lib/mihari/mixins/refang.rb +21 -0
  65. data/lib/mihari/mixins/retriable.rb +27 -0
  66. data/lib/mihari/mixins/rule.rb +79 -0
  67. data/lib/mihari/models/alert.rb +28 -1
  68. data/lib/mihari/models/artifact.rb +11 -1
  69. data/lib/mihari/notifiers/base.rb +9 -1
  70. data/lib/mihari/notifiers/exception_notifier.rb +50 -0
  71. data/lib/mihari/notifiers/slack.rb +29 -1
  72. data/lib/mihari/schemas/configuration.rb +42 -0
  73. data/lib/mihari/schemas/macros.rb +17 -0
  74. data/lib/mihari/schemas/rule.rb +72 -0
  75. data/lib/mihari/serializers/artifact.rb +1 -1
  76. data/lib/mihari/status.rb +14 -0
  77. data/lib/mihari/templates/rule.yml.erb +19 -0
  78. data/lib/mihari/type_checker.rb +8 -3
  79. data/lib/mihari/version.rb +1 -1
  80. data/lib/mihari/web/controllers/base_controller.rb +1 -1
  81. data/lib/mihari/web/public/index.html +1 -21
  82. data/lib/mihari/web/public/redoc-static.html +2 -2
  83. data/lib/mihari/web/public/static/js/app.ab213f7c.js +12 -0
  84. data/lib/mihari/web/public/static/js/app.ab213f7c.js.map +1 -0
  85. data/mihari.gemspec +19 -12
  86. metadata +138 -65
  87. data/.rubocop.yml +0 -161
  88. data/lib/mihari/analyzers/free_text.rb +0 -48
  89. data/lib/mihari/analyzers/http_hash.rb +0 -100
  90. data/lib/mihari/analyzers/passive_dns.rb +0 -59
  91. data/lib/mihari/analyzers/passive_ssl.rb +0 -55
  92. data/lib/mihari/analyzers/reverse_whois.rb +0 -55
  93. data/lib/mihari/analyzers/securitytrails_domain_feed.rb +0 -59
  94. data/lib/mihari/analyzers/ssh_fingerprint.rb +0 -58
  95. data/lib/mihari/cli.rb +0 -126
  96. data/lib/mihari/commands/config.rb +0 -27
  97. data/lib/mihari/commands/free_text.rb +0 -21
  98. data/lib/mihari/commands/http_hash.rb +0 -25
  99. data/lib/mihari/commands/passive_dns.rb +0 -21
  100. data/lib/mihari/commands/passive_ssl.rb +0 -21
  101. data/lib/mihari/commands/reverse_whois.rb +0 -21
  102. data/lib/mihari/commands/securitytrails_domain_feed.rb +0 -23
  103. data/lib/mihari/commands/ssh_fingerprint.rb +0 -21
  104. data/lib/mihari/config.rb +0 -83
  105. data/lib/mihari/configurable.rb +0 -21
  106. data/lib/mihari/html.rb +0 -43
  107. data/lib/mihari/retriable.rb +0 -17
  108. data/lib/mihari/slack_monkeypatch.rb +0 -16
@@ -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,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uuidtools"
4
+
5
+ module Mihari
6
+ module Analyzers
7
+ class Rule < Base
8
+ option :title
9
+ option :description
10
+ option :queries
11
+
12
+ option :id, default: proc {}
13
+ option :tags, default: proc { [] }
14
+ option :allowed_data_types, default: proc { ALLOWED_DATA_TYPES }
15
+
16
+ attr_reader :source
17
+
18
+ def initialize(**kwargs)
19
+ super(**kwargs)
20
+
21
+ @source = id || UUIDTools::UUID.md5_create(UUIDTools::UUID_URL_NAMESPACE, title + description).to_s
22
+ end
23
+
24
+ ANALYZER_TO_CLASS = {
25
+ "binaryedge" => BinaryEdge,
26
+ "censys" => Censys,
27
+ "circl" => CIRCL,
28
+ "crtsh" => Crtsh,
29
+ "dnpedia" => DNPedia,
30
+ "dnstwister" => DNSTwister,
31
+ "onyphe" => Onyphe,
32
+ "otx" => OTX,
33
+ "passivetotal" => PassiveTotal,
34
+ "pulsedive" => Pulsedive,
35
+ "securitytrails" => SecurityTrails,
36
+ "shodan" => Shodan,
37
+ "spyse" => Spyse,
38
+ "urlscan" => Urlscan,
39
+ "virustotal" => VirusTotal,
40
+ "zoomeye" => ZoomEye
41
+ }.freeze
42
+
43
+ #
44
+ # Returns a list of artifacts matched with queries
45
+ #
46
+ # @return [Array<Mihari::Artifact>]
47
+ #
48
+ def artifacts
49
+ artifacts = []
50
+
51
+ queries.each do |params|
52
+ analyzer_name = params[:analyzer]
53
+ klass = get_analyzer_class(analyzer_name)
54
+
55
+ query = params[:query]
56
+ analyzer = klass.new(query, **params)
57
+
58
+ # Use #normalized_artifacts method to get atrifacts as Array<Mihari::Artifact>
59
+ # So Mihari::Artifact object has "source" attribute (e.g. "Shodan")
60
+ artifacts << analyzer.normalized_artifacts
61
+ end
62
+
63
+ artifacts.flatten
64
+ end
65
+
66
+ #
67
+ # Normalize artifacts
68
+ # - Uniquefy artifacts by #uniq(&:data)
69
+ # - Reject an invalid artifact (for just in case)
70
+ # - Select artifacts with allowed data types
71
+ #
72
+ # @return [Array<Mihari::Artifact>]
73
+ #
74
+ def normalized_artifacts
75
+ @normalized_artifacts ||= artifacts.uniq(&:data).select(&:valid?).select do |artifact|
76
+ allowed_data_types.include? artifact.data_type
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ #
83
+ # Get analyzer class
84
+ #
85
+ # @param [String] analyzer_name
86
+ #
87
+ # @return [Class<Mihari::Analyzers::Base>] analyzer class
88
+ #
89
+ def get_analyzer_class(analyzer_name)
90
+ analyzer = ANALYZER_TO_CLASS[analyzer_name]
91
+ return analyzer if analyzer
92
+
93
+ raise ArgumentError, "#{analyzer_name} is not supported"
94
+ end
95
+ end
96
+ end
97
+ end