Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/spec/support/lib/module_validation.rb
52105 views
1
require 'active_model'
2
3
module ModuleValidation
4
# Checks if values within arrays included within the passed list of acceptable values
5
class ArrayInclusionValidator < ActiveModel::EachValidator
6
def validate_each(record, attribute, value)
7
unless value.is_a?(Array)
8
record.errors.add(attribute, "#{attribute} must be an array")
9
return
10
end
11
12
# Special cases for modules/exploits/bsd/finger/morris_fingerd_bof.rb which has a one-off architecture defined in
13
# the module itself, and that value is not included in the valid list of architectures.
14
# https://github.com/rapid7/metasploit-framework/blob/389d84cbf0d7c58727846466d9a9f6a468f32c61/modules/exploits/bsd/finger/morris_fingerd_bof.rb#L11
15
return if attribute == :arch && value == ["vax"] && record.fullname == "exploit/bsd/finger/morris_fingerd_bof"
16
return if value == options[:sentinel_value]
17
18
invalid_options = value - options[:in]
19
message = "contains invalid values #{invalid_options.inspect} - only #{options[:in].inspect} is allowed"
20
21
if invalid_options.any?
22
record.errors.add(attribute, :array_inclusion, message: message, value: value)
23
end
24
end
25
end
26
27
# Validates module metadata
28
class Validator < SimpleDelegator
29
include ActiveModel::Validations
30
31
validate :validate_filename_is_snake_case
32
validate :validate_reference_ctx_id
33
validate :validate_author_bad_chars
34
validate :validate_target_platforms
35
validate :validate_default_target
36
validate :validate_description_does_not_contain_non_printable_chars
37
validate :validate_name_does_not_contain_non_printable_chars
38
validate :validate_attack_reference_format
39
validate :validate_url_reference_format
40
41
attr_reader :mod
42
43
def initialize(mod)
44
super
45
@mod = mod
46
end
47
48
#
49
# Acceptable Stability ratings
50
#
51
VALID_STABILITY_VALUES = [
52
Msf::CRASH_SAFE,
53
Msf::CRASH_SERVICE_RESTARTS,
54
Msf::CRASH_SERVICE_DOWN,
55
Msf::CRASH_OS_RESTARTS,
56
Msf::CRASH_OS_DOWN,
57
Msf::SERVICE_RESOURCE_LOSS,
58
Msf::OS_RESOURCE_LOSS
59
]
60
61
#
62
# Acceptable Side-effect ratings
63
#
64
VALID_SIDE_EFFECT_VALUES = [
65
Msf::ARTIFACTS_ON_DISK,
66
Msf::CONFIG_CHANGES,
67
Msf::IOC_IN_LOGS,
68
Msf::ACCOUNT_LOCKOUTS,
69
Msf::ACCOUNT_LOGOUT,
70
Msf::SCREEN_EFFECTS,
71
Msf::AUDIO_EFFECTS,
72
Msf::PHYSICAL_EFFECTS
73
]
74
75
#
76
# Acceptable Reliability ratings
77
#
78
VALID_RELIABILITY_VALUES = [
79
Msf::FIRST_ATTEMPT_FAIL,
80
Msf::REPEATABLE_SESSION,
81
Msf::UNRELIABLE_SESSION,
82
Msf::EVENT_DEPENDENT
83
]
84
85
#
86
# Acceptable site references
87
#
88
VALID_REFERENCE_CTX_ID_VALUES = %w[
89
ATT&CK
90
CVE
91
CWE
92
BID
93
MSB
94
EDB
95
GHSA
96
OSV
97
US-CERT-VU
98
ZDI
99
URL
100
WPVDB
101
PACKETSTORM
102
LOGO
103
SOUNDTRACK
104
OSVDB
105
VTS
106
OVE
107
]
108
109
def validate_notes_values_are_arrays
110
notes.each do |k, v|
111
unless v.is_a?(Array)
112
errors.add :notes, "note value #{k.inspect} must be an array, got #{v.inspect}"
113
end
114
end
115
end
116
117
def validate_crash_safe_not_present_in_stability_notes
118
if rank == Msf::ExcellentRanking && !stability.include?(Msf::CRASH_SAFE)
119
return if stability == Msf::UNKNOWN_STABILITY
120
121
errors.add :stability, "must have CRASH_SAFE value if module has an ExcellentRanking, instead found #{stability.inspect}"
122
end
123
end
124
125
def validate_filename_is_snake_case
126
unless file_path.split('/').last.match?(/^[a-z0-9]+(?:_[a-z0-9]+)*\.rb$/)
127
errors.add :file_path, "must be snake case, instead found #{file_path.inspect}"
128
end
129
end
130
131
def validate_reference_ctx_id
132
references_ctx_id_list = references.select { |ref| ref.respond_to?(:ctx_id) }.map(&:ctx_id)
133
invalid_references = references_ctx_id_list - VALID_REFERENCE_CTX_ID_VALUES
134
135
invalid_references.each do |ref|
136
if ref.casecmp?('NOCVE')
137
errors.add :references, "#{ref} please include NOCVE values in the 'notes' section, rather than in 'references'"
138
elsif ref.casecmp?('AKA')
139
errors.add :references, "#{ref} please include AKA values in the 'notes' section, rather than in 'references'"
140
else
141
errors.add :references, "#{ref} is not valid, must be in #{VALID_REFERENCE_CTX_ID_VALUES}"
142
end
143
end
144
end
145
146
def validate_author_bad_chars
147
author.each do |i|
148
if i.name =~ /^@.+$/
149
errors.add :author, "must not include username handles, found #{i.name.inspect}. Try leaving it in a comment instead"
150
end
151
end
152
end
153
154
def validate_target_platforms
155
if platform.blank? && type == 'exploit'
156
targets.each do |target|
157
if target.platform.blank?
158
errors.add :platform, 'must be included either within targets or platform module metadata'
159
end
160
end
161
end
162
end
163
164
def validate_default_target
165
return unless respond_to?(:default_target)
166
return if default_target == 0
167
168
number_of_targets = respond_to?(:targets) ? targets.size : 0
169
170
return if default_target < number_of_targets
171
172
errors.add :default_target, "is out of range. Must specify a valid target index between 0 and #{number_of_targets - 1}, got '#{default_target}'"
173
end
174
175
def validate_attack_reference_format
176
references.each do |ref|
177
next unless ref.respond_to?(:ctx_id) && ref.respond_to?(:ctx_val)
178
next unless ref.ctx_id == 'ATT&CK'
179
180
val = ref.ctx_val
181
prefix = val[/\A[A-Z]+/]
182
valid_format = Msf::Mitre::Attack::Categories::PATHS.key?(prefix) && val.match?(/\A#{prefix}[\d.]+\z/)
183
whitespace = val.match?(/\s/)
184
185
unless valid_format && !whitespace
186
errors.add :references, "ATT&CK reference '#{val}' is invalid. Must start with one of #{Msf::Mitre::Attack::Categories::PATHS.keys.inspect} and be followed by digits/periods, no whitespace."
187
end
188
end
189
end
190
191
def validate_url_reference_format
192
references.each do |ref|
193
next unless ref.respond_to?(:ctx_id) && ref.respond_to?(:ctx_val)
194
next unless ref.ctx_id == 'URL'
195
196
val = ref.ctx_val
197
begin
198
uri = URI.parse(val)
199
unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
200
errors.add :references, "URL reference '#{val}' is not a valid HTTP(s) URI with valid percent encoding"
201
end
202
rescue URI::InvalidURIError => e
203
errors.add :references, "URL reference '#{val}' is not a valid HTTP(s) URI with valid percent encoding"
204
end
205
end
206
end
207
208
def has_notes?
209
!notes.empty?
210
end
211
212
def validate_description_does_not_contain_non_printable_chars
213
unless description&.match?(/\A[ -~\t\n]*\z/)
214
# Blank descriptions are validated elsewhere, so we will return early to not also add this error
215
# and cause unnecessary confusion.
216
return if description.nil?
217
218
errors.add :description, 'must only contain human-readable printable ascii characters, including newlines and tabs'
219
end
220
end
221
222
def validate_name_does_not_contain_non_printable_chars
223
unless name&.match?(/\A[ -~]+\z/)
224
errors.add :name, 'must only contain human-readable printable ascii characters'
225
end
226
end
227
228
validates :mod, presence: true
229
230
with_options if: :has_notes? do |mod|
231
mod.validate :validate_crash_safe_not_present_in_stability_notes
232
mod.validate :validate_notes_values_are_arrays
233
234
mod.validates :stability,
235
'module_validation/array_inclusion': { in: VALID_STABILITY_VALUES, sentinel_value: Msf::UNKNOWN_STABILITY }
236
237
mod.validates :side_effects,
238
'module_validation/array_inclusion': { in: VALID_SIDE_EFFECT_VALUES, sentinel_value: Msf::UNKNOWN_SIDE_EFFECTS }
239
240
mod.validates :reliability,
241
'module_validation/array_inclusion': { in: VALID_RELIABILITY_VALUES, sentinel_value: Msf::UNKNOWN_RELIABILITY }
242
end
243
244
validates :arch,
245
'module_validation/array_inclusion': { in: Rex::Arch::ARCH_TYPES }
246
247
validates :license,
248
presence: true,
249
inclusion: { in: LICENSES, message: 'must include a valid license' }
250
251
validates :rank,
252
presence: true,
253
inclusion: { in: Msf::RankingName.keys, message: 'must include a valid module ranking' }
254
255
validates :author,
256
presence: true
257
258
validates :name,
259
presence: true,
260
format: { with: /\A[^&<>]+\z/, message: 'must not contain the characters &<>' }
261
262
validates :description,
263
presence: true
264
end
265
end
266
267