Cribado-CSP

The CSP-violation sieve.

A page ships a Content-Security-Policy; when a browser blocks something the policy forbids, it reports the violation to Cribado-CSP. It stores it, deduplicates it, and tells you what actually changed โ€” so you catch a broken or over-tightened policy before your users do.

๐Ÿค– This page was written by an LLM, on the ideas, instruction, and editing of humans. AI-written text here is always marked, visibly, like this โ€” that's factor X.

1endpoint to point a policy at
2report formats, both accepted
1row per distinct violation, however often it fires
0AI required for the insights

Content-Security-Policy is the browser feature that stops injected scripts, rogue frames and exfiltration by refusing to load anything the page didn't declare. It comes with a built-in feedback channel: the browser will POST a report every time it blocks something. Almost nobody listens to that channel, because the hosted services that do are metered per report and the raw stream is a firehose of noise. Cribado-CSP is the listener you run yourself, shipped as one small self-hosted binary.

The premise in one line: a CSP report stream is a dedup problem, not a logging problem. A policy misfiring a thousand times a minute is one fact with a big counter, not a thousand rows. Store the fact once, count it, and watch the counters for the handful of changes that mean something.

How it works

One endpoint, both formats, and plain SQL where you'd expect magic.

One endpoint, both report formats

Browsers speak two dialects: the legacy report-uri POST and the Reporting-API report-to batch. Cribado-CSP accepts both at the same URL, content-negotiated, so old and new browsers land in the same table.

Dedup-and-count, not a firehose

Each distinct violation โ€” same site, directive, blocked host, disposition โ€” is one row with a counter. Volume becomes a number on a row instead of a runaway table, and the interesting signal is the counter's slope.

Deterministic insights

The warnings โ€” a never-before-seen blocked host, a directive suddenly firing far above its baseline, a real enforce-mode breakage โ€” are plain SQL thresholds you can read and audit. An optional ๐Ÿค– summary layer is planned on top, opt-in; the tool works fully without it.

Pointing a page at it

Two response headers, and the browser does the rest.

Each site gets an opaque ingest key. Add the reporting directive to the page's policy โ€” legacy form, Reporting-API form, or both:

Content-Security-Policy-Report-Only:
    default-src 'self'; report-uri https://<host>/r/<key>

# or, via the Reporting API:
Reporting-Endpoints: cribado-csp="https://<host>/r/<key>"
Content-Security-Policy-Report-Only:
    default-src 'self'; report-to cribado-csp

Start in report-only mode to see what a policy would break, tighten it against real data, then enforce. Sites and keys are managed from the dashboard โ€” create sites, mint and disable keys, copy ingest URLs โ€” or from equivalent ops subcommands for the first key on a fresh box.

The trade-off, stated plainly

Cribado-CSP's operators can read every violation it stores. On purpose.

Cribado is built on CARLOS, whose family rule is server-blindness: the server holds only ciphertext it cannot open. Cribado deviates, deliberately, because the product is analytics over the reports โ€” you cannot cheaply compute "top blocked hosts" over data you cannot read. A CSP report is operational telemetry about your own apps, closer to a server log than to a private message. The family rule for deviations applies instead: enumerated, justified, published, never hidden. No end-to-end encryption is claimed, here or anywhere.

Status

Fewer claims, all of them checkable.

v1, in progress. The ingest endpoint and the analytics dashboard run today; deployment to production is the current work. Cribado-CSP exists to watch its makers' own apps first โ€” it becomes interesting to anyone else only after it has earned its keep there. The source is not yet published; the licence is being decided before first publication, and open source is the family default โ€” not optional, only deferred. When that changes, this page will link to it.