Eerlijke website-security en privacy-reconnaissance — onder PVNL-vlag
Project description
wimsalabim
Honest, audit-grade website security & privacy reconnaissance.
Wet als code · Bewijs als output · Geen ML-cargo-cult · Geen telemetrie
[!IMPORTANT]
wimsalabimis defensive tooling. It refuses, in code, to perform active scans against targets for which the operator cannot prove authorization (NL Sr 138ab compliance). This is a feature, not friction.
In one sentence
Scan a website for security and privacy issues, get a deterministic, signed, audit-grade report — without sending a single byte to a third party.
Table of contents
|
Getting started Features
Internals |
Operations Compliance Development Reference |
Why wimsalabim
The website-security-scanner space is crowded. Most tools fall into one of three buckets:
| Bucket | Examples | Problem |
|---|---|---|
| SaaS scanners | SecurityHeaders, SSL Labs, Detectify | Send your target list to a third party. Fine for public spec checks; not fine for sensitive scope. |
| Big offensive frameworks | Nuclei, Burp, ZAP | Active by default. Powerful, but assume you know your legal limits. |
| Single-domain CLIs | sslyze, testssl.sh, dnsrecon | One concern each. You stitch them yourself. |
wimsalabim lives in a different bucket: a single, opinionated CLI that:
- Runs fully local — no SaaS fan-out, no telemetry, no analytics.
- Is passive by default — active analyzers refuse to run unless you prove authorization.
- Produces signed, deterministic, machine-readable reports suitable as forensic evidence.
- Uses a transparent rule-based risk engine — every score is explainable to a non-technical reader (or a judge).
- Ships with strict typing, tests, and a security audit so the tool itself is at the standard it asks of its targets.
If you need a scanner that survives a code review by a paranoid CISO, this is it.
30-second tour
# Install
pipx install wimsalabim
# Scan a domain (passive only — works on any target)
wimsalabim scan example.com
# Get JSON for CI
wimsalabim scan example.com --format json -o report.json
# Sign the report (Ed25519)
wimsalabim keys init
wimsalabim scan example.com --sign --format json -o signed.json
wimsalabim verify signed.json # → ✓ valid
# Active port scan (requires authorization proof)
wimsalabim scan example.com \
--auth-self-owned ./my-domains.txt \
--enable ports
Sample output:
─────────────────────────── wimsalabim · example.com ───────────────────────────
target example.com started 2026-05-10T02:04:30Z duration 363 ms
dns_recon grade A 1 finding(s) 205 ms
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ sev ┃ id ┃ title ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ low │ dns.caa.absent │ No CAA record │
└──────────┴────────────────┴───────────────┘
headers grade D 7 finding(s) 351 ms
[…]
╭─────────────────── RISK B 28.0/100 engine=rules ────────────────────╮
│ Grade B (28.0/100) — 1 high — 4 rule(s) fired total. │
│ ● WSL-HEADERS-001 +12 Strict-Transport-Security header missing │
│ → HSTS absent — downgrade attacks possible on first connect. │
│ […] │
╰─────────────────────────────────────────────────────────────────────────╯
Installation
Recommended: pipx
pipx install wimsalabim
Alternative: pip in a venv
python -m venv .venv
source .venv/bin/activate
pip install wimsalabim
From source
git clone https://github.com/WimLee115/wimsalabim.git
cd wimsalabim
pip install -e ".[dev]"
Optional dependencies
| Feature | Install |
|---|---|
| OpenTimestamps anchors | pipx install opentimestamps-client |
| Tor transport | A running local Tor daemon (port 9050) |
Supported environments
| OS | Python | Status |
|---|---|---|
| Linux | 3.10 / 3.11 / 3.12 / 3.13 | ✓ CI |
| macOS | 3.10 / 3.11 / 3.12 / 3.13 | ✓ CI |
| Windows | 3.10 / 3.11 / 3.12 / 3.13 | ✓ CI |
| FreeBSD | 3.11+ | best-effort |
Quick start
# Show help and global options
wimsalabim --help
# List all registered analyzers and their legal class
wimsalabim analyzers
# A clean passive scan (DNS + TLS + headers, no port-scan)
wimsalabim scan example.com
# Pick specific analyzers
wimsalabim scan example.com --enable dns_recon --enable headers
# Skip noisy ones
wimsalabim scan example.com --disable headers
# Output formats
wimsalabim scan example.com --format json
wimsalabim scan example.com --format markdown
wimsalabim scan example.com --format sarif
# Save the report alongside terminal rendering
wimsalabim scan example.com --format json -o report.json
# Verify a signed report
wimsalabim verify signed-report.json
# Manage signing keys
wimsalabim keys init
wimsalabim keys show
The ten upgrades
wimsalabim v0.2.0 is a complete refactor of v0.1. Each pillar below is a deliberate departure from the previous design:
| # | Pillar | Implementation |
|---|---|---|
| 01 | Authorization Gate | Active analyzers refuse to run without verified proof. See legal compliance. |
| 02 | Provenance Engine | Every Finding carries a Source block (kind, target, timestamp, body hash). |
| 03 | Crypto-signed reports | Ed25519 signature over RFC-8785 canonical JSON; OpenTimestamps anchor optional. |
| 04 | Honest risk engine | Rule-based, traceable, explainable. No IsolationForest decoration with hardcoded "training_samples=500". |
| 05 | Plugin architecture | @analyzer(...) decorator + capability manifest; registration is explicit. |
| 06 | Standards-aware export | SARIF 2.1.0, JSON canonical, GitHub-flavored Markdown, rich terminal. |
| 07 | Watchlist mode | SQLite snapshot + diff detection over time. |
| 08 | Privacy-by-design infrastructure | Telemetry blacklist enforced at the HTTP-client level; WHOIS-PII redacted by default. |
| 09 | Rigorous QA | mypy --strict, ruff 40+ rule sets, bandit, pip-audit, ≥ 70% test coverage. |
| 10 | Meesterlijke TUI | Rich live rendering, color-coded grades, structured panels. |
Built-in analyzers
| Analyzer | Legal class | Purpose |
|---|---|---|
dns_recon |
passive | A / AAAA / MX / NS / TXT / SOA / CNAME / CAA + DNSSEC presence |
tls |
passive | Single TLS handshake; leaf-cert validity, expiry, protocol, cipher |
headers |
passive | Single GET; security & info-leak headers |
ports |
active | Async TCP-connect scan over a conservative port set (auth required) |
Legal class semantics:
- passive — uses only public data (DNS, CT, WHOIS, single GET, single TLS handshake). Not "binnendringen" under NL Sr 138ab.
- active — performs network probes that go beyond what a regular client would do. Refused unless
Authorizationproves operator owns the target or runs an authorized bug-bounty. - intrusive — anything that could alter or stress target state. Refused unless authorized and
--allow-intrusiveis provided.
Output formats
Rich (default)
Color-coded, tabular terminal output with a final risk panel. Honors NO_COLOR and TERM=dumb.
JSON (canonical)
wimsalabim scan example.com --format json -o report.json
- RFC-8785 canonical key-ordering (sorted, no insignificant whitespace).
- ISO-8601 UTC timestamps.
- Schema version:
wimsalabim/2.0. - Idempotent: same input + same time-of-day yields byte-identical output (modulo
started_at).
{
"schema_version": "wimsalabim/2.0",
"tool_version": "0.2.0",
"target": "example.com",
"started_at": "2026-05-10T02:04:30Z",
"duration_ms": 363.0,
"config_hash": "ea1b…",
"analyzers": {
"dns_recon": {
"name": "dns_recon",
"legal_class": "passive",
"status": "ok",
"report": {
"analyzer": "dns_recon",
"grade": "A",
"findings": [{"id": "dns.caa.absent", "severity": "low", …}],
"metadata": {"dnssec": true, "total_records": 9}
}
}
},
"risk": {
"engine": "rules",
"overall_score": 28.0,
"grade": "B",
"rules_fired": [{"rule_id": "WSL-HEADERS-001", …}]
}
}
Markdown
GitHub-flavored, suitable for direct posting in PR comments or issues:
wimsalabim scan example.com --format markdown
SARIF 2.1.0
For GitHub Code Scanning, GitLab Security Dashboard, Defect Dojo:
wimsalabim scan example.com --format sarif -o sarif.json
# Upload to GitHub Code Scanning:
gh codeql database upload sarif.json
Findings carry CWE-ids and (where applicable) CVSS-vectors so the receiving system can prioritize correctly.
Authorization paths
Three ways to prove you may run active analyzers against a target. Pick one; failures are loud.
1 · Self-owned manifest
Local file with one host per line:
# my-domains.txt
example.com
api.example.com
internal.lab.example.com
wimsalabim scan api.example.com \
--auth-self-owned ./my-domains.txt \
--enable ports
Use case: dev/CI environments where you have independent ownership evidence.
2 · DNS TXT record
Publish a TXT record at _wimsalabim-auth.<target>:
_wimsalabim-auth.example.com. IN TXT "v=wimsalabim1 pubkey=<base64-ed25519-32B>"
wimsalabim scan example.com \
--auth-dns-txt "<your-pubkey-base64>" \
--enable ports
Use case: bug-bounty programs with a stable public pubkey per researcher.
3 · Well-known file
Publish a signed manifest at https://<target>/.well-known/wimsalabim-auth.txt:
v=wimsalabim1 pubkey=<base64> sig=<base64> target=example.com
wimsalabim scan example.com \
--auth-well-known "<expected-pubkey-base64>" \
--enable ports
Use case: bug-bounty programs that prefer file-based assertion (HackerOne-style).
[!CAUTION] Without an authorization proof, the orchestrator emits
deniedfor every active analyzer. The denial is logged in the report so reviewers see the gate did its job.
Cryptographic integrity
Sign
wimsalabim keys init
wimsalabim scan example.com --sign --format json -o signed.json
What happens:
- Report is canonicalized (RFC 8785).
signatureandsigning_pubkeyfields are stripped.- Remaining bytes are SHA-256 hashed.
- Hash is signed with Ed25519 (private key in
~/.wimsalabim/keys/signing.key, mode 0600). - Signature + pubkey are added back to the JSON.
Verify
wimsalabim verify signed.json
Output:
✓ valid pubkey a4er2DIQ/HIwnIHp… digest 0ea1269b8089d100…
If the report was tampered:
✗ INVALID signature
$ echo $?
2
Anchor with OpenTimestamps (optional)
# Install the upstream client
pipx install opentimestamps-client
# Compute digest, stamp it
sha256sum signed.json | awk '{print $1}' > signed.digest
ots stamp signed.digest
# Wait a few hours for upgrade
ots upgrade signed.digest.ots
# Verify against Bitcoin block headers
ots verify signed.digest.ots
The .ots proof is an objectively-verifiable timestamp anchored on the Bitcoin chain. No trust in any single party required.
Watchlist mode
The wimsalabim watch subcommand re-scans one or more targets on a fixed
interval, persists each result to a SQLite baseline, and reports drift
(added/removed findings, grade changes) against the previous snapshot.
# Scan example.com every hour, default DB at ~/.wimsalabim/watch.sqlite
wimsalabim watch example.com
# Multiple targets, 15-minute interval, only print rounds with a diff
wimsalabim watch --interval 900 --diff-only a.example b.example
# One-shot smoke test (records baseline + exits)
wimsalabim watch --once example.com
Stops cleanly on SIGINT / SIGTERM. A failing scan does not bring the
daemon down; it logs and proceeds to the next target. Authorization
options (--auth-self-owned, --auth-dns-txt, --auth-well-known) are
verified once per target at startup.
Programmatic usage
from pathlib import Path
import asyncio
from wimsalabim.core.orchestrator import Orchestrator, OrchestratorConfig
from wimsalabim.core.authorization import AuthorizationGate
from wimsalabim.core.registry import all_analyzers
import wimsalabim.analyzers # registers built-ins
from wimsalabim.watch import BaselineStore
store = BaselineStore(Path("./baseline.sqlite"))
async def watch(target: str) -> None:
orch = Orchestrator(
config=OrchestratorConfig(target=target, enabled=("dns_recon", "tls", "headers")),
registrations=list(all_analyzers().values()),
gate=AuthorizationGate(),
)
report = await orch.run()
diff = store.diff_against_previous(report)
if diff and diff.is_meaningful:
notify(diff)
store.record(report)
asyncio.run(watch("example.com"))
The Diff object lists added findings, removed findings, and grade changes per analyzer.
Architecture
%%{init: {'theme':'dark'}}%%
flowchart TD
CLI["CLI<br/>(click)"]
Orch["Orchestrator<br/>(async, per-analyzer timeout)"]
Gate["AuthorizationGate<br/>(legal_class check)"]
HTTP["HTTP client factory<br/>(telemetry blacklist hook)"]
Reg["Registry<br/>(@analyzer decorator)"]
Risk["HeuristicRiskEngine<br/>(rule-based)"]
Disp["Renderers<br/>rich · json · md · sarif"]
Crypto["Ed25519 sign/verify<br/>+ OTS anchor"]
CLI --> Orch
Orch --> Gate
Orch --> HTTP
Orch --> Reg
Reg -.-> A1["dns_recon"]
Reg -.-> A2["tls"]
Reg -.-> A3["headers"]
Reg -.-> A4["ports (active)"]
Orch --> Risk
Risk --> Disp
Disp --> Crypto
classDef gate fill:#221,stroke:#fc6,color:#fff
class Gate gate
Module breakdown
src/wimsalabim/
├── cli.py Click entrypoint, slim
├── analyzers/
│ ├── base.py BaseAnalyzer ABC + AnalysisContext
│ ├── dns_recon.py DNS analyzer (passive)
│ ├── tls.py TLS analyzer (passive)
│ ├── headers.py HTTP headers analyzer (passive)
│ └── ports.py TCP-connect scan (active)
├── core/
│ ├── schema.py Pydantic v2 frozen models
│ ├── exceptions.py Typed exception hierarchy
│ ├── logging.py structlog setup
│ ├── registry.py @analyzer + capabilities
│ ├── authorization.py AuthorizationGate + verification helpers
│ ├── orchestrator.py Async runner with timeouts
│ ├── http_client.py httpx factory + telemetry guard
│ ├── privacy.py WHOIS redact + telemetry blacklist
│ ├── canonical.py RFC 8785 canonical JSON
│ ├── crypto.py Ed25519 sign/verify
│ └── timestamps.py OpenTimestamps wrapper
├── risk/
│ ├── rules.py Rule registry
│ └── heuristic.py Rule executor + grading
├── display/
│ ├── rich_renderer.py Rich terminal output
│ ├── markdown.py GitHub Markdown export
│ └── sarif.py SARIF 2.1.0 export
└── watch/
└── baseline.py SQLite snapshot store + Diff
Schema & contracts
Every BaseReport produced by an analyzer satisfies this contract:
class BaseReport(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
analyzer: str
target: str
started_at: datetime # always UTC
duration_ms: float # ≥ 0
grade: Grade = "N/A" # Literal["A","B","C","D","F","N/A"]
findings: list[Finding]
metadata: dict[str, str|int|float|bool]
Every Finding carries provenance:
class Finding(BaseModel):
id: str # e.g. "tls.cert.expiring_soon"
title: str
description: str
severity: Severity # Literal["critical","high","medium","low","info"]
source: Source # required
cwe: str | None # CWE-NNN
cvss_vector: str | None # CVSS v4.0 if applicable
cvss_score: float | None # 0.0–10.0
remediation: str | None
references: list[str]
Every Source answers where did this come from:
class Source(BaseModel):
kind: str # "http","dns","tls","ct_log","whois",…
target: str
timestamp: datetime # always UTC
body_sha256: str | None # 64-hex when applicable
metadata: dict[str, str]
These are pydantic v2 frozen models — once produced, immutable. That is what lets us hash, sign, and timestamp them safely.
The risk engine
Two engines, both honest:
--engine=rules (default)
Every point in the score traces back to a registered Rule with id, severity, CWE, and rationale. Adding a rule = appending to RULE_REGISTRY in src/wimsalabim/risk/rules.py.
Rule(
rule_id="WSL-TLS-002",
name="TLS certificate expiring within 7 days",
severity="critical",
points=25.0,
cwe="CWE-298",
predicate=lambda r: _has_finding(r, "tls", "tls.cert.expiring_soon"),
rationale_fn=lambda _: "Leaf certificate expires within 7 days — outage risk imminent.",
)
Grading is severity-aware: any unmitigated critical pulls the final grade to D-or-worse, even when the raw point sum lands in C-band. This reflects how rational operators actually treat criticals.
--engine=ml (planned, conditional)
We will only ship a ml engine if it satisfies all of:
- A real model file in the repository (
models/risk_v0.X.onnx) with a published SHA-256. - A
model_card.mddescribing training data, features, expected accuracy, and known biases. - Reproducible training: dataset hash, training script, fixed seed, train/val/test split.
- A test suite that verifies model predictions for held-out samples.
Until those exist, there is no ML engine. The previous version's "ML modules" were sklearn classes imported but never trained — that is dishonest and is gone.
Plugin development
Write your own analyzer
# my_org/scanners/cookie_audit.py
from datetime import datetime, timezone
from wimsalabim.analyzers.base import AnalysisContext, BaseAnalyzer
from wimsalabim.core.exceptions import NetworkError
from wimsalabim.core.registry import Capabilities, analyzer
from wimsalabim.core.schema import BaseReport, Finding, Source
@analyzer(
"cookie_audit",
legal_class="passive",
capabilities=Capabilities(network=("https",), rate_limit_per_second=2, timeout_seconds=10.0),
description="Inspect Set-Cookie attributes (Secure, HttpOnly, SameSite).",
)
class CookieAnalyzer(BaseAnalyzer):
async def analyze(self, context: AnalysisContext) -> BaseReport:
started = datetime.now(tz=timezone.utc)
try:
response = await context.http.get(f"https://{context.target}/")
except Exception as exc:
raise NetworkError(kind="http", target=context.target, message=str(exc)) from exc
findings: list[Finding] = []
# ... inspect response.cookies and emit findings ...
return BaseReport(
analyzer="cookie_audit",
target=context.target,
started_at=started,
duration_ms=(datetime.now(tz=timezone.utc) - started).total_seconds() * 1000,
findings=findings,
metadata={},
)
Then register it in your project:
# my_project/__init__.py
import my_org.scanners.cookie_audit # imports trigger @analyzer
Capabilities declaration
| Field | Purpose |
|---|---|
network |
What protocols you'll touch (dns, https, ...). The Authorization Gate consults this for legal-class enforcement. |
rate_limit_per_second |
Hint for the orchestrator's rate limiter (orchestrator-side rate limiting is on the roadmap). |
timeout_seconds |
Per-call soft timeout the orchestrator will enforce. |
Legal class
| Class | When to pick it |
|---|---|
passive |
You only read public data (DNS, CT, single GET, single TLS handshake). |
active |
You probe the target beyond what a regular client would do. |
intrusive |
You may alter target state (writes, fuzzing, load). |
If in doubt: choose the stricter option. The framework will refuse to run you unless authorization is proven. Better than introducing legal risk.
Privacy guarantees
┌────────────────────────────────────────────────────────────────┐
│ │
│ 1. No telemetry. No analytics. No phone-home. Ever. │
│ 2. WHOIS PII (registrant name/email/phone) redacted unless │
│ --show-pii is explicitly set. │
│ 3. Bodies hashed up to 64 KB; full content never stored or │
│ transmitted. │
│ 4. Reports stored locally; signing keys local + 0600. │
│ 5. --via-tor routes both HTTP and DNS via SOCKS5+Tor. │
│ 6. --offline forbids any outbound network call. │
│ │
└────────────────────────────────────────────────────────────────┘
Verified by:
core/privacy.TELEMETRY_BLACKLISTenforced via httpx event hook inmake_client().tests/test_no_telemetry.py(21 cases) statically forbids importing known telemetry SDKs.tests/test_privacy.pyvalidates redaction logic.
Configuration
CLI flags (full)
Usage: wimsalabim scan [OPTIONS] TARGET
Options:
-e, --enable TEXT Enable only these analyzers (repeatable).
-d, --disable TEXT Disable these analyzers (repeatable).
--via-tor Route HTTP via local Tor SOCKS5 (127.0.0.1:9050).
--offline Forbid any outbound network call.
--show-pii Do not redact PII (use only on data you own).
--allow-intrusive Permit intrusive analyzers (extra confirmation).
--auth-self-owned PATH Path to self-owned domains manifest.
--auth-dns-txt TEXT Verify _wimsalabim-auth.<target> TXT record.
--auth-well-known TEXT Verify /.well-known/wimsalabim-auth.txt manifest.
--format [rich|json|markdown|sarif]
Output format (default: rich).
-o, --output PATH Write report to file (also rendered to stdout).
--sign Sign the report (Ed25519).
--keys-dir PATH Signing key directory (default: ~/.wimsalabim/keys).
--verbose / --quiet
-h, --help Show this message and exit.
Environment variables
| Variable | Effect |
|---|---|
NO_COLOR |
Disable color in rich rendering. |
WIMSALABIM_KEYS_DIR |
Override default ~/.wimsalabim/keys (planned for v0.3). |
Configuration file (planned for v0.3)
A ~/.config/wimsalabim/config.toml will allow persistent flags. For v0.2.0, all configuration is via CLI flags.
Performance & resources
Numbers below from a passive scan of example.com over residential broadband:
| Metric | Value |
|---|---|
| Wall-clock duration | ~360 ms |
| Concurrent analyzers | 3 (parallel via asyncio) |
| Peak memory | ~25 MB |
| Outbound HTTP | 1 GET to https://target/ |
| Outbound DNS | 9 record lookups |
| Outbound TLS | 1 handshake |
| Disk I/O | 1 SQLite write (if watching), 1 JSON file (if -o) |
ports analyzer scales with port-list size; default 23 ports complete in 1–3 seconds against a responsive host.
Use as a library
import asyncio
from wimsalabim.core.orchestrator import Orchestrator, OrchestratorConfig
from wimsalabim.core.authorization import AuthorizationGate
from wimsalabim.core.registry import all_analyzers
from wimsalabim.risk.heuristic import HeuristicRiskEngine
import wimsalabim.analyzers # register built-ins
async def scan(target: str) -> None:
orch = Orchestrator(
config=OrchestratorConfig(
target=target,
enabled=("dns_recon", "headers"),
),
registrations=list(all_analyzers().values()),
gate=AuthorizationGate(),
)
report = await orch.run()
risk = HeuristicRiskEngine().assess(report.analyzers)
print(report.model_dump_json(indent=2))
print("RISK:", risk.grade, risk.overall_score)
asyncio.run(scan("example.com"))
Public API stable surface (semver-tracked from v1.0):
wimsalabim.core.schema—Source,Finding,BaseReport,ScanReport,RiskAssessment, …wimsalabim.core.orchestrator—Orchestrator,OrchestratorConfigwimsalabim.core.authorization—AuthorizationGate,Authorizationwimsalabim.core.registry—analyzerdecorator,Capabilities,all_analyzerswimsalabim.risk.heuristic—HeuristicRiskEnginewimsalabim.display—render_rich,render_markdown,render_sarifwimsalabim.watch—BaselineStore,Diff
Legal compliance
NL Sr art. 138ab — computervredebreuk
"Hij die opzettelijk en wederrechtelijk binnendringt in een geautomatiseerd werk …"
The AuthorizationGate refuses to run active and intrusive analyzers without verified proof. There is no escape hatch in the CLI — no --yolo, no --force. This is the technical implementation of binnendringen alleen met toestemming.
EU GDPR / NL AVG
| Article | How we honor it |
|---|---|
| Art. 5 | Data minimization: bodies hashed (not stored), WHOIS-PII redacted. |
| Art. 6 | We process only public data + operator-controlled targets. |
| Art. 25 | Privacy by design: telemetry blacklist enforced at HTTP-client layer. |
| Art. 32 | Security of processing: signed reports, local 0600 keys. |
EU AI-Act
The HeuristicRiskEngine is not an AI system under the AI-Act:
- No learned model. No training. No statistical generalization.
- Every score derives deterministically from a registered rule.
- Fully explainable; no "automated decision-making" without a human-readable predicate.
If we ever ship --engine=ml, it will pass the same explainability test plus dataset and model cards.
NCSC responsible disclosure
Findings produced by wimsalabim:
- Generate a signed JSON or SARIF report.
- Send to the target's security contact (look up
security.txtfirst, thenRFC 9116). - Apply a 90-day disclosure window (industry standard) unless the receiver requests shorter.
Threat model
We use STRIDE for self-analysis. Full table in docs/PENTEST_REPORT.md. High-level:
| Threat | Status | Mitigation |
|---|---|---|
| Spoofing operator | Mitigated | Authorization paths require operator-controlled DNS/file. |
| Tampering with reports | Mitigated | Ed25519-signed canonical JSON + tamper-detect via verify. |
| Repudiation | Mitigated | signing_pubkey + verified_at block in report. |
| Information disclosure | Mitigated | WHOIS-PII redact, body-hash limited, no telemetry. |
| DoS via slow analyzer | Mitigated | Per-analyzer timeout enforced by orchestrator. |
| Elevation of privilege | Mitigated | AuthorizationGate.check() is mandatory and fail-closed. |
Security audit
A full pentest report is committed to the repository at docs/PENTEST_REPORT.md.
Summary:
| Tool | Result |
|---|---|
mypy --strict |
0 errors across 30 source files |
ruff (40+ rule sets) |
All checks passed |
bandit |
5 LOW (acceptable subprocess in OTS), 0 medium, 0 high |
pip-audit |
No known vulnerabilities |
pytest |
118 / 118 passed |
| Coverage | 76.42% (over 70% threshold) |
Manual review covered SSRF, path traversal, command injection, regex DoS, race conditions, secrets/credential leaks. Zero critical or high findings.
Comparison with other tools
| Capability | wimsalabim |
nuclei |
sslyze |
dnsrecon |
SecurityHeaders.com |
|---|---|---|---|---|---|
| Local execution (no SaaS) | ✓ | ✓ | ✓ | ✓ | ✗ |
| Passive-by-default with hard gate | ✓ | ✗ | n/a | n/a | ✓ |
| Signed reports (Ed25519) | ✓ | ✗ | ✗ | ✗ | ✗ |
| OpenTimestamps anchor | ✓ (optional) | ✗ | ✗ | ✗ | ✗ |
| SARIF 2.1.0 output | ✓ | ✓ | ✗ | ✗ | ✗ |
| Pydantic-validated schema | ✓ | partial | ✗ | ✗ | n/a |
| Rule-based explainable risk score | ✓ | (per template) | ✗ | ✗ | ✓ (opaque) |
| WHOIS PII redacted by default | ✓ | n/a | n/a | n/a | n/a |
mypy --strict clean |
✓ | n/a (Go) | partial | ✗ | n/a |
| Plugin API for custom analyzers | ✓ | ✓ | ✗ | ✗ | ✗ |
Where the others excel:
- nuclei: vast template ecosystem for vulnerability detection. We complement, not replace.
- sslyze: deeper TLS analysis (we do single-handshake; sslyze does cipher-suite enumeration).
- dnsrecon: more DNS modes (zone transfer, brute-force) — by design, we stay passive.
Testing & coverage
make test # full suite + coverage
make lint # ruff check + format
make typecheck # mypy --strict
make audit # bandit + pip-audit
make all # everything in CI order
Without make:
pytest -v # 118 tests
pytest --cov=wimsalabim --cov-report=term-missing # with coverage
ruff format --check src tests
ruff check src tests
mypy src
bandit -r src --severity-level low
pip-audit
Typing
Strictness:
[tool.mypy]
strict = true
disallow_untyped_defs = true
disallow_any_generics = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unreachable = true
no_implicit_optional = true
Result: 0 errors across the codebase.
Linting
Active rule sets: E, F, W, I, N, UP, B, C4, SIM, RUF, ASYNC, TRY, PTH, PL. Result: 0 violations.
Releasing
- Bump
versioninpyproject.toml. - Update
CHANGELOG.md. - Tag:
git tag -s v0.X.Y -m "release v0.X.Y". - Push tag:
git push origin v0.X.Y. - The release CI builds wheels + sdist, runs tests + audit, signs the artifacts, and uploads to PyPI.
(The release CI workflow is planned — currently make build produces the artifacts locally.)
Roadmap
2026 Q2 · Public release of v0.2.0 [DONE]
2026 Q2 · `wimsalabim watch` CLI subcommand [DONE]
2026 Q2 · Mock TLS server for tls.py coverage ≥ 85% [next]
2026 Q2 · Socket-mock fixture for ports.py coverage ≥ 85%
2026 Q2 · Periodic re-verification of authorization in watch sessions
2026 Q3 · CycloneDX VEX export
2026 Q3 · Mutation testing (mutmut) with ≥ 80% kill rate
2026 Q3 · Atheris fuzzing on parsers (cert, headers)
2026 Q4 · Remediation wizard (interactive --fix flow)
2026 Q4 · Shell completions (bash / zsh / fish)
2027 · PQC hybrid keys (X25519 + ML-KEM)
2027 · Distributed-watch federation across PVNL operators
Troubleshooting
denied in scan output
Means the AuthorizationGate refused an active analyzer. Either:
- Use only passive analyzers:
--enable dns_recon --enable headers --enable tls. - Provide an authorization proof:
--auth-self-owned,--auth-dns-txt, or--auth-well-known.
OpenTimestampsUnavailable: ots CLI not found
Install the upstream client:
pipx install opentimestamps-client
OTS-anchoring is opt-in; the rest of wimsalabim works without it.
NetworkError: blocked: target is on the telemetry blacklist
You scanned a domain that overlaps with the telemetry blacklist (e.g., *.google-analytics.com). This is intentional — wimsalabim won't speak to known surveillance endpoints. If you genuinely need to scan such a host, it is out-of-scope for this tool.
httpx.ConnectError: All connection attempts failed
The target is not reachable from your network. Check connectivity, DNS, and firewall.
Signature verification failed on an unsigned report
wimsalabim verify requires both signature and signing_pubkey fields. Use --sign when generating.
FAQ
Why not just use nmap + sslyze + dnsrecon?
You can. We needed one tool that:
- Refuses unauthorized active scans by design.
- Produces a single signed report instead of three text logs.
- Runs in CI without requiring root.
Why no GUI?
CLI tools compose; GUIs don't. The --format markdown output renders fine in any markdown viewer; SARIF is consumed by GitHub/GitLab dashboards directly.
Why Python and not Go/Rust?
Speed is not the bottleneck — network I/O is. Python with asyncio is fast enough, and the ecosystem (pydantic, rich, structlog, click) gives us safety + UX rapidly. We may rewrite hot paths in Rust if benchmarks ever demand it.
Can I run this in CI?
Yes. Pin the version, use --format sarif to upload to GitHub Code Scanning, and use --auth-self-owned with a manifest committed to your repo. Example workflow snippet:
- name: Wimsalabim scan
run: |
pipx install wimsalabim
wimsalabim scan ${{ vars.TARGET_DOMAIN }} \
--auth-self-owned ./security/owned-domains.txt \
--format sarif -o sarif.json
- uses: github/codeql-action/upload-sarif@v3
with: { sarif_file: sarif.json }
Does this replace a real pentest?
No. It catches the easy 80%. A real pentest finds the hard 20% (auth-flow flaws, business-logic bugs, complex chains). Use both.
Contributing
We welcome contributions. Read CONTRIBUTING.md and CODE_OF_CONDUCT.md.
Quick start:
git clone https://github.com/WimLee115/wimsalabim.git
cd wimsalabim
make dev # creates venv, installs dev deps, hooks
make all # lint + typecheck + test + audit
What we're looking for:
- New passive analyzers (cookie audit, security.txt, robots.txt, sitemap.xml, ...).
- Mock fixtures for
tls.pyandports.pyto lift coverage. - More CWE/CVSS mappings on existing findings.
- Translations of user-facing strings (we currently render English).
What we don't accept:
- Active offensive features without a clear authorization story.
- Telemetry, analytics, or phone-home.
- "AI" features that aren't backed by a published model card.
Security disclosure
Vulnerabilities in wimsalabim itself: see SECURITY.md. 90-day disclosure window.
License
AGPL-3.0-or-later for code; CC BY-SA 4.0 for documentation.
See LICENSE.
AGPL-3.0 means: if you run
wimsalabimas a service, you must share your modifications with users of that service. We chose this deliberately to keep the ecosystem open.
Citation
If you use wimsalabim in academic work, please cite:
@software{wimsalabim_2026,
title = {wimsalabim: Honest, audit-grade website security and privacy reconnaissance},
author = {WimLee115},
year = {2026},
url = {https://github.com/WimLee115/wimsalabim},
note = {Under PVNL flag — Privacy Verzet NL}
}
A CITATION.cff is provided for automatic citation tooling.
Acknowledgements
- The
cryptography,pydantic,httpx,click,rich,structlog,dnspythoncommunities for the building blocks. - The OpenTimestamps team for making timestamp-anchoring simple and free.
- The OASIS SARIF working group for the result-interchange format.
- Our fellow travelers under the PVNL (Privacy Verzet NL) flag — for the conviction that privacy is a civil right, not a feature.
╔═══════════════════════════════════════════════════════════╗
║ ║
║ "Wet als code. Bewijs als output." ║
║ ║
║ CAPTAIN WIMLEE115 · MMXXVI ║
║ ║
║ PVNL · Privacy Verzet NL ║
║ ║
╚═══════════════════════════════════════════════════════════╝
Documentation · Pentest report · Legal kader · PVNL umbrella
Project details
Release history Release notifications | RSS feed
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 wimsalabim-0.3.0.tar.gz.
File metadata
- Download URL: wimsalabim-0.3.0.tar.gz
- Upload date:
- Size: 120.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
459a3c4a7e0f0944d99e17090b7694b8bf6d8239fcc5bc52db95750537689c87
|
|
| MD5 |
7698306503568043d1d3b23d1905fa00
|
|
| BLAKE2b-256 |
9942cfeecabd877dad01c06a099722fae933ddcd3d9e135c47a11064ba15df0a
|
Provenance
The following attestation bundles were made for wimsalabim-0.3.0.tar.gz:
Publisher:
release.yml on WimLee115/wimsalabim
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wimsalabim-0.3.0.tar.gz -
Subject digest:
459a3c4a7e0f0944d99e17090b7694b8bf6d8239fcc5bc52db95750537689c87 - Sigstore transparency entry: 1488875570
- Sigstore integration time:
-
Permalink:
WimLee115/wimsalabim@5976b8e96e1421e134763754a435d86c6cc2abbe -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/WimLee115
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5976b8e96e1421e134763754a435d86c6cc2abbe -
Trigger Event:
push
-
Statement type:
File details
Details for the file wimsalabim-0.3.0-py3-none-any.whl.
File metadata
- Download URL: wimsalabim-0.3.0-py3-none-any.whl
- Upload date:
- Size: 72.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aa70dfefa1e626acb581ddd8e5f3ccd10d843e488be862fc0f8d8f41ee14637c
|
|
| MD5 |
41c06c8b54a5675148bf12eeb3e05634
|
|
| BLAKE2b-256 |
d789af798d2d2764007c6615182049628b6a9df60438c31be848fa259d33d2fd
|
Provenance
The following attestation bundles were made for wimsalabim-0.3.0-py3-none-any.whl:
Publisher:
release.yml on WimLee115/wimsalabim
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wimsalabim-0.3.0-py3-none-any.whl -
Subject digest:
aa70dfefa1e626acb581ddd8e5f3ccd10d843e488be862fc0f8d8f41ee14637c - Sigstore transparency entry: 1488875847
- Sigstore integration time:
-
Permalink:
WimLee115/wimsalabim@5976b8e96e1421e134763754a435d86c6cc2abbe -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/WimLee115
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5976b8e96e1421e134763754a435d86c6cc2abbe -
Trigger Event:
push
-
Statement type: