mihari 5.4.1 → 5.4.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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", {