mihari 1.3.2 → 2.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 (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