2020-01-22 17:39:48 -05:00
#!/usr/bin/env ruby
# -*- coding: binary -*-
#
# Check (recursively) for style compliance violations and other
# tree inconsistencies.
#
# by h00die
#
require 'fileutils'
require 'find'
require 'time'
2021-05-02 18:22:06 -05:00
SUPPRESS_INFO_MESSAGES = ! ! ENV [ 'MSF_SUPPRESS_INFO_MESSAGES' ]
2020-01-22 17:39:48 -05:00
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' )
2020-07-23 20:37:39 -04:00
end
2020-01-22 17:39:48 -05:00
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
2020-03-24 09:51:21 -04:00
has_bad_scenario_sub = false
2020-01-22 17:39:48 -05:00
@lines . each do | line |
if line =~ / ^ # # Vulnerable Application$ /
has_vulnerable_application = true
next
end
2022-12-14 18:28:16 +00:00
if line =~ / ^ # # Verification Steps$ / || line =~ / ^ # # Module usage$ /
2020-01-22 17:39:48 -05:00
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
2020-03-24 09:51:21 -04:00
if line =~ / # # # Version and OS$ /
has_bad_scenario_sub = true
next
end
2020-01-22 17:39:48 -05:00
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
2021-05-02 18:17:58 -05:00
# INFO because there may be no documentation-worthy options
info ( 'Missing Section: ## Options' )
2020-01-22 17:39:48 -05:00
end
2020-01-22 19:24:01 -05:00
if has_bad_description
2020-01-22 17:39:48 -05:00
warn ( 'Descriptions should be within Vulnerable Application, or an H3 sub-section of Vulnerable Application' )
end
2020-01-22 19:24:01 -05:00
if has_bad_intro
2020-01-22 17:39:48 -05:00
warn ( 'Intro/Introduction should be within Vulnerable Application, or an H3 sub-section of Vulnerable Application' )
end
2020-03-24 09:51:21 -04:00
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
2020-01-22 17:39:48 -05:00
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
2021-05-02 18:13:34 -05:00
# This checks that the H2 headings are in the right order. Options are optional.
2020-01-22 17:39:48 -05:00
def h2_order
2022-12-14 18:28:16 +00:00
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' )
2020-01-22 17:39:48 -05:00
end
end
def line_checks
idx = 0
2020-01-22 21:08:32 -05:00
in_codeblock = false
2020-04-02 19:10:39 -04:00
in_options = false
2020-01-22 21:08:32 -05:00
2020-01-22 17:39:48 -05:00
@lines . each do | ln |
idx += 1
2025-05-24 13:34:32 +10:00
tback = ln . scan ( '```' )
2020-07-23 20:37:39 -04:00
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
2020-01-22 21:08:32 -05:00
end
2020-04-02 19:10:39 -04:00
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
2024-01-07 13:28:13 -05:00
if in_options && ln =~ / ^ \ s* \ * \ *[a-z]+ \ * \ *$ /i # catch options in old format like **command** instead of ### command
2020-04-02 19:10:39 -04:00
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
2024-02-16 12:58:49 +01:00
if in_options && ln =~ / ^ \ s*[ \ * # ]{2,3} \ s*(rhost|rhosts|rport|lport|lhost|srvhost|srvport|ssl|uripath|session|proxies|payload|targeturi) \ *{0,2}$ /i
warn ( 'Universal options such as rhost(s), rport, lport, lhost, srvhost, srvport, ssl, uripath, session, proxies, payload, targeturi can be removed.' , idx )
2020-04-02 19:10:39 -04:00
end
2020-01-28 14:28:18 -05:00
# find spaces at EOL not in a code block which is ``` or starts with four spaces
if ! in_codeblock && ln =~ / [ \ t]$ / && ! ( ln =~ / ^ / )
2020-01-22 17:39:48 -05:00
warn ( " Spaces at EOL " , idx )
end
2020-03-24 09:15:04 -04:00
if ln =~ / Example steps in this format /
warn ( " Instructional text not removed " , idx )
end
2020-01-22 17:39:48 -05:00
if ln =~ / ^ # /
warn ( " No H1 ( # ) headers. If this is code, indent. " , idx )
end
l = 140
2020-07-23 20:37:39 -04:00
if ln . rstrip . length > l && ! in_codeblock
2020-01-22 17:39:48 -05:00
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