mihari 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (119) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +7 -0
  3. data/.overcommit.yml +12 -0
  4. data/README.md +7 -9
  5. data/exe/mihari +1 -1
  6. data/images/tines.png +0 -0
  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 +99 -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 +20 -30
  25. data/lib/mihari/analyzers/virustotal.rb +25 -22
  26. data/lib/mihari/analyzers/zoomeye.rb +16 -22
  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 -4
  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 +1 -1
  58. data/lib/mihari/emitters/slack.rb +5 -9
  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 +85 -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 +10 -0
  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/app.rb +2 -0
  81. data/lib/mihari/web/controllers/alerts_controller.rb +12 -3
  82. data/lib/mihari/web/controllers/artifacts_controller.rb +1 -3
  83. data/lib/mihari/web/controllers/base_controller.rb +22 -0
  84. data/lib/mihari/web/controllers/command_controller.rb +3 -4
  85. data/lib/mihari/web/controllers/config_controller.rb +1 -3
  86. data/lib/mihari/web/controllers/sources_controller.rb +1 -3
  87. data/lib/mihari/web/controllers/tags_controller.rb +1 -3
  88. data/lib/mihari/web/helpers/json.rb +2 -0
  89. data/lib/mihari/web/public/index.html +1 -21
  90. data/lib/mihari/web/public/redoc-static.html +2 -2
  91. data/lib/mihari/web/public/static/js/app.ab213f7c.js +12 -0
  92. data/lib/mihari/web/public/static/js/app.ab213f7c.js.map +1 -0
  93. data/lib/mihari/web/public/static/js/{app.bcc595df.js → app.cccddb2b.js} +2 -2
  94. data/lib/mihari/web/public/static/js/app.cccddb2b.js.map +1 -0
  95. data/mihari.gemspec +20 -10
  96. metadata +180 -63
  97. data/.rubocop.yml +0 -161
  98. data/lib/mihari/analyzers/free_text.rb +0 -48
  99. data/lib/mihari/analyzers/http_hash.rb +0 -100
  100. data/lib/mihari/analyzers/passive_dns.rb +0 -59
  101. data/lib/mihari/analyzers/passive_ssl.rb +0 -55
  102. data/lib/mihari/analyzers/reverse_whois.rb +0 -55
  103. data/lib/mihari/analyzers/securitytrails_domain_feed.rb +0 -59
  104. data/lib/mihari/analyzers/ssh_fingerprint.rb +0 -58
  105. data/lib/mihari/cli.rb +0 -124
  106. data/lib/mihari/commands/config.rb +0 -27
  107. data/lib/mihari/commands/free_text.rb +0 -21
  108. data/lib/mihari/commands/http_hash.rb +0 -25
  109. data/lib/mihari/commands/passive_dns.rb +0 -21
  110. data/lib/mihari/commands/passive_ssl.rb +0 -21
  111. data/lib/mihari/commands/reverse_whois.rb +0 -21
  112. data/lib/mihari/commands/securitytrails_domain_feed.rb +0 -23
  113. data/lib/mihari/commands/ssh_fingerprint.rb +0 -21
  114. data/lib/mihari/config.rb +0 -84
  115. data/lib/mihari/configurable.rb +0 -21
  116. data/lib/mihari/html.rb +0 -43
  117. data/lib/mihari/retriable.rb +0 -17
  118. data/lib/mihari/slack_monkeypatch.rb +0 -16
  119. data/lib/mihari/web/public/static/js/app.bcc595df.js.map +0 -1
@@ -5,26 +5,29 @@ require "securitytrails"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class SecurityTrails < 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 { "SecurityTrails 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 || "SecurityTrails 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[securitytrails_api_key]
29
32
  end
30
33
 
@@ -36,20 +39,20 @@ module Mihari
36
39
  %w[ip domain mail].include? type
37
40
  end
38
41
 
39
- def lookup
42
+ def search
40
43
  case type
41
44
  when "domain"
42
- domain_lookup
45
+ domain_search
43
46
  when "ip"
44
- ip_lookup
47
+ ip_search
45
48
  when "mail"
46
- mail_lookup
49
+ mail_search
47
50
  else
48
51
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
49
52
  end
50
53
  end
51
54
 
52
- def domain_lookup
55
+ def domain_search
53
56
  result = api.history.get_all_dns_history(query, type: "a")
54
57
  records = result["records"] || []
55
58
  records.map do |record|
@@ -57,16 +60,16 @@ module Mihari
57
60
  end.flatten.compact.uniq
58
61
  end
59
62
 
60
- def ip_lookup
63
+ def ip_search
61
64
  result = api.domains.search(filter: { ipv4: query })
62
65
  records = result["records"] || []
63
- records.map { |record| record["hostname"] }.compact.uniq
66
+ records.filter_map { |record| record["hostname"] }.uniq
64
67
  end
65
68
 
66
- def mail_lookup
69
+ def mail_search
67
70
  result = api.domains.search(filter: { whois_email: query })
68
71
  records = result["records"] || []
69
- records.map { |record| record["hostname"] }.compact.uniq
72
+ records.filter_map { |record| record["hostname"] }.uniq
70
73
  end
71
74
  end
72
75
  end
@@ -5,16 +5,10 @@ require "shodan"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class Shodan < 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 || "Shodan lookup"
15
- @description = description || "query = #{query}"
16
- @tags = tags
17
- end
8
+ param :query
9
+ option :title, default: proc { "Shodan search" }
10
+ option :description, default: proc { "query = #{query}" }
11
+ option :tags, default: proc { [] }
18
12
 
19
13
  def artifacts
20
14
  results = search
@@ -22,9 +16,9 @@ module Mihari
22
16
 
23
17
  results.map do |result|
24
18
  matches = result["matches"] || []
25
- matches.map do |match|
19
+ matches.filter_map do |match|
26
20
  match["ip_str"]
27
- end.compact
21
+ end
28
22
  end.flatten.compact.uniq
29
23
  end
30
24
 
@@ -32,7 +26,7 @@ module Mihari
32
26
 
33
27
  PAGE_SIZE = 100
34
28
 
35
- def config_keys
29
+ def configuration_keys
36
30
  %w[shodan_api_key]
37
31
  end
38
32
 
@@ -6,21 +6,14 @@ require "json"
6
6
  module Mihari
7
7
  module Analyzers
8
8
  class Spyse < Base
9
- attr_reader :query, :type, :title, :description, :tags
10
-
11
- def initialize(query, title: nil, description: nil, tags: [], type: "domain")
12
- super()
13
-
14
- @query = query
15
-
16
- @title = title || "Spyse lookup"
17
- @description = description || "query = #{query}"
18
- @tags = tags
19
- @type = type
20
- end
9
+ param :query
10
+ option :title, default: proc { "Spyse search" }
11
+ option :description, default: proc { "query = #{query}" }
12
+ option :type, default: proc { "domain" }
13
+ option :tags, default: proc { [] }
21
14
 
22
15
  def artifacts
23
- lookup || []
16
+ search || []
24
17
  end
25
18
 
26
19
  private
@@ -29,7 +22,7 @@ module Mihari
29
22
  @search_params ||= JSON.parse(query)
30
23
  end
31
24
 
32
- def config_keys
25
+ def configuration_keys
33
26
  %w[spyse_api_key]
34
27
  end
35
28
 
@@ -41,7 +34,7 @@ module Mihari
41
34
  %w[ip domain cert].include? type
42
35
  end
43
36
 
44
- def domain_lookup
37
+ def domain_search
45
38
  res = api.domain.search(search_params, limit: 100)
46
39
  items = res.dig("data", "items") || []
47
40
  items.map do |item|
@@ -49,7 +42,7 @@ module Mihari
49
42
  end.uniq.compact
50
43
  end
51
44
 
52
- def ip_lookup
45
+ def ip_search
53
46
  res = api.ip.search(search_params, limit: 100)
54
47
  items = res.dig("data", "items") || []
55
48
  items.map do |item|
@@ -57,12 +50,12 @@ module Mihari
57
50
  end.uniq.compact
58
51
  end
59
52
 
60
- def lookup
53
+ def search
61
54
  case type
62
55
  when "domain"
63
- domain_lookup
56
+ domain_search
64
57
  when "ip"
65
- ip_lookup
58
+ ip_search
66
59
  else
67
60
  raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
68
61
  end
@@ -2,34 +2,22 @@
2
2
 
3
3
  require "urlscan"
4
4
 
5
+ SUPPORTED_DATA_TYPES = %w[url domain ip].freeze
6
+
5
7
  module Mihari
6
8
  module Analyzers
7
9
  class Urlscan < Base
8
- attr_reader :title, :description, :query, :tags, :filter, :target_type, :use_pro, :use_similarity
9
-
10
- def initialize(
11
- query,
12
- description: nil,
13
- filter: nil,
14
- tags: [],
15
- target_type: "url",
16
- title: nil,
17
- use_pro: false,
18
- use_similarity: false
19
- )
20
- super()
10
+ param :query
11
+ option :title, default: proc { "urlscan search" }
12
+ option :description, default: proc { "query = #{query}" }
13
+ option :tags, default: proc { [] }
14
+ option :allowed_data_types, default: proc { SUPPORTED_DATA_TYPES }
15
+ option :use_similarity, default: proc { false }
21
16
 
22
- @query = query
23
- @title = title || "urlscan lookup"
24
- @description = description || "query = #{query}"
25
- @tags = tags
17
+ def initialize(*args, **kwargs)
18
+ super
26
19
 
27
- @filter = filter
28
- @target_type = target_type
29
- @use_pro = use_pro
30
- @use_similarity = use_similarity
31
-
32
- raise InvalidInputError, "type should be url, domain or ip." unless valid_target_type?
20
+ raise InvalidInputError, "allowed_data_types should be any of url, domain and ip." unless valid_alllowed_data_types?
33
21
  end
34
22
 
35
23
  def artifacts
@@ -37,14 +25,17 @@ module Mihari
37
25
  return [] unless result
38
26
 
39
27
  results = result["results"] || []
40
- results.map do |match|
41
- match.dig "page", target_type
42
- end.compact.uniq
28
+
29
+ allowed_data_types.map do |type|
30
+ results.filter_map do |match|
31
+ match.dig "page", type
32
+ end.uniq
33
+ end.flatten
43
34
  end
44
35
 
45
36
  private
46
37
 
47
- def config_keys
38
+ def configuration_keys
48
39
  %w[urlscan_api_key]
49
40
  end
50
41
 
@@ -54,13 +45,12 @@ module Mihari
54
45
 
55
46
  def search
56
47
  return api.pro.similar(query) if use_similarity
57
- return api.pro.search(query: query, filter: filter, size: 10_000) if use_pro
58
48
 
59
49
  api.search(query, size: 10_000)
60
50
  end
61
51
 
62
- def valid_target_type?
63
- %w[url domain ip].include? target_type
52
+ def valid_alllowed_data_types?
53
+ allowed_data_types.all? { |type| SUPPORTED_DATA_TYPES.include? type }
64
54
  end
65
55
  end
66
56
  end
@@ -5,26 +5,29 @@ require "virustotal"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class VirusTotal < Base
8
- attr_reader :indicator, :type, :title, :description, :tags
8
+ include Mixins::Refang
9
9
 
10
- def initialize(indicator, title: nil, description: nil, tags: [])
11
- super()
10
+ param :query
11
+ option :title, default: proc { "VirusTotal search" }
12
+ option :description, default: proc { "query = #{query}" }
13
+ option :tags, default: proc { [] }
12
14
 
13
- @indicator = indicator
14
- @type = TypeChecker.type(indicator)
15
+ attr_reader :type
15
16
 
16
- @title = title || "VirusTotal lookup"
17
- @description = description || "indicator = #{indicator}"
18
- @tags = tags
17
+ def initialize(*args, **kwargs)
18
+ super
19
+
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[virustotal_api_key]
29
32
  end
30
33
 
@@ -36,33 +39,33 @@ module Mihari
36
39
  %w[ip domain].include? type
37
40
  end
38
41
 
39
- def lookup
42
+ def search
40
43
  case type
41
44
  when "domain"
42
- domain_lookup
45
+ domain_search
43
46
  when "ip"
44
- ip_lookup
47
+ ip_search
45
48
  else
46
- raise InvalidInputError, "#{indicator}(type: #{type || "unknown"}) is not supported." unless valid_type?
49
+ raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
47
50
  end
48
51
  end
49
52
 
50
- def domain_lookup
51
- res = api.domain.resolutions(indicator)
53
+ def domain_search
54
+ res = api.domain.resolutions(query)
52
55
 
53
56
  data = res["data"] || []
54
- data.map do |item|
57
+ data.filter_map do |item|
55
58
  item.dig("attributes", "ip_address")
56
- end.compact.uniq
59
+ end.uniq
57
60
  end
58
61
 
59
- def ip_lookup
60
- res = api.ip_address.resolutions(indicator)
62
+ def ip_search
63
+ res = api.ip_address.resolutions(query)
61
64
 
62
65
  data = res["data"] || []
63
- data.map do |item|
66
+ data.filter_map do |item|
64
67
  item.dig("attributes", "host_name")
65
- end.compact.uniq
68
+ end.uniq
66
69
  end
67
70
  end
68
71
  end
@@ -5,24 +5,18 @@ require "zoomeye"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class ZoomEye < Base
8
- attr_reader :title, :description, :query, :tags, :type
9
-
10
- def initialize(query, title: nil, description: nil, tags: [], type: "host")
11
- super()
12
-
13
- @query = query
14
- @title = title || "ZoomEye lookup"
15
- @description = description || "query = #{query}"
16
- @tags = tags
17
- @type = type
18
- end
8
+ param :query
9
+ option :title, default: proc { "ZoomEye search" }
10
+ option :description, default: proc { "query = #{query}" }
11
+ option :tags, default: proc { [] }
12
+ option :type, default: proc { "host" }
19
13
 
20
14
  def artifacts
21
15
  case type
22
16
  when "host"
23
- host_lookup
17
+ host_search
24
18
  when "web"
25
- web_lookup
19
+ web_search
26
20
  else
27
21
  raise InvalidInputError, "#{type} type is not supported." unless valid_type?
28
22
  end
@@ -36,12 +30,12 @@ module Mihari
36
30
  %w[host web].include? type
37
31
  end
38
32
 
39
- def config_keys
40
- %w[zoomeye_password zoomeye_username]
33
+ def configuration_keys
34
+ %w[zoomeye_api_key]
41
35
  end
42
36
 
43
37
  def api
44
- @api ||= ::ZoomEye::API.new(username: Mihari.config.zoomeye_username, password: Mihari.config.zoomeye_password)
38
+ @api ||= ::ZoomEye::API.new(api_key: Mihari.config.zoomeye_api_key)
45
39
  end
46
40
 
47
41
  def convert_responses(responses)
@@ -53,16 +47,16 @@ module Mihari
53
47
  end.flatten.compact.uniq
54
48
  end
55
49
 
56
- def _host_lookup(query, page: 1)
50
+ def _host_search(query, page: 1)
57
51
  api.host.search(query, page: page)
58
52
  rescue ::ZoomEye::Error => _e
59
53
  nil
60
54
  end
61
55
 
62
- def host_lookup
56
+ def host_search
63
57
  responses = []
64
58
  (1..Float::INFINITY).each do |page|
65
- res = _host_lookup(query, page: page)
59
+ res = _host_search(query, page: page)
66
60
  break unless res
67
61
 
68
62
  total = res["total"].to_i
@@ -72,16 +66,16 @@ module Mihari
72
66
  convert_responses responses.compact
73
67
  end
74
68
 
75
- def _web_lookup(query, page: 1)
69
+ def _web_search(query, page: 1)
76
70
  api.web.search(query, page: page)
77
71
  rescue ::ZoomEye::Error => _e
78
72
  nil
79
73
  end
80
74
 
81
- def web_lookup
75
+ def web_search
82
76
  responses = []
83
77
  (1..Float::INFINITY).each do |page|
84
- res = _web_lookup(query, page: page)
78
+ res = _web_search(query, page: page)
85
79
  break unless res
86
80
 
87
81
  total = res["total"].to_i