mihari 5.4.3 → 5.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/frontend/package-lock.json +2399 -1504
  3. data/frontend/package.json +22 -22
  4. data/lib/mihari/analyzers/base.rb +25 -10
  5. data/lib/mihari/analyzers/binaryedge.rb +1 -7
  6. data/lib/mihari/analyzers/circl.rb +1 -1
  7. data/lib/mihari/analyzers/dnstwister.rb +1 -1
  8. data/lib/mihari/analyzers/otx.rb +1 -1
  9. data/lib/mihari/analyzers/passivetotal.rb +1 -1
  10. data/lib/mihari/analyzers/pulsedive.rb +1 -1
  11. data/lib/mihari/analyzers/rule.rb +18 -13
  12. data/lib/mihari/analyzers/securitytrails.rb +1 -1
  13. data/lib/mihari/analyzers/urlscan.rb +1 -1
  14. data/lib/mihari/analyzers/virustotal.rb +1 -1
  15. data/lib/mihari/analyzers/zoomeye.rb +1 -1
  16. data/lib/mihari/clients/binaryedge.rb +4 -7
  17. data/lib/mihari/clients/crtsh.rb +1 -3
  18. data/lib/mihari/clients/publsedive.rb +1 -1
  19. data/lib/mihari/clients/shodan.rb +2 -2
  20. data/lib/mihari/commands/alert.rb +42 -13
  21. data/lib/mihari/commands/rule.rb +11 -7
  22. data/lib/mihari/commands/search.rb +54 -22
  23. data/lib/mihari/config.rb +5 -0
  24. data/lib/mihari/emitters/base.rb +9 -3
  25. data/lib/mihari/emitters/slack.rb +1 -1
  26. data/lib/mihari/enrichers/base.rb +13 -0
  27. data/lib/mihari/enrichers/google_public_dns.rb +16 -1
  28. data/lib/mihari/enrichers/ipinfo.rb +9 -13
  29. data/lib/mihari/enrichers/shodan.rb +1 -2
  30. data/lib/mihari/enrichers/whois.rb +2 -2
  31. data/lib/mihari/errors.rb +16 -10
  32. data/lib/mihari/feed/parser.rb +2 -2
  33. data/lib/mihari/models/artifact.rb +1 -1
  34. data/lib/mihari/models/autonomous_system.rb +11 -5
  35. data/lib/mihari/models/cpe.rb +10 -4
  36. data/lib/mihari/models/dns.rb +11 -16
  37. data/lib/mihari/models/geolocation.rb +11 -5
  38. data/lib/mihari/models/port.rb +10 -4
  39. data/lib/mihari/models/reverse_dns.rb +10 -4
  40. data/lib/mihari/models/whois.rb +4 -1
  41. data/lib/mihari/schemas/analyzer.rb +1 -0
  42. data/lib/mihari/services/alert_builder.rb +43 -0
  43. data/lib/mihari/services/alert_proxy.rb +7 -25
  44. data/lib/mihari/services/alert_runner.rb +9 -0
  45. data/lib/mihari/services/rule_builder.rb +47 -0
  46. data/lib/mihari/services/rule_proxy.rb +5 -61
  47. data/lib/mihari/services/rule_runner.rb +9 -4
  48. data/lib/mihari/structs/binaryedge.rb +89 -0
  49. data/lib/mihari/structs/shodan.rb +2 -1
  50. data/lib/mihari/structs/urlscan.rb +1 -3
  51. data/lib/mihari/structs/virustotal_intelligence.rb +1 -3
  52. data/lib/mihari/type_checker.rb +1 -1
  53. data/lib/mihari/version.rb +1 -1
  54. data/lib/mihari/web/endpoints/alerts.rb +33 -15
  55. data/lib/mihari/web/endpoints/artifacts.rb +53 -25
  56. data/lib/mihari/web/endpoints/configs.rb +2 -2
  57. data/lib/mihari/web/endpoints/ip_addresses.rb +3 -5
  58. data/lib/mihari/web/endpoints/rules.rb +97 -71
  59. data/lib/mihari/web/endpoints/tags.rb +15 -5
  60. data/lib/mihari/web/public/assets/index-ef33a6cd.js +1738 -0
  61. data/lib/mihari/web/public/index.html +1 -1
  62. data/lib/mihari/web/public/redoc-static.html +419 -382
  63. data/lib/mihari.rb +4 -0
  64. data/mihari.gemspec +5 -4
  65. metadata +28 -11
  66. data/lib/mihari/web/public/assets/index-4d7eda9f.js +0 -1738
@@ -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.3.0",
21
- "@vueuse/router": "^10.3.0",
22
- "ace-builds": "^1.24.1",
23
- "axios": "^1.4.0",
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.9",
26
+ "dayjs": "^1.11.10",
27
27
  "font-awesome-animation": "^1.1.1",
28
- "js-sha256": "^0.9.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.4",
36
+ "vue-router": "^4.2.5",
37
37
  "vue3-ace-editor": "^2.2.3"
38
38
  },
39
39
  "devDependencies": {
40
- "@redocly/cli": "1.0.2",
41
- "@rushstack/eslint-patch": "^1.3.3",
42
- "@tsconfig/node20": "^20.1.1",
43
- "@types/jsdom": "^21.1.1",
44
- "@types/node": "^20.5.1",
45
- "@types/url-parse": "^1.4.8",
46
- "@typescript-eslint/eslint-plugin": "^6.4.0",
47
- "@typescript-eslint/parser": "^6.4.0",
48
- "@vitejs/plugin-vue": "^4.3.1",
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": "^11.0.3",
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.47.0",
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.2",
62
- "typescript": "~5.1.6",
61
+ "prettier": "^3.0.3",
62
+ "typescript": "~5.2.2",
63
63
  "vite": "^4.4.9",
64
- "vitest": "^0.34.2",
65
- "vue-tsc": "^1.8.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
- @interval ||= options[:interval]
30
+ options[:interval]
29
31
  end
30
32
 
31
33
  #
32
34
  # @return [Integer]
33
35
  #
34
36
  def retry_interval
35
- @retry_interval ||= options[:retry_interval] || Mihari.config.retry_interval
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
- @retry_times ||= options[:retry_times] || Mihari.config.retry_times
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
- @pagination_limit ||= options[:pagination_limit] || Mihari.config.pagination_limit
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
- @normalized_artifacts ||= artifacts.compact.sort.map do |artifact|
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 do |key|
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 do |res|
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
@@ -36,7 +36,7 @@ module Mihari
36
36
  when "hash"
37
37
  client.passive_ssl_search query
38
38
  else
39
- raise InvalidInputError, "#{@query}(type: #{@type || "unknown"}) is not supported."
39
+ raise ValueError, "#{@query}(type: #{@type || "unknown"}) is not supported."
40
40
  end
41
41
  end
42
42
 
@@ -19,7 +19,7 @@ module Mihari
19
19
  end
20
20
 
21
21
  def artifacts
22
- raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
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|
@@ -31,7 +31,7 @@ module Mihari
31
31
  when "ip"
32
32
  client.ip_search(query)
33
33
  else
34
- raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
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 InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
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 InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
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(&:normalized_artifacts).map do |artifact|
64
- artifact.rule_id = rule.id
65
- artifact
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
- emission = emitter.emit
117
- Mihari.logger.info "Emission by #{emitter.class} is succeeded"
118
- emission
119
- rescue StandardError => e
120
- Mihari.logger.info "Emission by #{emitter.class} is failed: #{e}"
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
- @analyzers ||= rule.queries.map do |query_params|
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
- @valid_emitters ||= emitters.select(&:valid?)
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 InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
39
+ raise ValueError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
40
40
  end
41
41
  end
42
42
 
@@ -25,7 +25,7 @@ module Mihari
25
25
 
26
26
  return if valid_allowed_data_types?
27
27
 
28
- raise InvalidInputError, "allowed_data_types should be any of url, domain and ip."
28
+ raise ValueError, "allowed_data_types should be any of url, domain and ip."
29
29
  end
30
30
 
31
31
  def artifacts
@@ -31,7 +31,7 @@ module Mihari
31
31
  when "ip"
32
32
  ip_search
33
33
  else
34
- raise InvalidInputError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
34
+ raise ValueError, "#{query}(type: #{type || "unknown"}) is not supported." unless valid_type?
35
35
  end
36
36
  end
37
37
 
@@ -33,7 +33,7 @@ module Mihari
33
33
  convert(res)
34
34
  end.flatten
35
35
  else
36
- raise InvalidInputError, "#{type} type is not supported." unless valid_type?
36
+ raise ValueError, "#{type} type is not supported." unless valid_type?
37
37
  end
38
38
  end
39
39
 
@@ -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 [Hash]
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<Hash>]
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 < page_size
52
+ break if res.events.length < res.pagesize
56
53
 
57
54
  sleep_interval
58
55
  end
@@ -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 do |value|
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
@@ -49,7 +49,7 @@ module Mihari
49
49
  params["key"] = api_key
50
50
 
51
51
  res = get(path, params: params)
52
- JSON.parse(res.body.to_s)
52
+ JSON.parse res.body.to_s
53
53
  end
54
54
  end
55
55
  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::Result.from_dynamic! JSON.parse(res.body.to_s)
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::Result>]
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
- proxy = Mihari::Services::AlertProxy.from_path(path)
16
- proxy.validate!
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
- runner = Mihari::Services::AlertRunner.new(proxy)
22
+ result = builder.result.bind(run_proxy_l).bind(check_nil_l)
19
23
 
20
- begin
21
- alert = runner.run
22
- rescue ActiveRecord::RecordNotFound => e
23
- # if there is a ActiveRecord::RecordNotFound, output that error without the stack trace
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
- if alert.nil?
29
- Mihari.logger.info "There is no new artifact found"
30
- return
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
- data = Mihari::Entities::Alert.represent(alert)
34
- puts JSON.pretty_generate(data.as_json)
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
@@ -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
- rule = Services::RuleProxy.from_path_or_id(path)
19
-
20
- begin
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.from_path File.expand_path("../templates/rule.yml.erb", __dir__)
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
- desc "search [PATH]", "Search by a rule"
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
- rule = Services::RuleProxy.from_path_or_id path_or_id
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
- begin
21
- rule.validate!
22
- rescue RuleValidationError
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
- message = "There is diff in the rule (#{rule.id}). Are you sure you want to overwrite the rule? (y/n)"
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.update_or_create
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
- if alert.nil?
45
- Mihari.logger.info "There is no new artifact found"
46
- return
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
- data = Mihari::Entities::Alert.represent(alert)
50
- puts JSON.pretty_generate(data.as_json)
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