We have built up the GitHub Actions pipeline through five sessions: the project basics, software composition analysis, license compliance, and static application security testing. The next layer is container scanning — looking for vulnerabilities inside the Docker image we ship, not just in the source we wrote. In Part 6 of our series, Patrick Steger and I split the work into two GitHub Actions sub-workflows: one builds the image and pushes it to the registry, the other pulls it back and runs Trivy on it.
What Container Image Scanning Covers#
Container image scanning looks for vulnerabilities in everything that ends up inside the Docker image — both operating system components and the application libraries you bring in. That overlap with SCA is by design, and Patrick calls it out upfront: most findings from container scanning will already have been flagged by software composition analysis on the source dependencies. You can configure that overlap away if it bothers you, but we leave it on the default in this video and accept the duplication for completeness.
Two Sub-Workflows: Build and Scan#
The pattern we use in this series is to keep the main pipeline lean and push real work into separate workflow files. For container scanning, that means two new sub-jobs:
- A build Docker image job that creates the image and stores it in the GitHub Container Registry. It outputs the image tag.
- A container image scan job that takes that tag as input, pulls the image, scans it with Trivy, and pushes the findings into the GitHub Security tab.
In the main pipeline, the Docker job depends on the build job (we need the compiled application jar inside the image). The scan job declares the Docker job as a prerequisite and reads the image tag from its outputs. By naming both jobs starting with “D”, they sort nicely at the bottom of the GitHub Actions UI.
The Build Docker Image Job#
The build job lives in docker.yml. It can be triggered manually or called from the main workflow. We declare an outputs: block at the workflow and job level that surfaces the image tag for downstream consumers. We also declare the container registry and the image name as inputs.
The steps are straightforward: check out the source so we have the Dockerfile, download the prebuilt application binary from the earlier build step, log in to the container registry using the credentials GitHub provides, extract Docker metadata (tags, labels), and then build and publish the image with the docker/build-push-action. The tag we publish under is the same one we expose as the job’s output.
The Container Scan Job#
The scan job lives in its own file too. It can be started manually if you supply the image tag, or called from the main pipeline — which is what we do here, passing the tag from the build job. The steps log in to the container registry, docker pull the image, and run Trivy against it. The most important option is the output format: we set it to SARIF so GitHub can ingest it natively. The final step uploads the SARIF file with the standard github/codeql-action/upload-sarif action, which makes the findings show up in the Security tab.
Reviewing Findings in GitHub#
After the run we head to the Security tab → Code scanning and filter by tool — Trivy. A lot of the findings sit inside app.jar. Those are our application’s own dependencies, the same set SCA already flagged in an earlier session. There are also genuinely new findings from the OS layer — Patrick points to one in an SSL library as an example. Container scanning added roughly a hundred new entries on top of what we already had.
What to Do With All Those Findings#
Patrick’s advice is unambiguous: the default action is to fix them, and “fix” almost always means update the libraries. Updating a vulnerable dependency removes the finding from Trivy and from the SCA scanner in one move. If you do not want the duplicates between SCA and the container scan, you can exclude app.jar from Trivy — but that is cosmetics, not security.
When updating is not an option, the work begins. You need to assess whether the vulnerability actually affects your application, judge the residual risk, and then decide on a workaround, an accepted-risk decision, or — if the risk is high enough — taking the application offline. Patrick says it twice for emphasis: default action is remediate. We will cover how to manage and triage vulnerabilities inside GitHub in a later session.
Key Takeaways#
Scan the image, not just the source. SAST and SCA inspect what you wrote and what you depend on. Container scanning checks the OS layer and everything that landed inside the image. Without it, you ship vulnerabilities you never tested for.
Split build and scan into two workflows. One job builds and publishes the image and exposes the tag. The other consumes the tag and scans. Clean inputs and outputs make the dependency obvious and the files reusable.
SARIF is the integration that matters. Configure Trivy to emit SARIF and upload it with
upload-sarif. That single choice is what puts the findings into the Security tab where developers actually see them.Expect overlap with SCA. Most container findings will already be in your SCA report. That is normal. Pick one as the source of truth or filter
app.jarout of Trivy — but do not panic at the duplication.Default action: update the library. A version bump usually clears findings in both Trivy and SCA. Workarounds and risk acceptance are fallbacks, not first moves.
Triaging takes process, not tooling. When you cannot update, you need impact analysis, risk assessment, and an explicit decision. Tooling surfaces the issue; people decide what to do with it. Manage that workflow deliberately, not by ad-hoc Slack threads.
