Skip to content

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:

  1. finds the issue by a marker in its body carrying $CI_PROJECT_PATH (the title is cosmetic; renaming it is safe), creating it with report_label if absent;
  2. always rewrites the body — findings per section, and a Last scanned line 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;
  3. posts one comment listing new and cleared findings, only when the set changed;
  4. 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