Red-team catalog reference: profiles, raw-protocol attacks and verdict classification
Diátaxis quadrant: Reference. Audience: defensive-test developers who need the authoritative catalog map and profile contract.
This page is the authoritative human-readable map of the Red catalog: the profile contract every .mjs file honours, the labels used for evaluation, the verdict classification applied by the end-to-end runner, the 65 catalog entries, and the Go raw-protocol client. It is a lookup reference, not a build guide. To author a profile, see Write a Red profile; for how the pieces fit together, see Red catalog architecture.
Important: This catalog is purely defensive, local-only and self-target-only. Everything here exercises your own detector’s test catalog against your own loopback server; it teaches understanding and extending that catalog, never third-party evasion. Read the Red-team rules of engagement before running anything — that page carries the guardrail these profiles operate under (loopback target hard-coded, no external hosts). Blocked traffic here is “automated” / “not verified as human”. Every verdict named below is reference-measured on your machine, not a guarantee: coherent browser or human-assisted automation-class adversaries (anti-detect plus real-human click-farms) remain a design boundary mitigated by rate and reputation, and the heuristic rules datacenter-network browser rule / no interaction observed rule / missing or replayed request-integrity token rule can challenge some real humans.
The profile contract
Every profile is an ES module under test/redteam/ that exports three things. The shape is fixed because test/e2e/runner.mjs imports the module and reads exactly these fields.
| Export | Type | Meaning |
|---|---|---|
run(baseURL) |
async function returning {verdict, riskScore, hardRuleFired, topContributors} |
Drives one attack against baseURL and returns exactly what POST /api/collect returns — the runner reads verdict, riskScore, hardRuleFired, and topContributors[].id. |
label |
const string |
Classification label. Must start with bot: for an automated sample (see below). |
needsBrowser |
const boolean |
true if the profile launches a real browser via _driver.mjs; false if it POSTs a spoofed report or shells a raw attack. |
The returned object is the /api/collect response verbatim: verdict is one of ALLOW | CHALLENGE | DENY, riskScore is 0–100, hardRuleFired is the winning enforcement-rule identifier or empty, and topContributors is the ranked {id, score} list. The runner records topContributors.map(c => c.id) into results.json.
Helper modules _bin.mjs (wraps the Go cmd/redteam binary) and _driver.mjs (launches the browser) export no run() and are not profiles.
The bot: label rule
classify() decides whether a sample is automated or human baseline solely from the label prefix:
- A label starting with
bot:is scored as automation. - The only non-bot label is
human(inhuman.mjs), the human baseline.
Warning: Getting the label wrong silently flips a profile into the human baseline. A profile that forgets the
bot:prefix is treated as a human sample: its DENY becomes a false positive and its ALLOW becomes a true negative, quietly corrupting the reported rates. There is no error — the mislabel just changes which column the sample lands in.
Verdict classification
classify(label, verdict) in runner.mjs maps each result to a true positive, false negative, false positive, true negative, expected boundary result, or additional boundary catch. The exact machine strings remain in the source and result files:
const denied = verdict === 'DENY' || verdict === 'CHALLENGE';
if (label.startsWith('ceiling:')) return denied ? 'CEIL-CAUGHT' : 'CEILING';
if (label.startsWith('bot:')) return denied ? 'TP' : 'FN';
return verdict === 'DENY' ? 'FP' : 'TN';
| Sample kind | Verdict | Outcome |
|---|---|---|
bot (label starts bot:) |
CHALLENGE or DENY | True positive (caught) |
| bot | ALLOW | False negative (missed) |
ceiling (label starts ceiling:) |
ALLOW | Expected boundary result, not a miss |
| ceiling | CHALLENGE or DENY | Additional catch beyond the stated boundary |
human (label = human) |
DENY | false positive (wrongly denied) |
| human | ALLOW or CHALLENGE | True negative |
Note: A
ceiling:label is not an expected automated denial. The singlenative_coherent_ceilingprofile represents the coherent-automation detection boundary: ALLOW is the honest expected result under the current reference policy. A CHALLENGE or DENY is an additional catch, not the pass condition.
Note: A human CHALLENGE is scored as a true negative, not a false positive. The
humanFPRfield is therefore a denial-only metric. It under-reports human friction because a challenged human does not count against it. Inspect the baseline challenge rate separately; do not read a clean field as “zero human friction”.
The 65-entry catalog
The catalog is 65 entries = 63 automated behavior profiles + 1 coherent boundary case + 1 synthetic human baseline. Rows are grouped by increasing attacker cost. “Expected enforcement rule” names the predicate the profile is designed to exercise; where no single predicate is pinned, the expected result is score-driven and must be verified from the run.
The catalog is organised as a five-band automation cost ladder defined in test/e2e/tiers.mjs — from direct Hypertext Transfer Protocol automation through real-browser automation and the coherent browser or human-assisted automation boundary. assertCoverage() in runner.mjs fails if this catalog and the machine-readable ladder drift. The raw-protocol checks are a parallel axis that eighteen .mjs catalog profiles invoke through _bin.mjs.
flowchart TD
CAT["65-entry catalog · 63 automated + 1 boundary + 1 human baseline"]
CAT --> BASE["Baseline · 1"]
CAT --> Direct["Direct HTTP automation · 15"]
CAT --> Default["Off-the-shelf browser automation · 12"]
CAT --> Stealth["Stealth-patched browser automation · 17"]
CAT --> Real["Real-browser automation · 19"]
CAT --> Boundary["Coherent boundary case · 1 (ALLOW by design)"]
RAW["Raw-protocol attacks · 18 -attack values (cmd/redteam)"]
Direct -. "for example tls-static" .-> RAW
Default -. "for example flood" .-> RAW
Real -. "for example distributed" .-> RAW
Node counts sum to the catalog total: 1 + 15 + 12 + 17 + 19 + 1 = 65.
Baseline
The single must-not-block sample. A DENY here is a false positive.
| File | label |
needsBrowser |
Tell reproduced | Expected verdict / enforcement rule |
|---|---|---|---|---|
human.mjs |
human |
true | Real browser drive (installed Edge / Playwright Firefox), genuine interaction | ALLOW (no enforcement rule) |
direct HTTP automation · trivial ($0, a script, no browser)
Non-browser HTTP/uTLS clients, header/token tricks, and the cheapest behaviour tells. Blue’s expectation: reliable.
| File | label |
needsBrowser |
Tell reproduced | Expected verdict / enforcement rule |
|---|---|---|---|---|
nonbrowser_ua.mjs |
bot:nonbrowser-ua |
false | Bare HTTP-library UA (x.non_browser_ua) |
DENY / missing browser evidence rule |
http_client.mjs |
bot:http-client |
false | Browser UA but zero client-side (WASM/JS) evidence — HTTP parrot | DENY / browser claim without execution evidence rule |
tls_parrot.mjs |
bot:tls-parrot |
false | uTLS Chrome ClientHello parrot; browser UA, no JS residual | DENY / browser claim without execution evidence rule |
curl_impersonate_chrome.mjs |
bot:curl-impersonate-chrome |
false | Command-line client impersonating a desktop Chrome encrypted connection without browser execution evidence | DENY / browser claim without execution evidence rule |
curl_impersonate_chrome99_android.mjs |
bot:curl-impersonate-chrome99-android |
false | Command-line client impersonating Android Chrome without browser execution evidence | DENY / browser claim without execution evidence rule |
tls_static.mjs |
bot:tls-static |
false | Static parrot: byte-identical ClientHello every connection, no extension permutation (l5.traffic.tls_no_permutation) |
DENY / in-session protocol-fingerprint rotation rule |
sec_chua_absent.mjs |
bot:sec-chua-absent |
false | Chrome UA without Sec-CH-UA (x.uach_present) |
CHALLENGE/DENY (score-driven) |
sec_fetch_absent.mjs |
bot:sec-fetch-absent |
false | Chrome UA without Sec-Fetch-* (sec_fetch_missing) |
CHALLENGE/DENY (score-driven) |
rit_absent.mjs |
bot:rit-absent |
false | API call with no request-integrity token | CHALLENGE / missing or replayed request-integrity token rule |
rit_replay.mjs |
bot:rit-replay |
false | request-integrity token replay — stale counter (l5.rit.stale_replay) |
CHALLENGE / missing or replayed request-integrity token rule |
rit_tamper.mjs |
bot:rit-tamper |
false | request-integrity token body tamper — HMAC over observed body fails (l5.rit.header_tampered) |
DENY / request-body integrity failure rule |
ua_rotate.mjs |
bot:ua-rotate |
false | Mid-session User-Agent rotation (l5.traffic.ua_rotation) |
CHALLENGE/DENY (score-driven; not a pinned enforcement rule) |
xff_spoof.mjs |
bot:xff-spoof |
false | Forged private X-Forwarded-For (l5.header.forwarded_private) |
CHALLENGE/DENY (score-driven) |
behavior_no_interaction.mjs |
bot:no-interaction |
false | Zero interaction over the window | CHALLENGE / no interaction observed rule |
behavior_untrusted.mjs |
bot:untrusted-events |
false | isTrusted=false injected events (l4.event.untrusted) |
CHALLENGE/DENY (score-driven) |
off-the-shelf browser automation · low ($, off-the-shelf tools on defaults)
Naive automation frameworks, single-axis TLS/engine churn, and cheap resource/DoS abuse. Blue’s expectation: reliable.
| File | label |
needsBrowser |
Tell reproduced | Expected verdict / enforcement rule |
|---|---|---|---|---|
selenium.mjs |
bot:selenium |
true | cdc_ artifacts + webdriver planted via addInitScript |
DENY / hard automation artifact rule |
puppeteer.mjs |
bot:puppeteer |
true | Headless token + webdriver | DENY / headless browser plus another automation indicator rule |
playwright_plain.mjs |
bot:playwright |
true | Headless + webdriver | DENY / headless browser plus another automation indicator rule |
undetected.mjs |
bot:undetected-chromedriver |
true | Headless, webdriver stripped | DENY / headless browser plus another automation indicator rule |
direct_cdp.mjs |
bot:direct-cdp |
true | Raw Chrome DevTools Protocol driver (Chrome DevTools Protocol leak) | DENY / browser-control leak plus automation evidence rule |
tls_rotate.mjs |
bot:tls-rotate |
false | Mid-session TLS engine rotation (l5.traffic.engine_rotation) |
DENY / in-session protocol-fingerprint rotation rule |
ja4_churn.mjs |
bot:ja4-churn |
false | 3+ distinct JA4 fingerprints in one session | DENY / in-session protocol-fingerprint rotation rule |
grease_absent_js.mjs |
bot:grease-absent-js |
false | No-GREASE Go TLS + JS (grease_absent + x.ua_vs_ja4) |
CHALLENGE/DENY (score-driven) |
video_scrape.mjs |
bot:video-scrape |
false | Media Range-storm on a heavy resource | DENY / in-session protocol-fingerprint rotation rule |
watermark_strip.mjs |
bot:watermark-strip |
false | Resource leak + metadata strip (forensic trace) | DENY / wm-traced (forensic trace) |
flood.mjs |
bot:flood |
false | Application-layer request flood (l5.abuse.flood) |
DENY / Hypertext Transfer Protocol version 2 denial-of-service protection rule |
rapid_reset.mjs |
bot:h2-rapid-reset |
false | HTTP/2 Rapid Reset DoS, CVE-2023-44487 (l5.h2dos.rapid_reset) |
DENY / Hypertext Transfer Protocol version 2 denial-of-service protection rule |
stealth-patched browser automation · moderate ($$, stealth / rotation / fingerprint-spoof / humanizers)
Stealth-patched automation, multi-axis rotation, fingerprint-spoof contradictions, and mouse/keystroke humanizers. Blue’s expectation: reliable.
| File | label |
needsBrowser |
Tell reproduced | Expected verdict / enforcement rule |
|---|---|---|---|---|
puppeteer_stealth.mjs |
bot:puppeteer-stealth |
true | Patched native toString (client integrity) |
DENY / stealth browser modification rule |
playwright_stealth.mjs |
bot:playwright-stealth |
true | Patched natives (client integrity stage) | DENY / stealth browser modification rule |
patchright.mjs |
bot:patchright |
true | Console API disabled (l3.guard.console_disabled) |
DENY / browser-control leak plus automation evidence rule |
rebrowser_cdp_stripped.mjs |
bot:rebrowser-cdp-stripped |
true | Browser-control runtime evidence stripped while residual behavior remains | CHALLENGE/DENY (verify from trace) |
mobile_ua_desktop_profile.mjs |
bot:mobile-ua-desktop-profile |
true | Mobile user-agent claim paired with desktop pointer and display characteristics | CHALLENGE / advanced browser residual rule |
near_ceiling_audio_24k.mjs |
bot:near-ceiling-audio-24k |
false | Audio sample rate inconsistent with the claimed browser environment | CHALLENGE / advanced browser residual rule |
near_ceiling_no_widevine.mjs |
bot:near-ceiling-no-widevine |
false | Chrome-like container lacks Widevine, media devices, and voices in the combined residual pattern | CHALLENGE / Chromium-container residual rule |
browser_use_cdp.mjs |
bot:browser-use-cdp |
true | Browser agent driven through the browser-control protocol with residual interaction evidence | CHALLENGE/DENY (verify from trace) |
multi_axis_rotate.mjs |
bot:multi-axis-rotate |
false | UA + TLS rotate together | DENY / in-session protocol-fingerprint rotation rule or multi-axis identity rotation rule |
adv_webgpu_mismatch.mjs |
bot:webgpu-mismatch |
false | WebGL vs WebGPU vendor contradiction (l2.adv.webgpu_mismatch) |
CHALLENGE/DENY (score-driven) |
headless_ua_token.mjs |
bot:headless-ua-token |
false | HeadlessChrome UA token + a second tell | CHALLENGE/DENY (score-driven) |
signal_forgery.mjs |
bot:signal-forgery |
false | Forged l7.pass.solved / l7.pow.solved — stripped, no ALLOW (round-3 provenance) |
CHALLENGE |
behavior_machine_keystroke.mjs |
bot:machine-keystroke |
false | Sub-25ms machine typing (l4.key.machine_speed) |
CHALLENGE/DENY (score-driven) |
behavior_teleport_click.mjs |
bot:teleport-click |
false | Clicks with no approach trajectory (l4.mouse.click_no_trajectory) |
CHALLENGE/DENY (score-driven) |
behavior_bezier_mouse.mjs |
bot:bezier-mouse |
false | Pathologically smooth ghost-cursor path (l4.mouse.*) |
CHALLENGE/DENY (score-driven) |
behavior_fixed_typing.mjs |
bot:fixed-typing |
false | Fixed-interval typing, near-zero variance (l4.key.*) |
CHALLENGE/DENY (score-driven) |
ai_burst_silence.mjs |
bot:ai-burst-silence |
false | LLM inference-loop cadence (l4.agent.burst_silence) |
CHALLENGE/DENY (score-driven) |
real-browser automation · high ($$$, real engine + AI + proxy infrastructure)
Real-engine anti-detect (nodriver / camoufox / xvfb / anti-detect), full AI-agent cadence, and residential-proxy-rotation correlation. Blue’s expectation: degrades gracefully — scores and challenges or denies at lower confidence.
| File | label |
needsBrowser |
Tell reproduced | Expected verdict / enforcement rule |
|---|---|---|---|---|
nodriver.mjs |
bot:nodriver |
true | Headful frontier — behaviour only, no display tells | CHALLENGE / no interaction observed rule |
xvfb_headful.mjs |
bot:xvfb-headful |
true | Headful, no display tells — behaviour | CHALLENGE / no interaction observed rule |
antidetect.mjs |
bot:anti-detect-browser |
true | Coherent spoof + behaviour | CHALLENGE / no interaction observed rule |
camoufox.mjs |
bot:camoufox |
true | Firefox fork (Playwright Firefox stand-in) | CHALLENGE / no interaction observed rule |
ai_agent.mjs |
bot:ai-agent |
true | LLM browser-agent cadence — teleport click, no trajectory | DENY / automated-browser interaction signature rule |
ai_full_cadence.mjs |
bot:ai-full-cadence |
false | Teleport + burst-silence + machine keystroke fused | DENY / automated-browser interaction signature rule |
distributed.mjs |
bot:distributed-proxy |
false | Rotating residential-proxy pool — one fingerprint across many subnets (l5.correlation.proxy_rotation) |
DENY / cross-session correlation rule |
privacy_evasion.mjs |
bot:privacy-evasion |
false | Proxy-rotation + forged adBlock/GPC — still cross-session correlation rule (round-5) | DENY / cross-session correlation rule |
squid_forward.mjs |
bot:squid-forward |
false | Explicit forward-proxy hop headers | CHALLENGE / network residual policy |
openvpn_exit.mjs |
bot:openvpn-exit |
false | Virtual-private-network exit combined with a public-address leak | CHALLENGE / network residual policy |
wireguard_hop.mjs |
bot:wireguard-hop |
false | Mid-session source hop with a multi-hop forwarding chain | CHALLENGE / network residual policy |
tor_exit.mjs |
bot:tor-exit |
false | Multi-hop circuit plus rotating exit networks under one fingerprint | DENY / cross-session correlation rule, or CHALLENGE / network residual policy |
anon_proxy_chain.mjs |
bot:anon-proxy-chain |
false | Long anonymous forward-proxy chain | CHALLENGE / network residual policy |
elite_anon_proxy.mjs |
bot:elite-anon-proxy |
false | Multi-hop forwarding chain with common proxy headers removed | CHALLENGE / network residual policy |
cdn_ip_spoof.mjs |
bot:cdn-ip-spoof |
false | Forged content-delivery-network client-address headers | CHALLENGE / network residual policy |
proxy_ua_rotate.mjs |
bot:proxy-ua-rotate |
false | Source hop combined with a user-agent change | DENY / multi-axis identity rotation rule |
fp_churn_proxy.mjs |
bot:fp-churn-proxy |
false | Fingerprint churn follows a rotating proxy pool | DENY / cross-session correlation rule |
stacked_proxy_vpn.mjs |
bot:stacked-proxy-vpn |
false | Forward proxy stacked over a virtual-private-network exit | CHALLENGE / network residual policy |
socks_exit_hop.mjs |
bot:socks-exit-hop |
false | Tunneling exit hop combined with multi-hop forwarding evidence | CHALLENGE / network residual policy |
Coherent browser or human-assisted automation · very high — documented boundary
A fully coherent BotBrowser-class engine spoof (or genuine human behaviour on a real engine): not separable by detection alone. Blue’s honest expectation: NOT solved. The coherent case scores ALLOW; it is priced only by rate limiting and cross-session reputation (the attested / ceiling-guard mechanism), not caught by a signal. This single profile carries the ceiling: label, so its ALLOW is the expected honest result, not a false negative.
| File | label |
needsBrowser |
Tell reproduced | Expected verdict / enforcement rule |
|---|---|---|---|---|
native_coherent_ceiling.mjs |
ceiling:native-coherent |
false | Internally consistent browser and encrypted-connection implementation | ALLOW by design — the documented coherent-automation boundary under the current reference policy |
Note: The expected verdict and enforcement rule in these tables are reference-measured from the bundled catalog run against the local engine; run self-validation to reproduce them for your build. An enforcement rule fires on a specific tell (for example headless browser plus another automation indicator rule on a headless browser that also exposes
webdriver), so it is stable across environments; the exact risk score is not, and a profile whose tell is score-driven rather than rule-driven can land on a different enforcement rule if signal availability changes.
Raw-protocol attacks (cmd/redteam)
The Go cmd/redteam client drives evasions a browser cannot: it controls the TLS ClientHello (via uTLS) and request-integrity tokens precisely, and it spoofs the client-hint / sec-fetch headers a real Chrome sends so the header consistency checks stay quiet — isolating the one bypass under test. It exposes eighteen real -attack values.
Note: These are eighteen values the
-attackswitch incmd/redteam/main.goimplements; the table below documents a representative seven. The full set also includesprivacy-evasion,signal-forgery,nonbrowser-ua,sec-chua-absent,sec-fetch-absent,rit-absent,ja4-churn,multi-axis-rotate,grease-absent-js,coherent-ceiling, andxff-spoof. (HTTP/2 Rapid Reset is exercised by therapid_resetnode profile, not this binary.)
-attack |
What it constructs | Signal targeted | enforcement rule |
|---|---|---|---|
flood |
90 rapid POST /api/collect requests from one TLS fingerprint, each carrying JS-execution evidence (so the flood signal, not browser_no_js, is the clean catch) |
l5.abuse.flood |
Hypertext Transfer Protocol version 2 denial-of-service protection rule |
distributed |
One fixed device fingerprint across four different exit IPs (X-Forwarded-For); each session looks legit in isolation |
l5.correlation.proxy_rotation |
cross-session correlation rule |
tls-static |
Four cookied requests in one session, all with a byte-identical HelloChrome_100 ClientHello (a pre-110 fingerprint that does not permute extension order) |
l5.traffic.tls_no_permutation |
in-session protocol-fingerprint rotation rule |
tls-rotate |
Two cookied requests in one session with different TLS stacks (Chrome then Firefox) | l5.traffic.engine_rotation |
in-session protocol-fingerprint rotation rule |
ua-rotate |
Two cookied requests with different User-Agents, same TLS | l5.traffic.ua_rotation |
— (score-based; not a pinned enforcement rule) |
rit-replay |
One valid signed request advances the counter, then replays the same token/counter (stale) | l5.rit.stale_replay |
missing or replayed request-integrity token rule |
rit-tamper |
Signs one body, then sends a different body with that token — the HMAC over the observed body fails | l5.rit.header_tampered |
request-body integrity failure rule |
The -host flag defaults to 127.0.0.1:8443. That default is advisory, not enforced — the CLI connects to whatever host:port is passed, so keeping it loopback is the operator’s responsibility. (Contrast the Detection Observatory launcher, which is structurally locked to loopback.)
Two entry points for the same attack
Every raw-protocol -attack value also exists as a .mjs profile that shells to the Go binary via _bin.mjs — eighteen profiles in all (for example flood, distributed, tls-static). Running the profile and running the raw -attack exercise the same underlying attack; the profile wrapper is what lets the e2e runner and the Observatory catalog reach it.
Where verdict detail comes from
The full per-stage trace is not in every output. Sources, from least to most detail:
node test/redteam/run-one.mjs <profile> https://127.0.0.1:8443stdout — intentionally minimal:{label, verdict, riskScore, hardRuleFired}. It dropstopContributors.POST /api/collectJSON — the full{verdict, riskScore, hardRuleFired, topContributors}returned to every profile’srun().test/e2e/results.json— the runner’s record per run, including thetopcontributor id list.GET /playground/explain/{id}(Detection Observatory) — the completeScoreTracefor a stored session: per-signal contributions, dedup keep/drop, per-stage cap, and the ordered enforcement-rule evaluation. See Run the Detection Observatory.
Related pages
- enforcement rules, verdicts and signal reference — the Core rules table and verdict bands.
- Red catalog architecture — how profiles, helpers, the runner and the Go client fit together.
- Write a Red profile — authoring guide, including the three registration points.
- Self-validation Red-team run — running the suite and reading the rates.
- Run the Detection Observatory — the live feed and the score trace.
- Red-team rules of engagement — the guardrail these attacks operate under.