mihari 0.17.4 → 1.1.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +155 -0
  4. data/.travis.yml +7 -1
  5. data/Gemfile +2 -0
  6. data/README.md +41 -72
  7. data/config/pre_commit.yml +3 -0
  8. data/docker/Dockerfile +1 -1
  9. data/lib/mihari.rb +12 -8
  10. data/lib/mihari/alert_viewer.rb +16 -34
  11. data/lib/mihari/analyzers/base.rb +7 -19
  12. data/lib/mihari/analyzers/basic.rb +3 -1
  13. data/lib/mihari/analyzers/binaryedge.rb +3 -3
  14. data/lib/mihari/analyzers/censys.rb +2 -2
  15. data/lib/mihari/analyzers/circl.rb +2 -2
  16. data/lib/mihari/analyzers/onyphe.rb +3 -3
  17. data/lib/mihari/analyzers/passivetotal.rb +2 -2
  18. data/lib/mihari/analyzers/pulsedive.rb +2 -2
  19. data/lib/mihari/analyzers/securitytrails.rb +2 -2
  20. data/lib/mihari/analyzers/securitytrails_domain_feed.rb +2 -2
  21. data/lib/mihari/analyzers/shodan.rb +2 -2
  22. data/lib/mihari/analyzers/virustotal.rb +2 -2
  23. data/lib/mihari/analyzers/zoomeye.rb +2 -2
  24. data/lib/mihari/cli.rb +13 -4
  25. data/lib/mihari/config.rb +68 -2
  26. data/lib/mihari/configurable.rb +1 -1
  27. data/lib/mihari/database.rb +68 -0
  28. data/lib/mihari/emitters/base.rb +1 -1
  29. data/lib/mihari/emitters/database.rb +29 -0
  30. data/lib/mihari/emitters/misp.rb +8 -1
  31. data/lib/mihari/emitters/slack.rb +4 -2
  32. data/lib/mihari/emitters/stdout.rb +2 -1
  33. data/lib/mihari/emitters/the_hive.rb +28 -14
  34. data/lib/mihari/models/alert.rb +11 -0
  35. data/lib/mihari/models/artifact.rb +27 -0
  36. data/lib/mihari/models/tag.rb +10 -0
  37. data/lib/mihari/models/tagging.rb +10 -0
  38. data/lib/mihari/notifiers/slack.rb +7 -4
  39. data/lib/mihari/serializers/alert.rb +12 -0
  40. data/lib/mihari/serializers/artifact.rb +9 -0
  41. data/lib/mihari/serializers/tag.rb +9 -0
  42. data/lib/mihari/slack_monkeypatch.rb +16 -0
  43. data/lib/mihari/status.rb +1 -1
  44. data/lib/mihari/type_checker.rb +1 -1
  45. data/lib/mihari/version.rb +1 -1
  46. data/mihari.gemspec +13 -6
  47. metadata +140 -36
  48. data/lib/mihari/artifact.rb +0 -36
  49. data/lib/mihari/cache.rb +0 -35
  50. data/lib/mihari/the_hive.rb +0 -42
  51. data/lib/mihari/the_hive/alert.rb +0 -25
  52. data/lib/mihari/the_hive/artifact.rb +0 -33
  53. data/lib/mihari/the_hive/base.rb +0 -14
@@ -23,6 +23,10 @@ module Mihari
23
23
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
24
24
  end
25
25
 
26
+ def source
27
+ self.class.to_s.split("::").last
28
+ end
29
+
26
30
  # @return [Array<String>]
27
31
  def tags
28
32
  []
@@ -37,7 +41,7 @@ module Mihari
37
41
  end
38
42
 
39
43
  def run_emitter(emitter)
40
- emitter.run(title: title, description: description, artifacts: unique_artifacts, tags: tags)
44
+ emitter.run(title: title, description: description, artifacts: unique_artifacts, source: source, tags: tags)
41
45
  rescue StandardError => e
42
46
  puts "Emission by #{emitter.class} is failed: #{e}"
43
47
  end
@@ -48,32 +52,16 @@ module Mihari
48
52
 
49
53
  private
50
54
 
51
- def the_hive
52
- @the_hive ||= TheHive.new
53
- end
54
-
55
- def cache
56
- @cache ||= Cache.new
57
- end
58
-
59
55
  # @return [Array<Mihari::Artifact>]
60
56
  def normalized_artifacts
61
57
  @normalized_artifacts ||= artifacts.compact.uniq.sort.map do |artifact|
62
- artifact.is_a?(Artifact) ? artifact : Artifact.new(artifact)
58
+ artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact)
63
59
  end.select(&:valid?)
64
60
  end
65
61
 
66
- def uncached_artifacts
67
- @uncached_artifacts ||= normalized_artifacts.reject do |artifact|
68
- cache.cached? artifact.data
69
- end
70
- end
71
-
72
62
  # @return [Array<Mihari::Artifact>]
73
63
  def unique_artifacts
74
- return uncached_artifacts unless the_hive.valid?
75
-
76
- @unique_artifacts ||= the_hive.artifact.find_non_existing_artifacts(uncached_artifacts)
64
+ @unique_artifacts ||= normalized_artifacts.select(&:unique?)
77
65
  end
78
66
 
79
67
  def set_unique_artifacts
@@ -6,12 +6,14 @@ module Mihari
6
6
  attr_accessor :title
7
7
  attr_reader :description
8
8
  attr_reader :artifacts
9
+ attr_reader :source
9
10
  attr_reader :tags
10
11
 
11
- def initialize(title:, description:, artifacts:, tags: [])
12
+ def initialize(title:, description:, artifacts:, source:, tags: [])
12
13
  @title = title
13
14
  @description = description
14
15
  @artifacts = artifacts
16
+ @source = source
15
17
  @tags = tags
16
18
  end
17
19
  end
@@ -26,7 +26,7 @@ module Mihari
26
26
  results.map do |result|
27
27
  events = result.dig("events") || []
28
28
  events.map do |event|
29
- event.dig "origin", "ip"
29
+ event.dig "target", "ip"
30
30
  end.compact
31
31
  end.flatten.compact.uniq
32
32
  end
@@ -52,11 +52,11 @@ module Mihari
52
52
  end
53
53
 
54
54
  def config_keys
55
- %w(BINARYEDGE_API_KEY)
55
+ %w(binaryedge_api_key)
56
56
  end
57
57
 
58
58
  def api
59
- @api ||= ::BinaryEdge::API.new
59
+ @api ||= ::BinaryEdge::API.new(Mihari.config.binaryedge_api_key)
60
60
  end
61
61
  end
62
62
  end
@@ -86,11 +86,11 @@ module Mihari
86
86
  end
87
87
 
88
88
  def config_keys
89
- %w(CENSYS_ID CENSYS_SECRET)
89
+ %w(censys_id censys_secret)
90
90
  end
91
91
 
92
92
  def api
93
- @api ||= ::Censys::API.new
93
+ @api ||= ::Censys::API.new(Mihari.config.censys_id, Mihari.config.censys_secret)
94
94
  end
95
95
  end
96
96
  end
@@ -27,11 +27,11 @@ module Mihari
27
27
  private
28
28
 
29
29
  def config_keys
30
- %w(CIRCL_PASSIVE_USERNAME CIRCL_PASSIVE_PASSWORD)
30
+ %w(circl_passive_password circl_passive_username)
31
31
  end
32
32
 
33
33
  def api
34
- @api ||= ::PassiveCIRCL::API.new
34
+ @api ||= ::PassiveCIRCL::API.new(username: Mihari.config.circl_passive_username, password: Mihari.config.circl_passive_password)
35
35
  end
36
36
 
37
37
  def lookup
@@ -35,15 +35,15 @@ module Mihari
35
35
  PAGE_SIZE = 10
36
36
 
37
37
  def config_keys
38
- %w(ONYPHE_API_KEY)
38
+ %w(onyphe_api_key)
39
39
  end
40
40
 
41
41
  def api
42
- @api ||= ::Onyphe::API.new
42
+ @api ||= ::Onyphe::API.new(Mihari.config.onyphe_api_key)
43
43
  end
44
44
 
45
45
  def search_with_page(query, page: 1)
46
- api.datascan(query, page: page)
46
+ api.simple.datascan(query, page: page)
47
47
  end
48
48
 
49
49
  def search
@@ -30,11 +30,11 @@ module Mihari
30
30
  private
31
31
 
32
32
  def config_keys
33
- %w(PASSIVETOTAL_USERNAME PASSIVETOTAL_API_KEY)
33
+ %w(passivetotal_username passivetotal_api_key)
34
34
  end
35
35
 
36
36
  def api
37
- @api ||= ::PassiveTotal::API.new
37
+ @api ||= ::PassiveTotal::API.new(username: Mihari.config.passivetotal_username, api_key: Mihari.config.passivetotal_api_key)
38
38
  end
39
39
 
40
40
  def valid_type?
@@ -30,11 +30,11 @@ module Mihari
30
30
  private
31
31
 
32
32
  def config_keys
33
- %w(PULSEDIVE_API_KEY)
33
+ %w(pulsedive_api_key)
34
34
  end
35
35
 
36
36
  def api
37
- @api ||= ::Pulsedive::API.new
37
+ @api ||= ::Pulsedive::API.new(Mihari.config.pulsedive_api_key)
38
38
  end
39
39
 
40
40
  def valid_type?
@@ -30,11 +30,11 @@ module Mihari
30
30
  private
31
31
 
32
32
  def config_keys
33
- %w(SECURITYTRAILS_API_KEY)
33
+ %w(securitytrails_api_key)
34
34
  end
35
35
 
36
36
  def api
37
- @api ||= ::SecurityTrails::API.new
37
+ @api ||= ::SecurityTrails::API.new(Mihari.config.securitytrails_api_key)
38
38
  end
39
39
 
40
40
  def valid_type?
@@ -32,11 +32,11 @@ module Mihari
32
32
  private
33
33
 
34
34
  def config_keys
35
- %w(SECURITYTRAILS_API_KEY)
35
+ %w(securitytrails_api_key)
36
36
  end
37
37
 
38
38
  def api
39
- @api ||= ::SecurityTrails::API.new
39
+ @api ||= ::SecurityTrails::API.new(Mihari.config.securitytrails_api_key)
40
40
  end
41
41
 
42
42
  def valid_type?
@@ -36,11 +36,11 @@ module Mihari
36
36
  PAGE_SIZE = 100
37
37
 
38
38
  def config_keys
39
- %w(SHODAN_API_KEY)
39
+ %w(shodan_api_key)
40
40
  end
41
41
 
42
42
  def api
43
- @api ||= ::Shodan::API.new
43
+ @api ||= ::Shodan::API.new(key: Mihari.config.shodan_api_key)
44
44
  end
45
45
 
46
46
  def search_with_page(query, page: 1)
@@ -30,11 +30,11 @@ module Mihari
30
30
  private
31
31
 
32
32
  def config_keys
33
- %w(VIRUSTOTAL_API_KEY)
33
+ %w(virustotal_api_key)
34
34
  end
35
35
 
36
36
  def api
37
- @api = ::VirusTotal::API.new
37
+ @api = ::VirusTotal::API.new(key: Mihari.config.virustotal_api_key)
38
38
  end
39
39
 
40
40
  def valid_type?
@@ -41,11 +41,11 @@ module Mihari
41
41
  end
42
42
 
43
43
  def config_keys
44
- %w(ZOOMEYE_USERNAME ZOOMEYE_PASSWORD)
44
+ %w(zoomeye_password zoomeye_username)
45
45
  end
46
46
 
47
47
  def api
48
- @api ||= ::ZoomEye::API.new
48
+ @api ||= ::ZoomEye::API.new(username: Mihari.config.zoomeye_username, password: Mihari.config.zoomeye_password)
49
49
  end
50
50
 
51
51
  def convert_responses(responses)
@@ -242,17 +242,22 @@ module Mihari
242
242
  artifacts = json.dig("artifacts")
243
243
  tags = json.dig("tags") || []
244
244
 
245
- basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts, tags: tags)
245
+ basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts, source: "json", tags: tags)
246
246
  basic.run
247
247
  end
248
248
  end
249
249
 
250
250
  desc "alerts", "Show the alerts on TheHive"
251
251
  method_option :limit, type: :string, default: "5", desc: "Number of alerts to show (or 'all' to show all the alerts)"
252
+ method_option :title, type: :string, desc: "Title to filter"
253
+ method_option :source, type: :string, desc: "Source to filter"
254
+ method_option :tag, type: :string, desc: "Tag to filter"
252
255
  def alerts
253
256
  with_error_handling do
254
- viewer = AlertViewer.new(limit: options["limit"])
255
- alerts = viewer.list
257
+ load_configuration
258
+
259
+ viewer = AlertViewer.new
260
+ alerts = viewer.list(limit: options["limit"], title: options["title"], source: options["source"], tag: options[:tag])
256
261
  puts JSON.pretty_generate(alerts)
257
262
  end
258
263
  end
@@ -261,6 +266,7 @@ module Mihari
261
266
  def status
262
267
  with_error_handling do
263
268
  load_configuration
269
+
264
270
  puts JSON.pretty_generate(Status.check)
265
271
  end
266
272
  end
@@ -286,7 +292,10 @@ module Mihari
286
292
 
287
293
  def load_configuration
288
294
  config = options["config"]
289
- Config.load_from_yaml(config) if config
295
+ return unless config
296
+
297
+ Config.load_from_yaml(config)
298
+ Database.connect
290
299
  end
291
300
 
292
301
  def run_analyzer(analyzer_class, query:, options:)
@@ -4,6 +4,58 @@ 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 :passivetotal_api_key
16
+ attr_accessor :passivetotal_username
17
+ attr_accessor :pulsedive_api_key
18
+ attr_accessor :securitytrails_api_key
19
+ attr_accessor :shodan_api_key
20
+ attr_accessor :slack_channel
21
+ attr_accessor :slack_webhook_url
22
+ attr_accessor :thehive_api_endpoint
23
+ attr_accessor :thehive_api_key
24
+ attr_accessor :virustotal_api_key
25
+ attr_accessor :zoomeye_password
26
+ attr_accessor :zoomeye_username
27
+
28
+ attr_accessor :database
29
+
30
+ def initialize
31
+ load_from_env
32
+ end
33
+
34
+ def load_from_env
35
+ @binaryedge_api_key = ENV["BINARYEDGE_API_KEY"]
36
+ @censys_id = ENV["CENSYS_ID"]
37
+ @censys_secret = ENV["CENSYS_SECRET"]
38
+ @circl_passive_password = ENV["CIRCL_PASSIVE_PASSWORD"]
39
+ @circl_passive_username = ENV["CIRCL_PASSIVE_USERNAME"]
40
+ @misp_api_endpoint = ENV["MISP_API_ENDPOINT"]
41
+ @misp_api_key = ENV["MISP_API_KEY"]
42
+ @onyphe_api_key = ENV["ONYPHE_API_KEY"]
43
+ @passivetotal_api_key = ENV["PASSIVETOTAL_API_KEY"]
44
+ @passivetotal_username = ENV["PASSIVETOTAL_USERNAME"]
45
+ @pulsedive_api_key = ENV["PULSEDIVE_API_KEY"]
46
+ @securitytrails_api_key = ENV["SECURITYTRAILS_API_KEY"]
47
+ @shodan_api_key = ENV["SHODAN_API_KEY"]
48
+ @slack_channel = ENV["SLACK_CHANNEL"]
49
+ @slack_webhook_url = ENV["SLACK_WEBHOOK_URL"]
50
+ @thehive_api_endpoint = ENV["THEHIVE_API_ENDPOINT"]
51
+ @thehive_api_key = ENV["THEHIVE_API_KEY"]
52
+ @virustotal_api_key = ENV["VIRUSTOTAL_API_KEY"]
53
+ @zoomeye_password = ENV["ZOOMEYE_PASSWORD"]
54
+ @zoomeye_username = ENV["ZOOMEYE_USERNAME"]
55
+
56
+ @database = ENV["DATABASE"] || "mihari.db"
57
+ end
58
+
7
59
  class << self
8
60
  def load_from_yaml(path)
9
61
  raise ArgumentError, "#{path} does not exist." unless File.exist?(path)
@@ -15,10 +67,24 @@ module Mihari
15
67
  return
16
68
  end
17
69
 
18
- yaml.each do |key, value|
19
- ENV[key.upcase] = value
70
+ Mihari.configure do |config|
71
+ yaml.each do |key, value|
72
+ config.send("#{key.downcase}=".to_sym, value)
73
+ end
20
74
  end
21
75
  end
22
76
  end
23
77
  end
78
+
79
+ class << self
80
+ def config
81
+ @config ||= Config.new
82
+ end
83
+
84
+ attr_writer :config
85
+
86
+ def configure
87
+ yield config
88
+ end
89
+ end
24
90
  end
@@ -3,7 +3,7 @@
3
3
  module Mihari
4
4
  module Configurable
5
5
  def configured?
6
- config_keys.all? { |key| ENV.key? key }
6
+ config_keys.all? { |key| Mihari.config.send(key) }
7
7
  end
8
8
 
9
9
  def configuration_status
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ class InitialSchema < ActiveRecord::Migration[6.0]
6
+ def change
7
+ create_table :tags, if_not_exists: true do |t|
8
+ t.string :name, null: false
9
+ end
10
+
11
+ create_table :alerts, if_not_exists: true do |t|
12
+ t.string :title, null: false
13
+ t.string :description, null: true
14
+ t.string :source, null: false
15
+ t.timestamps
16
+ end
17
+
18
+ create_table :artifacts, if_not_exists: true do |t|
19
+ t.string :data, null: false
20
+ t.string :data_type, null: false
21
+ t.belongs_to :alert, foreign_key: true
22
+ t.timestamps
23
+ end
24
+
25
+ create_table :taggings, if_not_exists: true do |t|
26
+ t.integer :tag_id
27
+ t.integer :alert_id
28
+ end
29
+
30
+ add_index :taggings, :tag_id, if_not_exists: true
31
+ add_index :taggings, [:tag_id, :alert_id], unique: true, if_not_exists: true
32
+ end
33
+ end
34
+
35
+ def adapter
36
+ return "postgresql" if Mihari.config.database.start_with?("postgresql://", "postgres://")
37
+
38
+ "sqlite3"
39
+ end
40
+
41
+ module Mihari
42
+ class Database
43
+ class << self
44
+ def connect
45
+ case adapter
46
+ when "postgresql"
47
+ ActiveRecord::Base.establish_connection(Mihari.config.database)
48
+ else
49
+ ActiveRecord::Base.establish_connection(
50
+ adapter: adapter,
51
+ database: Mihari.config.database
52
+ )
53
+ end
54
+
55
+ ActiveRecord::Migration.verbose = false
56
+ InitialSchema.migrate(:up)
57
+ rescue StandardError
58
+ # Do nothing
59
+ end
60
+
61
+ def destroy!
62
+ InitialSchema.migrate(:down) if ActiveRecord::Base.connected?
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ Mihari::Database.connect