Container Image Security Scanner¶
The Container Image Security Scanner (scanner-container) is a first-class VirtualSutra agent that scans container images for CVEs, OS compliance violations, and generates Software Bills of Materials (SBOMs). It is dispatched directly by the orchestrator - one task per ContainerImageConfig in the inspection scope - and runs in parallel with cloud and codebase scanner tasks.
Availability: Pro and Enterprise · Tenant-scoped
See the Tier & Scope Matrix for how this is enforced today.
Coordinator image: ghcr.io/virtual-sutra/scanner-container
Redis stream: vsutra:tasks:container_scanner
AgentType: CONTAINER_SCANNER
In Kubernetes, the coordinator spawns a multi-container Job using dedicated sub-images - each tool runs in its own container, sharing the pulled image layer via an emptyDir volume:
| Sub-image | Tool | Capability |
|---|---|---|
ghcr.io/virtual-sutra/scanner-pull | Skopeo | Image pull (init container) |
ghcr.io/virtual-sutra/scanner-trivy | Trivy | CVE scanning (N1) |
ghcr.io/virtual-sutra/scanner-syft | Syft | SBOM generation (N3) |
ghcr.io/virtual-sutra/scanner-openscap | OpenSCAP + SSG | OS compliance (N2, opt-in) |
Capabilities¶
| Track | Capability | Tools | Status |
|---|---|---|---|
| N1 | CVE scanning | Trivy | Implemented |
| N2 | OS compliance | OpenSCAP + SCAP Security Guide + umoci | Implemented (opt-in) |
| N3 | SBOM generation | Syft (CycloneDX) | Implemented |
All image pulls use Skopeo for daemon-free operation - no Docker socket is required.
Prerequisites¶
The following tools must be present in the scanner container image. They are included in the official ghcr.io/virtual-sutra/scanner-container image.
N1 - CVE Scanning (required)¶
- Skopeo >= 1.13 - daemon-free image pull to OCI layout
- Trivy >= 0.50 - CVE and misconfiguration scanning
N2 - OS Compliance (opt-in, enable_openscap: true)¶
- OpenSCAP >= 1.3 (
openscap-scannerpackage) - SCAP Security Guide (SSG) -
scap-security-guidepackage - umoci >= 0.4 - extracts OCI image filesystem for
oscap-chrootevaluation
N2 is disabled by default (
enable_openscap: false) because image extraction is slow. Enable it per image via theContainerImageConfig. See OS compliance findings for the operating systems it assesses and how to read its results.
N3 - SBOM Generation (required)¶
- Syft >= 1.0 - generates CycloneDX JSON SBOM
Configuration¶
Container images to scan are declared in InspectionConfig.scope.container_images as a list of ContainerImageConfig objects.
{
"scope": {
"container_images": [
{
"registry": "ghcr.io",
"repository": "my-org/my-app",
"tag": "v1.2.3",
"platforms": ["linux/amd64", "linux/arm64"],
"enable_trivy": true,
"enable_openscap": false,
"enable_sbom": true,
"license_blocklist": ["GPL-3.0", "AGPL-3.0"],
"credentials_ref": "env://REGISTRY_PASSWORD",
"username_ref": "env://REGISTRY_USERNAME"
}
]
}
}
ContainerImageConfig fields¶
| Field | Type | Default | Description |
|---|---|---|---|
registry | string | required | Registry hostname (e.g. ghcr.io, docker.io, 123456789.dkr.ecr.us-east-1.amazonaws.com) |
repository | string | required | Repository path (e.g. my-org/my-app) |
tag | string | "latest" | Image tag or digest |
platforms | list[string] | ["linux/amd64"] | Target platforms to scan |
enable_trivy | bool | true | Run Trivy CVE scan |
enable_openscap | bool | false | Run OpenSCAP OS compliance scan (slow - opt-in) |
openscap_profile | string | cis | Benchmark profile: cis or stig (STIG on Ubuntu and RHEL only) |
enable_sbom | bool | true | Generate CycloneDX SBOM with Syft |
license_blocklist | list[string] | [] | SPDX license identifiers that produce a finding when found in the SBOM |
credentials_ref | string | null | Secret ref for registry password (e.g. env://REGISTRY_PASSWORD, aws_sm://my-secret) |
username_ref | string | null | Secret ref for registry username |
Registry Authentication¶
Public registries¶
No credentials needed. Leave credentials_ref and username_ref unset.
Private registries (generic)¶
Set REGISTRY_USERNAME and REGISTRY_PASSWORD environment variables on the scanner container, or use per-image credentials_ref / username_ref secret references.
AWS ECR¶
Use standard AWS IAM credentials. Authenticate with the ECR auth helper or pre-authenticate:
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 123456789.dkr.ecr.us-east-1.amazonaws.com
Or assign the AmazonEC2ContainerRegistryReadOnly IAM policy to the scanner's execution role.
Azure ACR¶
Create a service principal with the AcrPull role:
az ad sp create-for-rbac --name vsutra-scanner \
--role AcrPull \
--scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.ContainerRegistry/registries/<acr>
Store the password in a secret and reference it via credentials_ref.
GCP Artifact Registry¶
Create a service account with roles/artifactregistry.reader and provide the JSON key:
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:vsutra-scanner@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/artifactregistry.reader"
Store the JSON key content in a secret and reference it via credentials_ref.
Findings¶
CVE findings (N1 - Trivy)¶
| Field | Value |
|---|---|
resource_type | container:image |
tags.cve_id | CVE identifier (e.g. CVE-2024-12345) |
tags.package | Affected package name and version |
tags.fixed_version | Version that fixes the CVE (if available) |
tags.scanner | "trivy" |
| Compliance mappings | CIS Docker Benchmark, NIST SP 800-190 |
OS compliance findings (N2 - OpenSCAP)¶
The benchmark is chosen from the image's own /etc/os-release:
| Image OS | cis profile | stig profile |
|---|---|---|
| Debian 12, Debian 13 | CIS Level 1 Server | not supported |
| Ubuntu 22.04, Ubuntu 24.04 | CIS Level 1 Server | STIG |
| RHEL / UBI 8, 9, 10 | CIS Level 1 Server | STIG |
RHEL / UBI benchmarks read the image's RPM database. OpenSCAP does that only by chroot(), which the scanner (non-root, no capabilities) is not permitted. Such an image is therefore not evaluated in this release: it gets CKV_VSUTRA_IMG_OSCAP_2, and the OpenSCAP sub-scan fails as a coverage gap. Support for RHEL / UBI images is planned. Every other identified OS is not supported for OS compliance: Alpine, Debian 11, Ubuntu 20.04, Rocky, AlmaLinux, CentOS Stream, Amazon Linux, Oracle Linux, SLES, Fedora, and STIG on Debian. An image whose OS cannot be identified (no os-release, such as scratch or static images) fails the sub-scan.
An assessed image gets CKV_VSUTRA_IMG_OSCAP_1, plus CKV_VSUTRA_IMG_OSCAP_2 when some of its rules were not evaluated; an unsupported one gets CKV_VSUTRA_IMG_OSCAP_0; an image whose OS cannot be identified gets none. All are informational:
check_id | When | Tags |
|---|---|---|
CKV_VSUTRA_IMG_OSCAP_1 | The benchmark ran. Always present for an assessed image | openscap_status: assessed; rule counts (rules_evaluated, rules_passed, rules_failed, rules_undecided, rules_set_aside and one rules_set_aside_<class> per set-aside class below, rules_not_evaluated, rules_not_run_by_oscap, rules_evaluated_by_oscap); profile_id, benchmark, benchmark_version |
CKV_VSUTRA_IMG_OSCAP_0 | The OS is not supported | openscap_status: not_supported, os_id, os_version |
CKV_VSUTRA_IMG_OSCAP_2 | Rules were not evaluated because their checks need chroot(): the whole benchmark (RHEL / UBI) or single rules. The sub-scan fails | openscap_status: not_evaluated, not_evaluated_reason (rpm_database_needs_chroot or probe_needs_chroot), rules_selected, rules_evaluated, rules_not_evaluated |
Every rule that failed and could be judged on an image is one finding:
| Field | Value |
|---|---|
resource_type | container:os |
check_id | The XCCDF rule id, verbatim, e.g. xccdf_org.ssgproject.content_rule_accounts_umask_etc_profile. It is the same on every supported OS |
title | The benchmark's rule title, prefixed OS compliance: |
severity | The rule's XCCDF severity (info → informational, unknown → low) |
compliance_controls | The controls the rule's own references cite (CIS section, NIST 800-53, PCI DSS, and others) |
remediation_steps | The benchmark's shell remediation, when it has one. It is written for a running host: apply it where the image is built |
tags | rule_id, result, xccdf_severity, profile (cis / stig), profile_id, benchmark, benchmark_version, platform, scanner: "openscap" |
A rule OpenSCAP could not decide is a finding titled "OS compliance check could not be evaluated: …", at medium severity for error and informational for unknown. It is not a failure of the rule.
These are results for an image, not for a host. The image is unpacked without root and nothing is running in it. Four classes of rule are therefore set aside: they are neither passes nor failures, are counted on the summary, and their controls are recorded as not evaluated:
file_ownership: every unpacked file belongs to the scanner's own user.host_state: kernel parameters, services, mounts, the bootloader and similar.script_check: rules answered by a script that would run in the scanner, not in the image.not_checked: the benchmark ships no check for the rule.
0 failed on the summary never means "compliant": the set-aside rules have to be assessed on the host or cluster the image runs on.
OpenSCAP never downloads remote content, on any deployment profile. A rule whose check needs remote content is set aside as not checked, and its controls are recorded as not evaluated. In the shipped content (SSG 0.1.82), the only such rule is Debian's security-patch check, and no profile the scanner runs selects it.
License violation findings (N3 - Syft)¶
| Field | Value |
|---|---|
resource_type | package:{type} (e.g. package:npm, package:pip) |
severity | medium |
tags.license | SPDX license identifier |
tags.package | Package name and version |
tags.scanner | "syft" |
SBOM Artifacts¶
CycloneDX JSON SBOMs are written to SBOM_OUTPUT_DIR/{run_id}/{image_slug}.cdx.json (default: /tmp/vsutra-sboms/).
SBOMs are accessible via the API:
GET /runs/{run_id}/sbom- list available SBOM files for a runGET /runs/{run_id}/sbom/{filename}- download a specific CycloneDX SBOM file
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
REDIS_URL | required | Redis connection URL |
REGISTRY_USERNAME | - | Default registry username (overridden per image by username_ref) |
REGISTRY_PASSWORD | - | Default registry password (overridden per image by credentials_ref) |
SBOM_OUTPUT_DIR | /tmp/vsutra-sboms | Directory for SBOM artifact files |
SCAP_CONTENT_PATH | /usr/share/xml/scap/ssg/content | Directory holding the ssg-<product>-ds.xml datastreams; the one for the image's OS is chosen per image. A value that is not a directory (the old single-file form) fails the sub-scan |
SCANNER_SPAWNER_BACKEND | kubernetes | Spawner backend for sub-scanner Jobs |
SPAWNER_JOB_TTL_SECONDS | 600 | TTL for completed scan Jobs |
SPAWNER_K8S_NAMESPACE | release namespace | Namespace for spawned Jobs |
SPAWNER_SCANNER_CONTAINER_PULL_IMAGE | ghcr.io/virtual-sutra/scanner-pull:latest | Override pull sub-image |
SPAWNER_SCANNER_CONTAINER_TRIVY_IMAGE | ghcr.io/virtual-sutra/scanner-trivy:latest | Override Trivy sub-image |
SPAWNER_SCANNER_CONTAINER_SYFT_IMAGE | ghcr.io/virtual-sutra/scanner-syft:latest | Override Syft sub-image |
SPAWNER_SCANNER_CONTAINER_OPENSCAP_IMAGE | ghcr.io/virtual-sutra/scanner-openscap:latest | Override OpenSCAP sub-image |
LOG_LEVEL | INFO | Logging level |
Resource Types Summary¶
resource_type | Source |
|---|---|
container:image | Trivy CVE findings |
container:os | OpenSCAP: one finding per failing rule, plus the status records CKV_VSUTRA_IMG_OSCAP_0, _1 and _2 described above |
package:npm | Syft license violation (npm package) |
package:pip | Syft license violation (Python package) |
package:go | Syft license violation (Go module) |
package:java | Syft license violation (Java/Maven package) |