mihari 2.3.1 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +7 -0
- data/.overcommit.yml +12 -0
- data/README.md +1 -9
- data/docker/Dockerfile +1 -1
- data/exe/mihari +1 -1
- data/lib/mihari.rb +89 -15
- data/lib/mihari/analyzers/base.rb +49 -8
- data/lib/mihari/analyzers/basic.rb +1 -2
- data/lib/mihari/analyzers/binaryedge.rb +7 -13
- data/lib/mihari/analyzers/censys.rb +26 -63
- data/lib/mihari/analyzers/circl.rb +20 -17
- data/lib/mihari/analyzers/crtsh.rb +6 -13
- data/lib/mihari/analyzers/dnpedia.rb +6 -12
- data/lib/mihari/analyzers/dnstwister.rb +13 -10
- data/lib/mihari/analyzers/onyphe.rb +6 -12
- data/lib/mihari/analyzers/otx.rb +22 -19
- data/lib/mihari/analyzers/passivetotal.rb +22 -21
- data/lib/mihari/analyzers/pulsedive.rb +16 -13
- data/lib/mihari/analyzers/rule.rb +97 -0
- data/lib/mihari/analyzers/securitytrails.rb +22 -19
- data/lib/mihari/analyzers/shodan.rb +7 -13
- data/lib/mihari/analyzers/spyse.rb +12 -19
- data/lib/mihari/analyzers/urlscan.rb +22 -27
- data/lib/mihari/analyzers/virustotal.rb +25 -22
- data/lib/mihari/analyzers/zoomeye.rb +14 -20
- data/lib/mihari/cli/analyzer.rb +44 -0
- data/lib/mihari/cli/base.rb +27 -0
- data/lib/mihari/cli/init.rb +13 -0
- data/lib/mihari/cli/main.rb +30 -0
- data/lib/mihari/cli/mixins/utils.rb +88 -0
- data/lib/mihari/cli/validator.rb +11 -0
- data/lib/mihari/commands/binaryedge.rb +1 -1
- data/lib/mihari/commands/censys.rb +1 -1
- data/lib/mihari/commands/circl.rb +2 -2
- data/lib/mihari/commands/crtsh.rb +1 -1
- data/lib/mihari/commands/dnpedia.rb +1 -1
- data/lib/mihari/commands/dnstwister.rb +2 -2
- data/lib/mihari/commands/init.rb +46 -0
- data/lib/mihari/commands/json.rb +1 -1
- data/lib/mihari/commands/onyphe.rb +1 -1
- data/lib/mihari/commands/otx.rb +2 -2
- data/lib/mihari/commands/passivetotal.rb +2 -2
- data/lib/mihari/commands/pulsedive.rb +2 -2
- data/lib/mihari/commands/search.rb +77 -0
- data/lib/mihari/commands/securitytrails.rb +2 -2
- data/lib/mihari/commands/shodan.rb +1 -1
- data/lib/mihari/commands/spyse.rb +1 -1
- data/lib/mihari/commands/urlscan.rb +2 -2
- data/lib/mihari/commands/validator.rb +38 -0
- data/lib/mihari/commands/virustotal.rb +2 -2
- data/lib/mihari/commands/zoomeye.rb +1 -1
- data/lib/mihari/constraints.rb +5 -0
- data/lib/mihari/database.rb +13 -2
- data/lib/mihari/emitters/base.rb +2 -2
- data/lib/mihari/emitters/database.rb +1 -1
- data/lib/mihari/emitters/misp.rb +3 -1
- data/lib/mihari/emitters/slack.rb +6 -10
- data/lib/mihari/emitters/the_hive.rb +1 -1
- data/lib/mihari/emitters/webhook.rb +53 -0
- data/lib/mihari/mixins/configurable.rb +38 -0
- data/lib/mihari/mixins/configuration.rb +90 -0
- data/lib/mihari/mixins/hash.rb +20 -0
- data/lib/mihari/mixins/refang.rb +21 -0
- data/lib/mihari/mixins/retriable.rb +27 -0
- data/lib/mihari/mixins/rule.rb +79 -0
- data/lib/mihari/models/alert.rb +28 -1
- data/lib/mihari/models/artifact.rb +11 -1
- data/lib/mihari/notifiers/base.rb +9 -1
- data/lib/mihari/notifiers/exception_notifier.rb +50 -0
- data/lib/mihari/notifiers/slack.rb +29 -1
- data/lib/mihari/schemas/configuration.rb +42 -0
- data/lib/mihari/schemas/macros.rb +17 -0
- data/lib/mihari/schemas/rule.rb +72 -0
- data/lib/mihari/serializers/artifact.rb +1 -1
- data/lib/mihari/status.rb +14 -0
- data/lib/mihari/templates/rule.yml.erb +19 -0
- data/lib/mihari/type_checker.rb +8 -3
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/controllers/base_controller.rb +1 -1
- data/lib/mihari/web/public/index.html +1 -21
- data/lib/mihari/web/public/redoc-static.html +2 -2
- data/lib/mihari/web/public/static/js/app.ab213f7c.js +12 -0
- data/lib/mihari/web/public/static/js/app.ab213f7c.js.map +1 -0
- data/mihari.gemspec +19 -12
- metadata +138 -65
- data/.rubocop.yml +0 -161
- data/lib/mihari/analyzers/free_text.rb +0 -48
- data/lib/mihari/analyzers/http_hash.rb +0 -100
- data/lib/mihari/analyzers/passive_dns.rb +0 -59
- data/lib/mihari/analyzers/passive_ssl.rb +0 -55
- data/lib/mihari/analyzers/reverse_whois.rb +0 -55
- data/lib/mihari/analyzers/securitytrails_domain_feed.rb +0 -59
- data/lib/mihari/analyzers/ssh_fingerprint.rb +0 -58
- data/lib/mihari/cli.rb +0 -126
- data/lib/mihari/commands/config.rb +0 -27
- data/lib/mihari/commands/free_text.rb +0 -21
- data/lib/mihari/commands/http_hash.rb +0 -25
- data/lib/mihari/commands/passive_dns.rb +0 -21
- data/lib/mihari/commands/passive_ssl.rb +0 -21
- data/lib/mihari/commands/reverse_whois.rb +0 -21
- data/lib/mihari/commands/securitytrails_domain_feed.rb +0 -23
- data/lib/mihari/commands/ssh_fingerprint.rb +0 -21
- data/lib/mihari/config.rb +0 -83
- data/lib/mihari/configurable.rb +0 -21
- data/lib/mihari/html.rb +0 -43
- data/lib/mihari/retriable.rb +0 -17
- data/lib/mihari/slack_monkeypatch.rb +0 -16
@@ -5,13 +5,13 @@ module Mihari
|
|
5
5
|
module SecurityTrails
|
6
6
|
def self.included(thor)
|
7
7
|
thor.class_eval do
|
8
|
-
desc "securitytrails [IP|DOMAIN|EMAIL]", "SecurityTrails
|
8
|
+
desc "securitytrails [IP|DOMAIN|EMAIL]", "SecurityTrails search by an ip, domain or email"
|
9
9
|
method_option :title, type: :string, desc: "title"
|
10
10
|
method_option :description, type: :string, desc: "description"
|
11
11
|
method_option :tags, type: :array, desc: "tags"
|
12
12
|
def securitytrails(indiactor)
|
13
13
|
with_error_handling do
|
14
|
-
run_analyzer Analyzers::SecurityTrails, query:
|
14
|
+
run_analyzer Analyzers::SecurityTrails, query: indiactor, options: options
|
15
15
|
end
|
16
16
|
end
|
17
17
|
map "st" => :securitytrails
|
@@ -5,7 +5,7 @@ module Mihari
|
|
5
5
|
module Shodan
|
6
6
|
def self.included(thor)
|
7
7
|
thor.class_eval do
|
8
|
-
desc "shodan [QUERY]", "Shodan host search
|
8
|
+
desc "shodan [QUERY]", "Shodan host search"
|
9
9
|
method_option :title, type: :string, desc: "title"
|
10
10
|
method_option :description, type: :string, desc: "description"
|
11
11
|
method_option :tags, type: :array, desc: "tags"
|
@@ -5,7 +5,7 @@ module Mihari
|
|
5
5
|
module Spyse
|
6
6
|
def self.included(thor)
|
7
7
|
thor.class_eval do
|
8
|
-
desc "spyse [QUERY]", "Spyse search
|
8
|
+
desc "spyse [QUERY]", "Spyse search"
|
9
9
|
method_option :title, type: :string, desc: "title"
|
10
10
|
method_option :description, type: :string, desc: "description"
|
11
11
|
method_option :tags, type: :array, desc: "tags"
|
@@ -5,11 +5,11 @@ module Mihari
|
|
5
5
|
module Urlscan
|
6
6
|
def self.included(thor)
|
7
7
|
thor.class_eval do
|
8
|
-
desc "urlscan [QUERY]", "urlscan search
|
8
|
+
desc "urlscan [QUERY]", "urlscan search"
|
9
9
|
method_option :title, type: :string, desc: "title"
|
10
10
|
method_option :description, type: :string, desc: "description"
|
11
11
|
method_option :tags, type: :array, desc: "tags"
|
12
|
-
method_option :target_type, type: :string, default: "url", desc: "target type to fetch from
|
12
|
+
method_option :target_type, type: :string, default: "url", desc: "target type to fetch from search results (target type should be 'url', 'domain' or 'ip')"
|
13
13
|
method_option :use_similarity, type: :boolean, default: false, desc: "use similarity API or not"
|
14
14
|
def urlscan(query)
|
15
15
|
with_error_handling do
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Commands
|
5
|
+
module Validator
|
6
|
+
include Mixins::Rule
|
7
|
+
include Mixins::Configuration
|
8
|
+
|
9
|
+
def self.included(thor)
|
10
|
+
thor.class_eval do
|
11
|
+
desc "rule [PATH]", "Validate format of a rule file"
|
12
|
+
def rule(path)
|
13
|
+
# convert str(YAML) to hash or str(path/YAML file) to hash
|
14
|
+
rule = load_rule(path)
|
15
|
+
|
16
|
+
# validate rule schema
|
17
|
+
validate_rule rule
|
18
|
+
|
19
|
+
puts "Valid format. The input is parsed as the following:"
|
20
|
+
puts rule.to_yaml
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "config [PATH]", "Validate format of a config file"
|
24
|
+
def config(path)
|
25
|
+
# convert str(YAML) to hash or str(path/YAML file) to hash
|
26
|
+
config = load_config(path)
|
27
|
+
|
28
|
+
# validate config schema
|
29
|
+
validate_config config
|
30
|
+
|
31
|
+
puts "Valid format. The input is parsed as the following:"
|
32
|
+
puts config.to_yaml
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -5,13 +5,13 @@ module Mihari
|
|
5
5
|
module VirusTotal
|
6
6
|
def self.included(thor)
|
7
7
|
thor.class_eval do
|
8
|
-
desc "virustotal [IP|DOMAIN]", "VirusTotal resolutions
|
8
|
+
desc "virustotal [IP|DOMAIN]", "VirusTotal resolutions search by an ip or domain"
|
9
9
|
method_option :title, type: :string, desc: "title"
|
10
10
|
method_option :description, type: :string, desc: "description"
|
11
11
|
method_option :tags, type: :array, desc: "tags"
|
12
12
|
def virustotal(indiactor)
|
13
13
|
with_error_handling do
|
14
|
-
run_analyzer Analyzers::VirusTotal, query:
|
14
|
+
run_analyzer Analyzers::VirusTotal, query: indiactor, options: options
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
@@ -5,7 +5,7 @@ module Mihari
|
|
5
5
|
module ZoomEye
|
6
6
|
def self.included(thor)
|
7
7
|
thor.class_eval do
|
8
|
-
desc "zoomeye [QUERY]", "ZoomEye search
|
8
|
+
desc "zoomeye [QUERY]", "ZoomEye search"
|
9
9
|
method_option :title, type: :string, desc: "title"
|
10
10
|
method_option :description, type: :string, desc: "description"
|
11
11
|
method_option :tags, type: :array, desc: "tags"
|
data/lib/mihari/database.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require "active_record"
|
4
4
|
|
5
|
-
class InitialSchema < ActiveRecord::Migration[6.
|
5
|
+
class InitialSchema < ActiveRecord::Migration[6.1]
|
6
6
|
def change
|
7
7
|
create_table :tags, if_not_exists: true do |t|
|
8
8
|
t.string :name, null: false
|
@@ -32,6 +32,12 @@ class InitialSchema < ActiveRecord::Migration[6.0]
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
class V3Schema < ActiveRecord::Migration[6.1]
|
36
|
+
def change
|
37
|
+
add_column :artifacts, :source, :string, if_not_exists: true
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
35
41
|
def adapter
|
36
42
|
return "postgresql" if Mihari.config.database.start_with?("postgresql://", "postgres://")
|
37
43
|
return "mysql2" if Mihari.config.database.start_with?("mysql2://")
|
@@ -54,7 +60,9 @@ module Mihari
|
|
54
60
|
end
|
55
61
|
|
56
62
|
ActiveRecord::Migration.verbose = false
|
63
|
+
|
57
64
|
InitialSchema.migrate(:up)
|
65
|
+
V3Schema.migrate(:up)
|
58
66
|
rescue StandardError
|
59
67
|
# Do nothing
|
60
68
|
end
|
@@ -65,7 +73,10 @@ module Mihari
|
|
65
73
|
end
|
66
74
|
|
67
75
|
def destroy!
|
68
|
-
|
76
|
+
return unless ActiveRecord::Base.connected?
|
77
|
+
|
78
|
+
InitialSchema.migrate(:down)
|
79
|
+
V3Schema.migrate(:down)
|
69
80
|
end
|
70
81
|
end
|
71
82
|
end
|
data/lib/mihari/emitters/base.rb
CHANGED
@@ -10,7 +10,7 @@ module Mihari
|
|
10
10
|
def emit(title:, description:, artifacts:, source:, tags: [])
|
11
11
|
return if artifacts.empty?
|
12
12
|
|
13
|
-
tags = tags.
|
13
|
+
tags = tags.filter_map { |name| Tag.find_or_create_by(name: name) }.uniq
|
14
14
|
taggings = tags.map { |tag| Tagging.new(tag_id: tag.id) }
|
15
15
|
|
16
16
|
alert = Alert.new(
|
data/lib/mihari/emitters/misp.rb
CHANGED
@@ -21,6 +21,8 @@ module Mihari
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def emit(title:, artifacts:, tags: [], **_options)
|
24
|
+
return if artifacts.empty?
|
25
|
+
|
24
26
|
event = ::MISP::Event.new(info: title)
|
25
27
|
|
26
28
|
artifacts.each do |artifact|
|
@@ -36,7 +38,7 @@ module Mihari
|
|
36
38
|
|
37
39
|
private
|
38
40
|
|
39
|
-
def
|
41
|
+
def configuration_keys
|
40
42
|
%w[misp_api_endpoint misp_api_key]
|
41
43
|
end
|
42
44
|
|
@@ -2,21 +2,17 @@
|
|
2
2
|
|
3
3
|
require "slack-notifier"
|
4
4
|
require "digest/sha2"
|
5
|
-
require "
|
6
|
-
|
7
|
-
require "mihari/slack_monkeypatch"
|
5
|
+
require "dry-initializer"
|
8
6
|
|
9
7
|
module Mihari
|
10
8
|
module Emitters
|
11
9
|
class Attachment
|
12
10
|
include Mem
|
13
11
|
|
14
|
-
|
12
|
+
extend Dry::Initializer
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
@data_type = data_type
|
19
|
-
end
|
14
|
+
option :data
|
15
|
+
option :data_type
|
20
16
|
|
21
17
|
def actions
|
22
18
|
[vt_link, urlscan_link, censys_link, shodan_link].compact
|
@@ -91,7 +87,7 @@ module Mihari
|
|
91
87
|
memoize :_vt_link
|
92
88
|
|
93
89
|
def _censys_link
|
94
|
-
data_type == "ip" ? "https://censys.io/
|
90
|
+
data_type == "ip" ? "https://search.censys.io/hosts/#{data}" : nil
|
95
91
|
end
|
96
92
|
memoize :_censys_link
|
97
93
|
|
@@ -147,7 +143,7 @@ module Mihari
|
|
147
143
|
|
148
144
|
private
|
149
145
|
|
150
|
-
def
|
146
|
+
def configuration_keys
|
151
147
|
%w[slack_webhook_url]
|
152
148
|
end
|
153
149
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "net/http"
|
5
|
+
require "uri"
|
6
|
+
|
7
|
+
module Mihari
|
8
|
+
module Emitters
|
9
|
+
class Webhook < Base
|
10
|
+
# @return [true, false]
|
11
|
+
def valid?
|
12
|
+
webhook_url?
|
13
|
+
end
|
14
|
+
|
15
|
+
def emit(title:, description:, artifacts:, source:, tags:)
|
16
|
+
return if artifacts.empty?
|
17
|
+
|
18
|
+
uri = URI(Mihari.config.webhook_url)
|
19
|
+
data = {
|
20
|
+
title: title,
|
21
|
+
description: description,
|
22
|
+
artifacts: artifacts.map(&:data),
|
23
|
+
source: source,
|
24
|
+
tags: tags
|
25
|
+
}
|
26
|
+
|
27
|
+
if use_json_body
|
28
|
+
Net::HTTP.post(uri, data.to_json, "Content-Type" => "application/json")
|
29
|
+
else
|
30
|
+
Net::HTTP.post_form(uri, data)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def configuration_keys
|
37
|
+
%w[webhook_url]
|
38
|
+
end
|
39
|
+
|
40
|
+
def webhook_url
|
41
|
+
@webhook_url ||= Mihari.config.webhook_url
|
42
|
+
end
|
43
|
+
|
44
|
+
def webhook_url?
|
45
|
+
!webhook_url.nil?
|
46
|
+
end
|
47
|
+
|
48
|
+
def use_json_body
|
49
|
+
@use_json_body ||= Mihari.config.webhook_use_json_body
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Mihari
|
4
|
+
module Mixins
|
5
|
+
module Configurable
|
6
|
+
#
|
7
|
+
# Check whether it is configured or not
|
8
|
+
#
|
9
|
+
# @return [Boolean]
|
10
|
+
#
|
11
|
+
def configured?
|
12
|
+
configuration_keys.all? { |key| Mihari.config.send(key) }
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# Configuration values
|
17
|
+
#
|
18
|
+
# @return [Array<Hash>, nil] Configuration values as a list of hash. Returns nil if there is any keys.
|
19
|
+
#
|
20
|
+
def configuration_values
|
21
|
+
return nil if configuration_keys.empty?
|
22
|
+
|
23
|
+
configuration_keys.map do |key|
|
24
|
+
{ key: key.upcase, value: Mihari.config.send(key) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Configuration keys
|
30
|
+
#
|
31
|
+
# @return [Array<String>] A list of cofiguration keys
|
32
|
+
#
|
33
|
+
def configuration_keys
|
34
|
+
[]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "colorize"
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
module Mihari
|
7
|
+
module Mixins
|
8
|
+
module Configuration
|
9
|
+
#
|
10
|
+
# Load config file into hash
|
11
|
+
#
|
12
|
+
# @param [String] path Path to YAML file
|
13
|
+
#
|
14
|
+
# @return [Hash]
|
15
|
+
#
|
16
|
+
def load_config(path)
|
17
|
+
data = _load_config(path)
|
18
|
+
data.transform_keys(&:downcase)
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# Validate config schema
|
23
|
+
#
|
24
|
+
# @param [Hash] config
|
25
|
+
#
|
26
|
+
def validate_config(config)
|
27
|
+
error_message = "Failed to parse the input as a config!"
|
28
|
+
|
29
|
+
contract = Schemas::ConfigurationContract.new
|
30
|
+
result = contract.call(config)
|
31
|
+
unless result.errors.empty?
|
32
|
+
puts error_message.colorize(:red)
|
33
|
+
show_validation_errors result.errors
|
34
|
+
raise ArgumentError, "Invalid config schema"
|
35
|
+
end
|
36
|
+
|
37
|
+
# check keys
|
38
|
+
# TODO: check keys with dry-schema
|
39
|
+
valid_keys = Mihari.config.values.keys
|
40
|
+
config.each_key do |key|
|
41
|
+
unless valid_keys.include?(key)
|
42
|
+
puts error_message.colorize(:red)
|
43
|
+
raise ArgumentError, "#{key} is not a valid key."
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
# Returns a template for config
|
50
|
+
#
|
51
|
+
# @return [String] A template for config
|
52
|
+
#
|
53
|
+
def config_template
|
54
|
+
config = Mihari.config.values.keys.map do |key|
|
55
|
+
[key.to_s, nil]
|
56
|
+
end.to_h
|
57
|
+
|
58
|
+
YAML.dump(config)
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Create (blank) config file
|
63
|
+
#
|
64
|
+
# @param [String] filename
|
65
|
+
# @param [Dry::Files] files
|
66
|
+
# @param [String] template
|
67
|
+
#
|
68
|
+
# @return [nil]
|
69
|
+
#
|
70
|
+
def initialize_config_yaml(filename, files = Dry::Files.new, template: config_template)
|
71
|
+
files.write(filename, template)
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def show_validation_errors(errors)
|
77
|
+
errors.messages.each do |message|
|
78
|
+
path = message.path.map(&:to_s).join
|
79
|
+
puts "- #{path} #{message.text}".colorize(:red)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def _load_config(path)
|
84
|
+
return YAML.safe_load(File.read(path), symbolize_names: true) if Pathname(path).exist?
|
85
|
+
|
86
|
+
YAML.safe_load(path, symbolize_names: true)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|