CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

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