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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +155 -0
- data/.travis.yml +7 -1
- data/Gemfile +2 -0
- data/README.md +41 -72
- data/config/pre_commit.yml +3 -0
- data/docker/Dockerfile +1 -1
- data/lib/mihari.rb +12 -8
- data/lib/mihari/alert_viewer.rb +16 -34
- data/lib/mihari/analyzers/base.rb +7 -19
- data/lib/mihari/analyzers/basic.rb +3 -1
- data/lib/mihari/analyzers/binaryedge.rb +3 -3
- data/lib/mihari/analyzers/censys.rb +2 -2
- data/lib/mihari/analyzers/circl.rb +2 -2
- data/lib/mihari/analyzers/onyphe.rb +3 -3
- data/lib/mihari/analyzers/passivetotal.rb +2 -2
- data/lib/mihari/analyzers/pulsedive.rb +2 -2
- data/lib/mihari/analyzers/securitytrails.rb +2 -2
- data/lib/mihari/analyzers/securitytrails_domain_feed.rb +2 -2
- data/lib/mihari/analyzers/shodan.rb +2 -2
- data/lib/mihari/analyzers/virustotal.rb +2 -2
- data/lib/mihari/analyzers/zoomeye.rb +2 -2
- data/lib/mihari/cli.rb +13 -4
- data/lib/mihari/config.rb +68 -2
- data/lib/mihari/configurable.rb +1 -1
- data/lib/mihari/database.rb +68 -0
- data/lib/mihari/emitters/base.rb +1 -1
- data/lib/mihari/emitters/database.rb +29 -0
- data/lib/mihari/emitters/misp.rb +8 -1
- data/lib/mihari/emitters/slack.rb +4 -2
- data/lib/mihari/emitters/stdout.rb +2 -1
- data/lib/mihari/emitters/the_hive.rb +28 -14
- data/lib/mihari/models/alert.rb +11 -0
- data/lib/mihari/models/artifact.rb +27 -0
- data/lib/mihari/models/tag.rb +10 -0
- data/lib/mihari/models/tagging.rb +10 -0
- data/lib/mihari/notifiers/slack.rb +7 -4
- data/lib/mihari/serializers/alert.rb +12 -0
- data/lib/mihari/serializers/artifact.rb +9 -0
- data/lib/mihari/serializers/tag.rb +9 -0
- data/lib/mihari/slack_monkeypatch.rb +16 -0
- data/lib/mihari/status.rb +1 -1
- data/lib/mihari/type_checker.rb +1 -1
- data/lib/mihari/version.rb +1 -1
- data/mihari.gemspec +13 -6
- metadata +140 -36
- data/lib/mihari/artifact.rb +0 -36
- data/lib/mihari/cache.rb +0 -35
- data/lib/mihari/the_hive.rb +0 -42
- data/lib/mihari/the_hive/alert.rb +0 -25
- data/lib/mihari/the_hive/artifact.rb +0 -33
- 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
|
-
|
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 "
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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(
|
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)
|
data/lib/mihari/cli.rb
CHANGED
@@ -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
|
-
|
255
|
-
|
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
|
-
|
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:)
|
data/lib/mihari/config.rb
CHANGED
@@ -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
|
-
|
19
|
-
|
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
|
data/lib/mihari/configurable.rb
CHANGED
@@ -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
|