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.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.rubocop.yml +155 -0
- data/.travis.yml +1 -0
- data/Gemfile +2 -0
- data/README.md +30 -72
- data/config/pre_commit.yml +3 -0
- data/lib/mihari.rb +12 -8
- data/lib/mihari/alert_viewer.rb +6 -28
- data/lib/mihari/analyzers/base.rb +7 -19
- data/lib/mihari/analyzers/basic.rb +3 -1
- data/lib/mihari/analyzers/binaryedge.rb +2 -2
- 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 +2 -2
- data/lib/mihari/config.rb +68 -2
- data/lib/mihari/configurable.rb +1 -1
- data/lib/mihari/database.rb +45 -0
- data/lib/mihari/emitters/base.rb +1 -1
- data/lib/mihari/emitters/misp.rb +8 -1
- data/lib/mihari/emitters/slack.rb +2 -2
- data/lib/mihari/emitters/sqlite.rb +29 -0
- 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 +4 -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/status.rb +1 -1
- data/lib/mihari/type_checker.rb +1 -1
- data/lib/mihari/version.rb +1 -1
- data/mihari.gemspec +11 -5
- metadata +120 -31
- 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
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|
data/lib/mihari/cli.rb
CHANGED
@@ -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.
|
289
|
+
Config.load_configuration(config) if config
|
290
290
|
end
|
291
291
|
|
292
292
|
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,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
|
data/lib/mihari/emitters/base.rb
CHANGED
data/lib/mihari/emitters/misp.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
17
|
+
api.alert.create(
|
15
18
|
title: title,
|
16
19
|
description: description,
|
17
|
-
artifacts: artifacts.map
|
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
|
-
|
30
|
+
[Mihari.config.thehive_api_endpoint, Mihari.config.thehive_api_key]
|
28
31
|
end
|
29
32
|
|
30
|
-
def
|
31
|
-
@
|
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
|
-
|
35
|
-
|
37
|
+
# @return [true, false]
|
38
|
+
def api_endpont?
|
39
|
+
!Mihari.config.thehive_api_endpoint.nil?
|
36
40
|
end
|
37
41
|
|
38
|
-
|
39
|
-
|
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
|