diff --git a/README.md b/README.md index b609b11..4ff800b 100644 --- a/README.md +++ b/README.md @@ -39,20 +39,20 @@ Arguments: [ARGS ...] Additional arguments for the command Commands: - available completion download, install help list, ls purge remove, rm + search update, up ``` List popular wordlists available for download or installation: ```shell -$ ronin-wordlists available +$ ronin-wordlists search [ alexa-top-1000 ] * URL: https://github.com/urbanadventurer/WhatWeb/blob/master/plugin-development/alexa-top-1000.txt diff --git a/gemspec.yml b/gemspec.yml index 0c47b25..b484072 100644 --- a/gemspec.yml +++ b/gemspec.yml @@ -21,7 +21,7 @@ metadata: generated_files: - data/completions/ronin-wordlists - man/ronin-wordlists.1 - - man/ronin-wordlists-available.1 + - man/ronin-wordlists-search.1 - man/ronin-wordlists-completion.1 - man/ronin-wordlists-download.1 - man/ronin-wordlists-list.1 diff --git a/lib/ronin/wordlists/cli/commands/available.rb b/lib/ronin/wordlists/cli/commands/available.rb deleted file mode 100644 index b765c53..0000000 --- a/lib/ronin/wordlists/cli/commands/available.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true -# -# ronin-wordlists - A library and tool for managing wordlists. -# -# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com) -# -# ronin-wordlists is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published -# by the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# ronin-wordlists is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with ronin-wordlists. If not, see . -# - -require 'ronin/wordlists/cli/command' -require 'ronin/wordlists/cli/wordlist_index' - -module Ronin - module Wordlists - class CLI - module Commands - # - # Lists wordlists available for download or installation. - # - # ## Usage - # - # ronin-wordlists available [options] - # - # ## Options - # - # -h, --help Print help information - # - class Available < Command - - usage '[options]' - - description 'Lists wordlists available for download or installation' - - man_page 'ronin-wordlists-available.1' - - # - # Runs the `ronin-wordlists available` command. - # - def run - index = WordlistIndex.load - - index.each do |entry| - puts "[ #{entry.name} ]" - puts - puts " * URL: #{entry.url}" - puts " * Categories: #{entry.categories.join(', ')}" - puts " * Summary: #{entry.summary}" - puts - end - end - - end - end - end - end -end diff --git a/lib/ronin/wordlists/cli/commands/search.rb b/lib/ronin/wordlists/cli/commands/search.rb new file mode 100644 index 0000000..fd28b6f --- /dev/null +++ b/lib/ronin/wordlists/cli/commands/search.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true +# +# ronin-wordlists - A library and tool for managing wordlists. +# +# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com) +# +# ronin-wordlists is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# ronin-wordlists is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with ronin-wordlists. If not, see . +# + +require 'ronin/wordlists/cli/command' +require 'ronin/wordlists/cli/wordlist_index' + +require 'set' + +module Ronin + module Wordlists + class CLI + module Commands + # + # Lists wordlists available for download or installation. + # + # ## Usage + # + # ronin-wordlists search [options] + # + # ## Options + # + # -c, --category NAME Filters wordlists by a specific category + # -h, --help Print help information + # + # Arguments: + # [KEYWORD] Optional search keyword + # + class Search < Command + + usage '[options] [KEYWORD]' + + option :category, short: '-c', + value: { + type: String, + usage: 'NAME' + }, + desc: 'Filters wordlists by a specific category' do |category| + @categories << category + end + + argument :keyword, required: false, + desc: 'Optional search keyword' + + description 'Lists wordlists available for download or installation' + + man_page 'ronin-wordlists-search.1' + + # Wordlist categories to filter by. + # + # @return [Set] + attr_reader :categories + + # + # Initializes the `ronin-wordlists search` command. + # + # @param [Hash{Symbol => Object}] kwargs + # Additional keyword arguments for the command. + # + def initialize(**kwargs) + super(**kwargs) + + @categories = Set.new + end + + # + # Runs the `ronin-wordlists search` command. + # + # @param [String, nil] keyword + # The optional search keyword. + # + def run(keyword=nil) + search(keyword, categories: @categories).each do |entry| + print_entry(entry) + end + end + + # + # Searches for matching entries in the wordlist index file. + # + # @param [String, nil] keyword + # The optional search keyword. + # + # @param [Set, nil] categories + # The optional set of categories to filter by. + # + # @return [Enumerator::Lazy] + # The filtered wordlist index entries. + # + def search(keyword=nil, categories: Set.new) + entries = WordlistIndex.load.lazy + + unless categories.empty? + entries = entries.filter do |entry| + categories.subset?(entry.categories) + end + end + + if keyword + entries = entries.filter do |entry| + entry.name.include?(keyword) || + entry.summary.include?(keyword) || + entry.categories.include?(keyword) + end + end + + return entries + end + + # + # Prints an entry from the wordlist index file. + # + # @param [WordlistIndex::Entry] entry + # An entry from the wordlist index file. + # + def print_entry(entry) + puts "[ #{entry.name} ]" + puts + puts " * URL: #{entry.url}" + puts " * Categories: #{entry.categories.join(', ')}" + puts " * Summary: #{entry.summary}" + puts + end + + end + end + end + end +end diff --git a/man/ronin-wordlists-available.1.md b/man/ronin-wordlists-available.1.md deleted file mode 100644 index 78cc31b..0000000 --- a/man/ronin-wordlists-available.1.md +++ /dev/null @@ -1,27 +0,0 @@ -# ronin-wordlists-available 1 "2023-01-01" Ronin Wordlists "User Manuals" - -## NAME - -ronin-wordlists-available - Lists the available wordlists - -## SYNOPSIS - -`ronin-wordlists` `available` [*options*] - -## DESCRIPTION - -Lists popular wordlists that are available for download or installation, -by the `ronin-wordlists download` or `ronin-wordlists install` commands. - -## OPTIONS - -`-h`, `--help` -: Prints help information. - -## AUTHOR - -Postmodern - -## SEE ALSO - -[ronin-wordlists-download](ronin-wordlists-download.1.md) [ronin-wordlists-list](ronin-wordlists-list.1.md) [ronin-wordlists-remove](ronin-wordlists-remove.1.md) [ronin-wordlists-update](ronin-wordlists-update.1.md) diff --git a/man/ronin-wordlists-search.1.md b/man/ronin-wordlists-search.1.md new file mode 100644 index 0000000..bf3739b --- /dev/null +++ b/man/ronin-wordlists-search.1.md @@ -0,0 +1,37 @@ +# ronin-wordlists-search 1 "2023-01-01" Ronin Wordlists "User Manuals" + +## NAME + +ronin-wordlists-search - Lists the search wordlists + +## SYNOPSIS + +`ronin-wordlists` `search` [*options*] [*KEYWORD*] + +## DESCRIPTION + +Lists popular wordlists that are search for download or installation, +by the `ronin-wordlists download` or `ronin-wordlists install` commands. + +## ARGUMENTS + +*KEYWORD* +: Optional keyword to search wordlists by. + +## OPTIONS + +`-c`, `--category` *CATEGORY* +: Filters wordlists by the given category name. If the option is specified + multiple times then the wordlists belonging to all of the categories will be + displayed. + +`-h`, `--help` +: Prints help information. + +## AUTHOR + +Postmodern + +## SEE ALSO + +[ronin-wordlists-download](ronin-wordlists-download.1.md) [ronin-wordlists-list](ronin-wordlists-list.1.md) [ronin-wordlists-remove](ronin-wordlists-remove.1.md) [ronin-wordlists-update](ronin-wordlists-update.1.md) diff --git a/man/ronin-wordlists.1.md b/man/ronin-wordlists.1.md index 34683a0..e75b575 100644 --- a/man/ronin-wordlists.1.md +++ b/man/ronin-wordlists.1.md @@ -20,7 +20,7 @@ Command suite that manages wordlists. ## COMMANDS -`available` +`search` : Lists wordlists available for download or installation. `completion` @@ -61,4 +61,4 @@ Postmodern ## SEE ALSO -[ronin-wordlists-available](ronin-wordlists-available.1.md) [ronin-wordlists-completion](ronin-wordlists-completion.1.md) [ronin-wordlists-download](ronin-wordlists-download.1.md) [ronin-wordlists-list](ronin-wordlists-list.1.md) [ronin-wordlists-remove](ronin-wordlists-remove.1.md) [ronin-wordlists-update](ronin-wordlists-update.1.md) [ronin-wordlists-purge](ronin-wordlists-purge.1.md) +[ronin-wordlists-search](ronin-wordlists-search.1.md) [ronin-wordlists-completion](ronin-wordlists-completion.1.md) [ronin-wordlists-download](ronin-wordlists-download.1.md) [ronin-wordlists-list](ronin-wordlists-list.1.md) [ronin-wordlists-remove](ronin-wordlists-remove.1.md) [ronin-wordlists-update](ronin-wordlists-update.1.md) [ronin-wordlists-purge](ronin-wordlists-purge.1.md) diff --git a/spec/cli/commands/available_spec.rb b/spec/cli/commands/available_spec.rb deleted file mode 100644 index bea5155..0000000 --- a/spec/cli/commands/available_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -require 'spec_helper' -require 'ronin/wordlists/cli/commands/available' -require_relative 'man_page_example' - -describe Ronin::Wordlists::CLI::Commands::Available do - include_examples "man_page" -end diff --git a/spec/cli/commands/search_spec.rb b/spec/cli/commands/search_spec.rb new file mode 100644 index 0000000..da78d46 --- /dev/null +++ b/spec/cli/commands/search_spec.rb @@ -0,0 +1,271 @@ +require 'spec_helper' +require 'ronin/wordlists/cli/commands/search' +require_relative 'man_page_example' + +describe Ronin::Wordlists::CLI::Commands::Search do + include_examples "man_page" + + describe "#initialize" do + it "must initialize #categories to an empty Set" do + expect(subject.categories).to eq(Set.new) + end + end + + describe "options" do + context "when the '-c CATEGORY' option" do + let(:category1) { 'dns' } + let(:category2) { 'subdomains' } + + before do + subject.option_parser.parse( + ['-c', category1, '-c', category2] + ) + end + + it "must append the category names to #categories" do + expect(subject.categories).to eq(Set[category1, category2]) + end + end + end + + let(:name1) { 'rockyou' } + let(:url1) { 'https://github.com/brannondorsey/naive-hashcat/releases/download/data/rockyou.txt' } + let(:summary1) { 'Common passwords list.' } + let(:categories1) { %w[passwords] } + + let(:name2) { 'subdomains-1000' } + let(:url2) { 'https:/raw.githubusercontent./com/rbsec/dnscan/master/subdomains-1000.txt' } + let(:summary2) { 'Top 1000 most common subdomain names used by the dnscan util.' } + let(:categories2) { %w[dns subdomains] } + + let(:name3) { 'tlds' } + let(:url3) { 'https://raw.githubusercontent.com/rbsec/dnscan/master/tlds.txt' } + let(:summary3) { 'List of common TLDs used by the dnscan util.' } + let(:categories3) { %w[dns tlds] } + + let(:wordlist_index) do + Ronin::Wordlists::CLI::WordlistIndex.new( + { + name1 => Ronin::Wordlists::CLI::WordlistIndex::Entry.new( + name1, url: url1, + summary: summary1, + categories: categories1 + ), + + name2 => Ronin::Wordlists::CLI::WordlistIndex::Entry.new( + name2, url: url2, + summary: summary2, + categories: categories2 + ), + + name3 => Ronin::Wordlists::CLI::WordlistIndex::Entry.new( + name3, url: url3, + summary: summary3, + categories: categories3 + ) + } + ) + end + + describe "#run" do + before do + allow(Ronin::Wordlists::CLI::WordlistIndex).to receive(:load).and_return(wordlist_index) + end + + context "when no arguments are given" do + it "must print all entries in the wordlist index" do + expect { + subject.run + }.to output( + [ + "[ #{name1} ]", + '', + " * URL: #{url1}", + " * Categories: #{categories1.join(', ')}", + " * Summary: #{summary1}", + '', + "[ #{name2} ]", + '', + " * URL: #{url2}", + " * Categories: #{categories2.join(', ')}", + " * Summary: #{summary2}", + '', + "[ #{name3} ]", + '', + " * URL: #{url3}", + " * Categories: #{categories3.join(', ')}", + " * Summary: #{summary3}", + $/ + ].join($/) + ).to_stdout + end + end + + context "when the keyword argument is given" do + let(:keyword) { 'subdomain' } + + it "must print the entries that include the given keyword" do + expect { + subject.run(keyword) + }.to output( + [ + "[ #{name2} ]", + '', + " * URL: #{url2}", + " * Categories: #{categories2.join(', ')}", + " * Summary: #{summary2}", + $/ + ].join($/) + ).to_stdout + end + end + + context "when the category: keyword argument is given" do + let(:categories) { Set['dns'] } + + before do + subject.option_parser.parse( + categories.map { |category| + ['-c', category] + }.flatten + ) + end + + it "must print the entries who's #categories include the given categories: Set" do + expect { + subject.run + }.to output( + [ + "[ #{name2} ]", + '', + " * URL: #{url2}", + " * Categories: #{categories2.join(', ')}", + " * Summary: #{summary2}", + '', + "[ #{name3} ]", + '', + " * URL: #{url3}", + " * Categories: #{categories3.join(', ')}", + " * Summary: #{summary3}", + $/ + ].join($/) + ).to_stdout + end + end + + context "when both a keyword argument and categories: keyword argument are given" do + let(:keyword) { 'TLD' } + let(:categories) { Set['dns'] } + + before do + subject.option_parser.parse( + categories.map { |category| + ['-c', category] + }.flatten + ) + end + + it "must print the entries that contain the given keyword and who's #categories include the given categories: Set" do + expect { + subject.run(keyword) + }.to output( + [ + "[ #{name3} ]", + '', + " * URL: #{url3}", + " * Categories: #{categories3.join(', ')}", + " * Summary: #{summary3}", + $/ + ].join($/) + ).to_stdout + end + end + end + + describe "#search" do + before do + allow(Ronin::Wordlists::CLI::WordlistIndex).to receive(:load).and_return(wordlist_index) + end + + context "when no arguments are given" do + it "must return an Enumerator of all entries in the wordlist index" do + expect(subject.search.to_a).to eq(wordlist_index.to_a) + end + end + + context "when the keyword argument is given" do + let(:keyword) { 'subdomain' } + + it "must return an Enumerator of entries that include the given keyword" do + expect(subject.search(keyword).to_a).to eq( + wordlist_index.filter { |entry| + entry.name.include?(keyword) || + entry.summary.include?(keyword) || + entry.categories.include?(keyword) + } + ) + end + end + + context "when the category: keyword argument is given" do + let(:categories) { Set['dns', 'subdomains'] } + + it "must return an Enumerator of entries who's #categories include the given categories: Set" do + expect(subject.search(categories: categories).to_a).to eq( + wordlist_index.filter { |entry| + categories.subset?(entry.categories) + } + ) + end + end + + context "when both a keyword argument and categories: keyword argument are given" do + let(:keyword) { 'TLD' } + let(:categories) { Set['dns'] } + + it "must return an Enumerator of entries that contain the given keyword and who's #categories include the given categories: Set" do + expect(subject.search(keyword, categories: categories).to_a).to eq( + wordlist_index.filter { |entry| + categories.subset?(entry.categories) && + ( + entry.name.include?(keyword) || + entry.summary.include?(keyword) || + entry.categories.include?(keyword) + ) + } + ) + end + end + end + + describe "#print_entry" do + let(:name) { 'rockyou' } + let(:url) { 'https://github.com/brannondorsey/naive-hashcat/releases/download/data/rockyou.txt' } + + let(:summary) { 'Common passwords list.' } + let(:categories) { %w[passwords] } + + let(:entry) do + Ronin::Wordlists::CLI::WordlistIndex::Entry.new( + name, url: url, + summary: summary, + categories: categories + ) + end + + it "must print the entry's #name, #url, #categories, and #summary" do + expect { + subject.print_entry(entry) + }.to output( + [ + "[ #{name} ]", + '', + " * URL: #{url}", + " * Categories: #{categories.join(', ')}", + " * Summary: #{summary}", + $/ + ].join($/) + ).to_stdout + end + end +end