WebScan
Discover. Verify. Report.
An asynchronous web security auditor that verifies findings against response content before it reports them — so the list you hand a developer is short, specific, and worth their time.
[!WARNING] Scan only what you are authorized to scan. Use WebScan solely on systems you own or have explicit written permission to test. You are responsible for authorization, scope, rate limits, and the impact of every request it sends.
Contents
|
Getting started |
How it works |
|
Reference |
Project |
Why WebScan
Most scanners fail in one of two directions: they report everything and bury the real issue, or they stay quiet and miss it. WebScan is built around the first problem — every heuristic finding is checked against what the server actually returned before it reaches the report.
| Principle | What it means in practice |
|---|---|
| Signal over noise | Findings carry a firm / tentative / informational confidence tag. Soft-404 calibration, response-similarity checks, and site-wide deduplication run before anything is reported. |
| Safe by default | The default profile skips checks that mutate state or call external services. --safe-mode lowers concurrency, caps the request rate, respects robots.txt, and sends an honest User-Agent. |
| Built for review | Terminal output a human can read, --explain in plain language, evidence attached to findings, and --suggest-fixes for copy-paste remediation. |
| Automation ready | Six report formats, risk gates, report diffing, SARIF for Code Scanning, and webhooks — all with meaningful exit codes. |
| Small surface | Two runtime dependencies: aiohttp and PyYAML. Everything heavier is an opt-in extra. |
Install
python -m pip install webscan-security
The PyPI badge above shows the version pip install will actually give you.
GitHub releases can run ahead of PyPI; to track the newest code, install from
the repository instead:
python -m pip install "git+https://github.com/lutzashl290788-cell/webscan"
Optional extras
python -m pip install 'webscan-security[ai]' # Claude triage and summaries
python -m pip install 'webscan-security[serve]' # local HTTP backend + dashboard
python -m pip install 'webscan-security[dev]' # test and lint tooling
| Extra | Pulls in | Needed for |
|---|---|---|
ai |
anthropic |
--ai-triage, --ai-summary (also needs ANTHROPIC_API_KEY) |
serve |
fastapi, uvicorn |
webscan serve and the local history dashboard |
dev |
pytest, ruff, mypy, … |
Running the test suite and the linters |
From source, Docker, and pipx
# From a clone
git clone https://github.com/lutzashl290788-cell/webscan
cd webscan
python -m pip install .
# Isolated CLI install
pipx install webscan-security
# Docker — nothing to install
docker run --rm ghcr.io/lutzashl290788-cell/webscan -t https://example.com
Full details, including offline installs, are in docs/installation.md.
Requirements: Python 3.11 or newer. Linux, macOS, and Windows.
Quick start
1. Run a polite first audit. --safe-mode keeps the request rate low;
--explain adds a plain-language paragraph under each finding.
webscan -t https://example.com --safe-mode --explain
2. Save something you can share.
webscan -t https://example.com --safe-mode --format json html md -o reports/example
This writes reports/example.json, .html, and .md. The HTML report is
self-contained — it opens in a browser on a machine that has never heard of
WebScan.
3. Keep only what is worth acting on.
webscan -t https://example.com --min-severity high --min-confidence firm
Presets
A preset bundles the plugin selection, crawl behaviour, and confidence threshold into one flag, so a useful scan does not require writing a config file first.
| Preset | What it does | Reach for it when |
|---|---|---|
quick |
Default plugins, firm findings only |
You want a fast, high-confidence look |
safe |
Polite crawl, soft-404 handling, firm findings only |
The target is production and you are a guest on it |
full |
Safe crawl plus DNS, technology, and robots/sitemap recon | You are profiling a target, not just checking it |
active |
Defaults plus the heavier active checks | You have authorisation for intrusive probes |
webscan -t https://example.com --preset quick
webscan -t https://example.com --preset safe
webscan -t https://example.com --preset full
webscan -t https://example.com --preset active
A preset sets defaults, so explicit flags still win: --preset safe --depth 4
gives you the safe preset with a deeper crawl. Presets cannot be combined with
--config / --profile — pick one source of configuration, and WebScan will
say so plainly if you pick both.
Choose your workflow
| You need to… | Start here | You get |
|---|---|---|
| Baseline a site you own | webscan -t https://example.com --safe-mode |
Terminal summary |
| Find routes before auditing | webscan -t https://example.com --crawl --depth 3 |
Crawl plus a scan of everything discovered |
| See only actionable signal | --min-severity high --min-confidence firm |
High-impact, directly verified findings |
| Scan behind a login | --cookie, --header, --basic-auth, --login-url |
Session-aware checks |
| Compare two deployments | webscan diff baseline.json current.json --fail-on-new |
New, fixed, and changed findings |
| Gate a pull request | --format sarif -o report --fail-on-risk 70 |
SARIF artifact and a non-zero exit code |
| Share results outside the team | --format html md --anonymize |
Redacted, offline-readable reports |
| Revisit past scans | webscan serve |
Local dashboard at http://127.0.0.1:8000 |
The scan pipeline
targets (-t) or file (-f)
│
▼
normalize + deduplicate
│
├──── optional crawl (--crawl) ────▶ discovered URLs
▼
async engine
│
├──▶ passive plugins inspect what the server already sent
└──▶ active plugins send probes, then verify the response
│
▼
content verification → confidence → severity → site-wide dedup
│
├──▶ terminal summary
├──▶ json · jsonl · md · html · sarif · csv
└──▶ risk score · OWASP mapping · diff · webhook
Concurrency is bounded twice — by the connection pool and by a semaphore — so a
scan cannot accidentally flood a target. --stealth drops both to a single
in-flight request.
Plugin catalog
WebScan ships 41 plugins: 33 run by default, 8 are opt-in. Run
webscan --list-plugins for the registry as installed; the annotated catalogue
is in docs/plugins.md.
Passive checks
These read what the server already sent — responses, certificates, HTML, JavaScript, cookies, headers — and never send an exploit payload.
headers · secrets · cors · cookies · ssl_tls · security_txt ·
jwt_audit · csrf · clickjacking · verbose_errors ·
prototype_pollution · csp_analyzer · waf_detect · websocket_security
Active checks
These send probes and then verify the response before reporting anything.
| Plugin | Coverage |
|---|---|
config_files |
Exposed .env, .git, backups, keys, dumps, common config files |
directories |
Admin panels, debug paths, open listings, sensitive directories |
sql_injection |
Error-based, boolean-blind, and time-blind variants |
xss |
Reflected parameters, classified by injection context |
path_traversal · lfi_rfi |
Unix/Windows traversal and PHP wrapper probes, content verified |
open_redirect |
Redirect payload variants with Location host validation |
ssrf |
Cloud metadata and localhost response signatures |
http_methods |
Dangerous PUT, DELETE, TRACE, CONNECT, PATCH |
subdomains |
Certificate Transparency, with optional DNS brute force |
graphql_depth |
Depth abuse and field-suggestion disclosure |
xxe |
XML entity probes with per-scan markers |
idor |
Object-ID variation with similarity and auth-error suppression |
cache_poisoning · host_header_injection |
Host and forwarding header reflection |
ssti |
Jinja2, Twig, FreeMarker, ERB, and Smarty syntax |
backup_files |
Source-verified .bak, .old, .orig, ~, .save |
file_upload |
Harmless upload with predicted-URL accessibility check |
web_cache_deception |
Dynamic URLs with static extensions on sensitive responses |
Opt-in checks
Eight plugins stay out of the default run, for three distinct reasons.
| Plugin | Coverage | Why it is opt-in |
|---|---|---|
mass_assignment |
API role and property injection | 🔧 May mutate target state |
race_condition |
Concurrent request comparison | 🔧 May mutate target state |
request_smuggling |
CL.TE and TE.CL timing probes | 🔧 May mutate target state |
cve_lookup |
Detected software mapped to known CVEs | 🌐 Queries an external database |
dns_security |
DNSSEC, CAA, SPF, DMARC, DKIM | 🌐 Queries public DNS resolvers |
graphql |
Endpoints with introspection enabled | 🔍 Reconnaissance, not a vulnerability |
tech_fingerprint |
Server, framework, CMS identification | 🔍 Reconnaissance, not a vulnerability |
robots_sitemap |
robots.txt / sitemap.xml leaks and hygiene |
🔍 Reconnaissance, not a vulnerability |
webscan -t https://example.com --plugins graphql cve_lookup # ONLY these two
webscan -t https://example.com --preset active # defaults + active opt-ins
webscan -t https://example.com --preset full # defaults + recon opt-ins
[!IMPORTANT]
--pluginsreplaces the default set — it does not add to it. When you want the defaults plus something extra, use a preset or list every plugin you want.
Confidence and severity
Every finding carries both. Severity says how bad it would be; confidence says how sure WebScan is that it is real.
| Confidence | Meaning | Typical use |
|---|---|---|
firm |
Directly observed, or proven by a content check | Hand straight to a developer |
tentative |
Strong heuristic that needs a human to confirm | Triage queue |
informational |
Best-practice note or manual-review signal | Hardening backlog |
webscan -t https://example.com --min-confidence firm # strongest false-positive filter
--min-confidence drops findings from all output, reports included.
--min-severity filters the console summary only.
Reports
| Format | Flag | Best for |
|---|---|---|
| JSON | --format json |
APIs, archives, custom tooling |
| JSONL | --format jsonl |
jq, streaming, line-oriented pipelines |
| Markdown | --format md |
Pull requests and human review |
| HTML | --format html |
Self-contained stakeholder reports |
| SARIF | --format sarif |
GitHub Code Scanning, IDE integrations |
| CSV | --format csv |
Excel, Jira, data imports |
# One finding per line, then filter with jq
webscan -t https://example.com --format jsonl -o findings
jq 'select(.severity == "critical")' findings.jsonl
# Redact local paths, hostname/username, and private IPs before sharing
webscan -t https://example.com --format html sarif -o public-report --anonymize
Full details — every field, the anonymiser's guarantees, and the HTML report's structure — are in docs/reports.md.
Risk, compliance, and diffing
# 0-100 score with an A-F grade, plus OWASP Top 10 2021 mapping
webscan -t https://example.com --risk-score --compliance
# What changed since the last run?
webscan diff baseline.json current.json --fail-on-new
webscan diff reads two JSON reports and prints what is new, what was
fixed, and what changed. With --fail-on-new it exits 1 as soon as a
new CRITICAL or HIGH finding appears — which is the check you want in CI.
Local dashboard
python -m pip install 'webscan-security[serve]'
webscan serve
Open http://127.0.0.1:8000 to launch scans, revisit previous ones, and filter
findings by severity, confidence, plugin, or free-text search.
History lives in ~/.webscan/history.db and is never uploaded. Point it
somewhere else with --history-db /path/to/history.db or WEBSCAN_HISTORY_DB.
[!CAUTION]
servebinds to127.0.0.1on purpose. It is a local helper, not a hardened public service — put your own authentication and rate limiting in front of it before exposing it to any network you do not control.
CLI reference
Targets, discovery, and authentication
Targets
-t, --target URL [URL ...] One or more target URLs
-f, --file FILE One URL per line (# comments allowed)
Discovery
--crawl Spider each target before scanning
--depth N Maximum crawl depth (default: 2)
--max-urls N URL cap per seed (default: 200)
--scope DOMAIN Restrict the crawl to a host
--exclude PATTERN ... Skip URLs containing these substrings
--ignore-robots Ignore robots.txt while crawling
Authentication
--cookie STRING Raw Cookie header
--header NAME:VALUE Extra header (repeatable)
--basic-auth USER:PASS HTTP Basic credentials
--login-url URL Form login endpoint
--login-data STRING URL-encoded login body
Network, safety, and evasion
--safe-mode Polite rate, honest UA, lower concurrency, robots respected
--stealth UA rotation, jitter, concurrency 1, spoofed forwarding headers
--proxy URL HTTP or SOCKS5 proxy
--user-agent STRING Override the User-Agent
--random-agent Rotate browser-like User-Agents
--delay SEC Fixed delay before each target
--random-delay Jitter --delay by x0.5-x1.5
--rate-limit N Cap requests per second
--retries N Retries on timeouts and 429/5xx (default: 2)
--retry-backoff SEC Base backoff, doubled each attempt (default: 0.5)
--strict-ssl Enforce certificate verification
--no-verify-ssl Skip verification (already the default; kept for compatibility)
--no-bruteforce Certificate Transparency only in the subdomains plugin
--soft-404 Calibrate against a bogus path and suppress soft-404 matches
TLS verification is off by default so the scanner can audit hosts with self-signed or expired certificates. Turn it on with
--strict-sslwhen a valid certificate is itself part of what you are checking.
Plugins, output, and performance
Plugins and configuration
--preset NAME quick | safe | full | active
--config FILE / --profile NAME YAML config file and a named profile
--plugins NAME [...] Select plugins (replaces the default set)
--list-plugins Print the registry and exit
Output
-o, --output PATH Report base path, without extension
--format FMT [...] json | jsonl | md | html | sarif | csv (default: json md)
--min-severity LEVEL critical | high | medium | low | info
--min-confidence LEVEL firm | tentative | informational
--explain Plain-language explanation under each finding
--suggest-fixes Copy-paste remediation (nginx, Python, curl)
--anonymize Redact local paths, host/user data, private IPs
--risk-score Print the 0-100 score and A-F grade
--compliance Map findings to OWASP Top 10 2021
--webhook-url URL Post a summary to Slack/Discord/Teams/HTTP
--ai-triage / --ai-summary Claude triage and executive summary ([ai] extra)
--fail-on LEVEL Exit 1 at or above this severity (default: critical/high)
--fail-on-risk N Exit 1 when the risk score is below N
-v / -q / --no-color Verbose, quiet, or no ANSI colour
Performance
-c, --concurrency N Max concurrent targets (default: 10)
--timeout SEC Per-request timeout (default: 10)
Subcommands: webscan serve (local backend) and webscan diff (compare two
reports). webscan --help always prints the reference for your installed
version; webscan -V prints that version.
Configuration profiles
Keep reusable settings in YAML and commit them. Explicit CLI flags override whatever the profile says.
profiles:
quick:
plugins: [headers, cookies, ssl_tls]
concurrency: 30
deep:
plugins: [headers, sql_injection, xss, ssrf, cve_lookup]
crawl: true
depth: 3
format: [json, sarif]
webscan -t https://example.com --config webscan.yml --profile deep
webscan -t https://example.com --config webscan.yml --profile deep --concurrency 5
Every supported key is documented in docs/configuration.md.
CI/CD
Exit codes
| Code | When |
|---|---|
0 |
The scan completed and no gate was tripped |
1 |
A finding at or above --fail-on (default: CRITICAL/HIGH), a risk score below --fail-on-risk, a new regression under diff --fail-on-new, or a fatal error |
GitHub Actions
name: WebScan
on: [workflow_dispatch]
permissions:
security-events: write
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with: { python-version: "3.14" }
- run: pip install .
- run: webscan -t "${{ secrets.STAGING_URL }}" --safe-mode --min-severity high --format sarif -o report
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v4
with: { sarif_file: report.sarif }
Docker
docker run --rm ghcr.io/lutzashl290788-cell/webscan -t https://example.com
docker run --rm -v "$(pwd)/reports:/reports" ghcr.io/lutzashl290788-cell/webscan \
-t https://example.com -o /reports/scan --format json html
GitLab CI, Jenkins, and gating strategies are covered in docs/ci-cd.md.
Python API
import asyncio
import webscan
report = asyncio.run(webscan.scan(
["https://example.com"],
plugins=["headers", "cookies", "config_files"],
soft_404=True,
))
for target in report.targets:
for finding in target.findings:
print(finding.severity.value, finding.confidence.value, finding.title)
The same ScanReport renders through Reporter(report) with .to_json(),
.to_jsonl(), .to_markdown(), .to_html(), .to_sarif(), and .to_csv().
See docs/python-api.md.
Architecture
webscan/
├── cli.py CLI parsing, presets, safety notice, subcommands
├── engine.py Async orchestration and concurrency limits
├── crawler.py Breadth-first URL and form discovery
├── net.py Proxy, rate limiting, User-Agent, stealth
├── models.py Finding, Severity, Confidence, ScanReport
├── reporter.py Six output formats and the terminal summary
├── risk.py 0-100 score and A-F grade
├── compliance.py OWASP Top 10 2021 mapping
├── diff.py New / fixed / changed report comparison
├── server.py Optional HTTP backend ([serve] extra)
└── plugins/ Built-in and entry-point-discovered checks
Runtime dependencies are deliberately small — aiohttp and PyYAML. The module
map and design constraints are in docs/architecture.md.
Benchmark
A cold, single-target Safe Mode run against other general-purpose scanners. Treat it as a reference point, not a promise for every network or target.
| Scanner | Time | Findings | False positives | Coverage |
|---|---|---|---|---|
| WebScan 2.8 | 7.1s | 16 | 0 | 41 plugins |
| Nuclei 3.8 | 34.2s | 21 | not measured | template-based |
| Nikto 2.6 | 42.6s | 30 | 5+ observed | legacy checks |
Target httpbin.org, 41 plugins, Safe Mode, one cold run. Reproduce it with
python benchmarks/run_benchmark.py --help.
Documentation
Full reference documentation lives in docs/.
| Guide | Covers |
|---|---|
| Installation | pip, extras, Docker, pipx, offline installs |
| Quickstart | First scan to first report |
| Plugin reference | All 41 checks, opt-in rationale, the confidence model |
| Configuration | YAML profiles, every key, environment variables |
| Reports | Six formats, anonymising, diffing, risk scoring, dashboard |
| CI/CD integration | Exit codes, gating, GitHub Actions, GitLab, Jenkins |
| Python API | scan(), report dataclasses, rendering |
| Plugin development | The BasePlugin contract, helpers, testing, publishing |
| Architecture | Module map, pipeline, concurrency, design constraints |
| Troubleshooting | Install problems, noisy results, missing checks, CI issues |
Development
python -m pip install -e ".[dev]"
ruff check webscan tests
mypy webscan
pytest -q
Install the git hooks to run the same checks CI does:
pip install pre-commit && pre-commit install
To add a check: subclass BasePlugin, implement run(), and register it under
the webscan.plugins entry-point group. Reuse the active helpers for retry,
soft-404 calibration, and response similarity so your plugin inherits the same
false-positive discipline as the built-ins. The contract is in
docs/plugin-development.md; the contribution
process is in CONTRIBUTING.md.
Security
Found a vulnerability in WebScan itself? Please do not open a public issue — report it privately through GitHub Security Advisories. Response targets, supported versions, and the security model of running a scan are in SECURITY.md.
Legal
WebScan is for authorized security testing only. Use it solely on systems you own or have explicit written permission to test. Unauthorized scanning may be illegal in your jurisdiction. You are solely responsible for how you use it.
Made for responsible testing and fewer false positives.
Documentation · Report a bug · Security policy · Contribute · Changelog
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file webscan_security-2.8.3.tar.gz.
File metadata
- Download URL: webscan_security-2.8.3.tar.gz
- Upload date:
- Size: 293.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
73cee8340ae39145bb59a9cc145d6b7435d9d83c0ea7261bc654610bbcfefdfd
|
|
| MD5 |
cb2d434f8ee106b5b9795cf1579c6fba
|
|
| BLAKE2b-256 |
d432a232a7ace1ecafcd41c93464aa9a1564c09a3cb66a65d573bf710a008e9f
|
Provenance
The following attestation bundles were made for webscan_security-2.8.3.tar.gz:
Publisher:
publish-pypi.yml on lutzashl290788-cell/webscan
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
webscan_security-2.8.3.tar.gz -
Subject digest:
73cee8340ae39145bb59a9cc145d6b7435d9d83c0ea7261bc654610bbcfefdfd - Sigstore transparency entry: 2702179149
- Sigstore integration time:
-
Permalink:
lutzashl290788-cell/webscan@fc8b7316513a7cda2f8d3260caad43777ae4b76b -
Branch / Tag:
refs/tags/v2.8.3 - Owner: https://github.com/lutzashl290788-cell
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@fc8b7316513a7cda2f8d3260caad43777ae4b76b -
Trigger Event:
push
-
Statement type:
File details
Details for the file webscan_security-2.8.3-py3-none-any.whl.
File metadata
- Download URL: webscan_security-2.8.3-py3-none-any.whl
- Upload date:
- Size: 230.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e9ffc4250bcf3356027d99e1c504403f37ea71554be4f9f253726d5b01e01a9
|
|
| MD5 |
7b8ffb6e8828b522605a29d3b5b12a5b
|
|
| BLAKE2b-256 |
9ff4f1eefa17923a259a75567852e3ecb4ffac9233594a6521ccd29a4c11a93a
|
Provenance
The following attestation bundles were made for webscan_security-2.8.3-py3-none-any.whl:
Publisher:
publish-pypi.yml on lutzashl290788-cell/webscan
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
webscan_security-2.8.3-py3-none-any.whl -
Subject digest:
5e9ffc4250bcf3356027d99e1c504403f37ea71554be4f9f253726d5b01e01a9 - Sigstore transparency entry: 2702179304
- Sigstore integration time:
-
Permalink:
lutzashl290788-cell/webscan@fc8b7316513a7cda2f8d3260caad43777ae4b76b -
Branch / Tag:
refs/tags/v2.8.3 - Owner: https://github.com/lutzashl290788-cell
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@fc8b7316513a7cda2f8d3260caad43777ae4b76b -
Trigger Event:
push
-
Statement type: