dumbtv
dumbtv takes the ads, screen tracking and update nags out of your Fire TV over ADB. No root, none of your apps touched, every step can be undone. Free and open source.
Recon and hunting for authorized security testing, with hash-chained evidence and human-signed receipts for every lane.
SecurityPython · HTMLGitHub @attackledgerOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Proves what a security test covered: a hash-chained evidence ledger with signed, timestamped receipts and reports anyone can verify.
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Prove what was tested. An evidence system for offensive security.
AttackLedger runs recon and testing engagements (bug bounty, penetration tests,
internal assessments) and records every test as evidence in a tamper-evident
ledger. Coverage is a computed result backed by receipts. It can be mapped to the
controls auditors ask about, such as PCI DSS 11.4, ISO 27001 A.8.8 and DORA
testing requirements.
The core rule is borrowed from audit practice:Read the full README ↗Prove what was tested. An evidence system for offensive security.
AttackLedger runs recon and testing engagements (bug bounty, penetration tests, internal assessments) and records every test as evidence in a tamper-evident ledger. Coverage is a computed result backed by receipts. It can be mapped to the controls auditors ask about, such as PCI DSS 11.4, ISO 27001 A.8.8 and DORA testing requirements.
The core rule is borrowed from audit practice:
A control without evidence is a control that was not performed. In AttackLedger, "done" is a computed state, never a statement. If the receipt is missing, the work is open.
AI agents are fast, and they are also prone to completion bias. They declare a host "fully tested" after a partial pass, call a surface "exhausted" when it was rate-limited, or report "no findings" when they could not see the response. AttackLedger turns those failure modes into gates:
| Failure mode | Gate |
|---|---|
| "Done" asserted without proof | gates/close_gate.sh / gates/is_closed.sh: closure is derived from receipts and fails closed |
| Hosts silently dropped between recon and testing | gates/coverage_audit.sh + the coverage-auditor agent diff the full inventory against what was actually tested |
| Agent stops with unchecked items | hooks/lane_gate.sh (SubagentStop hook) sends the agent back while any - [ ] remains in its lane checklist |
| Destructive requests slipping through | hooks/delete_guard.sh (PreToolUse hook) requires explicit approval for DELETE-class actions |
| Weak findings reaching a report | the finding-validator agent re-verifies impact cold, before anything is filed |
An engagement is split into lanes, where a lane is one host combined with one role. Each role has a mandate, a checklist and a hard entry condition:
recon ──► mapper ──► authz ─┐
authflow ├──► finding-validator ──► report
logic ─┤
injection┘
mobile (offline, parallel)
authz, authflow, logic and injection lanes cannot open until this model exists.An orchestrator, which itself does not hunt, opens lanes, briefs agents from templates, caps concurrency and triages what comes back. A finding without evidence is returned to its agent.
See docs/ARCHITECTURE.md for the layers, the contracts for
adding a recon module or a hunt executor, and the invariants. The decisions behind
them are in docs/DECISIONS.md.
| Path | Contents |
|---|---|
methodology/ | Orchestrator playbook, agent execution prompt, secret-hunt flow, skill map |
roles/ | One mandate per role, plus the agent brief template |
checklists/ | Per-role lane checklists (what the lane gate enforces) |
agents/ | Claude Code sub-agent definitions |
skills/ | Claude Code skills: hunt-orchestrate, webapp-checklist (OWASP WSTG-based) |
gates/ | Closure, coverage and nightly audit scripts |
hooks/ | Claude Code hooks (lane gate, delete guard) |
lanes/ | Lane opening, partition checks, class matrix, agent context budget |
recon/ | Recon pipeline and helpers |
board/ | Tier board generator (attack-surface priority board) |
tools/ | Release gate used to keep this repository clean |
worker image bundles the recon tools (subfinder, dnsx,
httpx, katana, nuclei and its templates, assetfinder, gau, waybackurls,
feroxbuster, Arjun), so nothing else needs installing to run the app.agents/, skills/, hooks/, gates/). The
app does not need it. Hunt agents inside the app need only an Anthropic API key
(see Hunt agents).tools/caido_pull.py on your own machine (docs/IMPORT.md).recon/ and gates/ outside Docker: the ProjectDiscovery
stack, ffuf/feroxbuster, jq and Python 3.echo "ATTACKLEDGER_DEV_KEY=1" >> .env # trial only: a public development encryption key
docker compose up -d --build # api, worker, web, postgres, local lab target
python3 tools/seed_demo.py # optional demo engagements
open http://localhost:8080
ATTACKLEDGER_REQUIRE_SIGN_IN=1, D-045).shop.lab.test for the bundled lab, with a
research header such as X-Bug-Bounty: your-handle for the steps that send traffic.
Record the authorization with a policy URL (https://example.com/policy for the lab).All ports bind to 127.0.0.1. To require a token (do this before exposing the API
anywhere else), set ATTACKLEDGER_API_TOKEN in a .env file next to docker-compose.yml.
The lab service is a local practice target that answers as shop.lab.test inside
the compose network.
Evidence is encrypted at rest with a key per engagement, wrapped by a master key
(docs/ENCRYPTION.md). The API and worker refuse to start without one. For anything
beyond a trial, generate a key with
docker compose run --rm --no-deps api python -m app.vault generate, store it in a file
and set ATTACKLEDGER_MASTER_KEY_FILE, instead of ATTACKLEDGER_DEV_KEY=1. Keep the
master key out of your backups of the database and blob volume.
The database password defaults to change-me. Set POSTGRES_PASSWORD in .env
before the first start; Postgres keeps the password it was created with, so changing
it later also means changing it inside the database (ALTER USER attackledger PASSWORD ...).
Closed lanes are timestamped by DigiCert's public RFC 3161 service by default. Only a
SHA-256 hash leaves the machine, once per closed lane. Set ATTACKLEDGER_TSA_URL=off
in .env to send nothing (see People and roles).
The API is documented at http://localhost:8000/docs (sign in first once people exist) and http://localhost:8000/openapi.json.
Lanes and checklist items come from packs (packs/*.yaml), not code:
| Pack | Lanes | Use |
|---|---|---|
bug-bounty | recon, model, access control, auth & sessions, business logic, input handling, mobile | Bug bounty programs |
web-pentest-wstg | one per OWASP WSTG category (97 tests) | Scoped pentests, internal assessments |
Each item maps to controls in packs/controls.yaml (PCI DSS v4.0.1, ISO/IEC
27001:2022 Annex A, EU DORA), and each mapping states its evidence strength (partial or
supporting; none is full). The Controls view shows, per control, how many mapped
items on in-scope hosts are backed by receipted evidence. The mappings were reviewed
against public sources by Claude on 2026-10-09, not by a qualified assessor
(docs/CONTROLS.md). They are indicative and do not amount to a compliance determination.
Packs fail closed. A pack that references an unknown control, has a dependency cycle or contains a lane without items will not load, and the API will not start.
Every engagement exports a coverage report (Report tab, or
GET /engagements/{id}/report and /report.html). It contains the scope,
the recorded authorization, every lane and item, the evidence, the recon runs
and the control mapping. It is also verifiable offline:
python3 tools/verify_report.py attackledger-report.html
The verifier uses only the Python standard library and shares no code with AttackLedger. It performs these checks:
chain_hash = sha256(prev_hash + record)). Editing, deleting or
reordering an entry breaks the chain.Checks 4 and 5 print SKIP when no receipt is signed or timestamped: there is
nothing to check, and a skip is neither a pass nor a failure. Add
--require-signatures to fail any receipt that is not signed. The exit code is 0
only if no check failed.
The HTML report is served with default-src 'none'. No script runs in it and
nothing is fetched. All evidence text is escaped.
The pipeline follows the author's own recon process and runs in the worker
container. Steps:
| Step | Tools | Traffic |
|---|---|---|
| Find subdomains | subfinder (all sources), assetfinder, crt.sh, then DNS | passive + DNS |
| Resolve hosts | dnsx (A/AAAA, then CNAME) | DNS |
| Scan ports | connect probes made by the traffic gateway, top 100, port 25 skipped | target, opt-in per engagement |
| Find live web servers | httpx: status, title, stack, CDN, per open port | target |
| Golden targets | scoring: AUTH +4, TITLE +4, APPTECH +2, ODDPORT/KEYWORD/200 +1 | computed |
| Crawl golden hosts | katana: same host only, JS parsing, logout/delete paths never followed | target |
| Read well-known files | robots.txt, security.txt, and directory listings of the paths robots.txt names (at most 13 GETs per service) | target |
| Collect archived URLs | gau, waybackurls | passive |
| Analyse JavaScript | endpoints, GraphQL operations, sourcemaps, secret candidates (REAL / PUBLIC / NOISE) | target |
| Discover content | feroxbuster on golden hosts, one scan at a time, catch-all pages filtered by fingerprint, no recursion | target, opt-in |
| Discover hidden parameters | Arjun on dynamic endpoints, one thread, fixed delay | target, opt-in |
| Route parameters | gf-style classes → the hunt lane that tests them | none (computed) |
| Dork checklist | click-ready Google dorks per root (manual) | none (computed) |
| Scan for known issues | nuclei: takeovers (all hosts), exposures/misconfig/stack templates (one per cluster), panels/vulns/CVEs (golden); read-only templates only, checked by content | target, opt-in |
The engagement's requests per second value is a hard ceiling for every step that sends traffic, port scanning included. There is no multiplier. nuclei takes one request per tick with no retries. Above the tools' own flags, the traffic gateway enforces the ceiling for all tools together: no sliding one-second window holds more than the limit (Juice Shop benchmark through the gateway: peak 20 at a limit of 20, 0 non-GET of 5,507).
All target traffic goes through the gateway (D-039, docs/GATEWAY.md). The worker has no
route to the internet; the gateway enforces scope, GET/HEAD/OPTIONS only, the rate ceiling
and the identification, answers DNS only for in-scope names, and logs every request
(GET /engagements/{id}/gateway-log).
Secret candidates are stored masked and hashed, never in full, and are never tested against any service. JS fetches follow no redirects.
Crawl and archive output is cleaned in the same way as uro: static files are
dropped and URLs that differ only in parameter values are collapsed. Only
in-scope URLs are kept. A job ends in one of these states:
A job stopped at the time limit (WORKER_JOB_TIMEOUT, default 30 min) or at a
step's per-run target limit is marked partial, lists the targets it did not reach and can be resumed with
Run remaining.
A job is refused unless the engagement has:
*.example.com covers subdomains only; exclusions always win),https://example.com/policy (what the demo uses),The worker re-checks scope on every target and on every host a tool reports. Redirects are not followed.
Saving the scope adds each exact entry (shop.lab.test) as a host, unless an
exclusion matches it. Wildcards stay rules: recon finds their hosts. Saving never
removes a host or brings one back into scope; you can also add hosts on the Ledger tab.
A lane can be worked by a Claude agent. Put ANTHROPIC_API_KEY=... in .env (it is
passed to the traffic gateway only, which adds it to the agent's API calls; the worker and
its tools never see it), set the lane's executor to Claude agent and start a run
from the lane. The agent:
ATTACKLEDGER_AGENT_MODEL picks the model: claude-opus-5-5 (default),
claude-sonnet-5-5 (half the price) or claude-haiku-5-5 (much cheaper, weaker for
this work). Set a monthly spend limit in the Anthropic Console before the first run.
Runs stop at the first limit they reach: turns (default 15), requests (30), an estimated
cost (default $0.50) and the worker time limit. The cost is bounded before each call, from
the token counts the API returns and the price table in server/app/agentloop.py (check it
against current pricing), so a run stops before a call that could take it over. The worker
caps every run whatever it asked for: ATTACKLEDGER_AGENT_MAX_USD (default $1.00),
ATTACKLEDGER_AGENT_MAX_TURNS (30) and ATTACKLEDGER_AGENT_MAX_TOKENS (2,000,000). The model sees
only the first 4,000 characters of each response, and 40,000 per run; the full response
is kept as evidence. The loop has run against the real Claude API on the bundled lab only
(docs/BENCHMARK.md): try it on the lab first.
With no accounts, the API is open on localhost (or guarded by ATTACKLEDGER_API_TOKEN).
Add the first owner on the People page, or from the command line:
docker compose exec -it api python -m app.people create --email [email protected] --name "Your Name" --owner
The command asks for the password in the terminal (not echoed). In a script, pass it in
ATTACKLEDGER_NEW_PASSWORD instead (docker compose exec -e ATTACKLEDGER_NEW_PASSWORD ...);
it is never taken as an argument.
Owners set a person's first password only; people then change their own under "Your account". A forgotten password is reset on the server, which also ends that person's sessions:
docker compose exec -it api python -m app.people set-password --email [email protected]
Every signing key registered or revoked is recorded in a hash-chained key log
(python -m app.people key-log lists and checks it), and the person sees new keys the
next time they sign in.
Every administrative change (scope, authorization, roles, people, settings) is recorded in a hash-chained audit log that reports carry and the verifier checks: who held the reviewer role, and which scope was in force, when each receipt was issued. Captured evidence is redacted before it reaches the append-only ledger, so a client's live credentials are never stored; an owner can turn this off for a lab, and the audit log records it.
From then on everyone signs in with email and password. Owners add people and give them
roles per engagement on its Team tab: viewer (reads coverage, evidence and reports),
tester (runs recon, works lanes, attaches evidence) and reviewer (signs receipts).
Turn on separation of duties there to stop anyone signing a lane they attached
evidence to. Behind HTTPS, set ATTACKLEDGER_COOKIE_SECURE=1.
A reviewer's first close creates a signing key in their browser; the private key never
leaves it. Each receipt is then signed, and the report carries the signature and the
public key, so tools/verify_report.py checks who signed what without trusting the
server. Turn on require signatures on the Team tab to refuse unsigned closes.
Signing needs HTTPS or localhost.
The HTML report (/engagements/{id}/report.html) is written for clients and auditors: open
it in a browser and print to PDF, or check it offline with python3 tools/verify_report.py report.html.
Clients and auditors sign in as viewers: they read coverage, evidence and reports, and
the Verify tab checks every receipt's signature in their browser. The authoritative check
is python3 tools/verify_report.py report.json (it loads the timestamp roots in tools/tsa-roots/).
Receipts are also timestamped by an RFC 3161 timestamp authority: DigiCert's public
service by default (ATTACKLEDGER_TSA_URL). Only a SHA-256 hash is sent, once per
closed lane; set ATTACKLEDGER_TSA_URL=off to send nothing. The report carries each
token, and the verifier checks it against the roots in tools/tsa-roots/ (DigiCert's
is included) or any you pass:
python3 tools/verify_report.py report.json --tsa-root authority-root.pem
The API applies Alembic migrations at startup. The worker waits until the database reaches the latest revision. A database created before v0.2 is stamped at the first revision only if its schema matches the models exactly. If it does not, the API refuses to start and leaves the data alone.
# after changing server/app/models.py
cd server && DATABASE_URL=... alembic revision --autogenerate -m "describe the change"
A test fails if a model change ships without its migration.
AttackLedger is for authorized testing only: programs whose scope you are in, or systems you own or have written permission to test. The framework assumes and enforces:
v0.9.0, the MVP for a design partner plus test accounts, approved writes, organizations and an
internal security review (see CHANGELOG.md and docs/MVP.md).
Before 1.0, minor versions may change the data model; migrations upgrade existing databases.
Hunt agents are a preview and have run against the real Claude API on the bundled lab only
(docs/BENCHMARK.md). Expect rough edges.
Murat Kabak · contact: [email protected]
AttackLedger is dual-licensed:
Copyright (C) 2026 Murat Kabak.
Contributions are welcome; see CONTRIBUTING.md. To keep dual licensing possible, every contributor signs the Contributor License Agreement once, by replying to the bot's comment on their first pull request. Contributors keep the copyright in their work.