mihari 0.17.5 → 1.0.0

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