2010-11-05 00:05:34 +00:00
#!/usr/bin/env ruby
2013-10-16 10:40:42 -05:00
# -*- coding: binary -*-
2018-03-20 11:33:34 +00:00
2010-11-05 00:05:34 +00:00
#
# Check (recursively) for style compliance violations and other
# tree inconsistencies.
#
2014-04-02 11:12:00 -05:00
# by jduck, todb, and friends
2010-11-05 00:05:34 +00:00
#
2018-03-20 11:33:34 +00:00
2013-10-16 10:40:42 -05:00
require 'fileutils'
require 'find'
2014-01-31 14:19:04 -06:00
require 'time'
2021-01-29 17:59:14 +00:00
require 'rubocop'
require 'open3'
require 'optparse'
2010-11-05 00:05:34 +00:00
2011-12-12 15:12:01 -06:00
CHECK_OLD_RUBIES = ! ! ENV [ 'MSF_CHECK_OLD_RUBIES' ]
2014-06-12 13:46:10 -05:00
SUPPRESS_INFO_MESSAGES = ! ! ENV [ 'MSF_SUPPRESS_INFO_MESSAGES' ]
2011-12-12 15:12:01 -06:00
if CHECK_OLD_RUBIES
2013-09-30 13:47:53 -05:00
require 'rvm'
warn " This is going to take a while, depending on the number of Rubies you have installed. "
2011-12-12 15:12:01 -06:00
end
2011-10-16 15:53:19 +00:00
2012-10-12 02:55:16 -05:00
class String
2013-09-30 13:47:53 -05:00
def red
" \e [1;31;40m #{ self } \e [0m "
end
2010-11-05 00:05:34 +00:00
2013-09-30 13:47:53 -05:00
def yellow
" \e [1;33;40m #{ self } \e [0m "
end
2013-01-04 14:09:37 -06:00
2013-10-16 10:40:42 -05:00
def green
" \e [1;32;40m #{ self } \e [0m "
end
2014-04-21 18:04:14 +02:00
def cyan
" \e [1;36;40m #{ self } \e [0m "
end
2010-11-05 00:05:34 +00:00
end
2021-01-29 17:59:14 +00:00
class RuboCopRunnerException < StandardError ; end
# Wrapper around RuboCop that requires modules to be linted
# In the future this class may have the responsibility of ensuring core library files are linted
class RuboCopRunner
##
# Run Rubocop on the given file
#
# @param [String] full_filepath
# @param [Hash] options specifying autocorrect functionality
# @return [Integer] RuboCop::CLI status code
def run ( full_filepath , options = { } )
unless requires_rubocop? ( full_filepath )
2024-07-24 16:33:50 +01:00
puts " #{ full_filepath } - [*] Rubocop not required for older modules skipping. If making a large update - run rubocop #{ " rubocop -a #{ full_filepath } " . yellow } and verify all issues are resolved "
2021-01-29 17:59:14 +00:00
return RuboCop :: CLI :: STATUS_SUCCESS
end
rubocop = RuboCop :: CLI . new
args = %w[ --format simple ]
2021-02-24 20:24:57 +00:00
args << '-a' if options [ :auto_correct ]
args << '-A' if options [ :auto_correct_all ]
2021-01-29 17:59:14 +00:00
args << full_filepath
rubocop_result = rubocop . run ( args )
if rubocop_result != RuboCop :: CLI :: STATUS_SUCCESS
puts " #{ full_filepath } - [ #{ 'ERROR' . red } ] Rubocop failed. Please run #{ " rubocop -a #{ full_filepath } " . yellow } and verify all issues are resolved "
end
rubocop_result
end
private
##
# For now any modules created after 3a046f01dae340c124dd3895e670983aef5fe0c5
# will require Rubocop to be ran.
#
# This epoch was chosen from the landing date of the initial PR to
# enforce consistent module formatting with Rubocop:
#
# https://github.com/rapid7/metasploit-framework/pull/12990
#
# @param [String] full_filepath
# @return [Boolean] true if this file requires rubocop, false otherwise
def requires_rubocop? ( full_filepath )
required_modules . include? ( full_filepath )
end
def required_modules
return @required_modules if @required_modules
previously_merged_modules = new_modules_for ( '3a046f01dae340c124dd3895e670983aef5fe0c5..HEAD' )
staged_modules = new_modules_for ( '--cached' )
@required_modules = previously_merged_modules + staged_modules
if @required_modules . empty?
raise RuboCopRunnerException , 'Error retrieving new modules when verifying Rubocop'
end
@required_modules
end
def new_modules_for ( commit )
# Example output:
# M modules/exploits/osx/local/vmware_bash_function_root.rb
# A modules/exploits/osx/local/vmware_fusion_lpe.rb
raw_diff_summary , status = :: Open3 . capture2 ( " git diff -b --name-status -l0 --summary #{ commit } " )
if ! status . success? && exception
raise RuboCopRunnerException , " Command failed with status ( #{ status . exitstatus } ): #{ commit } "
end
diff_summary = raw_diff_summary . lines . map do | line |
status , file = line . split ( ' ' ) . each ( & :strip )
{ status : status , file : file }
end
diff_summary . each_with_object ( [ ] ) do | summary , acc |
next unless summary [ :status ] == 'A'
acc << summary [ :file ]
end
end
end
class MsftidyRunner
2011-12-12 15:12:01 -06:00
2014-01-31 14:19:04 -06:00
# Status codes
2017-09-15 10:27:04 +02:00
OK = 0
2018-07-17 18:11:16 -05:00
WARNING = 1
ERROR = 2
2014-01-31 14:19:04 -06:00
2014-08-26 15:30:08 -05:00
# Some compiles regexes
REGEX_MSF_EXPLOIT = / \ < Msf::Exploit /
REGEX_IS_BLANK_OR_END = / ^ \ s*end \ s*$ /
2014-01-31 14:19:04 -06:00
attr_reader :full_filepath , :source , :stat , :name , :status
2013-10-16 10:40:42 -05:00
2013-09-30 13:47:53 -05:00
def initialize ( source_file )
2013-10-16 10:40:42 -05:00
@full_filepath = source_file
2015-12-22 13:38:31 -08:00
@module_type = File . dirname ( File . expand_path ( @full_filepath ) ) [ / \/ modules \/ ([^ \/ ]+) / , 1 ]
2013-09-30 13:47:53 -05:00
@source = load_file ( source_file )
2014-08-26 15:30:08 -05:00
@lines = @source . lines # returns an enumerator
2014-01-31 14:19:04 -06:00
@status = OK
2013-10-16 10:40:42 -05:00
@name = File . basename ( source_file )
2013-09-30 13:47:53 -05:00
end
public
#
2014-01-31 14:19:04 -06:00
# Display a warning message, given some text and a number. Warnings
# are usually style issues that may be okay for people who aren't core
# Framework developers.
2013-09-30 13:47:53 -05:00
#
2014-01-31 14:19:04 -06:00
# @return status [Integer] Returns WARNINGS unless we already have an
# error.
def warn ( txt , line = 0 ) line_msg = ( line > 0 ) ? " : #{ line } " : ''
2014-03-01 12:02:41 +01:00
puts " #{ @full_filepath } #{ line_msg } - [ #{ 'WARNING' . yellow } ] #{ cleanup_text ( txt ) } "
2018-07-17 18:11:16 -05:00
@status = WARNING if @status < WARNING
2013-09-30 13:47:53 -05:00
end
2014-01-31 14:19:04 -06:00
#
# Display an error message, given some text and a number. Errors
# can break things or are so egregiously bad, style-wise, that they
# really ought to be fixed.
#
# @return status [Integer] Returns ERRORS
2013-09-30 13:47:53 -05:00
def error ( txt , line = 0 )
2014-01-23 14:21:48 -06:00
line_msg = ( line > 0 ) ? " : #{ line } " : ''
2014-03-01 12:02:41 +01:00
puts " #{ @full_filepath } #{ line_msg } - [ #{ 'ERROR' . red } ] #{ cleanup_text ( txt ) } "
2018-07-17 18:11:16 -05:00
@status = ERROR if @status < ERROR
2013-09-30 13:47:53 -05:00
end
2014-01-31 14:19:04 -06:00
# Currently unused, but some day msftidy will fix errors for you.
2013-10-16 10:40:42 -05:00
def fixed ( txt , line = 0 )
2014-01-23 14:21:48 -06:00
line_msg = ( line > 0 ) ? " : #{ line } " : ''
2014-03-01 12:02:41 +01:00
puts " #{ @full_filepath } #{ line_msg } - [ #{ 'FIXED' . green } ] #{ cleanup_text ( txt ) } "
2013-10-16 10:40:42 -05:00
end
2014-04-21 18:04:14 +02:00
#
# Display an info message. Info messages do not alter the exit status.
#
def info ( txt , line = 0 )
2014-06-12 13:46:10 -05:00
return if SUPPRESS_INFO_MESSAGES
2014-04-21 18:04:14 +02:00
line_msg = ( line > 0 ) ? " : #{ line } " : ''
puts " #{ @full_filepath } #{ line_msg } - [ #{ 'INFO' . cyan } ] #{ cleanup_text ( txt ) } "
end
2013-09-30 13:47:53 -05:00
##
#
# The functions below are actually the ones checking the source code
#
##
2013-11-08 16:11:48 -06:00
def check_shebang
2014-08-26 15:30:08 -05:00
if @lines . first =~ / ^ # ! /
2013-11-08 16:11:48 -06:00
warn ( " Module should not have a # ! line " )
end
end
2014-05-29 11:52:17 -05:00
# Updated this check to see if Nokogiri::XML.parse is being called
# specifically. The main reason for this concern is that some versions
# of libxml2 are still vulnerable to XXE attacks. REXML is safer (and
# slower) since it's pure ruby. Unfortunately, there is no pure Ruby
# HTML parser (except Hpricot which is abandonware) -- easy checks
# can avoid Nokogiri (most modules use regex anyway), but more complex
# checks tends to require Nokogiri for HTML element and value parsing.
2014-02-02 11:51:21 -06:00
def check_nokogiri
2014-05-29 11:52:17 -05:00
msg = " Using Nokogiri in modules can be risky, use REXML instead. "
2014-02-02 11:51:21 -06:00
has_nokogiri = false
2014-05-29 11:52:17 -05:00
has_nokogiri_xml_parser = false
2014-08-26 15:30:08 -05:00
@lines . each do | line |
2014-05-29 13:06:47 -05:00
if has_nokogiri
if line =~ / Nokogiri::XML \ .parse / or line =~ / Nokogiri::XML::Reader /
2014-05-29 11:52:17 -05:00
has_nokogiri_xml_parser = true
break
end
2014-05-29 13:06:47 -05:00
else
2014-07-17 09:29:13 -07:00
has_nokogiri = line_has_require? ( line , 'nokogiri' )
2014-02-02 11:51:21 -06:00
end
end
2014-05-29 11:52:17 -05:00
error ( msg ) if has_nokogiri_xml_parser
2014-02-02 11:51:21 -06:00
end
2013-09-30 13:47:53 -05:00
def check_ref_identifiers
2018-07-13 14:19:00 -05:00
in_super = false
in_refs = false
2018-08-30 10:46:26 -05:00
in_notes = false
2018-07-13 14:19:00 -05:00
cve_assigned = false
2013-09-30 13:47:53 -05:00
2014-08-26 15:30:08 -05:00
@lines . each do | line |
2014-01-30 14:39:28 -06:00
if ! in_super and line =~ / \ s+super \ ( /
2013-09-30 13:47:53 -05:00
in_super = true
elsif in_super and line =~ / [[:space:]]*def \ w+[ \ ( \ w+ \ )]* /
in_super = false
break
end
2014-01-08 20:32:30 -06:00
if in_super and line =~ / ["']References["'][[:space:]]*=> /
2013-09-30 13:47:53 -05:00
in_refs = true
elsif in_super and in_refs and line =~ / ^[[:space:]]+ \ ],* /m
2018-08-30 10:46:26 -05:00
in_refs = false
elsif in_super and line =~ / ["']Notes["'][[:space:]]*=> /
in_notes = true
2013-09-30 13:47:53 -05:00
elsif in_super and in_refs and line =~ / [^ # ]+ \ [[[:space:]]*['"](.+)['"][[:space:]]*,[[:space:]]*['"](.+)['"][[:space:]]* \ ] /
identifier = $1 . strip . upcase
value = $2 . strip
case identifier
when 'CVE'
2018-07-13 14:19:00 -05:00
cve_assigned = true
2015-03-31 22:15:33 -05:00
warn ( " Invalid CVE format: ' #{ value } ' " ) if value !~ / ^ \ d{4} \ - \ d{4,}$ /
2013-09-30 13:47:53 -05:00
when 'BID'
warn ( " Invalid BID format: ' #{ value } ' " ) if value !~ / ^ \ d+$ /
when 'MSB'
warn ( " Invalid MSB format: ' #{ value } ' " ) if value !~ / ^MS \ d+ \ - \ d+$ /
when 'MIL'
warn ( " milw0rm references are no longer supported. " )
when 'EDB'
warn ( " Invalid EDB reference " ) if value !~ / ^ \ d+$ /
when 'US-CERT-VU'
warn ( " Invalid US-CERT-VU reference " ) if value !~ / ^ \ d+$ /
2013-10-21 15:30:07 -05:00
when 'ZDI'
2020-04-28 18:48:10 -05:00
warn ( " Invalid ZDI reference " ) if value !~ / ^ \ d{2}- \ d{3,4}$ /
2015-07-30 01:22:48 -03:00
when 'WPVDB'
2021-02-06 12:20:03 +01:00
warn ( " Invalid WPVDB reference " ) if value !~ / ^ \ d+$ / and value !~ / ^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}?$ /
2015-09-01 23:25:01 -05:00
when 'PACKETSTORM'
warn ( " Invalid PACKETSTORM reference " ) if value !~ / ^ \ d+$ /
2018-08-30 10:46:26 -05:00
when 'URL'
2018-08-27 21:44:42 +02:00
if value =~ / ^https?: \/ \/ cvedetails \ .com \/ cve /
2013-09-30 13:47:53 -05:00
warn ( " Please use 'CVE' for ' #{ value } ' " )
2022-04-19 20:35:21 +00:00
elsif value =~ %r{ ^https?://cve \ .mitre \ .org/cgi-bin/cvename \ .cgi }
warn ( " Please use 'CVE' for ' #{ value } ' " )
2018-08-27 21:44:42 +02:00
elsif value =~ / ^https?: \/ \/ www \ .securityfocus \ .com \/ bid \/ /
2013-09-30 13:47:53 -05:00
warn ( " Please use 'BID' for ' #{ value } ' " )
2018-08-27 21:44:42 +02:00
elsif value =~ / ^https?: \/ \/ www \ .microsoft \ .com \/ technet \/ security \/ bulletin \/ /
2013-09-30 13:47:53 -05:00
warn ( " Please use 'MSB' for ' #{ value } ' " )
2018-08-26 04:18:38 +00:00
elsif value =~ / ^https?: \/ \/ www \ .exploit \ -db \ .com \/ exploits \/ /
2013-09-30 13:47:53 -05:00
warn ( " Please use 'EDB' for ' #{ value } ' " )
2018-08-27 21:44:42 +02:00
elsif value =~ / ^https?: \/ \/ www \ .kb \ .cert \ .org \/ vuls \/ id \/ /
2013-09-30 13:47:53 -05:00
warn ( " Please use 'US-CERT-VU' for ' #{ value } ' " )
2018-08-28 13:49:31 +02:00
elsif value =~ / ^https?: \/ \/ wpvulndb \ .com \/ vulnerabilities \/ /
2015-07-30 01:22:48 -03:00
warn ( " Please use 'WPVDB' for ' #{ value } ' " )
2021-02-06 12:20:03 +01:00
elsif value =~ / ^https?: \/ \/ wpscan \ .com \/ vulnerability \/ /
warn ( " Please use 'WPVDB' for ' #{ value } ' " )
2015-12-24 09:12:24 -08:00
elsif value =~ / ^https?: \/ \/ (?:[^ \ .]+ \ .)?packetstormsecurity \ .(?:com|net|org) \/ /
2015-09-01 23:25:01 -05:00
warn ( " Please use 'PACKETSTORM' for ' #{ value } ' " )
2013-09-30 13:47:53 -05:00
end
2018-08-30 10:46:26 -05:00
when 'AKA'
warn ( " Please include AKA values in the 'notes' section, rather than in 'references'. " )
2013-09-30 13:47:53 -05:00
end
end
2018-08-30 10:46:26 -05:00
# If a NOCVE reason was provided in notes, ignore the fact that the references might lack a CVE
if in_super and in_notes and line =~ / ^[[:space:]]+["']NOCVE["'][[:space:]]+=>[[:space:]]+ \ [*["'](.+)["'] \ ]* /
cve_assigned = true
end
2013-09-30 13:47:53 -05:00
end
2018-07-13 14:19:00 -05:00
# This helps us track when CVEs aren't assigned
2021-11-04 07:13:11 -04:00
if ! cve_assigned && is_exploit_module?
2018-08-30 10:46:26 -05:00
info ( 'No CVE references found. Please check before you land!' )
end
2013-09-30 13:47:53 -05:00
end
2017-04-26 03:30:02 -05:00
def check_self_class
in_register = false
@lines . each do | line |
( in_register = true ) if line =~ / ^ \ s*register_(?:advanced_)?options /
( in_register = false ) if line =~ / ^ \ s*end /
if in_register && line =~ / \ ], \ s*self \ .class \ s* \ ) /
warn ( 'Explicitly using self.class in register_* is not necessary' )
break
end
end
end
2014-07-17 09:29:13 -07:00
# See if 'require "rubygems"' or equivalent is used, and
2018-03-20 11:33:34 +00:00
# warn if so. Since Ruby 1.9 this has not been necessary and
2024-01-07 13:28:13 -05:00
# the framework only supports 1.9+
2014-07-17 09:29:13 -07:00
def check_rubygems
2014-08-26 15:30:08 -05:00
@lines . each do | line |
2014-07-17 09:29:13 -07:00
if line_has_require? ( line , 'rubygems' )
warn ( " Explicitly requiring/loading rubygems is not necessary " )
break
end
end
end
2017-04-26 02:53:36 -05:00
def check_msf_core
@lines . each do | line |
if line_has_require? ( line , 'msf/core' )
warn ( 'Explicitly requiring/loading msf/core is not necessary' )
break
end
end
end
2014-07-17 09:29:13 -07:00
# Does the given line contain a require/load of the specified library?
def line_has_require? ( line , lib )
line =~ / ^ \ s*(require|load) \ s+['"] #{ lib } ['"] /
end
2018-10-05 11:30:45 -05:00
# This check also enforces namespace module name reversibility
2013-09-30 13:47:53 -05:00
def check_snake_case_filename
2018-10-05 13:24:45 -05:00
if @name !~ / ^[a-z0-9]+(?:_[a-z0-9]+)* \ .rb$ /
2018-10-10 14:23:13 -05:00
warn ( 'Filenames must be lowercase alphanumeric snake case.' )
2013-09-30 13:47:53 -05:00
end
end
2013-10-15 13:35:52 -05:00
def check_comment_splat
if @source =~ / ^ # This file is part of the Metasploit Framework and may be subject to /
2015-03-09 23:59:13 -05:00
warn ( " Module contains old license comment. " )
2013-10-15 13:35:52 -05:00
end
2019-02-04 21:16:01 +00:00
if @source =~ / ^ # This module requires Metasploit: http: /
warn ( " Module license comment link does not use https:// URL scheme. " )
2019-02-07 00:42:28 -06:00
fixed ( '# This module requires Metasploit: https://metasploit.com/download' , 1 )
2019-02-04 21:16:01 +00:00
end
2013-10-15 13:35:52 -05:00
end
2013-09-30 13:47:53 -05:00
def check_old_keywords
max_count = 10
counter = 0
if @source =~ / ^ # # /
2014-08-26 15:30:08 -05:00
@lines . each do | line |
2013-09-30 13:47:53 -05:00
# If exists, the $Id$ keyword should appear at the top of the code.
# If not (within the first 10 lines), then we assume there's no
# $Id$, and then bail.
break if counter > = max_count
if line =~ / ^ # [[:space:]]* \ $Id \ $ /i
warn ( " Keyword $Id$ is no longer needed. " )
break
end
counter += 1
end
end
2014-01-08 20:32:30 -06:00
if @source =~ / ["']Version["'][[:space:]]*=>[[:space:]]*['"] \ $Revision \ $['"] /
2013-09-30 13:47:53 -05:00
warn ( " Keyword $Revision$ is no longer needed. " )
end
end
def check_verbose_option
if @source =~ / Opt(Bool|String).new \ ([[:space:]]*('|")VERBOSE('|")[[:space:]]*,[[:space:]]* \ [[[:space:]]* /
warn ( " VERBOSE Option is already part of advanced settings, no need to add it manually. " )
end
end
def check_badchars
badchars = %Q|&<=>|
in_super = false
in_author = false
2014-08-26 15:30:08 -05:00
@lines . each do | line |
2013-09-30 13:47:53 -05:00
#
# Mark our "super" code block
#
2014-01-30 14:39:28 -06:00
if ! in_super and line =~ / \ s+super \ ( /
2013-09-30 13:47:53 -05:00
in_super = true
elsif in_super and line =~ / [[:space:]]*def \ w+[ \ ( \ w+ \ )]* /
in_super = false
break
end
#
# While in super() code block
#
2014-01-08 20:32:30 -06:00
if in_super and line =~ / ["']Name["'][[:space:]]*=>[[:space:]]*['|"](.+)['|"] /
2013-09-30 13:47:53 -05:00
# Now we're checking the module titlee
mod_title = $1
mod_title . each_char do | c |
if badchars . include? ( c )
error ( " ' #{ c } ' is a bad character in module title. " )
end
end
# Since we're looking at the module title, this line clearly cannot be
# the author block, so no point to run more code below.
next
end
2015-11-06 13:23:14 -08:00
# XXX: note that this is all very fragile and regularly incorrectly parses
# the author
2013-09-30 13:47:53 -05:00
#
# Mark our 'Author' block
#
2014-01-08 20:32:30 -06:00
if in_super and ! in_author and line =~ / ["']Author["'][[:space:]]*=> /
2013-09-30 13:47:53 -05:00
in_author = true
elsif in_super and in_author and line =~ / \ ],* \ n / or line =~ / ['"][[:print:]]*['"][[:space:]]*=> /
in_author = false
end
#
2015-11-06 13:23:14 -08:00
# While in 'Author' block, check for malformed authors
2013-09-30 13:47:53 -05:00
#
if in_super and in_author
2015-11-06 13:23:14 -08:00
if line =~ / Author['"] \ s*=> \ s*['"](.*)['"], /
author_name = Regexp . last_match ( 1 )
elsif line =~ / Author /
2013-09-30 13:47:53 -05:00
author_name = line . scan ( / \ [[[:space:]]*['"](.+)['"] / ) . flatten [ - 1 ] || ''
else
author_name = line . scan ( / ['"](.+)['"] / ) . flatten [ - 1 ] || ''
end
if author_name =~ / ^@.+$ /
error ( " No Twitter handles, please. Try leaving it in a comment instead. " )
end
2015-11-06 13:23:14 -08:00
unless author_name . empty?
author_open_brackets = author_name . scan ( '<' ) . size
author_close_brackets = author_name . scan ( '>' ) . size
if author_open_brackets != author_close_brackets
error ( " Author has unbalanced brackets: #{ author_name } " )
end
end
2013-09-30 13:47:53 -05:00
end
end
end
def check_extname
if File . extname ( @name ) != '.rb'
error ( " Module should be a '.rb' file, or it won't load. " )
end
end
2019-11-30 07:25:27 +00:00
def check_executable
if File . executable? ( @full_filepath )
error ( " Module should not be executable (+x) " )
end
end
2014-04-10 21:44:00 -05:00
def check_old_rubies
2013-09-30 13:47:53 -05:00
return true unless CHECK_OLD_RUBIES
return true unless Object . const_defined? :RVM
2013-10-16 10:40:42 -05:00
puts " Checking syntax for #{ @name } . "
2013-09-30 13:47:53 -05:00
rubies || = RVM . list_strings
2013-10-16 10:40:42 -05:00
res = %x{ rvm all do ruby -c #{ @full_filepath } } . split ( " \n " ) . select { | msg | msg =~ / Syntax OK / }
2013-09-30 13:47:53 -05:00
error ( " Fails alternate Ruby version check " ) if rubies . size != res . size
end
2014-08-26 15:30:08 -05:00
def is_exploit_module?
ret = false
if @source =~ REGEX_MSF_EXPLOIT
# having Msf::Exploit is good indicator, but will false positive on
# specs and other files containing the string, but not really acting
# as exploit modules, so here we check the file for some actual contents
# this could be done in a simpler way, but this let's us add more later
msf_exploit_line_no = nil
@lines . each_with_index do | line , idx |
2014-08-26 20:36:28 -05:00
if line =~ REGEX_MSF_EXPLOIT
2014-08-26 15:30:08 -05:00
# note the line number
msf_exploit_line_no = idx
elsif msf_exploit_line_no
# check there is anything but empty space between here and the next end
# something more complex could be added here
if line !~ REGEX_IS_BLANK_OR_END
# if the line is not 'end' and is not blank, prolly exploit module
ret = true
break
else
# then keep checking in case there are more than one Msf::Exploit
msf_exploit_line_no = nil
end
end
end
end
ret
end
2013-09-30 13:47:53 -05:00
def check_ranking
2014-08-26 15:30:08 -05:00
return unless is_exploit_module?
2013-09-30 13:47:53 -05:00
available_ranks = [
'ManualRanking' ,
'LowRanking' ,
'AverageRanking' ,
'NormalRanking' ,
'GoodRanking' ,
'GreatRanking' ,
'ExcellentRanking'
]
if @source =~ / Rank \ = ( \ w+) /
if not available_ranks . include? ( $1 )
error ( " Invalid ranking. You have ' #{ $1 } ' " )
end
2019-02-25 11:24:30 -06:00
elsif @source =~ / ['"](SideEffects|Stability|Reliability)['"] \ s*= /
2019-02-21 12:38:26 -06:00
info ( 'No Rank, however SideEffects, Stability, or Reliability are provided' )
2016-06-14 11:45:57 -05:00
else
2018-07-18 00:05:48 -05:00
warn ( 'No Rank specified. The default is NormalRanking. Please add an explicit Rank value.' )
2013-09-30 13:47:53 -05:00
end
end
def check_disclosure_date
2014-04-07 14:21:04 -05:00
return if @source =~ / Generic Payload Handler /
2013-09-30 13:47:53 -05:00
# Check disclosure date format
2018-11-16 11:40:12 -06:00
if @source =~ / ["']DisclosureDate["'].* \ = \ >[ \ x0d \ x20]*[' \ "](.+?)[' \ "] /
2018-11-16 11:48:44 -06:00
d = $1 #Captured date
2013-09-30 13:47:53 -05:00
# Flag if overall format is wrong
2018-11-16 11:48:44 -06:00
if d =~ / ^... (?: \ d{1,2},? )? \ d{4}$ /
# Flag if month format is wrong
m = d . split [ 0 ]
months = [
'Jan' , 'Feb' , 'Mar' , 'Apr' , 'May' , 'Jun' ,
'Jul' , 'Aug' , 'Sep' , 'Oct' , 'Nov' , 'Dec'
]
error ( 'Incorrect disclosure month format' ) if months . index ( m ) . nil?
2018-11-16 13:25:01 -06:00
# XXX: yyyy-mm is interpreted as yyyy-01-mm by Date::iso8601
2018-11-16 12:03:01 -06:00
elsif d =~ / ^ \ d{4}- \ d{2}- \ d{2}$ /
begin
Date . iso8601 ( d )
rescue ArgumentError
error ( 'Incorrect ISO 8601 disclosure date format' )
end
2018-11-16 11:48:44 -06:00
else
2013-09-30 13:47:53 -05:00
error ( 'Incorrect disclosure date format' )
end
else
2014-08-26 15:30:08 -05:00
error ( 'Exploit is missing a disclosure date' ) if is_exploit_module?
2013-09-30 13:47:53 -05:00
end
end
def check_bad_terms
# "Stack overflow" vs "Stack buffer overflow" - See explanation:
# http://blogs.technet.com/b/srd/archive/2009/01/28/stack-overflow-stack-exhaustion-not-the-same-as-stack-buffer-overflow.aspx
2021-02-13 04:10:13 +00:00
if @module_type == 'exploits' && @source . gsub ( " \n " , " " ) =~ / stack[[:space:]]+overflow /i
2013-09-30 13:47:53 -05:00
warn ( 'Contains "stack overflow" You mean "stack buffer overflow"?' )
2015-12-24 10:54:13 -08:00
elsif @module_type == 'auxiliary' && @source . gsub ( " \n " , " " ) =~ / stack[[:space:]]+overflow /i
2013-09-30 13:47:53 -05:00
warn ( 'Contains "stack overflow" You mean "stack exhaustion"?' )
end
end
2015-12-22 13:38:31 -08:00
def check_bad_super_class
# skip payloads, as they don't have a super class
return if @module_type == 'payloads'
# get the super class in an ugly way
2016-03-08 14:02:44 +01:00
unless ( super_class = @source . scan ( / class Metasploit(?: \ d|Module) \ s+< \ s+( \ S+) / ) . flatten . first )
2015-12-22 13:38:31 -08:00
error ( 'Unable to determine super class' )
return
end
prefix_super_map = {
2018-10-06 15:59:05 -05:00
'evasion' = > / ^Msf::Evasion$ / ,
2015-12-23 08:33:47 -08:00
'auxiliary' = > / ^Msf::Auxiliary$ / ,
'exploits' = > / ^Msf::Exploit(?:::Local|::Remote)?$ / ,
2015-12-22 13:38:31 -08:00
'encoders' = > / ^(?:Msf|Rex)::Encoder / ,
'nops' = > / ^Msf::Nop$ / ,
'post' = > / ^Msf::Post$ /
}
if prefix_super_map . key? ( @module_type )
unless super_class =~ prefix_super_map [ @module_type ]
error ( " Invalid super class for #{ @module_type } module (found ' #{ super_class } ', expected something like #{ prefix_super_map [ @module_type ] } " )
end
else
warn ( " Unexpected and potentially incorrect super class found (' #{ super_class } ') " )
end
end
2013-09-30 13:47:53 -05:00
def check_function_basics
functions = @source . scan ( / def ( \ w+) \ (*(.+) \ )* / )
functions . each do | func_name , args |
# Check argument length
args_length = args . split ( " , " ) . length
warn ( " Poorly designed argument list in ' #{ func_name } ()'. Try a hash. " ) if args_length > 6
end
end
2016-03-08 14:02:44 +01:00
def check_bad_class_name
if @source =~ / ^ \ s*class (Metasploit \ d+) \ s*< /
2016-03-16 13:31:24 -04:00
warn ( " Please use 'MetasploitModule' as the class name (you used #{ Regexp . last_match ( 1 ) } ) " )
2016-03-08 14:02:44 +01:00
end
end
2013-09-30 13:47:53 -05:00
def check_lines
url_ok = true
no_stdio = true
in_comment = false
in_literal = false
2020-11-17 18:06:17 +08:00
in_heredoc = false
2013-09-30 13:47:53 -05:00
src_ended = false
idx = 0
2014-08-26 15:30:08 -05:00
@lines . each do | ln |
2013-09-30 13:47:53 -05:00
idx += 1
# block comment awareness
if ln =~ / ^=end$ /
in_comment = false
next
end
in_comment = true if ln =~ / ^=begin$ /
next if in_comment
# block string awareness (ignore indentation in these)
in_literal = false if ln =~ / ^EOS$ /
next if in_literal
in_literal = true if ln =~ / \ < \ <-EOS$ /
2020-11-17 18:06:17 +08:00
# heredoc string awareness (ignore indentation in these)
if in_heredoc
in_heredoc = false if ln =~ / \ s #{ in_heredoc } $ /
next
end
if ln =~ / \ < \ < \ ~([A-Z]+)$ /
in_heredoc = $1
end
2013-09-30 13:47:53 -05:00
# ignore stuff after an __END__ line
src_ended = true if ln =~ / ^__END__$ /
next if src_ended
if ln =~ / [ \ t]$ /
warn ( " Spaces at EOL " , idx )
end
2013-10-01 12:22:46 -05:00
# Check for mixed tab/spaces. Upgrade this to an error() soon.
2013-09-30 13:47:53 -05:00
if ( ln . length > 1 ) and ( ln =~ / ^([ \ t ]*) / ) and ( $1 . match ( / \ x20 \ x09| \ x09 \ x20 / ) )
warn ( " Space-Tab mixed indent: #{ ln . inspect } " , idx )
end
2013-10-01 12:22:46 -05:00
# Check for tabs. Upgrade this to an error() soon.
if ( ln . length > 1 ) and ( ln =~ / ^ \ x09 / )
warn ( " Tabbed indent: #{ ln . inspect } " , idx )
end
2013-09-30 13:47:53 -05:00
if ln =~ / \ r$ /
warn ( " Carriage return EOL " , idx )
end
url_ok = false if ln =~ / \ .com \/ projects \/ Framework /
if ln =~ / File \ .open / and ln =~ / [ \ " \ '][arw] /
if not ln =~ / [ \ " \ '][wra] \ +?b \ +?[ \ " \ '] /
warn ( " File.open without binary mode " , idx )
end
end
if ln =~ / ^[ \ t]*load[ \ t]+[ \ x22 \ x27] /
error ( " Loading (not requiring) a file: #{ ln . inspect } " , idx )
end
# The rest of these only count if it's not a comment line
2015-02-09 13:40:18 -06:00
next if ln =~ / ^[[:space:]]* # /
2013-09-30 13:47:53 -05:00
2013-11-20 17:53:25 -06:00
if ln =~ / \ $std(?:out|err) /i or ln =~ / [[:space:]]puts /
2017-04-06 17:44:30 -05:00
next if ln =~ / ["'][^"']* \ $std(?:out|err)[^"']*["'] /
2013-09-30 13:47:53 -05:00
no_stdio = false
error ( " Writes to stdout " , idx )
end
2014-05-12 21:23:30 +02:00
# do not read Set-Cookie header (ignore commented lines)
2015-02-09 17:01:29 -06:00
if ln =~ / ^(?! \ s* # ).+ \ [['"]Set-Cookie['"] \ ](?! \ s*=[^=~]+) /i
2014-03-01 13:30:24 +01:00
warn ( " Do not read Set-Cookie header directly, use res.get_cookies instead: #{ ln } " , idx )
end
2014-03-28 22:43:53 +01:00
# Auxiliary modules do not have a rank attribute
2015-12-24 10:54:13 -08:00
if ln =~ / ^ \ s*Rank \ s*= \ s* / && @module_type == 'auxiliary'
2014-03-28 22:43:53 +01:00
warn ( " Auxiliary modules have no 'Rank': #{ ln } " , idx )
end
2014-12-11 23:10:07 +01:00
2015-02-09 16:18:43 -06:00
if ln =~ / ^ \ s*def \ s+(?:[^ \ ( \ ) # ]*[A-Z]+[^ \ ( \ )]*)(?: \ (.* \ ))?$ /
2014-12-11 23:10:07 +01:00
warn ( " Please use snake case on method names: #{ ln } " , idx )
end
2015-04-16 20:04:08 +02:00
2015-04-16 21:51:31 +02:00
if ln =~ / ^ \ s*fail_with \ ( /
2019-04-08 09:57:22 -05:00
unless ln =~ / ^ \ s*fail_with \ (.*Failure \ : \ :(?:None|Unknown|Unreachable|BadConfig|Disconnected|NotFound|UnexpectedReply|TimeoutExpired|UserInterrupt|NoAccess|NoTarget|NotVulnerable|PayloadFailed), /
2015-04-16 21:43:47 +02:00
error ( " fail_with requires a valid Failure:: reason as first parameter: #{ ln } " , idx )
end
2015-04-16 20:04:08 +02:00
end
2015-04-16 21:43:47 +02:00
2015-08-12 20:55:55 +02:00
if ln =~ / ['"]ExitFunction['"] \ s*=> /
warn ( " Please use EXITFUNC instead of ExitFunction #{ ln } " , idx )
2019-02-04 21:16:01 +00:00
fixed ( line . gsub ( 'ExitFunction' , 'EXITFUNC' ) , idx )
2015-08-12 20:55:55 +02:00
end
2019-02-04 21:16:01 +00:00
# Output from Base64.encode64 method contains '\n' new lines
# for line wrapping and string termination
if ln =~ / Base64 \ .encode64 /
info ( " Please use Base64.strict_encode64 instead of Base64.encode64 " )
end
2014-08-26 15:30:08 -05:00
end
2013-09-30 13:47:53 -05:00
end
2014-01-22 15:26:16 -06:00
def check_vuln_codes
checkcode = @source . scan ( / (Exploit::)?CheckCode::( \ w+) / ) . flatten [ 1 ]
if checkcode and checkcode !~ / ^Unknown|Safe|Detected|Appears|Vulnerable|Unsupported$ /
2014-01-23 14:21:48 -06:00
error ( " Unrecognized checkcode: #{ checkcode } " )
2014-01-22 15:26:16 -06:00
end
end
2014-04-14 14:49:26 -05:00
def check_vars_get
2017-02-23 02:13:59 -06:00
test = @source . scan ( / send_request_cgi \ s* \ (? \ s* \ {? \ s*['"]uri['"] \ s*=> \ s*[^=})]*? \ ?[^,})]+ /im )
2014-03-01 12:02:41 +01:00
unless test . empty?
test . each { | item |
2018-07-18 00:05:48 -05:00
warn ( " Please use vars_get in send_request_cgi: #{ item } " )
2014-03-01 12:02:41 +01:00
}
end
end
2014-06-17 15:44:43 +02:00
def check_newline_eof
if @source !~ / (?: \ r \ n| \ n) \ z /m
2018-07-18 00:05:48 -05:00
warn ( 'Please add a newline at the end of the file' )
2014-06-30 00:40:06 -05:00
end
end
def check_udp_sock_get
2014-07-14 14:36:08 -05:00
if @source =~ / udp_sock \ .get /m && @source !~ / udp_sock \ .get \ ([a-zA-Z0-9]+ /
2018-07-18 00:05:48 -05:00
warn ( 'Please specify a timeout to udp_sock.get' )
2014-06-30 00:40:06 -05:00
end
end
2014-12-11 18:36:42 -08:00
# At one point in time, somebody committed a module with a bad metasploit.com URL
2017-07-24 06:26:21 -07:00
# in the header -- http//metasploit.com/download rather than https://metasploit.com/download.
2014-12-11 18:36:42 -08:00
# This module then got copied and committed 20+ times and is used in numerous other places.
# This ensures that this stops.
2014-12-11 23:10:07 +01:00
def check_invalid_url_scheme
2019-02-04 21:16:01 +00:00
test = @source . scan ( / ^ # .+https? \/ \/ (?:www \ .)?metasploit.com / )
2014-12-11 23:10:07 +01:00
unless test . empty?
2015-01-22 14:27:34 -06:00
test . each { | item |
2018-07-18 00:05:48 -05:00
warn ( " Invalid URL: #{ item } " )
2015-01-22 14:27:34 -06:00
}
2014-12-11 23:10:07 +01:00
end
end
2015-04-21 11:27:22 -05:00
# Check for (v)print_debug usage, since it doesn't exist anymore
#
# @see https://github.com/rapid7/metasploit-framework/issues/3816
def check_print_debug
if @source =~ / print_debug /
error ( 'Please don\'t use (v)print_debug, use vprint_(status|good|error|warning) instead' )
end
end
2015-04-21 11:56:41 -05:00
# Check for modules registering the DEBUG datastore option
#
# @see https://github.com/rapid7/metasploit-framework/issues/3816
2015-04-21 12:22:24 -05:00
def check_register_datastore_debug
2015-04-21 12:13:40 -05:00
if @source =~ / Opt.* \ .new \ (["'](?i)DEBUG(?-i)["'] /
2015-04-21 11:56:41 -05:00
error ( 'Please don\'t register a DEBUG datastore option, it has an special meaning and is used for development' )
end
end
2015-04-21 12:22:24 -05:00
# Check for modules using the DEBUG datastore option
#
# @see https://github.com/rapid7/metasploit-framework/issues/3816
def check_use_datastore_debug
if @source =~ / datastore \ [["'](?i)DEBUG(?-i)["'] \ ] /
error ( 'Please don\'t use the DEBUG datastore option in production, it has an special meaning and is used for development' )
end
end
2016-11-18 07:32:20 -06:00
# Check for modules using the deprecated architectures
#
# @see https://github.com/rapid7/metasploit-framework/pull/7507
def check_arch
if @source =~ / ARCH_X86_64 /
error ( 'Please don\'t use the ARCH_X86_64 architecture, use ARCH_X64 instead' )
end
end
2019-03-26 13:27:57 -05:00
# Check for modules having an Author section to ensure attribution
#
def check_author
2019-03-29 10:44:22 -05:00
# Only the three common module types have a consistently defined info hash
2021-02-13 04:10:13 +00:00
return unless %w[ exploits auxiliary post ] . include? ( @module_type )
2019-03-29 10:44:22 -05:00
2019-03-26 13:27:57 -05:00
unless @source =~ / ["']Author["'][[:space:]]*=> /
error ( 'Missing "Author" info, please add' )
end
end
2021-01-22 23:29:16 +00:00
# Check for modules specifying a description
#
def check_description
# Payloads do not require a description
return if @module_type == 'payloads'
unless @source =~ / ["']Description["'][[:space:]]*=> /
error ( 'Missing "Description" info, please add' )
end
end
2021-02-15 11:01:01 +00:00
# Check for exploit modules specifying notes
#
def check_notes
# Only exploits require notes
return unless @module_type == 'exploits'
unless @source =~ / ["']Notes["'][[:space:]]*=> /
# This should be updated to warning eventually
info ( 'Missing "Notes" info, please add' )
end
end
2015-12-24 10:54:13 -08:00
#
# Run all the msftidy checks.
#
def run_checks
check_shebang
check_nokogiri
check_rubygems
2017-04-26 02:53:36 -05:00
check_msf_core
2015-12-24 10:54:13 -08:00
check_ref_identifiers
2017-04-26 03:30:02 -05:00
check_self_class
2015-12-24 10:54:13 -08:00
check_old_keywords
check_verbose_option
check_badchars
check_extname
2019-11-30 07:25:27 +00:00
check_executable
2015-12-24 10:54:13 -08:00
check_old_rubies
check_ranking
check_disclosure_date
check_bad_terms
check_bad_super_class
2016-03-08 14:02:44 +01:00
check_bad_class_name
2015-12-24 10:54:13 -08:00
check_function_basics
check_lines
check_snake_case_filename
check_comment_splat
check_vuln_codes
check_vars_get
check_newline_eof
check_udp_sock_get
check_invalid_url_scheme
check_print_debug
check_register_datastore_debug
check_use_datastore_debug
2016-11-18 07:32:20 -06:00
check_arch
2019-03-26 13:27:57 -05:00
check_author
2021-01-22 23:29:16 +00:00
check_description
2021-02-15 11:01:01 +00:00
check_notes
2015-12-24 10:54:13 -08:00
end
2013-09-30 13:47:53 -05:00
private
def load_file ( file )
2021-01-29 17:59:14 +00:00
f = File . open ( file , 'rb' )
2013-11-05 11:49:15 -06:00
@stat = f . stat
buf = f . read ( @stat . size )
2013-09-30 13:47:53 -05:00
f . close
return buf
end
2014-03-01 12:02:41 +01:00
def cleanup_text ( txt )
# remove line breaks
txt = txt . gsub ( / [ \ r \ n] / , ' ' )
# replace multiple spaces by one space
txt . gsub ( / \ s{2,} / , ' ' )
end
2012-10-12 02:55:16 -05:00
end
2010-11-05 00:05:34 +00:00
2021-01-29 17:59:14 +00:00
class Msftidy
def run ( dirs , options = { } )
@exit_status = 0
rubocop_runner = RuboCopRunner . new
dirs . each do | dir |
begin
Find . find ( dir ) do | full_filepath |
next if full_filepath =~ / \ .git[ \ x5c \ x2f] /
next unless File . file? full_filepath
next unless File . extname ( full_filepath ) == '.rb'
msftidy_runner = MsftidyRunner . new ( full_filepath )
# Executable files are now assumed to be external modules
# but also check for some content to be sure
next if File . executable? ( full_filepath ) && msftidy_runner . source =~ / require ["']metasploit["'] /
msftidy_runner . run_checks
@exit_status = msftidy_runner . status if ( msftidy_runner . status > @exit_status . to_i )
rubocop_result = rubocop_runner . run ( full_filepath , options )
@exit_status = MsftidyRunner :: ERROR if rubocop_result != RuboCop :: CLI :: STATUS_SUCCESS
end
rescue Errno :: ENOENT
$stderr . puts " #{ File . basename ( __FILE__ ) } : #{ dir } : No such file or directory "
end
end
@exit_status . to_i
end
end
2010-11-05 00:05:34 +00:00
##
#
# Main program
#
##
2015-12-24 10:54:13 -08:00
if __FILE__ == $PROGRAM_NAME
2021-01-29 17:59:14 +00:00
options = { }
options_parser = OptionParser . new do | opts |
opts . banner = " Usage: #{ File . basename ( __FILE__ ) } <directory or file> "
2010-11-05 00:05:34 +00:00
2021-01-29 17:59:14 +00:00
opts . on '-h' , '--help' , 'Help banner.' do
return print ( opts . help )
end
2021-02-24 20:24:57 +00:00
opts . on ( '-a' , '--auto-correct' , 'Auto-correct offenses (only when safe).' ) do | auto_correct |
options [ :auto_correct ] = auto_correct
end
opts . on ( '-A' , '--auto-correct-all' , 'Auto-correct offenses (safe and unsafe).' ) do | auto_correct_all |
options [ :auto_correct_all ] = auto_correct_all
2021-01-29 17:59:14 +00:00
end
end
options_parser . parse!
dirs = ARGV
2014-01-31 14:19:04 -06:00
2015-12-24 10:54:13 -08:00
if dirs . length < 1
2021-01-29 17:59:14 +00:00
$stderr . puts options_parser . help
2015-12-24 10:54:13 -08:00
@exit_status = 1
exit ( @exit_status )
end
2010-11-05 00:05:34 +00:00
2021-01-29 17:59:14 +00:00
msftidy = Msftidy . new
exit_status = msftidy . run ( dirs , options )
exit ( exit_status )
2015-12-24 10:54:13 -08:00
end