|
| 1 | +require 'jekyll' |
| 2 | +require 'json' |
| 3 | +require 'pathname' |
| 4 | + |
| 5 | +# |
| 6 | +# Helper class for extracting information related to Metasploit framework's stats |
| 7 | +# |
| 8 | +class MetasploitStats |
| 9 | + # @return [Hash<String, Integer>] A map of module type to the amount of modules |
| 10 | + def module_counts |
| 11 | + module_counts_by_type = modules.group_by { |mod| mod['type'].to_s }.transform_values { |mods| mods.count }.sort_by(&:first).to_h |
| 12 | + module_counts_by_type |
| 13 | + end |
| 14 | + |
| 15 | + # @return [Array<Hash<String, Hash>>] A nested array of module metadata, containing at least the keys :name, :total, :children |
| 16 | + def nested_module_counts |
| 17 | + create_nested_module_counts(modules) |
| 18 | + end |
| 19 | + |
| 20 | + protected |
| 21 | + |
| 22 | + # @param [Array<Hash>] modules |
| 23 | + # @param [String] parent_path The parent path to track the nesting depth when called recursively |
| 24 | + # i.e. auxiliary, then auxiliary/admin, then auxiliary/admin/foo, etc |
| 25 | + def create_nested_module_counts(modules, parent_path = '') |
| 26 | + # Group the modules by their prefix, i.e. auxiliary/payload/encoder/etc |
| 27 | + top_level_buckets = modules.select { |mod| mod['fullname'].start_with?(parent_path) }.group_by do |mod| |
| 28 | + remaining_paths = mod['fullname'].gsub(parent_path.empty? ? '' : %r{^#{parent_path}/}, '').split('/') |
| 29 | + remaining_paths[0] |
| 30 | + end.sort.to_h |
| 31 | + |
| 32 | + top_level_buckets.map do |(prefix, children)| |
| 33 | + current_path = parent_path.empty? ? prefix : "#{parent_path}/#{prefix}" |
| 34 | + mod = modules_by_fullname[current_path] |
| 35 | + { |
| 36 | + name: prefix, |
| 37 | + total: children.count, |
| 38 | + module_fullname: mod ? mod['fullname'] : nil, |
| 39 | + module_path: mod ? mod['path'] : nil, |
| 40 | + children: mod.nil? ? create_nested_module_counts(children, current_path) : [] |
| 41 | + } |
| 42 | + end |
| 43 | + end |
| 44 | + |
| 45 | + # @return [Array<Hash>] An array of Hashes containing each Metasploit module's metadata |
| 46 | + def modules |
| 47 | + return @modules if @modules |
| 48 | + |
| 49 | + module_metadata_path = '../db/modules_metadata_base.json' |
| 50 | + unless File.exist?(module_metadata_path) |
| 51 | + raise "Unable to find Metasploit module data, expected it to be at #{module_metadata_path}" |
| 52 | + end |
| 53 | + |
| 54 | + @modules = JSON.parse(File.binread(module_metadata_path)).values |
| 55 | + @modules |
| 56 | + end |
| 57 | + |
| 58 | + # @return [Hash<String, Hash>] A mapping of module name to Metasploit module metadata |
| 59 | + def modules_by_fullname |
| 60 | + @modules_by_fullname ||= @modules.each_with_object({}) do |mod, hash| |
| 61 | + fullname = mod['fullname'] |
| 62 | + hash[fullname] = mod |
| 63 | + end |
| 64 | + end |
| 65 | +end |
| 66 | + |
| 67 | +# Custom liquid filter implementation for visualizing nested Metasploit module metadata |
| 68 | +# |
| 69 | +# Intended usage: |
| 70 | +# {{ site.metasploit_nested_module_counts | module_tree }} |
| 71 | +module ModuleFilter |
| 72 | + # @param [Array<Hash>] modules The array of Metasploit cache information |
| 73 | + # @return [String] The module tree HTML representation of the given modules |
| 74 | + def module_tree(modules) |
| 75 | + rendered_children = render_modules(modules) |
| 76 | + |
| 77 | + <<~EOF |
| 78 | + <ul class="module-structure">#{rendered_children}</ul> |
| 79 | + EOF |
| 80 | + end |
| 81 | + |
| 82 | + module_function |
| 83 | + |
| 84 | + # @param [Array<Hash>] modules The array of Metasploit cache information |
| 85 | + # @return [String] The rendered tree HTML representation of the given modules |
| 86 | + def render_modules(modules) |
| 87 | + modules.map do |mod| |
| 88 | + result = "<li#{render_child_modules?(mod) ? ' class="folder"' : ''}>#{heading_for_mod(mod)}" |
| 89 | + if render_child_modules?(mod) |
| 90 | + result += "\n<ul>#{render_modules(mod[:children].sort_by { |mod| "#{render_child_modules?(mod) ? 0 : 1}-#{mod[:name]}" })}</ul>\n" |
| 91 | + end |
| 92 | + result += "</li>" |
| 93 | + result |
| 94 | + end.join("\n") |
| 95 | + end |
| 96 | + |
| 97 | + # @param [Hash] mod The module metadata object |
| 98 | + # @return [String] Human readable string for a module list such as `- <a>Auxiliary (1234)</a>` or `- Other (50)` |
| 99 | + def heading_for_mod(mod) |
| 100 | + if render_child_modules?(mod) |
| 101 | + "<a href=\"#\"><div class=\"target\">#{mod[:name]} (#{mod[:total]})</div></a>" |
| 102 | + else |
| 103 | + config = Jekyll.sites.first.config |
| 104 | + # Preference linking to module documentation over the module implementation |
| 105 | + module_docs_path = Pathname.new("documentation").join(mod[:module_path].gsub(/^\//, '')).sub_ext(".md") |
| 106 | + link_path = File.exist?(File.join('..', module_docs_path)) ? "/#{module_docs_path}" : mod[:module_path] |
| 107 | + docs_link = "#{config['gh_edit_repository']}/#{config['gh_edit_view_mode']}/#{config['gh_edit_branch']}#{link_path}" |
| 108 | + "<a href=\"#{docs_link}\" target=\"_blank\"><div class=\"target\">#{mod[:module_fullname]}</div></a>" |
| 109 | + end |
| 110 | + end |
| 111 | + |
| 112 | + # @param [Hash] mod The module metadata object |
| 113 | + # @return [TrueClass, FalseClass] |
| 114 | + def render_child_modules?(mod) |
| 115 | + mod[:children].length >= 1 && mod[:module_path].nil? |
| 116 | + end |
| 117 | +end |
| 118 | + |
| 119 | +# Register the Liquid filter so any Jekyll page can render module information |
| 120 | +Liquid::Template.register_filter(ModuleFilter) |
| 121 | + |
| 122 | +# Register the site initialization hook to populate global site information so any Jekyll page can access Metasploit stats information |
| 123 | +Jekyll::Hooks.register :site, :after_init do |site| |
| 124 | + begin |
| 125 | + Jekyll.logger.info 'Calculating module stats' |
| 126 | + |
| 127 | + metasploit_stats = MetasploitStats.new |
| 128 | + |
| 129 | + site.config['metasploit_total_module_count'] = metasploit_stats.module_counts.sum { |_type, count| count } |
| 130 | + site.config['metasploit_module_counts'] = metasploit_stats.module_counts |
| 131 | + site.config['metasploit_nested_module_counts'] = metasploit_stats.nested_module_counts |
| 132 | + |
| 133 | + Jekyll.logger.info 'Finished calculating module stats' |
| 134 | + rescue |
| 135 | + Jekyll.logger.error "Unable to to extractMetasploit stats" |
| 136 | + raise |
| 137 | + end |
| 138 | +end |
0 commit comments