2023-04-12 13:32:09 +01:00
require 'active_model'
module ModuleValidation
# Checks if values within arrays included within the passed list of acceptable values
class ArrayInclusionValidator < ActiveModel :: EachValidator
def validate_each ( record , attribute , value )
unless value . is_a? ( Array )
record . errors . add ( attribute , " #{ attribute } must be an array " )
return
end
2025-06-25 15:46:39 +01:00
# Special cases for modules/exploits/bsd/finger/morris_fingerd_bof.rb which has a one-off architecture defined in
# the module itself, and that value is not included in the valid list of architectures.
# https://github.com/rapid7/metasploit-framework/blob/389d84cbf0d7c58727846466d9a9f6a468f32c61/modules/exploits/bsd/finger/morris_fingerd_bof.rb#L11
return if attribute == :arch && value == [ " vax " ] && record . fullname == " exploit/bsd/finger/morris_fingerd_bof "
2025-06-23 12:43:46 +01:00
return if value == options [ :sentinel_value ]
2023-04-12 13:32:09 +01:00
invalid_options = value - options [ :in ]
message = " contains invalid values #{ invalid_options . inspect } - only #{ options [ :in ] . inspect } is allowed "
if invalid_options . any?
record . errors . add ( attribute , :array_inclusion , message : message , value : value )
end
end
end
# Validates module metadata
class Validator < SimpleDelegator
include ActiveModel :: Validations
validate :validate_filename_is_snake_case
validate :validate_reference_ctx_id
validate :validate_author_bad_chars
validate :validate_target_platforms
2025-06-25 10:46:26 +01:00
validate :validate_description_does_not_contain_non_printable_chars
validate :validate_name_does_not_contain_non_printable_chars
2025-06-06 12:39:33 +01:00
validate :validate_attack_reference_format
2023-04-12 13:32:09 +01:00
attr_reader :mod
def initialize ( mod )
super
@mod = mod
end
#
# Acceptable Stability ratings
#
VALID_STABILITY_VALUES = [
Msf :: CRASH_SAFE ,
Msf :: CRASH_SERVICE_RESTARTS ,
Msf :: CRASH_SERVICE_DOWN ,
Msf :: CRASH_OS_RESTARTS ,
Msf :: CRASH_OS_DOWN ,
Msf :: SERVICE_RESOURCE_LOSS ,
Msf :: OS_RESOURCE_LOSS
]
#
# Acceptable Side-effect ratings
#
VALID_SIDE_EFFECT_VALUES = [
Msf :: ARTIFACTS_ON_DISK ,
Msf :: CONFIG_CHANGES ,
Msf :: IOC_IN_LOGS ,
Msf :: ACCOUNT_LOCKOUTS ,
2024-10-31 16:30:32 +01:00
Msf :: ACCOUNT_LOGOUT ,
2023-04-12 13:32:09 +01:00
Msf :: SCREEN_EFFECTS ,
Msf :: AUDIO_EFFECTS ,
Msf :: PHYSICAL_EFFECTS
]
#
# Acceptable Reliability ratings
#
VALID_RELIABILITY_VALUES = [
Msf :: FIRST_ATTEMPT_FAIL ,
Msf :: REPEATABLE_SESSION ,
2024-04-22 15:38:56 -04:00
Msf :: UNRELIABLE_SESSION ,
Msf :: EVENT_DEPENDENT
2023-04-12 13:32:09 +01:00
]
#
# Acceptable site references
#
VALID_REFERENCE_CTX_ID_VALUES = %w[
2025-06-06 12:39:33 +01:00
ATT&CK
2023-04-12 13:32:09 +01:00
CVE
CWE
BID
MSB
EDB
US-CERT-VU
ZDI
URL
WPVDB
PACKETSTORM
LOGO
SOUNDTRACK
OSVDB
VTS
OVE
]
def validate_notes_values_are_arrays
notes . each do | k , v |
unless v . is_a? ( Array )
errors . add :notes , " note value #{ k . inspect } must be an array, got #{ v . inspect } "
end
end
end
def validate_crash_safe_not_present_in_stability_notes
if rank == Msf :: ExcellentRanking && ! stability . include? ( Msf :: CRASH_SAFE )
2025-06-24 12:10:42 +01:00
return if stability == Msf :: UNKNOWN_STABILITY
2023-04-12 13:32:09 +01:00
errors . add :stability , " must have CRASH_SAFE value if module has an ExcellentRanking, instead found #{ stability . inspect } "
end
end
def validate_filename_is_snake_case
unless file_path . split ( '/' ) . last . match? ( / ^[a-z0-9]+(?:_[a-z0-9]+)* \ .rb$ / )
errors . add :file_path , " must be snake case, instead found #{ file_path . inspect } "
end
end
def validate_reference_ctx_id
references_ctx_id_list = references . map ( & :ctx_id )
invalid_references = references_ctx_id_list - VALID_REFERENCE_CTX_ID_VALUES
invalid_references . each do | ref |
if ref . casecmp? ( 'NOCVE' )
errors . add :references , " #{ ref } please include NOCVE values in the 'notes' section, rather than in 'references' "
elsif ref . casecmp? ( 'AKA' )
errors . add :references , " #{ ref } please include AKA values in the 'notes' section, rather than in 'references' "
else
errors . add :references , " #{ ref } is not valid, must be in #{ VALID_REFERENCE_CTX_ID_VALUES } "
end
end
end
def validate_author_bad_chars
author . each do | i |
if i . name =~ / ^@.+$ /
errors . add :author , " must not include username handles, found #{ i . name . inspect } . Try leaving it in a comment instead "
end
end
end
def validate_target_platforms
if platform . blank? && type == 'exploit'
targets . each do | target |
if target . platform . blank?
errors . add :platform , 'must be included either within targets or platform module metadata'
end
end
end
end
2025-06-06 12:39:33 +01:00
def validate_attack_reference_format
references . each do | ref |
next unless ref . respond_to? ( :ctx_id ) && ref . respond_to? ( :ctx_val )
next unless ref . ctx_id == 'ATT&CK'
val = ref . ctx_val
prefix = val [ / \ A[A-Z]+ / ]
valid_format = Msf :: Mitre :: Attack :: Categories :: PATHS . key? ( prefix ) && val . match? ( / \ A #{ prefix } [ \ d.]+ \ z / )
whitespace = val . match? ( / \ s / )
unless valid_format && ! whitespace
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. "
end
end
end
2023-04-12 13:32:09 +01:00
def has_notes?
! notes . empty?
end
2025-06-25 10:46:26 +01:00
def validate_description_does_not_contain_non_printable_chars
unless description & . match? ( / \ A[ -~ \ t \ n]* \ z / )
# Blank descriptions are validated elsewhere, so we will return early to not also add this error
# and cause unnecessary confusion.
return if description . nil?
errors . add :description , 'must only contain human-readable printable ascii characters, including newlines and tabs'
end
end
def validate_name_does_not_contain_non_printable_chars
unless name & . match? ( / \ A[ -~]+ \ z / )
errors . add :name , 'must only contain human-readable printable ascii characters'
end
end
2023-04-12 13:32:09 +01:00
validates :mod , presence : true
with_options if : :has_notes? do | mod |
mod . validate :validate_crash_safe_not_present_in_stability_notes
mod . validate :validate_notes_values_are_arrays
mod . validates :stability ,
2025-06-23 12:43:46 +01:00
'module_validation/array_inclusion' : { in : VALID_STABILITY_VALUES , sentinel_value : Msf :: UNKNOWN_STABILITY }
2023-04-12 13:32:09 +01:00
mod . validates :side_effects ,
2025-06-23 12:43:46 +01:00
'module_validation/array_inclusion' : { in : VALID_SIDE_EFFECT_VALUES , sentinel_value : Msf :: UNKNOWN_SIDE_EFFECTS }
2023-04-12 13:32:09 +01:00
mod . validates :reliability ,
2025-06-23 12:43:46 +01:00
'module_validation/array_inclusion' : { in : VALID_RELIABILITY_VALUES , sentinel_value : Msf :: UNKNOWN_RELIABILITY }
2023-04-12 13:32:09 +01:00
end
2025-06-25 15:46:39 +01:00
validates :arch ,
'module_validation/array_inclusion' : { in : Rex :: Arch :: ARCH_TYPES }
2023-04-12 13:32:09 +01:00
validates :license ,
presence : true ,
inclusion : { in : LICENSES , message : 'must include a valid license' }
validates :rank ,
presence : true ,
inclusion : { in : Msf :: RankingName . keys , message : 'must include a valid module ranking' }
validates :author ,
presence : true
validates :name ,
presence : true ,
format : { with : / \ A[^&<>]+ \ z / , message : 'must not contain the characters &<>' }
validates :description ,
presence : true
end
end