Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/tools/dev/msftidy.rb
Views: 11766
#!/usr/bin/env ruby1# -*- coding: binary -*-23#4# Check (recursively) for style compliance violations and other5# tree inconsistencies.6#7# by jduck, todb, and friends8#910require 'fileutils'11require 'find'12require 'time'13require 'rubocop'14require 'open3'15require 'optparse'1617CHECK_OLD_RUBIES = !!ENV['MSF_CHECK_OLD_RUBIES']18SUPPRESS_INFO_MESSAGES = !!ENV['MSF_SUPPRESS_INFO_MESSAGES']1920if CHECK_OLD_RUBIES21require 'rvm'22warn "This is going to take a while, depending on the number of Rubies you have installed."23end2425class String26def red27"\e[1;31;40m#{self}\e[0m"28end2930def yellow31"\e[1;33;40m#{self}\e[0m"32end3334def green35"\e[1;32;40m#{self}\e[0m"36end3738def cyan39"\e[1;36;40m#{self}\e[0m"40end41end4243class RuboCopRunnerException < StandardError; end4445# Wrapper around RuboCop that requires modules to be linted46# In the future this class may have the responsibility of ensuring core library files are linted47class RuboCopRunner4849##50# Run Rubocop on the given file51#52# @param [String] full_filepath53# @param [Hash] options specifying autocorrect functionality54# @return [Integer] RuboCop::CLI status code55def run(full_filepath, options = {})56unless requires_rubocop?(full_filepath)57puts "#{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"58return RuboCop::CLI::STATUS_SUCCESS59end6061rubocop = RuboCop::CLI.new62args = %w[--format simple]63args << '-a' if options[:auto_correct]64args << '-A' if options[:auto_correct_all]65args << full_filepath66rubocop_result = rubocop.run(args)6768if rubocop_result != RuboCop::CLI::STATUS_SUCCESS69puts "#{full_filepath} - [#{'ERROR'.red}] Rubocop failed. Please run #{"rubocop -a #{full_filepath}".yellow} and verify all issues are resolved"70end7172rubocop_result73end7475private7677##78# For now any modules created after 3a046f01dae340c124dd3895e670983aef5fe0c579# will require Rubocop to be ran.80#81# This epoch was chosen from the landing date of the initial PR to82# enforce consistent module formatting with Rubocop:83#84# https://github.com/rapid7/metasploit-framework/pull/1299085#86# @param [String] full_filepath87# @return [Boolean] true if this file requires rubocop, false otherwise88def requires_rubocop?(full_filepath)89required_modules.include?(full_filepath)90end9192def required_modules93return @required_modules if @required_modules9495previously_merged_modules = new_modules_for('3a046f01dae340c124dd3895e670983aef5fe0c5..HEAD')96staged_modules = new_modules_for('--cached')9798@required_modules = previously_merged_modules + staged_modules99if @required_modules.empty?100raise RuboCopRunnerException, 'Error retrieving new modules when verifying Rubocop'101end102103@required_modules104end105106def new_modules_for(commit)107# Example output:108# M modules/exploits/osx/local/vmware_bash_function_root.rb109# A modules/exploits/osx/local/vmware_fusion_lpe.rb110raw_diff_summary, status = ::Open3.capture2("git diff -b --name-status -l0 --summary #{commit}")111112if !status.success? && exception113raise RuboCopRunnerException, "Command failed with status (#{status.exitstatus}): #{commit}"114end115116diff_summary = raw_diff_summary.lines.map do |line|117status, file = line.split(' ').each(&:strip)118{ status: status, file: file}119end120121diff_summary.each_with_object([]) do |summary, acc|122next unless summary[:status] == 'A'123124acc << summary[:file]125end126end127end128129class MsftidyRunner130131# Status codes132OK = 0133WARNING = 1134ERROR = 2135136# Some compiles regexes137REGEX_MSF_EXPLOIT = / \< Msf::Exploit/138REGEX_IS_BLANK_OR_END = /^\s*end\s*$/139140attr_reader :full_filepath, :source, :stat, :name, :status141142def initialize(source_file)143@full_filepath = source_file144@module_type = File.dirname(File.expand_path(@full_filepath))[/\/modules\/([^\/]+)/, 1]145@source = load_file(source_file)146@lines = @source.lines # returns an enumerator147@status = OK148@name = File.basename(source_file)149end150151public152153#154# Display a warning message, given some text and a number. Warnings155# are usually style issues that may be okay for people who aren't core156# Framework developers.157#158# @return status [Integer] Returns WARNINGS unless we already have an159# error.160def warn(txt, line=0) line_msg = (line>0) ? ":#{line}" : ''161puts "#{@full_filepath}#{line_msg} - [#{'WARNING'.yellow}] #{cleanup_text(txt)}"162@status = WARNING if @status < WARNING163end164165#166# Display an error message, given some text and a number. Errors167# can break things or are so egregiously bad, style-wise, that they168# really ought to be fixed.169#170# @return status [Integer] Returns ERRORS171def error(txt, line=0)172line_msg = (line>0) ? ":#{line}" : ''173puts "#{@full_filepath}#{line_msg} - [#{'ERROR'.red}] #{cleanup_text(txt)}"174@status = ERROR if @status < ERROR175end176177# Currently unused, but some day msftidy will fix errors for you.178def fixed(txt, line=0)179line_msg = (line>0) ? ":#{line}" : ''180puts "#{@full_filepath}#{line_msg} - [#{'FIXED'.green}] #{cleanup_text(txt)}"181end182183#184# Display an info message. Info messages do not alter the exit status.185#186def info(txt, line=0)187return if SUPPRESS_INFO_MESSAGES188line_msg = (line>0) ? ":#{line}" : ''189puts "#{@full_filepath}#{line_msg} - [#{'INFO'.cyan}] #{cleanup_text(txt)}"190end191192##193#194# The functions below are actually the ones checking the source code195#196##197198def check_shebang199if @lines.first =~ /^#!/200warn("Module should not have a #! line")201end202end203204# Updated this check to see if Nokogiri::XML.parse is being called205# specifically. The main reason for this concern is that some versions206# of libxml2 are still vulnerable to XXE attacks. REXML is safer (and207# slower) since it's pure ruby. Unfortunately, there is no pure Ruby208# HTML parser (except Hpricot which is abandonware) -- easy checks209# can avoid Nokogiri (most modules use regex anyway), but more complex210# checks tends to require Nokogiri for HTML element and value parsing.211def check_nokogiri212msg = "Using Nokogiri in modules can be risky, use REXML instead."213has_nokogiri = false214has_nokogiri_xml_parser = false215@lines.each do |line|216if has_nokogiri217if line =~ /Nokogiri::XML\.parse/ or line =~ /Nokogiri::XML::Reader/218has_nokogiri_xml_parser = true219break220end221else222has_nokogiri = line_has_require?(line, 'nokogiri')223end224end225error(msg) if has_nokogiri_xml_parser226end227228def check_ref_identifiers229in_super = false230in_refs = false231in_notes = false232cve_assigned = false233234@lines.each do |line|235if !in_super and line =~ /\s+super\(/236in_super = true237elsif in_super and line =~ /[[:space:]]*def \w+[\(\w+\)]*/238in_super = false239break240end241242if in_super and line =~ /["']References["'][[:space:]]*=>/243in_refs = true244elsif in_super and in_refs and line =~ /^[[:space:]]+\],*/m245in_refs = false246elsif in_super and line =~ /["']Notes["'][[:space:]]*=>/247in_notes = true248elsif in_super and in_refs and line =~ /[^#]+\[[[:space:]]*['"](.+)['"][[:space:]]*,[[:space:]]*['"](.+)['"][[:space:]]*\]/249identifier = $1.strip.upcase250value = $2.strip251252case identifier253when 'CVE'254cve_assigned = true255warn("Invalid CVE format: '#{value}'") if value !~ /^\d{4}\-\d{4,}$/256when 'BID'257warn("Invalid BID format: '#{value}'") if value !~ /^\d+$/258when 'MSB'259warn("Invalid MSB format: '#{value}'") if value !~ /^MS\d+\-\d+$/260when 'MIL'261warn("milw0rm references are no longer supported.")262when 'EDB'263warn("Invalid EDB reference") if value !~ /^\d+$/264when 'US-CERT-VU'265warn("Invalid US-CERT-VU reference") if value !~ /^\d+$/266when 'ZDI'267warn("Invalid ZDI reference") if value !~ /^\d{2}-\d{3,4}$/268when 'WPVDB'269warn("Invalid WPVDB reference") if value !~ /^\d+$/ and value !~ /^[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}?$/270when 'PACKETSTORM'271warn("Invalid PACKETSTORM reference") if value !~ /^\d+$/272when 'URL'273if value =~ /^https?:\/\/cvedetails\.com\/cve/274warn("Please use 'CVE' for '#{value}'")275elsif value =~ %r{^https?://cve\.mitre\.org/cgi-bin/cvename\.cgi}276warn("Please use 'CVE' for '#{value}'")277elsif value =~ /^https?:\/\/www\.securityfocus\.com\/bid\//278warn("Please use 'BID' for '#{value}'")279elsif value =~ /^https?:\/\/www\.microsoft\.com\/technet\/security\/bulletin\//280warn("Please use 'MSB' for '#{value}'")281elsif value =~ /^https?:\/\/www\.exploit\-db\.com\/exploits\//282warn("Please use 'EDB' for '#{value}'")283elsif value =~ /^https?:\/\/www\.kb\.cert\.org\/vuls\/id\//284warn("Please use 'US-CERT-VU' for '#{value}'")285elsif value =~ /^https?:\/\/wpvulndb\.com\/vulnerabilities\//286warn("Please use 'WPVDB' for '#{value}'")287elsif value =~ /^https?:\/\/wpscan\.com\/vulnerability\//288warn("Please use 'WPVDB' for '#{value}'")289elsif value =~ /^https?:\/\/(?:[^\.]+\.)?packetstormsecurity\.(?:com|net|org)\//290warn("Please use 'PACKETSTORM' for '#{value}'")291end292when 'AKA'293warn("Please include AKA values in the 'notes' section, rather than in 'references'.")294end295end296297# If a NOCVE reason was provided in notes, ignore the fact that the references might lack a CVE298if in_super and in_notes and line =~ /^[[:space:]]+["']NOCVE["'][[:space:]]+=>[[:space:]]+\[*["'](.+)["']\]*/299cve_assigned = true300end301end302303# This helps us track when CVEs aren't assigned304if !cve_assigned && is_exploit_module?305info('No CVE references found. Please check before you land!')306end307end308309def check_self_class310in_register = false311@lines.each do |line|312(in_register = true) if line =~ /^\s*register_(?:advanced_)?options/313(in_register = false) if line =~ /^\s*end/314if in_register && line =~ /\],\s*self\.class\s*\)/315warn('Explicitly using self.class in register_* is not necessary')316break317end318end319end320321# See if 'require "rubygems"' or equivalent is used, and322# warn if so. Since Ruby 1.9 this has not been necessary and323# the framework only supports 1.9+324def check_rubygems325@lines.each do |line|326if line_has_require?(line, 'rubygems')327warn("Explicitly requiring/loading rubygems is not necessary")328break329end330end331end332333def check_msf_core334@lines.each do |line|335if line_has_require?(line, 'msf/core')336warn('Explicitly requiring/loading msf/core is not necessary')337break338end339end340end341342# Does the given line contain a require/load of the specified library?343def line_has_require?(line, lib)344line =~ /^\s*(require|load)\s+['"]#{lib}['"]/345end346347# This check also enforces namespace module name reversibility348def check_snake_case_filename349if @name !~ /^[a-z0-9]+(?:_[a-z0-9]+)*\.rb$/350warn('Filenames must be lowercase alphanumeric snake case.')351end352end353354def check_comment_splat355if @source =~ /^# This file is part of the Metasploit Framework and may be subject to/356warn("Module contains old license comment.")357end358if @source =~ /^# This module requires Metasploit: http:/359warn("Module license comment link does not use https:// URL scheme.")360fixed('# This module requires Metasploit: https://metasploit.com/download', 1)361end362end363364def check_old_keywords365max_count = 10366counter = 0367if @source =~ /^##/368@lines.each do |line|369# If exists, the $Id$ keyword should appear at the top of the code.370# If not (within the first 10 lines), then we assume there's no371# $Id$, and then bail.372break if counter >= max_count373374if line =~ /^#[[:space:]]*\$Id\$/i375warn("Keyword $Id$ is no longer needed.")376break377end378379counter += 1380end381end382383if @source =~ /["']Version["'][[:space:]]*=>[[:space:]]*['"]\$Revision\$['"]/384warn("Keyword $Revision$ is no longer needed.")385end386end387388def check_verbose_option389if @source =~ /Opt(Bool|String).new\([[:space:]]*('|")VERBOSE('|")[[:space:]]*,[[:space:]]*\[[[:space:]]*/390warn("VERBOSE Option is already part of advanced settings, no need to add it manually.")391end392end393394def check_badchars395badchars = %Q|&<=>|396397in_super = false398in_author = false399400@lines.each do |line|401#402# Mark our "super" code block403#404if !in_super and line =~ /\s+super\(/405in_super = true406elsif in_super and line =~ /[[:space:]]*def \w+[\(\w+\)]*/407in_super = false408break409end410411#412# While in super() code block413#414if in_super and line =~ /["']Name["'][[:space:]]*=>[[:space:]]*['|"](.+)['|"]/415# Now we're checking the module titlee416mod_title = $1417mod_title.each_char do |c|418if badchars.include?(c)419error("'#{c}' is a bad character in module title.")420end421end422423# Since we're looking at the module title, this line clearly cannot be424# the author block, so no point to run more code below.425next426end427428# XXX: note that this is all very fragile and regularly incorrectly parses429# the author430#431# Mark our 'Author' block432#433if in_super and !in_author and line =~ /["']Author["'][[:space:]]*=>/434in_author = true435elsif in_super and in_author and line =~ /\],*\n/ or line =~ /['"][[:print:]]*['"][[:space:]]*=>/436in_author = false437end438439440#441# While in 'Author' block, check for malformed authors442#443if in_super and in_author444if line =~ /Author['"]\s*=>\s*['"](.*)['"],/445author_name = Regexp.last_match(1)446elsif line =~ /Author/447author_name = line.scan(/\[[[:space:]]*['"](.+)['"]/).flatten[-1] || ''448else449author_name = line.scan(/['"](.+)['"]/).flatten[-1] || ''450end451452if author_name =~ /^@.+$/453error("No Twitter handles, please. Try leaving it in a comment instead.")454end455456unless author_name.empty?457author_open_brackets = author_name.scan('<').size458author_close_brackets = author_name.scan('>').size459if author_open_brackets != author_close_brackets460error("Author has unbalanced brackets: #{author_name}")461end462end463end464end465end466467def check_extname468if File.extname(@name) != '.rb'469error("Module should be a '.rb' file, or it won't load.")470end471end472473def check_executable474if File.executable?(@full_filepath)475error("Module should not be executable (+x)")476end477end478479def check_old_rubies480return true unless CHECK_OLD_RUBIES481return true unless Object.const_defined? :RVM482puts "Checking syntax for #{@name}."483rubies ||= RVM.list_strings484res = %x{rvm all do ruby -c #{@full_filepath}}.split("\n").select {|msg| msg =~ /Syntax OK/}485error("Fails alternate Ruby version check") if rubies.size != res.size486end487488def is_exploit_module?489ret = false490if @source =~ REGEX_MSF_EXPLOIT491# having Msf::Exploit is good indicator, but will false positive on492# specs and other files containing the string, but not really acting493# as exploit modules, so here we check the file for some actual contents494# this could be done in a simpler way, but this let's us add more later495msf_exploit_line_no = nil496@lines.each_with_index do |line, idx|497if line =~ REGEX_MSF_EXPLOIT498# note the line number499msf_exploit_line_no = idx500elsif msf_exploit_line_no501# check there is anything but empty space between here and the next end502# something more complex could be added here503if line !~ REGEX_IS_BLANK_OR_END504# if the line is not 'end' and is not blank, prolly exploit module505ret = true506break507else508# then keep checking in case there are more than one Msf::Exploit509msf_exploit_line_no = nil510end511end512end513end514ret515end516517def check_ranking518return unless is_exploit_module?519520available_ranks = [521'ManualRanking',522'LowRanking',523'AverageRanking',524'NormalRanking',525'GoodRanking',526'GreatRanking',527'ExcellentRanking'528]529530if @source =~ /Rank \= (\w+)/531if not available_ranks.include?($1)532error("Invalid ranking. You have '#{$1}'")533end534elsif @source =~ /['"](SideEffects|Stability|Reliability)['"]\s*=/535info('No Rank, however SideEffects, Stability, or Reliability are provided')536else537warn('No Rank specified. The default is NormalRanking. Please add an explicit Rank value.')538end539end540541def check_disclosure_date542return if @source =~ /Generic Payload Handler/543544# Check disclosure date format545if @source =~ /["']DisclosureDate["'].*\=\>[\x0d\x20]*['\"](.+?)['\"]/546d = $1 #Captured date547# Flag if overall format is wrong548if d =~ /^... (?:\d{1,2},? )?\d{4}$/549# Flag if month format is wrong550m = d.split[0]551months = [552'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',553'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'554]555556error('Incorrect disclosure month format') if months.index(m).nil?557# XXX: yyyy-mm is interpreted as yyyy-01-mm by Date::iso8601558elsif d =~ /^\d{4}-\d{2}-\d{2}$/559begin560Date.iso8601(d)561rescue ArgumentError562error('Incorrect ISO 8601 disclosure date format')563end564else565error('Incorrect disclosure date format')566end567else568error('Exploit is missing a disclosure date') if is_exploit_module?569end570end571572def check_bad_terms573# "Stack overflow" vs "Stack buffer overflow" - See explanation:574# http://blogs.technet.com/b/srd/archive/2009/01/28/stack-overflow-stack-exhaustion-not-the-same-as-stack-buffer-overflow.aspx575if @module_type == 'exploits' && @source.gsub("\n", "") =~ /stack[[:space:]]+overflow/i576warn('Contains "stack overflow" You mean "stack buffer overflow"?')577elsif @module_type == 'auxiliary' && @source.gsub("\n", "") =~ /stack[[:space:]]+overflow/i578warn('Contains "stack overflow" You mean "stack exhaustion"?')579end580end581582def check_bad_super_class583# skip payloads, as they don't have a super class584return if @module_type == 'payloads'585586# get the super class in an ugly way587unless (super_class = @source.scan(/class Metasploit(?:\d|Module)\s+<\s+(\S+)/).flatten.first)588error('Unable to determine super class')589return590end591592prefix_super_map = {593'evasion' => /^Msf::Evasion$/,594'auxiliary' => /^Msf::Auxiliary$/,595'exploits' => /^Msf::Exploit(?:::Local|::Remote)?$/,596'encoders' => /^(?:Msf|Rex)::Encoder/,597'nops' => /^Msf::Nop$/,598'post' => /^Msf::Post$/599}600601if prefix_super_map.key?(@module_type)602unless super_class =~ prefix_super_map[@module_type]603error("Invalid super class for #{@module_type} module (found '#{super_class}', expected something like #{prefix_super_map[@module_type]}")604end605else606warn("Unexpected and potentially incorrect super class found ('#{super_class}')")607end608end609610def check_function_basics611functions = @source.scan(/def (\w+)\(*(.+)\)*/)612613functions.each do |func_name, args|614# Check argument length615args_length = args.split(",").length616warn("Poorly designed argument list in '#{func_name}()'. Try a hash.") if args_length > 6617end618end619620def check_bad_class_name621if @source =~ /^\s*class (Metasploit\d+)\s*</622warn("Please use 'MetasploitModule' as the class name (you used #{Regexp.last_match(1)})")623end624end625626def check_lines627url_ok = true628no_stdio = true629in_comment = false630in_literal = false631in_heredoc = false632src_ended = false633idx = 0634635@lines.each do |ln|636idx += 1637638# block comment awareness639if ln =~ /^=end$/640in_comment = false641next642end643in_comment = true if ln =~ /^=begin$/644next if in_comment645646# block string awareness (ignore indentation in these)647in_literal = false if ln =~ /^EOS$/648next if in_literal649in_literal = true if ln =~ /\<\<-EOS$/650651# heredoc string awareness (ignore indentation in these)652if in_heredoc653in_heredoc = false if ln =~ /\s#{in_heredoc}$/654next655end656if ln =~ /\<\<\~([A-Z]+)$/657in_heredoc = $1658end659660# ignore stuff after an __END__ line661src_ended = true if ln =~ /^__END__$/662next if src_ended663664if ln =~ /[ \t]$/665warn("Spaces at EOL", idx)666end667668# Check for mixed tab/spaces. Upgrade this to an error() soon.669if (ln.length > 1) and (ln =~ /^([\t ]*)/) and ($1.match(/\x20\x09|\x09\x20/))670warn("Space-Tab mixed indent: #{ln.inspect}", idx)671end672673# Check for tabs. Upgrade this to an error() soon.674if (ln.length > 1) and (ln =~ /^\x09/)675warn("Tabbed indent: #{ln.inspect}", idx)676end677678if ln =~ /\r$/679warn("Carriage return EOL", idx)680end681682url_ok = false if ln =~ /\.com\/projects\/Framework/683if ln =~ /File\.open/ and ln =~ /[\"\'][arw]/684if not ln =~ /[\"\'][wra]\+?b\+?[\"\']/685warn("File.open without binary mode", idx)686end687end688689if ln =~/^[ \t]*load[ \t]+[\x22\x27]/690error("Loading (not requiring) a file: #{ln.inspect}", idx)691end692693# The rest of these only count if it's not a comment line694next if ln =~ /^[[:space:]]*#/695696if ln =~ /\$std(?:out|err)/i or ln =~ /[[:space:]]puts/697next if ln =~ /["'][^"']*\$std(?:out|err)[^"']*["']/698no_stdio = false699error("Writes to stdout", idx)700end701702# do not read Set-Cookie header (ignore commented lines)703if ln =~ /^(?!\s*#).+\[['"]Set-Cookie['"]\](?!\s*=[^=~]+)/i704warn("Do not read Set-Cookie header directly, use res.get_cookies instead: #{ln}", idx)705end706707# Auxiliary modules do not have a rank attribute708if ln =~ /^\s*Rank\s*=\s*/ && @module_type == 'auxiliary'709warn("Auxiliary modules have no 'Rank': #{ln}", idx)710end711712if ln =~ /^\s*def\s+(?:[^\(\)#]*[A-Z]+[^\(\)]*)(?:\(.*\))?$/713warn("Please use snake case on method names: #{ln}", idx)714end715716if ln =~ /^\s*fail_with\(/717unless ln =~ /^\s*fail_with\(.*Failure\:\:(?:None|Unknown|Unreachable|BadConfig|Disconnected|NotFound|UnexpectedReply|TimeoutExpired|UserInterrupt|NoAccess|NoTarget|NotVulnerable|PayloadFailed),/718error("fail_with requires a valid Failure:: reason as first parameter: #{ln}", idx)719end720end721722if ln =~ /['"]ExitFunction['"]\s*=>/723warn("Please use EXITFUNC instead of ExitFunction #{ln}", idx)724fixed(line.gsub('ExitFunction', 'EXITFUNC'), idx)725end726727# Output from Base64.encode64 method contains '\n' new lines728# for line wrapping and string termination729if ln =~ /Base64\.encode64/730info("Please use Base64.strict_encode64 instead of Base64.encode64")731end732end733end734735def check_vuln_codes736checkcode = @source.scan(/(Exploit::)?CheckCode::(\w+)/).flatten[1]737if checkcode and checkcode !~ /^Unknown|Safe|Detected|Appears|Vulnerable|Unsupported$/738error("Unrecognized checkcode: #{checkcode}")739end740end741742def check_vars_get743test = @source.scan(/send_request_cgi\s*\(?\s*\{?\s*['"]uri['"]\s*=>\s*[^=})]*?\?[^,})]+/im)744unless test.empty?745test.each { |item|746warn("Please use vars_get in send_request_cgi: #{item}")747}748end749end750751def check_newline_eof752if @source !~ /(?:\r\n|\n)\z/m753warn('Please add a newline at the end of the file')754end755end756757def check_udp_sock_get758if @source =~ /udp_sock\.get/m && @source !~ /udp_sock\.get\([a-zA-Z0-9]+/759warn('Please specify a timeout to udp_sock.get')760end761end762763# At one point in time, somebody committed a module with a bad metasploit.com URL764# in the header -- http//metasploit.com/download rather than https://metasploit.com/download.765# This module then got copied and committed 20+ times and is used in numerous other places.766# This ensures that this stops.767def check_invalid_url_scheme768test = @source.scan(/^#.+https?\/\/(?:www\.)?metasploit.com/)769unless test.empty?770test.each { |item|771warn("Invalid URL: #{item}")772}773end774end775776# Check for (v)print_debug usage, since it doesn't exist anymore777#778# @see https://github.com/rapid7/metasploit-framework/issues/3816779def check_print_debug780if @source =~ /print_debug/781error('Please don\'t use (v)print_debug, use vprint_(status|good|error|warning) instead')782end783end784785# Check for modules registering the DEBUG datastore option786#787# @see https://github.com/rapid7/metasploit-framework/issues/3816788def check_register_datastore_debug789if @source =~ /Opt.*\.new\(["'](?i)DEBUG(?-i)["']/790error('Please don\'t register a DEBUG datastore option, it has an special meaning and is used for development')791end792end793794# Check for modules using the DEBUG datastore option795#796# @see https://github.com/rapid7/metasploit-framework/issues/3816797def check_use_datastore_debug798if @source =~ /datastore\[["'](?i)DEBUG(?-i)["']\]/799error('Please don\'t use the DEBUG datastore option in production, it has an special meaning and is used for development')800end801end802803# Check for modules using the deprecated architectures804#805# @see https://github.com/rapid7/metasploit-framework/pull/7507806def check_arch807if @source =~ /ARCH_X86_64/808error('Please don\'t use the ARCH_X86_64 architecture, use ARCH_X64 instead')809end810end811812# Check for modules having an Author section to ensure attribution813#814def check_author815# Only the three common module types have a consistently defined info hash816return unless %w[exploits auxiliary post].include?(@module_type)817818unless @source =~ /["']Author["'][[:space:]]*=>/819error('Missing "Author" info, please add')820end821end822823# Check for modules specifying a description824#825def check_description826# Payloads do not require a description827return if @module_type == 'payloads'828829unless @source =~ /["']Description["'][[:space:]]*=>/830error('Missing "Description" info, please add')831end832end833834# Check for exploit modules specifying notes835#836def check_notes837# Only exploits require notes838return unless @module_type == 'exploits'839840unless @source =~ /["']Notes["'][[:space:]]*=>/841# This should be updated to warning eventually842info('Missing "Notes" info, please add')843end844end845846#847# Run all the msftidy checks.848#849def run_checks850check_shebang851check_nokogiri852check_rubygems853check_msf_core854check_ref_identifiers855check_self_class856check_old_keywords857check_verbose_option858check_badchars859check_extname860check_executable861check_old_rubies862check_ranking863check_disclosure_date864check_bad_terms865check_bad_super_class866check_bad_class_name867check_function_basics868check_lines869check_snake_case_filename870check_comment_splat871check_vuln_codes872check_vars_get873check_newline_eof874check_udp_sock_get875check_invalid_url_scheme876check_print_debug877check_register_datastore_debug878check_use_datastore_debug879check_arch880check_author881check_description882check_notes883end884885private886887def load_file(file)888f = File.open(file, 'rb')889@stat = f.stat890buf = f.read(@stat.size)891f.close892return buf893end894895def cleanup_text(txt)896# remove line breaks897txt = txt.gsub(/[\r\n]/, ' ')898# replace multiple spaces by one space899txt.gsub(/\s{2,}/, ' ')900end901end902903class Msftidy904def run(dirs, options = {})905@exit_status = 0906907rubocop_runner = RuboCopRunner.new908dirs.each do |dir|909begin910Find.find(dir) do |full_filepath|911next if full_filepath =~ /\.git[\x5c\x2f]/912next unless File.file? full_filepath913next unless File.extname(full_filepath) == '.rb'914915msftidy_runner = MsftidyRunner.new(full_filepath)916# Executable files are now assumed to be external modules917# but also check for some content to be sure918next if File.executable?(full_filepath) && msftidy_runner.source =~ /require ["']metasploit["']/919920msftidy_runner.run_checks921@exit_status = msftidy_runner.status if (msftidy_runner.status > @exit_status.to_i)922923rubocop_result = rubocop_runner.run(full_filepath, options)924@exit_status = MsftidyRunner::ERROR if rubocop_result != RuboCop::CLI::STATUS_SUCCESS925end926rescue Errno::ENOENT927$stderr.puts "#{File.basename(__FILE__)}: #{dir}: No such file or directory"928end929end930931@exit_status.to_i932end933end934935##936#937# Main program938#939##940941if __FILE__ == $PROGRAM_NAME942options = {}943options_parser = OptionParser.new do |opts|944opts.banner = "Usage: #{File.basename(__FILE__)} <directory or file>"945946opts.on '-h', '--help', 'Help banner.' do947return print(opts.help)948end949950opts.on('-a', '--auto-correct', 'Auto-correct offenses (only when safe).') do |auto_correct|951options[:auto_correct] = auto_correct952end953954opts.on('-A', '--auto-correct-all', 'Auto-correct offenses (safe and unsafe).') do |auto_correct_all|955options[:auto_correct_all] = auto_correct_all956end957end958options_parser.parse!959960dirs = ARGV961962if dirs.length < 1963$stderr.puts options_parser.help964@exit_status = 1965exit(@exit_status)966end967968msftidy = Msftidy.new969exit_status = msftidy.run(dirs, options)970exit(exit_status)971end972973974