Skip to content

Latest commit

 

History

History
99 lines (80 loc) · 6.21 KB

File metadata and controls

99 lines (80 loc) · 6.21 KB
id vulnerability-remediation
sidebar_position 5
title Vulnerability remediation
description What blocks a build or a release, why unfixable findings do not, and how to write an exception.

Vulnerability remediation

The policy is executable: scripts/check-release-vulnerabilities.ts evaluates a Trivy report against security/vulnerability-policy.json. This page states it in prose, which is what NIST SSDF RV.1.3 asks for. RV.2.1 accepts "remediation or other risk response" and prescribes no thresholds — only that ours are written down.

What blocks

A finding blocks when it is HIGH or CRITICAL and upstream has published a fix — Trivy reports a non-empty FixedVersion. MEDIUM and below never block, at any scan.

The fixable/unfixable split is the part most easily read wrong, so plainly: an unfixable HIGH is not an exception and does not need one. Four of the images in security/release-images.json are upstream — alpine, nats, nginx, and traefik — and cannot be patched here at all, so failing on a finding that has no fix removes no risk and leaves nobody an action. The share is not marginal: when the gate was first measured against real images, agent-pi reported 59 HIGH/CRITICAL findings of which 1 was fixable, and postgres 99 of which 23 were.

Gating on fix availability is the ordinary shape of this control. Google Binary Authorization exposes maximumFixableSeverity and maximumUnfixableSeverity as separate settings; Docker Scout's default policy is "no fixable critical or high vulnerabilities"; Snyk separates --fail-on=upgradable from --severity-threshold; Chainguard's remediation SLA starts when a patch exists rather than when the CVE is published.

Unfixable findings are still counted. The filter lives in the evaluator, not in a --ignore-unfixed flag on Trivy, so every HIGH and CRITICAL stays in the scan report and in the highCritical list of the .policy.json attached to the release. Suppressing the failure is the intent; suppressing the record is not, and the evidence bundle is signed on the assumption that it is complete.

Where the gate runs

When Subject Effect
Every image build (reusable-docker-build.yml) the linux/amd64 image the run just pushed fails the pull request
Release evidence (release.yml) both platforms of every first-party and upstream image no release is created
Weekly (rescan-main-images.yml) the linux/amd64 image behind each :main tag opens or updates a tracking issue
Weekly (rescan-release-images.yml) every subject in the latest published release reports on the vulnerability response issue

All four call the same script with the same policy file; scripts/ci-contract.test.ts asserts there is no second copy of either. The two scheduled rescans deliberately do not block: a finding published after a clean merge is nobody's commit and no revert removes it, so a red status would name no culprit and demand no action — Release management has how the drift is routed instead. A scanner failure is never treated as no findings.

Exceptions

An exception lets one fixable finding through for a bounded time. It is an entry in security/vulnerability-policy.json, and security/ has no CODEOWNERS rule of its own, so the default one applies: a member of the @ls1intum/hephaestus-maintainers team has to approve it.

  • image, platform, vulnerability, package, installedVersion — the match key. All five bind exactly, and installedVersion is what makes an exception die when the package moves.
  • digest — recorded so the exception names the artefact it was reviewed against, but deliberately not matched on. A release digest does not exist until the images are tagged, so a digest-keyed exception could only ever be written after the gate had already failed a release.
  • owner — who is accountable for retiring it.
  • statusaffected or not_affected, plus a justification in prose.
  • evidence — an HTTPS URL: the upstream advisory, the tracking issue, or the analysis the claim rests on. A link a reviewer can follow, not a sentence.
  • expires — RFC 3339 UTC, in the future, and at most 90 days out. The cap is not a renewal-free window; at 90 days the gate fails again and the finding is argued again with whatever upstream has published since.

not_affected justifications

A not_affected exception also carries a justificationCategory, one of the five in CISA's Minimum Requirements for Vulnerability Exploitability eXchange (VEX). Naming one forces the claim to say which way the finding does not apply, so prose like "not reachable" cannot stand in for an analysis nobody did.

Value Means
component_not_present the vulnerable component is not in the image
vulnerable_code_not_present the component ships, the affected code does not
vulnerable_code_not_in_execute_path the affected code ships but is never called
vulnerable_code_cannot_be_controlled_by_adversary it is called, but never with attacker-controlled input
inline_mitigations_already_exist it is reachable and controllable, but a compensating control blocks exploitation

A value outside those five is a malformed policy and throws. An affected exception carries no category — it defers a risk we concede applies, and claiming non-exploitability at the same time is a contradiction.

VEX is not adopted

The vocabulary above is all we take. OpenVEX has been frozen at v0.2.0 since 2023-08-22, and Trivy's --vex still carries an EXPERIMENTAL banner and matches on digest-based PURLs that a locally built image does not have. aquasecurity/vexhub, Trivy's default VEX repository, stopped updating on 2026-03-23 and has no OS-distro coverage at all — no Alpine, no Debian, no nginx — and Alpine publishes no VEX itself, which is where most findings here originate. Adopting it would mean hand-authoring documents that duplicate security/vulnerability-policy.json, minus the expiry field OpenVEX does not define. Revisit if the distros start publishing.