js

package
v1.8.0-rc1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 1 Imported by: 0

README

WAF challenge JS pipeline

This directory holds the JavaScript sources that the WAF challenge runtime serves to clients, and the build-time tools that turn them into the artifacts embedded in the crowdsec binary.

End users running make build do not need any of the tools below — the generated artifacts are committed to the repository and pulled in via //go:embed. The pipeline is only exercised when someone intentionally changes the JS sources.

What gets generated

Three pipeline steps, all driven by go:generate directives in generate.go:

Step Tool used Output Used at runtime by
cmd/bundle esbuild (Go module) fpscanner/fpscanner.js and challenge_code.js fpscanner.js is served as-is; challenge_code.js is the obfuscator input
cmd/obfuscate esbuild + javy obfuscate/index.wasm.gz wazero to obfuscate the dynamic key module per epoch
cmd/initialbundle wazero running the obfuscator wasm ../initial_bundle.js.gz Seeded into the challenge cache at startup so the service is ready instantly

The client JS is split into two bundles:

  • fpscanner/fpscanner.js — the public fingerprint scanner (fpscanner/src/*, entered via fpscanner/global.js), built as a minified IIFE that assigns globalThis.CrowdsecFingerprintScanner. It is served unobfuscated at ChallengeFPScannerPath via a plain <script src> tag (cacheable across challenge pages). fpscanner is public open-source code, so there is nothing to hide and no reason to spend CPU obfuscating it.
  • challenge_code.js — the crypto/glue (../challenge.js: SHA-256, HMAC, PoW, fingerprint obfuscation, submission, and the hook registration). This is the obfuscation input: cmd/initialbundle substitutes the internal-path placeholders and obfuscates it into ../initial_bundle.js.gz, which the runtime injects inline on the challenge page. The challenge code reads the scanner from globalThis.CrowdsecFingerprintScanner at use time.

There is no runtime re-obfuscation of the challenge code — the build-time initial_bundle.js.gz is the single static variant served. Only the sensitive per-epoch key module is re-obfuscated at runtime (see ../dynamic_module.go).

obfuscate/obfuscate.js is a small driver around the vendored obfuscate/javascript-obfuscator.js (≈1.6 MB, kept in tree so we don't pull from npm at build time). The driver reads source code from stdin, runs JavaScriptObfuscator.obfuscate(...) with the high-obfuscation preset (plus a reservedStrings entry for the __CSEC_CHALLENGE_HOOK_v1__ sentinel that bridges the static and dynamic bundles), and writes the obfuscated code to stdout. javy then compiles that driver to a WASM module.

How to regenerate

make generate-challenge-js

Or directly:

go generate ./pkg/appsec/challenge/js/...

Both invocations require javy (the Bytecode Alliance JS-to-WASM compiler) to be on PATH. There is no apt/brew package; install via the upstream releases:

curl -sL -o javy.gz \
  https://github.com/bytecodealliance/javy/releases/download/v8.1.1/javy-x86_64-linux-v8.1.1.gz
gunzip javy.gz && chmod +x javy && mv javy /usr/local/bin/

(Substitute the platform suffix for macOS/Windows.) javy is build-time only — it is not a runtime dependency of crowdsec.

Regeneration is fast now: only the small challenge_code.js (~4 KB) goes through the obfuscator's high-obfuscation preset; the bulk of the client JS (fpscanner, ~33 KB) is served raw and is never obfuscated.

javy is only needed by cmd/obfuscate (it rebuilds index.wasm.gz). If you only changed challenge.js or the fpscanner sources, you can skip javy and regenerate just the two bundles:

cd pkg/appsec/challenge/js && go run ./cmd/bundle && go run ./cmd/initialbundle

When to regenerate

Run make generate-challenge-js (or the two-step command above) after changing any of:

  • pkg/appsec/challenge/challenge.js
  • pkg/appsec/challenge/dynamic_module.js.tmpl
  • pkg/appsec/challenge/js/fpscanner/global.js
  • pkg/appsec/challenge/js/fpscanner/src/**/*.ts
  • pkg/appsec/challenge/js/obfuscate/obfuscate.js
  • The vendored pkg/appsec/challenge/js/obfuscate/javascript-obfuscator.js (only when intentionally upgrading)

The pipeline output bytes (fpscanner/fpscanner.js, challenge_code.js, index.wasm.gz, initial_bundle.js.gz) are committed so a clean git clone builds and runs without javy.

Sentinel survival

The __CSEC_CHALLENGE_HOOK_v1__ literal must appear verbatim in both the obfuscated challenge code (initial_bundle.js.gz) and any dynamic key module generated at runtime, so the two bundles meet at the same globalThis key after independent obfuscation passes. The reservedStrings: ["__CSEC_CHALLENGE_HOOK_v1__"] option in obfuscate/obfuscate.js is what guarantees this. If you change the sentinel string, change it in all three places:

  • obfuscate/obfuscate.jsreservedStrings
  • pkg/appsec/challenge/challenge.jsCSEC_HOOK_NAME
  • pkg/appsec/challenge/dynamic_module.js.tmplhookName

The Go-side regression test TestSplitBundle_HookSentinelInBakedBundle will fire on a mismatch.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ChallengeCode string

ChallengeCode is the minified (not-yet-obfuscated) challenge crypto/glue code. It still carries the __CROWDSEC_*_PATH__ placeholders; the runtime substitutes them before obfuscation (see buildChallengeBundle in static_bundle.go).

View Source
var FPScannerJS string

FPScannerJS is the standalone, unobfuscated fpscanner bundle served as-is.

Functions

This section is empty.

Types

This section is empty.

Directories

Path Synopsis
cmd
bundle command
initialbundle command
initialbundle is a build-time tool that produces a pre-obfuscated initial challenge bundle, embedded into the Go binary.
initialbundle is a build-time tool that produces a pre-obfuscated initial challenge bundle, embedded into the Go binary.
obfuscate command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL