Dozor — advisory-driven blocklists
Overview
Section titled “Overview”NORA keeps no advisory database. That is a deliberate architecture decision (ADR-2): the OSV exports for the ecosystems NORA proxies are roughly 360 MB compressed and rotate daily, and matching versions against them needs an index — an embedded database inside a process that starts in three seconds and idles under 50 MB.
Dozor is the companion that fills the
gap from outside. It compiles the OSV feed together with an inventory of what
your registry actually holds into a blocklist.json in the format NORA already
reads. No NORA changes, no plugin, no network between the two — the file is the
whole interface.
osv.dev snapshot ─┐registry inventory ─┼─► dozor build ─► blocklist.json ─► git PR ─► NORApolicy.toml ─┘Dozor is a compiler, not a service. It has no daemon and never sits on the download path, so it cannot fail a download or hold up a build.
Why it matters more than CVEs
Section titled “Why it matters more than CVEs”The npm OSV feed carries 228,684 advisories, and 96.8% of them are MAL-* —
malicious package reports, not CVEs: typosquats, hijacked maintainers,
poisoned releases. They carry no severity field at all, so a policy expressed
only as a severity threshold would discard almost all of them.
A CI scanner sees a malicious package only after your registry has already cached it and served it to the team. The registry is the one place where refusing it is prevention rather than detection.
Install
Section titled “Install”Download a binary from the releases page, or build from source:
git clone https://github.com/getnora-io/dozorcd dozor && cargo build --release# 1. snapshot the feed (or carry the zip in, for air-gapped sites)curl -o npm.zip https://osv-vulnerabilities.storage.googleapis.com/npm/all.zip
# 2. take an inventory of what NORA holdsdozor inventory --nora-data /var/lib/nora -o inventory.jsonl
# 3. compile the blocklistdozor build \ --feed-npm npm.zip \ --inventory inventory.jsonl \ --policy policy.toml \ --proactive \ -o blocklist.json
# 4. hand it to NORA — this is all the configuration there isexport NORA_CURATION_MODE=enforceexport NORA_CURATION_BLOCKLIST_PATH=/etc/nora/blocklist.json--proactive also blocks every wholly-malicious package in the feed whether or
not you have ever cached it, so the package is refused before its first
download.
Policy
Section titled “Policy”version = 1
[defaults]severity_threshold = "high" # critical | high | moderate | low | offmalicious = "block" # MAL-* reports have no severity — own switch
[[exception]]registry = "npm"name = "lodash"version = "4.17.20"reason = "pinned by a legacy build, tracked in JIRA-42"expires = "2026-12-31" # required — an expired exception fails the buildWhat a developer sees
Section titled “What a developer sees”The reason string Dozor writes travels through NORA untouched and lands in the
403 body:
{ "error": "blocked_by_policy", "context": { "package": "lisa-bubursumsum21-miaww", "version": "0.0.1-security.1", "rule": "blocklist", "reason": "MALICIOUS package (MAL-2025-115501) — blocked by dozor · https://osv.dev/vulnerability/MAL-2025-115501" }}GitOps
Section titled “GitOps”The output is an artifact, not a controller’s opinion:
- Deterministic — identical inputs give byte-identical output, so the daily job opens a pull request only when something actually changed.
- Verifiable — every output carries an
x-dozor-derivationblock with the digests of the feed snapshot, the inventory, the policy and the body itself.dozor verifyre-derives it, so you can prove months later why a build was blocked. - Reviewable — the blocklist is a file in git, and
git log -S <package>answers “when did we start blocking this, and who approved it”.
No operator, no CRD, no reconcile loop: a job that writes a file.
Air-gapped sites
Section titled “Air-gapped sites”dozor build never reaches the network. Copy the feed zip in by whatever means
you already use for artifacts, and the inventory comes from a cold copy of
NORA’s data directory. Nothing else is required.
Status
Section titled “Status”Dozor 0.1 covers npm. Every other ecosystem answers Unknown, and Unknown
never becomes “safe” — those versions are counted and sampled in
x-dozor-unmatched in the output rather than being silently allowed.
PyPI (PEP 440) is next, followed by Maven, Go and RPM/deb version semantics.