metasploit-framework/tools/dev/msftidy_docs.rb

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