mihari 5.4.3 → 5.4.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -25
- data/docs/alternatives.md +5 -0
- data/docs/analyzers/binaryedge.md +21 -0
- data/docs/analyzers/censys.md +23 -0
- data/docs/analyzers/circl.md +29 -0
- data/docs/analyzers/crtsh.md +25 -0
- data/docs/analyzers/dnstwister.md +23 -0
- data/docs/analyzers/feed.md +49 -0
- data/docs/analyzers/greynoise.md +21 -0
- data/docs/analyzers/hunterhow.md +25 -0
- data/docs/analyzers/index.md +79 -0
- data/docs/analyzers/onyphe.md +21 -0
- data/docs/analyzers/otx.md +23 -0
- data/docs/analyzers/passivetotal.md +36 -0
- data/docs/analyzers/pulsedive.md +23 -0
- data/docs/analyzers/securitytrails.md +32 -0
- data/docs/analyzers/shodan.md +21 -0
- data/docs/analyzers/urlscan.md +23 -0
- data/docs/analyzers/virustotal.md +34 -0
- data/docs/analyzers/virustotal_intelligence.md +22 -0
- data/docs/analyzers/zoomeye.md +25 -0
- data/docs/configuration.md +35 -0
- data/docs/emitters/database.md +22 -0
- data/docs/emitters/hive.md +18 -0
- data/docs/emitters/index.md +7 -0
- data/docs/emitters/misp.md +16 -0
- data/docs/emitters/slack.md +16 -0
- data/docs/emitters/webhook.md +63 -0
- data/docs/enrichers/google_public_dns.md +19 -0
- data/docs/enrichers/index.md +6 -0
- data/docs/enrichers/ipinfo.md +19 -0
- data/docs/enrichers/shodan.md +22 -0
- data/docs/enrichers/whois.md +17 -0
- data/docs/github_actions.md +43 -0
- data/docs/index.md +13 -0
- data/docs/installation.md +31 -0
- data/docs/requirements.md +20 -0
- data/docs/rule.md +165 -0
- data/docs/tags.md +3 -0
- data/docs/usage.md +100 -0
- data/frontend/package-lock.json +2414 -1516
- data/frontend/package.json +22 -22
- data/lib/mihari/analyzers/base.rb +25 -10
- data/lib/mihari/analyzers/binaryedge.rb +1 -7
- data/lib/mihari/analyzers/circl.rb +1 -1
- data/lib/mihari/analyzers/dnstwister.rb +1 -1
- data/lib/mihari/analyzers/otx.rb +1 -1
- data/lib/mihari/analyzers/passivetotal.rb +1 -1
- data/lib/mihari/analyzers/pulsedive.rb +1 -1
- data/lib/mihari/analyzers/rule.rb +18 -13
- data/lib/mihari/analyzers/securitytrails.rb +1 -1
- data/lib/mihari/analyzers/urlscan.rb +1 -1
- data/lib/mihari/analyzers/virustotal.rb +1 -1
- data/lib/mihari/analyzers/zoomeye.rb +1 -1
- data/lib/mihari/clients/binaryedge.rb +4 -7
- data/lib/mihari/clients/crtsh.rb +1 -3
- data/lib/mihari/clients/publsedive.rb +1 -1
- data/lib/mihari/clients/shodan.rb +2 -2
- data/lib/mihari/commands/alert.rb +42 -13
- data/lib/mihari/commands/rule.rb +11 -7
- data/lib/mihari/commands/search.rb +54 -22
- data/lib/mihari/config.rb +5 -0
- data/lib/mihari/emitters/base.rb +9 -3
- data/lib/mihari/emitters/slack.rb +1 -1
- data/lib/mihari/enrichers/base.rb +13 -0
- data/lib/mihari/enrichers/google_public_dns.rb +16 -1
- data/lib/mihari/enrichers/ipinfo.rb +9 -13
- data/lib/mihari/enrichers/shodan.rb +1 -2
- data/lib/mihari/enrichers/whois.rb +2 -2
- data/lib/mihari/errors.rb +16 -10
- data/lib/mihari/feed/parser.rb +2 -2
- data/lib/mihari/models/artifact.rb +1 -1
- data/lib/mihari/models/autonomous_system.rb +11 -5
- data/lib/mihari/models/cpe.rb +10 -4
- data/lib/mihari/models/dns.rb +11 -16
- data/lib/mihari/models/geolocation.rb +11 -5
- data/lib/mihari/models/port.rb +10 -4
- data/lib/mihari/models/reverse_dns.rb +10 -4
- data/lib/mihari/models/whois.rb +4 -1
- data/lib/mihari/schemas/analyzer.rb +1 -0
- data/lib/mihari/services/alert_builder.rb +43 -0
- data/lib/mihari/services/alert_proxy.rb +7 -25
- data/lib/mihari/services/alert_runner.rb +9 -0
- data/lib/mihari/services/rule_builder.rb +47 -0
- data/lib/mihari/services/rule_proxy.rb +5 -61
- data/lib/mihari/services/rule_runner.rb +9 -4
- data/lib/mihari/structs/binaryedge.rb +89 -0
- data/lib/mihari/structs/shodan.rb +2 -1
- data/lib/mihari/structs/urlscan.rb +1 -3
- data/lib/mihari/structs/virustotal_intelligence.rb +1 -3
- data/lib/mihari/type_checker.rb +1 -1
- data/lib/mihari/version.rb +1 -1
- data/lib/mihari/web/endpoints/alerts.rb +33 -15
- data/lib/mihari/web/endpoints/artifacts.rb +53 -25
- data/lib/mihari/web/endpoints/configs.rb +2 -2
- data/lib/mihari/web/endpoints/ip_addresses.rb +3 -5
- data/lib/mihari/web/endpoints/rules.rb +97 -71
- data/lib/mihari/web/endpoints/tags.rb +15 -5
- data/lib/mihari/web/public/assets/index-0a5a47bf.js +1740 -0
- data/lib/mihari/web/public/index.html +1 -1
- data/lib/mihari/web/public/redoc-static.html +419 -382
- data/lib/mihari.rb +4 -0
- data/mihari.gemspec +6 -5
- data/mkdocs.yml +35 -0
- data/requirements.txt +2 -0
- metadata +70 -12
- data/lib/mihari/web/public/assets/index-4d7eda9f.js +0 -1738
@@ -1,11 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "date"
|
4
|
-
require "erb"
|
5
3
|
require "json"
|
6
|
-
require "pathname"
|
7
|
-
require "securerandom"
|
8
|
-
require "yaml"
|
9
4
|
|
10
5
|
module Mihari
|
11
6
|
module Services
|
@@ -29,10 +24,9 @@ module Mihari
|
|
29
24
|
#
|
30
25
|
def initialize(data)
|
31
26
|
@data = data.deep_symbolize_keys
|
32
|
-
|
33
27
|
@errors = nil
|
34
28
|
|
35
|
-
validate
|
29
|
+
validate!
|
36
30
|
end
|
37
31
|
|
38
32
|
#
|
@@ -44,21 +38,14 @@ module Mihari
|
|
44
38
|
!@errors.empty?
|
45
39
|
end
|
46
40
|
|
47
|
-
def validate
|
41
|
+
def validate!
|
48
42
|
contract = Schemas::RuleContract.new
|
49
43
|
result = contract.call(data)
|
50
44
|
|
51
45
|
@data = result.to_h
|
52
46
|
@errors = result.errors
|
53
|
-
end
|
54
47
|
|
55
|
-
|
56
|
-
return unless errors?
|
57
|
-
|
58
|
-
Mihari.logger.error "Failed to parse the input as a rule:"
|
59
|
-
Mihari.logger.error JSON.pretty_generate(errors.to_h)
|
60
|
-
|
61
|
-
raise RuleValidationError, errors
|
48
|
+
raise ValidationError.new("Validation failed", errors) if errors?
|
62
49
|
end
|
63
50
|
|
64
51
|
def [](key)
|
@@ -178,9 +165,7 @@ module Mihari
|
|
178
165
|
# @return [Mihari::Services::Rule]
|
179
166
|
#
|
180
167
|
def from_yaml(yaml)
|
181
|
-
|
182
|
-
rescue Psych::SyntaxError => e
|
183
|
-
raise YAMLSyntaxError, e.message
|
168
|
+
new YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol])
|
184
169
|
end
|
185
170
|
|
186
171
|
#
|
@@ -189,48 +174,7 @@ module Mihari
|
|
189
174
|
# @return [Mihari::Services::Rule]
|
190
175
|
#
|
191
176
|
def from_model(model)
|
192
|
-
|
193
|
-
end
|
194
|
-
|
195
|
-
#
|
196
|
-
# Load a rule from path
|
197
|
-
#
|
198
|
-
# @param [String] path
|
199
|
-
#
|
200
|
-
# @return [Mihari::Services::Rule, nil]
|
201
|
-
#
|
202
|
-
def from_path(path)
|
203
|
-
return nil unless Pathname(path).exist?
|
204
|
-
|
205
|
-
from_yaml File.read(path)
|
206
|
-
end
|
207
|
-
|
208
|
-
#
|
209
|
-
# Load a rule from DB
|
210
|
-
#
|
211
|
-
# @param [String] id
|
212
|
-
#
|
213
|
-
# @return [Mihari::Services::Rule, nil]
|
214
|
-
#
|
215
|
-
def from_id(id)
|
216
|
-
return nil unless Mihari::Rule.exists?(id)
|
217
|
-
|
218
|
-
Services::RuleProxy.from_model Mihari::Rule.find(id)
|
219
|
-
end
|
220
|
-
|
221
|
-
#
|
222
|
-
# @param [String] path_or_id Path to YAML file or YAML string or ID of a rule in the database
|
223
|
-
#
|
224
|
-
# @return [Mihari::Services::Rule]
|
225
|
-
#
|
226
|
-
def from_path_or_id(path_or_id)
|
227
|
-
rule = from_path(path_or_id)
|
228
|
-
return rule unless rule.nil?
|
229
|
-
|
230
|
-
rule = from_id(path_or_id)
|
231
|
-
return rule unless rule.nil?
|
232
|
-
|
233
|
-
raise ArgumentError, "#{path_or_id} does not exist"
|
177
|
+
new model.data
|
234
178
|
end
|
235
179
|
end
|
236
180
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module Mihari
|
4
4
|
module Services
|
5
5
|
class RuleRunner
|
6
|
+
include Dry::Monads[:result, :try]
|
7
|
+
|
6
8
|
include Mixins::ErrorNotification
|
7
9
|
|
8
10
|
# @return [Mihari::Services::RuleProxy]
|
@@ -38,11 +40,14 @@ module Mihari
|
|
38
40
|
# @return [Mihari::Alert, nil]
|
39
41
|
#
|
40
42
|
def run
|
41
|
-
|
43
|
+
rule.analyzer.run
|
44
|
+
end
|
42
45
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
+
#
|
47
|
+
# @return [Dry::Monads::Result::Success<Mihari::Alert, nil>, Dry::Monads::Result::Failure]
|
48
|
+
#
|
49
|
+
def result
|
50
|
+
Try[StandardError] { run }.to_result
|
46
51
|
end
|
47
52
|
end
|
48
53
|
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Mihari
|
2
|
+
module Structs
|
3
|
+
module BinaryEdge
|
4
|
+
class Target < Dry::Struct
|
5
|
+
attribute :ip, Types::String
|
6
|
+
|
7
|
+
#
|
8
|
+
# @return [String]
|
9
|
+
#
|
10
|
+
def ip
|
11
|
+
attributes[:ip]
|
12
|
+
end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def from_dynamic!(d)
|
16
|
+
d = Types::Hash[d]
|
17
|
+
new(
|
18
|
+
ip: d.fetch("ip")
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Event < Dry::Struct
|
25
|
+
attribute :target, Target
|
26
|
+
|
27
|
+
#
|
28
|
+
# @return [Target]
|
29
|
+
#
|
30
|
+
def target
|
31
|
+
attributes[:target]
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
def from_dynamic!(d)
|
36
|
+
d = Types::Hash[d]
|
37
|
+
new(
|
38
|
+
target: Target.from_dynamic!(d.fetch("target"))
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class Response < Dry::Struct
|
45
|
+
# @!attribute [r] page
|
46
|
+
# @return [Integer]
|
47
|
+
attribute :page, Types::Integer
|
48
|
+
|
49
|
+
# @!attribute [r] pagesize
|
50
|
+
# @return [Integer]
|
51
|
+
attribute :pagesize, Types::Integer
|
52
|
+
|
53
|
+
# @!attribute [r] total
|
54
|
+
# @return [Integer]
|
55
|
+
attribute :total, Types::Integer
|
56
|
+
|
57
|
+
# @!attribute [r] events
|
58
|
+
# @return [Array<Event>]
|
59
|
+
attribute :events, Types.Array(Event)
|
60
|
+
|
61
|
+
#
|
62
|
+
# @return [Array<Event>]
|
63
|
+
#
|
64
|
+
def events
|
65
|
+
attributes[:events]
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# @return [Array<Artifact>]
|
70
|
+
#
|
71
|
+
def artifacts
|
72
|
+
events.map { |event| Artifact.new(data: event.target.ip) }
|
73
|
+
end
|
74
|
+
|
75
|
+
class << self
|
76
|
+
def from_dynamic!(d)
|
77
|
+
d = Types::Hash[d]
|
78
|
+
new(
|
79
|
+
page: d.fetch("page"),
|
80
|
+
pagesize: d.fetch("pagesize"),
|
81
|
+
total: d.fetch("total"),
|
82
|
+
events: d.fetch("events").map { |x| Event.from_dynamic!(x) }
|
83
|
+
)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -140,7 +140,7 @@ module Mihari
|
|
140
140
|
end
|
141
141
|
end
|
142
142
|
|
143
|
-
class
|
143
|
+
class Response < Dry::Struct
|
144
144
|
attribute :matches, Types.Array(Match)
|
145
145
|
attribute :total, Types::Int
|
146
146
|
|
@@ -197,6 +197,7 @@ module Mihari
|
|
197
197
|
def artifacts
|
198
198
|
matches.map do |match|
|
199
199
|
metadata = collect_metadata_by_ip(match.ip_str)
|
200
|
+
|
200
201
|
ports = collect_ports_by_ip(match.ip_str).map do |port|
|
201
202
|
Mihari::Port.new(port: port)
|
202
203
|
end
|
@@ -85,9 +85,7 @@ module Mihari
|
|
85
85
|
#
|
86
86
|
def artifacts
|
87
87
|
values = [page.url, page.domain, page.ip].compact
|
88
|
-
values.map
|
89
|
-
Mihari::Artifact.new(data: value, metadata: metadata)
|
90
|
-
end
|
88
|
+
values.map { |value| Mihari::Artifact.new(data: value, metadata: metadata) }
|
91
89
|
end
|
92
90
|
|
93
91
|
class << self
|
data/lib/mihari/type_checker.rb
CHANGED
data/lib/mihari/version.rb
CHANGED
@@ -54,18 +54,27 @@ module Mihari
|
|
54
54
|
requires :id, type: Integer
|
55
55
|
end
|
56
56
|
delete "/:id" do
|
57
|
+
extend Dry::Monads[:result, :try]
|
58
|
+
|
57
59
|
id = params["id"].to_i
|
58
60
|
|
59
|
-
|
61
|
+
result = Try do
|
60
62
|
alert = Mihari::Alert.find(id)
|
61
|
-
|
62
|
-
|
63
|
-
end
|
63
|
+
alert.destroy
|
64
|
+
end.to_result
|
64
65
|
|
65
|
-
|
66
|
+
if result.success?
|
67
|
+
status 204
|
68
|
+
return present({ message: "" }, with: Entities::Message)
|
69
|
+
end
|
66
70
|
|
67
|
-
|
68
|
-
|
71
|
+
failure = result.failure
|
72
|
+
case failure
|
73
|
+
when ActiveRecord::RecordNotFound
|
74
|
+
error!({ message: "ID:#{id} is not found" }, 404)
|
75
|
+
else
|
76
|
+
raise failure
|
77
|
+
end
|
69
78
|
end
|
70
79
|
|
71
80
|
desc "Create an alert", {
|
@@ -77,17 +86,26 @@ module Mihari
|
|
77
86
|
requires :artifacts, type: Array, documentation: { type: String, is_array: true, param_type: "body" }
|
78
87
|
end
|
79
88
|
post "/" do
|
80
|
-
|
81
|
-
runner = Services::AlertRunner.new(proxy)
|
89
|
+
extend Dry::Monads[:result, :try]
|
82
90
|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
91
|
+
result = Try do
|
92
|
+
proxy = Services::AlertProxy.new(params.to_snake_keys)
|
93
|
+
runner = Services::AlertRunner.new(proxy)
|
94
|
+
runner.run
|
95
|
+
end.to_result
|
96
|
+
|
97
|
+
if result.success?
|
98
|
+
status 201
|
99
|
+
return present(result.value!, with: Entities::Alert)
|
87
100
|
end
|
88
101
|
|
89
|
-
|
90
|
-
|
102
|
+
failure = result.failure
|
103
|
+
case failure
|
104
|
+
when ActiveRecord::RecordNotFound
|
105
|
+
error!({ message: "Rule:#{params["ruleId"]} is not found" }, 404)
|
106
|
+
else
|
107
|
+
raise failure
|
108
|
+
end
|
91
109
|
end
|
92
110
|
end
|
93
111
|
end
|
@@ -13,9 +13,11 @@ module Mihari
|
|
13
13
|
requires :id, type: Integer
|
14
14
|
end
|
15
15
|
get "/:id" do
|
16
|
+
extend Dry::Monads[:result, :try]
|
17
|
+
|
16
18
|
id = params[:id].to_i
|
17
19
|
|
18
|
-
|
20
|
+
result = Try do
|
19
21
|
artifact = Mihari::Artifact.includes(
|
20
22
|
:autonomous_system,
|
21
23
|
:geolocation,
|
@@ -23,18 +25,25 @@ module Mihari
|
|
23
25
|
:dns_records,
|
24
26
|
:reverse_dns_names
|
25
27
|
).find(id)
|
26
|
-
|
27
|
-
|
28
|
-
|
28
|
+
# TODO: improve queries
|
29
|
+
alert_ids = Mihari::Artifact.where(data: artifact.data).pluck(:alert_id)
|
30
|
+
tag_ids = Mihari::Tagging.where(alert_id: alert_ids).pluck(:tag_id)
|
31
|
+
tag_names = Mihari::Tag.where(id: tag_ids).distinct.pluck(:name)
|
29
32
|
|
30
|
-
|
31
|
-
alert_ids = Mihari::Artifact.where(data: artifact.data).pluck(:alert_id)
|
32
|
-
tag_ids = Mihari::Tagging.where(alert_id: alert_ids).pluck(:tag_id)
|
33
|
-
tag_names = Mihari::Tag.where(id: tag_ids).distinct.pluck(:name)
|
33
|
+
artifact.tags = tag_names
|
34
34
|
|
35
|
-
|
35
|
+
artifact
|
36
|
+
end.to_result
|
36
37
|
|
37
|
-
present
|
38
|
+
return present(result.value!, with: Entities::Artifact) if result.success?
|
39
|
+
|
40
|
+
failure = result.failure
|
41
|
+
case failure
|
42
|
+
when ActiveRecord::RecordNotFound
|
43
|
+
error!({ message: "ID:#{id} is not found" }, 404)
|
44
|
+
else
|
45
|
+
raise failure
|
46
|
+
end
|
38
47
|
end
|
39
48
|
|
40
49
|
desc "Enrich an artifact", {
|
@@ -46,9 +55,11 @@ module Mihari
|
|
46
55
|
requires :id, type: Integer
|
47
56
|
end
|
48
57
|
get "/:id/enrich" do
|
58
|
+
extend Dry::Monads[:result, :try]
|
59
|
+
|
49
60
|
id = params["id"].to_i
|
50
61
|
|
51
|
-
|
62
|
+
result = Try do
|
52
63
|
artifact = Mihari::Artifact.includes(
|
53
64
|
:autonomous_system,
|
54
65
|
:geolocation,
|
@@ -58,15 +69,23 @@ module Mihari
|
|
58
69
|
:cpes,
|
59
70
|
:ports
|
60
71
|
).find(id)
|
61
|
-
rescue ActiveRecord::RecordNotFound
|
62
|
-
error!({ message: "ID:#{id} is not found" }, 404)
|
63
|
-
end
|
64
72
|
|
65
|
-
|
66
|
-
|
73
|
+
artifact.enrich_all
|
74
|
+
artifact.save
|
75
|
+
end.to_result
|
67
76
|
|
68
|
-
|
69
|
-
|
77
|
+
if result.success?
|
78
|
+
status 201
|
79
|
+
return present({ message: "" }, with: Entities::Message)
|
80
|
+
end
|
81
|
+
|
82
|
+
failure = result.failure
|
83
|
+
case failure
|
84
|
+
when ActiveRecord::RecordNotFound
|
85
|
+
error!({ message: "ID:#{id} is not found" }, 404)
|
86
|
+
else
|
87
|
+
raise failure
|
88
|
+
end
|
70
89
|
end
|
71
90
|
|
72
91
|
desc "Delete an artifact", {
|
@@ -78,18 +97,27 @@ module Mihari
|
|
78
97
|
requires :id, type: Integer
|
79
98
|
end
|
80
99
|
delete "/:id" do
|
100
|
+
extend Dry::Monads[:result, :try]
|
101
|
+
|
81
102
|
id = params["id"].to_i
|
82
103
|
|
83
|
-
|
104
|
+
result = Try do
|
84
105
|
alert = Mihari::Artifact.find(id)
|
85
|
-
|
86
|
-
|
87
|
-
end
|
106
|
+
alert.destroy
|
107
|
+
end.to_result
|
88
108
|
|
89
|
-
|
109
|
+
if result.success?
|
110
|
+
status 204
|
111
|
+
return present({ message: "" }, with: Entities::Message)
|
112
|
+
end
|
90
113
|
|
91
|
-
|
92
|
-
|
114
|
+
failure = result.failure
|
115
|
+
case failure
|
116
|
+
when ActiveRecord::RecordNotFound
|
117
|
+
error!({ message: "ID:#{id} is not found" }, 404)
|
118
|
+
else
|
119
|
+
raise failure
|
120
|
+
end
|
93
121
|
end
|
94
122
|
end
|
95
123
|
end
|
@@ -10,9 +10,9 @@ module Mihari
|
|
10
10
|
summary: "Get configs"
|
11
11
|
}
|
12
12
|
get "/" do
|
13
|
-
configs = (Mihari.analyzers + Mihari.emitters + Mihari.enrichers).
|
13
|
+
configs = (Mihari.analyzers + Mihari.emitters + Mihari.enrichers).filter_map do |klass|
|
14
14
|
Mihari::Structs::Config.from_class(klass)
|
15
|
-
end
|
15
|
+
end
|
16
16
|
|
17
17
|
present(configs, with: Entities::Config)
|
18
18
|
end
|
@@ -16,11 +16,9 @@ module Mihari
|
|
16
16
|
ip = params[:ip].to_s
|
17
17
|
|
18
18
|
data = Enrichers::IPInfo.query(ip)
|
19
|
-
if data.nil?
|
20
|
-
|
21
|
-
|
22
|
-
present data, with: Entities::IPAddress
|
23
|
-
end
|
19
|
+
error!({ message: "IP:#{ip} is not found" }, 404) if data.nil?
|
20
|
+
|
21
|
+
present data, with: Entities::IPAddress
|
24
22
|
end
|
25
23
|
end
|
26
24
|
end
|