CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/tools/dev/msftidy_docs.rb
Views: 1904
1
#!/usr/bin/env ruby
2
# -*- coding: binary -*-
3
4
#
5
# Check (recursively) for style compliance violations and other
6
# tree inconsistencies.
7
#
8
# by h00die
9
#
10
11
require 'fileutils'
12
require 'find'
13
require 'time'
14
15
SUPPRESS_INFO_MESSAGES = !!ENV['MSF_SUPPRESS_INFO_MESSAGES']
16
17
class String
18
def red
19
"\e[1;31;40m#{self}\e[0m"
20
end
21
22
def yellow
23
"\e[1;33;40m#{self}\e[0m"
24
end
25
26
def green
27
"\e[1;32;40m#{self}\e[0m"
28
end
29
30
def cyan
31
"\e[1;36;40m#{self}\e[0m"
32
end
33
end
34
35
class MsftidyDoc
36
37
# Status codes
38
OK = 0
39
WARNING = 1
40
ERROR = 2
41
42
# Some compiles regexes
43
REGEX_MSF_EXPLOIT = / \< Msf::Exploit/
44
REGEX_IS_BLANK_OR_END = /^\s*end\s*$/
45
46
attr_reader :full_filepath, :source, :stat, :name, :status
47
48
def initialize(source_file)
49
@full_filepath = source_file
50
@module_type = File.dirname(File.expand_path(@full_filepath))[/\/modules\/([^\/]+)/, 1]
51
@source = load_file(source_file)
52
@lines = @source.lines # returns an enumerator
53
@status = OK
54
@name = File.basename(source_file)
55
end
56
57
public
58
59
#
60
# Display a warning message, given some text and a number. Warnings
61
# are usually style issues that may be okay for people who aren't core
62
# Framework developers.
63
#
64
# @return status [Integer] Returns WARNINGS unless we already have an
65
# error.
66
def warn(txt, line=0) line_msg = (line>0) ? ":#{line}" : ''
67
puts "#{@full_filepath}#{line_msg} - [#{'WARNING'.yellow}] #{cleanup_text(txt)}"
68
@status = WARNING if @status < WARNING
69
end
70
71
#
72
# Display an error message, given some text and a number. Errors
73
# can break things or are so egregiously bad, style-wise, that they
74
# really ought to be fixed.
75
#
76
# @return status [Integer] Returns ERRORS
77
def error(txt, line=0)
78
line_msg = (line>0) ? ":#{line}" : ''
79
puts "#{@full_filepath}#{line_msg} - [#{'ERROR'.red}] #{cleanup_text(txt)}"
80
@status = ERROR if @status < ERROR
81
end
82
83
# Currently unused, but some day msftidy will fix errors for you.
84
def fixed(txt, line=0)
85
line_msg = (line>0) ? ":#{line}" : ''
86
puts "#{@full_filepath}#{line_msg} - [#{'FIXED'.green}] #{cleanup_text(txt)}"
87
end
88
89
#
90
# Display an info message. Info messages do not alter the exit status.
91
#
92
def info(txt, line=0)
93
return if SUPPRESS_INFO_MESSAGES
94
line_msg = (line>0) ? ":#{line}" : ''
95
puts "#{@full_filepath}#{line_msg} - [#{'INFO'.cyan}] #{cleanup_text(txt)}"
96
end
97
98
##
99
#
100
# The functions below are actually the ones checking the source code
101
#
102
##
103
104
def has_module
105
module_filepath = @full_filepath.sub('documentation/','').sub('/exploit/', '/exploits/')
106
found = false
107
['.rb', '.py', '.go'].each do |ext|
108
if File.file? module_filepath.sub(/.md$/, ext)
109
found = true
110
break
111
end
112
end
113
unless found
114
error("Doc missing module. Check file name and path(s) are correct. Doc: #{@full_filepath}")
115
end
116
end
117
118
def check_start_with_vuln_app
119
unless @lines.first =~ /^## Vulnerable Application$/
120
warn('Docs should start with ## Vulnerable Application')
121
end
122
end
123
124
def has_h2_headings
125
has_vulnerable_application = false
126
has_verification_steps = false
127
has_scenarios = false
128
has_options = false
129
has_bad_description = false
130
has_bad_intro = false
131
has_bad_scenario_sub = false
132
133
@lines.each do |line|
134
if line =~ /^## Vulnerable Application$/
135
has_vulnerable_application = true
136
next
137
end
138
139
if line =~ /^## Verification Steps$/ || line =~ /^## Module usage$/
140
has_verification_steps = true
141
next
142
end
143
144
if line =~ /^## Scenarios$/
145
has_scenarios = true
146
next
147
end
148
149
if line =~ /^## Options$/
150
has_options = true
151
next
152
end
153
154
if line =~ /^## Description$/
155
has_bad_description = true
156
next
157
end
158
159
if line =~ /^## (Intro|Introduction)$/
160
has_bad_intro = true
161
next
162
end
163
164
if line =~ /### Version and OS$/
165
has_bad_scenario_sub = true
166
next
167
end
168
end
169
170
unless has_vulnerable_application
171
warn('Missing Section: ## Vulnerable Application')
172
end
173
174
unless has_verification_steps
175
warn('Missing Section: ## Verification Steps')
176
end
177
178
unless has_scenarios
179
warn('Missing Section: ## Scenarios')
180
end
181
182
unless has_options
183
# INFO because there may be no documentation-worthy options
184
info('Missing Section: ## Options')
185
end
186
187
if has_bad_description
188
warn('Descriptions should be within Vulnerable Application, or an H3 sub-section of Vulnerable Application')
189
end
190
191
if has_bad_intro
192
warn('Intro/Introduction should be within Vulnerable Application, or an H3 sub-section of Vulnerable Application')
193
end
194
195
if has_bad_scenario_sub
196
warn('Scenario sub-sections should include the vulnerable application version and OS tested on in an H3, not just ### Version and OS')
197
end
198
end
199
200
def check_newline_eof
201
if @source !~ /(?:\r\n|\n)\z/m
202
warn('Please add a newline at the end of the file')
203
end
204
end
205
206
# This checks that the H2 headings are in the right order. Options are optional.
207
def h2_order
208
unless @source =~ /^## Vulnerable Application$.+^## (Verification Steps|Module usage)$.+(?:^## Options$.+)?^## Scenarios$/m
209
warn('H2 headings in incorrect order. Should be: Vulnerable Application, Verification Steps/Module usage, Options, Scenarios')
210
end
211
end
212
213
def line_checks
214
idx = 0
215
in_codeblock = false
216
in_options = false
217
218
@lines.each do |ln|
219
idx += 1
220
221
tback = ln.scan(/```/)
222
if tback.length > 0
223
if tback.length.even?
224
warn("Should use single backquotes (`) for single line literals instead of triple backquotes (```)", idx)
225
else
226
in_codeblock = !in_codeblock
227
end
228
229
if ln =~ /^\s+```/
230
warn("Code blocks using triple backquotes (```) should not be indented", idx)
231
end
232
end
233
234
if ln =~ /## Options/
235
in_options = true
236
end
237
238
if ln =~ /## Scenarios/ || (in_options && ln =~ /$\s*## /) # we're not in options anymore
239
# we set a hard false here because there isn't a guarantee options exists
240
in_options = false
241
end
242
243
if in_options && ln =~ /^\s*\*\*[a-z]+\*\*$/i # catch options in old format like **command** instead of ### command
244
warn("Options should use ### instead of bolds (**)", idx)
245
end
246
247
# this will catch either bold or h2/3 universal options. Defaults aren't needed since they're not unique to this exploit
248
if in_options && ln =~ /^\s*[\*#]{2,3}\s*(rhost|rhosts|rport|lport|lhost|srvhost|srvport|ssl|uripath|session|proxies|payload|targeturi)\*{0,2}$/i
249
warn('Universal options such as rhost(s), rport, lport, lhost, srvhost, srvport, ssl, uripath, session, proxies, payload, targeturi can be removed.', idx)
250
end
251
# find spaces at EOL not in a code block which is ``` or starts with four spaces
252
if !in_codeblock && ln =~ /[ \t]$/ && !(ln =~ /^ /)
253
warn("Spaces at EOL", idx)
254
end
255
256
if ln =~ /Example steps in this format/
257
warn("Instructional text not removed", idx)
258
end
259
260
if ln =~ /^# /
261
warn("No H1 (#) headers. If this is code, indent.", idx)
262
end
263
264
l = 140
265
if ln.rstrip.length > l && !in_codeblock
266
warn("Line too long (#{ln.length}). Consider a newline (which resolves to a space in markdown) to break it up around #{l} characters.", idx)
267
end
268
269
end
270
end
271
272
#
273
# Run all the msftidy checks.
274
#
275
def run_checks
276
has_module
277
check_start_with_vuln_app
278
has_h2_headings
279
check_newline_eof
280
h2_order
281
line_checks
282
end
283
284
private
285
286
def load_file(file)
287
f = open(file, 'rb')
288
@stat = f.stat
289
buf = f.read(@stat.size)
290
f.close
291
return buf
292
end
293
294
def cleanup_text(txt)
295
# remove line breaks
296
txt = txt.gsub(/[\r\n]/, ' ')
297
# replace multiple spaces by one space
298
txt.gsub(/\s{2,}/, ' ')
299
end
300
end
301
302
##
303
#
304
# Main program
305
#
306
##
307
308
if __FILE__ == $PROGRAM_NAME
309
dirs = ARGV
310
311
@exit_status = 0
312
313
if dirs.length < 1
314
$stderr.puts "Usage: #{File.basename(__FILE__)} <directory or file>"
315
@exit_status = 1
316
exit(@exit_status)
317
end
318
319
dirs.each do |dir|
320
begin
321
Find.find(dir) do |full_filepath|
322
next if full_filepath =~ /\.git[\x5c\x2f]/
323
next unless File.file? full_filepath
324
next unless File.extname(full_filepath) == '.md'
325
msftidy = MsftidyDoc.new(full_filepath)
326
# Executable files are now assumed to be external modules
327
# but also check for some content to be sure
328
next if File.executable?(full_filepath) && msftidy.source =~ /require ["']metasploit["']/
329
msftidy.run_checks
330
@exit_status = msftidy.status if (msftidy.status > @exit_status.to_i)
331
end
332
rescue Errno::ENOENT
333
$stderr.puts "#{File.basename(__FILE__)}: #{dir}: No such file or directory"
334
end
335
end
336
337
exit(@exit_status.to_i)
338
end
339
340