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 (in human.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 single native_coherent_ceiling profile 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 humanFPR field 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 -attack switch in cmd/redteam/main.go implements; the table below documents a representative seven. The full set also includes privacy-evasion, signal-forgery, nonbrowser-ua, sec-chua-absent, sec-fetch-absent, rit-absent, ja4-churn, multi-axis-rotate, grease-absent-js, coherent-ceiling, and xff-spoof. (HTTP/2 Rapid Reset is exercised by the rapid_reset node 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.mjseighteen 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:8443 stdout — intentionally minimal: {label, verdict, riskScore, hardRuleFired}. It drops topContributors.
  • POST /api/collect JSON — the full {verdict, riskScore, hardRuleFired, topContributors} returned to every profile’s run().
  • test/e2e/results.json — the runner’s record per run, including the top contributor id list.
  • GET /playground/explain/{id} (Detection Observatory) — the complete ScoreTrace for a stored session: per-signal contributions, dedup keep/drop, per-stage cap, and the ordered enforcement-rule evaluation. See Run the Detection Observatory.