Path: blob/master/spec/support/lib/module_validation.rb
24699 views
require 'active_model'12module ModuleValidation3# Checks if values within arrays included within the passed list of acceptable values4class ArrayInclusionValidator < ActiveModel::EachValidator5def validate_each(record, attribute, value)6unless value.is_a?(Array)7record.errors.add(attribute, "#{attribute} must be an array")8return9end1011# Special cases for modules/exploits/bsd/finger/morris_fingerd_bof.rb which has a one-off architecture defined in12# the module itself, and that value is not included in the valid list of architectures.13# https://github.com/rapid7/metasploit-framework/blob/389d84cbf0d7c58727846466d9a9f6a468f32c61/modules/exploits/bsd/finger/morris_fingerd_bof.rb#L1114return if attribute == :arch && value == ["vax"] && record.fullname == "exploit/bsd/finger/morris_fingerd_bof"15return if value == options[:sentinel_value]1617invalid_options = value - options[:in]18message = "contains invalid values #{invalid_options.inspect} - only #{options[:in].inspect} is allowed"1920if invalid_options.any?21record.errors.add(attribute, :array_inclusion, message: message, value: value)22end23end24end2526# Validates module metadata27class Validator < SimpleDelegator28include ActiveModel::Validations2930validate :validate_filename_is_snake_case31validate :validate_reference_ctx_id32validate :validate_author_bad_chars33validate :validate_target_platforms34validate :validate_default_target35validate :validate_description_does_not_contain_non_printable_chars36validate :validate_name_does_not_contain_non_printable_chars37validate :validate_attack_reference_format3839attr_reader :mod4041def initialize(mod)42super43@mod = mod44end4546#47# Acceptable Stability ratings48#49VALID_STABILITY_VALUES = [50Msf::CRASH_SAFE,51Msf::CRASH_SERVICE_RESTARTS,52Msf::CRASH_SERVICE_DOWN,53Msf::CRASH_OS_RESTARTS,54Msf::CRASH_OS_DOWN,55Msf::SERVICE_RESOURCE_LOSS,56Msf::OS_RESOURCE_LOSS57]5859#60# Acceptable Side-effect ratings61#62VALID_SIDE_EFFECT_VALUES = [63Msf::ARTIFACTS_ON_DISK,64Msf::CONFIG_CHANGES,65Msf::IOC_IN_LOGS,66Msf::ACCOUNT_LOCKOUTS,67Msf::ACCOUNT_LOGOUT,68Msf::SCREEN_EFFECTS,69Msf::AUDIO_EFFECTS,70Msf::PHYSICAL_EFFECTS71]7273#74# Acceptable Reliability ratings75#76VALID_RELIABILITY_VALUES = [77Msf::FIRST_ATTEMPT_FAIL,78Msf::REPEATABLE_SESSION,79Msf::UNRELIABLE_SESSION,80Msf::EVENT_DEPENDENT81]8283#84# Acceptable site references85#86VALID_REFERENCE_CTX_ID_VALUES = %w[87ATT&CK88CVE89CWE90BID91MSB92EDB93US-CERT-VU94ZDI95URL96WPVDB97PACKETSTORM98LOGO99SOUNDTRACK100OSVDB101VTS102OVE103]104105def validate_notes_values_are_arrays106notes.each do |k, v|107unless v.is_a?(Array)108errors.add :notes, "note value #{k.inspect} must be an array, got #{v.inspect}"109end110end111end112113def validate_crash_safe_not_present_in_stability_notes114if rank == Msf::ExcellentRanking && !stability.include?(Msf::CRASH_SAFE)115return if stability == Msf::UNKNOWN_STABILITY116117errors.add :stability, "must have CRASH_SAFE value if module has an ExcellentRanking, instead found #{stability.inspect}"118end119end120121def validate_filename_is_snake_case122unless file_path.split('/').last.match?(/^[a-z0-9]+(?:_[a-z0-9]+)*\.rb$/)123errors.add :file_path, "must be snake case, instead found #{file_path.inspect}"124end125end126127def validate_reference_ctx_id128references_ctx_id_list = references.map(&:ctx_id)129invalid_references = references_ctx_id_list - VALID_REFERENCE_CTX_ID_VALUES130131invalid_references.each do |ref|132if ref.casecmp?('NOCVE')133errors.add :references, "#{ref} please include NOCVE values in the 'notes' section, rather than in 'references'"134elsif ref.casecmp?('AKA')135errors.add :references, "#{ref} please include AKA values in the 'notes' section, rather than in 'references'"136else137errors.add :references, "#{ref} is not valid, must be in #{VALID_REFERENCE_CTX_ID_VALUES}"138end139end140end141142def validate_author_bad_chars143author.each do |i|144if i.name =~ /^@.+$/145errors.add :author, "must not include username handles, found #{i.name.inspect}. Try leaving it in a comment instead"146end147end148end149150def validate_target_platforms151if platform.blank? && type == 'exploit'152targets.each do |target|153if target.platform.blank?154errors.add :platform, 'must be included either within targets or platform module metadata'155end156end157end158end159160def validate_default_target161return unless respond_to?(:default_target)162return if default_target == 0163164number_of_targets = respond_to?(:targets) ? targets.size : 0165166return if default_target < number_of_targets167168errors.add :default_target, "is out of range. Must specify a valid target index between 0 and #{number_of_targets - 1}, got '#{default_target}'"169end170171def validate_attack_reference_format172references.each do |ref|173next unless ref.respond_to?(:ctx_id) && ref.respond_to?(:ctx_val)174next unless ref.ctx_id == 'ATT&CK'175176val = ref.ctx_val177prefix = val[/\A[A-Z]+/]178valid_format = Msf::Mitre::Attack::Categories::PATHS.key?(prefix) && val.match?(/\A#{prefix}[\d.]+\z/)179whitespace = val.match?(/\s/)180181unless valid_format && !whitespace182errors.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."183end184end185end186187def has_notes?188!notes.empty?189end190191def validate_description_does_not_contain_non_printable_chars192unless description&.match?(/\A[ -~\t\n]*\z/)193# Blank descriptions are validated elsewhere, so we will return early to not also add this error194# and cause unnecessary confusion.195return if description.nil?196197errors.add :description, 'must only contain human-readable printable ascii characters, including newlines and tabs'198end199end200201def validate_name_does_not_contain_non_printable_chars202unless name&.match?(/\A[ -~]+\z/)203errors.add :name, 'must only contain human-readable printable ascii characters'204end205end206207validates :mod, presence: true208209with_options if: :has_notes? do |mod|210mod.validate :validate_crash_safe_not_present_in_stability_notes211mod.validate :validate_notes_values_are_arrays212213mod.validates :stability,214'module_validation/array_inclusion': { in: VALID_STABILITY_VALUES, sentinel_value: Msf::UNKNOWN_STABILITY }215216mod.validates :side_effects,217'module_validation/array_inclusion': { in: VALID_SIDE_EFFECT_VALUES, sentinel_value: Msf::UNKNOWN_SIDE_EFFECTS }218219mod.validates :reliability,220'module_validation/array_inclusion': { in: VALID_RELIABILITY_VALUES, sentinel_value: Msf::UNKNOWN_RELIABILITY }221end222223validates :arch,224'module_validation/array_inclusion': { in: Rex::Arch::ARCH_TYPES }225226validates :license,227presence: true,228inclusion: { in: LICENSES, message: 'must include a valid license' }229230validates :rank,231presence: true,232inclusion: { in: Msf::RankingName.keys, message: 'must include a valid module ranking' }233234validates :author,235presence: true236237validates :name,238presence: true,239format: { with: /\A[^&<>]+\z/, message: 'must not contain the characters &<>' }240241validates :description,242presence: true243end244end245246247