Hooks
The Application Security Component lets you hook into different stages to change behavior at runtime.
Hooks run in six phases:
on_load: Called just after the rules have been loaded into the engine.pre_eval: Called after a request has been received but before the rules are evaluated.post_eval: Called after the rules have been evaluated.on_match: Called after a successful match of a rule. If multiple rules, this hook will be called only once.on_challenge: Called for in-band requests carrying a valid challenge cookie, with the decodedfingerprintobject available. See Bot detection. (In-band only.)on_challenge_submit: Called when a client POSTs a challenge response to/crowdsec-internal/challenge/submit, after crypto validation and fingerprint decryption. See Bot detection. (In-band only.)
Using hooks
Hooks are configured in your AppSec config file.
The on_load hook only supports apply, while other hooks support filter and apply.
Both filter and apply of the same phase have access to the same helpers.
Except for on_load, hooks can be called twice per request: once for in-band processing and once for out-of-band processing. Use IsInBand and IsOutBand to filter the hook.
Hooks have the following format:
on_match:
- filter: IsInBand && 1 == 1
apply:
- valid expression
- valid expression
If the filter returns true, each of the expressions in the apply section are executed.
on_load
This hook is intended to be used to disable rules at loading (eg, to temporarily disable a rule that is causing false positives).
Available helpers
| Helper Name | Type | Description |
|---|---|---|
RemoveInBandRuleByName | func(tag str) | Disable the named in-band rule |
RemoveInBandRuleByTag | func(tag str) | Disable the in-band rule identified by the tag (multiple rules can have the same tag) |
RemoveInBandRuleByID | func(id int) | Disable the in-band rule identified by the ID |
RemoveOutBandRuleByName | func(tag str) | Disable the named out-of-band rule |
RemoveOutBandRuleByTag | func(tag str) | Disable the out-of-band rule identified by the tag (multiple rules can have the same tag) |
RemoveOutBandRuleByID | func(id int) | Disable the out-of-band rule identified by the ID |
SetRemediationByTag | func(tag str, remediation string) | Change the remediation of the in-band rule identified by the tag (multiple rules can have the same tag) |
SetRemediationByID | func(id int, remediation string) | Change the remediation of the in-band rule identified by the ID |
SetRemediationByName | func(name str, remediation string) | Change the remediation of the in-band rule identified by the name |
LoadAPISchemaWithName | func(ref str, filename str) | Load an OpenAPI schema from <data_dir>/schemas/<filename> and register it under ref. See OpenAPI Schema Validation. |
LoadAPISchemaWithOptions | func(ref str, filename str, opts map) | Same as LoadAPISchemaWithName but accepts per-schema policy overrides (on_route_not_found, on_method_not_allowed). |
RegisterAPISchemaBodyDecoder | func(content_type str, decoder str) | Enable a non-default body decoder for a Content-Type. See available decoders. |
SetMaxBodySize | func(size int) | Set the maximum request body size (in bytes) buffered and inspected by the engine. Defaults to 10MB. See Request body size handling |
SetBodySizeExceededAction | func(action str) | Set what happens when a request body exceeds the maximum size: drop (default), partial, or allow. See Request body size handling |
SetChallengeDifficulty | func(level str) | Set the default proof-of-work difficulty for every challenge served by this config. Valid levels: "disabled", "low", "medium" (default), "high", "impossible". See Challenge difficulty levels. Per-request overrides are available in pre_eval / post_eval / on_challenge. |
Example
name: crowdsecurity/my-appsec-config
default_remediation: ban
inband_rules:
- crowdsecurity/base-config
- crowdsecurity/vpatch-*
on_load:
- apply:
- RemoveInBandRuleByName("my_rule")
- SetRemediationByTag("my_tag", "captcha")
pre_eval
This hook is intended to be used to disable rules only for this particular request (eg, to disable a rule for a specific IP).
Available helpers
| Helper Name | Type | Description |
|---|---|---|
RemoveInBandRuleByName | func(tag str) | Disable the named in-band rule |
RemoveInBandRuleByTag | func(tag str) | Disable the in-band rule identified by the tag (multiple rules can have the same tag) |
RemoveInBandRuleByID | func(id int) | Disable the in-band rule identified by the ID |
RemoveOutBandRuleByName | func(tag str) | Disable the named out-of-band rule |
RemoveOutBandRuleByTag | func(tag str) | Disable the out-of-band rule identified by the tag (multiple rules can have the same tag) |
RemoveOutBandRuleByID | func(id int) | Disable the out-of-band rule identified by the ID |
IsInBand | bool | true if the request is in the in-band processing phase |
IsOutBand | bool | true if the request is in the out-of-band processing phase |
SetRemediationByTag | func(tag str, remediation string) | Change the remediation of the in-band rule identified by the tag (multiple rules can have the same tag) |
SetRemediationByID | func(id int, remediation string) | Change the remediation of the in-band rule identified by the ID |
SetRemediationByName | func(name str, remediation string) | Change the remediation of the in-band rule identified by the name |
req | http.Request | Original HTTP request received by the remediation component |
DropRequest | func(reason str) | Stop processing the request immediately and instruct the remediation component to block the request |
DisableBodyInspection | func() | Skip body inspection for the current request (also bypasses the maximum body size check). See Request body size handling |
ValidateRequestWithSchema | func(ref str) bool | Validate the current request against an OpenAPI schema previously loaded under ref (returns true on success). On failure, structured details are published to hook_vars (see OpenAPI Schema Validation). |
hook_vars | map[string]string | Per-request scratch space shared with later hooks and propagated to the resulting event. Helpers such as ValidateRequestWithSchema publish their results here. |
GrantChallengeCookie | func(reason str, ttl str?) | Mint a valid challenge cookie for this client (allowlist escape hatch for trusted user-agents or internal probes). reason is recorded in logs (≤256 bytes); optional ttl (a Go duration like "24h") overrides the configured cookie_ttl. |
SetChallengeDifficulty | func(level str) | Override the proof-of-work difficulty for this request. Valid levels: "disabled", "low", "medium" (default), "high", "impossible". See Challenge difficulty levels. |
MatchKnownBot | func(ip str, ua str, path str, ...files str) bool | true if the request matches a bot definition in one of the named files (verified by IP range or forward-confirmed reverse DNS). Used to skip the challenge for known-good crawlers. See Known bots. |
ExemptFromChallenge | func(reason str) | Exempt the current request from the challenge without minting a cookie (this request only). reason labels the exemption in logs and metrics. See Known bots. |
Example
name: crowdsecurity/my-appsec-config
default_remediation: ban
inband_rules:
- crowdsecurity/base-config
- crowdsecurity/vpatch-*
pre_eval:
- filter: IsInBand == true && req.RemoteAddr == "192.168.1.1"
apply:
- RemoveInBandRuleByName("my_rule")
post_eval
This hook is mostly intended for debugging or threat-hunting purposes.
Available helpers
| Helper Name | Type | Description |
|---|---|---|
IsInBand | bool | true if the request is in the in-band processing phase |
IsOutBand | bool | true if the request is in the out-of-band processing phase |
DumpRequest | func() | Dump the request to a file |
DumpFingerprint | func(label str) str | Append the decoded challenge fingerprint (plus request context) as one JSONL line to a dump file, for offline analysis. Returns the file path. See DumpFingerprint. |
req | http.Request | Original HTTP request received by the remediation component |
SendChallenge | func() | Instruct the AppSec component to serve a JavaScript challenge for this request. No-op if the request already carries a valid challenge cookie. See Bot detection. |
GrantChallengeCookie | func(reason str, ttl str?) | Mint a valid challenge cookie for this client (allowlist escape hatch for trusted user-agents or internal probes). reason is recorded in logs (≤256 bytes); optional ttl (a Go duration like "24h") overrides the configured cookie_ttl. |
SetChallengeDifficulty | func(level str) | Override the proof-of-work difficulty for this request. Valid levels: "disabled", "low", "medium" (default), "high", "impossible". See Challenge difficulty levels. |
MatchKnownBot | func(ip str, ua str, path str, ...files str) bool | true if the request matches a bot definition in one of the named files (verified by IP range or forward-confirmed reverse DNS). See Known bots. |
ExemptFromChallenge | func(reason str) | Exempt the current request from the challenge without minting a cookie (this request only). reason labels the exemption in logs and metrics. See Known bots. |
DumpRequest
In order to make DumpRequest write your request to a file, you have to call DumpRequest().ToJSON(), which will create a file in the OS temporary directory (eg, /tmp on Linux) with the following format: crowdsec_req_dump_<RANDOM_PART>.json.
You can configure what is dumped with the following options:
DumpRequest().NoFilters(): Clear any previous filters (ie. dump everything)DumpRequest().WithEmptyHeadersFilters(): Clear the headers filters, ie. dump all the headersDumpRequest().WithHeadersContentFilter(regexp string): Add a filter on the content of the headers, ie. dump only the headers that do not match the provided regular expressionDumpRequest().WithHeadersNameFilter(regexp string): Add a filter on the name of the headers, ie. dump only the headers that do not match the provided regular expressionDumpRequest().WithNoHeaders(): Do not dump the request headersDumpRequest().WithHeaders(): Dump all the request headers (override any previous filter)DumpRequest().WithBody(): Dump the request bodyDumpRequest().WithNoBody(): Do not dump the request bodyDumpRequest().WithEmptyArgsFilters(): Clear the query parameters filters, ie. dump all the query parametersDumpRequest().WithArgsContentFilter(regexp string): Add a filter on the content of the query parameters, ie. dump only the query parameters that do not match the provided regular expressionDumpRequest().WithArgsNameFilter(regexp string): Add a filter on the name of the query parameters, ie. dump only the query parameters that do not match the provided regular expression
By default, everything is dumped. All regexps are case-insensitive.
You can chain the options, for example:
DumpRequest().WithNoBody().WithArgsNameFilter("var1").WithArgsNameFilter("var2").ToJSON()
This will discard the body of the request, remove the query parameters var1 and var2 from the dump, and dump everything else.
Example
name: crowdsecurity/my-appsec-config
default_remediation: ban
inband_rules:
- crowdsecurity/base-config
- crowdsecurity/vpatch-*
post_eval:
- filter: IsInBand == true
apply:
- DumpRequest().NoFilters().WithBody().ToJSON()
on_match
This hook is intended to be used to change the behavior of the engine after a match (eg, to change the remediation that will be used dynamically).
Available helpers
| Helper Name | Type | Description |
|---|---|---|
SetRemediation | func(remediation string) | Change the remediation that will be returned to the remediation component |
SetReturnCode | func(code int) | Change the HTTP code that will be returned to the remediation component |
CancelAlert | func() | Prevent the Application Security Component to create a crowdsec alert |
SendAlert | func() | Force the Application Security Component to create a crowdsec alert |
CancelEvent | func() | Prevent the Application Security Component to create a crowdsec event |
SendEvent | func() | Force the Application Security Component to create a crowdsec event |
DumpRequest | func() | Dump the request to a file (see previous section for detailed usage) |
IsInBand | bool | true if the request is in the in-band processing phase |
IsOutBand | bool | true if the request is in the out-of-band processing phase |
evt | types.Event | The event that has been generated by the Application Security Component |
req | http.Request | Original HTTP request received by the remediation component |
MatchKnownBot | func(ip str, ua str, path str, ...files str) bool | true if the request matches a bot definition in one of the named files. See Known bots. |
ExemptFromChallenge | func(reason str) | Exempt the current request from the challenge without minting a cookie (this request only). reason labels the exemption in logs and metrics. See Known bots. |
Example
name: crowdsecurity/my-appsec-config
default_remediation: ban
inband_rules:
- crowdsecurity/base-config
- crowdsecurity/vpatch-*
on_match:
- filter: IsInBand == true && req.RemoteAddr == "192.168.1.1"
apply:
- CancelAlert()
- CancelEvent()
- filter: |
any( evt.Appsec.MatchedRules, #.name == "crowdsecurity/vpatch-env-access") and
req.RemoteAddr = "192.168.1.1"
apply:
- SetRemediation("allow")
- filter: evt.Appsec.MatchedRules.GetURI() contains "/foobar/"
apply:
- SetRemediation("allow")
on_challenge
This hook fires for in-band requests that carry a valid __crowdsec_challenge cookie — i.e. clients that have already passed the JavaScript challenge once. The decoded device fingerprint is available, so this is the right place to apply per-request decisions based on what the challenge learned about the client. Skipped if the request has no valid challenge cookie. In-band only.
See Bot detection for the broader picture.
Available helpers
| Helper Name | Type | Description |
|---|---|---|
SendChallenge | func() | Force a re-challenge for this request even though the client already has a cookie (e.g. when fingerprint mismatches indicate the cookie may have been replayed). |
SetChallengeDifficulty | func(level str) | Override the proof-of-work difficulty for the next challenge issued. See Challenge difficulty levels. |
SetRemediation | func(action str) | Set the remediation returned to the bouncer for this request. The only special value is allow (don't block); any other value is passed through as-is. See SetRemediation*. |
SetReturnCode | func(code int) | Set the HTTP status code returned to the bouncer for this request. |
DropRequest | func(reason str) | Block this request immediately (using the config's default remediation) based on what the fingerprint revealed. reason is recorded in logs. |
req | http.Request | Original HTTP request received by the remediation component. See req object. |
IsInBand | bool | true if the request is in the in-band processing phase (always true here — on_challenge is in-band only). |
EvaluateMismatches | func() MismatchReport | Run the configured mismatch checks against the fingerprint and return a structured report. Result is cached per request. See The MismatchReport object. |
fingerprint | FingerprintData | The decoded fingerprint object. See The fingerprint object. |
fingerprint.UAMobileMismatch | func() bool | true if the mobile signals carried by the fingerprint contradict the User-Agent header. |
fingerprint.AcceptLanguageMismatch | func(req http.Request) bool | true if the Accept-Language header is inconsistent with the languages reported by the fingerprint. |
fingerprint.TimezoneCountryMismatch | func(country str) bool | true if the timezone reported by the fingerprint is inconsistent with the given country code (typically obtained from a GeoIP lookup on the client IP). |
Example
on_challenge:
- filter: EvaluateMismatches().High() >= 1
apply:
- SendChallenge()
on_challenge_submit
This hook fires when a client POSTs a challenge response to /crowdsec-internal/challenge/submit, after the AppSec component has cryptographically validated the submission and decrypted the fingerprint, but before the success cookie is issued. This is the right place to refuse cookies to clients the challenge has positively identified as automation. In-band only.
Available helpers
| Helper Name | Type | Description |
|---|---|---|
RejectSubmission | func(reason str, verbosity str?) | Refuse to issue a challenge cookie despite a valid crypto submission. reason is recorded in logs. Optional verbosity: "minimal", "info" (default), "verbose" — controls how much fingerprint detail is logged. |
GrantChallengeCookie | func(reason str, ttl str?) | Issue the challenge cookie inline as part of the submit response (no 307 redirect). reason is recorded in logs; optional ttl (a Go duration like "24h") overrides the configured cookie_ttl. |
LogAccepted | func(msg str, verbosity str?) | Emit a structured "submission accepted" log line. Same verbosity semantics as RejectSubmission. |
EvaluateMismatches | func() MismatchReport | Same as in on_challenge — run the mismatch checks against the just-decrypted fingerprint. |
fingerprint | FingerprintData | The decoded fingerprint object — see The fingerprint object. |
req | http.Request | Original HTTP request received by the remediation component. Needed by fingerprint helpers that compare against request headers, e.g. fingerprint.AcceptLanguageMismatch(req). |
DumpFingerprint | func(label str) str | Append the just-decrypted fingerprint (plus request context) as one JSONL line to a dump file, for offline analysis. Returns the file path. See DumpFingerprint. |
Example
on_challenge_submit:
- filter: fingerprint.IsBot()
apply:
- RejectSubmission("fast-bot-detection")
- apply:
- LogAccepted("challenge submission accepted")
Detailed Helpers Information
SetRemediation*
When using SetRemediation* helpers, the only special value is allow: the remediation component won't block the request.
Any other values (including ban and captcha) are transmitted as-is to the remediation component.
DumpFingerprint
DumpFingerprint(label) is the fingerprint counterpart of DumpRequest: a threat-hunting aid that writes the decoded challenge fingerprint to disk so you can inspect it offline. It is available in the post_eval and on_challenge_submit hooks (the phases where a fingerprint is present).
Each call appends one JSON object per line (JSONL) — the fingerprint plus request context (client IP, remote address, User-Agent, host, URI, method, and a UTC timestamp) — to:
<datadir>/fingerprint_dumps/crowdsec_fp_dump_<label>.jsonl
The label names the file (so you can separate dumps by purpose, e.g. "suspected-automation"), and the call returns the path it wrote to. No configuration is required — the directory is created automatically. The call is a no-op (it logs a warning and returns an empty string) if no fingerprint is attached to the request or the dump directory cannot be created.
on_challenge_submit:
- filter: fingerprint.IsBot()
apply:
- DumpFingerprint("fast-bot-detection")
Known bots
Two helpers, available in pre_eval, post_eval and on_match, let you keep legitimate non-browser clients out of the challenge flow.
MatchKnownBot
MatchKnownBot(ip, ua, path, ...files) returns true when the request matches a bot definition in one of the named files. You pass the bot files to consult explicitly (e.g. "legit_bots/gptbot.json"); the helper only queries those, and matches if any of them matches. Matching a User-Agent alone is never enough: the source IP must also match the vendor's published ranges or pass a forward-confirmed reverse-DNS check (FCrDNS). The helper is fail-closed — an unparseable address, a DNS failure, or an unknown file returns false, so the request falls through to the normal challenge.
The bot definitions are loaded from <datadir>/legit_bots/*.json. The hub ships and updates them via the crowdsecurity/appsec-bot-challenge-exclude-* appsec-configs (search-engines, ai-crawlers, social, monitoring), which both call MatchKnownBot and declare the files they need in their data: section; you can add your own — see Authoring your own known-bot files for the file format. The shipped exclude-configs use it in pre_eval to exempt verified bots before the challenge is served:
pre_eval:
- filter: MatchKnownBot(req.RemoteAddr, req.UserAgent(), req.URL.Path, "legit_bots/gptbot.json")
apply:
- ExemptFromChallenge("gptbot")
Once ExemptFromChallenge(reason) has flagged a request, SendChallenge() becomes a no-op for the rest of that request, so the exempted client is never challenged.
ExemptFromChallenge vs GrantChallengeCookie
Both keep a client out of the challenge, but at different scopes:
| Helper | Scope | Cookie | Use for |
|---|---|---|---|
ExemptFromChallenge(reason) | The current request only | no | Verified known bots, well-known paths (robots.txt, /.well-known/*, feeds, webhooks) and per-request allowlisting where no state should persist. reason labels the exemption in logs and the cs_appsec_challenge_exempt_total metric. |
GrantChallengeCookie(reason, ttl?) | Persists across requests (until the cookie expires) | yes | Trusted user-agents or internal probes you want to let through for a whole session. |
Request body size handling
Before the request body is handed over to the rules engine, the Application Security Component reads it into memory itself. To protect the engine from oversized requests, the body is bounded by a maximum size (defaults to 10MB).
This limit is independent from the Coraza-level request_body_in_memory_limit option: it controls how much of the body CrowdSec buffers in the first place, before any rule is evaluated.
You can tune this behavior from an on_load hook:
-
SetMaxBodySize(size)sets the maximum body size, in bytes. The value must be a positive integer. -
SetBodySizeExceededAction(action)controls what happens when a body exceeds the maximum size:Action Behavior drop(default)The request is blocked using the default remediation, without inspecting the body. partialThe body is truncated to the maximum size and the kept portion is inspected. Content beyond the truncation point is discarded and will not match any rule. allowThe body is not inspected and the request is allowed to proceed (other zones are still evaluated).
name: crowdsecurity/my-appsec-config
default_remediation: ban
inband_rules:
- crowdsecurity/base-config
on_load:
- apply:
- SetMaxBodySize(20971520) # 20MB
- SetBodySizeExceededAction("partial")
DisableBodyInspection
DisableBodyInspection() can be called from a pre_eval hook to skip body inspection for the current request only. When body inspection is disabled:
- the request body is not read or processed, so body-based zones (
BODY_ARGS,RAW_BODY, …) won't match; - the maximum body size check is bypassed as well: a request that would otherwise be dropped for exceeding the limit is allowed through, because the operator has explicitly accepted that this body won't be processed.
pre_eval:
- filter: req.URL.Path startsWith "/upload"
apply:
- DisableBodyInspection()
req object
The pre_eval, on_match and post_eval hooks have access to a req variable that represents the HTTP request that was forwarded to the appsec.
It's a Go http.Request object, so you can directly access all the details about the request.
For example:
- To get the requested URI:
req.URL.Path - To get the client IP:
req.RemoteAddr - To get the HTTP method:
req.Method - To get the FQDN:
req.Host
Challenge difficulty levels
SetChallengeDifficulty(level) accepts the following levels. Numbers are approximate proof-of-work iteration counts and rough wall-clock solve times on a modern desktop browser; mobile is meaningfully slower.
| Level | Approx. iterations | Approx. solve time | When to use |
|---|---|---|---|
"disabled" | 0 (any nonce wins) | instant | Functional smoke testing or when you only care about the fingerprint, not the proof-of-work. |
"low" | ~1 024 | 0.2 – 2 s | Latency-sensitive endpoints, mobile-heavy traffic. |
"medium" | ~4 096 | 1 – 8 s | Default. Reasonable trade-off between user experience and attacker cost. |
"high" | ~32 768 | 7 – 60 s | Routes under active abuse; clients you already suspect. |
"impossible" | unsolvable | n/a | Hard block: the AppSec component rejects the submission server-side. Use to fully block a client without leaking the reason. |
The fingerprint object
In on_challenge and on_challenge_submit hooks, fingerprint exposes the device data collected by the in-browser library. It has two layers: a set of high-level helpers for the common decisions, and the raw fields underneath them when you need to branch on one specific signal.
Verdict and signal helpers (recommended)
These methods roll the raw signals up into the decisions rules usually need:
| Helper | Returns | Description |
|---|---|---|
fingerprint.IsBot() | bool | The recommended verdict: true if the in-browser fast-bot-detection library flagged the client. |
fingerprint.HasBotSignal() | bool | true if any fast-bot-detection signal fired. |
fingerprint.BotSignalCount() | int | How many distinct signals fired. |
fingerprint.BotSignals() | []str | The names of the signals that fired. |
fingerprint.HasAutomationSignal() | bool | A webdriver / Selenium / CDP / Playwright / bot-UA indicator was seen. |
fingerprint.HasHeadlessSignal() | bool | Headless-browser indicators (missing Chrome object, Swiftshader renderer, ...). |
fingerprint.HasMismatchSignal() | bool | Cross-context / cross-API inconsistencies (iframe/worker, platform, WebGL, languages). |
fingerprint.HasImpossibleDeviceSignal() | bool | Device specs outside plausible bounds (impossible memory / CPU count). |
The three atomic mismatch checks (fingerprint.UAMobileMismatch(), fingerprint.AcceptLanguageMismatch(req), fingerprint.TimezoneCountryMismatch(country)) are documented in the on_challenge table above; EvaluateMismatches() aggregates all mismatch signals into one report.
Raw fingerprint fields
Reach for these when a helper isn't specific enough (to reject on a specific signal):
| Field | Type | Description |
|---|---|---|
fingerprint.FastBotDetection.Bool() | bool | The raw library verdict IsBot() wraps. |
fingerprint.Bot.<Signal> | bool | Each individual fast-bot signal as a bool, e.g. fingerprint.Bot.CDP, fingerprint.Bot.Webdriver, fingerprint.Bot.MismatchWebGLInWorker. The Has*Signal() helpers above are roll-ups over these. |
fingerprint.Signals.<category>.<field> | mixed | The full collected fingerprint tree, grouped by category (Automation, Device, Browser, Graphics, Codecs, Locale, Contexts), e.g. fingerprint.Signals.Automation.Webdriver. |
fingerprint.Allowlisted | bool | true if the cookie was minted via GrantChallengeCookie(...) rather than a real challenge submission. |
fingerprint.AllowlistReason | str | Operator-supplied reason from GrantChallengeCookie(reason, ...), copied through to logs. |
fingerprint.FSID | str | Per-fingerprint identifier, stable across the cookie's lifetime. Useful for correlating logs. |
The individual fingerprint.Bot.* and fingerprint.Signals.* field names come from the open-source fpscanner library and evolve as it adds detections and browsers change. Treat the examples above as the current shape, not a stable contract. For the always-current list, see the exported Go type FingerprintData.
For the higher-level bot detection workflow (what the library actually detects, how to allowlist legitimate bots, behavioral scenarios), see Bot detection.
The MismatchReport object
EvaluateMismatches() returns a cached-per-request MismatchReport summarising every mismatch signal that fired against the current fingerprint.
| Method | Returns | Description |
|---|---|---|
.Count() | int | Total number of signals fired. |
.Empty() | bool | true if no signal fired. |
.High() / .Medium() / .Low() | int | Count of fired signals by severity. |
.BySeverity(sev str) | int | Count of fired signals at the given severity ("high", "medium", "low") — the generic form of .High() / .Medium() / .Low(). |
.Has(reason str) | bool | true if the specific signal reason fired. |
.Reasons() | []string | Stable-ordered list of fired reason keys. |
.String() | str | Compact human-readable form: "reason1(sev),reason2(sev)". Useful in logs. |
The reason keys accepted by .Has(reason) and returned by .Reasons() are, at the time of writing: cdp, webdriver, webdriver_writable, selenium, playwright, webdriver_iframe, webdriver_worker, headless_screen_resolution, missing_chrome_object, impossible_memory, high_cpu_count, mismatch_webgl_worker, mismatch_platform_iframe, mismatch_platform_worker, platform_mismatch, gpu_mismatch, bot_user_agent, inconsistent_etsl, ua_mobile, utc_timezone, accept_language, swiftshader_renderer, mismatch_languages, timezone_country.
These reasons derive from the fpscanner signals and may change as it and browsers evolve — treat the list above as the current shape, not a stable contract. The always-current source of truth is the exported Go API: KnownReasons() returns the full set the aggregator may emit, and each key has a matching Reason* constant (with SeverityFor(reason) giving its severity).
Example:
on_challenge_submit:
- filter: EvaluateMismatches().High() >= 1 && EvaluateMismatches().Has("cdp")
apply:
- RejectSubmission("high-severity-mismatch")