---
name: create-bug-bank-entry
description: Create or revise reusable toadex bug-bank coverage, a detector or a knowledge entry, for an established vulnerability or high-impact misconfiguration in third-party software, excluding the product under assessment. Use when validation confirmed such a bug, or when asked to add or revise an entry. Do not use to discover or validate vulnerabilities, perform target-specific testing, or write a finding report.
---

# Create a bug-bank entry

Create the smallest reusable entry that covers one vulnerability or misconfiguration of a third-party product with a low false-positive rate. A misconfiguration belongs in the bank when other deployments of the product can repeat it and it has significant impact, such as an exposed management, status or configuration endpoint, published or default keys and credentials, or an insecure default. Never cover the product under assessment.

## Choose the entry

1. Use the exact `technology` value from the affected host's `technologies` row. Search the bug bank for the vulnerability, misconfiguration or shared root cause with `toadcli bug search --tech "<technology>" <CVE, GHSA or distinctive term>`. Read each candidate with `toadcli bug show <bug_id>`. Revise matching coverage instead of creating a duplicate.
2. toadex runs no public template scanner, so the bank is its only coverage: a public detector or template for the same bug is a reference to adapt, not a reason to skip the entry.
3. Give the entry a detector whenever the affected product can be recognised over HTTP. Its `vulnerable(target)` tests what unauthenticated HTTP requests prove deterministically, without credentials or target-specific state: the whole bug, or the first link of a chain, such as the listing an exfiltration starts from, with the later steps left to the description. When they prove none of it, because the bug needs another protocol, credentials (an account the check would register counts), or a chain that depends on what each deployment ships, the detector is fingerprint-only: it leaves out `vulnerable` and calls `run(fingerprint=fingerprint)`. Detector runs list a fingerprint-only detector's matches as the hosts to test by hand and raise no findings from them, so its description carries the knowledge needed to test them. Only a product that cannot be recognised over HTTP gets a knowledge entry with no detector. Either way, cover the prerequisite, technique, constraints, expected result, and primary references, and attach one reusable Python or JavaScript helper when it materially helps apply the knowledge.

## Tag the entry

Every entry needs one to eight `tags` for its vulnerability classes and impacts, such as `["deserialization", "rce"]`, from: `rce`, `command-injection`, `sqli`, `nosql-injection`, `xss`, `ssrf`, `lfi`, `path-traversal`, `file-upload`, `auth-bypass`, `account-takeover`, `idor`, `privilege-escalation`, `deserialization`, `xxe`, `ssti`, `memory-corruption`, `info-disclosure`, `dos`, `request-smuggling`, `cache-poisoning`, `prototype-pollution`, `open-redirect`, `csrf`, `crlf-injection`, `ldap-injection`, `jwt`, `oauth`, `race-condition`, `misconfiguration`, `default-credentials`, `subdomain-takeover`, `supply-chain`. Submission rejects any other tag, and a revision keeps the entry's existing tags.

## Build a detector

1. Copy `scripts/detector.py` from this skill to `work/scratch/<specific-bug-id>.py`.
2. Cover one vulnerability or shared root cause. Reuse the existing entry ID when revising; otherwise use the CVE or GHSA when known, or a stable lowercase kebab-case ID. Keep the title specific and record affected and fixed versions, root cause, direct impact, and primary references in the description.
3. `fingerprint(target)` recognises the affected product or component wherever it is deployed, including where a login or a proxy rule keeps the bug out of reach, so it never sends the bug's own request: that belongs to `vulnerable`. The best signal is a response the component gives every visitor, such as a webhook receiver, a static file or a login page. Confirm on that same path where its behaviour allows, such as its answers to two kinds of request against a made-up sibling path: a proxy often passes only the paths a component must expose, so a second signal from the product's login page or headers loses those deployments. Start with the cheapest requests that reject unrelated hosts: one read-only request when the product shows at the root; when it answers only at a path, its likeliest locations first (the bank's findings and occurrences, then documented defaults), stopping at the first response that proves the product. Return `False` as soon as nothing gives a credible product signal; only then make a confirmatory request when one is needed. Never use bounty headers, generic statuses, common product words, login pages, or default errors as the deciding signal, nor any text the request itself carries: gateways, object stores and error pages echo the requested URL and body, so remove them from a response before matching. When the host reveals its version, reject fixed versions here; a fingerprint-only detector's matches are all a run reports. The version only rejects: a host that hides it, or runs a release other than the source target's, stays a match.
4. Keep request paths as literals or straightforward string patterns so the bank can display them in execution order. Every fingerprint and vulnerability-test path must be evident from the source.
5. A target is an origin. `target.contexts` holds up to ten application directories it is known to serve, such as `/app` its root redirects into, from the engagement's traffic. When the component can sit under an application's path, try its cheapest one or two probes under each context and under the product's documented default mount, such as `/identityiq` or `/jira`, as well as its full list at the root; never repeat the full list per context. Later requests reuse the base that answered.
6. When the bug can be tested, implement `vulnerable(target)` as the fewest deterministic requests that prove this exact bug by reading. It registers no account and leaves no file, record or session on the target: when only a write proves the bug, send one that changes nothing, such as the stored value written back, and stop at the first response that shows the bug rather than going on to its full impact. Where practical, combine multiple independent signals such as status plus a distinctive header, schema value, random marker, or state change. Count a response only when it shows the vulnerable component itself answered; a measurement or match on a proxy's or gateway's error proves nothing.
7. Keep the detector target-, engagement-, credential-, and researcher-agnostic. The source finding's requests are evidence, not a template: send only the parameters a check needs, and treat a path trick that passed the source target's proxy as one variant in `vulnerable` and a line in the description. What the source target showed beyond the product itself (an exact response body or build, its error page, an internal hostname, a tenant or object name) is that deployment's. Every request goes through `target`, which gives a run its route, its limits and a hit's stored exchanges: a detector opens no session, proxy, relay or tunnel of its own, retries nothing another way, and sets no identifying header, since the run adds each engagement's to `vulnerable`'s requests. Submission refuses a detector that does. Return booleans and send diagnostics to stderr.

A detector run may reach targets through CloudFront or Bunny rather than directly. Bunny drops the origin's `Server` header, so no deciding signal rests on it alone. A routed run keeps no cookie the target sets, since each request leaves from another IP: a flow that needs one copies it into `target.cookies`.

A blind bug (SSRF, XXE, blind command injection, deserialization gadgets that resolve or fetch) is proved by an OOB callback. `oob = await target.oob()` gives endpoints carrying a marker of their own in the target's engagement: `oob.http` (an origin; any path or subdomain of it calls back), `oob.dns` (a name; lookups of it or its subdomains call back) and `oob.smtp` (an address), each None when the engagement lacks that channel. `await oob.host(body, path=..., content_type=...)` serves a payload such as an external DTD and returns its URL. `await oob.hit(timeout=30)` is true once any of them called back. Use a fresh `target.oob()` for each check, so one target's callback never proves another. An SSRF or fetch is proved this way, by the callback or by reading back a payload `oob.host` served, never by what happens to listen on the source target's loopback or by a public callback or echo service. Only `vulnerable(target)` uses OOB; never in `fingerprint`. When verifying, name the target's engagement database with `--engagement <database>`; detector runs supply it themselves.

A detector run sends `fingerprint` to every live host across every engagement, hundreds of thousands at once, and most of them are unrelated, so every request a fingerprint sends before returning `False` is multiplied by all of them: a list of thousands of paths makes a run take hours. Weigh each further path against the deployments it would find. A run also lasts as long as its slowest host, so send a list of probes with `sweep` (imported with `Target` and `run`) rather than in batches that each wait for their slowest response. `await sweep(calls, window=32, until=found)` runs `calls`, functions that each start one probe, in order with `window` in flight, asks `until(result)` of each result as it comes, and once that is true starts no more and cancels the rest; it returns the results that came. Build `calls` with a plain `for` loop over the paths (`calls.append(lambda path=path: probe(target, path))`): the bank reads a detector's paths from loops, not comprehensions. Give probes a short `timeout=` of a few seconds, keeping the full timeout for the requests that prove the bug.

A detector run makes thousands of checks at once in each process, so a response time measured during one includes that process's queueing. When `vulnerable(target)` decides on response times, call `run(fingerprint=fingerprint, vulnerable=vulnerable, timing=True)`: each vulnerability check then runs with no other request in flight in its process.

## Verify and submit

Run the detector against the known affected target; a fingerprint-only detector must fingerprint it. Also reason against, or test when available, patched versions, similar products, generic error pages, and honeypot-like responses. It must not match any of them, save a patched version a fingerprint-only detector cannot tell apart. One affected target shows the detector can match, not that it finds the product: also run it against the hosts whose names, recorded headers or `technologies` rows show the product across engagements, and account for each one it does not match.

Submit a detector with `toadcli bug submit "<technology>" --detector <file> --target <origin> --tag <tag> ...`, where `--target` is the origin used for verification. Submission runs the detector against `--target` itself, from the worker's own address: when the target refuses that and answers through an egress route, add `--route <name>` with the route as `toadcli egress` names it. A detector that submission refuses is fixed or routed and submitted again, never replaced by a knowledge entry. Submit knowledge with `toadcli bug submit "<technology>" --entry-id <id> --title <title> --severity <severity> --detail @FILE --tag <tag> ...`, attaching any files that help reuse it (PoC, payloads, captures) with `--attach <file>`, repeated; detectors take `--attach` too. Always pass `--finding <finding_id>`, the finding that proved the bug: bug-bank hunts reuse its PoC, traffic and report to recognise and test the bug elsewhere. Pass `--host` when it adds provenance. Let submission validate and persist the entry; never insert bank rows directly or infer a version range from one host.

Finish only after submission accepts the entry and, for a detector, matches the known affected target.
