mihari 0.17.4 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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