Writing a justification¶
A justification is a written explanation of why a particular finding does not need to be fixed before this build ships. You write it in cdso_config.yml in your repository, keyed to the specific finding, and the pipeline carries it into the scan record the reviewer reads. The reviewer sees your text exactly as you wrote it, so write it for a person.
What can and cannot be justified¶
| Finding | Can it be justified? | Where it goes |
|---|---|---|
| Critical CVE | No. It has to be fixed. | |
| High CVE with a fix available | No. The reviewer will refuse it. | |
| High CVE with no fixed release | Yes, but expect the reviewer to look closely. | mitigations |
| Medium or Low CVE | Yes. | mitigations |
| Static analysis finding | Yes, by rule id. | semgrep.exclusions |
| Unverified credential pattern | Yes, by file. | trufflehog.excluded_files |
| Verified live credential | No. Revoke it, rotate it, remove it. | |
| Malware signature match | No. | |
| Dockerfile lint error | Yes, by rule code. | hadolint.ignores |
| Root user at PRODUCTION | Yes, with a reason a reviewer can check. | container_spec.root_user |
| Dynamic scan alert | Yes. | zap.skip_reason |
What a reviewer is looking for¶
A reviewer reading a justification wants answers to three questions, in this order.
- Why is this image not affected? For example, the vulnerable function is never called, the package is present but the feature is disabled, or the port is not exposed.
- What is the upgrade path? When is a fix expected, or why is none coming?
- What limits the risk in the meantime? Network policy, a non root user, a read only filesystem, and so on.
Two or three sentences that answer those questions are better than a long paragraph that does not.
Examples¶
A vulnerability with no fixed release:
my_service:
mitigations:
- CVE_2025_12345: >-
The affected function is in the XML parser, which this service
never loads; requests are JSON only and the parser module is not
imported. No fixed release exists upstream as of this build. The
container runs as a non root user with no outbound network.
A static analysis rule that does not apply:
my_service:
semgrep:
exclusions:
python.lang.security.audit.subprocess-shell-true: >-
The single match is in scripts/local_dev.py, which is not copied
into the image; the Dockerfile copies src/ only.
An unverified credential pattern in a test fixture:
Things the pipeline checks¶
- CVE keys use the CVE id with dashes replaced by underscores (
CVE_2025_12345). GHSA ids are not accepted. - The container key uses underscores where the container name has dashes.
- Exclusions use the map form shown above so the reason is carried into the record. A plain list carries no reason.
- A
mitigationsentry that is empty or reads "Mitigation pending" is treated as not ready, and the submission waits until it is written.
How Explainable AI helps¶
The finding page drafts a starting point from the evidence: what the scanner found, where, and what it would take to clear it. Use it to save time, but the words that go into the file are yours and the reviewer will hold you to them.
What the reviewer sees¶
Each justification appears beside the finding it covers, word for word, along with the CVE table from the scan record. Excluding a finding hides it from the gate, not from the reviewer. They see every exclusion and the reason you gave.