Container image scanning
Container image scanning answers the what’s vulnerable in this image question — complementing the SBOM’s what’s in this image. Findings come from two sources, and both are surfaced next to the image or commit they describe.
How it works
The daily registry sweep
fremforge scans the container images in your organisation’s package registry itself. Nothing has to be added to your workflows.
- What is scanned. Every tag of every container image. Each tag is resolved to its manifest digest, and the image is scanned with
trivy imagefor vulnerabilities and leaked secrets, including OS packages and language dependencies in every layer. - When. Once a day, after the Trivy vulnerability database is refreshed. A digest already scanned against the same database version is skipped, so a newly published CVE is raised against an image that has not changed.
- Where findings are kept. Keyed by the image digest (
frem.sh/<org>/<image>@sha256:<digest>). Dismissals carry over when an image is rescanned. - Limits. For a multi-architecture image, only the
linux/amd64variant is scanned. An image larger than 2 GiB (the sum of its layers as stored in the registry) is not pulled, so it is not scanned.
The same sweep also scans each repository’s default branch (trivy fs and trivy config).
The same image pull is also matched against the OSV list of malicious packages. That check is described in Malicious packages; a pull of an image with an open finding is refused, whatever the block action below. Since 2026-10-11 the same holds for images cached through the dependency proxy (see below).
Images cached through the dependency proxy
Images your organisation pulls through the dependency proxy are scanned the same way as the images you push.
- When. An image is queued for its first scan when your organisation first caches it, and is normally scanned within 15 minutes. After that it is rescanned whenever the Trivy vulnerability database changes, in the same daily sweep as your own images, for as long as it stays in your cache.
- What is scanned. The single-platform image your client pulled (for a multi-architecture tag, the variant you pulled), with
trivy imagefor vulnerabilities and leaked secrets. - Where findings are kept. Keyed
frem.sh/<org>/dependency-proxy/<upstream>/<repository>@sha256:<digest>, for example.../dependency-proxy/docker.io/library/alpine@sha256:…, next to your own images in Admin → Security › Findings › Container images and in the findings API. Dismissals carry over when the image is rescanned. - Block action.
block_pullapplies to proxied images too: a pull through the proxy of an image with an undismissed high or critical finding is refused with403 DENIED. - Malicious packages. The language packages inside a cached image are matched against OSV’s malicious-package advisories, as for pushed images, and a match is a finding on the Malware tab. Since 2026-10-11 a pull of a cached image with an open finding is refused, as for a pushed image; see Dependency proxy.
- Shared scans. An image is identified by its digest, so when several organisations cache the same image it is scanned once and each organisation gets its own copy of the findings. You only ever see findings for images in your own cache.
- When an image leaves your cache. Its findings stay while the image is still in use: pulled through the proxy by your organisation, or named in one of your deployments, in the last 30 days (the registry access log and the deployment records say which). After that they are removed at the next sweep. An image pulled again after its findings were removed gets them back at the next sweep, from the scan of its digest.
Images your Helm charts deploy
The images a Helm chart in your repository deploys are scanned for vulnerabilities, and the findings are reported on the repository’s fremforge/image-scan check.
- What is read. Every chart in the repository, that is every directory with a
Chart.yaml. Invalues.yaml: animage:with a reference (image: nginx:1.25.3), and the split form charts use most (image:withregistry,repository,taganddigestunder it). A split image with no tag, ortag: "", is the chart’sappVersion, which is what{{ .Values.image.tag | default .Chart.AppVersion }}deploys. Intemplates/: animage:line with a literal reference. Keys such asinitImageorsidecarImagecount too;imagePullPolicyandimagePullSecretsdo not. - What is not read. Templates are not rendered and no chart code runs, so an image assembled by template logic, values files other than
values.yaml(an overlay you choose at install time), and dependency charts that are not in the repository are not covered. A reference with no registry is Docker Hub (nginxisdocker.io/library/nginx), and one with no tag islatest. - How the image is fetched. Through your organisation’s dependency proxy, as a pull of yours would be: your upstream credentials, package policy, malware scan and cache apply. If your organisation has no dependency proxy, fremforge’s own proxy fetches it, anonymously and only from registries that allow that (for example
ghcr.io,quay.io,registry.k8s.io). An image your proxy refuses (for example one your policy blocks) is not scanned. The linux/amd64 image is scanned withtrivy image, for vulnerabilities. - Docker Hub needs your own credential. Docker Hub images are pulled only with your organisation’s own Docker Hub credential, never anonymously and never with a fremforge account. Without one, a Docker Hub image in a chart is not scanned: the
fremforge/image-scanstatus says so (“N Docker Hub image(s) in Helm charts not scanned: add your Docker Hub credential”), and Security › Findings › Container images lists the images with a link to add the credential under Upstream credentials. The images are scanned once you add it, from the repository’s next scan; findings scanned for another organisation are never shown to yours. - When. An image is queued the first time a scan names it, and is normally scanned within 15 to 30 minutes; its findings appear on the repository’s next scan. An image that is still named is scanned again every few days, because a tag can move and new advisories are published.
- How a finding reads. The target is the chart file and the image, for example
charts/web/values.yaml: docker.io/library/nginx:1.19.0, with the package, its version and the fixed version. The findings go through the image-scan threshold and dismissals, and a pull request is blocked only by findings it adds.
Newer tags for upstream images
An image you pull from upstream has no Dockerfile of yours to fix, so fremforge looks for a newer tag that fixes its findings. For each cached image with open high or critical findings, at most once a day:
- The tag you pulled. The digest is matched to the tag your organisation pulled it by, through the multi-architecture index when there is one.
- The candidate. This is the newest tag of the same line, which has the same shape and the same numbers except the last:
3.12.4-slimto3.12.10-slim, or16.4to16.10. It is never another major or minor version, because that is a migration rather than a fix. A tag without such a line, such as3.12-slim,16orpg17, is checked for a rebuild instead: the same tag, rebuilt upstream since you pulled it. - What it fixes. The candidate is pulled and scanned with the same scanner. Every open finding of yours that the candidate no longer has counts as fixed.
The finding then says one of these:
| Shown | Meaning |
|---|---|
Newer tag <tag> fixes N of this image’s findings | Move to that tag. Open PR with <tag> opens the pull request below |
Rebuilt upstream: pulling <tag> again fixes N | The same tag is newer upstream. Pull it again; nothing changes in your repository unless a reference pins a digest |
| Upstream image, no newer fixed tag | No tag of the same line, and no rebuild, has fewer of these findings |
| Upstream image: newer tags not checked yet | Not checked yet, or the registry is not one fremforge reads tags from (Docker Hub, GHCR, Quay, MCR, ECR Public and registry.k8s.io are) |
The same is in the findings API as upstream_update.
The pull request. Open PR with <tag>, or POST /orgs/{org}/dependency-updates/security-fixes/image-ref-requests with image_finding_id, searches your organisation’s repositories for references to the image at the tag you pulled. That covers docker compose files, Kubernetes manifests, Helm values with repository: and tag: on separate lines, and CI image: and container: entries, in any of the names the image goes by:
frem.sh/<org>/dependency-proxy/docker.io/library/python:3.12.4-slimdocker.io/library/python:3.12.4-slimpython:3.12.4-slim
Each repository that has a reference gets one security fix pull request that moves all of its references. A digest pin moves to the candidate’s digest. It has the usual branch, label, limits and merge setting. A Dockerfile FROM is not changed here: that is the base-image fix. When no repository references the image, nothing is opened, and the finding keeps showing the tag that fixes it. Up to 60 repositories and 400 YAML files are read per request.
Scanning from your own workflow
You can also scan in CI and send the results in. The fremforge runner image ships Trivy. This example scans the repo filesystem (trivy fs — dependencies + leaked secrets) and the IaC config (trivy config — Dockerfiles, Compose files), then POSTs the findings:
- name: Scan + push findings
run: |
trivy fs --format json --output trivy-fs.json .
trivy config --format json --output trivy-config.json .
curl -sS --fail \
-H "Authorization: Bearer ${{ secrets.FREMFORGE_API_TOKEN }}" \
-H 'Content-Type: application/json' \
-d @<(jq -c '{
image_ref: "'"${GITHUB_REPOSITORY}@${GITHUB_SHA}"'",
commit_sha: "'"${GITHUB_SHA}"'",
repo_full_name: "'"${GITHUB_REPOSITORY}"'",
findings: [.Results[].Vulnerabilities[] | {
cve_id: .VulnerabilityID,
severity: (.Severity|ascii_downcase),
package_name: .PkgName,
installed_version: .InstalledVersion,
fixed_version: .FixedVersion,
title: .Title,
target: .PkgPath
}]
}' trivy-fs.json) \
"https://frem.sh/_app/api/v1/image-scans/findings"The PAT needs the findings:write scope.
Policy toggles
Security › Policies › Image policies carries these settings (until 2026-10-15 they sat on Findings › Container images):
- Enabled — when off, ingest returns
202 {accepted: 0, status: 'disabled'}(still parsed, just dropped). Useful for pre-launch teams that want the scan to run but not gate. - Block action —
none|warn|block_pull:none— store findings, post no commit status. Build-side detection only. Pulls are not checked.warn— store findings, post commit statussuccesswith description noting the count. PR merge unblocked. Pulls are checked but never refused: a pull thatblock_pullwould refuse is served, and recorded aswould_blockin the registry access log and as the audit eventregistry.pull.would_block(see Audit log and SIEM).block_pull— store findings, post commit statusfailurewhen at least one undismissed finding at or above your block threshold exists. On a pull request, only findings the pull request introduces count (see Pull requests: only new findings block). Branch protection with the requiredfremforge/image-scanstatus check then blocks merge. It also blocks pulls: adocker pull(by tag or by digest) of an image with such a finding is refused with403 DENIED, and the message names the threshold and links to the findings. Dismiss or fix the findings to unblock it. Each refusal is the audit eventregistry.pull.blocked.
- Require signed images — when on, the ingest also accepts a
signature_statusfield (signed/unsigned/unknown). Unsigned images (or signed images from an issuer not on the allowlist) get a synthetic critical finding so the same block-pull path enforces the signing policy. See Cosign for the signing side.
Block threshold
The findings that block are those at or above one severity, set per organisation. It is the block_severity of the image_scan entry in your org-wide scan policy, the same setting for merges and for pulls:
block_severity | Blocks on |
|---|---|
critical | critical |
high (default) | high and critical |
medium | medium, high and critical |
low | every finding |
Set it with PUT /_app/api/v1/orgs/<org>/policy:
curl -X PUT -H "Authorization: Bearer ${FREMFORGE_PAT}" -H 'Content-Type: application/json' \
-d '{"scan_policy": {"image_scan": {"mode": "enforce", "block_severity": "critical"}}}' \
"https://frem.sh/_app/api/v1/orgs/acme/policy"or in configuration as code as scan_policy = { image_scan = { mode = "enforce", block_severity = "critical" } }. null, or no image_scan entry, means high, which is what every organisation had before the threshold could be set. Any other value is refused with 400. The token needs policy:write. The console has no control for it yet.
Only the org-wide entry counts. A per-repository scan-policy override can carry a block_severity, but an image pull is not tied to one repository, so the image gates do not read it.
Pull requests: only new findings block
The fremforge/image-scan check on a pull request fails only on findings the pull request introduces. fremforge compares the pull request’s findings at or above your block threshold with the latest scan of the branch it merges into. A finding that branch already has does not block, so a pull request that changes nothing vulnerable can merge in a repository that has open findings. The status then reads, for example, No new high/critical findings; 7 already on main.
- A finding is the same when its advisory, its package and its file are the same, the identity a dismissal is carried over by. The same advisory in another file is a new finding.
- A pull request that adds one new high finding to a repository with many existing ones fails: 1 new high/critical finding(s) …; merge blocked; 30 already on main.
- A pull request that fixes some findings and adds none passes.
- Existing findings are not hidden. They are listed, counted towards your SLA and gate image pulls as before.
The branch is scanned after a push to your default branch, at most once an hour, with the newest push winning. A finding that reached the branch since its last scan therefore still counts as the pull request’s until the next scan. It is never the other way round: a new finding is never mistaken for an existing one.
When there is no scan of the branch the pull request merges into, every finding at or above the threshold blocks, as before. For the default branch, that pull request’s scan also asks for a scan of the branch. Once the branch’s first scan is recorded, fremforge scans again, by itself, the open pull requests into it whose check failed meanwhile (at most 20 per repository, the most recently updated), so they turn green, or stay red only on what they add, without a re-run or a new push. A pull request into any other branch always counts every finding.
The image pull gate is unchanged: a pull of an image with an undismissed finding at or above the threshold is refused under block_pull, whether the finding is new or not.
Audit log and SIEM
Every pull decision of the image gates is written to your audit log and from there to your SIEM:
registry.pull.blocked— a pull was refused: byblock_pull(gateimage_scan), by malware matching (image_malware) by a layer that ClamAV found infected or could not scan (registry_scan, orregistry_blobfor a layer fetched directly by digest), or by the image signature check (signature).registry.pull.would_block— a pull was served that one of those gates would have refused: under yourwarn, or while a gate runs in observation mode.
There is one event per image digest, gate and decision per hour; the next event for the same digest carries the previous hour’s count.
Where findings surface
Admin → Security › Findings › Container images lists scans by image-ref + commit + PR. Each row shows:
- Severity counts (critical / high / medium / low)
- Fixable counts (findings with a non-null
fixed_version) - Status (
success/failure/skipped) + the commit status posted - Links to the upstream advisories (CVE, GHSA when present)
What a finding looks like in the DB
Stored fields per finding: cve_id, severity, package, installed_version, fixed_version, target (the in-image file path), and the scan_run_id that ties findings produced by the same run. The full Trivy output is NOT stored — just the per-finding rows, which is the queryable shape.
Retention
Findings follow the per-tenant audit retention policy. Aged-out findings keep their hash in the chain (tamper-evidence) but lose the per-row payload.
See also
- Malicious packages — the same image pull is also matched against OSV
MAL-*advisories; a pull of an image with an open finding is refused whatever the block action above. - Cosign — image signing + signed-images policy.
- SBOMs — package inventory in the image (independent of vulnerability state).
- OSV recommendations — the source-side complement (vulnerabilities in your dependencies before they ship).
- API reference —
POST /_app/api/v1/image-scans/findingsingest endpoint (tenant-scoped via the runner PAT).