Modern supply chain security requires more than just scanning for CVEs. A mature container security workflow combines three layers of analysis: SBOM generation to know what's inside your images, vulnerability scanning to find known weaknesses, and malware detection to catch threats that CVE databases don't cover.
This tutorial builds a complete image assessment pipeline using open-source tools:
An SBOM (Software Bill of Materials) is a complete inventory of every component, library, and dependency inside your software — with versions and license information. Think of it as a nutrition label for your container image.
Without an SBOM, you're flying blind:
SBOMs turn these questions from multi-day investigations into instant database queries.
Standard SBOM formats:
| Format | Maintainer | Use case |
|---|---|---|
| SPDX | Linux Foundation | Compliance, license tracking, government requirements (required by US EO 14028) |
| CycloneDX | OWASP | Security-focused, vulnerability correlation, VEX support |
| Syft JSON | Anchore | Native format, maximum detail, integrates with Grype |
Vulnerability scans identify known security weaknesses by matching package versions against CVE databases. Integrating scans into CI/CD ensures that every image is checked before it reaches production.
Key principles:
CVE databases only cover known vulnerabilities. They don't catch:
YARA fills this gap. It uses pattern-matching rules to identify malware signatures, suspicious strings, and behavioral indicators inside container filesystems. The YaraHunter project from Deepfence packages YARA with a curated CI/CD ruleset for container scanning.
Syft is an open-source SBOM generator from Anchore. It scans container images and filesystems to produce detailed component inventories.
Key capabilities:
# Generate SBOM in SPDX JSON format
syft <image> -o spdx-json > sbom.spdx.json
# Generate SBOM in CycloneDX format
syft <image> -o cyclonedx-json > sbom.cdx.json
# Generate SBOM from a local directory
syft dir:/path/to/project -o spdx-json
Grype is Anchore's vulnerability scanner. It matches packages against aggregated vulnerability databases from Alpine SecDB, Debian CVE Tracker, Red Hat OVAL, NVD, and GitHub Security Advisories.
Key capabilities:
--fail-on critical for CI/CD gates# Scan an image directly
grype <image>
# Scan from an SBOM (faster, no image pull needed)
syft <image> -o json | grype
# Fail on high/critical findings (for CI/CD)
grype <image> --fail-on high
# Output as SARIF for GitHub integration
grype <image> -o sarif > results.sarif
YARA is the industry standard for malware research and classification. Rules describe patterns — byte sequences, strings, file metadata — that identify malware families.
In the container security context, YARA scans the filesystem layers of an image looking for:
This lab builds a complete scan-and-attest workflow: build an image, generate an SBOM, scan for vulnerabilities and malware, then attach all results as OCI artifacts alongside the image.
# Install ORAS (OCI Registry As Storage) CLI
VERSION="1.1.0"
curl -LO "https://github.com/oras-project/oras/releases/download/v${VERSION}/oras_${VERSION}_linux_amd64.tar.gz"
mkdir -p oras-install/
tar -zxf oras_${VERSION}_*.tar.gz -C oras-install/
sudo mv oras-install/oras /usr/local/bin/
rm -rf oras_${VERSION}_*.tar.gz oras-install/
# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Install Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
# Verify installations
syft version
grype version
oras version
A local OCI registry lets you push, scan, and attach artifacts without needing Docker Hub credentials:
# Start a local OCI-compliant registry
docker run -d --restart=always -p "127.0.0.1:5000:5000" --name reg registry:2
We'll intentionally include the EICAR test file to trigger YARA malware rules — this validates that the scanning pipeline actually catches threats.
# Download EICAR test file (standard AV test pattern)
curl -O https://secure.eicar.org/eicar.com.txt
# Create a Dockerfile with the test file
cat <<'EOF' > Dockerfile
FROM alpine:3.10
WORKDIR /app
COPY ./eicar.com.txt /app/
CMD ["sh"]
EOF
# Build and push to local registry
docker build -t localhost:5000/alpine-test:0.1 .
docker push localhost:5000/alpine-test:0.1
Note: We're using alpine:3.10 intentionally — it's an older version with known CVEs, which makes the vulnerability scan results more interesting.
# Generate SBOM in SPDX JSON format
syft localhost:5000/alpine-test:0.1 -o spdx-json > sbom.spdx.json
# View a summary of discovered packages
syft localhost:5000/alpine-test:0.1
The SBOM output lists every package in the image with version, type, and license information. For alpine:3.10, you'll see packages like musl, busybox, apk-tools, zlib, and their exact versions.
# Scan the image directly
grype localhost:5000/alpine-test:0.1
# Or scan from the SBOM (faster for repeated scans)
grype sbom:sbom.spdx.json
# Generate a report file
grype localhost:5000/alpine-test:0.1 -o json > vuln-scan.json
# For CI/CD: fail on high/critical
grype localhost:5000/alpine-test:0.1 --fail-on high
With alpine:3.10, you'll see multiple CVEs in packages like musl, busybox, and libcrypto. The scan output shows:
# Run YaraHunter against the image
docker run -i --rm \
--name=deepfence-yarahunter \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /tmp:/home/deepfence/output \
deepfenceio/deepfence_malware_scanner_ce:2.0.0 \
--image-name localhost:5000/alpine-test:0.1 \
--output=json > malware-scan.json
# Check results
cat malware-scan.json | python3 -m json.tool | head -50
The EICAR test file should trigger a detection. In a real pipeline, any malware finding should immediately fail the build and trigger an incident response process.
OCI artifacts let you store scan results alongside the image in the registry. Anyone pulling the image can also pull its SBOM, vulnerability report, and malware scan results.
# Attach SBOM to the image
oras attach localhost:5000/alpine-test:0.1 \
sbom.spdx.json --artifact-type application/spdx+json
# Attach vulnerability scan report
oras attach localhost:5000/alpine-test:0.1 \
vuln-scan.json --artifact-type application/vnd.security.vuln+json
# Attach malware scan results
oras attach localhost:5000/alpine-test:0.1 \
malware-scan.json --artifact-type application/vnd.security.malware+json
# View the full supply chain artifact tree
oras discover localhost:5000/alpine-test:0.1 -o tree
The oras discover output shows all artifacts attached to the image — your SBOM, vulnerability report, and malware scan are now permanently linked to the image digest.
A Makefile ties the entire workflow together. Each target maps to a pipeline stage that can be called from Jenkins, GitLab CI, or GitHub Actions:
REG_URL := localhost:5000
NAME := alpine-test
VERSION := 0.1
.PHONY: build push scan-cve scan-sbom scan-malware attach-artifacts sign verify help
build: ## Build container image
@docker build . -t $(REG_URL)/$(NAME):$(VERSION)
push: ## Push to registry
@docker push $(REG_URL)/$(NAME):$(VERSION)
scan-sbom: ## Generate SBOM with Syft
@syft $(REG_URL)/$(NAME):$(VERSION) -o spdx-json > sbom.spdx.json
@echo "SBOM saved to sbom.spdx.json"
scan-cve: ## Vulnerability scan with Grype (fails on HIGH/CRITICAL)
@grype $(REG_URL)/$(NAME):$(VERSION) --fail-on high -o json > vuln-scan.json
scan-malware: ## Malware scan with YaraHunter
@docker run -i --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /tmp:/home/deepfence/output \
deepfenceio/deepfence_malware_scanner_ce:2.0.0 \
--image-name $(REG_URL)/$(NAME):$(VERSION) \
--output=json > malware-scan.json
scan-all: scan-sbom scan-cve scan-malware ## Run all scans
attach-artifacts: ## Attach SBOM and scan results as OCI artifacts
@oras attach $(REG_URL)/$(NAME):$(VERSION) sbom.spdx.json --artifact-type application/spdx+json
@oras attach $(REG_URL)/$(NAME):$(VERSION) vuln-scan.json --artifact-type application/vnd.security.vuln+json
@oras attach $(REG_URL)/$(NAME):$(VERSION) malware-scan.json --artifact-type application/vnd.security.malware+json
@echo "All artifacts attached"
sign: ## Sign image with Cosign
@cosign sign --key ~/.sigstore/cosign.key $(REG_URL)/$(NAME):$(VERSION)
verify: ## Verify image signature
@cosign verify --key cosign.pub $(REG_URL)/$(NAME):$(VERSION)
pipeline: build push scan-all attach-artifacts ## Run full pipeline
@echo "Pipeline complete"
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
Run the full pipeline with a single command:
make pipeline
Or run individual stages:
make build
make push
make scan-all
make attach-artifacts
Build-time scanning is necessary but not sufficient. New CVEs are published daily — an image that was clean at build time may have critical vulnerabilities discovered weeks later.
Strategies for continuous scanning:
grype against all images in your registry on a daily cron job# Example: scan all images in a local registry
for tag in $(oras repo tags localhost:5000/alpine-test); do
echo "Scanning alpine-test:$tag..."
grype localhost:5000/alpine-test:$tag --fail-on critical || echo "CRITICAL CVEs found in $tag"
done
A complete container security assessment combines three layers:
| Layer | Tool | What it catches |
|---|---|---|
| Inventory | Syft | All packages, libraries, and dependencies with versions |
| Vulnerabilities | Grype | Known CVEs matched against vulnerability databases |
| Malware | YARA/YaraHunter | Malware signatures, suspicious patterns, backdoors |
By generating SBOMs, running vulnerability and malware scans, and attaching results as OCI artifacts, you create a verifiable chain of evidence for every image in your supply chain. This multi-layered approach is essential for maintaining security in modern container environments.