Path: blob/master/spec/support/lib/module_validation.rb
52105 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_format38validate :validate_url_reference_format3940attr_reader :mod4142def initialize(mod)43super44@mod = mod45end4647#48# Acceptable Stability ratings49#50VALID_STABILITY_VALUES = [51Msf::CRASH_SAFE,52Msf::CRASH_SERVICE_RESTARTS,53Msf::CRASH_SERVICE_DOWN,54Msf::CRASH_OS_RESTARTS,55Msf::CRASH_OS_DOWN,56Msf::SERVICE_RESOURCE_LOSS,57Msf::OS_RESOURCE_LOSS58]5960#61# Acceptable Side-effect ratings62#63VALID_SIDE_EFFECT_VALUES = [64Msf::ARTIFACTS_ON_DISK,65Msf::CONFIG_CHANGES,66Msf::IOC_IN_LOGS,67Msf::ACCOUNT_LOCKOUTS,68Msf::ACCOUNT_LOGOUT,69Msf::SCREEN_EFFECTS,70Msf::AUDIO_EFFECTS,71Msf::PHYSICAL_EFFECTS72]7374#75# Acceptable Reliability ratings76#77VALID_RELIABILITY_VALUES = [78Msf::FIRST_ATTEMPT_FAIL,79Msf::REPEATABLE_SESSION,80Msf::UNRELIABLE_SESSION,81Msf::EVENT_DEPENDENT82]8384#85# Acceptable site references86#87VALID_REFERENCE_CTX_ID_VALUES = %w[88ATT&CK89CVE90CWE91BID92MSB93EDB94GHSA95OSV96US-CERT-VU97ZDI98URL99WPVDB100PACKETSTORM101LOGO102SOUNDTRACK103OSVDB104VTS105OVE106]107108def validate_notes_values_are_arrays109notes.each do |k, v|110unless v.is_a?(Array)111errors.add :notes, "note value #{k.inspect} must be an array, got #{v.inspect}"112end113end114end115116def validate_crash_safe_not_present_in_stability_notes117if rank == Msf::ExcellentRanking && !stability.include?(Msf::CRASH_SAFE)118return if stability == Msf::UNKNOWN_STABILITY119120errors.add :stability, "must have CRASH_SAFE value if module has an ExcellentRanking, instead found #{stability.inspect}"121end122end123124def validate_filename_is_snake_case125unless file_path.split('/').last.match?(/^[a-z0-9]+(?:_[a-z0-9]+)*\.rb$/)126errors.add :file_path, "must be snake case, instead found #{file_path.inspect}"127end128end129130def validate_reference_ctx_id131references_ctx_id_list = references.select { |ref| ref.respond_to?(:ctx_id) }.map(&:ctx_id)132invalid_references = references_ctx_id_list - VALID_REFERENCE_CTX_ID_VALUES133134invalid_references.each do |ref|135if ref.casecmp?('NOCVE')136errors.add :references, "#{ref} please include NOCVE values in the 'notes' section, rather than in 'references'"137elsif ref.casecmp?('AKA')138errors.add :references, "#{ref} please include AKA values in the 'notes' section, rather than in 'references'"139else140errors.add :references, "#{ref} is not valid, must be in #{VALID_REFERENCE_CTX_ID_VALUES}"141end142end143end144145def validate_author_bad_chars146author.each do |i|147if i.name =~ /^@.+$/148errors.add :author, "must not include username handles, found #{i.name.inspect}. Try leaving it in a comment instead"149end150end151end152153def validate_target_platforms154if platform.blank? && type == 'exploit'155targets.each do |target|156if target.platform.blank?157errors.add :platform, 'must be included either within targets or platform module metadata'158end159end160end161end162163def validate_default_target164return unless respond_to?(:default_target)165return if default_target == 0166167number_of_targets = respond_to?(:targets) ? targets.size : 0168169return if default_target < number_of_targets170171errors.add :default_target, "is out of range. Must specify a valid target index between 0 and #{number_of_targets - 1}, got '#{default_target}'"172end173174def validate_attack_reference_format175references.each do |ref|176next unless ref.respond_to?(:ctx_id) && ref.respond_to?(:ctx_val)177next unless ref.ctx_id == 'ATT&CK'178179val = ref.ctx_val180prefix = val[/\A[A-Z]+/]181valid_format = Msf::Mitre::Attack::Categories::PATHS.key?(prefix) && val.match?(/\A#{prefix}[\d.]+\z/)182whitespace = val.match?(/\s/)183184unless valid_format && !whitespace185errors.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."186end187end188end189190def validate_url_reference_format191references.each do |ref|192next unless ref.respond_to?(:ctx_id) && ref.respond_to?(:ctx_val)193next unless ref.ctx_id == 'URL'194195val = ref.ctx_val196begin197uri = URI.parse(val)198unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)199errors.add :references, "URL reference '#{val}' is not a valid HTTP(s) URI with valid percent encoding"200end201rescue URI::InvalidURIError => e202errors.add :references, "URL reference '#{val}' is not a valid HTTP(s) URI with valid percent encoding"203end204end205end206207def has_notes?208!notes.empty?209end210211def validate_description_does_not_contain_non_printable_chars212unless description&.match?(/\A[ -~\t\n]*\z/)213# Blank descriptions are validated elsewhere, so we will return early to not also add this error214# and cause unnecessary confusion.215return if description.nil?216217errors.add :description, 'must only contain human-readable printable ascii characters, including newlines and tabs'218end219end220221def validate_name_does_not_contain_non_printable_chars222unless name&.match?(/\A[ -~]+\z/)223errors.add :name, 'must only contain human-readable printable ascii characters'224end225end226227validates :mod, presence: true228229with_options if: :has_notes? do |mod|230mod.validate :validate_crash_safe_not_present_in_stability_notes231mod.validate :validate_notes_values_are_arrays232233mod.validates :stability,234'module_validation/array_inclusion': { in: VALID_STABILITY_VALUES, sentinel_value: Msf::UNKNOWN_STABILITY }235236mod.validates :side_effects,237'module_validation/array_inclusion': { in: VALID_SIDE_EFFECT_VALUES, sentinel_value: Msf::UNKNOWN_SIDE_EFFECTS }238239mod.validates :reliability,240'module_validation/array_inclusion': { in: VALID_RELIABILITY_VALUES, sentinel_value: Msf::UNKNOWN_RELIABILITY }241end242243validates :arch,244'module_validation/array_inclusion': { in: Rex::Arch::ARCH_TYPES }245246validates :license,247presence: true,248inclusion: { in: LICENSES, message: 'must include a valid license' }249250validates :rank,251presence: true,252inclusion: { in: Msf::RankingName.keys, message: 'must include a valid module ranking' }253254validates :author,255presence: true256257validates :name,258presence: true,259format: { with: /\A[^&<>]+\z/, message: 'must not contain the characters &<>' }260261validates :description,262presence: true263end264end265266267