Documentation
Validation Library Integration Requirements
This document defines the integration requirements for all rules compilers to ensure consistent security validation across the codebase, and describes the actual, currently-implemented enforcement mechanism.
There is no AdGuard-owned dependency anywhere in this pipeline.
bloqr-validator-core (src/validation/core/) and its CLI,
bloqr-validator-core-cli (src/validation/cli/, binary name
bloqr-validate), are Bloqr-authored Rust crates published to crates.io.
Their only external crates are general-purpose ones (serde, reqwest,
clap, thiserror, etc.) — see that crate's Cargo.toml, which cargo deny check pins to approved licenses/registries. The syntax validation module
deliberately reimplements AdGuard's open-source HostlistCompiler
validation semantics for output compatibility (see
docs/adr/0003-adguard-hostlist-compatibility.md) — that is a
behavioral-compatibility choice, not a code or package dependency.
Distribution model
Every compiler consumes bloqr-validator-core one of two ways, chosen per
language for architectural fit:
- Rust rules compiler (
src/compilers/rust/): a direct Cargo dependency onbloqr-validator-core(workspace path locally, crates.io version in published builds) — same language, no FFI or subprocess needed. - .NET, TypeScript, Python, PowerShell: all four consume the validator
through the
bloqr-validateCLI binary, built frombloqr-validator-core-cliand published as a static, dependency-free binary viacargo install bloqr-validator-core-clior the repo's release binaries. .NET additionally P/Invokes the nativebloqr_validatorcdylib directly (see ".NETP/Invoke" below) for the syntax-validation call in its own compilation pipeline; the native lib is packaged as aruntimes/{rid}/native/NuGet asset onBloqr.Compiler.Core(seesrc/common/dotnet/src/Bloqr.Compiler.Core/Bloqr.Compiler.Core.csproj) so it ships automatically with any consumer that references that package — no manual copy step, no separate install.
Whichever integration path a given language uses, the validator is always either statically linked/vendored as a real dependency (Rust), distributed as a packaged native asset (.NET), or invoked as a standalone binary resolved at runtime (TypeScript/Python/PowerShell) — never a fetched/optional/soft dependency that can silently be absent from a production build.
Enforcement strategy: fail-closed by default
Validation is enforced at the point where each compiler writes its final output, via a common pattern implemented independently — but consistently — in all five languages:
- Missing or failing validator invocation aborts compilation. If the
bloqr-validatebinary can't be found (TS/Python/PowerShell), the native library can't be loaded (.NET), the validator run fails, or its output can't be parsed, that is treated as a validation failure, not a skip — compilation aborts by default. - Any Error/Critical finding aborts compilation. Each language's
ValidationEventArgs/ValidationArgstype has always exposed apassed/Passedproperty (trueiff no Error/Critical findings); it is now actually wired into the abort decision (!passedtriggers abort), rather than being computed and left for an optional handler to notice. - Warnings can be escalated. Each language's existing
fail_on_warnings/FailOnWarnings/failOnWarningsoption (also used for config-validation warnings) additionally escalates rules-validator Warning-severity findings to an abort when set. - A registered event handler can still override. Setting
abort/Aborton the validation event from a customCompilationEventHandlercontinues to work exactly as before, and takes priority over the default fail-closed decision either direction. - Bypass is opt-in, explicit, and logged. Each language exposes exactly
one escape hatch — an
allow_unvalidated_output-style flag, set tofalse/off by default — that reverts to "findings are informational only." Every implementation logs a loud, explicit warning whenever this flag is used. There is no other way to produce compiler output without a validator run being attempted.
This opt-out is no longer needed for browser-mode compiles specifically.
Before #434, bloqr-validator-core's validator only understood DNS-level
syntax and rejected every cosmetic/browser-only rule outright, so a
browser-engine compile (epic #432) had 100% of its cosmetic rules flagged
invalid and required this flag just to complete. Since #434, the validator
is engine-aware (see "Engine-aware validation" below) and validates browser
artifacts against a grammar that actually understands them — fail-closed,
with no opt-out required. The flag itself was not removed: it remains a
real escape hatch for deliberate debugging (e.g. a corrupted/experimental
output a developer wants to inspect without the validator blocking it,
or the "native library/binary unavailable" degraded-mode case in the
Integration points below) — it is simply no longer load-bearing for the
common case of "I compiled a browser-syntax filter list."
Engine-aware validation (#434)
bloqr-validator-core::syntax validates against one of two grammars,
selected by an engine parameter (dns or browser, default dns for
full backward compatibility):
dns— the original grammar (docs/adr/0003-adguard-hostlist-compatibility.md): server-side/DNS-blocker syntax only. Cosmetic rules, extended CSS, scriptlet injection, and browser-only$modifiers ($script,$third-party, …) are rejected, exactly as before #434.browser— client-side/browser-engine syntax: accepts everythingdnsdoes, plus cosmetic rules and browser-only modifiers. Seedocs/adr/0005-browser-syntax-validation-engine.mdfor the grammar's scope and the build-vs-adopt decision behind how it's implemented (hand-rolled, not a third-party crate dependency).
Each language surfaces this the same way it surfaces everything else in this document — consistently, but via its own idiom:
| Language | API | CLI |
|---|---|---|
| Rust | Validator::validate_local_file_with_engine, ValidationEngine |
n/a (library) |
| .NET | IBloqrValidatorService.ValidateLocalFileAsync(path, engine, ct) |
n/a (library) |
| TypeScript | runRulesValidator(..., engine) |
bloqr-validate file --engine <dns|browser> |
| Python/PowerShell | shell out to bloqr-validate, same CLI flag |
bloqr-validate file --engine <dns|browser> |
Every wrapper's dual-engine compile path (epic #432) passes browser when
validating the browser-syntax output artifact and dns (the default) for
the DNS/server-side artifact — never the reverse, and never the opt-out
flag as a substitute for picking the right engine.
| Language | Opt-out flag (default false) |
CLI flag |
|---|---|---|
| Rust | allow_unvalidated_output (CompileOptions) |
--allow-unvalidated-output |
| .NET | AllowUnvalidatedOutput (CompilerOptions) |
--allow-unvalidated-output |
| TypeScript | allowUnvalidatedOutput (ExtendedCompileOptions) |
--allow-unvalidated-output |
| Python | allow_unvalidated_output (compile()/CLI) |
--allow-unvalidated-output |
| PowerShell | -AllowUnvalidatedOutput switch (Invoke-BloqrCompiler) |
-AllowUnvalidatedOutput |
Integration points
Rust (src/compilers/rust/core/src/compiler.rs): compile_rules() — the
function the shipped bloqr-compiler CLI actually calls — runs
validate_output_with_events() against bloqr-validator-core directly after
writing output, before returning success.
.NET (src/compilers/dotnet/src/Bloqr.Compiler.Dotnet/Services/BloqrCompilerService.cs):
ValidateOutputSyntaxAsync calls IBloqrValidatorService.ValidateLocalFileAsync
(P/Invoke into bloqr_validator) on the compiled output and raises
ValidationEventArgs (code RV001) through the same zero-trust event
pipeline documented in docs/event-pipeline.md.
TypeScript (src/compilers/typescript/src/orchestration/compiler.ts):
runRulesValidator() shells out to the bloqr-validate binary (resolved via
findRulesValidateBinary()) and dispatches a ValidationEvent.
Python (src/compilers/python/bloqr_compiler/compiler.py):
_run_rules_validator() shells out to bloqr-validate the same way, via
find_rules_validate_binary().
PowerShell (src/compilers/powershell/BloqrCompiler/Public/Invoke-BloqrCompiler.ps1):
Invoke-RulesValidator shells out to bloqr-validate via
Find-RulesValidateBinary.
Example (TypeScript)
// runCompiler() in src/orchestration/compiler.ts
const result = await hostlistCompiler.compile(config);
// Aborts by default unless options.allowUnvalidatedOutput is set;
// escalates Warning findings to an abort when options.failOnWarnings is set.
await runRulesValidator(
outputPath,
callbacks,
logger,
options.allowUnvalidatedOutput ?? false,
options.failOnWarnings ?? false,
);
The equivalent call sites in the other four languages follow the same shape:
run the validator, fail closed unless explicitly opted out, honor an
existing fail_on_warnings-style escalation, and let a registered handler
override the outcome either direction.
CI enforcement
tools/check-validation-compliance.sh, run by the integration-status job
in .github/workflows/validation-compliance.yml, is the real, exit-code-gated
source of truth (not a document-level checklist). For each language it
checks two things:
- Integration is present — the validator is actually invoked from that
language's compilation pipeline (e.g.
greps forrunRulesValidator,IBloqrValidatorService,_run_rules_validator, the Rustbloqr-validator/bloqr_validatorCargo dependency, orInvoke-RulesValidator). - Enforcement is fail-closed — a regression guard that greps for that
language's exact opt-out symbol (
allowUnvalidatedOutput,AllowUnvalidatedOutput,allow_unvalidated/allow_unvalidated_output) as proof the default path is enforced, not merely wired in. If a future change makes validation informational-only again without reintroducing the explicit opt-out, this check catches it.
The workflow also builds and tests bloqr-validator-core/
bloqr-validator-core-cli directly. A non-zero exit from the script fails
the job — there is no warnings-only mode; every language is expected to pass
both checks on main.
Run it locally the same way CI does:
./tools/check-validation-compliance.sh
Pull request expectations
Any change that touches a compiler's output path should keep the fail-closed default intact:
- The rules-validator is invoked on the compiled output before success is reported
- A missing/failing validator run, or an Error/Critical finding, aborts by default
- The
allow_unvalidated_output-style opt-out (if used) is explicit, off by default, and logged loudly when set -
tools/check-validation-compliance.shpasses locally