image-scan¶
Scans the image tarball a build job produced, twice, because two different people can act on the two answers — and, when told where, keeps one issue per image current with the findings so the answer reaches someone.
| job | what it scans | gate |
|---|---|---|
scan:os |
OS packages (--pkg-types os) — ours to fix with a base bump |
hard; your publish jobs needs: it |
scan:tools |
library packages inside upstream tool binaries (--pkg-types library) — cleared only when upstream rebuilds |
advisory by default (tools_allow_failure) |
scan:report |
nothing; reads the two JSON artifacts and upserts the dashboard issue | never gates (allow_failure: true) |
Each scan runs trivy once to JSON and renders the table in the job log from that JSON, so the report and the log describe the same findings.
Why the report exists¶
The nightly rebuild + rescan schedules on the image repositories catch a
newly-disclosed advisory against what is already shipped. They were doing
that — and telling nobody: an advisory scan:tools failure under a green
pipeline sat in a job log for weeks. Spec 0099 D6 puts it this way: the
release merge request gates what is about to ship; the nightly rescan is
surveillance of what has shipped, and the gap between releases was
measured at 17–24 days for five of the eight images.
The dashboard issue¶
scan:report exists only when report_project is set, and runs only on
default-branch pipelines (report_if) — a merge-request pipeline scans a
branch build, and a branch's findings are not what the image ships. Use a
private project: the body carries unpatched advisories.
On every run it:
- finds the issue by a marker in its body carrying
$CI_PROJECT_PATH(the title is cosmetic; renaming it is safe), creating it withreport_labelif absent; - always rewrites the body — findings per section, and a
Last scannedline with the pipeline URL. This happens on a closed issue too: a clean image and a dead token must not look the same, and a stale stamp is the tell; - posts one comment listing new and cleared findings, only when the set changed;
- closes the issue when both sections are empty, and reopens it on a finding, so history stays on one issue.
A finding is identified by (advisory, package, installed version). The body
also carries the rendered markdown as the image-scan-report.md artifact.
A scan that did not run is not a clean scan. scan:report runs
when: always so a failed scan still reports, but when a scan was skipped —
the build failed — there is no JSON, and the job exits 2 without touching
the issue. Reading absence as zero once closed a real dashboard issue as
clean.
Inputs¶
| Input | Type | Default | Description |
|---|---|---|---|
stage |
string | scan |
Stage for all three jobs. You must declare it. |
trivy_image |
string | ghcr.io/aquasecurity/trivy:0.73.0 |
Trivy image the scan jobs run in. |
trivy_cache_dir |
string | /opt/ci-cache/trivy |
Shared vuln-DB cache. When a DB is there it is copied into a job-local cache and the scan runs offline; absent, trivy downloads as normal. |
build_job |
string | build |
Your job whose artifact carries the tarball; both scans needs: it. |
input_tar |
string | image.tar |
Tarball passed to trivy image --input. |
severity |
string | HIGH,CRITICAL |
Severities that count, for gating and reporting alike. |
ignore_unfixed |
boolean | true |
Only advisories with a fixed version count. |
tools_allow_failure |
boolean | true |
Whether scan:tools is advisory. docs-tools sets false. |
if |
string | $CI_COMMIT_SHA |
rules:if: for the scan jobs. Always true by default. |
report_project |
string | "" |
Project holding the dashboard issue. Empty means no scan:report job. |
report_token |
string | $GITLAB_TOKEN |
Token with api scope on report_project. Not a job token — it cannot write another project's issues. |
report_title |
string | Image scan: $CI_PROJECT_NAME |
Issue title. |
report_label |
string | security-advisory |
Label applied on create and re-applied on update. |
image_version |
string | "v0.2.2" |
phpboyscout/images/ci-base tag scan:report runs on. |
report_if |
string | $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH |
rules:if: for scan:report; runs when: always so a failed scan still reports. |
report_dry_run |
boolean | false |
Render the body and the change comment to the log; write nothing, need no token. |
report_token defaults to $GITLAB_TOKEN rather than $CI_JOB_TOKEN, a
stated departure from the authoring rule: the
estate is on GitLab Free, which has no project access tokens, and a job token
cannot write issues elsewhere. renovate-merge and colophon are the
precedent.
Jobs produced¶
| Job | Runs when |
|---|---|
scan:os |
if (every pipeline by default) |
scan:tools |
if (every pipeline by default) |
scan:report |
report_project set and report_if (default-branch pipelines) |
Usage¶
Replacing a hand-written .scan-base / scan:os / scan:tools trio is a
drop-in: the job names are the same, so a publish job's
needs: ["scan:os"] keeps working.
stages: [lint, build, scan, publish]
include:
- component: gitlab.com/phpboyscout/cicd/[email protected]
inputs:
stage: scan
report_project: phpboyscout/org
The shared DB cache is copied, not opened¶
trivy keeps its cache in BoltDB. Two jobs on one runner opening the same file
lock each other out, so the cached DB is copied into /tmp for the job and
--skip-db-update runs against the copy. The metadata's UpdatedAt is
echoed so the log says how fresh the DB was.
What the self-test cannot prove¶
The live write. GITLAB_TOKEN is protected, so a merge-request pipeline never
sees it, and a job token cannot create issues. The self-test scans a real
tarball and renders the report in dry-run; the write is verified at the
consumer, on its first default-branch pipeline after adoption.
See also¶
image-release— the release-time path, which relies on the release merge request having scanned the tagged commit- Spec 0099