mihari 5.4.1 → 5.4.2

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.
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mihari/commands/alert"
4
+
5
+ module Mihari
6
+ module CLI
7
+ class Alert < Base
8
+ include Mihari::Commands::Alert
9
+ end
10
+ end
11
+ end
@@ -3,14 +3,16 @@
3
3
  require "thor"
4
4
 
5
5
  # Commands
6
+ require "mihari/commands/alert"
7
+ require "mihari/commands/database"
6
8
  require "mihari/commands/search"
7
9
  require "mihari/commands/version"
8
10
  require "mihari/commands/web"
9
- require "mihari/commands/database"
10
11
 
11
12
  # CLIs
12
13
  require "mihari/cli/base"
13
14
 
15
+ require "mihari/cli/alert"
14
16
  require "mihari/cli/database"
15
17
  require "mihari/cli/rule"
16
18
 
@@ -26,6 +28,9 @@ module Mihari
26
28
 
27
29
  desc "rule", "Sub commands for rule"
28
30
  subcommand "rule", Rule
31
+
32
+ desc "alert", "Sub commands for alert"
33
+ subcommand "alert", Alert
29
34
  end
30
35
  end
31
36
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Commands
5
+ module Alert
6
+ class << self
7
+ def included(thor)
8
+ thor.class_eval do
9
+ desc "add [PATH]", "Add an alert"
10
+ #
11
+ # @param [String] path
12
+ #
13
+ def add(path)
14
+ Mihari::Database.with_db_connection do
15
+ proxy = Mihari::Services::AlertProxy.from_path(path)
16
+ proxy.validate!
17
+
18
+ runner = Mihari::Services::AlertRunner.new(proxy)
19
+
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
25
+ return
26
+ end
27
+
28
+ if alert.nil?
29
+ Mihari.logger.info "There is no new artifact found"
30
+ return
31
+ end
32
+
33
+ data = Mihari::Entities::Alert.represent(alert)
34
+ puts JSON.pretty_generate(data.as_json)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -15,7 +15,7 @@ module Mihari
15
15
  # @param [String] path
16
16
  #
17
17
  def validate(path)
18
- rule = Services::Rule.from_path_or_id(path)
18
+ rule = Services::RuleProxy.from_path_or_id(path)
19
19
 
20
20
  begin
21
21
  rule.validate!
@@ -47,7 +47,7 @@ module Mihari
47
47
  # @return [Mihari::Services::Rule]
48
48
  #
49
49
  def rule_template
50
- Services::Rule.from_path File.expand_path("../templates/rule.yml.erb", __dir__)
50
+ Services::RuleProxy.from_path File.expand_path("../templates/rule.yml.erb", __dir__)
51
51
  end
52
52
 
53
53
  #
@@ -4,60 +4,6 @@ module Mihari
4
4
  module Commands
5
5
  module Search
6
6
  class << self
7
- class RuleWrapper
8
- include Mixins::ErrorNotification
9
-
10
- # @return [Nihari::Structs::Rule]
11
- attr_reader :rule
12
-
13
- # @return [Boolean]
14
- attr_reader :force_overwrite
15
-
16
- def initialize(rule, force_overwrite:)
17
- @rule = rule
18
- @force_overwrite = force_overwrite
19
- end
20
-
21
- def force_overwrite?
22
- force_overwrite
23
- end
24
-
25
- #
26
- # @return [Boolean]
27
- #
28
- def diff?
29
- model = Mihari::Rule.find(rule.id)
30
- model.data != rule.data.deep_stringify_keys
31
- rescue ActiveRecord::RecordNotFound
32
- false
33
- end
34
-
35
- def update_or_create
36
- rule.to_model.save
37
- end
38
-
39
- def run
40
- begin
41
- analyzer = rule.to_analyzer
42
- rescue ConfigurationError => e
43
- # if there is a configuration error, output that error without the stack trace
44
- Mihari.logger.error e.to_s
45
- return
46
- end
47
-
48
- with_error_notification do
49
- alert = analyzer.run
50
- if alert.nil?
51
- Mihari.logger.info "There is no new artifact found"
52
- return
53
- end
54
-
55
- data = Mihari::Entities::Alert.represent(alert)
56
- puts JSON.pretty_generate(data.as_json)
57
- end
58
- end
59
- end
60
-
61
7
  def included(thor)
62
8
  thor.class_eval do
63
9
  desc "search [PATH]", "Search by a rule"
@@ -69,7 +15,7 @@ module Mihari
69
15
  #
70
16
  def search(path_or_id)
71
17
  Mihari::Database.with_db_connection do
72
- rule = Services::Rule.from_path_or_id path_or_id
18
+ rule = Services::RuleProxy.from_path_or_id path_or_id
73
19
 
74
20
  begin
75
21
  rule.validate!
@@ -78,15 +24,30 @@ module Mihari
78
24
  end
79
25
 
80
26
  force_overwrite = options["force_overwrite"] || false
81
- wrapper = RuleWrapper.new(rule, force_overwrite: force_overwrite)
27
+ runner = Services::RuleRunner.new(rule, force_overwrite: force_overwrite)
82
28
 
83
- if wrapper.diff? && !force_overwrite
29
+ if runner.diff? && !force_overwrite
84
30
  message = "There is diff in the rule (#{rule.id}). Are you sure you want to overwrite the rule? (y/n)"
85
31
  return unless yes?(message)
86
32
  end
87
33
 
88
- wrapper.update_or_create
89
- wrapper.run
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
43
+
44
+ if alert.nil?
45
+ Mihari.logger.info "There is no new artifact found"
46
+ return
47
+ end
48
+
49
+ data = Mihari::Entities::Alert.represent(alert)
50
+ puts JSON.pretty_generate(data.as_json)
90
51
  end
91
52
  end
92
53
  end
data/lib/mihari/config.rb CHANGED
@@ -146,7 +146,7 @@ module Mihari
146
146
  @retry_times = ENV.fetch("RETRY_TIMES", 3).to_i
147
147
  @retry_interval = ENV.fetch("RETRY_INTERVAL", 5).to_i
148
148
 
149
- @pagination_limit = ENV.fetch("PAGINATION_LIMIT", 1000).to_i
149
+ @pagination_limit = ENV.fetch("PAGINATION_LIMIT", 100).to_i
150
150
  end
151
151
  end
152
152
  end
@@ -14,7 +14,7 @@ module Mihari
14
14
 
15
15
  #
16
16
  # @param [Array<Mihari::Artifact>] artifacts
17
- # @param [Mihari::Services::Rule] rule
17
+ # @param [Mihari::Services::RuleProxy] rule
18
18
  # @param [Hash] **_options
19
19
  #
20
20
  def initialize(artifacts:, rule:, **_options)
@@ -10,10 +10,10 @@ module Mihari
10
10
  #
11
11
  # Create an alert
12
12
  #
13
- # @return [Mihari::Alert]
13
+ # @return [Mihari::Alert, nil]
14
14
  #
15
15
  def emit
16
- return if artifacts.empty?
16
+ return nil if artifacts.empty?
17
17
 
18
18
  tags = rule.tags.filter_map { |name| Tag.find_or_create_by(name: name) }.uniq
19
19
  taggings = tags.map { |tag| Tagging.new(tag_id: tag.id) }
data/lib/mihari/errors.rb CHANGED
@@ -15,17 +15,38 @@ module Mihari
15
15
 
16
16
  class RuleValidationError < Error; end
17
17
 
18
+ class AlertValidationError < Error; end
19
+
18
20
  class YAMLSyntaxError < Error; end
19
21
 
20
22
  class ConfigurationError < Error; end
21
23
 
24
+ # errors for HTTP interactions
22
25
  class HTTPError < Error; end
23
26
 
24
- class StatusCodeError < HTTPError; end
25
-
26
27
  class NetworkError < HTTPError; end
27
28
 
28
29
  class TimeoutError < HTTPError; end
29
30
 
30
31
  class SSLError < HTTPError; end
32
+
33
+ class StatusCodeError < HTTPError
34
+ # @return [Integer]
35
+ attr_reader :status_code
36
+
37
+ # @return [String, nil]
38
+ attr_reader :body
39
+
40
+ #
41
+ # @param [String] msg
42
+ # @param [Integer] status_code
43
+ # @param [String, nil] body
44
+ #
45
+ def initialize(msg, status_code, body)
46
+ super(msg)
47
+
48
+ @status_code = status_code
49
+ @body = body
50
+ end
51
+ end
31
52
  end
data/lib/mihari/http.rb CHANGED
@@ -94,7 +94,13 @@ module Mihari
94
94
  Net::HTTP.start(url.host, url.port, https_options) do |http|
95
95
  res = http.request(req)
96
96
 
97
- raise StatusCodeError, "Unsuccessful response code returned: #{res.code}" unless res.is_a?(Net::HTTPSuccess)
97
+ unless res.is_a?(Net::HTTPSuccess)
98
+ raise StatusCodeError.new(
99
+ "Unsuccessful response code returned: #{res.code}",
100
+ res.code.to_i,
101
+ res.body
102
+ )
103
+ end
98
104
 
99
105
  res
100
106
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Schemas
5
+ Alert = Dry::Schema.Params do
6
+ required(:rule_id).value(:string)
7
+ required(:artifacts).value(array[:string])
8
+ end
9
+
10
+ class AlertContract < Dry::Validation::Contract
11
+ params(Alert)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Services
5
+ class AlertProxy
6
+ # @return [Hash]
7
+ attr_reader :data
8
+
9
+ # @return [Array, nil]
10
+ attr_reader :errors
11
+
12
+ #
13
+ # Initialize
14
+ #
15
+ # @param [Hash] data
16
+ #
17
+ def initialize(data)
18
+ @data = data.deep_symbolize_keys
19
+
20
+ @errors = nil
21
+
22
+ validate
23
+ end
24
+
25
+ #
26
+ # @return [Boolean]
27
+ #
28
+ def errors?
29
+ return false if @errors.nil?
30
+
31
+ !@errors.empty?
32
+ end
33
+
34
+ def validate
35
+ contract = Schemas::AlertContract.new
36
+ result = contract.call(data)
37
+
38
+ @data = result.to_h
39
+ @errors = result.errors
40
+ end
41
+
42
+ def validate!
43
+ return unless errors?
44
+
45
+ Mihari.logger.error "Failed to parse the input as an alert:"
46
+ Mihari.logger.error JSON.pretty_generate(errors.to_h)
47
+
48
+ raise AlertValidationError, errors
49
+ end
50
+
51
+ def [](key)
52
+ data key.to_sym
53
+ end
54
+
55
+ #
56
+ # @return [String]
57
+ #
58
+ def rule_id
59
+ @rule_id ||= data[:rule_id]
60
+ end
61
+
62
+ #
63
+ # @return [Array<Mihari::Artifact>]
64
+ #
65
+ def artifacts
66
+ @artifacts ||= data[:artifacts].map do |data|
67
+ artifact = Artifact.new(data: data)
68
+ artifact.rule_id = rule_id
69
+ artifact
70
+ end.uniq(&:data).select(&:valid?)
71
+ end
72
+
73
+ #
74
+ # @return [Mihari::Services::RuleProxy]
75
+ #
76
+ def rule
77
+ @rule ||= Services::RuleProxy.from_model(Mihari::Rule.find(rule_id))
78
+ end
79
+
80
+ class << self
81
+ #
82
+ # Load rule from YAML string
83
+ #
84
+ # @param [String] yaml
85
+ #
86
+ # @return [Mihari::Services::Alert]
87
+ #
88
+ def from_yaml(yaml)
89
+ Services::AlertProxy.new YAML.safe_load(yaml, permitted_classes: [Date, Symbol])
90
+ rescue Psych::SyntaxError => e
91
+ raise YAMLSyntaxError, e.message
92
+ end
93
+
94
+ # @param [String] path
95
+ #
96
+ # @return [Mihari::Services::Alert, nil]
97
+ #
98
+ def from_path(path)
99
+ return nil unless Pathname(path).exist?
100
+
101
+ from_yaml File.read(path)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Services
5
+ class AlertRunner
6
+ # @return [Mihari::Services::AlertProxy]
7
+ attr_reader :alert
8
+
9
+ def initialize(alert)
10
+ @alert = alert
11
+ end
12
+
13
+ #
14
+ # @return [Mihari::Alert]
15
+ #
16
+ def run
17
+ emitter = Mihari::Emitters::Database.new(artifacts: alert.artifacts, rule: alert.rule)
18
+ emitter.emit
19
+ end
20
+ end
21
+ end
22
+ end
@@ -9,7 +9,11 @@ require "yaml"
9
9
 
10
10
  module Mihari
11
11
  module Services
12
- class Rule
12
+ #
13
+ # proxy (or converter) class for rule
14
+ # proxying rule schema data into analyzer & model
15
+ #
16
+ class RuleProxy
13
17
  include Mixins::FalsePositive
14
18
 
15
19
  # @return [Hash]
@@ -141,7 +145,7 @@ module Mihari
141
145
  #
142
146
  # @return [Mihari::Rule]
143
147
  #
144
- def to_model
148
+ def model
145
149
  rule = Mihari::Rule.find(id)
146
150
 
147
151
  rule.title = title
@@ -161,7 +165,7 @@ module Mihari
161
165
  #
162
166
  # @return [Mihari::Analyzers::Rule]
163
167
  #
164
- def to_analyzer
168
+ def analyzer
165
169
  Mihari::Analyzers::Rule.new self
166
170
  end
167
171
 
@@ -174,7 +178,7 @@ module Mihari
174
178
  # @return [Mihari::Services::Rule]
175
179
  #
176
180
  def from_yaml(yaml)
177
- Services::Rule.new YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol])
181
+ Services::RuleProxy.new YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol])
178
182
  rescue Psych::SyntaxError => e
179
183
  raise YAMLSyntaxError, e.message
180
184
  end
@@ -185,7 +189,7 @@ module Mihari
185
189
  # @return [Mihari::Services::Rule]
186
190
  #
187
191
  def from_model(model)
188
- Services::Rule.new model.data
192
+ Services::RuleProxy.new model.data
189
193
  end
190
194
 
191
195
  #
@@ -211,7 +215,7 @@ module Mihari
211
215
  def from_id(id)
212
216
  return nil unless Mihari::Rule.exists?(id)
213
217
 
214
- Services::Rule.from_model Mihari::Rule.find(id)
218
+ Services::RuleProxy.from_model Mihari::Rule.find(id)
215
219
  end
216
220
 
217
221
  #
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mihari
4
+ module Services
5
+ class RuleRunner
6
+ include Mixins::ErrorNotification
7
+
8
+ # @return [Mihari::Services::RuleProxy]
9
+ attr_reader :rule
10
+
11
+ # @return [Boolean]
12
+ attr_reader :force_overwrite
13
+
14
+ def initialize(rule, force_overwrite:)
15
+ @rule = rule
16
+ @force_overwrite = force_overwrite
17
+ end
18
+
19
+ def force_overwrite?
20
+ force_overwrite
21
+ end
22
+
23
+ #
24
+ # @return [Boolean]
25
+ #
26
+ def diff?
27
+ model = Mihari::Rule.find(rule.id)
28
+ model.data != rule.data.deep_stringify_keys
29
+ rescue ActiveRecord::RecordNotFound
30
+ false
31
+ end
32
+
33
+ def update_or_create
34
+ rule.model.save
35
+ end
36
+
37
+ #
38
+ # @return [Mihari::Alert, nil]
39
+ #
40
+ def run
41
+ analyzer = rule.analyzer
42
+
43
+ with_error_notification do
44
+ analyzer.run
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "5.4.1"
4
+ VERSION = "5.4.2"
5
5
  end
@@ -67,6 +67,28 @@ module Mihari
67
67
  status 204
68
68
  present({ message: "" }, with: Entities::Message)
69
69
  end
70
+
71
+ desc "Create an alert", {
72
+ success: Entities::Alert,
73
+ summary: "Create an alert"
74
+ }
75
+ params do
76
+ requires :ruleId, type: String, documentation: { param_type: "body" }
77
+ requires :artifacts, type: Array, documentation: { type: String, is_array: true, param_type: "body" }
78
+ end
79
+ post "/" do
80
+ proxy = Services::AlertProxy.new(params.to_snake_keys)
81
+ runner = Services::AlertRunner.new(proxy)
82
+
83
+ begin
84
+ alert = runner.run
85
+ rescue ActiveRecord::RecordNotFound
86
+ error!({ message: "Rule:#{params["ruleId"]} is not found" }, 404)
87
+ end
88
+
89
+ status 201
90
+ present alert, with: Entities::Alert
91
+ end
70
92
  end
71
93
  end
72
94
  end
@@ -83,12 +83,12 @@ module Mihari
83
83
  id = params["id"].to_s
84
84
 
85
85
  begin
86
- rule = Mihari::Services::Rule.from_model(Mihari::Rule.find(id))
86
+ rule = Mihari::Services::RuleProxy.from_model(Mihari::Rule.find(id))
87
87
  rescue ActiveRecord::RecordNotFound
88
88
  error!({ message: "ID:#{id} is not found" }, 404)
89
89
  end
90
90
 
91
- analyzer = rule.to_analyzer
91
+ analyzer = rule.analyzer
92
92
  analyzer.run
93
93
 
94
94
  status 201
@@ -106,7 +106,7 @@ module Mihari
106
106
  yaml = params[:yaml]
107
107
 
108
108
  begin
109
- rule = Services::Rule.from_yaml(yaml)
109
+ rule = Services::RuleProxy.from_yaml(yaml)
110
110
  rescue YAMLSyntaxError => e
111
111
  error!({ message: e.message }, 400)
112
112
  end
@@ -129,13 +129,13 @@ module Mihari
129
129
  end
130
130
 
131
131
  begin
132
- rule.to_model.save
132
+ rule.model.save
133
133
  rescue ActiveRecord::RecordNotUnique
134
134
  error!({ message: "ID:#{rule.id} is already registered" }, 400)
135
135
  end
136
136
 
137
137
  status 201
138
- present rule.to_model, with: Entities::Rule
138
+ present rule.model, with: Entities::Rule
139
139
  end
140
140
 
141
141
  desc "Update a rule", {
@@ -157,7 +157,7 @@ module Mihari
157
157
  end
158
158
 
159
159
  begin
160
- rule = Services::Rule.from_yaml(yaml)
160
+ rule = Services::RuleProxy.from_yaml(yaml)
161
161
  rescue YAMLSyntaxError => e
162
162
  error!({ message: e.message }, 400)
163
163
  end
@@ -172,13 +172,13 @@ module Mihari
172
172
  end
173
173
 
174
174
  begin
175
- rule.to_model.save
175
+ rule.model.save
176
176
  rescue ActiveRecord::RecordNotUnique
177
177
  error!({ message: "ID:#{id} is already registered" }, 400)
178
178
  end
179
179
 
180
180
  status 201
181
- present rule.to_model, with: Entities::Rule
181
+ present rule.model, with: Entities::Rule
182
182
  end
183
183
 
184
184
  desc "Delete a rule", {