Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

docs/v2/05-intake-chain.md

5. The intake chain

Every package walks the same chain before it is served. The chain has four repositories: incoming → quarantine → approved → production. Only what passed is served.

States

A package's states, in order (server/lib/src/promote/intake.rs, ArtifactState): UNTRUSTED, INGESTED, SCANNED, POLICY_CHECKED, APPROVED, SEALED, PROMOTED, AVAILABLE.

The console's Security page and holger-cli intake list --queue <q> show the queues (intake.rs, the queue table; an unknown name is refused and the known ones listed):

queue states
incoming UNTRUSTED, INGESTED, SCANNED
quarantined QUARANTINED
awaiting-approval POLICY_CHECKED
blocked BLOCKED
available APPROVED, SEALED, PROMOTED, AVAILABLE

Duties and approvals

Who may move a package is a duty (server/lib/src/promote/duties.rs):

duty may
Developer read AVAILABLE packages only
Importer ingest; move to SCANNED and POLICY_CHECKED
SecurityApprover approve, quarantine, release, block; seal onward
RepositoryAdmin configure; seal onward; import a bundle; grant a first import. Never approves
Auditor read; changes nothing

Walk a package by hand

Upload as an Importer, then move it one state at a time:

curl -fsS -X PUT -H "Authorization: Bearer $IMPORTER_TOKEN" \
  --data-binary @demo-1.0.txt "$HOLGER/incoming/demo-1.0.txt"
holger-cli --at "$HOLGER_GRPC" --token-file importer.token intake advance --name demo-1.0.txt --reason scan
holger-cli --at "$HOLGER_GRPC" --token-file importer.token intake advance --name demo-1.0.txt --reason policy
holger-cli --at "$HOLGER_GRPC" --token-file approver1.token intake advance --name demo-1.0.txt --reason approved
holger-cli --at "$HOLGER_GRPC" --token-file approver2.token intake advance --name demo-1.0.txt --reason approved
for s in sealed promoted available; do
  holger-cli --at "$HOLGER_GRPC" --token-file admin.token intake advance --name demo-1.0.txt --reason "$s"
done
holger-cli --at "$HOLGER_GRPC" --token-file auditor.token intake state --name demo-1.0.txt

The same over HTTP: POST /-/intake/advance?name=<file>&verb=advance&reason=<why> and GET /-/intake/state?name=<file>; the evidence at GET /-/intake/evidence?sha256=<hex> (server/lib/src/exposed/http.rs). Over gRPC: PromotionService.AdvanceArtifact, GetArtifactState and ListIntakeArtifacts. The README guide walks a wheel exactly so (chapter 3).

Every package is scanned in its ecosystem

The vulnerability scan matches a package against the advisories of its own ecosystem.

The proof is server/lib/tests/an_upload_is_scanned_as_its_format.rs: the real log4j-core 2.14.1 POM uploaded under the Maven view is refused for Log4Shell (a_maven_upload_is_scanned_as_maven_and_log4shell_refuses_it), and a .pom in the raw root is held (a_package_file_in_the_raw_root_is_held_not_passed_as_raw).

No intel, no pass

The vulnerability scan reads only the local advisory database (chapter 4). Without one, or with one that is too old, it holds every package. It never passes one with "0 findings" (intel/src/resolve.rs, fold_staleness):

state of the database finding what the scan says effect
none installed HOLGER-INTEL-MISSING no vulnerability data (HOLGER-INTEL-MISSING) holds
older than block_after_days (30) HOLGER-INTEL-EXPIRED vulnerability data N d old (HOLGER-INTEL-EXPIRED) holds
older than warn_after_days (7) HOLGER-INTEL-STALE vulnerability data N d old (HOLGER-INTEL-STALE) warns

The age counts from the bundle's snapshot date, not from the import. These words are not counted as advisory findings in the scan's sentence (server/lib/src/intel.rs). Tests: with_no_intel_imported_the_cve_scan_holds_and_says_why and with_intel_past_its_block_limit_the_cve_scan_holds_and_says_why in an_upload_is_scanned_as_its_format.rs.

Fill or refresh the database with a signed intel bundle:

holger intel build --feeds-dir feeds --policy intel-trust.ron \
  --release-cert releaser.der --release-key releaser.pk8 \
  --approve "security-reviewer=reviewer.der,reviewer.pk8" --out intel.holger.znippy
# the first intel bundle on a box needs an admin's grant, once
holger bundle allow-first-import --config holger.ron --from-bundle intel.holger.znippy --policy intel-trust.ron
holger intel import intel.holger.znippy --config holger.ron

Over HTTP the bundle goes to POST /-/intel/import (RepositoryAdmin or SecurityApprover); the first-import grant is POST /-/bundle/first-import (RepositoryAdmin). Without the grant the import is refused, holger-import-9 (server/lib/src/import_door.rs; test an_intel_bundle_posted_over_http_is_imported_and_the_scan_reads_it in server/lib/tests/the_box_imports_bundles_without_a_shell.rs).

The scan worker must be this server's build

Every scan runs in holger-scan-worker, in a container (chapter 8). The worker must be the same build as the server:

After every upgrade, rebuild the worker image on each host: cargo xtask scan-worker image (podman), or the README's recipe with the new version as the tag. A RON that names an old tag in sandbox: (image: …) must name the new one.

A scan that could not run is retried

When the scanners could not judge a pulled package (the sandbox refused, the worker is not this build, clamd did not answer), it is recorded as scan-failed, not refused:

(server/lib/src/pull_intake/mod.rs, ScanFailed; test a_pull_the_scanners_could_not_judge_is_scanned_again_once_they_can in server/lib/tests/pull_through_runs_the_intake_chain.rs.)

The pull-through

A client asks Holger; Holger pulls the package from the source named in pull_through: (chapter 4), and the package walks the chain like an upload (server/lib/src/pull_intake/door.rs):

moment the client gets
the pull is running 503 with Retry-After (default 2 s)
it waits for approval 404 naming its state ("… stands at POLICY_CHECKED in the intake chain, waiting for approval")
it was refused 403 with the reason (pip: 404)
it is AVAILABLE 200

Nothing is approved automatically by default, so run the client again after the approvals. A client that resolves dependencies one level at a time (pip, Maven) needs one run per level.

Pinned versions through the pull-through

A pinned version that is not the newest pulls through. The index names the versions that are not AVAILABLE in the way each client skips unless it is pinned exactly:

client how a version that is not AVAILABLE is listed proof
pip yanked, with the reason (PEP 592 / PEP 691 yanked, PEP 503 data-yanked) a_pinned_older_release_is_listed_yanked_and_pulled_when_its_link_is_asked (pull_through_runs_the_intake_chain.rs)
npm deprecated, with the reason; latest and every dist-tag name AVAILABLE versions only a_pinned_npm_version_that_is_not_latest_is_listed_deprecated_and_pulled_when_asked (pull_through_npm_maven_crates.rs)
cargo "yanked": true in the sparse index, so a Cargo.lock pin pulls exactly that .crate a_locked_crate_version_that_is_not_newest_is_listed_yanked_and_pulled_when_its_crate_is_asked (pull_through_npm_maven_crates.rs)

The reason a client prints is "not in Holger yet: … pulls it into Holger's intake chain, where it waits for approval", or "held by Holger's intake chain (STATE), waiting for approval" (server/lib/src/pull_intake/mod.rs, ecosystems.rs). Blocked and quarantined files are never listed.

Maven

Go

@v/list and @latest name only AVAILABLE versions. Holger serves no checksum database and does not proxy sum.golang.org (server/lib/src/pull_intake/go.rs), so the client turns the checksum database off or uses one the site runs (chapter 3).

A pull is approved as one dependency set (3.1.1)

When a client asks for a package, Holger pulls its whole dependency closure at once: for Maven, every POM Maven reads (parents, imported BOMs, every version the graph visits) and the JARs of the versions that win mediation; for npm, cargo and Python, the dependencies their registries' metadata names. Every file walks the chain on its own (inspection, malware, the scanners and provenance), and the approvers see ONE set:

A Maven POM's advisories are a warning (security-policy change, 3.1.1)

A .pom carries no code. Maven reads POMs to build the dependency graph, and it reads the POMs of old versions that dependency mediation then drops, without fetching their JARs. A real mvn dependency:get of commons-lang3 reads the POMs of commons-collections 3.2.1, log4j 1.2.12 and dom4j 1.1 under maven-dependency-plugin 3.7.0. Holger used to refuse those POMs by their CVEs, and Maven fails on a refused descriptor, so the plugin could never resolve.

Since 3.1.1, the vulnerability scan of a Maven .pom records its advisories as a WARN: they are named in the evidence, in the dependency set and in the console, and the rule cve_max does not refuse on them. The JAR, WAR, EAR or AAR of the same coordinates is matched and refused as before. Malware, licence, provenance, secrets and the inspection gates still block a POM. The rule applies to the Maven ecosystem and the .pom suffix only (server/lib/src/scanner/mod.rs, advisories_warn_only).

Scanned pushes to an oci repository

On a Holger with an intake chain, every oci repository is a scanned registry (server/lib/src/promote/oci_push.rs):

Still in v1