| 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. |
The policy is executable: scripts/check-release-vulnerabilities.ts evaluates a Trivy report against
security/vulnerability-policy.json. This page states it in prose.
Disclosure — how to report a vulnerability privately — is SECURITY.md; this page is remediation
only.
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.
An unfixable HIGH is not an exception and does not need one. It remains recorded, but without a patched version there is no upgrade for the gate to require.
Unfixable findings stay in the scan report and in the highCritical list of the .policy.json
attached to the release; the filter is in the evaluator, not a --ignore-unfixed flag.
| When | Subject | Effect |
|---|---|---|
Every image build (reusable-docker-build.yml) |
the linux/amd64 image the run just pushed |
fails the run |
A pull request touching security/release-images.json (ci-security-scan.yml) |
both platforms of each pinned upstream digest | fails the run |
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, and both platforms of each pinned upstream digest |
opens or updates a tracking issue |
Weekly (rescan-release-images.yml) |
every subject in the latest published release | fails and comments on the vulnerability response issue |
All five evaluate security/vulnerability-policy.json through scripts/check-release-vulnerabilities.ts;
the release and the rescans reach it through verify-release-evidence.ts, scan-main-images.ts and
scan-upstream-images.ts, and scripts/ci-contract.test.ts asserts there is one policy file. That
test also asserts the scans before the release cover exactly the subject set the release gate covers,
both derived from security/release-images.json: the upstream images are shipped by digest and never
built here, so nothing scanned them until the release gate did, and v0.75.0 failed on a finding that
had been in the pinned alpine digest for days. The scheduled rescans do not fail a commit status: a
finding published after a clean merge has no commit to revert. A scanner failure is never treated as
no findings.
A pinned upstream image cannot be patched by rebuilding anything here. Its remedy is a digest bump in
security/release-images.json — which Renovate proposes, and which the pull request proposing it now
scans — and until upstream republishes the tag there may be no digest to bump to.
That is also why the upstream scan covers both platforms while the scans of images we build cover
linux/amd64 only. An exception matches on image | platform | vulnerability | package | installedVersion, so a single-platform scan can only ever half-check a subject. For an image we
build, the other half costs little to leave to the release: a finding on either architecture is
fixed by the same rebuild, and the Version PR preflight scans both before the release is cut. For a
pinned upstream digest there is no rebuild, the fix is a digest bump nobody can make until upstream
publishes one, and the bump arrives in a pull request — so an arm64-only finding has to fail that
pull request rather than the release.
An exception lets one fixable finding through for at most 90 days. 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, andinstalledVersionis 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.status—affectedornot_affected, plus ajustificationin 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—YYYY-MM-DDTHH:MM:SSZ, in the future, at most 90 days out; when it lapses the gate fails again.
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.