require 'jekyll' require 'json' require 'pathname' # # Helper class for extracting information related to Metasploit framework's stats # class MetasploitStats def total_module_count modules.length end # @return [Hash] A map of module type to the amount of modules def module_counts module_counts_by_type = modules.group_by { |mod| mod['type'].to_s }.transform_values { |mods| mods.count }.sort_by(&:first).to_h module_counts_by_type end # @return [Array>] A nested array of module metadata, containing at least the keys :name, :total, :children def nested_module_counts create_nested_module_counts(modules) end protected # @param [Array] modules # @param [String] parent_path The parent path to track the nesting depth when called recursively # i.e. auxiliary, then auxiliary/admin, then auxiliary/admin/foo, etc def create_nested_module_counts(modules, parent_path = '') # Group the modules by their prefix, i.e. auxiliary/payload/encoder/etc top_level_buckets = modules.select { |mod| mod['fullname'].start_with?(parent_path) }.group_by do |mod| remaining_paths = mod['fullname'].gsub(parent_path.empty? ? '' : %r{^#{parent_path}/}, '').split('/') remaining_paths[0] end.sort.to_h top_level_buckets.map do |(prefix, children)| current_path = parent_path.empty? ? prefix : "#{parent_path}/#{prefix}" mod = modules_by_fullname[current_path] { name: prefix, total: children.count, module_fullname: mod ? mod['fullname'] : nil, module_path: mod ? mod['path'] : nil, children: mod.nil? ? create_nested_module_counts(children, current_path) : [] } end end # @return [Array] An array of Hashes containing each Metasploit module's metadata def modules return @modules if @modules module_metadata_path = '../db/modules_metadata_base.json' unless File.exist?(module_metadata_path) raise "Unable to find Metasploit module data, expected it to be at #{module_metadata_path}" end @modules = JSON.parse(File.binread(module_metadata_path)).values @modules end # @return [Hash] A mapping of module name to Metasploit module metadata def modules_by_fullname @modules_by_fullname ||= @modules.each_with_object({}) do |mod, hash| fullname = mod['fullname'] hash[fullname] = mod end end end # Custom liquid filter implementation for visualizing nested Metasploit module metadata # # Intended usage: # {{ site.metasploit_nested_module_counts | module_tree }} module ModuleFilter # @param [Array] modules The array of Metasploit cache information # @return [String] The module tree HTML representation of the given modules def module_tree(modules, title = 'Modules', show_controls = false) rendered_children = render_modules(modules) controls = <<~EOF EOF <<~EOF
#{show_controls ? controls : ''}
EOF end module_function # @param [Array] modules The array of Metasploit cache information # @return [String] The rendered tree HTML representation of the given modules def render_modules(modules) modules.map do |mod| classes = render_child_modules?(mod) ? ' class="folder"' : '' result = "#{heading_for_mod(mod)}" if render_child_modules?(mod) result += "\n
    #{render_modules(mod[:children].sort_by { |mod| "#{render_child_modules?(mod) ? 0 : 1}-#{mod[:name]}" })}
\n" end result += "" result end.join("\n") end # @param [Hash] mod The module metadata object # @return [String] Human readable string for a module list such as `- Auxiliary (1234)` or `- Other (50)` def heading_for_mod(mod) if render_child_modules?(mod) "
#{mod[:name]} (#{mod[:total]})
" else config = Jekyll.sites.first.config # Preference linking to module documentation over the module implementation module_docs_path = Pathname.new("documentation").join(mod[:module_path].gsub(/^\//, '')).sub_ext(".md") link_path = File.exist?(File.join('..', module_docs_path)) ? "/#{module_docs_path}" : mod[:module_path] docs_link = "#{config['gh_edit_repository']}/#{config['gh_edit_view_mode']}/#{config['gh_edit_branch']}#{link_path}" "
#{mod[:module_fullname]}
" end end # @param [Hash] mod The module metadata object # @return [TrueClass, FalseClass] def render_child_modules?(mod) mod[:children].length >= 1 && mod[:module_path].nil? end end # Register the Liquid filter so any Jekyll page can render module information Liquid::Template.register_filter(ModuleFilter) # Register the site initialization hook to populate global site information so any Jekyll page can access Metasploit stats information Jekyll::Hooks.register :site, :after_init do |site| begin Jekyll.logger.info 'Calculating module stats' metasploit_stats = MetasploitStats.new site.config['metasploit_total_module_count'] = metasploit_stats.total_module_count site.config['metasploit_module_counts'] = metasploit_stats.module_counts site.config['metasploit_nested_module_counts'] = metasploit_stats.nested_module_counts Jekyll.logger.info 'Finished calculating module stats' rescue Jekyll.logger.error "Unable to to extractMetasploit stats" raise end end