diff --git a/.rubocop.yml b/.rubocop.yml index 3c916bd08e..bb9824ecd1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -23,6 +23,7 @@ require: - ./lib/rubocop/cop/lint/deprecated_gem_version.rb - ./lib/rubocop/cop/lint/module_enforce_notes.rb - ./lib/rubocop/cop/lint/detect_invalid_pack_directives.rb + - ./lib/rubocop/cop/lint/detect_metadata_trailing_leading_whitespace.rb Layout/SpaceBeforeBrackets: Enabled: true @@ -672,3 +673,6 @@ Style/UnpackFirst: Disabling to make it easier to copy/paste `unpack('h*')` expressions from code into a debugging REPL. Enabled: false + +Lint/DetectMetadataTrailingLeadingWhitespace: + Enabled: true diff --git a/lib/rubocop/cop/lint/detect_metadata_trailing_leading_whitespace.rb b/lib/rubocop/cop/lint/detect_metadata_trailing_leading_whitespace.rb new file mode 100644 index 0000000000..49dbcd5abe --- /dev/null +++ b/lib/rubocop/cop/lint/detect_metadata_trailing_leading_whitespace.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: trueAdd commentMore actions + +module RuboCop + module Cop + module Lint + # Checks for leading or trailing whitespace in Metasploit module metadata keys/values + # inside the initialize method. Recursively checks all hash and array values, except for + # keys listed in EXEMPT_KEYS. + # + # EXEMPT_KEYS can be extended to skip additional metadata fields as needed. + # + # @example + # # bad + # 'Name' => ' value ' + # 'Author' => [' hd'] + # + # # good + # 'Name' => 'value' + # 'Author' => ['hd'] + class DetectMetadataTrailingLeadingWhitespace < Base + extend AutoCorrector + MSG = 'Metadata key or value has leading or trailing whitespace.' + EXEMPT_KEYS = %w[Description Payload].freeze + + # Called for every method definition node + # Only processes the initialize method + # @param node [RuboCop::AST::DefNode] + def on_def(node) + return unless node.method_name == :initialize + + node.each_descendant(:hash) do |hash_node| + hash_node.pairs.each do |pair| + key = extract_string(pair.key) + next if key && EXEMPT_KEYS.any? { |exempt| key.casecmp?(exempt) } + check_value(pair.value) + if key && (key != key.strip) + add_offense(pair.key, message: MSG) do |corrector| + corrector.replace(pair.key.loc.expression, key.strip.inspect) + end + end + end + end + end + + private + + # Recursively checks a value node for whitespace issues + # @param node [RuboCop::AST::Node] + def check_value(node) + case node.type + when :str, :dstr + value = extract_string(node) + if value && value != value.strip + add_offense(node, message: MSG) do |corrector| + replacement = node.sym_type? ? ":#{value.strip}" : value.strip.inspect + corrector.replace(node.loc.expression, replacement) + end + end + when :array + node.children.each { |child| check_value(child) } + when :hash + node.pairs.each do |pair| + key = extract_string(pair.key) + next if key && EXEMPT_KEYS.any? { |exempt| key.casecmp?(exempt) } + if key && key != key.strip + add_offense(pair.key, message: MSG) do |corrector| + corrector.replace(pair.key.loc.expression, key.strip.inspect) + end + end + check_value(pair.value) + end + end + end + + # Extracts the string value from a node (handles str, sym, dstr) + # @param node [RuboCop::AST::Node] + # @return [String, nil] + def extract_string(node) + return unless node + if node.str_type? || node.sym_type? + node.value.to_s + elsif node.dstr_type? + # For dynamic strings, join all child string values + node.children.map { |c| c.is_a?(Parser::AST::Node) ? extract_string(c) : c.to_s }.join + end + end + end + end + end +end diff --git a/spec/rubocop/cop/lint/detect_metadata_trailing_leading_whitespace_spec.rb b/spec/rubocop/cop/lint/detect_metadata_trailing_leading_whitespace_spec.rb new file mode 100644 index 0000000000..4518f1a445 --- /dev/null +++ b/spec/rubocop/cop/lint/detect_metadata_trailing_leading_whitespace_spec.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: trueAdd commentMore actions + +require 'rubocop/cop/lint/detect_metadata_trailing_leading_whitespace' +require 'rubocop/rspec/support' + +RSpec.describe RuboCop::Cop::Lint::DetectMetadataTrailingLeadingWhitespace, :config do + subject(:cop) { described_class.new(config) } + + let(:config) { RuboCop::Config.new } + + it 'registers an offense for leading/trailing whitespace in Name' do + expect_offense(<<~RUBY) + def initialize(info = {}) + super(update_info(info, + 'Name' => ' value ', + ^^^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + )) + end + RUBY + end + + it 'registers an offense for leading/trailing whitespace in Author (array)' do + expect_offense(<<~RUBY) + def initialize(info = {}) + super(update_info(info, + 'Author' => [ + ' author ', + ^^^^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + ], + )) + end + RUBY + end + + it 'registers an offense for leading/trailing whitespace in License' do + expect_offense(<<~RUBY) + def initialize(info = {}) + super(update_info(info, + 'License' => ' MSF_LICENSE ', + ^^^^^^^^^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + )) + end + RUBY + end + + it 'registers an offense for leading/trailing whitespace in Privileged' do + expect_offense(<<~RUBY) + def initialize(info = {}) + super(update_info(info, + 'Privileged' => ' true ', + ^^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + )) + end + RUBY + end + + it 'registers an offense for leading/trailing whitespace in DefaultOptions (hash)' do + expect_offense(<<~RUBY) + def initialize(info = {}) + super(update_info(info, + 'DefaultOptions' => { + 'WfsDelay' => ' 10 ', + ^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + }, + )) + end + RUBY + end + + it 'registers an offense for leading/trailing whitespace in References (array of arrays)' do + expect_offense(<<~RUBY) + def initialize(info = {}) + super(update_info(info, + 'References' => [ + [ ' CVE ', ' 1999-0504 ' ], + ^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + ^^^^^^^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + ], + )) + end + RUBY + end + + it 'registers an offense for leading/trailing whitespace in Platform' do + expect_offense(<<~RUBY) + def initialize(info = {}) + super(update_info(info, + 'Platform' => ' win ', + ^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + )) + end + RUBY + end + + it 'registers an offense for leading/trailing whitespace in Targets (array of arrays)' do + expect_offense(<<~RUBY) + def initialize(info = {}) + super(update_info(info, + 'Targets' => [ + [ ' Automatic ', { 'Arch' => [ ' ARCH_X86 ', ' ARCH_X64 ' ] } ], + ^^^^^^^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + ^^^^^^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + ^^^^^^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + ], + )) + end + RUBY + end + + it 'registers an offense for leading/trailing whitespace in DefaultTarget' do + expect_offense(<<~RUBY) + def initialize(info = {}) + super(update_info(info, + 'DefaultTarget' => ' 0 ', + ^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + )) + end + RUBY + end + + it 'registers an offense for leading/trailing whitespace in DisclosureDate' do + expect_offense(<<~RUBY) + def initialize(info = {}) + super(update_info(info, + 'DisclosureDate' => ' 1999-01-01 ', + ^^^^^^^^^^^^^^ Lint/DetectMetadataTrailingLeadingWhitespace: Metadata key or value has leading or trailing whitespace. + )) + end + RUBY + end +end