mihari 5.4.3 → 5.4.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/frontend/package-lock.json +2399 -1504
- 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-ef33a6cd.js +1738 -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 +5 -4
- metadata +28 -11
- data/lib/mihari/web/public/assets/index-4d7eda9f.js +0 -1738
data/frontend/package.json
CHANGED
@@ -17,15 +17,15 @@
|
|
17
17
|
"@fortawesome/fontawesome-svg-core": "^6.4.2",
|
18
18
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
19
19
|
"@fortawesome/vue-fontawesome": "^3.0.3",
|
20
|
-
"@vueuse/core": "^10.
|
21
|
-
"@vueuse/router": "^10.
|
22
|
-
"ace-builds": "^1.
|
23
|
-
"axios": "^1.
|
20
|
+
"@vueuse/core": "^10.4.1",
|
21
|
+
"@vueuse/router": "^10.4.1",
|
22
|
+
"ace-builds": "^1.28.0",
|
23
|
+
"axios": "^1.5.0",
|
24
24
|
"bulma": "^0.9.4",
|
25
25
|
"bulma-helpers": "^0.4.3",
|
26
|
-
"dayjs": "^1.11.
|
26
|
+
"dayjs": "^1.11.10",
|
27
27
|
"font-awesome-animation": "^1.1.1",
|
28
|
-
"js-sha256": "^0.
|
28
|
+
"js-sha256": "^0.10.1",
|
29
29
|
"truncate": "^3.0.0",
|
30
30
|
"ts-dedent": "^2.2.0",
|
31
31
|
"url-parse": "^1.5.10",
|
@@ -33,24 +33,24 @@
|
|
33
33
|
"vue": "^3.3.4",
|
34
34
|
"vue-concurrency": "4.0.1",
|
35
35
|
"vue-json-pretty": "^2.2.4",
|
36
|
-
"vue-router": "^4.2.
|
36
|
+
"vue-router": "^4.2.5",
|
37
37
|
"vue3-ace-editor": "^2.2.3"
|
38
38
|
},
|
39
39
|
"devDependencies": {
|
40
|
-
"@redocly/cli": "1.0
|
41
|
-
"@rushstack/eslint-patch": "^1.
|
42
|
-
"@tsconfig/node20": "^20.1.
|
43
|
-
"@types/jsdom": "^21.1.
|
44
|
-
"@types/node": "^20.
|
45
|
-
"@types/url-parse": "^1.4.
|
46
|
-
"@typescript-eslint/eslint-plugin": "^6.
|
47
|
-
"@typescript-eslint/parser": "^6.
|
48
|
-
"@vitejs/plugin-vue": "^4.3.
|
40
|
+
"@redocly/cli": "1.2.0",
|
41
|
+
"@rushstack/eslint-patch": "^1.4.0",
|
42
|
+
"@tsconfig/node20": "^20.1.2",
|
43
|
+
"@types/jsdom": "^21.1.3",
|
44
|
+
"@types/node": "^20.6.4",
|
45
|
+
"@types/url-parse": "^1.4.9",
|
46
|
+
"@typescript-eslint/eslint-plugin": "^6.7.2",
|
47
|
+
"@typescript-eslint/parser": "^6.7.2",
|
48
|
+
"@vitejs/plugin-vue": "^4.3.4",
|
49
49
|
"@vue/eslint-config-prettier": "^8.0.0",
|
50
|
-
"@vue/eslint-config-typescript": "^
|
50
|
+
"@vue/eslint-config-typescript": "^12.0.0",
|
51
51
|
"@vue/test-utils": "2.4.1",
|
52
52
|
"@vue/tsconfig": "^0.4.0",
|
53
|
-
"eslint": "^8.
|
53
|
+
"eslint": "^8.50.0",
|
54
54
|
"eslint-config-prettier": "^9.0.0",
|
55
55
|
"eslint-plugin-prettier": "^5.0.0",
|
56
56
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
@@ -58,10 +58,10 @@
|
|
58
58
|
"husky": "^8.0.3",
|
59
59
|
"jsdom": "^22.1.0",
|
60
60
|
"npm-run-all": "^4.1.5",
|
61
|
-
"prettier": "^3.0.
|
62
|
-
"typescript": "~5.
|
61
|
+
"prettier": "^3.0.3",
|
62
|
+
"typescript": "~5.2.2",
|
63
63
|
"vite": "^4.4.9",
|
64
|
-
"vitest": "^0.34.
|
65
|
-
"vue-tsc": "^1.8.
|
64
|
+
"vitest": "^0.34.5",
|
65
|
+
"vue-tsc": "^1.8.13"
|
66
66
|
}
|
67
67
|
}
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module Mihari
|
4
4
|
module Analyzers
|
5
5
|
class Base
|
6
|
+
include Dry::Monads[:result, :try]
|
7
|
+
|
6
8
|
include Mixins::Configurable
|
7
9
|
include Mixins::Retriable
|
8
10
|
|
@@ -25,28 +27,38 @@ module Mihari
|
|
25
27
|
# @return [Integer, nil]
|
26
28
|
#
|
27
29
|
def interval
|
28
|
-
|
30
|
+
options[:interval]
|
29
31
|
end
|
30
32
|
|
31
33
|
#
|
32
34
|
# @return [Integer]
|
33
35
|
#
|
34
36
|
def retry_interval
|
35
|
-
|
37
|
+
options[:retry_interval] || Mihari.config.retry_interval
|
36
38
|
end
|
37
39
|
|
38
40
|
#
|
39
41
|
# @return [Integer]
|
40
42
|
#
|
41
43
|
def retry_times
|
42
|
-
|
44
|
+
options[:retry_times] || Mihari.config.retry_times
|
43
45
|
end
|
44
46
|
|
45
47
|
#
|
46
48
|
# @return [Integer]
|
47
49
|
#
|
48
50
|
def pagination_limit
|
49
|
-
|
51
|
+
options[:pagination_limit] || Mihari.config.pagination_limit
|
52
|
+
end
|
53
|
+
|
54
|
+
#
|
55
|
+
# @return [Boolean]
|
56
|
+
#
|
57
|
+
def ignore_error?
|
58
|
+
ignore_error = options[:ignore_error]
|
59
|
+
return ignore_error unless ignore_error.nil?
|
60
|
+
|
61
|
+
Mihari.config.ignore_error
|
50
62
|
end
|
51
63
|
|
52
64
|
# @return [Array<String>, Array<Mihari::Artifact>]
|
@@ -63,7 +75,7 @@ module Mihari
|
|
63
75
|
#
|
64
76
|
def normalized_artifacts
|
65
77
|
retry_on_error(times: retry_times, interval: retry_interval) do
|
66
|
-
|
78
|
+
artifacts.compact.sort.map do |artifact|
|
67
79
|
# No need to set data_type manually
|
68
80
|
# It is set automatically in #initialize
|
69
81
|
artifact = artifact.is_a?(Artifact) ? artifact : Artifact.new(data: artifact)
|
@@ -73,6 +85,13 @@ module Mihari
|
|
73
85
|
end
|
74
86
|
end
|
75
87
|
|
88
|
+
#
|
89
|
+
# @return [Dry::Monads::Result::Success<Array<Mihari::Artifact>>, Dry::Monads::Result::Failure]
|
90
|
+
#
|
91
|
+
def result
|
92
|
+
Try[StandardError] { normalized_artifacts }.to_result
|
93
|
+
end
|
94
|
+
|
76
95
|
# @return [String]
|
77
96
|
def class_name
|
78
97
|
self.class.to_s.split("::").last
|
@@ -80,8 +99,6 @@ module Mihari
|
|
80
99
|
|
81
100
|
alias_method :source, :class_name
|
82
101
|
|
83
|
-
private
|
84
|
-
|
85
102
|
class << self
|
86
103
|
#
|
87
104
|
# Initialize an analyzer by query params
|
@@ -97,9 +114,7 @@ module Mihari
|
|
97
114
|
query = copied[:query]
|
98
115
|
|
99
116
|
# delete analyzer and query
|
100
|
-
%i[analyzer query].each
|
101
|
-
copied.delete key
|
102
|
-
end
|
117
|
+
%i[analyzer query].each { |key| copied.delete key }
|
103
118
|
|
104
119
|
copied[:options] = copied[:options] || nil
|
105
120
|
|
@@ -18,13 +18,7 @@ module Mihari
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def artifacts
|
21
|
-
client.search_with_pagination(query, pagination_limit: pagination_limit).map
|
22
|
-
events = res["events"] || []
|
23
|
-
events.filter_map do |event|
|
24
|
-
data = event.dig("target", "ip")
|
25
|
-
data.nil? ? nil : Artifact.new(data: data, source: source, metadata: event)
|
26
|
-
end
|
27
|
-
end
|
21
|
+
client.search_with_pagination(query, pagination_limit: pagination_limit).map(&:artifacts).flatten
|
28
22
|
end
|
29
23
|
|
30
24
|
def configuration_keys
|
@@ -19,7 +19,7 @@ module Mihari
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def artifacts
|
22
|
-
raise
|
22
|
+
raise ValueError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
23
23
|
|
24
24
|
domains = client.fuzz(query)
|
25
25
|
Parallel.map(domains) do |domain|
|
data/lib/mihari/analyzers/otx.rb
CHANGED
@@ -31,7 +31,7 @@ module Mihari
|
|
31
31
|
when "ip"
|
32
32
|
client.ip_search(query)
|
33
33
|
else
|
34
|
-
raise
|
34
|
+
raise ValueError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
@@ -38,7 +38,7 @@ module Mihari
|
|
38
38
|
when "hash"
|
39
39
|
client.ssl_search query
|
40
40
|
else
|
41
|
-
raise
|
41
|
+
raise ValueError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
@@ -25,7 +25,7 @@ module Mihari
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def artifacts
|
28
|
-
raise
|
28
|
+
raise ValueError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
29
29
|
|
30
30
|
indicator = client.get_indicator(query)
|
31
31
|
iid = indicator["iid"]
|
@@ -60,9 +60,16 @@ module Mihari
|
|
60
60
|
# @return [Array<Mihari::Artifact>]
|
61
61
|
#
|
62
62
|
def artifacts
|
63
|
-
analyzers.flat_map
|
64
|
-
|
65
|
-
|
63
|
+
analyzers.flat_map do |analyzer|
|
64
|
+
result = analyzer.result
|
65
|
+
|
66
|
+
raise result.failure if result.failure? && !analyzer.ignore_error?
|
67
|
+
|
68
|
+
artifacts = result.value!
|
69
|
+
artifacts.map do |artifact|
|
70
|
+
artifact.rule_id = rule.id
|
71
|
+
artifact
|
72
|
+
end
|
66
73
|
end
|
67
74
|
end
|
68
75
|
|
@@ -113,11 +120,12 @@ module Mihari
|
|
113
120
|
return [] if enriched_artifacts.empty?
|
114
121
|
|
115
122
|
Parallel.map(valid_emitters) do |emitter|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
123
|
+
result = emitter.result
|
124
|
+
|
125
|
+
Mihari.logger.info "Emission by #{emitter.class} is failed: #{result.failure}" if result.failure?
|
126
|
+
Mihari.logger.info "Emission by #{emitter.class} is succeeded" if result.success?
|
127
|
+
|
128
|
+
result.value_or nil
|
121
129
|
end.compact
|
122
130
|
end
|
123
131
|
|
@@ -127,9 +135,6 @@ module Mihari
|
|
127
135
|
# @return [Mihari::Alert, nil]
|
128
136
|
#
|
129
137
|
def run
|
130
|
-
# memoize enriched artifacts
|
131
|
-
enriched_artifacts
|
132
|
-
|
133
138
|
alert_or_something = bulk_emit
|
134
139
|
# returns Mihari::Alert created by the database emitter
|
135
140
|
alert_or_something.find { |res| res.is_a?(Mihari::Alert) }
|
@@ -167,7 +172,7 @@ module Mihari
|
|
167
172
|
# @return [Array<Mihari::Analyzers::Base>]
|
168
173
|
#
|
169
174
|
def analyzers
|
170
|
-
|
175
|
+
rule.queries.map do |query_params|
|
171
176
|
analyzer_name = query_params[:analyzer]
|
172
177
|
klass = get_analyzer_class(analyzer_name)
|
173
178
|
klass.from_query(query_params)
|
@@ -207,7 +212,7 @@ module Mihari
|
|
207
212
|
# @return [Array<Mihari::Emitters::Base>]
|
208
213
|
#
|
209
214
|
def valid_emitters
|
210
|
-
|
215
|
+
emitters.select(&:valid?)
|
211
216
|
end
|
212
217
|
|
213
218
|
#
|
@@ -36,7 +36,7 @@ module Mihari
|
|
36
36
|
when "mail"
|
37
37
|
client.mail_search query
|
38
38
|
else
|
39
|
-
raise
|
39
|
+
raise ValueError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
@@ -31,7 +31,7 @@ module Mihari
|
|
31
31
|
when "ip"
|
32
32
|
ip_search
|
33
33
|
else
|
34
|
-
raise
|
34
|
+
raise ValueError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
@@ -22,7 +22,7 @@ module Mihari
|
|
22
22
|
# @param [Integer] page Default 1, Maximum: 500
|
23
23
|
# @param [Integer, nil] only_ips If selected, only output IP addresses, ports and protocols.
|
24
24
|
#
|
25
|
-
# @return [
|
25
|
+
# @return [Structs::BinaryEdge::Response]
|
26
26
|
#
|
27
27
|
def search(query, page: 1, only_ips: nil)
|
28
28
|
params = {
|
@@ -32,7 +32,7 @@ module Mihari
|
|
32
32
|
}.compact
|
33
33
|
|
34
34
|
res = get("/query/search", params: params)
|
35
|
-
JSON.parse(res.body.to_s)
|
35
|
+
Structs::BinaryEdge::Response.from_dynamic! JSON.parse(res.body.to_s)
|
36
36
|
end
|
37
37
|
|
38
38
|
#
|
@@ -40,19 +40,16 @@ module Mihari
|
|
40
40
|
# @param [Integer, nil] only_ips
|
41
41
|
# @param [Integer] pagination_limit
|
42
42
|
#
|
43
|
-
# @return [Enumerable<
|
43
|
+
# @return [Enumerable<Structs::BinaryEdge::Response.>]
|
44
44
|
#
|
45
45
|
def search_with_pagination(query, only_ips: nil, pagination_limit: Mihari.config.pagination_limit)
|
46
46
|
Enumerator.new do |y|
|
47
47
|
(1..pagination_limit).each do |page|
|
48
48
|
res = search(query, page: page, only_ips: only_ips)
|
49
49
|
|
50
|
-
page_size = res["pagesize"].to_i
|
51
|
-
events = res["events"] || []
|
52
|
-
|
53
50
|
y.yield res
|
54
51
|
|
55
|
-
break if events.length <
|
52
|
+
break if res.events.length < res.pagesize
|
56
53
|
|
57
54
|
sleep_interval
|
58
55
|
end
|
data/lib/mihari/clients/crtsh.rb
CHANGED
@@ -28,9 +28,7 @@ module Mihari
|
|
28
28
|
|
29
29
|
parsed.map do |result|
|
30
30
|
values = result["name_value"].to_s.lines.map(&:chomp)
|
31
|
-
values.map
|
32
|
-
Artifact.new(data: value, metadata: result)
|
33
|
-
end
|
31
|
+
values.map { |value| Artifact.new(data: value, metadata: result) }
|
34
32
|
end.flatten
|
35
33
|
end
|
36
34
|
end
|
@@ -37,7 +37,7 @@ module Mihari
|
|
37
37
|
key: api_key
|
38
38
|
}
|
39
39
|
res = get("/shodan/host/search", params: params)
|
40
|
-
Structs::Shodan::
|
40
|
+
Structs::Shodan::Response.from_dynamic! JSON.parse(res.body.to_s)
|
41
41
|
end
|
42
42
|
|
43
43
|
#
|
@@ -45,7 +45,7 @@ module Mihari
|
|
45
45
|
# @param [Boolean] minify
|
46
46
|
# @param [Integer] pagination_limit
|
47
47
|
#
|
48
|
-
# @return [Enumerable<Structs::Shodan::
|
48
|
+
# @return [Enumerable<Structs::Shodan::Response>]
|
49
49
|
#
|
50
50
|
def search_with_pagination(query, minify: true, pagination_limit: Mihari.config.pagination_limit)
|
51
51
|
Enumerator.new do |y|
|
@@ -6,32 +6,61 @@ module Mihari
|
|
6
6
|
class << self
|
7
7
|
def included(thor)
|
8
8
|
thor.class_eval do
|
9
|
+
include Dry::Monads[:result, :try]
|
10
|
+
|
9
11
|
desc "add [PATH]", "Add an alert"
|
10
12
|
#
|
11
13
|
# @param [String] path
|
12
14
|
#
|
13
15
|
def add(path)
|
14
16
|
Mihari::Database.with_db_connection do
|
15
|
-
|
16
|
-
|
17
|
+
builder = Mihari::Services::AlertBuilder.new(path)
|
18
|
+
|
19
|
+
run_proxy_l = ->(proxy) { run_proxy proxy }
|
20
|
+
check_nil_l = ->(alert_or_nil) { check_nil alert_or_nil }
|
17
21
|
|
18
|
-
|
22
|
+
result = builder.result.bind(run_proxy_l).bind(check_nil_l)
|
19
23
|
|
20
|
-
|
21
|
-
alert =
|
22
|
-
|
23
|
-
|
24
|
-
Mihari.logger.error e.to_s
|
24
|
+
if result.success?
|
25
|
+
alert = result.value!
|
26
|
+
data = Mihari::Entities::Alert.represent(alert)
|
27
|
+
puts JSON.pretty_generate(data.as_json)
|
25
28
|
return
|
26
29
|
end
|
27
30
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
+
failure = result.failure
|
32
|
+
case failure
|
33
|
+
when ValidationError
|
34
|
+
Mihari.logger.error "Failed to parse the input as an alert:"
|
35
|
+
Mihari.logger.error JSON.pretty_generate(failure.errors.to_h)
|
36
|
+
when StandardError
|
37
|
+
raise failure
|
38
|
+
else
|
39
|
+
Mihari.logger.info failure
|
31
40
|
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
no_commands do
|
45
|
+
#
|
46
|
+
# @param [Mihari::Services::AlertProxy] proxy
|
47
|
+
#
|
48
|
+
def run_proxy(proxy)
|
49
|
+
Dry::Monads::Try[StandardError] do
|
50
|
+
runner = Mihari::Services::AlertRunner.new(proxy)
|
51
|
+
runner.run
|
52
|
+
end.to_result
|
53
|
+
end
|
32
54
|
|
33
|
-
|
34
|
-
|
55
|
+
#
|
56
|
+
# @param [Mihari::Alert, nil] alert_or_nil
|
57
|
+
#
|
58
|
+
def check_nil(alert_or_nil)
|
59
|
+
if alert_or_nil.nil?
|
60
|
+
Failure "There is no new artifact found"
|
61
|
+
else
|
62
|
+
Success alert_or_nil
|
63
|
+
end
|
35
64
|
end
|
36
65
|
end
|
37
66
|
end
|
data/lib/mihari/commands/rule.rb
CHANGED
@@ -8,6 +8,8 @@ module Mihari
|
|
8
8
|
class << self
|
9
9
|
def included(thor)
|
10
10
|
thor.class_eval do
|
11
|
+
include Dry::Monads[:result, :try]
|
12
|
+
|
11
13
|
desc "validate [PATH]", "Validate a rule file"
|
12
14
|
#
|
13
15
|
# Validate format of a rule
|
@@ -15,15 +17,17 @@ module Mihari
|
|
15
17
|
# @param [String] path
|
16
18
|
#
|
17
19
|
def validate(path)
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
rule.validate!
|
20
|
+
res = Dry::Monads::Try[ValidationError] do
|
21
|
+
Services::RuleProxy.from_yaml(File.read(path))
|
22
|
+
end.fmap do |rule|
|
22
23
|
Mihari.logger.info "Valid format. The input is parsed as the following:"
|
23
24
|
Mihari.logger.info rule.data.to_yaml
|
24
|
-
rescue RuleValidationError
|
25
|
-
nil
|
26
25
|
end
|
26
|
+
|
27
|
+
return unless res.error?
|
28
|
+
|
29
|
+
Mihari.logger.error "Failed to parse the input as a rule:"
|
30
|
+
Mihari.logger.error JSON.pretty_generate(res.exception.errors.to_h)
|
27
31
|
end
|
28
32
|
|
29
33
|
desc "init [PATH]", "Initialize a new rule file"
|
@@ -47,7 +51,7 @@ module Mihari
|
|
47
51
|
# @return [Mihari::Services::Rule]
|
48
52
|
#
|
49
53
|
def rule_template
|
50
|
-
Services::RuleProxy.
|
54
|
+
Services::RuleProxy.from_yaml File.read(File.expand_path("../templates/rule.yml.erb", __dir__))
|
51
55
|
end
|
52
56
|
|
53
57
|
#
|
@@ -6,7 +6,9 @@ module Mihari
|
|
6
6
|
class << self
|
7
7
|
def included(thor)
|
8
8
|
thor.class_eval do
|
9
|
-
|
9
|
+
include Dry::Monads[:result, :try]
|
10
|
+
|
11
|
+
desc "search [PATH_OR_ID]", "Search by a rule"
|
10
12
|
method_option :force_overwrite, type: :boolean, aliases: "-f", desc: "Force an overwrite the rule"
|
11
13
|
#
|
12
14
|
# Search by a rule
|
@@ -15,39 +17,69 @@ module Mihari
|
|
15
17
|
#
|
16
18
|
def search(path_or_id)
|
17
19
|
Mihari::Database.with_db_connection do
|
18
|
-
|
20
|
+
builder = Services::RuleBuilder.new(path_or_id)
|
21
|
+
|
22
|
+
check_diff_l = ->(rule) { check_diff rule }
|
23
|
+
update_and_run_l = ->(runner) { update_and_run runner }
|
24
|
+
check_nil_l = ->(alert_or_nil) { check_nil alert_or_nil }
|
19
25
|
|
20
|
-
|
21
|
-
|
22
|
-
|
26
|
+
result = builder.result.bind(check_diff_l).bind(update_and_run_l).bind(check_nil_l)
|
27
|
+
|
28
|
+
if result.success?
|
29
|
+
alert = result.value!
|
30
|
+
data = Mihari::Entities::Alert.represent(alert)
|
31
|
+
puts JSON.pretty_generate(data.as_json)
|
23
32
|
return
|
24
33
|
end
|
25
34
|
|
35
|
+
failure = result.failure
|
36
|
+
case failure
|
37
|
+
when ValidationError
|
38
|
+
Mihari.logger.error "Failed to parse the input as a rule:"
|
39
|
+
Mihari.logger.error JSON.pretty_generate(failure.errors.to_h)
|
40
|
+
when StandardError
|
41
|
+
raise failure
|
42
|
+
else
|
43
|
+
Mihari.logger.info failure
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
no_commands do
|
49
|
+
#
|
50
|
+
# @param [Mihari::Services::RuleProxy] rule
|
51
|
+
#
|
52
|
+
def check_diff(rule)
|
26
53
|
force_overwrite = options["force_overwrite"] || false
|
27
54
|
runner = Services::RuleRunner.new(rule, force_overwrite: force_overwrite)
|
55
|
+
message = "There is a diff in the rule (#{rule.id}). Are you sure you want to overwrite the rule? (y/n)"
|
28
56
|
|
29
|
-
if runner.diff? && !force_overwrite
|
30
|
-
|
31
|
-
return unless yes?(message)
|
57
|
+
if runner.diff? && !force_overwrite && !yes?(message)
|
58
|
+
return Failure("Stop overwriting the rule (#{rule.id})")
|
32
59
|
end
|
33
60
|
|
34
|
-
runner
|
35
|
-
|
36
|
-
begin
|
37
|
-
alert = runner.run
|
38
|
-
rescue ConfigurationError => e
|
39
|
-
# if there is a configuration error, output that error without the stack trace
|
40
|
-
Mihari.logger.error e.to_s
|
41
|
-
return
|
42
|
-
end
|
61
|
+
Success runner
|
62
|
+
end
|
43
63
|
|
44
|
-
|
45
|
-
|
46
|
-
|
64
|
+
#
|
65
|
+
# @param [Mihari::Alert, nil] alert_or_nil
|
66
|
+
#
|
67
|
+
def check_nil(alert_or_nil)
|
68
|
+
if alert_or_nil.nil?
|
69
|
+
Failure "There is no new artifact found"
|
70
|
+
else
|
71
|
+
Success alert_or_nil
|
47
72
|
end
|
73
|
+
end
|
48
74
|
|
49
|
-
|
50
|
-
|
75
|
+
#
|
76
|
+
# @param [Mihari::Services::RuleRunner] runner
|
77
|
+
#
|
78
|
+
def update_and_run(runner)
|
79
|
+
Dry::Monads::Try[StandardError] do
|
80
|
+
runner.update_or_create
|
81
|
+
runner.run
|
82
|
+
end.to_result
|
51
83
|
end
|
52
84
|
end
|
53
85
|
end
|
data/lib/mihari/config.rb
CHANGED
@@ -93,6 +93,9 @@ module Mihari
|
|
93
93
|
# @return [Integer]
|
94
94
|
attr_reader :pagination_limit
|
95
95
|
|
96
|
+
# @return [Boolean]
|
97
|
+
attr_reader :ignore_error
|
98
|
+
|
96
99
|
def initialize
|
97
100
|
@binaryedge_api_key = ENV.fetch("BINARYEDGE_API_KEY", nil)
|
98
101
|
|
@@ -147,6 +150,8 @@ module Mihari
|
|
147
150
|
@retry_interval = ENV.fetch("RETRY_INTERVAL", 5).to_i
|
148
151
|
|
149
152
|
@pagination_limit = ENV.fetch("PAGINATION_LIMIT", 100).to_i
|
153
|
+
|
154
|
+
@ignore_error = ENV.fetch("IGNORE_ERROR", false)
|
150
155
|
end
|
151
156
|
end
|
152
157
|
end
|