Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/tools/dev/msftidy_docs.rb
Views: 11766
#!/usr/bin/env ruby1# -*- coding: binary -*-23#4# Check (recursively) for style compliance violations and other5# tree inconsistencies.6#7# by h00die8#910require 'fileutils'11require 'find'12require 'time'1314SUPPRESS_INFO_MESSAGES = !!ENV['MSF_SUPPRESS_INFO_MESSAGES']1516class String17def red18"\e[1;31;40m#{self}\e[0m"19end2021def yellow22"\e[1;33;40m#{self}\e[0m"23end2425def green26"\e[1;32;40m#{self}\e[0m"27end2829def cyan30"\e[1;36;40m#{self}\e[0m"31end32end3334class MsftidyDoc3536# Status codes37OK = 038WARNING = 139ERROR = 24041# Some compiles regexes42REGEX_MSF_EXPLOIT = / \< Msf::Exploit/43REGEX_IS_BLANK_OR_END = /^\s*end\s*$/4445attr_reader :full_filepath, :source, :stat, :name, :status4647def initialize(source_file)48@full_filepath = source_file49@module_type = File.dirname(File.expand_path(@full_filepath))[/\/modules\/([^\/]+)/, 1]50@source = load_file(source_file)51@lines = @source.lines # returns an enumerator52@status = OK53@name = File.basename(source_file)54end5556public5758#59# Display a warning message, given some text and a number. Warnings60# are usually style issues that may be okay for people who aren't core61# Framework developers.62#63# @return status [Integer] Returns WARNINGS unless we already have an64# error.65def warn(txt, line=0) line_msg = (line>0) ? ":#{line}" : ''66puts "#{@full_filepath}#{line_msg} - [#{'WARNING'.yellow}] #{cleanup_text(txt)}"67@status = WARNING if @status < WARNING68end6970#71# Display an error message, given some text and a number. Errors72# can break things or are so egregiously bad, style-wise, that they73# really ought to be fixed.74#75# @return status [Integer] Returns ERRORS76def error(txt, line=0)77line_msg = (line>0) ? ":#{line}" : ''78puts "#{@full_filepath}#{line_msg} - [#{'ERROR'.red}] #{cleanup_text(txt)}"79@status = ERROR if @status < ERROR80end8182# Currently unused, but some day msftidy will fix errors for you.83def fixed(txt, line=0)84line_msg = (line>0) ? ":#{line}" : ''85puts "#{@full_filepath}#{line_msg} - [#{'FIXED'.green}] #{cleanup_text(txt)}"86end8788#89# Display an info message. Info messages do not alter the exit status.90#91def info(txt, line=0)92return if SUPPRESS_INFO_MESSAGES93line_msg = (line>0) ? ":#{line}" : ''94puts "#{@full_filepath}#{line_msg} - [#{'INFO'.cyan}] #{cleanup_text(txt)}"95end9697##98#99# The functions below are actually the ones checking the source code100#101##102103def has_module104module_filepath = @full_filepath.sub('documentation/','').sub('/exploit/', '/exploits/')105found = false106['.rb', '.py', '.go'].each do |ext|107if File.file? module_filepath.sub(/.md$/, ext)108found = true109break110end111end112unless found113error("Doc missing module. Check file name and path(s) are correct. Doc: #{@full_filepath}")114end115end116117def check_start_with_vuln_app118unless @lines.first =~ /^## Vulnerable Application$/119warn('Docs should start with ## Vulnerable Application')120end121end122123def has_h2_headings124has_vulnerable_application = false125has_verification_steps = false126has_scenarios = false127has_options = false128has_bad_description = false129has_bad_intro = false130has_bad_scenario_sub = false131132@lines.each do |line|133if line =~ /^## Vulnerable Application$/134has_vulnerable_application = true135next136end137138if line =~ /^## Verification Steps$/ || line =~ /^## Module usage$/139has_verification_steps = true140next141end142143if line =~ /^## Scenarios$/144has_scenarios = true145next146end147148if line =~ /^## Options$/149has_options = true150next151end152153if line =~ /^## Description$/154has_bad_description = true155next156end157158if line =~ /^## (Intro|Introduction)$/159has_bad_intro = true160next161end162163if line =~ /### Version and OS$/164has_bad_scenario_sub = true165next166end167end168169unless has_vulnerable_application170warn('Missing Section: ## Vulnerable Application')171end172173unless has_verification_steps174warn('Missing Section: ## Verification Steps')175end176177unless has_scenarios178warn('Missing Section: ## Scenarios')179end180181unless has_options182# INFO because there may be no documentation-worthy options183info('Missing Section: ## Options')184end185186if has_bad_description187warn('Descriptions should be within Vulnerable Application, or an H3 sub-section of Vulnerable Application')188end189190if has_bad_intro191warn('Intro/Introduction should be within Vulnerable Application, or an H3 sub-section of Vulnerable Application')192end193194if has_bad_scenario_sub195warn('Scenario sub-sections should include the vulnerable application version and OS tested on in an H3, not just ### Version and OS')196end197end198199def check_newline_eof200if @source !~ /(?:\r\n|\n)\z/m201warn('Please add a newline at the end of the file')202end203end204205# This checks that the H2 headings are in the right order. Options are optional.206def h2_order207unless @source =~ /^## Vulnerable Application$.+^## (Verification Steps|Module usage)$.+(?:^## Options$.+)?^## Scenarios$/m208warn('H2 headings in incorrect order. Should be: Vulnerable Application, Verification Steps/Module usage, Options, Scenarios')209end210end211212def line_checks213idx = 0214in_codeblock = false215in_options = false216217@lines.each do |ln|218idx += 1219220tback = ln.scan(/```/)221if tback.length > 0222if tback.length.even?223warn("Should use single backquotes (`) for single line literals instead of triple backquotes (```)", idx)224else225in_codeblock = !in_codeblock226end227228if ln =~ /^\s+```/229warn("Code blocks using triple backquotes (```) should not be indented", idx)230end231end232233if ln =~ /## Options/234in_options = true235end236237if ln =~ /## Scenarios/ || (in_options && ln =~ /$\s*## /) # we're not in options anymore238# we set a hard false here because there isn't a guarantee options exists239in_options = false240end241242if in_options && ln =~ /^\s*\*\*[a-z]+\*\*$/i # catch options in old format like **command** instead of ### command243warn("Options should use ### instead of bolds (**)", idx)244end245246# this will catch either bold or h2/3 universal options. Defaults aren't needed since they're not unique to this exploit247if in_options && ln =~ /^\s*[\*#]{2,3}\s*(rhost|rhosts|rport|lport|lhost|srvhost|srvport|ssl|uripath|session|proxies|payload|targeturi)\*{0,2}$/i248warn('Universal options such as rhost(s), rport, lport, lhost, srvhost, srvport, ssl, uripath, session, proxies, payload, targeturi can be removed.', idx)249end250# find spaces at EOL not in a code block which is ``` or starts with four spaces251if !in_codeblock && ln =~ /[ \t]$/ && !(ln =~ /^ /)252warn("Spaces at EOL", idx)253end254255if ln =~ /Example steps in this format/256warn("Instructional text not removed", idx)257end258259if ln =~ /^# /260warn("No H1 (#) headers. If this is code, indent.", idx)261end262263l = 140264if ln.rstrip.length > l && !in_codeblock265warn("Line too long (#{ln.length}). Consider a newline (which resolves to a space in markdown) to break it up around #{l} characters.", idx)266end267268end269end270271#272# Run all the msftidy checks.273#274def run_checks275has_module276check_start_with_vuln_app277has_h2_headings278check_newline_eof279h2_order280line_checks281end282283private284285def load_file(file)286f = open(file, 'rb')287@stat = f.stat288buf = f.read(@stat.size)289f.close290return buf291end292293def cleanup_text(txt)294# remove line breaks295txt = txt.gsub(/[\r\n]/, ' ')296# replace multiple spaces by one space297txt.gsub(/\s{2,}/, ' ')298end299end300301##302#303# Main program304#305##306307if __FILE__ == $PROGRAM_NAME308dirs = ARGV309310@exit_status = 0311312if dirs.length < 1313$stderr.puts "Usage: #{File.basename(__FILE__)} <directory or file>"314@exit_status = 1315exit(@exit_status)316end317318dirs.each do |dir|319begin320Find.find(dir) do |full_filepath|321next if full_filepath =~ /\.git[\x5c\x2f]/322next unless File.file? full_filepath323next unless File.extname(full_filepath) == '.md'324msftidy = MsftidyDoc.new(full_filepath)325# Executable files are now assumed to be external modules326# but also check for some content to be sure327next if File.executable?(full_filepath) && msftidy.source =~ /require ["']metasploit["']/328msftidy.run_checks329@exit_status = msftidy.status if (msftidy.status > @exit_status.to_i)330end331rescue Errno::ENOENT332$stderr.puts "#{File.basename(__FILE__)}: #{dir}: No such file or directory"333end334end335336exit(@exit_status.to_i)337end338339340