mihari 0.17.5 → 1.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.rubocop.yml +155 -0
  4. data/.travis.yml +1 -0
  5. data/Gemfile +2 -0
  6. data/README.md +30 -72
  7. data/config/pre_commit.yml +3 -0
  8. data/lib/mihari.rb +12 -8
  9. data/lib/mihari/alert_viewer.rb +6 -28
  10. data/lib/mihari/analyzers/base.rb +7 -19
  11. data/lib/mihari/analyzers/basic.rb +3 -1
  12. data/lib/mihari/analyzers/binaryedge.rb +2 -2
  13. data/lib/mihari/analyzers/censys.rb +2 -2
  14. data/lib/mihari/analyzers/circl.rb +2 -2
  15. data/lib/mihari/analyzers/onyphe.rb +3 -3
  16. data/lib/mihari/analyzers/passivetotal.rb +2 -2
  17. data/lib/mihari/analyzers/pulsedive.rb +2 -2
  18. data/lib/mihari/analyzers/securitytrails.rb +2 -2
  19. data/lib/mihari/analyzers/securitytrails_domain_feed.rb +2 -2
  20. data/lib/mihari/analyzers/shodan.rb +2 -2
  21. data/lib/mihari/analyzers/virustotal.rb +2 -2
  22. data/lib/mihari/analyzers/zoomeye.rb +2 -2
  23. data/lib/mihari/cli.rb +2 -2
  24. data/lib/mihari/config.rb +68 -2
  25. data/lib/mihari/configurable.rb +1 -1
  26. data/lib/mihari/database.rb +45 -0
  27. data/lib/mihari/emitters/base.rb +1 -1
  28. data/lib/mihari/emitters/misp.rb +8 -1
  29. data/lib/mihari/emitters/slack.rb +2 -2
  30. data/lib/mihari/emitters/sqlite.rb +29 -0
  31. data/lib/mihari/emitters/stdout.rb +2 -1
  32. data/lib/mihari/emitters/the_hive.rb +28 -14
  33. data/lib/mihari/models/alert.rb +11 -0
  34. data/lib/mihari/models/artifact.rb +27 -0
  35. data/lib/mihari/models/tag.rb +10 -0
  36. data/lib/mihari/models/tagging.rb +10 -0
  37. data/lib/mihari/notifiers/slack.rb +4 -4
  38. data/lib/mihari/serializers/alert.rb +12 -0
  39. data/lib/mihari/serializers/artifact.rb +9 -0
  40. data/lib/mihari/serializers/tag.rb +9 -0
  41. data/lib/mihari/status.rb +1 -1
  42. data/lib/mihari/type_checker.rb +1 -1
  43. data/lib/mihari/version.rb +1 -1
  44. data/mihari.gemspec +11 -5
  45. metadata +120 -31
  46. data/lib/mihari/artifact.rb +0 -36
  47. data/lib/mihari/cache.rb +0 -35
  48. data/lib/mihari/the_hive.rb +0 -42
  49. data/lib/mihari/the_hive/alert.rb +0 -25
  50. data/lib/mihari/the_hive/artifact.rb +0 -33
  51. data/lib/mihari/the_hive/base.rb +0 -14
@@ -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
@@ -52,11 +52,11 @@ module Mihari
52
52
  end
53
53
 
54
54
  def config_keys
55
- %w(BINARYEDGE_API_KEY)
55
+ [Mihari.config.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
+ [Mihari.config.censys_id, Mihari.config.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
+ [Mihari.config.circl_passive_password, Mihari.config.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
+ [Mihari.config.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
+ [Mihari.config.passivetotal_username, Mihari.config.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
+ [Mihari.config.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
+ [Mihari.config.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
+ [Mihari.config.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
+ [Mihari.config.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
+ [Mihari.config.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
+ [Mihari.config.zoomeye_password, Mihari.config.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,7 +242,7 @@ 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
@@ -286,7 +286,7 @@ module Mihari
286
286
 
287
287
  def load_configuration
288
288
  config = options["config"]
289
- Config.load_from_yaml(config) if config
289
+ Config.load_configuration(config) if config
290
290
  end
291
291
 
292
292
  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| !key.nil? }
7
7
  end
8
8
 
9
9
  def configuration_status
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ ActiveRecord::Base.establish_connection(
6
+ adapter: "sqlite3",
7
+ database: Mihari.config.database
8
+ )
9
+
10
+ class InitialSchema < ActiveRecord::Migration[6.0]
11
+ def change
12
+ create_table :tags, if_not_exists: true do |t|
13
+ t.string :name, null: false
14
+ end
15
+
16
+ create_table :alerts, if_not_exists: true do |t|
17
+ t.string :title, null: false
18
+ t.string :description, null: true
19
+ t.string :source, null: false
20
+ t.timestamps
21
+ end
22
+
23
+ create_table :artifacts, if_not_exists: true do |t|
24
+ t.string :data, null: false
25
+ t.string :data_type, null: false
26
+ t.belongs_to :alert, foreign_key: true
27
+ t.timestamps
28
+ end
29
+
30
+ create_table :taggings, if_not_exists: true do |t|
31
+ t.integer :tag_id
32
+ t.integer :alert_id
33
+ end
34
+
35
+ add_index :taggings, :tag_id, if_not_exists: true
36
+ add_index :taggings, [:tag_id, :alert_id], unique: true, if_not_exists: true
37
+ end
38
+ end
39
+
40
+ begin
41
+ ActiveRecord::Migration.verbose = false
42
+ InitialSchema.migrate(:up)
43
+ rescue StandardError
44
+ # Do nothing
45
+ end
@@ -16,7 +16,7 @@ module Mihari
16
16
  end
17
17
 
18
18
  def run(**params)
19
- retry_on_error { emit(params) }
19
+ retry_on_error { emit(**params) }
20
20
  end
21
21
 
22
22
  def emit(*)
@@ -6,6 +6,13 @@ require "net/ping"
6
6
  module Mihari
7
7
  module Emitters
8
8
  class MISP < Base
9
+ def initialize
10
+ ::MISP.configure do |config|
11
+ config.api_endpoint = Mihari.config.misp_api_endpoint
12
+ config.api_key = Mihari.config.misp_api_key
13
+ end
14
+ end
15
+
9
16
  # @return [true, false]
10
17
  def valid?
11
18
  api_endpoint? && api_key? && ping?
@@ -28,7 +35,7 @@ module Mihari
28
35
  private
29
36
 
30
37
  def config_keys
31
- %w(MISP_API_ENDPOINT MISP_API_KEY)
38
+ [Mihari.config.misp_api_endpoint, Mihari.config.misp_api_key]
32
39
  end
33
40
 
34
41
  def build_attribute(artifact)
@@ -123,7 +123,7 @@ module Mihari
123
123
  ].join("\n")
124
124
  end
125
125
 
126
- def emit(title:, description:, artifacts:, tags: [])
126
+ def emit(title:, description:, artifacts:, tags: [], **_options)
127
127
  return if artifacts.empty?
128
128
 
129
129
  attachments = to_attachments(artifacts)
@@ -135,7 +135,7 @@ module Mihari
135
135
  private
136
136
 
137
137
  def config_keys
138
- %w(SLACK_WEBHOOK_URL)
138
+ [Mihari.config.slack_webhook_url]
139
139
  end
140
140
  end
141
141
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Emitters
5
+ class SQLite < Base
6
+ def valid?
7
+ true
8
+ end
9
+
10
+ def emit(title:, description:, artifacts:, source:, tags: [])
11
+ return if artifacts.empty?
12
+
13
+ tags = tags.map { |name| Tag.find_or_create_by(name: name) }.compact.uniq
14
+ taggings = tags.map { |tag| Tagging.new(tag_id: tag.id) }
15
+
16
+ alert = Alert.new(
17
+ title: title,
18
+ description: description,
19
+ artifacts: artifacts,
20
+ source: source,
21
+ taggings: taggings
22
+ )
23
+
24
+ alert.save
25
+ alert
26
+ end
27
+ end
28
+ end
29
+ end
@@ -9,11 +9,12 @@ module Mihari
9
9
  true
10
10
  end
11
11
 
12
- def emit(title:, description:, artifacts:, tags:)
12
+ def emit(title:, description:, artifacts:, source:, tags:)
13
13
  h = {
14
14
  title: title,
15
15
  description: description,
16
16
  artifacts: artifacts.map(&:data),
17
+ source: source,
17
18
  tags: tags
18
19
  }
19
20
  puts JSON.pretty_generate(h)
@@ -1,42 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "hachi"
4
+ require "net/ping"
5
+
3
6
  module Mihari
4
7
  module Emitters
5
8
  class TheHive < Base
6
9
  # @return [true, false]
7
10
  def valid?
8
- the_hive.valid?
11
+ api_endpont? && api_key? && ping?
9
12
  end
10
13
 
11
- def emit(title:, description:, artifacts:, tags: [])
14
+ def emit(title:, description:, artifacts:, tags: [], **_options)
12
15
  return if artifacts.empty?
13
16
 
14
- the_hive.alert.create(
17
+ api.alert.create(
15
18
  title: title,
16
19
  description: description,
17
- artifacts: artifacts.map(&:to_h),
18
- tags: tags
20
+ artifacts: artifacts.map { |artifact| { data: artifact.data, data_type: artifact.data_type, message: description } },
21
+ tags: tags,
22
+ type: "external",
23
+ source: "mihari"
19
24
  )
20
-
21
- save_as_cache artifacts.map(&:data)
22
25
  end
23
26
 
24
27
  private
25
28
 
26
29
  def config_keys
27
- %w(THEHIVE_API_ENDPOINT THEHIVE_API_KEY)
30
+ [Mihari.config.thehive_api_endpoint, Mihari.config.thehive_api_key]
28
31
  end
29
32
 
30
- def the_hive
31
- @the_hive ||= Mihari::TheHive.new
33
+ def api
34
+ @api ||= Hachi::API.new(api_endpoint: Mihari.config.thehive_api_endpoint, api_key: Mihari.config.thehive_api_key)
32
35
  end
33
36
 
34
- def cache
35
- @cache ||= Cache.new
37
+ # @return [true, false]
38
+ def api_endpont?
39
+ !Mihari.config.thehive_api_endpoint.nil?
36
40
  end
37
41
 
38
- def save_as_cache(data)
39
- cache.save data
42
+ # @return [true, false]
43
+ def api_key?
44
+ !Mihari.config.thehive_api_key.nil?
45
+ end
46
+
47
+ def ping?
48
+ base_url = Mihari.config.thehive_api_endpoint
49
+ base_url = base_url.end_with?("/") ? base_url[0..-2] : base_url
50
+ url = "#{base_url}/index.html"
51
+
52
+ http = Net::Ping::HTTP.new(url)
53
+ http.ping?
40
54
  end
41
55
  end
42
56
  end