From b0cfb4aacf724477eb5e83a369d0d54921a5556e Mon Sep 17 00:00:00 2001 From: wchen-r7 Date: Tue, 16 Feb 2016 22:44:03 -0600 Subject: [PATCH] Add info -d to show module documentation in .md --- Gemfile | 2 + Gemfile.lock | 20 +- data/markdown.css | 233 ++++++++++++++ lib/msf/ui/console/command_dispatcher/core.rb | 16 +- lib/msf/util/document_generator.rb | 285 ++++++++++++++++++ 5 files changed, 550 insertions(+), 6 deletions(-) create mode 100644 data/markdown.css create mode 100644 lib/msf/util/document_generator.rb diff --git a/Gemfile b/Gemfile index 9893680a4d..49f7bad8ae 100755 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,8 @@ group :development do gem 'yard' # for development and testing purposes gem 'pry' + # module documentation + gem 'octokit', '~> 4.0' end group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 4cf7c7588b..ed54bee260 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -57,9 +57,10 @@ GEM multi_json (~> 1.3) thread_safe (~> 0.1) tzinfo (~> 0.3.37) + addressable (2.3.8) arel (4.0.2) - arel-helpers (2.1.1) - activerecord (= 4.0.13) + arel-helpers (2.2.0) + activerecord (>= 3.1.0, < 5) aruba (0.6.2) childprocess (>= 0.3.6) cucumber (>= 1.1.1) @@ -95,6 +96,8 @@ GEM factory_girl_rails (4.5.0) factory_girl (~> 4.5.0) railties (>= 3.0.0) + faraday (0.9.2) + multipart-post (>= 1.2, < 3) ffi (1.9.8) filesize (0.1.1) fivemat (1.3.2) @@ -139,17 +142,20 @@ GEM mime-types (2.6.1) mini_portile2 (2.0.0) minitest (4.7.5) - msgpack (0.7.1) + msgpack (0.7.4) multi_json (1.11.2) multi_test (0.1.2) + multipart-post (2.0.0) network_interface (0.0.1) nokogiri (1.6.7.2) mini_portile2 (~> 2.0.0.rc2) + octokit (4.2.0) + sawyer (~> 0.6.0, >= 0.5.3) openssl-ccm (1.2.1) packetfu (1.1.11) network_interface (~> 0.0) pcaprub (~> 0.12) - pcaprub (0.12.0) + pcaprub (0.12.1) pg (0.18.4) pg_array_parser (0.0.9) postgres_ext (2.4.1) @@ -200,8 +206,11 @@ GEM rspec-mocks (~> 3.3.0) rspec-support (~> 3.3.0) rspec-support (3.3.0) - rubyntlm (0.5.2) + rubyntlm (0.6.0) rubyzip (1.1.7) + sawyer (0.6.0) + addressable (~> 2.3.5) + faraday (~> 0.8, < 0.10) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) simplecov (0.9.2) @@ -238,6 +247,7 @@ DEPENDENCIES factory_girl_rails (~> 4.5.0) fivemat (~> 1.3.1) metasploit-framework! + octokit (~> 4.0) pry rake (>= 10.0.0) redcarpet diff --git a/data/markdown.css b/data/markdown.css new file mode 100644 index 0000000000..8bad383d05 --- /dev/null +++ b/data/markdown.css @@ -0,0 +1,233 @@ +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote { + margin: 0; + padding: 0; +} +body { + font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #737373; + margin: 10px 13px 10px 13px; +} +a { + color: #0069d6; +} +a:hover { + color: #0050a3; + text-decoration: none; +} +a img { + border: none; +} +p { + margin-bottom: 9px; +} +h1, +h2, +h3, +h4, +h5, +h6 { + color: #404040; + line-height: 36px; +} +h1 { + margin-bottom: 18px; + font-size: 30px; +} +h2 { + font-size: 24px; +} +h3 { + font-size: 18px; +} +h4 { + font-size: 16px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 13px; +} +hr { + margin: 0 0 19px; + border: 0; + border-bottom: 1px solid #ccc; +} +blockquote { + padding: 13px 13px 21px 15px; + margin-bottom: 18px; + font-family:georgia,serif; + font-style: italic; +} +blockquote:before { + content:"\201C"; + font-size:40px; + margin-left:-10px; + font-family:georgia,serif; + color:#eee; +} +blockquote p { + font-size: 14px; + font-weight: 300; + line-height: 18px; + margin-bottom: 0; + font-style: italic; +} +code, pre { + font-family: Monaco, Andale Mono, Courier New, monospace; +} +code { + background-color: #fee9cc; + color: rgba(0, 0, 0, 0.75); + padding: 1px 3px; + font-size: 12px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +pre { + display: block; + padding: 14px; + margin: 0 0 18px; + line-height: 16px; + font-size: 11px; + border: 1px solid #d9d9d9; + white-space: pre-wrap; + word-wrap: break-word; +} +pre code { + background-color: #fff; + color:#737373; + font-size: 11px; + padding: 0; +} +@media screen and (min-width: 768px) { + body { + width: 748px; + margin:10px auto; + } +} + + +/* +Description: Foundation 4 docs style for highlight.js +Author: Dan Allen +Website: http://foundation.zurb.com/docs/ +Version: 1.0 +Date: 2013-04-02 +*/ + +pre code { + display: block; padding: 0.5em; + background: #eee; +} + +pre .decorator, +pre .annotation { + color: #000077; +} + +pre .attribute { + color: #070; +} + +pre .value, +pre .string, +pre .scss .value .string { + color: #d14; +} + +pre .comment { + color: #998; + font-style: italic; +} + +pre .function .title { + color: #900; +} + +pre .class { + color: #458; +} + +pre .id, +pre .pseudo, +pre .constant, +pre .hexcolor { + color: teal; +} + +pre .variable { + color: #336699; +} + +pre .javadoc { + color: #997700; +} + +pre .pi, +pre .doctype { + color: #3344bb; +} + +pre .number { + color: #099; +} + +pre .important { + color: #f00; +} + +pre .label { + color: #970; +} + +pre .preprocessor { + color: #579; +} + +pre .reserved, +pre .keyword, +pre .scss .value { + color: #000; +} + +pre .regexp { + background-color: #fff0ff; + color: #880088; +} + +pre .symbol { + color: #990073; +} + +pre .symbol .string { + color: #a60; +} + +pre .tag { + color: #007700; +} + +pre .at_rule, +pre .at_rule .keyword { + color: #088; +} + +pre .at_rule .preprocessor { + color: #808; +} + +pre .scss .tag, +pre .scss .attribute { + color: #339; +} \ No newline at end of file diff --git a/lib/msf/ui/console/command_dispatcher/core.rb b/lib/msf/ui/console/command_dispatcher/core.rb index a05d06080f..451dea0f28 100644 --- a/lib/msf/ui/console/command_dispatcher/core.rb +++ b/lib/msf/ui/console/command_dispatcher/core.rb @@ -16,6 +16,7 @@ require 'msf/ui/console/command_dispatcher/nop' require 'msf/ui/console/command_dispatcher/payload' require 'msf/ui/console/command_dispatcher/auxiliary' require 'msf/ui/console/command_dispatcher/post' +require 'msf/util/document_generator' module Msf module Ui @@ -743,7 +744,9 @@ class Core def cmd_info_help print_line "Usage: info [mod2 mod3 ...]" print_line - print_line "Optionally the flag '-j' will print the data in json format" + print_line "Options:" + print_line "* The flag '-j' will print the data in json format" + print_line "* The flag '-d' will show the markdown version with a browser" print_line "Queries the supplied module or modules for information. If no module is given," print_line "show info for the currently active module." print_line @@ -754,15 +757,24 @@ class Core # def cmd_info(*args) dump_json = false + show_doc = false + if args.include?('-j') args.delete('-j') dump_json = true end + if args.include?('-d') + args.delete('-d') + show_doc = true + end + if (args.length == 0) if (active_module) if dump_json print(Serializer::Json.dump_module(active_module) + "\n") + elsif show_doc + Msf::Util::DocumentGenerator.get_module_document(active_module) else print(Serializer::ReadableText.dump_module(active_module)) end @@ -783,6 +795,8 @@ class Core print_error("Invalid module: #{name}") elsif dump_json print(Serializer::Json.dump_module(mod) + "\n") + elsif show_doc + Msf::Util::DocumentGenerator.get_module_document(mod) else print(Serializer::ReadableText.dump_module(mod)) end diff --git a/lib/msf/util/document_generator.rb b/lib/msf/util/document_generator.rb new file mode 100644 index 0000000000..a4b53b4fc8 --- /dev/null +++ b/lib/msf/util/document_generator.rb @@ -0,0 +1,285 @@ +### +# +# This provides methods to generate documentation for a module. +# +### + +require 'octokit' +require 'nokogiri' +require 'redcarpet' +require 'net/http' +require 'erb' + +module Redcarpet + module Render + class MsfMdHTML < Redcarpet::Render::HTML + def block_code(code, language) + "
" \
+          "#{code}" \
+        "
" + end + end + end +end + + +module Msf + module Util + module DocumentGenerator + + class HTMLwithPygments < Redcarpet::Render::HTML + def block_code(code, language) + "Nope" + end + end + + class DocumentNormalizer + + CSS_BASE_PATH = File.expand_path(File.join(Msf::Config.data_directory, 'markdown.css' )) + + def get_md_content(items) + md_to_html(ERB.new(%Q|## #{items[:mod_name]} + + #{normalize_description(items[:mod_description])} + + ## Module Name + + #{items[:mod_fullname]} + + ## Authors + + #{normalize_authors(items[:mod_authors])} + + <% unless items[:mod_pull_requests].empty? %> + ## Related Pull Requests + + #{normalize_pull_requests(items[:mod_pull_requests])} + <% end %> + + <% unless items[:mod_refs].empty? %> + ## References + + #{normalize_references(items[:mod_refs])} + <% end %> + + ## Platforms + #{normalize_platforms(items[:mod_platforms])} + + ## Reliability + #{normalize_rank(items[:mod_rank])} + + ## Demo + + #{normalize_demo_output(items[:mod_demo])} + |).result(binding())) + end + + private + + def md_to_html(md) + md.gsub!(/\x20{12}/, '') + r = Redcarpet::Markdown.new(Redcarpet::Render::MsfMdHTML, fenced_code_blocks: true) + css_path = + %Q| + + + + + + #{r.render(md)} + + + | + end + + def normalize_pull_requests(pull_requests) + formatted_pr = [] + + pull_requests.each_pair do |number, pr| + formatted_pr << "* ##{number} - #{pr[:title]}" + end + + formatted_pr * "\n" + end + + def normalize_description(description) + Rex::Text.wordwrap(Rex::Text.compress(description)) + end + + def normalize_authors(authors) + if authors.kind_of?(Array) + authors.collect { |a| "* #{a}" } * "\n" + else + authors + end + end + + def normalize_targets(targets) + targets.collect { |c| "* #{c.name}" } * "\n" + end + + def normalize_references(refs) + refs.collect { |r| "* #{r}" } * "\n" + end + + def normalize_platforms(platforms) + if platforms.kind_of?(Array) + platforms.collect { |p| "* #{p}" } * "\n" + else + platforms + end + end + + def normalize_rank(rank) + "[#{Msf::RankingName[rank].capitalize}](https://github.com/rapid7/metasploit-framework/wiki/Exploit-Ranking)" + end + + def normalize_demo_output(mod) + %Q|``` + msf > use #{mod.fullname} + msf #{mod.type}(#{mod.shortname}) > show targets + ... a list of targets ... + msf #{mod.type}(#{mod.shortname}) > set TARGET + msf #{mod.type}(#{mod.shortname}) > show options + ... show and set options ... + msf #{mod.type}(#{mod.shortname}) > run + ```| + end + + end + + class PullRequestFinder + class Exception < RuntimeError; end + + MANUAL_BASE_PATH = File.expand_path(File.join(Msf::Config.module_directory, '..', 'documentation', 'modules' )) + + attr_accessor :git_client + attr_accessor :repository + attr_accessor :branch + attr_accessor :owner + attr_accessor :git_access_token + + def initialize + unless ENV.has_key?('GITHUB_OAUTH_TOKEN') + raise PullRequestFinder::Exception, 'GITHUB_OAUTH_TOKEN environment variable not set.' + end + + self.owner = 'rapid7' + self.repository = "#{owner}/metasploit-framework" + self.branch = 'master' + self.git_access_token = ENV['GITHUB_OAUTH_TOKEN'] + self.git_client = Octokit::Client.new(access_token: git_access_token) + end + + def search(mod) + file_name = get_normalized_module_name(mod) + commits = get_commits_from_file(file_name) + get_pull_requests_from_commits(commits) + end + + private + + def get_normalized_module_name(mod) + source_fname = mod.method(:initialize).source_location.first + source_fname.scan(/(modules.+)/).flatten.first || '' + end + + def get_commits_from_file(path) + commits = git_client.commits(repository, branch, path: path) + if commits.empty? + # Possibly the path is wrong. + raise PullRequestFinder::Exception, 'No commits found.' + end + + commits + end + + def get_author(commit) + if commit.author + return commit.author[:login].to_s + end + + '' + end + + def is_author_blacklisted?(commit) + ['tabassassin'].include?(get_author(commit)) + end + + def get_pull_requests_from_commits(commits) + pull_requests = {} + + commits.each do |commit| + next if is_author_blacklisted?(commit) + + pr = get_pull_request_from_commit(commit) + unless pr.empty? + pull_requests[pr[:number]] = pr + end + end + + pull_requests + end + + def get_pull_request_from_commit(commit) + sha = commit.sha + url = URI.parse("https://github.com/#{repository}/branch_commits/#{sha}") + cli = Net::HTTP.new(url.host, url.port) + cli.use_ssl = true + req = Net::HTTP::Get.new(url.request_uri) + res = cli.request(req) + n = Nokogiri::HTML(res.body) + found_pr_link = n.at('li[@class="pull-request"]//a') + + # If there is no PR associated with this commit, it's probably from the SVN days. + return {} unless found_pr_link + + href = found_pr_link.attributes['href'].text + title = found_pr_link.attributes['title'].text + + # Filter out all the pull requests that do not belong to rapid7. + # If this happens, it's probably because the PR was submitted to somebody's fork. + return {} unless /^\/#{owner}\// === href + + { number: href.scan(/\d+$/).flatten.first, title: title } + end + + end + + def self.get_module_document(mod) + manual_path = File.join(PullRequestFinder::MANUAL_BASE_PATH, mod.fullname) + + if File.exists?(manual_path) + Rex::Compat.open_webrtc_browser("file://#{manual_path}") + else + pr_finder = PullRequestFinder.new + pr = pr_finder.search(mod) + n = DocumentNormalizer.new + items = { + mod_description: mod.description, + mod_authors: mod.send(:module_info)['Author'], + mod_fullname: mod.fullname, + mod_name: mod.name, + mod_pull_requests: pr, + mod_refs: mod.references, + mod_rank: mod.rank, + mod_platforms: mod.send(:module_info)['Platform'], + mod_options: mod.datastore, + mod_demo: mod + } + + if mod.respond_to?(:targets) && mod.targets + items[:mod_targets] = mod.targets + end + + md = n.get_md_content(items) + f = Rex::Quickfile.new(["#{mod.shortname}_doc", '.html']) + f.write(md) + f.close + Rex::Compat.open_webrtc_browser("file://#{f.path}") + end + end + + end + end +end