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