mihari 2.1.0 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +6 -0
- data/README.md +8 -0
- data/bin/console +1 -0
- data/docker/Dockerfile +1 -1
- data/examples/ipinfo_hosted_domains.rb +1 -1
- data/images/{eyecatch.png → overview.png} +0 -0
- data/images/tines.png +0 -0
- data/images/web_alerts.png +0 -0
- data/images/web_config.png +0 -0
- data/lib/mihari.rb +1 -0
- data/lib/mihari/analyzers/base.rb +10 -1
- data/lib/mihari/analyzers/circl.rb +3 -3
- data/lib/mihari/analyzers/onyphe.rb +2 -2
- data/lib/mihari/analyzers/urlscan.rb +1 -6
- data/lib/mihari/analyzers/zoomeye.rb +2 -2
- data/lib/mihari/cli.rb +12 -3
- data/lib/mihari/commands/config.rb +7 -1
- data/lib/mihari/commands/dnstwister.rb +2 -0
- data/lib/mihari/commands/json.rb +6 -0
- data/lib/mihari/commands/onyphe.rb +2 -0
- data/lib/mihari/commands/passive_dns.rb +2 -0
- data/lib/mihari/commands/securitytrails.rb +2 -0
- data/lib/mihari/commands/shodan.rb +2 -0
- data/lib/mihari/commands/spyse.rb +2 -0
- data/lib/mihari/commands/urlscan.rb +2 -2
- data/lib/mihari/commands/virustotal.rb +2 -0
- data/lib/mihari/commands/web.rb +2 -0
- data/lib/mihari/commands/zoomeye.rb +2 -0
- data/lib/mihari/config.rb +5 -4
- data/lib/mihari/emitters/slack.rb +0 -3
- data/lib/mihari/emitters/webhook.rb +60 -0
- data/lib/mihari/models/artifact.rb +13 -2
- data/lib/mihari/notifiers/slack.rb +0 -1
- data/lib/mihari/status.rb +1 -9
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/app.rb +22 -137
- data/lib/mihari/web/controllers/alerts_controller.rb +75 -0
- data/lib/mihari/web/controllers/artifacts_controller.rb +24 -0
- data/lib/mihari/web/controllers/base_controller.rb +22 -0
- data/lib/mihari/web/controllers/command_controller.rb +26 -0
- data/lib/mihari/web/controllers/config_controller.rb +13 -0
- data/lib/mihari/web/controllers/sources_controller.rb +12 -0
- data/lib/mihari/web/controllers/tags_controller.rb +28 -0
- data/lib/mihari/web/helpers/json.rb +53 -0
- data/lib/mihari/web/public/index.html +2 -2
- data/lib/mihari/web/public/redoc-static.html +519 -0
- data/lib/mihari/web/public/static/js/{app.280cbdb7.js → app.cccddb2b.js} +3 -3
- data/lib/mihari/web/public/static/js/app.cccddb2b.js.map +1 -0
- data/mihari.gemspec +10 -6
- metadata +96 -30
- data/lib/mihari/slack_monkeypatch.rb +0 -16
- data/lib/mihari/web/public/static/js/app.280cbdb7.js.map +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 388f48a9001d38fd83f4a6d527d7c5826a490be1d339596d174a9f619a78cb0c
|
4
|
+
data.tar.gz: 7e0a6e2fcbe9ad1792c21472b8be7706a86b09fbbce951edb1829a76493aaedc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dfcde6c4fa80ae12c56606157c6800c7e321cef71ed3e4aa9250805ea51126c74a19b3f73040630d169966fb17d834d8c45b37cc6f7baa808d7eea3e7c585fb9
|
7
|
+
data.tar.gz: d477cdcc4b4075e7671263f32ed5e81daad42e499eded5dffcecfba2d7568b779e99010a64a271a652db3f928800506cb6a4c7cf4060816742d4bb8d5bbec86a
|
data/.rubocop.yml
CHANGED
@@ -4,6 +4,9 @@
|
|
4
4
|
require:
|
5
5
|
- rubocop-performance
|
6
6
|
|
7
|
+
AllCops:
|
8
|
+
NewCops: enable
|
9
|
+
|
7
10
|
Style/Alias:
|
8
11
|
Enabled: false
|
9
12
|
StyleGuide: https://relaxed.ruby.style/#stylealias
|
@@ -151,5 +154,8 @@ Lint/AssignmentInCondition:
|
|
151
154
|
Layout/LineLength:
|
152
155
|
Enabled: false
|
153
156
|
|
157
|
+
Style/StringLiteralsInInterpolation:
|
158
|
+
Enabled: false
|
159
|
+
|
154
160
|
Metrics:
|
155
161
|
Enabled: false
|
data/README.md
CHANGED
@@ -8,10 +8,14 @@
|
|
8
8
|
|
9
9
|
![img](https://github.com/ninoseki/mihari/raw/master/images/logo.png)
|
10
10
|
|
11
|
+
[![](images/tines.png)](https://tines.io?utm_source=github&utm_medium=sponsorship&utm_campaign=ninoseki)
|
12
|
+
|
11
13
|
Mihari is a framework for continuous OSINT based threat hunting.
|
12
14
|
|
13
15
|
## How it works
|
14
16
|
|
17
|
+
![img](https://github.com/ninoseki/mihari/raw/master/images/overview.png)
|
18
|
+
|
15
19
|
- Mihari makes a query against Shodan, Censys, VirusTotal, SecurityTrails, etc. and extracts artifacts (IP addresses, domains, URLs or hashes).
|
16
20
|
- Mihari checks whether a DB (SQLite3, PostgreSQL or MySQL) contains the artifacts or not.
|
17
21
|
- If it doesn't contain the artifacts:
|
@@ -59,3 +63,7 @@ See [Usage](https://github.com/ninoseki/mihari/wiki/Usage) for more information.
|
|
59
63
|
## License
|
60
64
|
|
61
65
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
66
|
+
|
67
|
+
## Acknowledgement
|
68
|
+
|
69
|
+
Mihari is proudly supported by [Tines.io](https://tines.io?utm_source=github&utm_medium=sponsorship&utm_campaign=ninoseki), The SOAR Platform for Enterprise Security Teams.
|
data/bin/console
CHANGED
data/docker/Dockerfile
CHANGED
File without changes
|
data/images/tines.png
ADDED
Binary file
|
data/images/web_alerts.png
CHANGED
Binary file
|
data/images/web_config.png
CHANGED
Binary file
|
data/lib/mihari.rb
CHANGED
@@ -8,6 +8,13 @@ module Mihari
|
|
8
8
|
include Configurable
|
9
9
|
include Retriable
|
10
10
|
|
11
|
+
attr_accessor :ignore_old_artifacts, :ignore_threshold
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@ignore_old_artifacts = false
|
15
|
+
@ignore_threshold = 0
|
16
|
+
end
|
17
|
+
|
11
18
|
# @return [Array<String>, Array<Mihari::Artifact>]
|
12
19
|
def artifacts
|
13
20
|
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
|
@@ -61,7 +68,9 @@ module Mihari
|
|
61
68
|
|
62
69
|
# @return [Array<Mihari::Artifact>]
|
63
70
|
def unique_artifacts
|
64
|
-
@unique_artifacts ||= normalized_artifacts.select
|
71
|
+
@unique_artifacts ||= normalized_artifacts.select do |artifact|
|
72
|
+
artifact.unique?(ignore_old_artifacts: ignore_old_artifacts, ignore_threshold: ignore_threshold)
|
73
|
+
end
|
65
74
|
end
|
66
75
|
|
67
76
|
def set_unique_artifacts
|
@@ -46,14 +46,14 @@ module Mihari
|
|
46
46
|
def passive_dns_lookup
|
47
47
|
results = api.dns.query(@query)
|
48
48
|
results.map do |result|
|
49
|
-
type = result
|
50
|
-
type == "A" ? result
|
49
|
+
type = result["rrtype"]
|
50
|
+
type == "A" ? result["rdata"] : nil
|
51
51
|
end.compact.uniq
|
52
52
|
end
|
53
53
|
|
54
54
|
def passive_ssl_lookup
|
55
55
|
result = api.ssl.cquery(@query)
|
56
|
-
seen = result
|
56
|
+
seen = result["seen"] || []
|
57
57
|
seen.uniq
|
58
58
|
end
|
59
59
|
end
|
@@ -21,10 +21,10 @@ module Mihari
|
|
21
21
|
return [] unless results
|
22
22
|
|
23
23
|
flat_results = results.map do |result|
|
24
|
-
result
|
24
|
+
result["results"]
|
25
25
|
end.flatten.compact
|
26
26
|
|
27
|
-
flat_results.map { |result| result
|
27
|
+
flat_results.map { |result| result["ip"] }.compact.uniq
|
28
28
|
end
|
29
29
|
|
30
30
|
private
|
@@ -5,16 +5,14 @@ require "urlscan"
|
|
5
5
|
module Mihari
|
6
6
|
module Analyzers
|
7
7
|
class Urlscan < Base
|
8
|
-
attr_reader :title, :description, :query, :tags, :
|
8
|
+
attr_reader :title, :description, :query, :tags, :target_type, :use_similarity
|
9
9
|
|
10
10
|
def initialize(
|
11
11
|
query,
|
12
12
|
description: nil,
|
13
|
-
filter: nil,
|
14
13
|
tags: [],
|
15
14
|
target_type: "url",
|
16
15
|
title: nil,
|
17
|
-
use_pro: false,
|
18
16
|
use_similarity: false
|
19
17
|
)
|
20
18
|
super()
|
@@ -24,9 +22,7 @@ module Mihari
|
|
24
22
|
@description = description || "query = #{query}"
|
25
23
|
@tags = tags
|
26
24
|
|
27
|
-
@filter = filter
|
28
25
|
@target_type = target_type
|
29
|
-
@use_pro = use_pro
|
30
26
|
@use_similarity = use_similarity
|
31
27
|
|
32
28
|
raise InvalidInputError, "type should be url, domain or ip." unless valid_target_type?
|
@@ -54,7 +50,6 @@ module Mihari
|
|
54
50
|
|
55
51
|
def search
|
56
52
|
return api.pro.similar(query) if use_similarity
|
57
|
-
return api.pro.search(query: query, filter: filter, size: 10_000) if use_pro
|
58
53
|
|
59
54
|
api.search(query, size: 10_000)
|
60
55
|
end
|
@@ -37,11 +37,11 @@ module Mihari
|
|
37
37
|
end
|
38
38
|
|
39
39
|
def config_keys
|
40
|
-
%w[
|
40
|
+
%w[zoomeye_api_key]
|
41
41
|
end
|
42
42
|
|
43
43
|
def api
|
44
|
-
@api ||= ::ZoomEye::API.new(
|
44
|
+
@api ||= ::ZoomEye::API.new(api_key: Mihari.config.zoomeye_api_key)
|
45
45
|
end
|
46
46
|
|
47
47
|
def convert_responses(responses)
|
data/lib/mihari/cli.rb
CHANGED
@@ -33,7 +33,10 @@ require "mihari/commands/web"
|
|
33
33
|
|
34
34
|
module Mihari
|
35
35
|
class CLI < Thor
|
36
|
-
class_option :config, type: :string, desc: "
|
36
|
+
class_option :config, type: :string, desc: "Path to the config file"
|
37
|
+
|
38
|
+
class_option :ignore_old_artifacts, type: :boolean, default: false, desc: "Whether to ignore old artifacts from checking or not. Only affects with analyze commands."
|
39
|
+
class_option :ignore_threshold, type: :numeric, default: 0, desc: "Number of days to define whether an artifact is old or not. Only affects with analyze commands."
|
37
40
|
|
38
41
|
include Mihari::Commands::BinaryEdge
|
39
42
|
include Mihari::Commands::Censys
|
@@ -96,16 +99,22 @@ module Mihari
|
|
96
99
|
options = normalize_options(options)
|
97
100
|
|
98
101
|
analyzer = analyzer_class.new(query, **options)
|
102
|
+
|
103
|
+
analyzer.ignore_old_artifacts = options[:ignore_old_artifacts] || false
|
104
|
+
analyzer.ignore_threshold = options[:ignore_threshold] || 0
|
105
|
+
|
99
106
|
analyzer.run
|
100
107
|
end
|
101
108
|
|
102
109
|
def symbolize_hash_keys(hash)
|
103
|
-
hash.
|
110
|
+
hash.transform_keys(&:to_sym)
|
104
111
|
end
|
105
112
|
|
106
113
|
def normalize_options(options)
|
107
114
|
# Delete :config because it is not intended to use for running an analyzer
|
108
|
-
|
115
|
+
[:config, :ignore_old_artifacts, :ignore_threshold].each do |ignore_key|
|
116
|
+
options.delete(ignore_key)
|
117
|
+
end
|
109
118
|
options
|
110
119
|
end
|
111
120
|
|
@@ -1,3 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "colorize"
|
4
|
+
|
1
5
|
module Mihari
|
2
6
|
module Commands
|
3
7
|
module Config
|
@@ -8,11 +12,13 @@ module Mihari
|
|
8
12
|
def init_config
|
9
13
|
filename = options["filename"]
|
10
14
|
|
11
|
-
|
15
|
+
warning = "#{filename} exists. Do you want to overwrite it? (y/n)"
|
16
|
+
if File.exist?(filename) && !(yes? warning)
|
12
17
|
return
|
13
18
|
end
|
14
19
|
|
15
20
|
Mihari::Config.initialize_yaml filename
|
21
|
+
puts "The config file is initialized as #{filename}.".colorize(:blue)
|
16
22
|
end
|
17
23
|
end
|
18
24
|
end
|
data/lib/mihari/commands/json.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Mihari
|
2
4
|
module Commands
|
3
5
|
module JSON
|
@@ -18,6 +20,10 @@ module Mihari
|
|
18
20
|
tags = json["tags"] || []
|
19
21
|
|
20
22
|
basic = Analyzers::Basic.new(title: title, description: description, artifacts: artifacts, source: "json", tags: tags)
|
23
|
+
|
24
|
+
basic.ignore_old_artifacts = options["ignore_old_artifacts"] || false
|
25
|
+
basic.ignore_threshold = options["ignore_threshold"] || 0
|
26
|
+
|
21
27
|
basic.run
|
22
28
|
end
|
23
29
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Mihari
|
2
4
|
module Commands
|
3
5
|
module Urlscan
|
@@ -7,9 +9,7 @@ module Mihari
|
|
7
9
|
method_option :title, type: :string, desc: "title"
|
8
10
|
method_option :description, type: :string, desc: "description"
|
9
11
|
method_option :tags, type: :array, desc: "tags"
|
10
|
-
method_option :filter, type: :string, desc: "filter for urlscan pro search"
|
11
12
|
method_option :target_type, type: :string, default: "url", desc: "target type to fetch from lookup results (target type should be 'url', 'domain' or 'ip')"
|
12
|
-
method_option :use_pro, type: :boolean, default: false, desc: "use pro search API or not"
|
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
|
data/lib/mihari/commands/web.rb
CHANGED
data/lib/mihari/config.rb
CHANGED
@@ -4,7 +4,7 @@ require "yaml"
|
|
4
4
|
|
5
5
|
module Mihari
|
6
6
|
class Config
|
7
|
-
attr_accessor :binaryedge_api_key, :censys_id, :censys_secret, :circl_passive_password, :circl_passive_username, :misp_api_endpoint, :misp_api_key, :onyphe_api_key, :otx_api_key, :passivetotal_api_key, :passivetotal_username, :pulsedive_api_key, :securitytrails_api_key, :shodan_api_key, :slack_channel, :slack_webhook_url, :spyse_api_key, :thehive_api_endpoint, :thehive_api_key, :urlscan_api_key, :virustotal_api_key, :
|
7
|
+
attr_accessor :binaryedge_api_key, :censys_id, :censys_secret, :circl_passive_password, :circl_passive_username, :misp_api_endpoint, :misp_api_key, :onyphe_api_key, :otx_api_key, :passivetotal_api_key, :passivetotal_username, :pulsedive_api_key, :securitytrails_api_key, :shodan_api_key, :slack_channel, :slack_webhook_url, :spyse_api_key, :thehive_api_endpoint, :thehive_api_key, :urlscan_api_key, :virustotal_api_key, :zoomeye_api_key, :webhook_url, :webhook_use_json_body, :database
|
8
8
|
|
9
9
|
def initialize
|
10
10
|
load_from_env
|
@@ -32,8 +32,9 @@ module Mihari
|
|
32
32
|
@thehive_api_key = ENV["THEHIVE_API_KEY"]
|
33
33
|
@urlscan_api_key = ENV["URLSCAN_API_KEY"]
|
34
34
|
@virustotal_api_key = ENV["VIRUSTOTAL_API_KEY"]
|
35
|
-
@
|
36
|
-
@
|
35
|
+
@zoomeye_api_key = ENV["ZOOMEYE_API_KEY"]
|
36
|
+
@webhook_url = ENV["WEBHOOK_URL"]
|
37
|
+
@webhook_use_json_body = ENV["WEBHOOK_USE_JSON_BODY"]
|
37
38
|
|
38
39
|
@database = ENV["DATABASE"] || "mihari.db"
|
39
40
|
end
|
@@ -58,7 +59,7 @@ module Mihari
|
|
58
59
|
|
59
60
|
def initialize_yaml(filename)
|
60
61
|
keys = new.instance_variables.map do |key|
|
61
|
-
key.to_s[1
|
62
|
+
key.to_s[1..]
|
62
63
|
end
|
63
64
|
|
64
65
|
config = keys.map do |key|
|
@@ -0,0 +1,60 @@
|
|
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 config_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 ||= truthy?(Mihari.config.webhook_use_json_body || 'false')
|
50
|
+
end
|
51
|
+
|
52
|
+
def truthy?(value)
|
53
|
+
return true if value == "true"
|
54
|
+
return true if value == true
|
55
|
+
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|