mihari 4.7.0 → 4.7.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/mihari/analyzers/clients/otx.rb +36 -0
  3. data/lib/mihari/analyzers/otx.rb +19 -11
  4. data/lib/mihari/analyzers/rule.rb +0 -1
  5. data/lib/mihari/commands/init.rb +25 -2
  6. data/lib/mihari/commands/search.rb +2 -7
  7. data/lib/mihari/commands/validator.rb +10 -5
  8. data/lib/mihari/errors.rb +2 -0
  9. data/lib/mihari/models/alert.rb +6 -1
  10. data/lib/mihari/models/geolocation.rb +2 -4
  11. data/lib/mihari/models/port.rb +1 -1
  12. data/lib/mihari/models/rule.rb +7 -2
  13. data/lib/mihari/structs/filters.rb +71 -0
  14. data/lib/mihari/structs/ipinfo.rb +4 -4
  15. data/lib/mihari/structs/rule.rb +188 -139
  16. data/lib/mihari/version.rb +1 -1
  17. data/lib/mihari/web/endpoints/alerts.rb +1 -1
  18. data/lib/mihari/web/endpoints/rules.rb +13 -5
  19. data/lib/mihari/web/public/index.html +1 -1
  20. data/lib/mihari/web/public/redoc-static.html +796 -763
  21. data/lib/mihari/web/public/static/css/chunk-vendors.5013d549.css +7 -0
  22. data/lib/mihari/web/public/static/js/app.524d9ed2.js +2 -0
  23. data/lib/mihari/web/public/static/js/app.524d9ed2.js.map +1 -0
  24. data/lib/mihari/web/public/static/js/{chunk-vendors.dde2116c.js → chunk-vendors.64580a1f.js} +7 -7
  25. data/lib/mihari/web/public/static/js/chunk-vendors.64580a1f.js.map +1 -0
  26. data/lib/mihari.rb +1 -2
  27. data/mihari.gemspec +10 -11
  28. data/sig/lib/mihari/cli/base.rbs +0 -2
  29. data/sig/lib/mihari/models/alert.rbs +3 -3
  30. data/sig/lib/mihari/models/rule.rbs +2 -2
  31. data/sig/lib/mihari/structs/filters.rbs +40 -0
  32. data/sig/lib/mihari/structs/ipinfo.rbs +2 -2
  33. data/sig/lib/mihari/structs/rule.rbs +36 -43
  34. metadata +30 -49
  35. data/lib/mihari/mixins/rule.rb +0 -84
  36. data/lib/mihari/structs/alert.rb +0 -44
  37. data/lib/mihari/web/public/static/css/chunk-vendors.06251949.css +0 -7
  38. data/lib/mihari/web/public/static/js/app-legacy.9d5c9c3d.js +0 -2
  39. data/lib/mihari/web/public/static/js/app-legacy.9d5c9c3d.js.map +0 -1
  40. data/lib/mihari/web/public/static/js/app.823b5af7.js +0 -2
  41. data/lib/mihari/web/public/static/js/app.823b5af7.js.map +0 -1
  42. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.b110c129.js +0 -25
  43. data/lib/mihari/web/public/static/js/chunk-vendors-legacy.b110c129.js.map +0 -1
  44. data/lib/mihari/web/public/static/js/chunk-vendors.dde2116c.js.map +0 -1
  45. data/sig/lib/mihari/mixins/rule.rbs +0 -36
  46. data/sig/lib/mihari/structs/alert.rbs +0 -27
@@ -1,187 +1,236 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "date"
4
+ require "erb"
5
+ require "pathname"
6
+ require "yaml"
7
+
3
8
  module Mihari
4
9
  module Structs
5
- module Rule
6
- class SearchFilter < Dry::Struct
7
- attribute? :description, Types::String.optional
8
- attribute? :tag_name, Types::String.optional
9
- attribute? :title, Types::String.optional
10
- attribute? :from_at, Types::DateTime.optional
11
- attribute? :to_at, Types::DateTime.optional
10
+ class Rule
11
+ # @return [Hash]
12
+ attr_reader :data
13
+
14
+ # @return [String]
15
+ attr_reader :yaml
16
+
17
+ # @return [Array, nil]
18
+ attr_reader :errors
19
+
20
+ # @return [String]
21
+ attr_writer :id
22
+
23
+ def initialize(data, yaml)
24
+ @data = data.deep_symbolize_keys
25
+ @yaml = yaml
26
+
27
+ @errors = nil
28
+ @no_method_error = nil
29
+
30
+ validate
12
31
  end
13
32
 
14
- class SearchFilterWithPagination < SearchFilter
15
- attribute? :page, Types::Int.default(1)
16
- attribute? :limit, Types::Int.default(10)
17
-
18
- def without_pagination
19
- SearchFilter.new(
20
- description: description,
21
- from_at: from_at,
22
- tag_name: tag_name,
23
- title: title,
24
- to_at: to_at
25
- )
33
+ #
34
+ # @return [Boolean]
35
+ #
36
+ def errors?
37
+ return false if @errors.nil?
38
+
39
+ !@errors.empty?
40
+ end
41
+
42
+ #
43
+ # @return [Array<String>]
44
+ #
45
+ def error_messages
46
+ return [] if @errors.nil?
47
+
48
+ @errors.messages.filter_map do |message|
49
+ path = message.path.map(&:to_s).join
50
+ "#{path} #{message.text}"
51
+ rescue NoMethodError
52
+ nil
26
53
  end
27
54
  end
28
55
 
29
- class Rule
30
- # @return [Hash]
31
- attr_reader :data
56
+ def validate
57
+ begin
58
+ contract = Schemas::RuleContract.new
59
+ result = contract.call(data)
60
+ rescue NoMethodError => e
61
+ @no_method_error = e
62
+ return
63
+ end
64
+
65
+ @data = result.to_h
66
+ @errors = result.errors
67
+ end
32
68
 
33
- # @return [String]
34
- attr_reader :yaml
69
+ def validate!
70
+ raise RuleValidationError, "Data should be a hash" unless data.is_a?(Hash)
71
+ raise RuleValidationError, error_messages.join("\n") if errors?
72
+ raise RuleValidationError, "Something wrong with queries, emitters or enrichers." unless @no_method_error.nil?
73
+ rescue RuleValidationError => e
74
+ message = "Failed to parse the input as a rule"
75
+ message += ": #{e.message}" unless e.message.empty?
76
+ Mihari.logger.error message
35
77
 
36
- # @return [Array, nil]
37
- attr_reader :errors
78
+ raise e
79
+ end
38
80
 
39
- # @return [String]
40
- attr_writer :id
81
+ def [](key)
82
+ data[key.to_sym]
83
+ end
41
84
 
42
- def initialize(data, yaml)
43
- @data = data.deep_symbolize_keys
44
- @yaml = yaml
85
+ #
86
+ # @return [String]
87
+ #
88
+ def id
89
+ @id ||= data[:id] || UUIDTools::UUID.md5_create(UUIDTools::UUID_URL_NAMESPACE, data.to_yaml).to_s
90
+ end
45
91
 
46
- @errors = nil
47
- @no_method_error = nil
92
+ #
93
+ # @return [String]
94
+ #
95
+ def title
96
+ @title ||= data[:title]
97
+ end
48
98
 
49
- validate
50
- end
99
+ #
100
+ # @return [String]
101
+ #
102
+ def description
103
+ @description ||= data[:description]
104
+ end
51
105
 
52
- #
53
- # @return [Boolean]
54
- #
55
- def errors?
56
- return false if @errors.nil?
106
+ #
107
+ # @return [Mihari::Rule]
108
+ #
109
+ def to_model
110
+ rule = Mihari::Rule.find(id)
111
+
112
+ rule.title = title
113
+ rule.description = description
114
+ rule.data = data
115
+ rule.yaml = yaml
116
+
117
+ rule
118
+ rescue ActiveRecord::RecordNotFound
119
+ Mihari::Rule.new(
120
+ id: id,
121
+ title: title,
122
+ description: description,
123
+ data: data,
124
+ yaml: yaml
125
+ )
126
+ end
57
127
 
58
- !@errors.empty?
59
- end
128
+ #
129
+ # @return [Mihari::Analyzers::Rule]
130
+ #
131
+ def to_analyzer
132
+ analyzer = Mihari::Analyzers::Rule.new(
133
+ title: self[:title],
134
+ description: self[:description],
135
+ tags: self[:tags],
136
+ queries: self[:queries],
137
+ allowed_data_types: self[:allowed_data_types],
138
+ disallowed_data_values: self[:disallowed_data_values],
139
+ emitters: self[:emitters],
140
+ enrichers: self[:enrichers],
141
+ id: id
142
+ )
143
+ analyzer.ignore_old_artifacts = self[:ignore_old_artifacts]
144
+ analyzer.ignore_threshold = self[:ignore_threshold]
145
+
146
+ analyzer
147
+ end
148
+
149
+ class << self
150
+ include Mixins::Database
60
151
 
61
152
  #
62
- # @return [Array<String>]
153
+ # @param [Mihari::Rule] model
63
154
  #
64
- def error_messages
65
- return [] if @errors.nil?
155
+ # @return [Mihari::Structs::Rule]
156
+ #
157
+ def from_model(model)
158
+ data = model.data.deep_symbolize_keys
159
+ # set ID if YAML data do not have ID
160
+ data[:id] = model.id unless data.key?(:id)
66
161
 
67
- @errors.messages.map do |message|
68
- path = message.path.map(&:to_s).join
69
- "#{path} #{message.text}"
70
- end
162
+ Structs::Rule.new(data, model.yaml)
71
163
  end
72
164
 
73
- def validate
74
- begin
75
- contract = Schemas::RuleContract.new
76
- result = contract.call(data)
77
- rescue NoMethodError => e
78
- @no_method_error = e
79
- return
80
- end
165
+ #
166
+ # @param [String] yaml
167
+ #
168
+ # @return [Mihari::Structs::Rule]
169
+ # @param [String, nil] id
170
+ #
171
+ def from_yaml(yaml, id: nil)
172
+ data = load_erb_yaml(yaml)
173
+ # set ID if id is given & YAML data do not have ID
174
+ data[:id] = id if !id.nil? && !data.key?(:id)
81
175
 
82
- @data = result.to_h
83
- @errors = result.errors
176
+ Structs::Rule.new(data, yaml)
84
177
  end
85
178
 
86
- def validate!
87
- raise RuleValidationError, "Data should be a hash" unless data.is_a?(Hash)
179
+ #
180
+ # @param [String] path_or_id Path to YAML file or YAML string or ID of a rule in the database
181
+ #
182
+ # @return [Mihari::Structs::Rule]
183
+ #
184
+ def from_path_or_id(path_or_id)
185
+ yaml = nil
88
186
 
89
- raise RuleValidationError, error_messages.join("\n") if errors?
187
+ yaml = load_yaml_from_file(path_or_id) if File.exist?(path_or_id)
188
+ yaml = load_yaml_from_db(path_or_id) if yaml.nil?
90
189
 
91
- raise RuleValidationError, "Something wrong with queries, emitters or enrichers." unless @no_method_error.nil?
190
+ Structs::Rule.from_yaml yaml
92
191
  end
93
192
 
94
- def [](key)
95
- data[key.to_sym]
96
- end
193
+ private
97
194
 
98
195
  #
99
- # @return [String]
196
+ # Load ERR templated YAML
100
197
  #
101
- def id
102
- @id ||= data[:id] || UUIDTools::UUID.md5_create(UUIDTools::UUID_URL_NAMESPACE, data.to_yaml).to_s
103
- end
104
-
198
+ # @param [String] yaml
105
199
  #
106
- # @return [String]
200
+ # @return [Hash]
107
201
  #
108
- def title
109
- @title ||= data[:title]
202
+ def load_erb_yaml(yaml)
203
+ YAML.safe_load(ERB.new(yaml).result, permitted_classes: [Date, Symbol], symbolize_names: true)
204
+ rescue Psych::SyntaxError => e
205
+ raise YAMLSyntaxError, e.message
110
206
  end
111
207
 
112
208
  #
113
- # @return [String]
209
+ # Load YAML string from path
114
210
  #
115
- def description
116
- @description ||= data[:description]
117
- end
118
-
211
+ # @param [String] path
119
212
  #
120
- # @return [Mihari::Rule]
213
+ # @return [String, nil]
121
214
  #
122
- def to_model
123
- rule = Mihari::Rule.find(id)
215
+ def load_yaml_from_file(path)
216
+ return nil unless Pathname(path).exist?
124
217
 
125
- rule.title = title
126
- rule.description = description
127
- rule.data = data
128
- rule.yaml = yaml
129
-
130
- rule
131
- rescue ActiveRecord::RecordNotFound
132
- Mihari::Rule.new(
133
- id: id,
134
- title: title,
135
- description: description,
136
- data: data,
137
- yaml: yaml
138
- )
218
+ File.read path
139
219
  end
140
220
 
141
221
  #
142
- # @return [Mihari::Analyzers::Rule]
143
- #
144
- def to_analyzer
145
- analyzer = Mihari::Analyzers::Rule.new(
146
- title: self[:title],
147
- description: self[:description],
148
- tags: self[:tags],
149
- queries: self[:queries],
150
- allowed_data_types: self[:allowed_data_types],
151
- disallowed_data_values: self[:disallowed_data_values],
152
- emitters: self[:emitters],
153
- enrichers: self[:enrichers],
154
- id: id
155
- )
156
- analyzer.ignore_old_artifacts = self[:ignore_old_artifacts]
157
- analyzer.ignore_threshold = self[:ignore_threshold]
158
-
159
- analyzer
160
- end
161
-
162
- class << self
163
- include Mixins::Rule
164
-
165
- #
166
- # @param [Mihari::Rule] model
167
- #
168
- # @return [Mihari::Structs::Rule::Rule]
169
- #
170
- def from_model(model)
171
- data = model.data.deep_symbolize_keys
172
- data[:id] = model.id unless data.key?(:id)
173
-
174
- Structs::Rule::Rule.new(data, model.yaml)
175
- end
176
-
177
- #
178
- # @param [String] yaml
179
- #
180
- # @return [Mihari::Structs::Rule::Rule]
181
- #
182
- def from_yaml(yaml)
183
- data = load_erb_yaml(yaml)
184
- Structs::Rule::Rule.new(data, yaml)
222
+ # Load YAML string from the database
223
+ #
224
+ # @param [String] id <description>
225
+ #
226
+ # @return [Hash]
227
+ #
228
+ def load_yaml_from_db(id)
229
+ with_db_connection do
230
+ rule = Mihari::Rule.find(id)
231
+ rule.yaml || rule.symbolized_data.to_yaml
232
+ rescue ActiveRecord::RecordNotFound
233
+ raise ArgumentError, "ID:#{id} is not found in the database"
185
234
  end
186
235
  end
187
236
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mihari
4
- VERSION = "4.7.0"
4
+ VERSION = "4.7.3"
5
5
  end
@@ -43,7 +43,7 @@ module Mihari
43
43
  # symbolize hash keys
44
44
  filter = filter.to_h.symbolize_keys
45
45
 
46
- search_filter_with_pagenation = Structs::Alert::SearchFilterWithPagination.new(**filter)
46
+ search_filter_with_pagenation = Structs::Filters::Alert::SearchFilterWithPagination.new(**filter)
47
47
  alerts = Mihari::Alert.search(search_filter_with_pagenation)
48
48
  total = Mihari::Alert.count(search_filter_with_pagenation.without_pagination)
49
49
 
@@ -36,7 +36,7 @@ module Mihari
36
36
  # symbolize hash keys
37
37
  filter = filter.to_h.symbolize_keys
38
38
 
39
- search_filter_with_pagenation = Structs::Rule::SearchFilterWithPagination.new(**filter)
39
+ search_filter_with_pagenation = Structs::Filters::Rule::SearchFilterWithPagination.new(**filter)
40
40
  rules = Mihari::Rule.search(search_filter_with_pagenation)
41
41
  total = Mihari::Rule.count(search_filter_with_pagenation.without_pagination)
42
42
 
@@ -79,7 +79,7 @@ module Mihari
79
79
  error!({ message: "ID:#{id} is not found" }, 404)
80
80
  end
81
81
 
82
- struct = Mihari::Structs::Rule::Rule.from_model(rule)
82
+ struct = Mihari::Structs::Rule.from_model(rule)
83
83
  analyzer = struct.to_analyzer
84
84
  analyzer.run
85
85
 
@@ -96,7 +96,12 @@ module Mihari
96
96
  end
97
97
  post "/" do
98
98
  yaml = params[:yaml]
99
- rule = Structs::Rule::Rule.from_yaml(yaml)
99
+
100
+ begin
101
+ rule = Structs::Rule.from_yaml(yaml)
102
+ rescue YAMLSyntaxError => e
103
+ error!({ message: e.message }, 400)
104
+ end
100
105
 
101
106
  # check ID duplication
102
107
  begin
@@ -144,8 +149,11 @@ module Mihari
144
149
  error!({ message: "ID:#{id} is not found" }, 404)
145
150
  end
146
151
 
147
- rule = Structs::Rule::Rule.from_yaml(yaml)
148
- rule.id = id
152
+ begin
153
+ rule = Structs::Rule.from_yaml(yaml, id: id)
154
+ rescue YAMLSyntaxError => e
155
+ error!({ message: e.message }, 400)
156
+ end
149
157
 
150
158
  begin
151
159
  rule.validate!
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="icon" href="/static/favicon.ico"/><title>Mihari</title><script defer="defer" type="module" src="/static/js/chunk-vendors.dde2116c.js"></script><script defer="defer" type="module" src="/static/js/app.823b5af7.js"></script><link href="/static/css/chunk-vendors.06251949.css" rel="stylesheet"><link href="/static/css/app.2a5d3d21.css" rel="stylesheet"><script defer="defer" src="/static/js/chunk-vendors-legacy.b110c129.js" nomodule></script><script defer="defer" src="/static/js/app-legacy.9d5c9c3d.js" nomodule></script></head><body><noscript><strong>We're sorry but Mihari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1"/><link rel="icon" href="/static/favicon.ico"/><title>Mihari</title><script defer="defer" src="/static/js/chunk-vendors.64580a1f.js"></script><script defer="defer" src="/static/js/app.524d9ed2.js"></script><link href="/static/css/chunk-vendors.5013d549.css" rel="stylesheet"><link href="/static/css/app.2a5d3d21.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but Mihari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>