An automated vulnerability assessment platform that orchestrates 210 open-source security tools, aggregates and deduplicates findings, optionally chains tools into a discovery data-flow graph, runs an optional OpenAI-compatible LLM analysis layer for triage, clustering, and remediation, generates proof-of-concept scripts, drives optional bug-bounty and pentester agents (Pydantic AI) to prove and PoC findings, and produces professional Markdown, HTML, JSON, and PDF reports — all from a single BlackArch Linux Docker image.
- Architecture
- Tools
- Target Type Gating
- Scan Modes
- Tool Chaining
- Authenticated Scanning
- LLM Analysis
- PoC Generation and Execution
- Agentic Testing
- Plugin System
- Report Formats
- Quick Start
- scanner.sh — Docker Wrapper
- Configuration
- Environment Variables
- Project Structure
- Adding a New Tool
- Development
- DefectDojo Integration
config.toml / env vars / CLI args
↓
AppConfig (pydantic, 3-layer merge: TOML < env < CLI)
↓
Plugin loader — auto-discovers ./plugins/ + ~/.vuln-scanner/plugins/
↓
ScanOrchestrator
• classify_target() → TargetType
• tool.applies_to(target) — skips mismatched pairs
• asyncio + ThreadPoolExecutor — parallel (tool × target) tasks
• AuthConfig forwarded to every applicable tool
• optional chaining: produces/consumes assets in a wave/fixpoint loop
↓
ScanResult[] → Assessment (+ chain_edges / assets_by_type)
↓
LLMAnalyzer (optional)
• Pass 1: triage + PoC design (threaded, per result)
• Pass 2: PoC generation (PocGenerator, host-safe)
• Pass 3: mitigation (evidence-informed)
• Pass 4: clustering + exec summary
↓
PocRunner (container-only, VS_IN_CONTAINER=1 guard)
↓
AgentOrchestrator (optional, container-only, sequential)
• bug-bounty / pentester agents (Pydantic AI)
• drive tools with custom args + run sandboxed code
• scope-guarded, denylisted, audited → Assessment.agent_reports
↓
┌────────┬────────┬────────┐
│ .md │ .html │ .json │ (all formats written in parallel)
└────────┴────────┴────────┘
↓
DefectDojo (optional)
All scanning tools, PoC execution, and agent actions run inside a BlackArch Linux Docker container — nothing is installed on the host.
210 tools organized by category. Each tool declares the target types it supports; the orchestrator skips incompatible pairings automatically.
| Tool | Notes |
|---|---|
nmap |
Full port scan with service/version detection |
rustscan |
Fast port scanner, feeds into nmap |
masscan |
High-speed TCP/UDP scanner |
naabu |
Port scanner with service detection |
netdiscover |
ARP-based host discovery |
| Tool | Notes |
|---|---|
nuclei |
Template-based vulnerability scanner |
nikto |
Web server misconfiguration scanner |
wapiti |
Black-box web vulnerability scanner |
ffuf |
Fast web fuzzer (dirs, params, headers) |
feroxbuster |
Content discovery with recursion |
gobuster |
URI/DNS/vhost brute-forcer |
wfuzz |
Web application fuzzer |
dalfox |
XSS scanner with parameter analysis |
xsstrike |
Advanced XSS detection engine |
commix |
Command injection exploiter |
sqlmap |
Automated SQL injection and takeover |
nosqlmap |
NoSQL injection scanner |
httpx |
HTTP probing and fingerprinting |
whatweb |
Web technology fingerprinter |
wafw00f |
WAF detection and fingerprinting |
wpscan |
WordPress vulnerability scanner |
acunetix |
Web vulnerability scanner (API-based) |
arachni |
Web application security scanner |
zap |
OWASP ZAP DAST scanner |
wapiti |
Black-box vulnerability scanner |
drheader |
HTTP security header analyser |
humble |
HTTP header security checker |
hakrawler |
Fast web crawler for URLs and endpoints |
katana |
Next-gen web crawling framework |
gau |
Known URL collector (AlienVault, WaybackMachine) |
jsluice |
JavaScript secrets and URL extractor |
corscanner |
CORS misconfiguration scanner |
crlfuzz |
CRLF injection scanner |
smuggler |
HTTP request smuggling detector |
linkfinder |
Endpoint discovery in JavaScript/HTML source |
cariddi |
Web crawler with secret and endpoint detection |
| Tool | Notes |
|---|---|
kiterunner |
API route discovery with kite files |
graphql_cop |
GraphQL security auditor |
restler |
Stateful REST API fuzzer |
apifuzzer |
OpenAPI/Swagger-based fuzzer |
cherrybomb |
OpenAPI spec security linter |
arjun |
HTTP parameter discovery |
paramspider |
Parameter mining from wayback/sources |
| Tool | Notes |
|---|---|
amass |
Subdomain enumeration (passive + active) |
subfinder |
Fast passive subdomain enumeration |
dnsx |
DNS resolver and probe toolkit |
dnsrecon |
DNS enumeration and zone transfer |
fierce |
DNS reconnaissance and host discovery |
theharvester |
OSINT: emails, names, hosts, subdomains |
puredns |
Fast subdomain brute-forcer with wildcard filtering |
alterx |
Subdomain permutation engine |
waybackurls |
Historical URL collection from Wayback Machine |
httprobe |
Live HTTP/HTTPS host prober |
| Tool | Notes |
|---|---|
testssl |
TLS configuration and cipher suite audit |
sslyze |
TLS scanner (cipher suites, Heartbleed, ROBOT) |
sslscan |
SSL/TLS service scanner |
tlsx |
Fast TLS probing |
tls_attacker |
TLS protocol attack tool |
ssh_audit |
SSH configuration and algorithm auditor |
| Tool | Notes |
|---|---|
smbmap |
SMB share enumeration and permissions |
enum4linux |
SMB/NetBIOS enumeration |
crackmapexec |
Active Directory and SMB assessment |
openvas |
OpenVAS vulnerability scanner |
| Tool | Notes |
|---|---|
bandit |
Python SAST — common security anti-patterns |
semgrep |
Multi-language SAST with community rules |
gosec |
Go security checker |
bearer |
Data-flow SAST with privacy and security rules |
horusec |
Multi-language SAST engine |
brakeman |
Ruby on Rails SAST scanner |
flawfinder |
C/C++ static analysis for common flaws |
dependency_check |
OWASP dependency vulnerability scanner |
pip_audit |
Python package vulnerability checker |
| Tool | Notes |
|---|---|
osv-scanner |
Open Source Vulnerability database scanner |
npm-audit |
Node.js package vulnerability audit |
govulncheck |
Go module vulnerability checker |
| Tool | Notes |
|---|---|
gitleaks |
Git history secret scanner |
trufflehog |
Deep entropy-based secret finder |
secretfinder |
Secrets in JS files and endpoints |
detect-secrets |
Baseline-based secret scanner |
noseyparker |
High-speed secret scanner with pattern rules |
| Tool | Notes |
|---|---|
checkov |
Terraform/K8s/Dockerfile IaC scanner |
tfsec |
Terraform static analysis |
terrascan |
Multi-cloud IaC security scanner |
hadolint |
Dockerfile best-practice linter |
| Tool | Notes |
|---|---|
prowler |
AWS/GCP/Azure security posture assessment |
kube-bench |
CIS Kubernetes Benchmark checker |
| Tool | Notes |
|---|---|
trivy |
Container image + filesystem vulnerability scanner |
grype |
Container and package vulnerability matcher |
The orchestrator classifies each target into one or more types and only runs tools that declare support for that type. This eliminates noise from e.g. SMB tools running against web URLs.
| Type | Example | Tools that match |
|---|---|---|
HOST |
example.com |
DNS, SSL, web, SMB tools |
IP |
10.0.0.1 |
Network, port, SMB tools |
CIDR |
10.0.0.0/24 |
Network scanners |
URL |
https://app.example.com |
Web, API, SSL tools |
PATH |
/src/myapp |
SAST, SCA, secrets, IaC tools |
REPO |
https://github.com/org/repo |
Secrets, SAST, SCA tools |
IMAGE |
myapp:latest |
Container scanners |
CLOUD |
aws:profile=prod, arn:aws:… |
Cloud posture tools (prowler, kube-bench, terrascan) |
Classification is automatic — just pass the target string; the scanner figures out the type.
Cloud target formats recognised:
- AWS ARN:
arn:aws:iam::123456789012:root - Named profile shorthand:
aws:profile=production - GCP project:
projects/my-project-id - Azure subscription UUID:
00000000-0000-0000-0000-000000000000
| Mode | Description |
|---|---|
paranoid |
Maximum stealth — passive probing, minimal footprint |
passive |
No active attacks — enumeration and banner grabbing only (default) |
active |
Standard vulnerability checks enabled |
aggressive |
Full scan: all templates, brute-force, fast timing |
By default the orchestrator runs a flat tool × target matrix. Enable
[chaining] to turn it into a live data-flow graph instead: tools that
produce assets (subdomains, live hosts, open ports, URLs, params, tech
fingerprints…) feed tools that consume those asset types in later waves.
- Wave 0 runs tools with no dependencies against the CLI targets.
- Each subsequent wave is triggered by the assets discovered so far, up to
max_depth, until a fixpoint (no new work) is reached. - An
asset_predicategates routing so, e.g.,wpscanonly fires on atechasset whose fingerprint containswordpress, and TLS tools only on TLS ports. - Passive/paranoid modes only propagate passive asset types; budgets cap how many assets of each type carry forward.
[chaining]
enabled = true
max_depth = 5 # max wave count (prevents unbounded expansion)
max_new_targets = 200 # hard cap on newly-discovered targets per run
[chaining.asset_budgets] # per-asset-type carry-forward caps
subdomain = 500
live_host = 500
url = 1000
open_port = 300The discovery graph is surfaced in every report as a Discovery Chain section
— assets found per type, wave count, and the chain edges
(source_tool → asset → triggered_tool) — and in JSON as chain_edges,
stats.assets_by_type, and stats.waves_run.
Credentials are forwarded to all applicable web tools (nuclei, ffuf, feroxbuster, gobuster, nikto, sqlmap, dalfox, wpscan, wapiti, katana, hakrawler, arjun, wfuzz, corscanner, kiterunner, httpx).
Applied to every target unless a per-target override exists.
Via config:
[scan.auth]
bearer_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
username = "admin"
password = "secret"
[scan.auth.cookies]
session = "abc123"
[scan.auth.headers]
X-API-Key = "my-api-key"Via environment variables (global only):
VS_AUTH_BEARER_TOKEN=eyJ...
VS_AUTH_USERNAME=admin
VS_AUTH_PASSWORD=secretVia CLI (global only):
vuln-scanner --targets https://app.example.com \
--auth-bearer eyJ... \
--auth-cookie session=abc123 \
--auth-header X-API-Key=secretWhen scanning multiple targets that require different credentials, define per-target overrides under [scan.auth.targets."<target>"]. A matching entry replaces the global config for that target entirely — there is no merge. Per-target auth is config-file only (env vars and CLI flags only set the global default).
[scan.auth]
# Global fallback — used for any target without a specific entry
bearer_token = "default-token"
# JWT for the main app
[scan.auth.targets."https://app.example.com"]
bearer_token = "app-specific-jwt"
# Cookie session for the admin panel
[scan.auth.targets."https://admin.example.com"]
[scan.auth.targets."https://admin.example.com".cookies]
session = "s%3Aabc123"
csrftoken = "xyz789"
# HTTP Basic for an internal API
[scan.auth.targets."10.0.0.50"]
username = "apiuser"
password = "s3cret"
# Form login for a legacy app
[scan.auth.targets."https://legacy.example.com"]
login_url = "https://legacy.example.com/login"
username = "admin"
password = "password123"
[scan.auth.targets."https://legacy.example.com".login_data]
_token = "csrf-value-here"Resolution: per-target config > global config
When an API key is present, the LLM layer activates automatically. It performs two passes over the scan results:
| Pass | Name | What it does |
|---|---|---|
| 1 | Enrich & triage | Assigns CWE, confidence, false-positive flag, exploitability summary, mitigation, remediation, and a PoC plan for each finding in a single call |
| 2 | Clustering | Groups findings by root cause, writes shared remediations, and produces an executive summary |
PoC generation and execution are a separate optional phase (see [llm.features] generate_poc / execute_poc), and post-PoC mitigation re-enrichment runs only when confirmed PoCs are produced.
The LLM client is OpenAI-API-compatible — works with OpenAI, Azure OpenAI, Ollama, vLLM, LM Studio, OpenRouter, and any other compatible endpoint.
[llm]
enabled = "auto" # "auto" | true | false (auto = on when api_key present)
api_key = "" # or set OPENAI_API_KEY env var
base_url = "" # leave empty for OpenAI; set for Ollama/vLLM/etc.
model = "gpt-4o" # REQUIRED when LLM is active — no default
# Sampling parameters (all OpenAI-compatible)
temperature = 0.2
top_p = 0.95
max_tokens = 4096
# top_k and other non-standard params go in extra_body:
# [llm.extra_body]
# top_k = 40Ollama example:
[llm]
base_url = "http://localhost:11434/v1"
api_key = "ollama"
model = "llama3.2"vLLM example:
[llm]
base_url = "http://localhost:8000/v1"
api_key = "token-abc123"
model = "meta-llama/Meta-Llama-3-8B-Instruct"Each LLM capability is a named feature, toggleable globally and overridable per tool or per category.
| Feature | Default | Description |
|---|---|---|
logs_analysis |
on | Feed tool's raw output to the LLM |
enrich |
on | CWE / confidence / false-positive / exploitability triage |
classify |
on | Classify finding type and risk |
cluster |
on | Group findings by root cause |
mitigation |
on | Generate mitigation and remediation |
generate_poc |
on | Write PoC scripts as report assets |
execute_poc |
off | Run PoCs in-container (requires VS_IN_CONTAINER=1) |
false_positive_filter |
on | Suppress likely false positives from the report |
Global feature config:
[llm.features]
generate_poc = true
execute_poc = false # enable only inside Docker
# Per-tool override — disable PoC for bandit (SAST, no runtime target)
[llm.features.tool.bandit]
generate_poc = false
# Per-category override — disable log analysis for noisy crawlers
[llm.features.category.web]
logs_analysis = falseFeature precedence: tool override > category override > global
All LLM prompts are overridable:
[llm.prompts]
enrich_system = "You are a senior penetration tester..."
mitigation_user = "Write remediation steps for: {title}..."
# Available placeholders: {title} {severity} {description} {cwe}
# {exploitability} {tool} {target} {cves} {raw_output}[llm]
include_tools = [] # empty = all tools
exclude_tools = ["hakrawler", "gau"]
include_categories = []
exclude_categories = ["dns"]The LLM writes self-contained Python and/or Bash scripts per finding. Scripts use tools already in the BlackArch image (curl, sqlmap, nuclei, dalfox, etc.) and are written to <report>_assets/poc/. Generation never executes code — it only writes files.
[llm.poc]
languages = ["python", "bash"]
only_severities = ["critical", "high", "medium"]
max_pocs = 20
allow_git_clone = false # permit cloning official exploit PoCs from GitHubPoC execution is gated behind two independent guards:
execute_poc = truein[llm.features]VS_IN_CONTAINER=1environment variable (baked into the Docker image)
The runner refuses silently if either guard is missing, so it cannot execute on the host. A static denylist rejects scripts containing destructive patterns (rm -rf /, mkfs., fork bombs, etc.) before execution.
# Enable PoC execution inside the container
VS_LLM_FEATURE_EXECUTE_POC=true docker compose ... run --rm scanner ...After tool execution and the static LLM analysis pass, an optional agentic layer (built on Pydantic AI) can drive the existing tools with custom arguments and run sandboxed code to prove or exploit findings. It is container-only and off by default.
Two profiles, same machinery / different permissions and prompt:
| Profile | Goal | Exploitation |
|---|---|---|
bug_bounty |
Prove a bug exists (non-destructive) | Never — evidence only |
pentester |
Produce a working PoC | Dry-run by default; live exec opt-in |
Agents run strictly one at a time (no parallelism) so they never contend on a target. Each agent has its own tunable prompt, model, timeout, and ceilings; when a wall-clock deadline or tool-call/token ceiling is hit, the agent is asked for a final summary — a report is always produced.
- Container gate — nothing runs unless
VS_IN_CONTAINER=1. - Scope enforcement — every host an agent touches (in
run_toolargs, inrun_codesource, in OOB payloads) is validated against[scope]+ the scan allowlist. Out-of-scope actions are refused. - Sandbox —
run_coderuns under POSIX rlimits (CPU, memory, process count, file size) so a runaway or fork bomb cannot take down the host. - Denylist — destructive/anti-forensic patterns (
rm -rf /,mkfs, fork bombs, reverse shells, credential-file access…) are rejected before execution. - Audit trail — every action is appended to
run_dir/agent_logs/<agent>.jsonl. - Verification — a bug's PoC is independently re-run before it is marked
verified. - Secret scrubbing — API keys, tokens, and private keys are redacted from agent output before it reaches any report or submission.
[agents]
enabled = true # master switch (also requires VS_IN_CONTAINER=1)
scope_enforcement = true # keep on; every host is scope-checked
# Resource limits applied to run_code
[agents.sandbox]
cpu_seconds = 30
memory_mb = 512
max_procs = 64 # fork-bomb guard
file_size_mb = 50
timeout = 120
network = "lab" # "lab" | "none" | "host"
# Bug-bounty submission reports (per confirmed bug)
[agents.submission]
enabled = true
formats = ["markdown", "json"]
# template = "..." # optional; {title} {severity} {affected_url} …
# One [[agents.agents]] block per agent — run in listed order.
[[agents.agents]]
name = "hunter"
kind = "bug_bounty"
timeout = 600 # wall-clock seconds
max_tool_calls = 40
# system_prompt = "..." # optional; overrides the built-in default
# model = "gpt-4o" # optional; else inherits [llm].model
[[agents.agents]]
name = "operator"
kind = "pentester"
allow_exploitation = true # required for live exec …
require_approval = false # … and this must be false, in active/aggressive modeLive exploitation for a pentester requires all of: allow_exploitation = true, require_approval = false, scan mode active/aggressive, and the
container gate. Absent any of these it emits an ordered exploit plan instead
of firing it.
Agent tools available to the model: list_tools, run_tool, run_code,
oob_get_callback / oob_check (interactsh OAST for blind bugs), save_bug,
record_poc.
Results appear in an Agent Operations report section and, for bug-bounty
findings, as ready-to-submit files under run_dir/agent_submissions/.
Env toggles: VS_AGENTS_ENABLED, VS_AGENTS_SCOPE_ENFORCEMENT.
Drop a .py file defining one or more AbstractTool subclasses into ./plugins/ (or ~/.vuln-scanner/plugins/) and they are auto-discovered at startup — no code changes needed.
Discovery order (later entries override on name collision):
./plugins/(relative to CWD)~/.vuln-scanner/plugins/- Extra dirs configured via
[plugins] dirsor--plugin-dir
Example plugin (plugins/my_scanner.py):
from vuln_scanner.tools.abstract import AbstractTool
from vuln_scanner.tools.enums import Severity, ScanStatus, TargetType
from vuln_scanner.tools.models import Finding, ScanInput, ScanResult
class MyScannerTool(AbstractTool):
name: str = "my-scanner"
category: str = "web"
# Only runs against URL targets — skipped automatically for IPs, paths, etc.
applicable_targets: frozenset[TargetType] = frozenset({TargetType.URL})
def build_command(self, target: str, scan_input: ScanInput) -> list[str]:
return ["my-scanner", "--target", target, "--json"]
def parse_output(self, raw: str, target: str) -> list[Finding]:
...Config:
[plugins]
enabled = true
dirs = ["/opt/company-scanners"]CLI:
vuln-scanner --plugin-dir /opt/company-scanners --targets https://app.example.comPlugin tools are registered globally but the orchestrator's type-gating controls which targets each plugin actually runs against. A plugin declaring applicable_targets = frozenset({TargetType.URL}) will never fire against an IP or a filesystem path.
To restrict a plugin to specific target strings beyond type-gating (e.g., only run against a known staging host), return ScanStatus.SKIPPED inside run():
def run(self, target: str, scan_input: ScanInput) -> ScanResult:
if "staging" not in target:
return ScanResult(tool=self.name, target=target, status=ScanStatus.SKIPPED)
return super().run(target, scan_input)There is no config-level per-target plugin filter — that logic belongs in the plugin itself.
Four formats are generated in parallel. Select any combination:
[report]
formats = ["markdown", "html", "json", "pdf"]
output_dir = "./reports"Or via CLI: --formats markdown html json pdf
Professional structured report following industry pentest conventions:
- Executive Summary — prose for management
- Scope and Methodology — target list, tools used, scan config
- Severity Rating Guide — CVSS ranges
- Findings Overview — risk distribution matrix + per-target breakdown
- Vulnerability Clusters — root-cause groupings (LLM-generated)
- Detailed Findings — per finding: ID, severity, affected system, description, business impact, analyst note, mitigation, permanent remediation, PoC references
- Discovery Chain — assets discovered per type, wave count, and chain edges (
source_tool → asset → triggered_tool) — present when[chaining]is enabled - Agent Operations — per-agent summary, proven bugs, PoC artifacts, dry-run exploit plans, and audit-log paths — present when the agentic layer ran
- Appendix A — scan errors
- Appendix B — PoC asset index
Findings from multiple tools reporting the same issue on the same target are deduplicated into a single entry showing all contributing tools.
Self-contained single-file report (no external dependencies) with:
- Light/dark theme toggle
- Severity-colour-coded finding cards
- Collapsible cluster sections
- Stats grid and executive summary hero
- Discovery Chain and Agent Operations sections (when applicable)
Full structured dump of the Assessment model — findings, LLM enrichment, clusters, stats, PoC records, chain_edges / stats.assets_by_type, and agent_reports. Suitable for CI/CD pipeline ingestion and downstream tooling.
Print-ready report (reportlab) with cover page, executive summary, stats, clusters, and detailed findings.
The poc.sh script starts DefectDojo, three vulnerable targets, and the scanner in one command.
Prerequisites: docker, docker compose plugin, curl, python3
./poc.sh| Step | Action |
|---|---|
| 1 | Checks prerequisites |
| 2 | Loads .env (copies from .env.example if missing) |
| 3 | Starts DefectDojo stack |
| 4 | Waits for DefectDojo API to be ready |
| 5 | Obtains API token via admin credentials |
| 6 | Starts vulnerable target containers |
| 7 | Waits for each target to be reachable |
| 8 | Builds the scanner Docker image |
| 9 | Runs the scanner, generates reports, pushes to DefectDojo |
| 10 | Prints summary with URLs and teardown instructions |
With LLM analysis:
# Copy the example env and add your key
cp .env.example .env
# Edit .env: set OPENAI_API_KEY and VS_LLM_MODEL
./poc.shOverride scan mode:
SCAN_MODE=active ./poc.shTeardown:
docker compose down -v
docker compose -f docker-compose.target.yaml down -v| App | URL | Description |
|---|---|---|
| OWASP Juice Shop | http://localhost:3000 | Modern Node.js app covering OWASP Top 10 |
| WebGoat | http://localhost:8888/WebGoat | Java/Spring intentionally insecure app |
Remote Lab — pentest-ground.com
Publicly available, intentionally-vulnerable systems maintained by pentest-ground.com. No setup required — scan directly to validate tools and PoC generation.
| System | URL | Type | Vulnerability Classes |
|---|---|---|---|
| DVWA | https://pentest-ground.com:4280 |
Classic Web App | CSRF, XSS, SQLi |
| DVGQL | https://pentest-ground.com:5013 |
GraphQL API | CMDi, XSS, SQLi |
| RestFlaw | https://pentest-ground.com:9000 |
REST API | SQLi, Code Injection, XXE |
| GuardianLeaks | https://pentest-ground.com:81 |
Web App | XSS, SSRF, Code Injection |
vuln-scanner --targets \
https://pentest-ground.com:4280 \
https://pentest-ground.com:5013 \
https://pentest-ground.com:9000 \
https://pentest-ground.com:81 \
--mode activescanner.sh is the recommended day-to-day interface for running the scanner. It wraps docker compose run so you never need to type the compose invocation manually — just pass targets and flags directly.
./scanner.sh [OPTIONS] [-- SCANNER_ARGS...]| Flag | Description |
|---|---|
-t, --targets HOST... |
One or more scan targets (URL, IP, CIDR, path, image) |
-m, --mode MODE |
Scan mode: passive | active | aggressive | paranoid |
-c, --config FILE |
Config file to mount (default: ./config.toml) |
-f, --formats FMT |
Report formats, comma-separated: markdown,html,json; repeatable |
--no-llm |
Disable LLM enrichment |
--llm-model MODEL |
LLM model override (e.g. gpt-4o, claude-sonnet-4-5) |
--llm-min-severity SEV |
Minimum severity for LLM: info|low|medium|high|critical |
--include-tools TOOLS |
Comma-separated list of tools to run |
--exclude-tools TOOLS |
Comma-separated list of tools to skip |
-e, --env KEY=VALUE |
Pass an extra environment variable to the container |
-b, --build |
Rebuild the Docker image before running |
-n, --no-defectdojo |
Skip DefectDojo integration |
--shell |
Open an interactive shell inside the container instead of scanning |
-h, --help |
Show help |
Everything after -- is forwarded verbatim to the scanner entrypoint, bypassing all wrapper logic.
# Scan using ./config.toml (targets and mode come from the config)
./scanner.sh
# Quick scan with explicit targets and mode
./scanner.sh -t https://app.example.com 192.168.1.0/24 -m active
# Use a custom config file
./scanner.sh -c /path/to/prod.toml
# Enable LLM enrichment with a specific model
./scanner.sh -t https://app.example.com --llm-model gpt-4o
# Run only specific tools
./scanner.sh -t https://app.example.com --include-tools nuclei,dalfox,ffuf
# Rebuild the image first, then scan
./scanner.sh --build -t https://app.example.com -m active
# Full manual passthrough to the scanner entrypoint
./scanner.sh -- --targets https://t.example.com --mode aggressive --formats markdown html json
# Open an interactive shell (all tools, volumes, and env available)
./scanner.sh --shell
./scanner.sh --build --shell- Loads
.env(copies from.env.exampleif missing) - Copies
config.example.toml→config.tomlif no config exists - Creates the
vuln_scanner_networkDocker network if not present - Mounts a custom
--configfile into the container at/app/config.toml - Rebuilds the image when
--buildis passed
Copy the annotated template:
cp config.example.toml config.tomlFull reference:
[scan]
targets = ["192.168.1.1", "https://app.example.com", "/src/myapp"]
mode = "passive" # paranoid | passive | active | aggressive
timeout = 300 # per-tool timeout in seconds
rate_limit = null # requests/sec; null = no limit
# Authenticated scanning — forwarded to all applicable web tools
[scan.auth]
bearer_token = "" # Authorization: Bearer <token>
username = "" # HTTP Basic username
password = "" # HTTP Basic password
login_url = "" # Form-based login URL
# [scan.auth.cookies]
# session = "abc123"
# [scan.auth.headers]
# X-API-Key = "secret"
[tools]
exclude = ["nikto"] # skip specific tools by name
[categories]
include = ["web", "ssl"] # limit to these categories; empty = all
[plugins]
enabled = true
# dirs = ["/opt/company-scanners"]
[report]
formats = ["markdown", "html", "json"]
output_dir = "./reports"
[defectdojo]
url = "http://localhost:8080"
api_key = ""
product_name = "My Product"
engagement_name = "Automated Scan"
# ── LLM Analysis ─────────────────────────────────────────────────────────────
[llm]
enabled = "auto" # "auto" | true | false
api_key = "" # or OPENAI_API_KEY env var
base_url = "" # leave empty for OpenAI
model = "" # required when active, e.g. "gpt-4o" or "llama3.2"
temperature = 0.2
top_p = 0.95
max_tokens = 4096
# extra_body = { top_k = 40 } # for Ollama/vLLM top_k support
exclude_tools = []
exclude_categories = []
[llm.features]
logs_analysis = true
enrich = true
classify = true
cluster = true
mitigation = true
generate_poc = true
execute_poc = false # container-only; set VS_LLM_FEATURE_EXECUTE_POC=true
false_positive_filter = true
# Per-tool feature overrides (tool > category > global precedence)
[llm.features.tool.bandit]
generate_poc = false
[llm.features.category.dns]
logs_analysis = false
[llm.poc]
languages = ["python", "bash"]
only_severities = ["critical", "high", "medium"]
max_pocs = 20
allow_git_clone = falseConfig merge precedence: CLI > env vars > config.toml > defaults
| Variable | CLI flag | Description |
|---|---|---|
VS_TARGETS |
--targets |
Space-separated target list |
VS_MODE |
--mode |
Scan mode |
VS_TIMEOUT |
--timeout |
Per-tool timeout (seconds) |
VS_RATE_LIMIT |
--rate-limit |
Rate limit (req/s) |
VS_MAX_CONCURRENT |
--max-concurrent |
Parallel tool slots |
VS_INCLUDE_TOOLS |
--include-tools |
Whitelist tools by name |
VS_EXCLUDE_TOOLS |
--exclude-tools |
Blacklist tools by name |
VS_INCLUDE_CATEGORIES |
--include-categories |
Whitelist categories |
VS_EXCLUDE_CATEGORIES |
--exclude-categories |
Blacklist categories |
VS_OUTPUT_DIR |
--output-dir |
Report output directory |
| Variable | CLI flag | Description |
|---|---|---|
VS_FORMATS |
--formats |
Report formats: markdown html json |
| Variable | CLI flag | Description |
|---|---|---|
OPENAI_API_KEY |
— | API key (standard env var, used as fallback) |
OPENAI_BASE_URL |
— | Base URL fallback (for non-OpenAI endpoints) |
VS_LLM_ENABLED |
--no-llm |
auto | true | false |
VS_LLM_MODEL |
--llm-model |
Model name (required when active) |
VS_LLM_TEMPERATURE |
— | Sampling temperature |
VS_LLM_MAX_TOKENS |
— | Max output tokens |
VS_LLM_LOG_RESPONSES |
— | Log a one-line summary of each LLM response live (default true) |
VS_LLM_FEATURE_<NAME> |
--llm-feature NAME=on |
Global feature toggle, e.g. VS_LLM_FEATURE_GENERATE_POC=false |
VS_LLM_FEATURE_EXECUTE_POC |
--llm-poc-execute |
Enable PoC execution (container-only) |
| Variable | CLI flag | Description |
|---|---|---|
VS_AGENTS_ENABLED |
— | Enable the agentic layer (also requires VS_IN_CONTAINER=1) |
VS_AGENTS_SCOPE_ENFORCEMENT |
— | Toggle per-host scope validation (default on) |
Per-agent settings (profiles, prompts, timeouts, sandbox, submissions) are set
under [agents] in the config file — see Agentic Testing.
| Variable | CLI flag | Description |
|---|---|---|
VS_AUTH_BEARER_TOKEN |
--auth-bearer |
Bearer token (Authorization: Bearer …) |
VS_AUTH_USERNAME |
--auth-user |
HTTP Basic username |
VS_AUTH_PASSWORD |
--auth-pass |
HTTP Basic password |
VS_AUTH_LOGIN_URL |
--auth-login-url |
Form-based login URL |
Cookies and extra headers must be set via config file or --auth-cookie / --auth-header CLI flags.
| Variable | CLI flag | Description |
|---|---|---|
VS_PLUGINS_ENABLED |
--no-plugins |
Enable/disable plugin auto-discovery |
VS_PLUGINS_DIRS |
--plugin-dir |
Extra plugin directories (space-separated) |
| Variable | CLI flag | Description |
|---|---|---|
VS_DEFECTDOJO_URL |
--defectdojo-url |
DefectDojo base URL |
VS_DEFECTDOJO_API_KEY |
--defectdojo-api-key |
API token |
VS_DEFECTDOJO_PRODUCT |
— | Product name |
VS_DEFECTDOJO_ENGAGEMENT |
— | Engagement name |
vuln_scanner/
├── config/
│ ├── models.py # AppConfig, AppLLMConfig, AppAgentsConfig, ChainingConfig (pydantic)
│ └── loader.py # 3-layer merge: TOML + env (VS_*) + CLI
│
├── tools/
│ ├── enums.py # Severity, Confidence, ScanStatus, ScanMode, TargetType
│ ├── models.py # Finding, ScanInput, ScanResult, ExecResult, AuthConfig (pydantic)
│ ├── target.py # classify_target() — maps target string to TargetType set
│ ├── abstract.py # AbstractTool ABC + run()/exec() subprocess helpers
│ ├── __init__.py # TOOL_REGISTRY (210 tools)
│ └── <tool>.py # One file per tool (210 total)
│
├── assets.py # Asset, AssetType, AssetStore (tool-chaining data flow)
│
├── llm/
│ ├── models.py # LLMConfig, LLMFeatures, PocConfig (pydantic)
│ ├── features.py # resolve_features() — tool > category > global merge
│ ├── client.py # LLMClient — thin openai SDK wrapper
│ ├── analyzer.py # LLMAnalyzer — 4-pass analysis pipeline
│ └── prompts.py # Default prompt templates (all overridable)
│
├── poc/
│ ├── models.py # Poc, PocVerdict
│ ├── generator.py # PocGenerator — writes scripts, never executes (host-safe)
│ └── runner.py # PocRunner — executes scripts (VS_IN_CONTAINER guard)
│
├── agents/ # Agentic layer (Pydantic AI, container-only)
│ ├── models.py # AgentConfig, AgentsConfig, AgentReport, AgentFinding, …
│ ├── guards.py # container gate, denylist, host extraction (scope)
│ ├── deps.py # AgentDeps — scope guard + ceilings shared by all tools
│ ├── sandbox.py # run_code under POSIX rlimits (CPU/mem/procs/fsize)
│ ├── audit.py # ActionLog — append-only JSONL per agent
│ ├── oob.py # interactsh/OAST wrapper for blind bugs
│ ├── agent_tools.py # list_tools, run_tool, run_code, save_bug, record_poc, oob_*
│ ├── prompts.py # bug-bounty / pentester system prompts (overridable)
│ ├── runner.py # AgentOrchestrator — sequential, timeout/ceiling→summary
│ ├── submission.py # per-bug submission reports (overridable template)
│ ├── scrub.py # secret redaction before reports/submissions
│ └── verifier.py # independent PoC re-run before a bug is marked verified
│
├── reports/
│ ├── base.py # AbstractReporter
│ ├── markdown.py # Professional Markdown (+ Discovery Chain, Agent Operations)
│ ├── html.py # Self-contained HTML with light/dark theme
│ ├── json_reporter.py # Full Assessment JSON dump
│ └── pdf.py # PDF report (reportlab)
│
├── defectdojo/
│ └── client.py # DefectDojoClient — push findings via REST API
│
├── plugins.py # Plugin auto-discovery (./plugins/, ~/.vuln-scanner/plugins/)
├── model.py # Assessment, Cluster, AssessmentStats, ChainEdge
└── orchestrator.py # ScanOrchestrator — type-gated async exec + chaining scheduler
plugins/ # Drop .py plugin files here (auto-discovered at startup)
main.py # Entry point
config.example.toml # Fully documented configuration template
.env.example # Environment variable reference
Dockerfile # BlackArch-based image; bakes VS_IN_CONTAINER=1
docker-compose.yaml # DefectDojo stack
docker-compose.scanner.yaml # Scanner service
docker-compose.target.yaml # Vulnerable test targets (Juice Shop, WebGoat)
scanner.sh # Convenience wrapper — runs the scanner via docker compose
poc.sh # End-to-end quick-start script (DefectDojo + targets + scanner)
For one-off or private tools, use the Plugin System — drop a .py file into ./plugins/ with no code changes. For tools that should ship with the project:
- Create
vuln_scanner/tools/mytool.py:
from vuln_scanner.tools.abstract import AbstractTool
from vuln_scanner.tools.enums import Severity, TargetType
from vuln_scanner.tools.models import Finding, ScanInput
class MyTool(AbstractTool):
name: str = "mytool"
category: str = "web"
# Declare which target types this tool supports.
# The orchestrator skips mismatched (tool, target) pairs automatically.
applicable_targets: frozenset[TargetType] = frozenset({TargetType.URL, TargetType.HOST})
def build_command(self, target: str, scan_input: ScanInput) -> list[str]:
return ["mytool", "--target", target]
def parse_output(self, raw: str, target: str) -> list[Finding]:
findings = []
for line in raw.splitlines():
if "VULN" in line:
findings.append(Finding(
title="Example finding",
severity=Severity.HIGH,
description=line,
tool=self.name,
target=target,
))
return findings- Register it in
vuln_scanner/tools/__init__.py:
from vuln_scanner.tools.mytool import MyTool
TOOL_REGISTRY: dict[str, type[AbstractTool]] = {
...
"mytool": MyTool,
}- Add the binary to
Dockerfile:
RUN pacman -Sy --noconfirm mytoolTips:
- For tools that write to a file instead of stdout, use
OUTPUT_FILE_SENTINELinbuild_command()and overriderun()to callself._run_with_tempfile(). - Tools with
applicable_targets = frozenset(TargetType)(the default) run against all target types — use this only for genuinely universal tools. - Binary not found →
ScanStatus.SKIPPED(hidden from report). Tool error →ScanStatus.FAILED(shown in Appendix A).
# Install with dev dependencies
uv sync
# Run tests (host-safe only — no real tool execution)
uv run pytest tests/ -v
# Lint
uv run ruff check .
uv run ruff format .Test categories:
tests/test_config.py— config merge and validationtests/test_target_typing.py—classify_target()andapplies_to()tests/test_orchestrator_gating.py— type-gating with mock toolstests/test_llm.py— LLM features, mocked client, PoC runner container guardtests/test_reports.py— all three reporters (Markdown, HTML, JSON)tests/test_nmap.py— nmap output parser
Safety rule: never run real scanning tools on the host. All tool execution happens inside the Docker container against the isolated target containers. The PocRunner enforces this — it checks VS_IN_CONTAINER=1 before executing any PoC script, and the Docker image bakes this variable in.
Findings are pushed automatically when api_key and product_name are configured.
Get your API key:
- Open DefectDojo at http://localhost:8080
- Log in (default:
admin/admin) - Go to Profile → API v2 Key
Manual push:
VS_DEFECTDOJO_API_KEY=your-key \
VS_DEFECTDOJO_PRODUCT="My App" \
uv run vuln-scanner --targets 192.168.1.1