Skip to content

Dozor — advisory-driven blocklists

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 ─► NORA
policy.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.

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.

Download a binary from the releases page, or build from source:

Terminal window
git clone https://github.com/getnora-io/dozor
cd dozor && cargo build --release
Terminal window
# 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 holds
dozor inventory --nora-data /var/lib/nora -o inventory.jsonl
# 3. compile the blocklist
dozor 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 is
export NORA_CURATION_MODE=enforce
export 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.

version = 1
[defaults]
severity_threshold = "high" # critical | high | moderate | low | off
malicious = "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 build

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"
}
}

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-derivation block with the digests of the feed snapshot, the inventory, the policy and the body itself. dozor verify re-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.

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.

Dozor blocks what the feed reports; NORA’s own controls hold what nobody has reported yet. They meet on the inventory: in Dozor 0.1 a per-version rule exists only for a version NORA holds on disk.

  • A release refused by min_release_age is never downloaded, so Dozor 0.1 writes no rule for it, and once it is old enough NORA serves it.
  • A release held by the digest quarantine in enforce mode is on disk while it is held, so a Dozor run in that window blocks it once the feed reports it.

Run Dozor with the quarantine in enforce mode and a quarantine_ttl no shorter than the time between Dozor runs plus the time the feed takes to report a malicious release. See the min-release-age filter.

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.