diff --git a/README.md b/README.md
index 32f3d4a..3f97696 100644
--- a/README.md
+++ b/README.md
@@ -316,6 +316,7 @@ end
* [ronin-repos] ~> 0.1
* [ronin-nmap] ~> 0.1
* [ronin-web-spider] ~> 0.2
+* [ronin-web-browser] ~> 0.1
## Install
@@ -392,3 +393,4 @@ along with ronin-recon. If not, see .
[ronin-masscan]: https://github.com/ronin-rb/ronin-masscan#readme
[ronin-nmap]: https://github.com/ronin-rb/ronin-nmap#readme
[ronin-web-spider]: https://github.com/ronin-rb/ronin-web-spider#readme
+[ronin-web-browser]: https://github.com/ronin-rb/ronin-web-browser#readme
diff --git a/gemspec.yml b/gemspec.yml
index 5534f80..76284fd 100644
--- a/gemspec.yml
+++ b/gemspec.yml
@@ -51,6 +51,7 @@ dependencies:
ronin-repos: ~> 0.1
ronin-nmap: ~> 0.1
ronin-web-spider: ~> 0.2
+ ronin-web-browser: ~> 0.1
development_dependencies:
bundler: ~> 2.0
diff --git a/lib/ronin/recon/builtin/web/screenshot.rb b/lib/ronin/recon/builtin/web/screenshot.rb
new file mode 100644
index 0000000..9bb554d
--- /dev/null
+++ b/lib/ronin/recon/builtin/web/screenshot.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+#
+# ronin-recon - A micro-framework and tool for performing reconnaissance.
+#
+# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
+#
+# ronin-recon 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-recon 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-recon. If not, see .
+#
+
+require 'ronin/recon/worker'
+
+require 'ronin/web/browser'
+
+module Ronin
+ module Recon
+ module Web
+ #
+ # A recon worker that takes a screenshot of a page.
+ #
+ # @since 0.2.0
+ #
+ class Screenshot < Worker
+
+ register 'web/screenshot'
+
+ summary 'Visits a website and takes a screenshot of it'
+ description <<~DESC
+ Visits a website and takes a screenshot of it.
+ DESC
+
+ accepts URL
+ outputs nil
+ concurrency 1
+
+ param :output_dir, String, required: true,
+ desc: 'The directory you want to save the screenshot to.'
+
+ param :format, Enum[:png, :jpg], required: true,
+ default: :png,
+ desc: 'The screenshot format.'
+
+ # The Web::Browser instance
+ #
+ # @return [Web::Browser]
+ #
+ # @api private
+ attr_reader :browser
+
+ #
+ # Initializes the `web/screenshot` worker.
+ #
+ # @param [Hash{Symbol => Object}] kwargs
+ # Additional keyword arguments.
+ #
+ # @api private
+ #
+ def initialize(**kwargs)
+ super(**kwargs)
+
+ @browser = Ronin::Web::Browser.new
+ end
+
+ #
+ # Visits a website and takes a screenshot of it.
+ #
+ # @param [Values::URL] url
+ # The URL of the website you want to screenshot.
+ #
+ def process(url)
+ browser.go_to(url)
+
+ path = path_for(browser.page.url)
+ FileUtils.mkdir_p(File.dirname(path))
+
+ browser.screenshot(path: path)
+ end
+
+ #
+ # Generates the file path for a given URL.
+ #
+ # @param [String] url
+ # The given url.
+ #
+ # @return [String]
+ # The relative file path that represents the URL.
+ #
+ def path_for(url)
+ page_url = URI(url)
+ path = File.join(params[:output_dir], page_url.host, page_url.request_uri)
+ path << 'index' if path.end_with?('/')
+ path << ".#{params[:format]}"
+ end
+ end
+ end
+ end
+end
diff --git a/spec/builtin/web/screenshot_spec.rb b/spec/builtin/web/screenshot_spec.rb
new file mode 100644
index 0000000..093cb3a
--- /dev/null
+++ b/spec/builtin/web/screenshot_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+require 'ronin/recon/builtin/web/screenshot'
+
+require 'webmock/rspec'
+
+describe Ronin::Recon::Web::Screenshot do
+ let(:dir) { Dir.mktmpdir('test-ronin-recon-web-screenshot') }
+
+ subject { described_class.new(params: { output_dir: dir }) }
+
+ before do
+ WebMock.disable_net_connect!(allow_localhost: true)
+ end
+
+ describe "#process" do
+ let(:url) { Ronin::Recon::Values::URL.new('https://www.example.com') }
+ let(:path) { File.join(dir,"www.example.com","index.png") }
+
+ before do
+ stub_request(:get, 'https://www.example.com')
+ .to_return(status: 200, body: "")
+ end
+
+ it "must visit a website and take a screenshot of it" do
+ subject.process(url)
+
+ expect(File.exist?(path)).to be(true)
+ end
+ end
+
+ describe "#path_for" do
+ context "when url ends with '/'" do
+ let(:url) { 'https://www.example.com/' }
+ let(:expected_path) { File.join(dir,'www.example.com','index.png') }
+
+ it "must add 'index' to the returned path" do
+ expect(subject.path_for(url)).to eq(expected_path)
+ end
+ end
+
+ context "when url does not ends with '/'" do
+ let(:url) { 'https://www.example.com/foo/bar.php' }
+ let(:expected_path) { File.join(dir, 'www.example.com','foo','bar.php.png') }
+
+ it "must return path" do
+ expect(subject.path_for(url)).to eq(expected_path)
+ end
+ end
+ end
+end