339 lines
8.8 KiB
Ruby
Executable File
339 lines
8.8 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# -*- coding: binary -*-
|
|
|
|
#
|
|
# Check (recursively) for style compliance violations and other
|
|
# tree inconsistencies.
|
|
#
|
|
# by h00die
|
|
#
|
|
|
|
require 'fileutils'
|
|
require 'find'
|
|
require 'time'
|
|
|
|
SUPPRESS_INFO_MESSAGES = !!ENV['MSF_SUPPRESS_INFO_MESSAGES']
|
|
|
|
class String
|
|
def red
|
|
"\e[1;31;40m#{self}\e[0m"
|
|
end
|
|
|
|
def yellow
|
|
"\e[1;33;40m#{self}\e[0m"
|
|
end
|
|
|
|
def green
|
|
"\e[1;32;40m#{self}\e[0m"
|
|
end
|
|
|
|
def cyan
|
|
"\e[1;36;40m#{self}\e[0m"
|
|
end
|
|
end
|
|
|
|
class MsftidyDoc
|
|
|
|
# Status codes
|
|
OK = 0
|
|
WARNING = 1
|
|
ERROR = 2
|
|
|
|
# Some compiles regexes
|
|
REGEX_MSF_EXPLOIT = / \< Msf::Exploit/
|
|
REGEX_IS_BLANK_OR_END = /^\s*end\s*$/
|
|
|
|
attr_reader :full_filepath, :source, :stat, :name, :status
|
|
|
|
def initialize(source_file)
|
|
@full_filepath = source_file
|
|
@module_type = File.dirname(File.expand_path(@full_filepath))[/\/modules\/([^\/]+)/, 1]
|
|
@source = load_file(source_file)
|
|
@lines = @source.lines # returns an enumerator
|
|
@status = OK
|
|
@name = File.basename(source_file)
|
|
end
|
|
|
|
public
|
|
|
|
#
|
|
# 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.
|
|
#
|
|
# @return status [Integer] Returns WARNINGS unless we already have an
|
|
# error.
|
|
def warn(txt, line=0) line_msg = (line>0) ? ":#{line}" : ''
|
|
puts "#{@full_filepath}#{line_msg} - [#{'WARNING'.yellow}] #{cleanup_text(txt)}"
|
|
@status = WARNING if @status < WARNING
|
|
end
|
|
|
|
#
|
|
# 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
|
|
def error(txt, line=0)
|
|
line_msg = (line>0) ? ":#{line}" : ''
|
|
puts "#{@full_filepath}#{line_msg} - [#{'ERROR'.red}] #{cleanup_text(txt)}"
|
|
@status = ERROR if @status < ERROR
|
|
end
|
|
|
|
# Currently unused, but some day msftidy will fix errors for you.
|
|
def fixed(txt, line=0)
|
|
line_msg = (line>0) ? ":#{line}" : ''
|
|
puts "#{@full_filepath}#{line_msg} - [#{'FIXED'.green}] #{cleanup_text(txt)}"
|
|
end
|
|
|
|
#
|
|
# Display an info message. Info messages do not alter the exit status.
|
|
#
|
|
def info(txt, line=0)
|
|
return if SUPPRESS_INFO_MESSAGES
|
|
line_msg = (line>0) ? ":#{line}" : ''
|
|
puts "#{@full_filepath}#{line_msg} - [#{'INFO'.cyan}] #{cleanup_text(txt)}"
|
|
end
|
|
|
|
##
|
|
#
|
|
# The functions below are actually the ones checking the source code
|
|
#
|
|
##
|
|
|
|
def has_module
|
|
module_filepath = @full_filepath.sub('documentation/','').sub('/exploit/', '/exploits/')
|
|
found = false
|
|
['.rb', '.py', '.go'].each do |ext|
|
|
if File.file? module_filepath.sub(/.md$/, ext)
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
unless found
|
|
error("Doc missing module. Check file name and path(s) are correct. Doc: #{@full_filepath}")
|
|
end
|
|
end
|
|
|
|
def check_start_with_vuln_app
|
|
unless @lines.first =~ /^## Vulnerable Application$/
|
|
warn('Docs should start with ## Vulnerable Application')
|
|
end
|
|
end
|
|
|
|
def has_h2_headings
|
|
has_vulnerable_application = false
|
|
has_verification_steps = false
|
|
has_scenarios = false
|
|
has_options = false
|
|
has_bad_description = false
|
|
has_bad_intro = false
|
|
has_bad_scenario_sub = false
|
|
|
|
@lines.each do |line|
|
|
if line =~ /^## Vulnerable Application$/
|
|
has_vulnerable_application = true
|
|
next
|
|
end
|
|
|
|
if line =~ /^## Verification Steps$/ || line =~ /^## Module usage$/
|
|
has_verification_steps = true
|
|
next
|
|
end
|
|
|
|
if line =~ /^## Scenarios$/
|
|
has_scenarios = true
|
|
next
|
|
end
|
|
|
|
if line =~ /^## Options$/
|
|
has_options = true
|
|
next
|
|
end
|
|
|
|
if line =~ /^## Description$/
|
|
has_bad_description = true
|
|
next
|
|
end
|
|
|
|
if line =~ /^## (Intro|Introduction)$/
|
|
has_bad_intro = true
|
|
next
|
|
end
|
|
|
|
if line =~ /### Version and OS$/
|
|
has_bad_scenario_sub = true
|
|
next
|
|
end
|
|
end
|
|
|
|
unless has_vulnerable_application
|
|
warn('Missing Section: ## Vulnerable Application')
|
|
end
|
|
|
|
unless has_verification_steps
|
|
warn('Missing Section: ## Verification Steps')
|
|
end
|
|
|
|
unless has_scenarios
|
|
warn('Missing Section: ## Scenarios')
|
|
end
|
|
|
|
unless has_options
|
|
# INFO because there may be no documentation-worthy options
|
|
info('Missing Section: ## Options')
|
|
end
|
|
|
|
if has_bad_description
|
|
warn('Descriptions should be within Vulnerable Application, or an H3 sub-section of Vulnerable Application')
|
|
end
|
|
|
|
if has_bad_intro
|
|
warn('Intro/Introduction should be within Vulnerable Application, or an H3 sub-section of Vulnerable Application')
|
|
end
|
|
|
|
if has_bad_scenario_sub
|
|
warn('Scenario sub-sections should include the vulnerable application version and OS tested on in an H3, not just ### Version and OS')
|
|
end
|
|
end
|
|
|
|
def check_newline_eof
|
|
if @source !~ /(?:\r\n|\n)\z/m
|
|
warn('Please add a newline at the end of the file')
|
|
end
|
|
end
|
|
|
|
# This checks that the H2 headings are in the right order. Options are optional.
|
|
def h2_order
|
|
unless @source =~ /^## Vulnerable Application$.+^## (Verification Steps|Module usage)$.+(?:^## Options$.+)?^## Scenarios$/m
|
|
warn('H2 headings in incorrect order. Should be: Vulnerable Application, Verification Steps/Module usage, Options, Scenarios')
|
|
end
|
|
end
|
|
|
|
def line_checks
|
|
idx = 0
|
|
in_codeblock = false
|
|
in_options = false
|
|
|
|
@lines.each do |ln|
|
|
idx += 1
|
|
|
|
tback = ln.scan(/```/)
|
|
if tback.length > 0
|
|
if tback.length.even?
|
|
warn("Should use single backquotes (`) for single line literals instead of triple backquotes (```)", idx)
|
|
else
|
|
in_codeblock = !in_codeblock
|
|
end
|
|
|
|
if ln =~ /^\s+```/
|
|
warn("Code blocks using triple backquotes (```) should not be indented", idx)
|
|
end
|
|
end
|
|
|
|
if ln =~ /## Options/
|
|
in_options = true
|
|
end
|
|
|
|
if ln =~ /## Scenarios/ || (in_options && ln =~ /$\s*## /) # we're not in options anymore
|
|
# we set a hard false here because there isn't a guarantee options exists
|
|
in_options = false
|
|
end
|
|
|
|
if in_options && ln =~ /^\s*\*\*[a-z]+\*\*$/i # catch options in old format like **command** instead of ### comand
|
|
warn("Options should use ### instead of bolds (**)", idx)
|
|
end
|
|
|
|
# this will catch either bold or h2/3 universal options. Defaults aren't needed since they're not unique to this exploit
|
|
if in_options && ln =~ /^\s*[\*#]{2,3}\s*(rhost|rhosts|rport|lport|lhost|srvhost|srvport|ssl|uripath|session|proxies|payload)\*{0,2}$/i
|
|
warn('Universal options such as rhost(s), rport, lport, lhost, srvhost, srvport, ssl, uripath, session, proxies, payload can be removed.', idx)
|
|
end
|
|
# find spaces at EOL not in a code block which is ``` or starts with four spaces
|
|
if !in_codeblock && ln =~ /[ \t]$/ && !(ln =~ /^ /)
|
|
warn("Spaces at EOL", idx)
|
|
end
|
|
|
|
if ln =~ /Example steps in this format/
|
|
warn("Instructional text not removed", idx)
|
|
end
|
|
|
|
if ln =~ /^# /
|
|
warn("No H1 (#) headers. If this is code, indent.", idx)
|
|
end
|
|
|
|
l = 140
|
|
if ln.rstrip.length > l && !in_codeblock
|
|
warn("Line too long (#{ln.length}). Consider a newline (which resolves to a space in markdown) to break it up around #{l} characters.", idx)
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
#
|
|
# Run all the msftidy checks.
|
|
#
|
|
def run_checks
|
|
has_module
|
|
check_start_with_vuln_app
|
|
has_h2_headings
|
|
check_newline_eof
|
|
h2_order
|
|
line_checks
|
|
end
|
|
|
|
private
|
|
|
|
def load_file(file)
|
|
f = open(file, 'rb')
|
|
@stat = f.stat
|
|
buf = f.read(@stat.size)
|
|
f.close
|
|
return buf
|
|
end
|
|
|
|
def cleanup_text(txt)
|
|
# remove line breaks
|
|
txt = txt.gsub(/[\r\n]/, ' ')
|
|
# replace multiple spaces by one space
|
|
txt.gsub(/\s{2,}/, ' ')
|
|
end
|
|
end
|
|
|
|
##
|
|
#
|
|
# Main program
|
|
#
|
|
##
|
|
|
|
if __FILE__ == $PROGRAM_NAME
|
|
dirs = ARGV
|
|
|
|
@exit_status = 0
|
|
|
|
if dirs.length < 1
|
|
$stderr.puts "Usage: #{File.basename(__FILE__)} <directory or file>"
|
|
@exit_status = 1
|
|
exit(@exit_status)
|
|
end
|
|
|
|
dirs.each do |dir|
|
|
begin
|
|
Find.find(dir) do |full_filepath|
|
|
next if full_filepath =~ /\.git[\x5c\x2f]/
|
|
next unless File.file? full_filepath
|
|
next unless File.extname(full_filepath) == '.md'
|
|
msftidy = MsftidyDoc.new(full_filepath)
|
|
# Executable files are now assumed to be external modules
|
|
# but also check for some content to be sure
|
|
next if File.executable?(full_filepath) && msftidy.source =~ /require ["']metasploit["']/
|
|
msftidy.run_checks
|
|
@exit_status = msftidy.status if (msftidy.status > @exit_status.to_i)
|
|
end
|
|
rescue Errno::ENOENT
|
|
$stderr.puts "#{File.basename(__FILE__)}: #{dir}: No such file or directory"
|
|
end
|
|
end
|
|
|
|
exit(@exit_status.to_i)
|
|
end
|