diff --git a/Gemfile b/Gemfile
index 8fe2f6f4..dffb0ef8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -38,6 +38,13 @@ gem 'ronin-web-browser', '~> 0.1', github: 'ronin-rb/ronin-web-browser'
 gem 'ronin-web-session_cookie', '~> 0.1', github: 'ronin-rb/ronin-web-session_cookie',
                                           branch: 'main'
 
+gem 'ronin-db',              '~> 0.2', github: 'ronin-rb/ronin-db',
+                                       branch: '0.2.0'
+gem 'ronin-db-activerecord', '~> 0.2', github: 'ronin-rb/ronin-db-activerecord',
+                                       branch: '0.2.0'
+gem 'ronin-vulns',           '~> 0.2', github: 'ronin-rb/ronin-vulns',
+                                       branch: '0.2.0'
+
 group :development do
   gem 'rake'
   gem 'rubygems-tasks',  '~> 0.1'
diff --git a/README.md b/README.md
index 2ba15980..fb176280 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@ Commands:
     session-cookie
     spider
     user-agent
+    vulns
     wordlist
     xml
 ```
@@ -600,6 +601,7 @@ For more examples, see [ronin-web-browser][ronin-web-browser-examples].
 * [ronin-web-user_agents] ~> 0.1
 * [ronin-web-session_cookie] ~> 0.1
 * [ronin-core] ~> 0.2
+* [ronin-vulns] ~> 0.2
 
 ## Install
 
diff --git a/gemspec.yml b/gemspec.yml
index efd4ffd0..6ea3b9b7 100644
--- a/gemspec.yml
+++ b/gemspec.yml
@@ -38,6 +38,7 @@ generated_files:
   - man/ronin-web-reverse-proxy.1
   - man/ronin-web-session-cookie.1
   - man/ronin-web-user-agent.1
+  - man/ronin-web-vulns.1
   - man/ronin-web-wordlist.1
   - man/ronin-web-xml.1
 
@@ -56,6 +57,7 @@ dependencies:
   ronin-web-user_agents: ~> 0.1
   ronin-web-session_cookie: ~> 0.1
   ronin-core: ~> 0.2
+  ronin-vulns: ~> 0.2
 
 development_dependencies:
   bundler: ~> 2.0
diff --git a/lib/ronin/web/cli/commands/vulns.rb b/lib/ronin/web/cli/commands/vulns.rb
new file mode 100644
index 00000000..f0d5d598
--- /dev/null
+++ b/lib/ronin/web/cli/commands/vulns.rb
@@ -0,0 +1,308 @@
+# frozen_string_literal: true
+#
+# ronin-web - A collection of useful web helper methods and commands.
+#
+# Copyright (c) 2006-2024 Hal Brodigan (postmodern.mod3 at gmail.com)
+#
+# ronin-web is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ronin-web 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with ronin-web.  If not, see <https://www.gnu.org/licenses/>.
+#
+
+require 'ronin/web/cli/command'
+require 'ronin/web/cli/spider_options'
+require 'ronin/core/cli/logging'
+require 'ronin/vulns/url_scanner'
+require 'ronin/vulns/cli/printing'
+require 'ronin/vulns/cli/importable'
+
+module Ronin
+  module Web
+    class CLI
+      module Commands
+        #
+        # ## Usage
+        #
+        #     ronin-web vulns [options] {--host HOST | --domain DOMAIN | --site URL}
+        #
+        # ## Options
+        #
+        #         --host HOST                  Spiders the specific HOST
+        #         --domain DOMAIN              Spiders the whole domain
+        #         --site URL                   Spiders the website, starting at the URL
+        #         --open-timeout SECS          Sets the connection open timeout
+        #         --read-timeout SECS          Sets the read timeout
+        #         --ssl-timeout SECS           Sets the SSL connection timeout
+        #         --continue-timeout SECS      Sets the continue timeout
+        #         --keep-alive-timeout SECS    Sets the connection keep alive timeout
+        #     -P, --proxy PROXY                Sets the proxy to use.
+        #     -H, --header NAME: VALUE         Sets a default header
+        #         --host-header NAME=VALUE     Sets a default header
+        #     -u chrome-linux|chrome-macos|chrome-windows|chrome-iphone|chrome-ipad|chrome-android|firefox-linux|firefox-macos|firefox-windows|firefox-iphone|firefox-ipad|firefox-android|safari-macos|safari-iphone|safari-ipad|edge,
+        #         --user-agent                 The User-Agent to use
+        #     -U, --user-agent-string STRING   The User-Agent string to use
+        #     -R, --referer URL                Sets the Referer URL
+        #         --delay SECS                 Sets the delay in seconds between each request
+        #     -l, --limit COUNT                Only spiders up to COUNT pages
+        #     -d, --max-depth DEPTH            Only spiders up to max depth
+        #         --enqueue URL                Adds the URL to the queue
+        #         --visited URL                Marks the URL as previously visited
+        #         --strip-fragments            Enables/disables stripping the fragment component of every URL
+        #         --strip-query                Enables/disables stripping the query component of every URL
+        #         --visit-host HOST            Visit URLs with the matching host name
+        #         --visit-hosts-like /REGEX/   Visit URLs with hostnames that match the REGEX
+        #         --ignore-host HOST           Ignore the host name
+        #         --ignore-hosts-like /REGEX/  Ignore the host names matching the REGEX
+        #         --visit-port PORT            Visit URLs with the matching port number
+        #         --visit-ports-like /REGEX/   Visit URLs with port numbers that match the REGEX
+        #         --ignore-port PORT           Ignore the port number
+        #         --ignore-ports-like /REGEX/  Ignore the port numbers matching the REGEXP
+        #         --visit-link URL             Visit the URL
+        #         --visit-links-like /REGEX/   Visit URLs that match the REGEX
+        #         --ignore-link URL            Ignore the URL
+        #         --ignore-links-like /REGEX/  Ignore URLs matching the REGEX
+        #         --visit-ext FILE_EXT         Visit URLs with the matching file ext
+        #         --visit-exts-like /REGEX/    Visit URLs with file exts that match the REGEX
+        #         --ignore-ext FILE_EXT        Ignore the URLs with the file ext
+        #         --ignore-exts-like /REGEX/   Ignore URLs with file exts matching the REGEX
+        #     -r, --robots                     Specifies whether to honor robots.txt
+        #     -v, --verbose                    Enables verbose output
+        #         --lfi-os unix|windows        Sets the OS to test for
+        #         --lfi-depth COUNT            Sets the directory depth to escape up
+        #         --lfi-filter-bypass null_byte|double_escape|base64|rot13|zlib
+        #                                      Sets the filter bypass strategy to use
+        #         --rfi-filter-bypass double-encode|suffix-escape|null-byte
+        #                                      Optional filter-bypass strategy to use
+        #         --rfi-script-lang asp|asp.net|coldfusion|jsp|php|perl
+        #                                      Explicitly specify the scripting language to test for
+        #         --rfi-test-script-url URL    Use an alternative test script URL
+        #         --sqli-escape-quote          Escapes quotation marks
+        #         --sqli-escape-parens         Escapes parenthesis
+        #         --sqli-terminate             Terminates the SQL expression with a --
+        #         --ssti-test-expr {X*Y | X/Z | X+Y | X-Y}
+        #                                      Optional numeric test to use
+        #         --open-redirect-url URL      Optional test URL to try to redirect to
+        #
+        # @since 2.0.0
+        #
+        class Vulns < Command
+
+          include Core::CLI::Logging
+          include SpiderOptions
+          include Ronin::Vulns::CLI::Printing
+          include Ronin::Vulns::CLI::Importable
+
+          option :first, desc: 'Stops spidering once the first vulnerability is found' do
+            @scan_mode = :first
+          end
+
+          option :all, short: '-A',
+                       desc:  'Spiders every URL and tests every param' do
+                         @scan_mode = :all
+                       end
+
+          option :print_curl, desc: 'Also prints an example curl command for each vulnerability'
+
+          option :print_http, desc: 'Also prints an example HTTP request for each vulnerability'
+
+          option :import, desc: 'Imports discovered vulnerabilities into the database'
+
+          option :lfi_os, value: {
+                            type: [:unix, :windows]
+                          },
+                          desc: 'Sets the OS to test for'
+
+          option :lfi_depth, value: {
+                               type:  Integer,
+                               usage: 'COUNT'
+                             },
+                             desc: 'Sets the directory depth to escape up'
+
+          option :lfi_filter_bypass, value: {
+                                       type: [
+                                         :null_byte,
+                                         :double_escape,
+                                         :base64,
+                                         :rot13,
+                                         :zlib
+                                       ]
+                                     },
+                                     desc: 'Sets the filter bypass strategy to use'
+
+          option :rfi_filter_bypass, value: {
+                                       type: {
+                                         'double-encode' => :double_encode,
+                                         'suffix-escape' => :suffix_escape,
+                                         'null-byte'     => :null_byte
+                                       }
+                                     },
+                                     desc: 'Optional filter-bypass strategy to use'
+
+          option :rfi_script_lang, value: {
+                                     type:  {
+                                       'asp'        => :asp,
+                                       'asp.net'    => :asp_net,
+                                       'coldfusion' => :cold_fusion,
+                                       'jsp'        => :jsp,
+                                       'php'        => :php,
+                                       'perl'       => :perl
+                                     }
+                                   },
+                                   desc: 'Explicitly specify the scripting language to test for'
+
+          option :rfi_test_script_url, value: {
+                                         type:  String,
+                                         usage: 'URL'
+                                       },
+                                       desc: 'Use an alternative test script URL'
+
+          option :sqli_escape_quote, desc: 'Escapes quotation marks'
+
+          option :sqli_escape_parens, desc: 'Escapes parenthesis'
+
+          option :sqli_terminate, desc: 'Terminates the SQL expression with a --'
+
+          option :ssti_test_expr, value: {
+                                    type: %r{\A\d+\s*[\*/\+\-]\s*\d+\z},
+                                    usage: '{X*Y | X/Z | X+Y | X-Y}'
+                                  },
+                                  desc: 'Optional numeric test to use' do |expr|
+                                    @ssti_test_expr = Ronin::Vulns::SSTI::TestExpression.parse(expr)
+                                  end
+
+          option :open_redirect_url, value: {
+                                       type:  String,
+                                       usage: 'URL'
+                                     },
+                                     desc: 'Optional test URL to try to redirect to'
+
+          description "Spiders a website and scans every URL for web vulnerabilities"
+
+          man_page 'ronin-web-vulns.1'
+
+          # The scan mode
+          #
+          # @return [:first, :all]
+          attr_reader :scan_mode
+
+          #
+          # Initializes the `ronin-web vulns` command.
+          #
+          # @param [Hash{Symbol => Object}] kwargs
+          #   Additional keyword arguments.
+          #
+          def initialize(**kwargs)
+            super(**kwargs)
+
+            @scan_mode = :all
+          end
+
+          #
+          # Runs the `ronin-web vulns` command.
+          #
+          def run
+            db_connect if options[:import]
+
+            vulns = []
+
+            begin
+              new_agent do |agent|
+                case @scan_mode
+                when :first
+                  agent.every_url do |url|
+                    log_info "Testing #{url}"
+
+                    if (vuln = test_url(url))
+                      process_vuln(vuln)
+                      vulns << vuln
+
+                      agent.stop
+                    end
+                  end
+                when :all
+                  agent.every_url do |url|
+                    log_info "Testing #{url}"
+
+                    scan_url(url) do |vuln|
+                      process_vuln(vuln)
+                      vulns << vuln
+                    end
+                  end
+                end
+              end
+            rescue Interrupt
+              puts
+            end
+
+            puts unless vulns.empty?
+            print_vulns(vulns)
+          end
+
+          #
+          # Logs and optioanlly imports a new discovered web vulnerability.
+          #
+          # @param [Ronin::Vulns::WebVuln] vuln
+          #   The discovered web vulnerability.
+          #
+          # @since 2.0.0
+          #
+          def process_vuln(vuln)
+            log_vuln(vuln)
+            import_vuln(vuln) if options[:import]
+          end
+
+          #
+          # Scans the URL for web vulnerabilities.
+          #
+          # @param [URI::HTTP, String] url
+          #   The URL to scan.
+          #
+          # @yield [vuln]
+          #   The given block will be yielded each discovered web vulnerability.
+          #
+          # @yieldparam [Ronin::Vulns::LFI,
+          #              Ronin::Vulns::RFI,
+          #              Ronin::Vulns::SQLI,
+          #              Ronin::Vulns::SSTI,
+          #              Ronin::Vulns::ReflectedXSS,
+          #              Ronin::Vulns::OpenRedirect] vuln
+          #   A discovered web vulnerability in the URL.
+          #
+          def scan_url(url,&block)
+            Ronin::Vulns::URLScanner.scan(url,&block)
+          end
+
+          #
+          # Tests the URL for web vulnerabilities and prints the first
+          # vulnerability.
+          #
+          # @param [URI::HTTP, String] url
+          #   The URL to scan.
+          #
+          # @return [Ronin::Vulns::LFI,
+          #          Ronin::Vulns::RFI,
+          #          Ronin::Vulns::SQLI,
+          #          Ronin::Vulns::SSTI,
+          #          Ronin::Vulns::ReflectedXSS,
+          #          Ronin::Vulns::OpenRedirect, nil]
+          #   The first discovered web vulnerability or `nil` if no
+          #   vulnerabilities were discovered.
+          #
+          def test_url(url)
+            Ronin::Vulns::URLScanner.test(url)
+          end
+
+        end
+      end
+    end
+  end
+end
diff --git a/man/ronin-web-vulns.1.md b/man/ronin-web-vulns.1.md
new file mode 100644
index 00000000..5d510e13
--- /dev/null
+++ b/man/ronin-web-vulns.1.md
@@ -0,0 +1,177 @@
+# ronin-web-spider 1 "2022-01-01" Ronin Web "User Manuals"
+
+## SYNOPSIS
+
+`ronin-web spider` [*options*] {`--host` *HOST* \| `--domain` *DOMAIN* \| `--site` *URL*}
+
+## DESCRIPTION
+
+Spiders a website and tests every URL for web vulnerabilities.
+
+## OPTIONS
+
+`--host` *HOST*
+  Spiders the specific *HOST*.
+
+`--domain` *DOMAIN*
+  Spiders the whole *DOMAIN*.
+
+`--site` *URL*
+  Spiders the website, starting at the *URL*.
+
+`--open-timeout` *SECS*
+  Sets the connection open timeout.
+
+`--read-timeout` *SECS*
+  Sets the read timeout.
+
+`--ssl-timeout` *SECS*
+  Sets the SSL connection timeout.
+
+`--continue-timeout` *SECS*
+  Sets the continue timeout.
+
+`--keep-alive-timeout` *SECS*
+  Sets the connection keep alive timeout.
+
+`-P`, `--proxy` *PROXY*
+  Sets the proxy to use.
+
+`-H`, `--header` "*NAME*: *VALUE*"
+  Sets a default header.
+
+`--host-header` *NAME*=*VALUE*
+  Sets a default header.
+
+`-u`, `--user-agent` chrome-linux|chrome-macos|chrome-windows|chrome-iphone|chrome-ipad|chrome-android|firefox-linux|firefox-macos|firefox-windows|firefox-iphone|firefox-ipad|firefox-android|safari-macos|safari-iphone|safari-ipad|edge
+  The `User-Agent` to use.
+
+`-U`, `--user-agent-string` *STRING*
+  The raw `User-Agent` string to use.
+
+`-R`, `--referer` *URL*
+  Sets the `Referer` URL.
+
+`--delay` *SECS*
+  Sets the delay in seconds between each request.
+
+`-l`, `--limit` *COUNT*
+  Only spiders up to *COUNT* pages.
+
+`-d`, `--max-depth` *DEPTH*
+  Only spiders up to max depth.
+
+`--enqueue` *URL*
+  Adds the URL to the queue.
+
+`--visited` *URL*
+  Marks the URL as previously visited.
+
+`--strip-fragments`
+  Enables/disables stripping the fragment component of every URL.
+
+`--strip-query`
+  Enables/disables stripping the query component of every URL.
+
+`--visit-host` *HOST*
+  Visit URLs with the matching host name.
+
+`--visit-hosts-like` `/`*REGEX*`/`
+  Visit URLs with hostnames that match the *REGEX*.
+
+`--ignore-host` *HOST*
+  Ignore the host name.
+
+`--ignore-hosts-like` `/`*REGEX*`/`
+  Ignore the host names matching the *REGEX*.
+
+`--visit-port` *PORT*
+  Visit URLs with the matching port number.
+
+`--visit-ports-like` `/`*REGEX*`/`
+  Visit URLs with port numbers that match the *REGEX*.
+
+`--ignore-port` *PORT*
+  Ignore the port number.
+
+`--ignore-ports-like` `/`*REGEX*`/`
+  Ignore the port numbers matching the *REGEXP*.
+
+`--visit-link` *URL*
+  Visit the *URL*.
+
+`--visit-links-like` `/`*REGEX*`/`
+  Visit URLs that match the *REGEX*.
+
+`--ignore-link` *URL*
+  Ignore the *URL*.
+
+`--ignore-links-like` `/`*REGEX*`/`
+  Ignore URLs matching the *REGEX*.
+
+`--visit-ext` *FILE_EXT*
+  Visit URLs with the matching file ext.
+
+`--visit-exts-like` `/`*REGEX*`/`
+  Visit URLs with file exts that match the *REGEX*.
+
+`--ignore-ext` *FILE_EXT*
+  Ignore the URLs with the file ext.
+
+`--ignore-exts-like` `/`*REGEX*`/`
+  Ignore URLs with file exts matching the REGEX.
+
+`-r`, `--robots`
+  Specifies whether to honor `robots.txt`.
+
+`--lfi-os` `unix`\|`windows`
+: Sets the OS to test for.
+
+`--lfi-depth` *NUM*
+: Sets the directory depth to escape up.
+
+`--lfi-filter-bypass` `null_byte`\|`double_escape`\|`base64`\|`rot13`\|`zlib`
+: Sets the filter bypass strategy to use.
+
+`--rfi-filter-bypass` `double-encode`\|`suffix-escape`\|`null-byte`
+: Optional filter-bypass strategy to use.
+
+`--rfi-script-lang` `asp`\|`asp.net`\|`coldfusion`\|`jsp`\|`php`\|`perl`
+: Explicitly specify the scripting language to test for.
+
+`--rfi-test-script-url` *URL*
+: Use an alternative test script URL.
+
+`--sqli-escape-quote`
+: Escapes quotation marks.
+
+`--sqli-escape-parens`
+: Escapes parenthesis.
+
+`--sqli-terminate`
+: Terminates the SQL expression with a `--`.
+
+`--ssti-test-expr` {*X*\**Y* \| *X*/*Z* \| *X*+*Y* \| *X*-*Y*}
+: Optional numeric test to use.
+
+`--open-redirect-url` *URL*
+: Optional test URL to try to redirect to.
+
+`-h`, `--help`
+  Print help information.
+
+## ENVIRONMENT
+
+*HTTP_PROXY*
+	Sets the global HTTP proxy.
+
+*RONIN_HTTP_PROXY*
+    Sets the HTTP proxy for Ronin.
+
+## AUTHOR
+
+Postmodern <postmodern.mod3@gmail.com>
+
+## SEE ALSO
+
+ronin-web-spider(1)
diff --git a/spec/cli/commands/vulns_spec.rb b/spec/cli/commands/vulns_spec.rb
new file mode 100644
index 00000000..dfe6e69a
--- /dev/null
+++ b/spec/cli/commands/vulns_spec.rb
@@ -0,0 +1,7 @@
+require 'spec_helper'
+require 'ronin/web/cli/commands/vulns'
+require_relative 'man_page_example'
+
+describe Ronin::Web::CLI::Commands::Vulns do
+  include_examples "man_page"
+end