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