msftidy.rb 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  1. #!/usr/bin/env ruby
  2. # -*- coding: binary -*-
  3. #
  4. # Check (recursively) for style compliance violations and other
  5. # tree inconsistencies.
  6. #
  7. # by jduck, todb, and friends
  8. #
  9. require 'fileutils'
  10. require 'find'
  11. require 'time'
  12. require 'rubocop'
  13. require 'open3'
  14. require 'optparse'
  15. CHECK_OLD_RUBIES = !!ENV['MSF_CHECK_OLD_RUBIES']
  16. SUPPRESS_INFO_MESSAGES = !!ENV['MSF_SUPPRESS_INFO_MESSAGES']
  17. if CHECK_OLD_RUBIES
  18. require 'rvm'
  19. warn "This is going to take a while, depending on the number of Rubies you have installed."
  20. end
  21. class String
  22. def red
  23. "\e[1;31;40m#{self}\e[0m"
  24. end
  25. def yellow
  26. "\e[1;33;40m#{self}\e[0m"
  27. end
  28. def green
  29. "\e[1;32;40m#{self}\e[0m"
  30. end
  31. def cyan
  32. "\e[1;36;40m#{self}\e[0m"
  33. end
  34. end
  35. class RuboCopRunnerException < StandardError; end
  36. # Wrapper around RuboCop that requires modules to be linted
  37. # In the future this class may have the responsibility of ensuring core library files are linted
  38. class RuboCopRunner
  39. ##
  40. # Run Rubocop on the given file
  41. #
  42. # @param [String] full_filepath
  43. # @param [Hash] options specifying autocorrect functionality
  44. # @return [Integer] RuboCop::CLI status code
  45. def run(full_filepath, options = {})
  46. unless requires_rubocop?(full_filepath)
  47. return RuboCop::CLI::STATUS_SUCCESS
  48. end
  49. rubocop = RuboCop::CLI.new
  50. args = %w[--format simple]
  51. args << '-a' if options[:auto_correct]
  52. args << '-A' if options[:auto_correct_all]
  53. args << full_filepath
  54. rubocop_result = rubocop.run(args)
  55. if rubocop_result != RuboCop::CLI::STATUS_SUCCESS
  56. puts "#{full_filepath} - [#{'ERROR'.red}] Rubocop failed. Please run #{"rubocop -a #{full_filepath}".yellow} and verify all issues are resolved"
  57. end
  58. rubocop_result
  59. end
  60. private
  61. ##
  62. # For now any modules created after 3a046f01dae340c124dd3895e670983aef5fe0c5
  63. # will require Rubocop to be ran.
  64. #
  65. # This epoch was chosen from the landing date of the initial PR to
  66. # enforce consistent module formatting with Rubocop:
  67. #
  68. # https://github.com/rapid7/metasploit-framework/pull/12990
  69. #
  70. # @param [String] full_filepath
  71. # @return [Boolean] true if this file requires rubocop, false otherwise
  72. def requires_rubocop?(full_filepath)
  73. required_modules.include?(full_filepath)
  74. end
  75. def required_modules
  76. return @required_modules if @required_modules
  77. previously_merged_modules = new_modules_for('3a046f01dae340c124dd3895e670983aef5fe0c5..HEAD')
  78. staged_modules = new_modules_for('--cached')
  79. @required_modules = previously_merged_modules + staged_modules
  80. if @required_modules.empty?
  81. raise RuboCopRunnerException, 'Error retrieving new modules when verifying Rubocop'
  82. end
  83. @required_modules
  84. end
  85. def new_modules_for(commit)
  86. # Example output:
  87. # M modules/exploits/osx/local/vmware_bash_function_root.rb
  88. # A modules/exploits/osx/local/vmware_fusion_lpe.rb
  89. raw_diff_summary, status = ::Open3.capture2("git diff -b --name-status -l0 --summary #{commit}")
  90. if !status.success? && exception
  91. raise RuboCopRunnerException, "Command failed with status (#{status.exitstatus}): #{commit}"
  92. end
  93. diff_summary = raw_diff_summary.lines.map do |line|
  94. status, file = line.split(' ').each(&:strip)
  95. { status: status, file: file}
  96. end
  97. diff_summary.each_with_object([]) do |summary, acc|
  98. next unless summary[:status] == 'A'
  99. acc << summary[:file]
  100. end
  101. end
  102. end
  103. class MsftidyRunner
  104. # Status codes
  105. OK = 0
  106. WARNING = 1
  107. ERROR = 2
  108. # Some compiles regexes
  109. REGEX_MSF_EXPLOIT = / \< Msf::Exploit/
  110. REGEX_IS_BLANK_OR_END = /^\s*end\s*$/
  111. attr_reader :full_filepath, :source, :stat, :name, :status
  112. def initialize(source_file)
  113. @full_filepath = source_file
  114. @module_type = File.dirname(File.expand_path(@full_filepath))[/\/modules\/([^\/]+)/, 1]
  115. @source = load_file(source_file)
  116. @lines = @source.lines # returns an enumerator
  117. @status = OK
  118. @name = File.basename(source_file)
  119. end
  120. public
  121. #
  122. # Display a warning message, given some text and a number. Warnings
  123. # are usually style issues that may be okay for people who aren't core
  124. # Framework developers.
  125. #
  126. # @return status [Integer] Returns WARNINGS unless we already have an
  127. # error.
  128. def warn(txt, line=0) line_msg = (line>0) ? ":#{line}" : ''
  129. puts "#{@full_filepath}#{line_msg} - [#{'WARNING'.yellow}] #{cleanup_text(txt)}"
  130. @status = WARNING if @status < WARNING
  131. end
  132. #
  133. # Display an error message, given some text and a number. Errors
  134. # can break things or are so egregiously bad, style-wise, that they
  135. # really ought to be fixed.
  136. #
  137. # @return status [Integer] Returns ERRORS
  138. def error(txt, line=0)
  139. line_msg = (line>0) ? ":#{line}" : ''
  140. puts "#{@full_filepath}#{line_msg} - [#{'ERROR'.red}] #{cleanup_text(txt)}"
  141. @status = ERROR if @status < ERROR
  142. end
  143. # Currently unused, but some day msftidy will fix errors for you.
  144. def fixed(txt, line=0)
  145. line_msg = (line>0) ? ":#{line}" : ''
  146. puts "#{@full_filepath}#{line_msg} - [#{'FIXED'.green}] #{cleanup_text(txt)}"
  147. end
  148. #
  149. # Display an info message. Info messages do not alter the exit status.
  150. #
  151. def info(txt, line=0)
  152. return if SUPPRESS_INFO_MESSAGES
  153. line_msg = (line>0) ? ":#{line}" : ''
  154. puts "#{@full_filepath}#{line_msg} - [#{'INFO'.cyan}] #{cleanup_text(txt)}"
  155. end
  156. ##
  157. #
  158. # The functions below are actually the ones checking the source code
  159. #
  160. ##
  161. def check_shebang
  162. if @lines.first =~ /^#!/
  163. warn("Module should not have a #! line")
  164. end
  165. end
  166. # Updated this check to see if Nokogiri::XML.parse is being called
  167. # specifically. The main reason for this concern is that some versions
  168. # of libxml2 are still vulnerable to XXE attacks. REXML is safer (and
  169. # slower) since it's pure ruby. Unfortunately, there is no pure Ruby
  170. # HTML parser (except Hpricot which is abandonware) -- easy checks
  171. # can avoid Nokogiri (most modules use regex anyway), but more complex
  172. # checks tends to require Nokogiri for HTML element and value parsing.
  173. def check_nokogiri
  174. msg = "Using Nokogiri in modules can be risky, use REXML instead."
  175. has_nokogiri = false
  176. has_nokogiri_xml_parser = false
  177. @lines.each do |line|
  178. if has_nokogiri
  179. if line =~ /Nokogiri::XML\.parse/ or line =~ /Nokogiri::XML::Reader/
  180. has_nokogiri_xml_parser = true
  181. break
  182. end
  183. else
  184. has_nokogiri = line_has_require?(line, 'nokogiri')
  185. end
  186. end
  187. error(msg) if has_nokogiri_xml_parser
  188. end
  189. def check_ref_identifiers
  190. in_super = false
  191. in_refs = false
  192. in_notes = false
  193. cve_assigned = false
  194. @lines.each do |line|
  195. if !in_super and line =~ /\s+super\(/
  196. in_super = true
  197. elsif in_super and line =~ /[[:space:]]*def \w+[\(\w+\)]*/
  198. in_super = false
  199. break
  200. end
  201. if in_super and line =~ /["']References["'][[:space:]]*=>/
  202. in_refs = true
  203. elsif in_super and in_refs and line =~ /^[[:space:]]+\],*/m
  204. in_refs = false
  205. elsif in_super and line =~ /["']Notes["'][[:space:]]*=>/
  206. in_notes = true
  207. elsif in_super and in_refs and line =~ /[^#]+\[[[:space:]]*['"](.+)['"][[:space:]]*,[[:space:]]*['"](.+)['"][[:space:]]*\]/
  208. identifier = $1.strip.upcase
  209. value = $2.strip
  210. case identifier
  211. when 'CVE'
  212. cve_assigned = true
  213. warn("Invalid CVE format: '#{value}'") if value !~ /^\d{4}\-\d{4,}$/
  214. when 'BID'
  215. warn("Invalid BID format: '#{value}'") if value !~ /^\d+$/
  216. when 'MSB'
  217. warn("Invalid MSB format: '#{value}'") if value !~ /^MS\d+\-\d+$/
  218. when 'MIL'
  219. warn("milw0rm references are no longer supported.")
  220. when 'EDB'
  221. warn("Invalid EDB reference") if value !~ /^\d+$/
  222. when 'US-CERT-VU'
  223. warn("Invalid US-CERT-VU reference") if value !~ /^\d+$/
  224. when 'ZDI'
  225. warn("Invalid ZDI reference") if value !~ /^\d{2}-\d{3,4}$/
  226. when 'WPVDB'
  227. 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}?$/
  228. when 'PACKETSTORM'
  229. warn("Invalid PACKETSTORM reference") if value !~ /^\d+$/
  230. when 'URL'
  231. if value =~ /^https?:\/\/cvedetails\.com\/cve/
  232. warn("Please use 'CVE' for '#{value}'")
  233. elsif value =~ %r{^https?://cve\.mitre\.org/cgi-bin/cvename\.cgi}
  234. warn("Please use 'CVE' for '#{value}'")
  235. elsif value =~ /^https?:\/\/www\.securityfocus\.com\/bid\//
  236. warn("Please use 'BID' for '#{value}'")
  237. elsif value =~ /^https?:\/\/www\.microsoft\.com\/technet\/security\/bulletin\//
  238. warn("Please use 'MSB' for '#{value}'")
  239. elsif value =~ /^https?:\/\/www\.exploit\-db\.com\/exploits\//
  240. warn("Please use 'EDB' for '#{value}'")
  241. elsif value =~ /^https?:\/\/www\.kb\.cert\.org\/vuls\/id\//
  242. warn("Please use 'US-CERT-VU' for '#{value}'")
  243. elsif value =~ /^https?:\/\/wpvulndb\.com\/vulnerabilities\//
  244. warn("Please use 'WPVDB' for '#{value}'")
  245. elsif value =~ /^https?:\/\/wpscan\.com\/vulnerability\//
  246. warn("Please use 'WPVDB' for '#{value}'")
  247. elsif value =~ /^https?:\/\/(?:[^\.]+\.)?packetstormsecurity\.(?:com|net|org)\//
  248. warn("Please use 'PACKETSTORM' for '#{value}'")
  249. end
  250. when 'AKA'
  251. warn("Please include AKA values in the 'notes' section, rather than in 'references'.")
  252. end
  253. end
  254. # If a NOCVE reason was provided in notes, ignore the fact that the references might lack a CVE
  255. if in_super and in_notes and line =~ /^[[:space:]]+["']NOCVE["'][[:space:]]+=>[[:space:]]+\[*["'](.+)["']\]*/
  256. cve_assigned = true
  257. end
  258. end
  259. # This helps us track when CVEs aren't assigned
  260. if !cve_assigned && is_exploit_module?
  261. info('No CVE references found. Please check before you land!')
  262. end
  263. end
  264. def check_self_class
  265. in_register = false
  266. @lines.each do |line|
  267. (in_register = true) if line =~ /^\s*register_(?:advanced_)?options/
  268. (in_register = false) if line =~ /^\s*end/
  269. if in_register && line =~ /\],\s*self\.class\s*\)/
  270. warn('Explicitly using self.class in register_* is not necessary')
  271. break
  272. end
  273. end
  274. end
  275. # See if 'require "rubygems"' or equivalent is used, and
  276. # warn if so. Since Ruby 1.9 this has not been necessary and
  277. # the framework only supports 1.9+
  278. def check_rubygems
  279. @lines.each do |line|
  280. if line_has_require?(line, 'rubygems')
  281. warn("Explicitly requiring/loading rubygems is not necessary")
  282. break
  283. end
  284. end
  285. end
  286. def check_msf_core
  287. @lines.each do |line|
  288. if line_has_require?(line, 'msf/core')
  289. warn('Explicitly requiring/loading msf/core is not necessary')
  290. break
  291. end
  292. end
  293. end
  294. # Does the given line contain a require/load of the specified library?
  295. def line_has_require?(line, lib)
  296. line =~ /^\s*(require|load)\s+['"]#{lib}['"]/
  297. end
  298. # This check also enforces namespace module name reversibility
  299. def check_snake_case_filename
  300. if @name !~ /^[a-z0-9]+(?:_[a-z0-9]+)*\.rb$/
  301. warn('Filenames must be lowercase alphanumeric snake case.')
  302. end
  303. end
  304. def check_comment_splat
  305. if @source =~ /^# This file is part of the Metasploit Framework and may be subject to/
  306. warn("Module contains old license comment.")
  307. end
  308. if @source =~ /^# This module requires Metasploit: http:/
  309. warn("Module license comment link does not use https:// URL scheme.")
  310. fixed('# This module requires Metasploit: https://metasploit.com/download', 1)
  311. end
  312. end
  313. def check_old_keywords
  314. max_count = 10
  315. counter = 0
  316. if @source =~ /^##/
  317. @lines.each do |line|
  318. # If exists, the $Id$ keyword should appear at the top of the code.
  319. # If not (within the first 10 lines), then we assume there's no
  320. # $Id$, and then bail.
  321. break if counter >= max_count
  322. if line =~ /^#[[:space:]]*\$Id\$/i
  323. warn("Keyword $Id$ is no longer needed.")
  324. break
  325. end
  326. counter += 1
  327. end
  328. end
  329. if @source =~ /["']Version["'][[:space:]]*=>[[:space:]]*['"]\$Revision\$['"]/
  330. warn("Keyword $Revision$ is no longer needed.")
  331. end
  332. end
  333. def check_verbose_option
  334. if @source =~ /Opt(Bool|String).new\([[:space:]]*('|")VERBOSE('|")[[:space:]]*,[[:space:]]*\[[[:space:]]*/
  335. warn("VERBOSE Option is already part of advanced settings, no need to add it manually.")
  336. end
  337. end
  338. def check_badchars
  339. badchars = %Q|&<=>|
  340. in_super = false
  341. in_author = false
  342. @lines.each do |line|
  343. #
  344. # Mark our "super" code block
  345. #
  346. if !in_super and line =~ /\s+super\(/
  347. in_super = true
  348. elsif in_super and line =~ /[[:space:]]*def \w+[\(\w+\)]*/
  349. in_super = false
  350. break
  351. end
  352. #
  353. # While in super() code block
  354. #
  355. if in_super and line =~ /["']Name["'][[:space:]]*=>[[:space:]]*['|"](.+)['|"]/
  356. # Now we're checking the module titlee
  357. mod_title = $1
  358. mod_title.each_char do |c|
  359. if badchars.include?(c)
  360. error("'#{c}' is a bad character in module title.")
  361. end
  362. end
  363. # Since we're looking at the module title, this line clearly cannot be
  364. # the author block, so no point to run more code below.
  365. next
  366. end
  367. # XXX: note that this is all very fragile and regularly incorrectly parses
  368. # the author
  369. #
  370. # Mark our 'Author' block
  371. #
  372. if in_super and !in_author and line =~ /["']Author["'][[:space:]]*=>/
  373. in_author = true
  374. elsif in_super and in_author and line =~ /\],*\n/ or line =~ /['"][[:print:]]*['"][[:space:]]*=>/
  375. in_author = false
  376. end
  377. #
  378. # While in 'Author' block, check for malformed authors
  379. #
  380. if in_super and in_author
  381. if line =~ /Author['"]\s*=>\s*['"](.*)['"],/
  382. author_name = Regexp.last_match(1)
  383. elsif line =~ /Author/
  384. author_name = line.scan(/\[[[:space:]]*['"](.+)['"]/).flatten[-1] || ''
  385. else
  386. author_name = line.scan(/['"](.+)['"]/).flatten[-1] || ''
  387. end
  388. if author_name =~ /^@.+$/
  389. error("No Twitter handles, please. Try leaving it in a comment instead.")
  390. end
  391. unless author_name.empty?
  392. author_open_brackets = author_name.scan('<').size
  393. author_close_brackets = author_name.scan('>').size
  394. if author_open_brackets != author_close_brackets
  395. error("Author has unbalanced brackets: #{author_name}")
  396. end
  397. end
  398. end
  399. end
  400. end
  401. def check_extname
  402. if File.extname(@name) != '.rb'
  403. error("Module should be a '.rb' file, or it won't load.")
  404. end
  405. end
  406. def check_executable
  407. if File.executable?(@full_filepath)
  408. error("Module should not be executable (+x)")
  409. end
  410. end
  411. def check_old_rubies
  412. return true unless CHECK_OLD_RUBIES
  413. return true unless Object.const_defined? :RVM
  414. puts "Checking syntax for #{@name}."
  415. rubies ||= RVM.list_strings
  416. res = %x{rvm all do ruby -c #{@full_filepath}}.split("\n").select {|msg| msg =~ /Syntax OK/}
  417. error("Fails alternate Ruby version check") if rubies.size != res.size
  418. end
  419. def is_exploit_module?
  420. ret = false
  421. if @source =~ REGEX_MSF_EXPLOIT
  422. # having Msf::Exploit is good indicator, but will false positive on
  423. # specs and other files containing the string, but not really acting
  424. # as exploit modules, so here we check the file for some actual contents
  425. # this could be done in a simpler way, but this let's us add more later
  426. msf_exploit_line_no = nil
  427. @lines.each_with_index do |line, idx|
  428. if line =~ REGEX_MSF_EXPLOIT
  429. # note the line number
  430. msf_exploit_line_no = idx
  431. elsif msf_exploit_line_no
  432. # check there is anything but empty space between here and the next end
  433. # something more complex could be added here
  434. if line !~ REGEX_IS_BLANK_OR_END
  435. # if the line is not 'end' and is not blank, prolly exploit module
  436. ret = true
  437. break
  438. else
  439. # then keep checking in case there are more than one Msf::Exploit
  440. msf_exploit_line_no = nil
  441. end
  442. end
  443. end
  444. end
  445. ret
  446. end
  447. def check_ranking
  448. return unless is_exploit_module?
  449. available_ranks = [
  450. 'ManualRanking',
  451. 'LowRanking',
  452. 'AverageRanking',
  453. 'NormalRanking',
  454. 'GoodRanking',
  455. 'GreatRanking',
  456. 'ExcellentRanking'
  457. ]
  458. if @source =~ /Rank \= (\w+)/
  459. if not available_ranks.include?($1)
  460. error("Invalid ranking. You have '#{$1}'")
  461. end
  462. elsif @source =~ /['"](SideEffects|Stability|Reliability)['"]\s*=/
  463. info('No Rank, however SideEffects, Stability, or Reliability are provided')
  464. else
  465. warn('No Rank specified. The default is NormalRanking. Please add an explicit Rank value.')
  466. end
  467. end
  468. def check_disclosure_date
  469. return if @source =~ /Generic Payload Handler/
  470. # Check disclosure date format
  471. if @source =~ /["']DisclosureDate["'].*\=\>[\x0d\x20]*['\"](.+?)['\"]/
  472. d = $1 #Captured date
  473. # Flag if overall format is wrong
  474. if d =~ /^... (?:\d{1,2},? )?\d{4}$/
  475. # Flag if month format is wrong
  476. m = d.split[0]
  477. months = [
  478. 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
  479. 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
  480. ]
  481. error('Incorrect disclosure month format') if months.index(m).nil?
  482. # XXX: yyyy-mm is interpreted as yyyy-01-mm by Date::iso8601
  483. elsif d =~ /^\d{4}-\d{2}-\d{2}$/
  484. begin
  485. Date.iso8601(d)
  486. rescue ArgumentError
  487. error('Incorrect ISO 8601 disclosure date format')
  488. end
  489. else
  490. error('Incorrect disclosure date format')
  491. end
  492. else
  493. error('Exploit is missing a disclosure date') if is_exploit_module?
  494. end
  495. end
  496. def check_bad_terms
  497. # "Stack overflow" vs "Stack buffer overflow" - See explanation:
  498. # http://blogs.technet.com/b/srd/archive/2009/01/28/stack-overflow-stack-exhaustion-not-the-same-as-stack-buffer-overflow.aspx
  499. if @module_type == 'exploits' && @source.gsub("\n", "") =~ /stack[[:space:]]+overflow/i
  500. warn('Contains "stack overflow" You mean "stack buffer overflow"?')
  501. elsif @module_type == 'auxiliary' && @source.gsub("\n", "") =~ /stack[[:space:]]+overflow/i
  502. warn('Contains "stack overflow" You mean "stack exhaustion"?')
  503. end
  504. end
  505. def check_bad_super_class
  506. # skip payloads, as they don't have a super class
  507. return if @module_type == 'payloads'
  508. # get the super class in an ugly way
  509. unless (super_class = @source.scan(/class Metasploit(?:\d|Module)\s+<\s+(\S+)/).flatten.first)
  510. error('Unable to determine super class')
  511. return
  512. end
  513. prefix_super_map = {
  514. 'evasion' => /^Msf::Evasion$/,
  515. 'auxiliary' => /^Msf::Auxiliary$/,
  516. 'exploits' => /^Msf::Exploit(?:::Local|::Remote)?$/,
  517. 'encoders' => /^(?:Msf|Rex)::Encoder/,
  518. 'nops' => /^Msf::Nop$/,
  519. 'post' => /^Msf::Post$/
  520. }
  521. if prefix_super_map.key?(@module_type)
  522. unless super_class =~ prefix_super_map[@module_type]
  523. error("Invalid super class for #{@module_type} module (found '#{super_class}', expected something like #{prefix_super_map[@module_type]}")
  524. end
  525. else
  526. warn("Unexpected and potentially incorrect super class found ('#{super_class}')")
  527. end
  528. end
  529. def check_function_basics
  530. functions = @source.scan(/def (\w+)\(*(.+)\)*/)
  531. functions.each do |func_name, args|
  532. # Check argument length
  533. args_length = args.split(",").length
  534. warn("Poorly designed argument list in '#{func_name}()'. Try a hash.") if args_length > 6
  535. end
  536. end
  537. def check_bad_class_name
  538. if @source =~ /^\s*class (Metasploit\d+)\s*</
  539. warn("Please use 'MetasploitModule' as the class name (you used #{Regexp.last_match(1)})")
  540. end
  541. end
  542. def check_lines
  543. url_ok = true
  544. no_stdio = true
  545. in_comment = false
  546. in_literal = false
  547. in_heredoc = false
  548. src_ended = false
  549. idx = 0
  550. @lines.each do |ln|
  551. idx += 1
  552. # block comment awareness
  553. if ln =~ /^=end$/
  554. in_comment = false
  555. next
  556. end
  557. in_comment = true if ln =~ /^=begin$/
  558. next if in_comment
  559. # block string awareness (ignore indentation in these)
  560. in_literal = false if ln =~ /^EOS$/
  561. next if in_literal
  562. in_literal = true if ln =~ /\<\<-EOS$/
  563. # heredoc string awareness (ignore indentation in these)
  564. if in_heredoc
  565. in_heredoc = false if ln =~ /\s#{in_heredoc}$/
  566. next
  567. end
  568. if ln =~ /\<\<\~([A-Z]+)$/
  569. in_heredoc = $1
  570. end
  571. # ignore stuff after an __END__ line
  572. src_ended = true if ln =~ /^__END__$/
  573. next if src_ended
  574. if ln =~ /[ \t]$/
  575. warn("Spaces at EOL", idx)
  576. end
  577. # Check for mixed tab/spaces. Upgrade this to an error() soon.
  578. if (ln.length > 1) and (ln =~ /^([\t ]*)/) and ($1.match(/\x20\x09|\x09\x20/))
  579. warn("Space-Tab mixed indent: #{ln.inspect}", idx)
  580. end
  581. # Check for tabs. Upgrade this to an error() soon.
  582. if (ln.length > 1) and (ln =~ /^\x09/)
  583. warn("Tabbed indent: #{ln.inspect}", idx)
  584. end
  585. if ln =~ /\r$/
  586. warn("Carriage return EOL", idx)
  587. end
  588. url_ok = false if ln =~ /\.com\/projects\/Framework/
  589. if ln =~ /File\.open/ and ln =~ /[\"\'][arw]/
  590. if not ln =~ /[\"\'][wra]\+?b\+?[\"\']/
  591. warn("File.open without binary mode", idx)
  592. end
  593. end
  594. if ln =~/^[ \t]*load[ \t]+[\x22\x27]/
  595. error("Loading (not requiring) a file: #{ln.inspect}", idx)
  596. end
  597. # The rest of these only count if it's not a comment line
  598. next if ln =~ /^[[:space:]]*#/
  599. if ln =~ /\$std(?:out|err)/i or ln =~ /[[:space:]]puts/
  600. next if ln =~ /["'][^"']*\$std(?:out|err)[^"']*["']/
  601. no_stdio = false
  602. error("Writes to stdout", idx)
  603. end
  604. # do not read Set-Cookie header (ignore commented lines)
  605. if ln =~ /^(?!\s*#).+\[['"]Set-Cookie['"]\](?!\s*=[^=~]+)/i
  606. warn("Do not read Set-Cookie header directly, use res.get_cookies instead: #{ln}", idx)
  607. end
  608. # Auxiliary modules do not have a rank attribute
  609. if ln =~ /^\s*Rank\s*=\s*/ && @module_type == 'auxiliary'
  610. warn("Auxiliary modules have no 'Rank': #{ln}", idx)
  611. end
  612. if ln =~ /^\s*def\s+(?:[^\(\)#]*[A-Z]+[^\(\)]*)(?:\(.*\))?$/
  613. warn("Please use snake case on method names: #{ln}", idx)
  614. end
  615. if ln =~ /^\s*fail_with\(/
  616. unless ln =~ /^\s*fail_with\(.*Failure\:\:(?:None|Unknown|Unreachable|BadConfig|Disconnected|NotFound|UnexpectedReply|TimeoutExpired|UserInterrupt|NoAccess|NoTarget|NotVulnerable|PayloadFailed),/
  617. error("fail_with requires a valid Failure:: reason as first parameter: #{ln}", idx)
  618. end
  619. end
  620. if ln =~ /['"]ExitFunction['"]\s*=>/
  621. warn("Please use EXITFUNC instead of ExitFunction #{ln}", idx)
  622. fixed(line.gsub('ExitFunction', 'EXITFUNC'), idx)
  623. end
  624. # Output from Base64.encode64 method contains '\n' new lines
  625. # for line wrapping and string termination
  626. if ln =~ /Base64\.encode64/
  627. info("Please use Base64.strict_encode64 instead of Base64.encode64")
  628. end
  629. end
  630. end
  631. def check_vuln_codes
  632. checkcode = @source.scan(/(Exploit::)?CheckCode::(\w+)/).flatten[1]
  633. if checkcode and checkcode !~ /^Unknown|Safe|Detected|Appears|Vulnerable|Unsupported$/
  634. error("Unrecognized checkcode: #{checkcode}")
  635. end
  636. end
  637. def check_vars_get
  638. test = @source.scan(/send_request_cgi\s*\(?\s*\{?\s*['"]uri['"]\s*=>\s*[^=})]*?\?[^,})]+/im)
  639. unless test.empty?
  640. test.each { |item|
  641. warn("Please use vars_get in send_request_cgi: #{item}")
  642. }
  643. end
  644. end
  645. def check_newline_eof
  646. if @source !~ /(?:\r\n|\n)\z/m
  647. warn('Please add a newline at the end of the file')
  648. end
  649. end
  650. def check_udp_sock_get
  651. if @source =~ /udp_sock\.get/m && @source !~ /udp_sock\.get\([a-zA-Z0-9]+/
  652. warn('Please specify a timeout to udp_sock.get')
  653. end
  654. end
  655. # At one point in time, somebody committed a module with a bad metasploit.com URL
  656. # in the header -- http//metasploit.com/download rather than https://metasploit.com/download.
  657. # This module then got copied and committed 20+ times and is used in numerous other places.
  658. # This ensures that this stops.
  659. def check_invalid_url_scheme
  660. test = @source.scan(/^#.+https?\/\/(?:www\.)?metasploit.com/)
  661. unless test.empty?
  662. test.each { |item|
  663. warn("Invalid URL: #{item}")
  664. }
  665. end
  666. end
  667. # Check for (v)print_debug usage, since it doesn't exist anymore
  668. #
  669. # @see https://github.com/rapid7/metasploit-framework/issues/3816
  670. def check_print_debug
  671. if @source =~ /print_debug/
  672. error('Please don\'t use (v)print_debug, use vprint_(status|good|error|warning) instead')
  673. end
  674. end
  675. # Check for modules registering the DEBUG datastore option
  676. #
  677. # @see https://github.com/rapid7/metasploit-framework/issues/3816
  678. def check_register_datastore_debug
  679. if @source =~ /Opt.*\.new\(["'](?i)DEBUG(?-i)["']/
  680. error('Please don\'t register a DEBUG datastore option, it has an special meaning and is used for development')
  681. end
  682. end
  683. # Check for modules using the DEBUG datastore option
  684. #
  685. # @see https://github.com/rapid7/metasploit-framework/issues/3816
  686. def check_use_datastore_debug
  687. if @source =~ /datastore\[["'](?i)DEBUG(?-i)["']\]/
  688. error('Please don\'t use the DEBUG datastore option in production, it has an special meaning and is used for development')
  689. end
  690. end
  691. # Check for modules using the deprecated architectures
  692. #
  693. # @see https://github.com/rapid7/metasploit-framework/pull/7507
  694. def check_arch
  695. if @source =~ /ARCH_X86_64/
  696. error('Please don\'t use the ARCH_X86_64 architecture, use ARCH_X64 instead')
  697. end
  698. end
  699. # Check for modules having an Author section to ensure attribution
  700. #
  701. def check_author
  702. # Only the three common module types have a consistently defined info hash
  703. return unless %w[exploits auxiliary post].include?(@module_type)
  704. unless @source =~ /["']Author["'][[:space:]]*=>/
  705. error('Missing "Author" info, please add')
  706. end
  707. end
  708. # Check for modules specifying a description
  709. #
  710. def check_description
  711. # Payloads do not require a description
  712. return if @module_type == 'payloads'
  713. unless @source =~ /["']Description["'][[:space:]]*=>/
  714. error('Missing "Description" info, please add')
  715. end
  716. end
  717. # Check for exploit modules specifying notes
  718. #
  719. def check_notes
  720. # Only exploits require notes
  721. return unless @module_type == 'exploits'
  722. unless @source =~ /["']Notes["'][[:space:]]*=>/
  723. # This should be updated to warning eventually
  724. info('Missing "Notes" info, please add')
  725. end
  726. end
  727. #
  728. # Run all the msftidy checks.
  729. #
  730. def run_checks
  731. check_shebang
  732. check_nokogiri
  733. check_rubygems
  734. check_msf_core
  735. check_ref_identifiers
  736. check_self_class
  737. check_old_keywords
  738. check_verbose_option
  739. check_badchars
  740. check_extname
  741. check_executable
  742. check_old_rubies
  743. check_ranking
  744. check_disclosure_date
  745. check_bad_terms
  746. check_bad_super_class
  747. check_bad_class_name
  748. check_function_basics
  749. check_lines
  750. check_snake_case_filename
  751. check_comment_splat
  752. check_vuln_codes
  753. check_vars_get
  754. check_newline_eof
  755. check_udp_sock_get
  756. check_invalid_url_scheme
  757. check_print_debug
  758. check_register_datastore_debug
  759. check_use_datastore_debug
  760. check_arch
  761. check_author
  762. check_description
  763. check_notes
  764. end
  765. private
  766. def load_file(file)
  767. f = File.open(file, 'rb')
  768. @stat = f.stat
  769. buf = f.read(@stat.size)
  770. f.close
  771. return buf
  772. end
  773. def cleanup_text(txt)
  774. # remove line breaks
  775. txt = txt.gsub(/[\r\n]/, ' ')
  776. # replace multiple spaces by one space
  777. txt.gsub(/\s{2,}/, ' ')
  778. end
  779. end
  780. class Msftidy
  781. def run(dirs, options = {})
  782. @exit_status = 0
  783. rubocop_runner = RuboCopRunner.new
  784. dirs.each do |dir|
  785. begin
  786. Find.find(dir) do |full_filepath|
  787. next if full_filepath =~ /\.git[\x5c\x2f]/
  788. next unless File.file? full_filepath
  789. next unless File.extname(full_filepath) == '.rb'
  790. msftidy_runner = MsftidyRunner.new(full_filepath)
  791. # Executable files are now assumed to be external modules
  792. # but also check for some content to be sure
  793. next if File.executable?(full_filepath) && msftidy_runner.source =~ /require ["']metasploit["']/
  794. msftidy_runner.run_checks
  795. @exit_status = msftidy_runner.status if (msftidy_runner.status > @exit_status.to_i)
  796. rubocop_result = rubocop_runner.run(full_filepath, options)
  797. @exit_status = MsftidyRunner::ERROR if rubocop_result != RuboCop::CLI::STATUS_SUCCESS
  798. end
  799. rescue Errno::ENOENT
  800. $stderr.puts "#{File.basename(__FILE__)}: #{dir}: No such file or directory"
  801. end
  802. end
  803. @exit_status.to_i
  804. end
  805. end
  806. ##
  807. #
  808. # Main program
  809. #
  810. ##
  811. if __FILE__ == $PROGRAM_NAME
  812. options = {}
  813. options_parser = OptionParser.new do |opts|
  814. opts.banner = "Usage: #{File.basename(__FILE__)} <directory or file>"
  815. opts.on '-h', '--help', 'Help banner.' do
  816. return print(opts.help)
  817. end
  818. opts.on('-a', '--auto-correct', 'Auto-correct offenses (only when safe).') do |auto_correct|
  819. options[:auto_correct] = auto_correct
  820. end
  821. opts.on('-A', '--auto-correct-all', 'Auto-correct offenses (safe and unsafe).') do |auto_correct_all|
  822. options[:auto_correct_all] = auto_correct_all
  823. end
  824. end
  825. options_parser.parse!
  826. dirs = ARGV
  827. if dirs.length < 1
  828. $stderr.puts options_parser.help
  829. @exit_status = 1
  830. exit(@exit_status)
  831. end
  832. msftidy = Msftidy.new
  833. exit_status = msftidy.run(dirs, options)
  834. exit(exit_status)
  835. end