2010-11-05 00:05:34 +00:00
|
|
|
#!/usr/bin/env ruby
|
2013-10-16 15:40:42 +00:00
|
|
|
# -*- coding: binary -*-
|
2010-11-05 00:05:34 +00:00
|
|
|
#
|
|
|
|
# Check (recursively) for style compliance violations and other
|
|
|
|
# tree inconsistencies.
|
|
|
|
#
|
2014-04-02 16:12:00 +00:00
|
|
|
# by jduck, todb, and friends
|
2010-11-05 00:05:34 +00:00
|
|
|
#
|
2013-10-16 15:40:42 +00:00
|
|
|
require 'fileutils'
|
|
|
|
require 'find'
|
2014-01-31 20:19:04 +00:00
|
|
|
require 'time'
|
2010-11-05 00:05:34 +00:00
|
|
|
|
2011-12-12 21:12:01 +00:00
|
|
|
CHECK_OLD_RUBIES = !!ENV['MSF_CHECK_OLD_RUBIES']
|
2014-06-12 18:46:10 +00:00
|
|
|
SUPPRESS_INFO_MESSAGES = !!ENV['MSF_SUPPRESS_INFO_MESSAGES']
|
2011-12-12 21:12:01 +00:00
|
|
|
|
|
|
|
if CHECK_OLD_RUBIES
|
2013-09-30 18:47:53 +00:00
|
|
|
require 'rvm'
|
|
|
|
warn "This is going to take a while, depending on the number of Rubies you have installed."
|
2011-12-12 21:12:01 +00:00
|
|
|
end
|
2011-10-16 15:53:19 +00:00
|
|
|
|
2012-10-12 07:55:16 +00:00
|
|
|
class String
|
2013-09-30 18:47:53 +00:00
|
|
|
def red
|
|
|
|
"\e[1;31;40m#{self}\e[0m"
|
|
|
|
end
|
2010-11-05 00:05:34 +00:00
|
|
|
|
2013-09-30 18:47:53 +00:00
|
|
|
def yellow
|
|
|
|
"\e[1;33;40m#{self}\e[0m"
|
|
|
|
end
|
2013-01-04 20:09:37 +00:00
|
|
|
|
2013-10-16 15:40:42 +00:00
|
|
|
def green
|
|
|
|
"\e[1;32;40m#{self}\e[0m"
|
|
|
|
end
|
|
|
|
|
2014-04-21 16:04:14 +00:00
|
|
|
def cyan
|
|
|
|
"\e[1;36;40m#{self}\e[0m"
|
|
|
|
end
|
|
|
|
|
2013-09-30 18:47:53 +00:00
|
|
|
def ascii_only?
|
|
|
|
self =~ Regexp.new('[\x00-\x08\x0b\x0c\x0e-\x19\x7f-\xff]', nil, 'n') ? false : true
|
|
|
|
end
|
2010-11-05 00:05:34 +00:00
|
|
|
end
|
|
|
|
|
2012-10-12 07:55:16 +00:00
|
|
|
class Msftidy
|
2011-12-12 21:12:01 +00:00
|
|
|
|
2014-01-31 20:19:04 +00:00
|
|
|
# Status codes
|
|
|
|
OK = 0x00
|
|
|
|
WARNINGS = 0x10
|
|
|
|
ERRORS = 0x20
|
|
|
|
|
2014-08-26 20:30:08 +00:00
|
|
|
# Some compiles regexes
|
|
|
|
REGEX_MSF_EXPLOIT = / \< Msf::Exploit/
|
|
|
|
REGEX_IS_BLANK_OR_END = /^\s*end\s*$/
|
|
|
|
|
2014-01-31 20:19:04 +00:00
|
|
|
attr_reader :full_filepath, :source, :stat, :name, :status
|
2013-10-16 15:40:42 +00:00
|
|
|
|
2013-09-30 18:47:53 +00:00
|
|
|
def initialize(source_file)
|
2013-10-16 15:40:42 +00:00
|
|
|
@full_filepath = source_file
|
2013-09-30 18:47:53 +00:00
|
|
|
@source = load_file(source_file)
|
2014-08-26 20:30:08 +00:00
|
|
|
@lines = @source.lines # returns an enumerator
|
2014-01-31 20:19:04 +00:00
|
|
|
@status = OK
|
2013-10-16 15:40:42 +00:00
|
|
|
@name = File.basename(source_file)
|
2013-09-30 18:47:53 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
public
|
|
|
|
|
|
|
|
#
|
2014-01-31 20:19:04 +00:00
|
|
|
# Display a warning message, given some text and a number. Warnings
|
|
|
|
# are usually style issues that may be okay for people who aren't core
|
|
|
|
# Framework developers.
|
2013-09-30 18:47:53 +00:00
|
|
|
#
|
2014-01-31 20:19:04 +00:00
|
|
|
# @return status [Integer] Returns WARNINGS unless we already have an
|
|
|
|
# error.
|
|
|
|
def warn(txt, line=0) line_msg = (line>0) ? ":#{line}" : ''
|
2014-03-01 11:02:41 +00:00
|
|
|
puts "#{@full_filepath}#{line_msg} - [#{'WARNING'.yellow}] #{cleanup_text(txt)}"
|
2014-01-31 20:19:04 +00:00
|
|
|
@status == ERRORS ? @status = ERRORS : @status = WARNINGS
|
2013-09-30 18:47:53 +00:00
|
|
|
end
|
|
|
|
|
2014-01-31 20:19:04 +00:00
|
|
|
#
|
|
|
|
# Display an error message, given some text and a number. Errors
|
|
|
|
# can break things or are so egregiously bad, style-wise, that they
|
|
|
|
# really ought to be fixed.
|
|
|
|
#
|
|
|
|
# @return status [Integer] Returns ERRORS
|
2013-09-30 18:47:53 +00:00
|
|
|
def error(txt, line=0)
|
2014-01-23 20:21:48 +00:00
|
|
|
line_msg = (line>0) ? ":#{line}" : ''
|
2014-03-01 11:02:41 +00:00
|
|
|
puts "#{@full_filepath}#{line_msg} - [#{'ERROR'.red}] #{cleanup_text(txt)}"
|
2014-01-31 20:19:04 +00:00
|
|
|
@status = ERRORS
|
2013-09-30 18:47:53 +00:00
|
|
|
end
|
|
|
|
|
2014-01-31 20:19:04 +00:00
|
|
|
# Currently unused, but some day msftidy will fix errors for you.
|
2013-10-16 15:40:42 +00:00
|
|
|
def fixed(txt, line=0)
|
2014-01-23 20:21:48 +00:00
|
|
|
line_msg = (line>0) ? ":#{line}" : ''
|
2014-03-01 11:02:41 +00:00
|
|
|
puts "#{@full_filepath}#{line_msg} - [#{'FIXED'.green}] #{cleanup_text(txt)}"
|
2013-10-16 15:40:42 +00:00
|
|
|
end
|
|
|
|
|
2014-04-21 16:04:14 +00:00
|
|
|
#
|
|
|
|
# Display an info message. Info messages do not alter the exit status.
|
|
|
|
#
|
|
|
|
def info(txt, line=0)
|
2014-06-12 18:46:10 +00:00
|
|
|
return if SUPPRESS_INFO_MESSAGES
|
2014-04-21 16:04:14 +00:00
|
|
|
line_msg = (line>0) ? ":#{line}" : ''
|
|
|
|
puts "#{@full_filepath}#{line_msg} - [#{'INFO'.cyan}] #{cleanup_text(txt)}"
|
|
|
|
end
|
2013-09-30 18:47:53 +00:00
|
|
|
|
|
|
|
##
|
|
|
|
#
|
|
|
|
# The functions below are actually the ones checking the source code
|
|
|
|
#
|
|
|
|
##
|
|
|
|
|
2013-11-05 17:49:15 +00:00
|
|
|
def check_mode
|
|
|
|
unless (@stat.mode & 0111).zero?
|
|
|
|
warn("Module should not be marked executable")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-11-08 22:11:48 +00:00
|
|
|
def check_shebang
|
2014-08-26 20:30:08 +00:00
|
|
|
if @lines.first =~ /^#!/
|
2013-11-08 22:11:48 +00:00
|
|
|
warn("Module should not have a #! line")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-05-29 16:52:17 +00:00
|
|
|
# Updated this check to see if Nokogiri::XML.parse is being called
|
|
|
|
# specifically. The main reason for this concern is that some versions
|
|
|
|
# of libxml2 are still vulnerable to XXE attacks. REXML is safer (and
|
|
|
|
# slower) since it's pure ruby. Unfortunately, there is no pure Ruby
|
|
|
|
# HTML parser (except Hpricot which is abandonware) -- easy checks
|
|
|
|
# can avoid Nokogiri (most modules use regex anyway), but more complex
|
|
|
|
# checks tends to require Nokogiri for HTML element and value parsing.
|
2014-02-02 17:51:21 +00:00
|
|
|
def check_nokogiri
|
2014-05-29 16:52:17 +00:00
|
|
|
msg = "Using Nokogiri in modules can be risky, use REXML instead."
|
2014-02-02 17:51:21 +00:00
|
|
|
has_nokogiri = false
|
2014-05-29 16:52:17 +00:00
|
|
|
has_nokogiri_xml_parser = false
|
2014-08-26 20:30:08 +00:00
|
|
|
@lines.each do |line|
|
2014-05-29 18:06:47 +00:00
|
|
|
if has_nokogiri
|
|
|
|
if line =~ /Nokogiri::XML\.parse/ or line =~ /Nokogiri::XML::Reader/
|
2014-05-29 16:52:17 +00:00
|
|
|
has_nokogiri_xml_parser = true
|
|
|
|
break
|
|
|
|
end
|
2014-05-29 18:06:47 +00:00
|
|
|
else
|
2014-07-17 16:29:13 +00:00
|
|
|
has_nokogiri = line_has_require?(line, 'nokogiri')
|
2014-02-02 17:51:21 +00:00
|
|
|
end
|
|
|
|
end
|
2014-05-29 16:52:17 +00:00
|
|
|
error(msg) if has_nokogiri_xml_parser
|
2014-02-02 17:51:21 +00:00
|
|
|
end
|
|
|
|
|
2013-09-30 18:47:53 +00:00
|
|
|
def check_ref_identifiers
|
|
|
|
in_super = false
|
|
|
|
in_refs = false
|
|
|
|
|
2014-08-26 20:30:08 +00:00
|
|
|
@lines.each do |line|
|
2014-01-30 20:39:28 +00:00
|
|
|
if !in_super and line =~ /\s+super\(/
|
2013-09-30 18:47:53 +00:00
|
|
|
in_super = true
|
|
|
|
elsif in_super and line =~ /[[:space:]]*def \w+[\(\w+\)]*/
|
|
|
|
in_super = false
|
|
|
|
break
|
|
|
|
end
|
|
|
|
|
2014-01-09 02:32:30 +00:00
|
|
|
if in_super and line =~ /["']References["'][[:space:]]*=>/
|
2013-09-30 18:47:53 +00:00
|
|
|
in_refs = true
|
|
|
|
elsif in_super and in_refs and line =~ /^[[:space:]]+\],*/m
|
|
|
|
break
|
|
|
|
elsif in_super and in_refs and line =~ /[^#]+\[[[:space:]]*['"](.+)['"][[:space:]]*,[[:space:]]*['"](.+)['"][[:space:]]*\]/
|
|
|
|
identifier = $1.strip.upcase
|
|
|
|
value = $2.strip
|
|
|
|
|
|
|
|
case identifier
|
|
|
|
when 'CVE'
|
|
|
|
warn("Invalid CVE format: '#{value}'") if value !~ /^\d{4}\-\d{4}$/
|
|
|
|
when 'OSVDB'
|
|
|
|
warn("Invalid OSVDB format: '#{value}'") if value !~ /^\d+$/
|
|
|
|
when 'BID'
|
|
|
|
warn("Invalid BID format: '#{value}'") if value !~ /^\d+$/
|
|
|
|
when 'MSB'
|
|
|
|
warn("Invalid MSB format: '#{value}'") if value !~ /^MS\d+\-\d+$/
|
|
|
|
when 'MIL'
|
|
|
|
warn("milw0rm references are no longer supported.")
|
|
|
|
when 'EDB'
|
|
|
|
warn("Invalid EDB reference") if value !~ /^\d+$/
|
|
|
|
when 'WVE'
|
|
|
|
warn("Invalid WVE reference") if value !~ /^\d+\-\d+$/
|
|
|
|
when 'US-CERT-VU'
|
|
|
|
warn("Invalid US-CERT-VU reference") if value !~ /^\d+$/
|
2013-10-21 20:30:07 +00:00
|
|
|
when 'ZDI'
|
|
|
|
warn("Invalid ZDI reference") if value !~ /^\d{2}-\d{3}$/
|
2013-09-30 18:47:53 +00:00
|
|
|
when 'URL'
|
|
|
|
if value =~ /^http:\/\/www\.osvdb\.org/
|
|
|
|
warn("Please use 'OSVDB' for '#{value}'")
|
|
|
|
elsif value =~ /^http:\/\/cvedetails\.com\/cve/
|
|
|
|
warn("Please use 'CVE' for '#{value}'")
|
|
|
|
elsif value =~ /^http:\/\/www\.securityfocus\.com\/bid\//
|
|
|
|
warn("Please use 'BID' for '#{value}'")
|
|
|
|
elsif value =~ /^http:\/\/www\.microsoft\.com\/technet\/security\/bulletin\//
|
|
|
|
warn("Please use 'MSB' for '#{value}'")
|
|
|
|
elsif value =~ /^http:\/\/www\.exploit\-db\.com\/exploits\//
|
|
|
|
warn("Please use 'EDB' for '#{value}'")
|
|
|
|
elsif value =~ /^http:\/\/www\.wirelessve\.org\/entries\/show\/WVE\-/
|
|
|
|
warn("Please use 'WVE' for '#{value}'")
|
|
|
|
elsif value =~ /^http:\/\/www\.kb\.cert\.org\/vuls\/id\//
|
|
|
|
warn("Please use 'US-CERT-VU' for '#{value}'")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-07-17 16:29:13 +00:00
|
|
|
# See if 'require "rubygems"' or equivalent is used, and
|
|
|
|
# warn if so. Since Ruby 1.9 this has not been necessary and
|
|
|
|
# the framework only suports 1.9+
|
|
|
|
def check_rubygems
|
2014-08-26 20:30:08 +00:00
|
|
|
@lines.each do |line|
|
2014-07-17 16:29:13 +00:00
|
|
|
if line_has_require?(line, 'rubygems')
|
|
|
|
warn("Explicitly requiring/loading rubygems is not necessary")
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Does the given line contain a require/load of the specified library?
|
|
|
|
def line_has_require?(line, lib)
|
|
|
|
line =~ /^\s*(require|load)\s+['"]#{lib}['"]/
|
|
|
|
end
|
|
|
|
|
2013-09-30 18:47:53 +00:00
|
|
|
def check_snake_case_filename
|
|
|
|
sep = File::SEPARATOR
|
|
|
|
good_name = Regexp.new "^[a-z0-9_#{sep}]+\.rb$"
|
|
|
|
unless @name =~ good_name
|
|
|
|
warn "Filenames should be alphanum and snake case."
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-10-15 18:35:52 +00:00
|
|
|
def check_comment_splat
|
|
|
|
if @source =~ /^# This file is part of the Metasploit Framework and may be subject to/
|
2013-10-15 18:53:38 +00:00
|
|
|
warn("Module contains old license comment, use tools/dev/resplat.rb <filename>.")
|
2013-10-15 18:35:52 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-09-30 18:47:53 +00:00
|
|
|
def check_old_keywords
|
|
|
|
max_count = 10
|
|
|
|
counter = 0
|
|
|
|
if @source =~ /^##/
|
2014-08-26 20:30:08 +00:00
|
|
|
@lines.each do |line|
|
2013-09-30 18:47:53 +00:00
|
|
|
# If exists, the $Id$ keyword should appear at the top of the code.
|
|
|
|
# If not (within the first 10 lines), then we assume there's no
|
|
|
|
# $Id$, and then bail.
|
|
|
|
break if counter >= max_count
|
|
|
|
|
|
|
|
if line =~ /^#[[:space:]]*\$Id\$/i
|
|
|
|
warn("Keyword $Id$ is no longer needed.")
|
|
|
|
break
|
|
|
|
end
|
|
|
|
|
|
|
|
counter += 1
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-01-09 02:32:30 +00:00
|
|
|
if @source =~ /["']Version["'][[:space:]]*=>[[:space:]]*['"]\$Revision\$['"]/
|
2013-09-30 18:47:53 +00:00
|
|
|
warn("Keyword $Revision$ is no longer needed.")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_verbose_option
|
|
|
|
if @source =~ /Opt(Bool|String).new\([[:space:]]*('|")VERBOSE('|")[[:space:]]*,[[:space:]]*\[[[:space:]]*/
|
|
|
|
warn("VERBOSE Option is already part of advanced settings, no need to add it manually.")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_badchars
|
|
|
|
badchars = %Q|&<=>|
|
|
|
|
|
|
|
|
in_super = false
|
|
|
|
in_author = false
|
|
|
|
|
2014-08-26 20:30:08 +00:00
|
|
|
@lines.each do |line|
|
2013-09-30 18:47:53 +00:00
|
|
|
#
|
|
|
|
# Mark our "super" code block
|
|
|
|
#
|
2014-01-30 20:39:28 +00:00
|
|
|
if !in_super and line =~ /\s+super\(/
|
2013-09-30 18:47:53 +00:00
|
|
|
in_super = true
|
|
|
|
elsif in_super and line =~ /[[:space:]]*def \w+[\(\w+\)]*/
|
|
|
|
in_super = false
|
|
|
|
break
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# While in super() code block
|
|
|
|
#
|
2014-01-09 02:32:30 +00:00
|
|
|
if in_super and line =~ /["']Name["'][[:space:]]*=>[[:space:]]*['|"](.+)['|"]/
|
2013-09-30 18:47:53 +00:00
|
|
|
# Now we're checking the module titlee
|
|
|
|
mod_title = $1
|
|
|
|
mod_title.each_char do |c|
|
|
|
|
if badchars.include?(c)
|
|
|
|
error("'#{c}' is a bad character in module title.")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if not mod_title.ascii_only?
|
|
|
|
error("Please avoid unicode or non-printable characters in module title.")
|
|
|
|
end
|
|
|
|
|
|
|
|
# Since we're looking at the module title, this line clearly cannot be
|
|
|
|
# the author block, so no point to run more code below.
|
|
|
|
next
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Mark our 'Author' block
|
|
|
|
#
|
2014-01-09 02:32:30 +00:00
|
|
|
if in_super and !in_author and line =~ /["']Author["'][[:space:]]*=>/
|
2013-09-30 18:47:53 +00:00
|
|
|
in_author = true
|
|
|
|
elsif in_super and in_author and line =~ /\],*\n/ or line =~ /['"][[:print:]]*['"][[:space:]]*=>/
|
|
|
|
in_author = false
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# While in 'Author' block, check for Twitter handles
|
|
|
|
#
|
|
|
|
if in_super and in_author
|
|
|
|
if line =~ /Author/
|
|
|
|
author_name = line.scan(/\[[[:space:]]*['"](.+)['"]/).flatten[-1] || ''
|
|
|
|
else
|
|
|
|
author_name = line.scan(/['"](.+)['"]/).flatten[-1] || ''
|
|
|
|
end
|
|
|
|
|
|
|
|
if author_name =~ /^@.+$/
|
|
|
|
error("No Twitter handles, please. Try leaving it in a comment instead.")
|
|
|
|
end
|
|
|
|
|
|
|
|
if not author_name.ascii_only?
|
|
|
|
error("Please avoid unicode or non-printable characters in Author")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_extname
|
|
|
|
if File.extname(@name) != '.rb'
|
|
|
|
error("Module should be a '.rb' file, or it won't load.")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-04-11 02:44:00 +00:00
|
|
|
def check_old_rubies
|
2013-09-30 18:47:53 +00:00
|
|
|
return true unless CHECK_OLD_RUBIES
|
|
|
|
return true unless Object.const_defined? :RVM
|
2013-10-16 15:40:42 +00:00
|
|
|
puts "Checking syntax for #{@name}."
|
2013-09-30 18:47:53 +00:00
|
|
|
rubies ||= RVM.list_strings
|
2013-10-16 15:40:42 +00:00
|
|
|
res = %x{rvm all do ruby -c #{@full_filepath}}.split("\n").select {|msg| msg =~ /Syntax OK/}
|
2013-09-30 18:47:53 +00:00
|
|
|
error("Fails alternate Ruby version check") if rubies.size != res.size
|
|
|
|
end
|
|
|
|
|
2014-08-26 20:30:08 +00:00
|
|
|
def is_exploit_module?
|
|
|
|
ret = false
|
|
|
|
if @source =~ REGEX_MSF_EXPLOIT
|
|
|
|
# having Msf::Exploit is good indicator, but will false positive on
|
|
|
|
# specs and other files containing the string, but not really acting
|
|
|
|
# as exploit modules, so here we check the file for some actual contents
|
|
|
|
# this could be done in a simpler way, but this let's us add more later
|
|
|
|
msf_exploit_line_no = nil
|
|
|
|
@lines.each_with_index do |line, idx|
|
2014-08-27 01:36:28 +00:00
|
|
|
if line =~ REGEX_MSF_EXPLOIT
|
2014-08-26 20:30:08 +00:00
|
|
|
# note the line number
|
|
|
|
msf_exploit_line_no = idx
|
|
|
|
elsif msf_exploit_line_no
|
|
|
|
# check there is anything but empty space between here and the next end
|
|
|
|
# something more complex could be added here
|
|
|
|
if line !~ REGEX_IS_BLANK_OR_END
|
|
|
|
# if the line is not 'end' and is not blank, prolly exploit module
|
|
|
|
ret = true
|
|
|
|
break
|
|
|
|
else
|
|
|
|
# then keep checking in case there are more than one Msf::Exploit
|
|
|
|
msf_exploit_line_no = nil
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
ret
|
|
|
|
end
|
|
|
|
|
2013-09-30 18:47:53 +00:00
|
|
|
def check_ranking
|
2014-08-26 20:30:08 +00:00
|
|
|
return unless is_exploit_module?
|
2013-09-30 18:47:53 +00:00
|
|
|
|
|
|
|
available_ranks = [
|
|
|
|
'ManualRanking',
|
|
|
|
'LowRanking',
|
|
|
|
'AverageRanking',
|
|
|
|
'NormalRanking',
|
|
|
|
'GoodRanking',
|
|
|
|
'GreatRanking',
|
|
|
|
'ExcellentRanking'
|
|
|
|
]
|
|
|
|
|
|
|
|
if @source =~ /Rank \= (\w+)/
|
|
|
|
if not available_ranks.include?($1)
|
|
|
|
error("Invalid ranking. You have '#{$1}'")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_disclosure_date
|
2014-04-07 19:21:04 +00:00
|
|
|
return if @source =~ /Generic Payload Handler/
|
2013-09-30 18:47:53 +00:00
|
|
|
|
|
|
|
# Check disclosure date format
|
2014-01-09 02:32:30 +00:00
|
|
|
if @source =~ /["']DisclosureDate["'].*\=\>[\x0d\x20]*['\"](.+)['\"]/
|
2013-09-30 18:47:53 +00:00
|
|
|
d = $1 #Captured date
|
|
|
|
# Flag if overall format is wrong
|
|
|
|
if d =~ /^... \d{1,2}\,* \d{4}/
|
|
|
|
# Flag if month format is wrong
|
|
|
|
m = d.split[0]
|
|
|
|
months = [
|
|
|
|
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
|
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
|
|
|
]
|
|
|
|
|
|
|
|
error('Incorrect disclosure month format') if months.index(m).nil?
|
|
|
|
else
|
|
|
|
error('Incorrect disclosure date format')
|
|
|
|
end
|
|
|
|
else
|
2014-08-26 20:30:08 +00:00
|
|
|
error('Exploit is missing a disclosure date') if is_exploit_module?
|
2013-09-30 18:47:53 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_title_casing
|
2014-01-21 15:26:16 +00:00
|
|
|
whitelist = %w{
|
2014-04-03 21:54:47 +00:00
|
|
|
a an and as at avserve callmenum configdir connect debug docbase dtspcd
|
|
|
|
execve file for from getinfo goaway gsad hetro historysearch htpasswd
|
|
|
|
ibstat id in inetd iseemedia jhot libxslt lmgrd lnk load main map
|
2014-01-21 15:26:16 +00:00
|
|
|
migrate mimencode multisort name net netcat nodeid ntpd nttrans of
|
|
|
|
on onreadystatechange or ovutil path pbot pfilez pgpass pingstr pls
|
|
|
|
popsubfolders prescan readvar relfile rev rexec rlogin rsh rsyslog sa
|
|
|
|
sadmind say sblistpack spamd sreplace tagprinter the to twikidraw udev
|
|
|
|
uplay user username via welcome with ypupdated zsudo
|
|
|
|
}
|
|
|
|
|
2014-01-09 02:32:30 +00:00
|
|
|
if @source =~ /["']Name["'][[:space:]]*=>[[:space:]]*['"](.+)['"],*$/
|
2013-09-30 18:47:53 +00:00
|
|
|
words = $1.split
|
|
|
|
words.each do |word|
|
2014-01-21 15:26:16 +00:00
|
|
|
if whitelist.include?(word)
|
2013-09-30 18:47:53 +00:00
|
|
|
next
|
|
|
|
elsif word =~ /^[a-z]+$/
|
|
|
|
warn("Suspect capitalization in module title: '#{word}'")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_bad_terms
|
|
|
|
# "Stack overflow" vs "Stack buffer overflow" - See explanation:
|
|
|
|
# http://blogs.technet.com/b/srd/archive/2009/01/28/stack-overflow-stack-exhaustion-not-the-same-as-stack-buffer-overflow.aspx
|
|
|
|
if @source =~ /class Metasploit\d < Msf::Exploit::Remote/ and @source.gsub("\n", "") =~ /stack[[:space:]]+overflow/i
|
|
|
|
warn('Contains "stack overflow" You mean "stack buffer overflow"?')
|
|
|
|
elsif @source =~ /class Metasploit\d < Msf::Auxiliary/ and @source.gsub("\n", "") =~ /stack[[:space:]]+overflow/i
|
|
|
|
warn('Contains "stack overflow" You mean "stack exhaustion"?')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_function_basics
|
|
|
|
functions = @source.scan(/def (\w+)\(*(.+)\)*/)
|
|
|
|
|
|
|
|
functions.each do |func_name, args|
|
|
|
|
# Check argument length
|
|
|
|
args_length = args.split(",").length
|
|
|
|
warn("Poorly designed argument list in '#{func_name}()'. Try a hash.") if args_length > 6
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_lines
|
|
|
|
url_ok = true
|
|
|
|
no_stdio = true
|
|
|
|
in_comment = false
|
|
|
|
in_literal = false
|
|
|
|
src_ended = false
|
|
|
|
idx = 0
|
|
|
|
|
2014-08-26 20:30:08 +00:00
|
|
|
@lines.each do |ln|
|
2013-09-30 18:47:53 +00:00
|
|
|
idx += 1
|
|
|
|
|
|
|
|
# block comment awareness
|
|
|
|
if ln =~ /^=end$/
|
|
|
|
in_comment = false
|
|
|
|
next
|
|
|
|
end
|
|
|
|
in_comment = true if ln =~ /^=begin$/
|
|
|
|
next if in_comment
|
|
|
|
|
|
|
|
# block string awareness (ignore indentation in these)
|
|
|
|
in_literal = false if ln =~ /^EOS$/
|
|
|
|
next if in_literal
|
|
|
|
in_literal = true if ln =~ /\<\<-EOS$/
|
|
|
|
|
|
|
|
# ignore stuff after an __END__ line
|
|
|
|
src_ended = true if ln =~ /^__END__$/
|
|
|
|
next if src_ended
|
|
|
|
|
|
|
|
if ln =~ /[\x00-\x08\x0b\x0c\x0e-\x19\x7f-\xff]/
|
|
|
|
error("Unicode detected: #{ln.inspect}", idx)
|
|
|
|
end
|
|
|
|
|
|
|
|
if ln =~ /[ \t]$/
|
|
|
|
warn("Spaces at EOL", idx)
|
|
|
|
end
|
|
|
|
|
2013-10-01 17:22:46 +00:00
|
|
|
# Check for mixed tab/spaces. Upgrade this to an error() soon.
|
2013-09-30 18:47:53 +00:00
|
|
|
if (ln.length > 1) and (ln =~ /^([\t ]*)/) and ($1.match(/\x20\x09|\x09\x20/))
|
|
|
|
warn("Space-Tab mixed indent: #{ln.inspect}", idx)
|
|
|
|
end
|
|
|
|
|
2013-10-01 17:22:46 +00:00
|
|
|
# Check for tabs. Upgrade this to an error() soon.
|
|
|
|
if (ln.length > 1) and (ln =~ /^\x09/)
|
|
|
|
warn("Tabbed indent: #{ln.inspect}", idx)
|
|
|
|
end
|
|
|
|
|
2013-09-30 18:47:53 +00:00
|
|
|
if ln =~ /\r$/
|
|
|
|
warn("Carriage return EOL", idx)
|
|
|
|
end
|
|
|
|
|
|
|
|
url_ok = false if ln =~ /\.com\/projects\/Framework/
|
|
|
|
if ln =~ /File\.open/ and ln =~ /[\"\'][arw]/
|
|
|
|
if not ln =~ /[\"\'][wra]\+?b\+?[\"\']/
|
|
|
|
warn("File.open without binary mode", idx)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
if ln =~/^[ \t]*load[ \t]+[\x22\x27]/
|
|
|
|
error("Loading (not requiring) a file: #{ln.inspect}", idx)
|
|
|
|
end
|
|
|
|
|
|
|
|
# The rest of these only count if it's not a comment line
|
|
|
|
next if ln =~ /[[:space:]]*#/
|
|
|
|
|
2013-11-20 23:53:25 +00:00
|
|
|
if ln =~ /\$std(?:out|err)/i or ln =~ /[[:space:]]puts/
|
|
|
|
next if ln =~ /^[\s]*["][^"]+\$std(?:out|err)/
|
2013-09-30 18:47:53 +00:00
|
|
|
no_stdio = false
|
|
|
|
error("Writes to stdout", idx)
|
|
|
|
end
|
|
|
|
|
2014-05-21 21:18:36 +00:00
|
|
|
# You should not change datastore in code. For reasons. See
|
|
|
|
# RM#8498 for discussion, starting at comment #16:
|
|
|
|
#
|
|
|
|
# https://dev.metasploit.com/redmine/issues/8498#note-16
|
2013-09-30 18:47:53 +00:00
|
|
|
if ln =~ /(?<!\.)datastore\[["'][^"']+["']\]\s*=(?![=~>])/
|
2014-05-21 21:18:36 +00:00
|
|
|
info("datastore is modified in code: #{ln}", idx)
|
2013-09-30 18:47:53 +00:00
|
|
|
end
|
2014-03-01 12:30:24 +00:00
|
|
|
|
2014-05-12 19:23:30 +00:00
|
|
|
# do not read Set-Cookie header (ignore commented lines)
|
|
|
|
if ln =~ /^(?!\s*#).+\[['"]Set-Cookie['"]\]/i
|
2014-03-01 12:30:24 +00:00
|
|
|
warn("Do not read Set-Cookie header directly, use res.get_cookies instead: #{ln}", idx)
|
|
|
|
end
|
2014-03-28 21:43:53 +00:00
|
|
|
|
|
|
|
# Auxiliary modules do not have a rank attribute
|
|
|
|
if ln =~ /^\s*Rank\s*=\s*/ and @source =~ /<\sMsf::Auxiliary/
|
|
|
|
warn("Auxiliary modules have no 'Rank': #{ln}", idx)
|
|
|
|
end
|
2014-08-26 20:30:08 +00:00
|
|
|
end
|
2013-09-30 18:47:53 +00:00
|
|
|
end
|
|
|
|
|
2014-01-22 21:26:16 +00:00
|
|
|
def check_vuln_codes
|
|
|
|
checkcode = @source.scan(/(Exploit::)?CheckCode::(\w+)/).flatten[1]
|
|
|
|
if checkcode and checkcode !~ /^Unknown|Safe|Detected|Appears|Vulnerable|Unsupported$/
|
2014-01-23 20:21:48 +00:00
|
|
|
error("Unrecognized checkcode: #{checkcode}")
|
2014-01-22 21:26:16 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-04-14 19:49:26 +00:00
|
|
|
def check_vars_get
|
2014-04-22 17:24:06 +00:00
|
|
|
test = @source.scan(/send_request_cgi\s*\(\s*\{?\s*['"]uri['"]\s*=>\s*[^=})]*?\?[^,})]+/im)
|
2014-03-01 11:02:41 +00:00
|
|
|
unless test.empty?
|
|
|
|
test.each { |item|
|
2014-04-22 17:24:06 +00:00
|
|
|
info("Please use vars_get in send_request_cgi: #{item}")
|
2014-03-01 11:02:41 +00:00
|
|
|
}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-06-17 13:44:43 +00:00
|
|
|
def check_newline_eof
|
|
|
|
if @source !~ /(?:\r\n|\n)\z/m
|
|
|
|
info('Please add a newline at the end of the file')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2014-06-30 05:40:06 +00:00
|
|
|
def check_sock_get
|
|
|
|
if @source =~ /\s+sock\.get(\s*|\(|\d+\s*|\d+\s*,\d+\s*)/m && @source !~ /sock\.get_once/
|
|
|
|
info('Please use sock.get_once instead of sock.get')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def check_udp_sock_get
|
2014-07-14 19:36:08 +00:00
|
|
|
if @source =~ /udp_sock\.get/m && @source !~ /udp_sock\.get\([a-zA-Z0-9]+/
|
2014-06-30 05:40:06 +00:00
|
|
|
info('Please specify a timeout to udp_sock.get')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-09-30 18:47:53 +00:00
|
|
|
private
|
|
|
|
|
|
|
|
def load_file(file)
|
|
|
|
f = open(file, 'rb')
|
2013-11-05 17:49:15 +00:00
|
|
|
@stat = f.stat
|
|
|
|
buf = f.read(@stat.size)
|
2013-09-30 18:47:53 +00:00
|
|
|
f.close
|
|
|
|
return buf
|
|
|
|
end
|
2014-03-01 11:02:41 +00:00
|
|
|
|
|
|
|
def cleanup_text(txt)
|
|
|
|
# remove line breaks
|
|
|
|
txt = txt.gsub(/[\r\n]/, ' ')
|
|
|
|
# replace multiple spaces by one space
|
|
|
|
txt.gsub(/\s{2,}/, ' ')
|
|
|
|
end
|
2012-10-12 07:55:16 +00:00
|
|
|
end
|
2010-11-05 00:05:34 +00:00
|
|
|
|
2014-01-31 20:19:04 +00:00
|
|
|
#
|
|
|
|
# Run all the msftidy checks.
|
|
|
|
#
|
|
|
|
# @param full_filepath [String] The full file path to check
|
|
|
|
# @return status [Integer] A status code suitable for use as an exit status
|
2013-10-16 15:40:42 +00:00
|
|
|
def run_checks(full_filepath)
|
|
|
|
tidy = Msftidy.new(full_filepath)
|
2013-11-05 17:49:15 +00:00
|
|
|
tidy.check_mode
|
2013-11-08 22:11:48 +00:00
|
|
|
tidy.check_shebang
|
2014-02-02 17:51:21 +00:00
|
|
|
tidy.check_nokogiri
|
2014-07-17 16:29:13 +00:00
|
|
|
tidy.check_rubygems
|
2013-09-30 18:47:53 +00:00
|
|
|
tidy.check_ref_identifiers
|
|
|
|
tidy.check_old_keywords
|
|
|
|
tidy.check_verbose_option
|
|
|
|
tidy.check_badchars
|
|
|
|
tidy.check_extname
|
2014-04-11 02:44:00 +00:00
|
|
|
tidy.check_old_rubies
|
2013-09-30 18:47:53 +00:00
|
|
|
tidy.check_ranking
|
|
|
|
tidy.check_disclosure_date
|
|
|
|
tidy.check_title_casing
|
|
|
|
tidy.check_bad_terms
|
|
|
|
tidy.check_function_basics
|
|
|
|
tidy.check_lines
|
2013-09-12 21:06:17 +00:00
|
|
|
tidy.check_snake_case_filename
|
2013-10-15 18:35:52 +00:00
|
|
|
tidy.check_comment_splat
|
2014-01-22 21:26:16 +00:00
|
|
|
tidy.check_vuln_codes
|
2014-04-14 19:49:26 +00:00
|
|
|
tidy.check_vars_get
|
2014-06-17 13:44:43 +00:00
|
|
|
tidy.check_newline_eof
|
2014-06-30 05:40:06 +00:00
|
|
|
tidy.check_sock_get
|
|
|
|
tidy.check_udp_sock_get
|
2014-01-31 20:19:04 +00:00
|
|
|
return tidy
|
2012-10-12 07:55:16 +00:00
|
|
|
end
|
2010-11-05 00:05:34 +00:00
|
|
|
|
|
|
|
##
|
|
|
|
#
|
|
|
|
# Main program
|
|
|
|
#
|
|
|
|
##
|
|
|
|
|
|
|
|
dirs = ARGV
|
|
|
|
|
2014-04-02 16:12:00 +00:00
|
|
|
@exit_status = 0
|
2014-01-31 20:19:04 +00:00
|
|
|
|
2014-04-02 16:12:00 +00:00
|
|
|
if dirs.length < 1
|
|
|
|
$stderr.puts "Usage: #{File.basename(__FILE__)} <directory or file>"
|
|
|
|
@exit_status = 1
|
|
|
|
exit(@exit_status)
|
2010-11-05 00:05:34 +00:00
|
|
|
end
|
|
|
|
|
2013-10-16 15:40:42 +00:00
|
|
|
dirs.each do |dir|
|
2013-11-20 22:36:07 +00:00
|
|
|
begin
|
|
|
|
Find.find(dir) do |full_filepath|
|
|
|
|
next if full_filepath =~ /\.git[\x5c\x2f]/
|
|
|
|
next unless File.file? full_filepath
|
|
|
|
next unless full_filepath =~ /\.rb$/
|
2014-01-31 20:19:04 +00:00
|
|
|
msftidy = run_checks(full_filepath)
|
|
|
|
@exit_status = msftidy.status if (msftidy.status > @exit_status.to_i)
|
2013-11-20 22:36:07 +00:00
|
|
|
end
|
|
|
|
rescue Errno::ENOENT
|
|
|
|
$stderr.puts "#{File.basename(__FILE__)}: #{dir}: No such file or directory"
|
2013-09-30 18:47:53 +00:00
|
|
|
end
|
2013-10-16 15:40:42 +00:00
|
|
|
end
|
2014-01-31 20:19:04 +00:00
|
|
|
|
|
|
|
exit(@exit_status.to_i)
|