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
data/lib/mihari/cli.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thor"
4
3
  require "json"
4
+ require "rack/builder"
5
+ require "rack/handler/webrick"
6
+ require "thor"
5
7
 
6
8
  module Mihari
7
9
  class CLI < Thor
@@ -46,7 +48,10 @@ module Mihari
46
48
  method_option :title, type: :string, desc: "title"
47
49
  method_option :description, type: :string, desc: "description"
48
50
  method_option :tags, type: :array, desc: "tags"
51
+ method_option :filter, type: :string, desc: "filter for urlscan pro search"
49
52
  method_option :target_type, type: :string, default: "url", desc: "target type to fetch from lookup results (target type should be 'url', 'domain' or 'ip')"
53
+ method_option :use_pro, type: :boolean, default: false, desc: "use pro search API or not"
54
+ method_option :use_similarity, type: :boolean, default: false, desc: "use similarity API or not"
50
55
  def urlscan(query)
51
56
  with_error_handling do
52
57
  run_analyzer Analyzers::Urlscan, query: query, options: options
@@ -256,44 +261,31 @@ module Mihari
256
261
  desc "import_from_json", "Give a JSON input via STDIN"
257
262
  def import_from_json(input = nil)
258
263
  with_error_handling do
259
- json = input || STDIN.gets.chomp
264
+ json = input || $stdin.gets.chomp
260
265
  raise ArgumentError, "Input not found: please give an input in a JSON format" unless json
261
266
 
262
267
  json = parse_as_json(json)
263
268
  raise ArgumentError, "Invalid input format: an input JSON data should have title, description and artifacts key" unless valid_json?(json)
264
269
 
265
- title = json.dig("title")
266
- description = json.dig("description")
267
- artifacts = json.dig("artifacts")
268
- tags = json.dig("tags") || []
270
+ title = json["title"]
271
+ description = json["description"]
272
+ artifacts = json["artifacts"]
273
+ tags = json["tags"] || []
269
274
 
270
275
  basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts, source: "json", tags: tags)
271
276
  basic.run
272
277
  end
273
278
  end
274
279
 
275
- desc "alerts", "Show the alerts on TheHive"
276
- method_option :limit, type: :string, default: "5", desc: "Number of alerts to show (or 'all' to show all the alerts)"
277
- method_option :title, type: :string, desc: "Title to filter"
278
- method_option :source, type: :string, desc: "Source to filter"
279
- method_option :tag, type: :string, desc: "Tag to filter"
280
- def alerts
281
- with_error_handling do
282
- load_configuration
280
+ desc "web", "Launch the web app"
281
+ method_option :port, type: :numeric, default: 9292
282
+ method_option :host, type: :string, default: "localhost"
283
+ def web
284
+ port = options["port"].to_i || 9292
285
+ host = options["host"] || "localhost"
283
286
 
284
- viewer = AlertViewer.new
285
- alerts = viewer.list(limit: options["limit"], title: options["title"], source: options["source"], tag: options[:tag])
286
- puts JSON.pretty_generate(alerts)
287
- end
288
- end
289
-
290
- desc "status", "Show the current configuration status"
291
- def status
292
- with_error_handling do
293
- load_configuration
294
-
295
- puts JSON.pretty_generate(Status.check)
296
- end
287
+ load_configuration
288
+ Mihari::App.run!(port: port, host: host)
297
289
  end
298
290
 
299
291
  no_commands do
@@ -312,7 +304,7 @@ module Mihari
312
304
 
313
305
  # @return [true, false]
314
306
  def valid_json?(json)
315
- %w(title description artifacts).all? { |key| json.key? key }
307
+ %w[title description artifacts].all? { |key| json.key? key }
316
308
  end
317
309
 
318
310
  def load_configuration
data/lib/mihari/config.rb CHANGED
@@ -4,31 +4,7 @@ require "yaml"
4
4
 
5
5
  module Mihari
6
6
  class Config
7
- attr_accessor :binaryedge_api_key
8
- attr_accessor :censys_id
9
- attr_accessor :censys_secret
10
- attr_accessor :circl_passive_password
11
- attr_accessor :circl_passive_username
12
- attr_accessor :misp_api_endpoint
13
- attr_accessor :misp_api_key
14
- attr_accessor :onyphe_api_key
15
- attr_accessor :otx_api_key
16
- attr_accessor :passivetotal_api_key
17
- attr_accessor :passivetotal_username
18
- attr_accessor :pulsedive_api_key
19
- attr_accessor :securitytrails_api_key
20
- attr_accessor :shodan_api_key
21
- attr_accessor :slack_channel
22
- attr_accessor :slack_webhook_url
23
- attr_accessor :spyse_api_key
24
- attr_accessor :thehive_api_endpoint
25
- attr_accessor :thehive_api_key
26
- attr_accessor :urlscan_api_key
27
- attr_accessor :virustotal_api_key
28
- attr_accessor :zoomeye_password
29
- attr_accessor :zoomeye_username
30
-
31
- attr_accessor :database
7
+ attr_accessor :binaryedge_api_key, :censys_id, :censys_secret, :circl_passive_password, :circl_passive_username, :misp_api_endpoint, :misp_api_key, :onyphe_api_key, :otx_api_key, :passivetotal_api_key, :passivetotal_username, :pulsedive_api_key, :securitytrails_api_key, :shodan_api_key, :slack_channel, :slack_webhook_url, :spyse_api_key, :thehive_api_endpoint, :thehive_api_key, :urlscan_api_key, :virustotal_api_key, :zoomeye_password, :zoomeye_username, :database
32
8
 
33
9
  def initialize
34
10
  load_from_env
@@ -6,13 +6,12 @@ module Mihari
6
6
  config_keys.all? { |key| Mihari.config.send(key) }
7
7
  end
8
8
 
9
- def configuration_status
9
+ def configuration_values
10
10
  return nil if config_keys.empty?
11
11
 
12
- names = config_keys.join(" and ")
13
- be_verb = config_keys.length == 1 ? "is" : "are"
14
- status = configured? ? "found" : "missing"
15
- "#{names} #{be_verb} #{status}"
12
+ config_keys.map do |key|
13
+ {key: key.upcase, value: Mihari.config.send(key)}
14
+ end
16
15
  end
17
16
 
18
17
  def config_keys
@@ -34,6 +34,7 @@ end
34
34
 
35
35
  def adapter
36
36
  return "postgresql" if Mihari.config.database.start_with?("postgresql://", "postgres://")
37
+ return "mysql2" if Mihari.config.database.start_with?("mysql2://")
37
38
 
38
39
  "sqlite3"
39
40
  end
@@ -43,7 +44,7 @@ module Mihari
43
44
  class << self
44
45
  def connect
45
46
  case adapter
46
- when "postgresql"
47
+ when "postgresql", "mysql2"
47
48
  ActiveRecord::Base.establish_connection(Mihari.config.database)
48
49
  else
49
50
  ActiveRecord::Base.establish_connection(
@@ -58,6 +59,11 @@ module Mihari
58
59
  # Do nothing
59
60
  end
60
61
 
62
+ def close
63
+ ActiveRecord::Base.clear_active_connections!
64
+ ActiveRecord::Base.connection.close
65
+ end
66
+
61
67
  def destroy!
62
68
  InitialSchema.migrate(:down) if ActiveRecord::Base.connected?
63
69
  end
@@ -7,6 +7,8 @@ module Mihari
7
7
  module Emitters
8
8
  class MISP < Base
9
9
  def initialize
10
+ super()
11
+
10
12
  ::MISP.configure do |config|
11
13
  config.api_endpoint = Mihari.config.misp_api_endpoint
12
14
  config.api_key = Mihari.config.misp_api_key
@@ -35,7 +37,7 @@ module Mihari
35
37
  private
36
38
 
37
39
  def config_keys
38
- %w(misp_api_endpoint misp_api_key)
40
+ %w[misp_api_endpoint misp_api_key]
39
41
  end
40
42
 
41
43
  def build_attribute(artifact)
@@ -61,7 +63,7 @@ module Mihari
61
63
  ip: "ip-dst",
62
64
  mail: "email-dst",
63
65
  url: "url",
64
- domain: "domain",
66
+ domain: "domain"
65
67
  }
66
68
  return table[type] if table.key?(type)
67
69
 
@@ -19,25 +19,31 @@ module Mihari
19
19
  end
20
20
 
21
21
  def actions
22
- [vt_link, urlscan_link, censys_link].compact
22
+ [vt_link, urlscan_link, censys_link, shodan_link].compact
23
23
  end
24
24
 
25
25
  def vt_link
26
26
  return nil unless _vt_link
27
27
 
28
- { type: "button", text: "Lookup on VirusTotal", url: _vt_link, }
28
+ {type: "button", text: "VirusTotal", url: _vt_link}
29
29
  end
30
30
 
31
31
  def urlscan_link
32
32
  return nil unless _urlscan_link
33
33
 
34
- { type: "button", text: "Lookup on urlscan.io", url: _urlscan_link, }
34
+ {type: "button", text: "urlscan.io", url: _urlscan_link}
35
35
  end
36
36
 
37
37
  def censys_link
38
38
  return nil unless _censys_link
39
39
 
40
- { type: "button", text: "Lookup on Censys", url: _censys_link, }
40
+ {type: "button", text: "Censys", url: _censys_link}
41
+ end
42
+
43
+ def shodan_link
44
+ return nil unless _shodan_link
45
+
46
+ {type: "button", text: "Shodan", url: _shodan_link}
41
47
  end
42
48
 
43
49
  # @return [Array]
@@ -89,6 +95,11 @@ module Mihari
89
95
  end
90
96
  memoize :_censys_link
91
97
 
98
+ def _shodan_link
99
+ data_type == "ip" ? "https://www.shodan.io/host/#{data}" : nil
100
+ end
101
+ memoize :_shodan_link
102
+
92
103
  # @return [String]
93
104
  def sha256
94
105
  Digest::SHA256.hexdigest data
@@ -96,7 +107,7 @@ module Mihari
96
107
 
97
108
  # @return [String]
98
109
  def defanged_data
99
- @defanged_data ||= data.to_s.gsub /\./, "[.]"
110
+ @defanged_data ||= data.to_s.gsub(/\./, "[.]")
100
111
  end
101
112
  end
102
113
 
@@ -121,7 +132,7 @@ module Mihari
121
132
  [
122
133
  "*#{title}*",
123
134
  "*Desc.*: #{description}",
124
- "*Tags*: #{tags.join(', ')}",
135
+ "*Tags*: #{tags.join(", ")}"
125
136
  ].join("\n")
126
137
  end
127
138
 
@@ -137,7 +148,7 @@ module Mihari
137
148
  private
138
149
 
139
150
  def config_keys
140
- %w(slack_webhook_url)
151
+ %w[slack_webhook_url]
141
152
  end
142
153
  end
143
154
  end
@@ -17,7 +17,7 @@ module Mihari
17
17
  api.alert.create(
18
18
  title: title,
19
19
  description: description,
20
- artifacts: artifacts.map { |artifact| { data: artifact.data, data_type: artifact.data_type, message: description } },
20
+ artifacts: artifacts.map { |artifact| {data: artifact.data, data_type: artifact.data_type, message: description} },
21
21
  tags: tags,
22
22
  type: "external",
23
23
  source: "mihari"
@@ -27,7 +27,7 @@ module Mihari
27
27
  private
28
28
 
29
29
  def config_keys
30
- %w(thehive_api_endpoint thehive_api_key)
30
+ %w[thehive_api_endpoint thehive_api_key]
31
31
  end
32
32
 
33
33
  def api
data/lib/mihari/errors.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Mihari
4
4
  class Error < StandardError; end
5
+
5
6
  class InvalidInputError < Error; end
7
+
6
8
  class RetryableError < Error; end
7
9
  end
@@ -1,11 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record"
4
+ require "active_record/filter"
4
5
 
5
6
  module Mihari
6
7
  class Alert < ActiveRecord::Base
7
8
  has_many :taggings, dependent: :destroy
8
9
  has_many :artifacts, dependent: :destroy
9
10
  has_many :tags, through: :taggings
11
+
12
+ class << self
13
+ def search(artifact_data: nil, description: nil, source: nil, tag_name: nil, title: nil, from_at: nil, to_at: nil, limit: 10, page: 1)
14
+ limit = limit.to_i
15
+ raise ArgumentError, "limit should be bigger than zero" unless limit.positive?
16
+
17
+ page = page.to_i
18
+ raise ArgumentError, "page should be bigger than zero" unless page.positive?
19
+
20
+ offset = (page - 1) * limit
21
+
22
+ relation = build_relation(artifact_data: artifact_data, title: title, description: description, source: source, tag_name: tag_name, from_at: from_at, to_at: to_at)
23
+ # relation = relation.group("alerts.id")
24
+
25
+ alerts = relation.limit(limit).offset(offset).order(id: :desc)
26
+
27
+ alerts.map do |alert|
28
+ json = AlertSerializer.new(alert).as_json
29
+ json[:artifacts] = json[:artifacts] || []
30
+ json[:tags] = json[:tags] || []
31
+ json
32
+ end
33
+ end
34
+
35
+ def count(artifact_data: nil, description: nil, source: nil, tag_name: nil, title: nil, from_at: nil, to_at: nil)
36
+ relation = build_relation(artifact_data: artifact_data, title: title, description: description, source: source, tag_name: tag_name, from_at: from_at, to_at: to_at)
37
+ relation.distinct("alerts.id").count
38
+ end
39
+
40
+ private
41
+
42
+ def build_relation(artifact_data: nil, title: nil, description: nil, source: nil, tag_name: nil, from_at: nil, to_at: nil)
43
+ relation = self
44
+ relation = joins(:tags) if tag_name
45
+ relation = joins(:artifacts) if artifact_data
46
+
47
+ relation = relation.where(artifacts: {data: artifact_data}) if artifact_data
48
+ relation = relation.where(tags: {name: tag_name}) if tag_name
49
+
50
+ relation = relation.where(source: source) if source
51
+ relation = relation.where(title: title) if title
52
+
53
+ relation = relation.filter(description: {like: "%#{description}%"}) if description
54
+
55
+ relation = relation.filter(created_at: {gte: from_at}) if from_at
56
+ relation = relation.filter(created_at: {lte: to_at}) if to_at
57
+
58
+ relation
59
+ end
60
+ end
10
61
  end
11
62
  end
@@ -6,7 +6,7 @@ class ArtifactValidator < ActiveModel::Validator
6
6
  def validate(record)
7
7
  return if record.data_type
8
8
 
9
- record.errors[:data] << "#{record.data} is not supported"
9
+ record.errors.add :data, "#{record.data} is not supported"
10
10
  end
11
11
  end
12
12
 
@@ -19,7 +19,7 @@ module Mihari
19
19
  def notify(exception)
20
20
  notify_to_stdout exception
21
21
 
22
- clean_message = exception.message.tr('`', "'")
22
+ clean_message = exception.message.tr("`", "'")
23
23
  attachments = to_attachments(exception, clean_message)
24
24
  notify_to_slack(text: clean_message, attachments: attachments) if @slack.valid?
25
25
  end
@@ -51,20 +51,20 @@ module Mihari
51
51
 
52
52
  def to_fields(clean_message, backtrace)
53
53
  fields = [
54
- { title: "Exception", value: clean_message },
55
- { title: "Hostname", value: hostname }
54
+ {title: "Exception", value: clean_message},
55
+ {title: "Hostname", value: hostname}
56
56
  ]
57
57
 
58
58
  if backtrace
59
59
  formatted_backtrace = format_backtrace(backtrace)
60
- fields << { title: "Backtrace", value: formatted_backtrace }
60
+ fields << {title: "Backtrace", value: formatted_backtrace}
61
61
  end
62
62
  fields
63
63
  end
64
64
 
65
65
  def hostname
66
66
  Socket.gethostname
67
- rescue StandardError => _e
67
+ rescue => _e
68
68
  "N/A"
69
69
  end
70
70
 
@@ -4,7 +4,7 @@ require "active_model_serializers"
4
4
 
5
5
  module Mihari
6
6
  class AlertSerializer < ActiveModel::Serializer
7
- attributes :title, :description, :source, :created_at
7
+ attributes :id, :title, :description, :source, :created_at
8
8
 
9
9
  has_many :artifacts
10
10
  has_many :tags, through: :taggings
@@ -4,6 +4,6 @@ require "active_model_serializers"
4
4
 
5
5
  module Mihari
6
6
  class ArtifactSerializer < ActiveModel::Serializer
7
- attributes :data, :data_type
7
+ attributes :id, :data, :data_type
8
8
  end
9
9
  end
@@ -4,6 +4,6 @@ require "active_model_serializers"
4
4
 
5
5
  module Mihari
6
6
  class TagSerializer < ActiveModel::Serializer
7
- attributes :name
7
+ attributes :id, :name
8
8
  end
9
9
  end
data/lib/mihari/status.rb CHANGED
@@ -3,9 +3,7 @@
3
3
  module Mihari
4
4
  class Status
5
5
  def check
6
- statuses.map do |key, value|
7
- [key, convert(**value)]
8
- end.to_h
6
+ statuses.transform_values { |value| convert(**value) }
9
7
  end
10
8
 
11
9
  def self.check
@@ -14,16 +12,17 @@ module Mihari
14
12
 
15
13
  private
16
14
 
17
- def convert(status:, message:)
15
+ def convert(is_configured:, values:, type:)
18
16
  {
19
- status: status ? "OK" : "Bad",
20
- message: message
17
+ is_configured: is_configured,
18
+ values: values,
19
+ type: type
21
20
  }
22
21
  end
23
22
 
24
23
  def statuses
25
24
  (Mihari.analyzers + Mihari.emitters).map do |klass|
26
- name = klass.to_s.downcase.split("::").last.to_s
25
+ name = klass.to_s.split("::").last.to_s
27
26
 
28
27
  [name, build_status(klass)]
29
28
  end.to_h.compact
@@ -33,10 +32,11 @@ module Mihari
33
32
  is_analyzer = klass.ancestors.include?(Mihari::Analyzers::Base)
34
33
 
35
34
  instance = is_analyzer ? klass.new("dummy") : klass.new
36
- status = instance.configured?
37
- message = instance.configuration_status
35
+ is_configured = instance.configured?
36
+ values = instance.configuration_values
37
+ type = is_analyzer ? "Analyzer" : "Emitter"
38
38
 
39
- message ? { status: status, message: message } : nil
39
+ values ? {is_configured: is_configured, values: values, type: type} : nil
40
40
  rescue ArgumentError => _e
41
41
  nil
42
42
  end