mihari 2.1.0 → 2.4.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/.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
|

|
10
10
|
|
11
|
+
[](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
|
+

|
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
|