mihari 1.3.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +68 -0
  3. data/README.md +20 -270
  4. data/Rakefile +1 -0
  5. data/build_frontend.sh +14 -0
  6. data/docker/Dockerfile +3 -2
  7. data/{screenshots → images}/alert.png +0 -0
  8. data/{screenshots → images}/eyecatch.png +0 -0
  9. data/images/logo.png +0 -0
  10. data/{screenshots → images}/misp.png +0 -0
  11. data/{screenshots → images}/slack.png +0 -0
  12. data/images/web_alerts.png +0 -0
  13. data/images/web_config.png +0 -0
  14. data/lib/mihari.rb +2 -2
  15. data/lib/mihari/analyzers/base.rb +1 -1
  16. data/lib/mihari/analyzers/basic.rb +3 -4
  17. data/lib/mihari/analyzers/binaryedge.rb +4 -7
  18. data/lib/mihari/analyzers/censys.rb +3 -7
  19. data/lib/mihari/analyzers/circl.rb +3 -5
  20. data/lib/mihari/analyzers/crtsh.rb +2 -6
  21. data/lib/mihari/analyzers/dnpedia.rb +3 -6
  22. data/lib/mihari/analyzers/dnstwister.rb +4 -9
  23. data/lib/mihari/analyzers/free_text.rb +2 -6
  24. data/lib/mihari/analyzers/http_hash.rb +3 -11
  25. data/lib/mihari/analyzers/onyphe.rb +3 -6
  26. data/lib/mihari/analyzers/otx.rb +4 -9
  27. data/lib/mihari/analyzers/passive_dns.rb +4 -9
  28. data/lib/mihari/analyzers/passive_ssl.rb +4 -9
  29. data/lib/mihari/analyzers/passivetotal.rb +9 -14
  30. data/lib/mihari/analyzers/pulsedive.rb +7 -12
  31. data/lib/mihari/analyzers/reverse_whois.rb +4 -9
  32. data/lib/mihari/analyzers/securitytrails.rb +12 -17
  33. data/lib/mihari/analyzers/securitytrails_domain_feed.rb +3 -7
  34. data/lib/mihari/analyzers/shodan.rb +9 -8
  35. data/lib/mihari/analyzers/spyse.rb +6 -11
  36. data/lib/mihari/analyzers/ssh_fingerprint.rb +2 -6
  37. data/lib/mihari/analyzers/urlscan.rb +21 -9
  38. data/lib/mihari/analyzers/virustotal.rb +6 -11
  39. data/lib/mihari/analyzers/zoomeye.rb +7 -11
  40. data/lib/mihari/cli.rb +20 -28
  41. data/lib/mihari/config.rb +1 -25
  42. data/lib/mihari/configurable.rb +4 -5
  43. data/lib/mihari/database.rb +7 -1
  44. data/lib/mihari/emitters/misp.rb +4 -2
  45. data/lib/mihari/emitters/slack.rb +18 -7
  46. data/lib/mihari/emitters/the_hive.rb +2 -2
  47. data/lib/mihari/errors.rb +2 -0
  48. data/lib/mihari/models/alert.rb +51 -0
  49. data/lib/mihari/models/artifact.rb +1 -1
  50. data/lib/mihari/notifiers/exception_notifier.rb +5 -5
  51. data/lib/mihari/serializers/alert.rb +1 -1
  52. data/lib/mihari/serializers/artifact.rb +1 -1
  53. data/lib/mihari/serializers/tag.rb +1 -1
  54. data/lib/mihari/status.rb +10 -10
  55. data/lib/mihari/type_checker.rb +4 -4
  56. data/lib/mihari/version.rb +1 -1
  57. data/lib/mihari/web/app.rb +126 -0
  58. data/lib/mihari/web/public/index.html +21 -0
  59. data/lib/mihari/web/public/static/favicon.ico +0 -0
  60. data/lib/mihari/web/public/static/fonts/fa-brands-400.099a9556.woff +0 -0
  61. data/lib/mihari/web/public/static/fonts/fa-brands-400.30cc681d.eot +0 -0
  62. data/lib/mihari/web/public/static/fonts/fa-brands-400.3b89dd10.ttf +0 -0
  63. data/lib/mihari/web/public/static/fonts/fa-brands-400.f7307680.woff2 +0 -0
  64. data/lib/mihari/web/public/static/fonts/fa-regular-400.1f77739c.ttf +0 -0
  65. data/lib/mihari/web/public/static/fonts/fa-regular-400.7124eb50.woff +0 -0
  66. data/lib/mihari/web/public/static/fonts/fa-regular-400.7630483d.eot +0 -0
  67. data/lib/mihari/web/public/static/fonts/fa-regular-400.f0f82301.woff2 +0 -0
  68. data/lib/mihari/web/public/static/fonts/fa-solid-900.1042e8ca.eot +0 -0
  69. data/lib/mihari/web/public/static/fonts/fa-solid-900.605ed792.ttf +0 -0
  70. data/lib/mihari/web/public/static/fonts/fa-solid-900.9fe5a17c.woff +0 -0
  71. data/lib/mihari/web/public/static/fonts/fa-solid-900.e8a427e1.woff2 +0 -0
  72. data/lib/mihari/web/public/static/img/fa-brands-400.ba7ed552.svg +3717 -0
  73. data/lib/mihari/web/public/static/img/fa-regular-400.0bb42845.svg +801 -0
  74. data/lib/mihari/web/public/static/img/fa-solid-900.376c1f97.svg +5034 -0
  75. data/lib/mihari/web/public/static/js/app.58b32d15.js +12 -0
  76. data/lib/mihari/web/public/static/js/app.58b32d15.js.map +1 -0
  77. data/mihari.gemspec +30 -25
  78. metadata +163 -56
  79. data/.travis.yml +0 -13
  80. data/lib/mihari/alert_viewer.rb +0 -23
@@ -5,12 +5,7 @@ require "pulsedive"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class Pulsedive < Base
8
- attr_reader :query
9
- attr_reader :type
10
-
11
- attr_reader :title
12
- attr_reader :description
13
- attr_reader :tags
8
+ attr_reader :query, :type, :title, :description, :tags
14
9
 
15
10
  def initialize(query, title: nil, description: nil, tags: [])
16
11
  super()
@@ -30,7 +25,7 @@ module Mihari
30
25
  private
31
26
 
32
27
  def config_keys
33
- %w(pulsedive_api_key)
28
+ %w[pulsedive_api_key]
34
29
  end
35
30
 
36
31
  def api
@@ -38,18 +33,18 @@ module Mihari
38
33
  end
39
34
 
40
35
  def valid_type?
41
- %w(ip domain).include? type
36
+ %w[ip domain].include? type
42
37
  end
43
38
 
44
39
  def lookup
45
- raise InvalidInputError, "#{query}(type: #{type || 'unknown'}) is not supported." unless valid_type?
40
+ raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
46
41
 
47
42
  indicator = api.indicator.get_by_value(query)
48
- iid = indicator.dig("iid")
43
+ iid = indicator["iid"]
49
44
 
50
45
  properties = api.indicator.get_properties_by_id(iid)
51
- (properties.dig("dns") || []).map do |property|
52
- property.dig("value") if ["A", "PTR"].include?(property.dig("name"))
46
+ (properties["dns"] || []).map do |property|
47
+ property["value"] if ["A", "PTR"].include?(property["name"])
53
48
  end.compact
54
49
  end
55
50
  end
@@ -5,16 +5,11 @@ require "parallel"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class ReveseWhois < Base
8
- attr_reader :query
9
- attr_reader :type
10
-
11
- attr_reader :title
12
- attr_reader :description
13
- attr_reader :tags
8
+ attr_reader :query, :type, :title, :description, :tags
14
9
 
15
10
  ANALYZERS = [
16
11
  Mihari::Analyzers::PassiveTotal,
17
- Mihari::Analyzers::SecurityTrails,
12
+ Mihari::Analyzers::SecurityTrails
18
13
  ].freeze
19
14
 
20
15
  def initialize(query, title: nil, description: nil, tags: [])
@@ -37,11 +32,11 @@ module Mihari
37
32
  private
38
33
 
39
34
  def valid_type?
40
- %w(mail).include? type
35
+ %w[mail].include? type
41
36
  end
42
37
 
43
38
  def analyzers
44
- raise InvalidInputError, "#{query}(type: #{type || 'unknown'}) is not supported." unless valid_type?
39
+ raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
45
40
 
46
41
  ANALYZERS.map do |klass|
47
42
  klass.new(query)
@@ -5,12 +5,7 @@ require "securitytrails"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class SecurityTrails < Base
8
- attr_reader :query
9
- attr_reader :type
10
-
11
- attr_reader :title
12
- attr_reader :description
13
- attr_reader :tags
8
+ attr_reader :query, :type, :title, :description, :tags
14
9
 
15
10
  def initialize(query, title: nil, description: nil, tags: [])
16
11
  super()
@@ -30,7 +25,7 @@ module Mihari
30
25
  private
31
26
 
32
27
  def config_keys
33
- %w(securitytrails_api_key)
28
+ %w[securitytrails_api_key]
34
29
  end
35
30
 
36
31
  def api
@@ -38,7 +33,7 @@ module Mihari
38
33
  end
39
34
 
40
35
  def valid_type?
41
- %w(ip domain mail).include? type
36
+ %w[ip domain mail].include? type
42
37
  end
43
38
 
44
39
  def lookup
@@ -50,28 +45,28 @@ module Mihari
50
45
  when "mail"
51
46
  mail_lookup
52
47
  else
53
- raise InvalidInputError, "#{query}(type: #{type || 'unknown'}) is not supported." unless valid_type?
48
+ raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
54
49
  end
55
50
  end
56
51
 
57
52
  def domain_lookup
58
53
  result = api.history.get_all_dns_history(query, type: "a")
59
- records = result.dig("records") || []
54
+ records = result["records"] || []
60
55
  records.map do |record|
61
- (record.dig("values") || []).map { |value| value.dig("ip") }
56
+ (record["values"] || []).map { |value| value["ip"] }
62
57
  end.flatten.compact.uniq
63
58
  end
64
59
 
65
60
  def ip_lookup
66
- result = api.domains.search( filter: { ipv4: query })
67
- records = result.dig("records") || []
68
- records.map { |record| record.dig("hostname") }.compact.uniq
61
+ result = api.domains.search(filter: {ipv4: query})
62
+ records = result["records"] || []
63
+ records.map { |record| record["hostname"] }.compact.uniq
69
64
  end
70
65
 
71
66
  def mail_lookup
72
- result = api.domains.search( filter: { whois_email: query })
73
- records = result.dig("records") || []
74
- records.map { |record| record.dig("hostname") }.compact.uniq
67
+ result = api.domains.search(filter: {whois_email: query})
68
+ records = result["records"] || []
69
+ records.map { |record| record["hostname"] }.compact.uniq
75
70
  end
76
71
  end
77
72
  end
@@ -5,11 +5,7 @@ require "securitytrails"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class SecurityTrailsDomainFeed < Base
8
- attr_reader :type
9
-
10
- attr_reader :title
11
- attr_reader :description
12
- attr_reader :tags
8
+ attr_reader :type, :title, :description, :tags
13
9
 
14
10
  def initialize(regexp, type: "registered", title: nil, description: nil, tags: [])
15
11
  super()
@@ -32,7 +28,7 @@ module Mihari
32
28
  private
33
29
 
34
30
  def config_keys
35
- %w(securitytrails_api_key)
31
+ %w[securitytrails_api_key]
36
32
  end
37
33
 
38
34
  def api
@@ -40,7 +36,7 @@ module Mihari
40
36
  end
41
37
 
42
38
  def valid_type?
43
- %w(all new registered).include? type
39
+ %w[all new registered].include? type
44
40
  end
45
41
 
46
42
  def regexp
@@ -5,10 +5,7 @@ require "shodan"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class Shodan < Base
8
- attr_reader :title
9
- attr_reader :description
10
- attr_reader :query
11
- attr_reader :tags
8
+ attr_reader :title, :description, :query, :tags
12
9
 
13
10
  def initialize(query, title: nil, description: nil, tags: [])
14
11
  super()
@@ -24,9 +21,9 @@ module Mihari
24
21
  return [] unless results || results.empty?
25
22
 
26
23
  results.map do |result|
27
- matches = result.dig("matches") || []
24
+ matches = result["matches"] || []
28
25
  matches.map do |match|
29
- match.dig "ip_str"
26
+ match["ip_str"]
30
27
  end.compact
31
28
  end.flatten.compact.uniq
32
29
  end
@@ -36,7 +33,7 @@ module Mihari
36
33
  PAGE_SIZE = 100
37
34
 
38
35
  def config_keys
39
- %w(shodan_api_key)
36
+ %w[shodan_api_key]
40
37
  end
41
38
 
42
39
  def api
@@ -58,7 +55,11 @@ module Mihari
58
55
  break unless res
59
56
 
60
57
  responses << res
61
- break if res.dig("total").to_i <= page * PAGE_SIZE
58
+ break if res["total"].to_i <= page * PAGE_SIZE
59
+ rescue JSON::ParserError
60
+ # ignore JSON::ParserError
61
+ # ref. https://github.com/ninoseki/mihari/issues/197
62
+ next
62
63
  end
63
64
  responses
64
65
  end
@@ -6,12 +6,7 @@ require "json"
6
6
  module Mihari
7
7
  module Analyzers
8
8
  class Spyse < Base
9
- attr_reader :query
10
- attr_reader :type
11
-
12
- attr_reader :title
13
- attr_reader :description
14
- attr_reader :tags
9
+ attr_reader :query, :type, :title, :description, :tags
15
10
 
16
11
  def initialize(query, title: nil, description: nil, tags: [], type: "domain")
17
12
  super()
@@ -35,7 +30,7 @@ module Mihari
35
30
  end
36
31
 
37
32
  def config_keys
38
- %w(spyse_api_key)
33
+ %w[spyse_api_key]
39
34
  end
40
35
 
41
36
  def api
@@ -43,14 +38,14 @@ module Mihari
43
38
  end
44
39
 
45
40
  def valid_type?
46
- %w(ip domain cert).include? type
41
+ %w[ip domain cert].include? type
47
42
  end
48
43
 
49
44
  def domain_lookup
50
45
  res = api.domain.search(search_params, limit: 100)
51
46
  items = res.dig("data", "items") || []
52
47
  items.map do |item|
53
- item.dig("name")
48
+ item["name"]
54
49
  end.uniq.compact
55
50
  end
56
51
 
@@ -58,7 +53,7 @@ module Mihari
58
53
  res = api.ip.search(search_params, limit: 100)
59
54
  items = res.dig("data", "items") || []
60
55
  items.map do |item|
61
- item.dig("ip")
56
+ item["ip"]
62
57
  end.uniq.compact
63
58
  end
64
59
 
@@ -69,7 +64,7 @@ module Mihari
69
64
  when "ip"
70
65
  ip_lookup
71
66
  else
72
- raise InvalidInputError, "#{query}(type: #{type || 'unknown'}) is not supported." unless valid_type?
67
+ raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
73
68
  end
74
69
  end
75
70
  end
@@ -5,11 +5,7 @@ require "parallel"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class SSHFingerprint < Base
8
- attr_reader :fingerprint
9
-
10
- attr_reader :title
11
- attr_reader :description
12
- attr_reader :tags
8
+ attr_reader :fingerprint, :title, :description, :tags
13
9
 
14
10
  def initialize(fingerprint, title: nil, description: nil, tags: [])
15
11
  super()
@@ -46,7 +42,7 @@ module Mihari
46
42
 
47
43
  [
48
44
  binary_edge,
49
- shodan,
45
+ shodan
50
46
  ].compact
51
47
  end
52
48
 
@@ -5,20 +5,29 @@ require "urlscan"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class Urlscan < Base
8
- attr_reader :title
9
- attr_reader :description
10
- attr_reader :query
11
- attr_reader :tags
12
- attr_reader :target_type
8
+ attr_reader :title, :description, :query, :tags, :filter, :target_type, :use_pro, :use_similarity
13
9
 
14
- def initialize(query, title: nil, description: nil, tags: [], target_type: "url")
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
+ )
15
20
  super()
16
21
 
17
22
  @query = query
18
23
  @title = title || "urlscan lookup"
19
24
  @description = description || "query = #{query}"
20
25
  @tags = tags
26
+
27
+ @filter = filter
21
28
  @target_type = target_type
29
+ @use_pro = use_pro
30
+ @use_similarity = use_similarity
22
31
 
23
32
  raise InvalidInputError, "type should be url, domain or ip." unless valid_target_type?
24
33
  end
@@ -27,7 +36,7 @@ module Mihari
27
36
  result = search
28
37
  return [] unless result
29
38
 
30
- results = result.dig("results") || []
39
+ results = result["results"] || []
31
40
  results.map do |match|
32
41
  match.dig "page", target_type
33
42
  end.compact.uniq
@@ -36,7 +45,7 @@ module Mihari
36
45
  private
37
46
 
38
47
  def config_keys
39
- %w(urlscan_api_key)
48
+ %w[urlscan_api_key]
40
49
  end
41
50
 
42
51
  def api
@@ -44,11 +53,14 @@ module Mihari
44
53
  end
45
54
 
46
55
  def search
56
+ return api.pro.similar(query) if use_similarity
57
+ return api.pro.search(query: query, filter: filter, size: 10_000) if use_pro
58
+
47
59
  api.search(query, size: 10_000)
48
60
  end
49
61
 
50
62
  def valid_target_type?
51
- %w(url domain ip).include? target_type
63
+ %w[url domain ip].include? target_type
52
64
  end
53
65
  end
54
66
  end
@@ -5,12 +5,7 @@ require "virustotal"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class VirusTotal < Base
8
- attr_reader :indicator
9
- attr_reader :type
10
-
11
- attr_reader :title
12
- attr_reader :description
13
- attr_reader :tags
8
+ attr_reader :indicator, :type, :title, :description, :tags
14
9
 
15
10
  def initialize(indicator, title: nil, description: nil, tags: [])
16
11
  super()
@@ -30,7 +25,7 @@ module Mihari
30
25
  private
31
26
 
32
27
  def config_keys
33
- %w(virustotal_api_key)
28
+ %w[virustotal_api_key]
34
29
  end
35
30
 
36
31
  def api
@@ -38,7 +33,7 @@ module Mihari
38
33
  end
39
34
 
40
35
  def valid_type?
41
- %w(ip domain).include? type
36
+ %w[ip domain].include? type
42
37
  end
43
38
 
44
39
  def lookup
@@ -48,14 +43,14 @@ module Mihari
48
43
  when "ip"
49
44
  ip_lookup
50
45
  else
51
- raise InvalidInputError, "#{indicator}(type: #{type || 'unknown'}) is not supported." unless valid_type?
46
+ raise InvalidInputError, "#{indicator}(type: #{type || "unknown"}) is not supported." unless valid_type?
52
47
  end
53
48
  end
54
49
 
55
50
  def domain_lookup
56
51
  res = api.domain.resolutions(indicator)
57
52
 
58
- data = res.dig("data") || []
53
+ data = res["data"] || []
59
54
  data.map do |item|
60
55
  item.dig("attributes", "ip_address")
61
56
  end.compact.uniq
@@ -64,7 +59,7 @@ module Mihari
64
59
  def ip_lookup
65
60
  res = api.ip_address.resolutions(indicator)
66
61
 
67
- data = res.dig("data") || []
62
+ data = res["data"] || []
68
63
  data.map do |item|
69
64
  item.dig("attributes", "host_name")
70
65
  end.compact.uniq
@@ -5,11 +5,7 @@ require "zoomeye"
5
5
  module Mihari
6
6
  module Analyzers
7
7
  class ZoomEye < Base
8
- attr_reader :title
9
- attr_reader :description
10
- attr_reader :query
11
- attr_reader :tags
12
- attr_reader :type
8
+ attr_reader :title, :description, :query, :tags, :type
13
9
 
14
10
  def initialize(query, title: nil, description: nil, tags: [], type: "host")
15
11
  super()
@@ -37,11 +33,11 @@ module Mihari
37
33
  PAGE_SIZE = 10
38
34
 
39
35
  def valid_type?
40
- %w(host web).include? type
36
+ %w[host web].include? type
41
37
  end
42
38
 
43
39
  def config_keys
44
- %w(zoomeye_password zoomeye_username)
40
+ %w[zoomeye_password zoomeye_username]
45
41
  end
46
42
 
47
43
  def api
@@ -50,9 +46,9 @@ module Mihari
50
46
 
51
47
  def convert_responses(responses)
52
48
  responses.map do |res|
53
- matches = res.dig("matches") || []
49
+ matches = res["matches"] || []
54
50
  matches.map do |match|
55
- match.dig "ip"
51
+ match["ip"]
56
52
  end
57
53
  end.flatten.compact.uniq
58
54
  end
@@ -69,7 +65,7 @@ module Mihari
69
65
  res = _host_lookup(query, page: page)
70
66
  break unless res
71
67
 
72
- total = res.dig("total").to_i
68
+ total = res["total"].to_i
73
69
  responses << res
74
70
  break if total <= page * PAGE_SIZE
75
71
  end
@@ -88,7 +84,7 @@ module Mihari
88
84
  res = _web_lookup(query, page: page)
89
85
  break unless res
90
86
 
91
- total = res.dig("total").to_i
87
+ total = res["total"].to_i
92
88
  responses << res
93
89
  break if total <= page * PAGE_SIZE
94
90
  end