Open-source, cross-platform document discovery and metadata leak analysis tool.
Designed as a spiritual successor to FOCA, without the Windows lock-in.
🇬🇧 English · 🇹🇷 Türkçe
⭐ If MetaScout is useful to you, consider starring the repo — it helps others find it too.
- What is this?
- Features
- Installation
- Quick start
- Scanning multiple targets
- Scanning a manual URL list
- Web UI
- REST API
- Docker
- Wayback Machine discovery
- Keyless search with DDGS
- Subdomain enumeration
- Content scanning for personal data (PII)
- Critical / sensitive files discovery
- Search engine API keys
- Full CLI reference
- Output layout
- Architecture
- Testing
- Troubleshooting
- Responsible use
- License
FOCA was the go-to tool for metadata-based information leakage testing for years, but it's unmaintained now and Windows-only. MetaScout rewrites the same idea (find documents published on a target site, extract their metadata, report what leaks) in Python: a fully command-line tool that installs the same way on macOS, Linux, and Windows.
It discovers PDF/Office documents published on a target, downloads them, extracts their metadata with ExifTool, and reports:
- Usernames (document authors, last-modified-by, home directory paths)
- Email addresses
- Software / version info (Office build, PDF producer, etc.)
- Operating system hints
- Internal file paths (
C:\Users\..., network shares) - Server / printer names (UNC paths,
\\server\share) - GPS coordinates, when a photo with location EXIF data (e.g. a phone photo pasted into a Word doc) is embedded in a document — a link to view it on a map is included in the report
- Multiple document discovery methods: direct site crawling,
sitemap.xml/robots.txtparsing, the Wayback Machine's archive (finds files no longer live on the site), and optional search engine dorking (Google/Serper/Bravesite: filetype:) - Multi-target scanning: scan dozens of domains belonging to one organization in a single run and get one merged report
- CLI and local web UI:
metascout scanfrom the terminal, ormetascout webfor a browser form - Passive subdomain enumeration via crt.sh (Certificate Transparency logs), no API key required, each subdomain gets scanned too
- Respects
robots.txtby default and sends an honest, non-spoofed User-Agent - Concurrent downloads with a size cap and sha256 deduplication
- Detailed HTML report (dark theme, findings grouped by category, English or Turkish) plus a JSON report for automation
- Opt-in document content scan for personal/critical data — national ID numbers, emails/phones, IBANs/card numbers, address/DOB hints, and signature hints — on top of the always-on metadata scan (see Content scanning for personal data)
- No exotic dependencies: the only native component is
exiftool, available on every platform
- Python 3.10 or newer
- ExifTool (required for metadata extraction)
- Git (optional, to clone the repo)
# No Homebrew? https://brew.sh
brew install exiftool python@3.12 git
git clone https://github.com/gorkemguler/metascout.git
cd metascout
python3 -m venv .venv
source .venv/bin/activate
pip install -e .On MacPorts, use sudo port install p5-image-exiftool instead.
Debian / Ubuntu
sudo apt update
sudo apt install -y libimage-exiftool-perl python3-venv python3-pip git
git clone https://github.com/gorkemguler/metascout.git
cd metascout
python3 -m venv .venv
source .venv/bin/activate
pip install -e .Fedora / RHEL / CentOS
sudo dnf install -y perl-Image-ExifTool python3 git
git clone https://github.com/gorkemguler/metascout.git
cd metascout
python3 -m venv .venv
source .venv/bin/activate
pip install -e .Arch Linux
sudo pacman -S perl-image-exiftool python git
git clone https://github.com/gorkemguler/metascout.git
cd metascout
python3 -m venv .venv
source .venv/bin/activate
pip install -e .1. Install Python
Download Python 3.10+ from python.org/downloads. On the installer screen, make sure "Add python.exe to PATH" is checked.
2. Install ExifTool (pick one)
- Chocolatey (in an elevated PowerShell):
choco install exiftool
- Scoop:
scoop install exiftool
- Manual: download the "Windows Executable" zip from exiftool.org,
rename the extracted
exiftool(-k).exetoexiftool.exe, and either copy it into a folder already on PATH (e.g.C:\Windows\) or add its folder to the system PATH (Settings › System › Advanced system settings › Environment Variables).
3. Install the project (PowerShell)
git clone https://github.com/gorkemguler/metascout.git
cd metascout
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -e .If PowerShell blocks script execution (
.venv\Scripts\Activate.ps1 cannot be loaded), run this once as your own user (no admin needed):Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
Using plain cmd.exe instead? Activate with .venv\Scripts\activate.bat.
On any platform, with the virtual environment active:
exiftool -ver
metascout scan --helpIf both print version/help text without errors, you're set.
Note:
pip install -e .installs themetascoutcommand only into whichever.venvwas active at the time. Open a new terminal, or runmetascoutfrom outside the project directory, and you'll getzsh: command not found: metascout(or'metascout' is not recognized...on Windows). That's not broken, the venv just isn't active. Either activate it each time (source .venv/bin/activate) or install it globally below.
If you'd rather run metascout from anywhere without activating a venv every
time, use pipx: it installs the package into its own
isolated environment but puts the command on your PATH.
# macOS
brew install pipx
pipx ensurepath
# Debian/Ubuntu
sudo apt install pipx
pipx ensurepath
# Windows (PowerShell)
python -m pip install --user pipx
python -m pipx ensurepathAfter pipx ensurepath, restart your terminal (or run exec zsh / exec bash), then install from the project directory:
pipx install --editable /full/path/to/metascout--editable means code changes under src/metascout/ take effect
immediately, no reinstall needed. After this, metascout works from any
directory without activating a venv.
metascout scan example.comBy default this uses site crawling (crawl), sitemap.xml, the Wayback
Machine, and DDGS —
no API key needed for any of them. Results are written to
./metascout_output/report.html and report.json.
metascout scan example.com \
--filetypes pdf,docx,xlsx \
--max-docs 100 \
--max-crawl-pages 500 \
--output-dir ./outScan several domains belonging to the same organization in one run and get a single merged report, no need to run the tool repeatedly and stitch reports together yourself:
metascout scan example.com example.org another-example.netFor a longer list, put them in a file and use --targets-file (one domain
per line, # starts a comment):
cat > domains.txt <<EOF
# Acme Corp domains
example.com
example.org
another-example.net
EOF
metascout scan --targets-file domains.txt --subdomainsWhen more than one target is given, the generated report.html/report.json
includes a "Targets" table breaking down how many documents were found per
domain.
If a discovery engine didn't work for you (blocked API, exhausted quota,
whatever) and you ended up gathering a list of document URLs by hand — from a
browser search, another tool, anywhere — feed that list straight in with
--urls-file. Discovery is skipped for those; they're downloaded, analyzed,
and included in the report exactly like anything else the engines find:
cat > urls.txt <<EOF
# gathered manually, google engine was rejected
https://example.com/reports/2023-annual.pdf
https://example.com/files/internal-notes.docx
EOF
metascout scan --urls-file urls.txtTARGETS can be omitted when --urls-file is used — the hostnames of those
URLs become the targets automatically (used for the report header and the
per-target breakdown). Pass TARGETS or --targets-file as well to also run
normal discovery alongside the manual list; results are merged and deduped
by URL, so a document already found by an engine won't be listed twice just
because it's also in your manual file. The web UI has the same field under
"Manuel URL listesi".
If you'd rather fill out a browser form than type CLI flags:
metascout webThis opens a local UI at http://127.0.0.1:8765/ (launches in your browser
automatically), in English by default — click TR in the top-right corner
to switch the whole page to Turkish (EN/TR, also reachable directly via
?lang=tr). Enter your targets (one per line), an optional manual URL list
(see Scanning a manual URL list — leave
targets empty and it'll derive them from the URLs), file extensions,
discovery engines, the subdomain toggle, and the report language (English or
Turkish, independent of the page's own language), then hit "Start scan".
While it's running, a live log box under the button streams the same
progress lines you'd see in the terminal (documents found, engines queried,
content-scan/visual-signature progress, ...) via server-sent events, so a
long scan doesn't just look frozen behind a spinner. When the scan
finishes, the report opens right in the browser with a "Download results
(.zip)" button in the top-right corner — bundles that run's
report.html, report.json, and every downloaded document into one zip,
so getting the full output onto your own machine doesn't need filesystem
access to wherever metascout web is actually running. The same files are
also saved as-is under --output-dir (default
./metascout_output/web-<timestamp>/).
metascout web --port 9000 --output-dir ~/metascout-workspace/metascout_outputThe UI only listens on 127.0.0.1 by default (change with --host). To
use the google/serper/brave checkboxes, the matching API keys need to
be set via environment variable or .env (see Search engine API
keys).
⚠️ This has no authentication, at all. Fine for one person on their own machine (the default). If you're tempted to runmetascout web --host 0.0.0.0so a team can share one instance: don't, not directly — anyone who can reach it can start scans (against any target they choose, using your server/IP) and download every other run's results, including ones containing real PII if--scan-contentwas used. Put it behind something that actually authenticates users first — a reverse proxy with basic auth, a Tailscale/WireGuard-only network, an SSO-aware gateway — before letting more than one trusted person reach it.
"Scan Existing Documents" (top nav) is a second, separate page for a different case: you already have documents — your own files, or ones gathered some other way — and just want them analyzed, with no target and no discovery. Give it either a local directory path (searched recursively) or a URL list (downloaded directly, no discovery), plus optional content-scan and visual-signature checks, same as the main form. It's kept on its own page rather than crammed into the main form specifically to keep that one from getting harder to read as more scan options get added.
"History" (top nav) lists every past run saved under --output-dir
(both metascout scan/local-scan runs and web UI runs, since they write
the same report.json/report.html files) — target(s), document count,
and timestamp, newest first — with a "View report" link to reopen it and a
"Download (.zip)" link, so past results are reachable from the browser
alone without needing filesystem access to wherever metascout web is
actually running.
With two or more runs, the History page also has a "Compare two runs" form: pick an earlier and a later run and see exactly what's new (and what's gone) between them — new/removed documents, new/removed metadata findings per category, new/removed content-scan hits. For tracking a target over time: scan it periodically (into its own timestamped output directory each time, the default) and diff any two runs to see what changed. The same comparison is available from the CLI without a browser at all:
metascout diff metascout_output/web-20260101-100000 metascout_output/web-20260201-100000The CLI and the web UI above are both for a person running scans by hand.
metascout api is a third, separate interface for a program to do
that instead — start a scan from your own app/pipeline, poll it, and pull
the result (JSON report, HTML report, or a zip of the whole run) into
wherever you actually want it: a SIEM, a ticket, an internal dashboard,
a scheduled job, whatever the integration needs.
pip install 'metascout[api]'
metascout apiThis starts a REST API on http://127.0.0.1:8000 with interactive,
auto-generated docs (Swagger UI) at http://127.0.0.1:8000/docs — every
request/response field, tried straight from the browser, no separate API
reference to keep in sync by hand. /openapi.json gives the same thing as
a machine-readable schema, for generating a client in whatever language your
enterprise app uses.
It's job-based, not a single blocking call: a scan can take anywhere
from seconds to hours, so POST /v1/scans returns immediately with a
job_id instead of holding the HTTP connection open for the whole run —
poll GET /v1/scans/{job_id} until status is "done" (or "error"),
then fetch the result.
# Start a scan
curl -s -X POST http://127.0.0.1:8000/v1/scans \
-H 'Content-Type: application/json' \
-d '{"targets": ["example.com"], "scan_content": true, "critical_files": true}'
# -> {"job_id": "…", "status": "queued", "links": {...}, ...}
# Poll until done
curl -s http://127.0.0.1:8000/v1/scans/<job_id>
# Once status is "done":
curl -s http://127.0.0.1:8000/v1/scans/<job_id>/report.json
curl -s http://127.0.0.1:8000/v1/scans/<job_id>/report.html
curl -s -o result.zip http://127.0.0.1:8000/v1/scans/<job_id>/download| Endpoint | Description |
|---|---|
GET /v1/health |
Liveness check + version + count of currently running/queued jobs |
POST /v1/scans |
Start a scan — same options as metascout scan (targets/manual_urls, filetypes, engines, scan_content, critical_files, ...) as a JSON body |
POST /v1/local-scans |
Start a scan of a server-local directory or a fixed URL list — same as metascout local-scan |
GET /v1/scans |
List jobs (newest first) with their current status |
GET /v1/scans/{job_id} |
One job's status, timestamps, and a findings summary once done |
GET /v1/scans/{job_id}/log |
Progress log lines collected so far (works while still running) |
GET /v1/scans/{job_id}/report.json |
The full JSON report — 409 if not done yet |
GET /v1/scans/{job_id}/report.html |
The full HTML report — 409 if not done yet |
GET /v1/scans/{job_id}/download |
Zip of the whole run (reports + downloaded documents) — 409 if not done yet |
metascout api options:
| Option | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
Use 0.0.0.0 to accept connections from other machines — see the warning below first |
--port |
8000 |
Port to listen on |
--output-dir |
./metascout_output |
Where each job's report.json/report.html/downloads/ get saved (one subfolder per job, same layout as the CLI/web UI) |
--max-workers |
2 |
Maximum scans actually running at once; extra jobs queue and wait their turn |
--max-pending |
50 |
Maximum scans queued or running at once; POST /v1/scans/local-scans returns 429 past this |
Job tracking (status, in-progress log lines) is in-memory only — it
doesn't survive a server restart. A finished job's report.json/
report.html are still safely written to disk under --output-dir the
whole time, same as the CLI/web UI, so a restart never loses a completed
result — only the live status of whatever was still queued/running at that
moment. --max-pending caps how many jobs can be queued/running at once
(POST /v1/scans/local-scans returns 429 past that) so a caller can't
grow this in-memory registry without limit.
⚠️ No built-in authentication, same posture as the Web UI and the Docker image: fine on your own machine or inside a trusted network as-is, but anyone who can reach it can start scans against any target they choose using your server, and pull every job's results — PII included, if--scan-contentwas used. Put it behind something that actually authenticates callers first — a reverse proxy with an API key or mTLS, a Tailscale/WireGuard-only network, an API gateway — before letting anything outside a trusted network reach it.--host 0.0.0.0on its own does not add any authentication.
Live-verified end to end: ran metascout api for real (not just against
FastAPI's in-process test client), POSTed a local-scan job at a directory
containing a .env with a fake AWS key over HTTP, polled GET /v1/scans/{job_id} to "done", and confirmed the key came back correctly
masked in report.json — plus report.html, the zip download, and the
Swagger UI at /docs all checked in a real browser.
For running the web UI (or the REST API above) without setting up Python/ExifTool/ImageMagick/Ghostscript by hand, or for putting it somewhere other than your own laptop:
git clone https://github.com/gorkemguler/metascout.git
cd metascout
docker build -t metascout .
docker run --rm -p 127.0.0.1:8765:8765 -v "$(pwd)/metascout_output:/data" metascoutThat runs it in the foreground (--rm cleans the container up the moment
it stops, e.g. Ctrl+C) — good for trying it out. To leave it running in
the background, and have it come back on its own if it crashes or the
machine reboots, drop --rm and add -d --restart unless-stopped
instead:
docker run -d --name metascout --restart unless-stopped \
-p 127.0.0.1:8765:8765 -v "$(pwd)/metascout_output:/data" metascoutOr with docker-compose.yml (included in the repo, restart: unless-stopped already set) — add -d the same way to run it detached:
docker compose up -d --build
--restart unless-stoppedmakes the container come back once Docker itself is running again — it doesn't make Docker itself start at boot. That's a one-time setting outside this project: Docker Desktop has a "Start Docker Desktop when you sign in" preference (macOS/Windows); on Linux,sudo systemctl enable dockerdoes the equivalent for the daemon. Do that once, and the container restart policy above takes it from there on every reboot.
Either way, http://localhost:8765 on your machine reaches the container's
web UI once it's running, and every scan's output (report.html,
report.json, downloads/) lands in ./metascout_output on the host
through the volume mount — persisted across container restarts, and
reachable without docker exec-ing into the container.
The image bundles everything, including the optional content-scan
and visual-signature extras (ImageMagick + Ghostscript included) — no
separate pip install step needed inside the container. That's a real
tradeoff: the image is meaningfully bigger than a bare pip install metascout because of it (see Visual (wet) signature
detection
for why those two alone add real weight), traded for genuinely working out
of the box.
API keys (GOOGLE_API_KEY, SERPER_API_KEY, BRAVE_API_KEY, ...) work the
same way as everywhere else in this project — copy .env.example to .env,
fill in what you have, and either pass --env-file .env to docker run or
uncomment the env_file: line in docker-compose.yml.
⚠️ Same warning as the Web UI section, worth repeating here because Docker is exactly where people reach for--host 0.0.0.0: this image has no authentication built in. Thedocker run/compose examples above bind the published port to127.0.0.1on the host on purpose — only reachable from the machine running the container. If you're deploying this somewhere other people should reach (a shared server, a cloud VM), put an authenticating reverse proxy in front of it first; don't just publish the port to0.0.0.0or a public interface. Anyone who can reach an unauthenticated instance can start scans against whatever target they choose using your server, and download every previous run's results — PII included, if--scan-contentwas used.
The same image also has the api extra baked in — override CMD to run
the REST API instead of the web UI:
docker run --rm -p 127.0.0.1:8000:8000 -v "$(pwd)/metascout_output:/data" metascout \
api --host 0.0.0.0 --port 8000 --output-dir /dataSee REST API above — the same "bind to 127.0.0.1 on
the host, put an authenticating proxy in front before exposing it further"
guidance applies there too.
The wayback engine (on by default, no API key needed) queries the Wayback
Machine's CDX Server API for every document
archive.org has ever captured under a target host — including files that
were later removed, unlinked, or made unreachable on the live site. This
often surfaces old reports, drafts, or internal documents that crawling the
current site would never find.
metascout scan example.com --engines waybackIt's scoped to exactly the host you pass (no automatic subdomain expansion —
use --subdomains for that, same as the other engines). If web.archive.org
is unreachable from your network (some ISPs block it), the engine just
returns no results for that host; the rest of the scan is unaffected.
Each result is reported under its original live-site URL — the same URL crawl, sitemap, or a dork engine would report for the same file — so a document found by more than one engine still shows up once in the report, not twice. Since that original URL is often exactly what's gone, MetaScout also keeps the actual archive.org snapshot address behind the scenes and downloads from there automatically if fetching the original URL fails.
The ddgs engine wraps DDGS, a Python
library that scrapes DuckDuckGo (and, with its default auto backend, falls
back across Bing, Brave, Google, Yandex, and others) for site:/filetype:
results with no API key or account at all:
metascout scan example.com --engines ddgsUnlike the other keyless engines (wayback, crawl, sitemap), this one is
a scraper rather than an official API, so it's the most fragile option here
in principle — results depend on whatever DDGS's maintainers currently keep
working against each engine's anti-bot defenses, and sustained use can get
rate-limited. In practice it's been fast and reliable in testing (e.g. 26
real PDFs found in ~2 seconds against a real target, no errors across
repeated runs), so it's part of the default engine set. Drop it from
--engines (or uncheck it in the web UI) if you'd rather not depend on a
scraper.
Pick which engine(s) DDGS itself queries with --ddgs-backend (default
auto; also accepts a single engine like duckduckgo, google, or bing,
or a comma-separated list to try in order). Notably, --ddgs-backend google
gets you real Google search results — the same source google_dork_search
and Serper both hit — with no API key at all. The engine walks multiple
result pages per filetype (with retries/backoff on failed pages) to get past
DDGS's own one-page-per-call limit, but Google's scraping defenses push back
hard on this in practice: across repeated live tests against the same real
target, a single dork went from 26 results (one page, no pagination) up to
anywhere between 50 and 114 out of ~300 real matches with pagination enabled
— and occasionally 0, when Google was mid-block. Treat ddgs+google as a
free, no-setup way to grab a partial sample or unblock a one-off query, not
as a volume-complete substitute for serper or Google's own (soon
discontinued) API — see Search engine API keys
below for those. The same field is exposed in the web UI as "DDGS backend".
--subdomains performs passive subdomain discovery via crt.sh
(Certificate Transparency log search, no API key required); every discovered
subdomain is scanned with the same document-discovery engines (crawl/sitemap/google/serper/brave):
metascout scan example.com --subdomains --max-subdomains 30crt.sh can be slow or rate-limited at times. In that case the scan silently
continues with an empty subdomain list; the scan of the main domain is unaffected.
Everything above scans document metadata (author, software, file paths —
tags exiftool pulls out). --scan-content goes further and reads each
downloaded document's actual body text, looking for personal/critical
data:
| Category | What it detects | Confidence |
|---|---|---|
tc_kimlik |
Turkish national ID numbers | High — checksum-validated (invalid numbers are filtered out) |
email_phone |
Emails (regex) and phone numbers (via phonenumbers, Google's libphonenumber port — international, not TR-only) |
High for phones (library-validated) |
iban_card |
IBANs (any ISO 13616 country, not just Turkey) and card numbers | High — mod-97 (IBAN) / Luhn (card) checksum-validated |
address_dob |
Address-like and date-of-birth-like text patterns | Low — regex heuristics, expect false positives |
signature |
"imza"/"signature"/"signed by"-style keywords in the text, and whether a PDF has an actual cryptographic signature field (/Sig) |
Keyword hits are a hint, not proof; the structural /Sig check is reliable |
secrets |
Leaked credentials in the document body: AWS access keys, Google API keys, GitHub/Slack/Stripe tokens, PEM private key blocks, DB connection strings (postgres://user:pass@host), JWTs |
High — matched against well-known credential formats, not generic entropy scoring, so it only catches known providers' key shapes |
infra |
Leaked infrastructure info in the document body: cloud storage/file-share links (S3, GCS, Azure Blob, Drive, Dropbox, SharePoint), and internal network topology — RFC 1918 private IPs and .local/.internal/.corp/.lan hostnames |
Medium — the link/IP itself is real if matched, but its presence doesn't confirm the resource is actually misconfigured/public; still needs a manual check |
It's off by default, opt-in, and heuristic — every hit is something to verify manually, not a confirmed leak the way a metadata finding is.
pip install 'metascout[content-scan]' # one-time: pulls in pypdf + phonenumbers
metascout scan example.com --scan-content
# or a subset:
metascout scan example.com --scan-content --content-categories tc_kimlik,iban_cardThe same toggle and category checkboxes are available in the web UI, under
"Scan document content for personal/critical data (PII)" — unchecked by
default. If you enable it without installing the extra, the scan still runs
and logs which dependency is missing instead of failing outright; PDF text
extraction and phone-number detection specifically need pypdf and
phonenumbers respectively, everything else (Office/OpenDocument text
extraction, email/TC-no/IBAN/card regex, signature keywords) works without
them.
Privacy in the report itself: the more sensitive categories are masked
at detection time — a TC no. shows as 123******78, a card number as
************1111, an IBAN keeps only its first/last 4 characters — so the
report and its JSON export never become a plaintext store of the actual
values. Emails/phones and the weak address/DOB hints are shown as found,
since that's already the point of surfacing them.
Text extraction covers PDF (via pypdf), .docx/.xlsx/.pptx, and
.odt/.ods/.odp. Legacy binary Office formats (.doc/.xls/.ppt)
aren't supported — they'd need a much heavier dependency (olefile) for
comparatively rare wins, so they're skipped (metadata scanning still works
on them as normal).
Everything above, including the signature category, only sees a document's
text — a keyword like "signed by" in the body, or a PDF's /Sig field.
None of that catches a scanned page with a handwritten signature and no
text layer at all. --visual-signature adds that: it rasterizes each
page and runs the signature-detect
heuristic image pipeline (brightness threshold → connected-component
extraction → aspect-ratio/pixel-density judgement) to flag ink blobs shaped
like a handwritten signature.
This is opt-in on two independent levels by design, and — unlike the
signature text/keyword category — does not require --scan-content;
it's its own switch that works with or without the rest of content scanning:
pip install 'metascout[visual-signature]'
metascout scan example.com --visual-signature- Installing
pip install 'metascout[visual-signature]'alone does nothing — you still need--visual-signatureon the command (or the checkbox in the web UI) to actually run it. - It's a genuinely heavier dependency than the rest of this project. On top
of the pip package, it needs ImageMagick and Ghostscript installed
system-wide (Wand shells out to ImageMagick, which delegates PDF
rasterization to Ghostscript) — confirmed live: without Ghostscript, it
fails outright with a
DelegateError. Expect ~150–250MB of native libraries on top of the usual install.
Run it later instead of inline. This check is slow enough (see below)
that most scans shouldn't wait on it. metascout visual-signature-scan
runs it separately, afterward, against documents a normal scan already
downloaded — no re-discovery, no re-download:
metascout scan example.com # fast, as usual
metascout visual-signature-scan ./metascout_output # slow, run whenever you wantIt reads report.json from the given output directory, checks every
successfully-downloaded document, prints a results table, and writes
visual_signature_report.json next to it.
Live test results (real corpus, EXPERIMENTAL status confirmed): run against 162 real PDFs collected during an authorized scan (form documents, announcements, and financial reports), the first 76 processed before the run was stopped for time:
| Count | |
|---|---|
| Flagged as containing a visual signature | 26 (34%) |
| Flagged as not containing one | 50 (66%) |
| Runtime errors | 0 |
Manually reviewing a sample of the flagged documents (with the real filenames/target omitted here — this project doesn't publish which specific documents belong to whom) found both outcomes: a document with a real company stamp and handwritten signature was correctly flagged, but so were two completely blank form templates — one because of its printed "Signature:" label and checkbox-grid borders, the other because of a logo and a diagonal watermark. This is exactly the kind of false positive the "heuristic, verify manually" warning above is about — treat every hit as something to look at, not a confirmed signature.
Runtime, measured on that same run: from well under a second up to
131 seconds for a single large multi-page financial report, dominated by
Ghostscript's PDF rasterization at 200 DPI per page. The 76-document sample
took about 1 hour 21 minutes in total — budget accordingly, and prefer
visual-signature-scan on a curated subset over --visual-signature on an
entire large scan.
Other things worth knowing before turning this on:
- The upstream project has been unmaintained since October 2022 and
already triggers a
FutureWarningagainst current scikit-image (suppressed here, but it's a real signal the algorithm's dependencies are aging). - Detection is heuristic and parameter-sensitive: the default aspect-ratio window rejects very wide/flat signature shapes, so real signatures can be missed depending on scan quality and signing style — verified live with synthetic test images (a compact signature-shaped stroke was correctly flagged; the same stroke stretched wider was not).
- If the check can't run at all (dependency missing, Ghostscript missing, a corrupt file) it's treated as "couldn't confirm," not "no signature" — nothing gets added to the report for that page rather than a false negative being reported as a hit.
- Install ImageMagick/Ghostscript the same way as
exiftool:brew install imagemagick ghostscript(macOS),apt install imagemagick ghostscript(Debian/Ubuntu), or the official Windows installers from imagemagick.org and ghostscript.com.
--scan-content's PII/secrets/infra detection reads a PDF's real text
layer — a scanned page (an ID card, a signed contract photographed or
run through a copier) usually has none at all, so content scanning was
completely blind to it. pip install 'metascout[ocr]' fixes that: when a
page's extracted text comes back under ~20 characters (a strong signal it's
scanned rather than genuinely near-blank), that page gets rasterized and
run through Tesseract OCR
instead, and the result feeds into the same detectors as normal text.
pip install 'metascout[ocr]'
metascout scan example.com --scan-content # no extra flag — OCR just kicks in automaticallyNo separate CLI flag: this is automatic once the extra (plus Tesseract, ImageMagick, and Ghostscript installed system-wide — same rasterization technique as visual signature detection above) is available. If it isn't, scanned pages are simply skipped, same as before OCR support existed — nothing breaks either way.
Live-verified end to end: built a synthetic scanned PDF (an image
containing real text, no text layer at all — pypdf's own extraction on it
confirmed empty), ran it through OCR, and a checksum-verified national ID
number embedded in the image was correctly recovered and flagged. Also
confirmed the honest limit of this: OCR text is noisy, and an email address
in the same test came back with a stray space inserted
(jane.doe @example.com), enough to dodge the email regex — treat OCR-sourced
hits as a genuine improvement over finding nothing, not as reliable as a
real text layer's.
Everything above — --filetypes, dork search included — looks for
document types (pdf/doc/docx/...). --critical-files runs a second,
independent discovery pass through the exact same engines, but for
plaintext/config-style files that tend to leak simply by existing and being
indexed: an exposed .env, a debug .log, a forgotten .sql/.bak dump.
metascout scan example.com --critical-filesDefault extensions: txt,log,conf,cfg,ini,env,yml,yaml,sql,bak — override
with --critical-file-types. Off by default; whatever it finds is listed in
its own "Critical / Sensitive Files" report section (and counts toward
the risk badge) rather than mixed into the regular documents list — being
publicly reachable is itself the finding here, independent of anything found
inside the file. Combine it with --scan-content to also run the
secrets/PII scan on whatever text these files actually contain — the exact
same detectors used for documents (see
Content scanning above),
now pointed at plaintext instead of PDFs/Office files:
metascout scan example.com --critical-files --scan-content --content-categories secrets,inframetascout local-scan DIRECTORY --critical-files does the same thing
locally: files under DIRECTORY matching --critical-file-types are listed
separately from --filetypes documents, without double-counting a file
whose extension happens to be in both lists. The web UI has a matching
"Also search for critical/sensitive files" checkbox on both the main
scan form and "Scan Existing Documents".
Live-verified end to end: a local test directory with a .env
containing AWS_ACCESS_KEY_ID=AKIA... and a .log mentioning an internal
hostname — both files turned up under "Critical / Sensitive Files" (size and
status), and with --scan-content on, the AWS key was correctly masked
(AKIA****...) and the internal hostname flagged under Content Scan, same
as it would be for a PDF. Also confirmed through the web UI: a directory
with only critical files and no regular documents still produces a full
report (risk badge included) instead of the old "nothing to analyze" dead
end.
The google, serper, and brave engines run classic FOCA-style
site:target filetype:pdf dork searches; each needs an API key:
cp .env.example .env
# fill in GOOGLE_API_KEY, GOOGLE_CSE_ID and/or BRAVE_API_KEYOnce a key is set, the matching google/serper/brave engine turns on automatically,
no extra step needed (added to the CLI's default --engines list, and its
checkbox is pre-checked in the web UI). Passing --engines explicitly
overrides this auto behavior, so you'd list the engines you want yourself.
Security note:
.envis already in .gitignore, so even if you keep it in this repo folder,git add .won't pick it up. Still, the safest setup is to keep your real keys in a separate folder outside the git repository entirely, e.g.~/metascout-workspace/.env. If you install metascout globally with pipx,metascout scanreads the.envfrom whatever directory you run it in, so you can run scans without ever touching the source repo.
-
Google: create a search engine at Programmable Search Engine (configure it to search the entire web) and get an API key for the Custom Search JSON API. Free tier: 100 queries/day.
If that's not enough, set
GOOGLE_API_KEYto a comma-separated list of keys (e.g. from separate Google Cloud projects that share the sameGOOGLE_CSE_ID):GOOGLE_API_KEY=key1,key2,key3. When one key's quota runs out, the scan automatically rotates to the next one.⚠️ Google is shutting this API down entirely on 2027-01-01, and it already rejects newly created Google Cloud projects — if you're getting403 PERMISSION_DENIEDon a new project/key even though the console shows the API as "enabled," that's Google blocking new customers, not a misconfiguration on your end. There's nothing to fix; use Serper below instead. -
Serper: sign up for free at serper.dev and grab your API key. Not an official Google product like the API above — it's a third-party service that returns real Google search results as JSON, and is the recommended replacement now that Google's own API is being discontinued. Free credit is included on signup; check serper.dev for the current amount and pricing, since it can change.
-
Brave: sign up at brave.com/search/api (a free "Data for AI" tier is available) and get your
X-Subscription-Token.
metascout scan example.com --engines crawl,sitemap,wayback,google,serper,brave,ddgsmetascout scan --help
metascout web --help
metascout api --help
metascout local-scan --help
metascout visual-signature-scan --help
metascout diff --helpmetascout scan takes one or more TARGET positional arguments
(metascout scan a.com b.com), or use --targets-file instead:
| Option | Default | Description |
|---|---|---|
--targets-file |
– | File with one domain/URL per line (# for comments) |
--urls-file |
– | File with one full document URL per line to scan directly, skipping discovery for those (# for comments) |
--filetypes |
pdf,doc,docx,xls,xlsx,ppt,pptx,odt,ods,odp |
File extensions to look for |
--engines |
crawl,sitemap,wayback,ddgs (+google/serper/brave auto-added if their API key is in .env) |
Comma-separated: crawl,sitemap,wayback,google,serper,brave,ddgs |
--subdomains / --no-subdomains |
off | Enumerate subdomains via crt.sh |
--max-subdomains |
20 |
Maximum subdomains to scan |
--max-docs |
50 |
Maximum documents to download and analyze |
--max-crawl-pages |
200 |
Max pages the crawler visits per host |
--max-crawl-depth |
3 |
Max link depth for the crawler |
--concurrency |
8 |
Concurrent downloads |
--timeout |
15 |
Per-request timeout in seconds |
--max-download-mb |
50 |
Max download size per document (MB) |
--output-dir |
./metascout_output |
Output directory |
--ignore-robots |
off | Ignore robots.txt (only with explicit authorization) |
--google-api-key, --google-cse-id, --serper-api-key, --brave-api-key |
– | Can also be set via env var or .env |
--ddgs-backend |
auto |
Backend(s) for the ddgs engine, e.g. duckduckgo, google, bing, or a comma-separated list |
--scan-content / --no-scan-content |
off | Also scan document body text for PII (see Content scanning); needs pip install 'metascout[content-scan]' |
--content-categories |
tc_kimlik,email_phone,iban_card,address_dob,signature,secrets,infra |
Comma-separated subset, only used with --scan-content |
--visual-signature / --no-visual-signature |
off | EXPERIMENTAL, independent of --scan-content: visual (image-based) signature detection; slow (see above), needs pip install 'metascout[visual-signature]' + ImageMagick + Ghostscript |
--critical-files / --no-critical-files |
off | Second discovery pass for plaintext/config-style files (see above) |
--critical-file-types |
txt,log,conf,cfg,ini,env,yml,yaml,sql,bak |
Comma-separated subset, only used with --critical-files |
--json-report / --no-json-report |
on | Produce a JSON report |
--html-report / --no-html-report |
on | Produce an HTML report |
--report-lang |
en |
HTML report language: en or tr |
metascout web options:
| Option | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
Local only, don't expose to the internet |
--port |
8765 |
Port to listen on |
--output-dir |
./metascout_output |
Where scan runs get saved |
--open-browser / --no-open-browser |
on | Auto-open the browser on startup |
metascout api options — see REST API above:
| Option | Default | Description |
|---|---|---|
--host |
127.0.0.1 |
Use 0.0.0.0 to accept connections from other machines — see the warning above first |
--port |
8000 |
Port to listen on |
--output-dir |
./metascout_output |
Where each job's report/downloads get saved |
--max-workers |
2 |
Maximum scans running at the same time |
--max-pending |
50 |
Maximum scans queued or running at once; POST /v1/scans returns 429 past this — bounds memory use against an unauthenticated caller submitting jobs in a loop |
metascout local-scan DIRECTORY — analyzes documents already sitting in
DIRECTORY (searched recursively): no discovery, no download, just
metadata extraction plus whichever optional checks you ask for. The CLI
equivalent of the web UI's "Scan Existing Documents" page — for a folder of
documents you already have, not a live target. (For the URL-list
equivalent from the command line, use metascout scan --urls-file urls.txt --engines "" — no target/--targets-file needed, hostnames are derived
from the URLs and no discovery runs since --engines is empty.)
metascout local-scan ~/Downloads/reports --scan-content --visual-signatureTakes the same --filetypes, --scan-content, --content-categories,
--visual-signature, --critical-files, --critical-file-types,
--json-report/--html-report, --report-lang, and --output-dir options
as metascout scan (see the table above).
metascout visual-signature-scan REPORT_DIR — runs the EXPERIMENTAL
visual signature check (see above)
against documents from a previous scan, without re-discovering or
re-downloading anything:
metascout visual-signature-scan --help| Argument/Option | Default | Description |
|---|---|---|
REPORT_DIR |
– | A scan's output directory (contains report.json), e.g. ./metascout_output or a web-YYYYMMDD-HHMMSS folder |
--json-out |
REPORT_DIR/visual_signature_report.json |
Where to write the results |
metascout diff RUN_A RUN_B — compares two previous scan runs and prints
what's new and what's gone between them (documents, metadata findings,
content-scan hits). See the "Compare two runs" paragraph under
Web UI.
metascout diff --help| Argument/Option | Default | Description |
|---|---|---|
RUN_A |
– | The earlier run's output directory (contains report.json) |
RUN_B |
– | The later run's output directory (contains report.json) |
--json-out |
– | Optional: also write the full diff as JSON to this path |
metascout_output/
├── downloads/ raw downloaded documents (metascout scan)
├── report.html visual summary report (metascout scan)
├── report.json raw findings for automation/integration (metascout scan)
└── web-20260101-120000/ each metascout web run gets its own timestamped folder
├── downloads/
├── report.html
└── report.json
src/metascout/
├── discovery/
│ ├── crawler.py direct site crawling (robots.txt aware)
│ ├── sitemap.py sitemap.xml / sitemap index parsing
│ ├── wayback.py Wayback Machine (archive.org) CDX API discovery
│ ├── search_engines.py Google/Serper/Brave dork search
│ ├── ddgs_search.py keyless DDGS (DuckDuckGo/other engines) dork search
│ └── subdomains.py passive subdomain discovery via crt.sh
├── downloader.py concurrent downloads, size cap, sha256
├── metadata/
│ ├── exiftool_wrapper.py exiftool subprocess wrapper
│ └── analyzer.py regex + field-based extraction, per-target counts
├── content_scan/ opt-in document *content* PII scan (--scan-content)
│ ├── text_extract.py PDF (pypdf) / Office / OpenDocument text extraction
│ ├── pii_patterns.py TC no./IBAN/card checksum validators, email/phone/DOB/address/signature regex
│ ├── signature.py PDF digital-signature (/Sig field) structural check
│ ├── visual_signature.py opt-in image-based signature detection (--visual-signature)
│ └── ocr.py opt-in OCR fallback for scanned PDF pages (automatic once installed)
├── report/
│ ├── html_report.py Jinja2-based HTML report (report_en/report_tr.html.jinja)
│ └── json_report.py JSON report
├── diff.py compares two report.json payloads (new/removed docs, findings, content hits)
├── api/ opt-in REST API service (`metascout api`), separate from web.py
│ ├── app.py FastAPI app + routes (job-based: POST starts, GET polls/fetches)
│ ├── jobs.py in-memory job registry + background thread pool that runs the pipeline
│ └── schemas.py Pydantic request/response models (also drives the auto-generated /docs)
├── pipeline.py discover → download → extract → analyze flow (shared by CLI, web, and api)
├── cli.py click-based `scan` / `web` / `api` / `local-scan` / `visual-signature-scan` / `diff` commands
└── web.py Flask-based local web UI
pip install -e . pytest
pytestzsh: command not found: metascout (or 'metascout' is not recognized... on PowerShell)
metascout only exists inside the .venv you installed it into. It's not
callable from any random terminal unless that venv is active. Two fixes:
cdinto the project and activate the venv:cd /path/to/metascout && source .venv/bin/activate(Windows:.venv\Scripts\Activate.ps1)- Or do a global install with pipx to make the command available everywhere.
exiftool not found on PATH
ExifTool isn't installed or isn't on PATH. Follow the step for your platform
under Installation, then verify with exiftool -ver.
On Windows, exiftool(-k).exe works but exiftool doesn't
The file in the zip is named exiftool(-k).exe. Rename it to exiftool.exe
and place it in a folder on PATH (see the Windows install steps above).
No documents discovered
The target has no publicly linked documents matching your extensions
(default: pdf,doc,docx,...), or robots.txt is blocking the crawler. Try a
wider net with --engines crawl,sitemap,wayback,google,serper,brave,ddgs, or (only if you're
authorized) --ignore-robots.
crt.sh is unresponsive / slow The service rate-limits occasionally; the scan silently continues with an empty subdomain list. Try again in a few minutes.
Google/Serper/Brave engine prints a "skipped" warning
The corresponding API key/CSE id isn't set. See Search engine API keys.
This tool is built for your own systems or targets you have written
authorization to test. It respects robots.txt by default and sends an
honest, non-spoofed User-Agent (MetaScout/0.1), so a target's operators can
see recon traffic in their logs and block it if they want to. Using it against
systems you're not authorized to test may be illegal; that responsibility is
entirely on the user.
MIT © 2026 Görkem Güler