Docker SBOM Guide for Container Images and Security
Table of Contents
- Introduction
- Understanding Container SBOMs
- Docker SBOM Fundamentals
- Essential Tools for Container SBOMs
- Syft for Docker Images
- Multi-Stage Build SBOM Generation
- Distroless and Minimal Images
- Language-Specific Container SBOMs
- Container Registry Integration
- CI/CD Pipeline Integration
- Security Scanning & Vulnerability Management
- Enterprise Container SBOM Management
- Best Practices
- Troubleshooting
Introduction
Container Software Bills of Materials (SBOMs) give you a defensible inventory of what is actually inside a Docker image: base OS packages, runtime libraries, application dependencies, certificates, and build artifacts. That visibility matters because container risk rarely comes from just one application package; it comes from the full image stack and the way it moves through registries and deployment pipelines.
This guide focuses on the operational side of Docker SBOMs: how to generate them with tools like Syft, when to export CycloneDX vs SPDX, how to validate output with the SBOM Validator, and how to keep container SBOMs useful in CI/CD instead of producing one-off artifacts no one checks.Why Container SBOMs Matter
Container images contain multiple layers of dependencies:
- Base operating system packages
- Runtime dependencies
- Application libraries and frameworks
- Development and build tools (in multi-stage builds)
- Configuration files and certificates
Container SBOMs help organizations:
- Track vulnerabilities across the entire container stack
- Meet compliance requirements for containerized applications
- Implement security policies based on component inventory
- Respond quickly to software supply chain security incidents
- Optimize image size by identifying unnecessary components
Understanding Container SBOMs
Container SBOM Scope and Layers
Image Layer Analysis# Examine Docker image layers
docker history myapp:latest --no-trunc --format "table {{.CreatedBy}}\t{{.Size}}\t{{.CreatedSince}}"
# Inspect specific layer contents
docker save myapp:latest | tar -tv | head -20- Image SBOM: Complete inventory of container image contents
- Runtime SBOM: Components available during container execution
- Build SBOM: Dependencies used during image construction
- Base Layer SBOM: Operating system and foundational components
Container-Specific Challenges
1. Layer Deduplication# Layers may contain duplicate packages
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y curl # Layer 1
RUN apt-get install -y wget # Layer 2 (may duplicate dependencies)# Build stage dependencies vs runtime dependencies
FROM node:18 as build
RUN npm install # Build dependencies
FROM nginx:alpine as runtime
COPY --from=build /app/dist /usr/share/nginx/html # Only runtime artifacts# Inherited vulnerabilities and dependencies
FROM ubuntu:22.04 # Inherits all Ubuntu packages
# Your application adds more componentsDocker SBOM Fundamentals
Basic Docker SBOM Generation
Quick SBOM Generation# Generate SBOM for local image
[Syft](/tools/syft) myapp:latest -o spdx-json=myapp-sbom.spdx.json
# Generate SBOM for remote image
syft ubuntu:22.04 -o [CycloneDX](/tools/cyclonedx-cli)-json=ubuntu-sbom.json
# Analyze running container
docker ps
syft $(docker inspect <container-id> -f '{{.Config.Image}}') -o json#!/bin/bash
# docker-sbom-comprehensive.sh
set -euo pipefail
IMAGE_NAME="${1:-}"
OUTPUT_DIR="${2:-./docker-sbom-analysis}"
if [ -z "$IMAGE_NAME" ]; then
echo "Usage: $0 <image-name> [output-dir]"
exit 1
fi
echo "=== Comprehensive Docker SBOM Analysis ==="
echo "Image: $IMAGE_NAME"
echo "Output: $OUTPUT_DIR"
# Create output structure
mkdir -p "$OUTPUT_DIR"/{sboms,layers,security,metadata}
# 1. Basic SBOM generation
echo "1. Generating comprehensive SBOMs..."
# Multiple format generation
syft "$IMAGE_NAME" -o spdx-json="$OUTPUT_DIR/sboms/complete.spdx.json" -v
syft "$IMAGE_NAME" -o cyclonedx-json="$OUTPUT_DIR/sboms/complete.cyclonedx.json" -v
syft "$IMAGE_NAME" -o syft-json="$OUTPUT_DIR/sboms/complete.syft.json" -v
# 2. Layer-by-layer analysis
echo "2. Analyzing image layers..."
# Extract layer information
docker history "$IMAGE_NAME" --no-trunc --format "json" > "$OUTPUT_DIR/layers/history.json"
# Create temporary directory for layer extraction
TEMP_DIR=$(mktemp -d)
docker save "$IMAGE_NAME" | tar -xC "$TEMP_DIR"
# Analyze each layer
if [ -f "$TEMP_DIR/manifest.json" ]; then
layers=$(jq -r '.[0].Layers[]' "$TEMP_DIR/manifest.json")
layer_num=0
for layer in $layers; do
echo " Analyzing layer $layer_num: $layer"
# Extract layer contents
layer_dir="$TEMP_DIR/layer_$layer_num"
mkdir -p "$layer_dir"
tar -xf "$TEMP_DIR/$layer" -C "$layer_dir" 2>/dev/null || echo " Layer extraction failed"
# Generate SBOM for layer if it contains packages
if [ -d "$layer_dir" ]; then
syft "dir:$layer_dir" -o json="$OUTPUT_DIR/layers/layer-$layer_num-sbom.json" 2>/dev/null || echo " No packages in layer"
fi
layer_num=$((layer_num + 1))
done
fi
# Cleanup temp directory
rm -rf "$TEMP_DIR"
# 3. Image metadata extraction
echo "3. Extracting image metadata..."
docker inspect "$IMAGE_NAME" > "$OUTPUT_DIR/metadata/inspect.json"
# Extract environment variables, labels, and configuration
jq '.[] | {
Config: .Config,
Architecture: .Architecture,
Os: .Os,
Size: .Size,
VirtualSize: .VirtualSize,
GraphDriver: .GraphDriver,
RootFS: .RootFS
}' "$OUTPUT_DIR/metadata/inspect.json" > "$OUTPUT_DIR/metadata/image-config.json"
# 4. Security analysis
echo "4. Running security analysis..."
# Vulnerability scanning with multiple tools
if command -v trivy &> /dev/null; then
trivy image --format json --output "$OUTPUT_DIR/security/trivy-vulns.json" "$IMAGE_NAME"
trivy image --format table --output "$OUTPUT_DIR/security/trivy-summary.txt" "$IMAGE_NAME"
fi
if command -v grype &> /dev/null; then
grype "$IMAGE_NAME" -o json > "$OUTPUT_DIR/security/grype-vulns.json"
grype "$IMAGE_NAME" -o table > "$OUTPUT_DIR/security/grype-summary.txt"
fi
# 5. Generate analysis summary
echo "5. Generating analysis summary..."
cat > "$OUTPUT_DIR/analysis-summary.md" << EOF
# Docker SBOM Analysis Summary
**Image:** \`$IMAGE_NAME\`
**Generated:** $(date)
**Analysis Directory:** \`$OUTPUT_DIR\`
## Image Information
- **Size:** \$(docker images "$IMAGE_NAME" --format "{{.Size}}")
- **Created:** \$(docker inspect "$IMAGE_NAME" -f "{{.Created}}")
- **Architecture:** \$(docker inspect "$IMAGE_NAME" -f "{{.Architecture}}")
- **OS:** \$(docker inspect "$IMAGE_NAME" -f "{{.Os}}")
## SBOM Files Generated
- Complete SPDX SBOM: \`sboms/complete.spdx.json\`
- Complete CycloneDX SBOM: \`sboms/complete.cyclonedx.json\`
- Syft Native SBOM: \`sboms/complete.syft.json\`
## Layer Analysis
- Image layers analyzed: \$(docker history "$IMAGE_NAME" --format "table" | wc -l | awk '{print \$1-1}')
- Layer SBOMs: \`layers/layer-*-sbom.json\`
- Layer history: \`layers/history.json\`
## Security Analysis
$(if [ -f "$OUTPUT_DIR/security/trivy-vulns.json" ]; then
echo "- Trivy vulnerabilities: \$(jq '.Results[0].Vulnerabilities | length' "$OUTPUT_DIR/security/trivy-vulns.json" 2>/dev/null || echo "0")"
fi)
$(if [ -f "$OUTPUT_DIR/security/grype-vulns.json" ]; then
echo "- Grype vulnerabilities: \$(jq '.matches | length' "$OUTPUT_DIR/security/grype-vulns.json" 2>/dev/null || echo "0")"
fi)
## Package Summary
$(jq -r '.artifacts[] | "\(.name)@\(.version) (\(.type))"' "$OUTPUT_DIR/sboms/complete.syft.json" | sort | uniq -c | sort -rn | head -10 | awk '{print "- " $2 " " $3 " (found " $1 " times)"}')
## Recommendations
1. Review high-severity vulnerabilities in security reports
2. Consider using distroless base images to reduce attack surface
3. Implement regular image scanning in [CI/CD](/guides/ci-cd) pipeline
4. Pin specific package versions for reproducible builds
EOF
# 6. Package statistics
echo "6. Generating package statistics..."
python3 << 'PYTHON_SCRIPT' > "$OUTPUT_DIR/package-statistics.json"
import json
import sys
from collections import defaultdict, Counter
try:
with open(sys.argv[1] + '/sboms/complete.syft.json', 'r') as f:
sbom = json.load(f)
stats = {
'total_packages': len(sbom.get('artifacts', [])),
'packages_by_type': defaultdict(int),
'packages_by_language': defaultdict(int),
'top_package_sources': [],
'license_distribution': defaultdict(int)
}
for artifact in sbom.get('artifacts', []):
# Package type distribution
pkg_type = artifact.get('type', 'unknown')
stats['packages_by_type'][pkg_type] += 1
# Language distribution
language = artifact.get('language', 'unknown')
if language != 'unknown':
stats['packages_by_language'][language] += 1
# License distribution
for license in artifact.get('licenses', []):
stats['license_distribution'][license] += 1
# Convert defaultdicts to regular dicts for JSON serialization
stats['packages_by_type'] = dict(stats['packages_by_type'])
stats['packages_by_language'] = dict(stats['packages_by_language'])
stats['license_distribution'] = dict(stats['license_distribution'])
print(json.dumps(stats, indent=2))
except Exception as e:
print(json.dumps({'error': str(e)}))
PYTHON_SCRIPT
echo ""
echo "=== Docker SBOM Analysis Complete ==="
echo "Analysis results saved to: $OUTPUT_DIR"
echo "Summary report: $OUTPUT_DIR/analysis-summary.md"
echo ""
echo "Quick stats:"
if [ -f "$OUTPUT_DIR/sboms/complete.syft.json" ]; then
echo "- Total packages: $(jq '.artifacts | length' "$OUTPUT_DIR/sboms/complete.syft.json")"
echo "- Package types: $(jq -r '.artifacts[].type' "$OUTPUT_DIR/sboms/complete.syft.json" | sort | uniq -c | awk '{print $2":"$1}' | tr '\n' ' ')"
fiEssential Tools for Container SBOMs
Syft - The Container SBOM Standard
Advanced Syft Configuration# .syft.yaml - Production container scanning configuration
output:
- "spdx-json=container-sbom.spdx.json"
- "cyclonedx-json=container-sbom.cyclonedx.json"
- "table=container-summary.txt"
# Enable all catalogers for comprehensive analysis
catalogers:
enabled:
- alpmdb-cataloger
- apkdb-cataloger
- binary-cataloger
- dotnet-deps-cataloger
- dpkgdb-cataloger
- go-module-binary-cataloger
- go-mod-file-cataloger
- java-cataloger
- javascript-lock-cataloger
- php-composer-installed-cataloger
- python-package-cataloger
- rpm-db-cataloger
- ruby-gemfile-cataloger
- rust-cargo-lock-cataloger
# Container-specific settings
package:
cataloger:
enabled: true
scope: "all-layers"
# File analysis
file:
cataloger:
enabled: true
scope: "all-layers"
# Registry authentication
registry:
insecure-skip-tls-verify: false
insecure-use-http: false
auth:
- authority: "registry.company.com"
username: "${REGISTRY_USERNAME}"
password: "${REGISTRY_PASSWORD}"Trivy - Container Security Scanner
Comprehensive Trivy Analysis#!/bin/bash
# trivy-comprehensive-scan.sh
IMAGE_NAME="$1"
OUTPUT_DIR="${2:-./trivy-analysis}"
mkdir -p "$OUTPUT_DIR"
echo "=== Comprehensive Trivy Analysis ==="
# 1. SBOM Generation
echo "1. Generating SBOM with Trivy..."
trivy image --format spdx-json --output "$OUTPUT_DIR/trivy-sbom.spdx.json" "$IMAGE_NAME"
trivy image --format cyclonedx --output "$OUTPUT_DIR/trivy-sbom.cyclonedx.json" "$IMAGE_NAME"
# 2. Vulnerability Scanning
echo "2. Vulnerability scanning..."
trivy image --format json --output "$OUTPUT_DIR/vulnerabilities.json" "$IMAGE_NAME"
trivy image --format table --output "$OUTPUT_DIR/vulnerabilities.txt" "$IMAGE_NAME"
# 3. Secret Scanning
echo "3. Scanning for secrets..."
trivy image --scanners secret --format json --output "$OUTPUT_DIR/secrets.json" "$IMAGE_NAME"
# 4. Configuration Scanning
echo "4. Configuration analysis..."
trivy image --scanners config --format json --output "$OUTPUT_DIR/config-issues.json" "$IMAGE_NAME"
# 5. License Scanning
echo "5. License analysis..."
trivy image --scanners license --format json --output "$OUTPUT_DIR/licenses.json" "$IMAGE_NAME"
# 6. Combined Analysis Report
echo "6. Generating combined report..."
trivy image --format json --output "$OUTPUT_DIR/complete-analysis.json" "$IMAGE_NAME"
echo "Analysis complete. Results in: $OUTPUT_DIR"Grype - Vulnerability Scanner
Container-Focused Grype Usage# Scan container image
grype myapp:latest -o json > container-vulnerabilities.json
# Scan with specific distro detection
grype myapp:latest --distro ubuntu:22.04 -o table
# Generate SBOM and scan in one workflow
syft myapp:latest -o cyclonedx-json=sbom.json
grype sbom:sbom.json -o tableSyft for Docker Images
Advanced Syft Container Analysis
Production Syft Container Scanning#!/usr/bin/env python3
"""
Production Container SBOM Generator using Syft
Handles multiple images, registries, and output formats
"""
import subprocess
import json
import argparse
import logging
from pathlib import Path
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed
class ProductionContainerSBOMGenerator:
def __init__(self, config_file=None):
self.logger = self._setup_logging()
self.config = self._load_config(config_file)
def _setup_logging(self):
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
return logging.getLogger(__name__)
def _load_config(self, config_file):
"""Load configuration for production scanning"""
default_config = {
"output_formats": ["spdx-json", "cyclonedx-json"],
"registries": {
"docker.io": {"auth_required": False},
"registry.company.com": {
"auth_required": True,
"username_env": "COMPANY_REGISTRY_USERNAME",
"password_env": "COMPANY_REGISTRY_PASSWORD"
}
},
"scanning": {
"include_layers": True,
"include_files": True,
"parallel_scans": 4,
"timeout_minutes": 30
},
"vulnerability_scanning": {
"enabled": True,
"tools": ["grype", "trivy"]
}
}
if config_file and Path(config_file).exists():
with open(config_file) as f:
user_config = json.load(f)
default_config.update(user_config)
return default_config
def generate_container_sbom(self, image_name, output_dir="./container-sboms"):
"""Generate comprehensive SBOM for container image"""
output_path = Path(output_dir) / self._sanitize_image_name(image_name)
output_path.mkdir(parents=True, exist_ok=True)
self.logger.info(f"Generating SBOM for: {image_name}")
self.logger.info(f"Output directory: {output_path}")
results = {
"image": image_name,
"timestamp": datetime.utcnow().isoformat() + "Z",
"sbom_files": [],
"vulnerability_reports": [],
"metadata": {}
}
# 1. Generate SBOMs in multiple formats
for fmt in self.config["output_formats"]:
sbom_file = output_path / f"sbom.{fmt.replace('-', '.')}"
try:
cmd = [
"syft", "packages", image_name,
"-o", f"{fmt}={sbom_file}",
"--config", "/dev/null" # Use default config
]
if self.config["scanning"]["include_layers"]:
cmd.extend(["--scope", "all-layers"])
self.logger.info(f"Running: {' '.join(cmd)}")
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=self.config["scanning"]["timeout_minutes"] * 60
)
if result.returncode == 0:
results["sbom_files"].append(str(sbom_file))
self.logger.info(f"✓ Generated {fmt} SBOM: {sbom_file}")
else:
self.logger.error(f"✗ Failed to generate {fmt} SBOM: {result.stderr}")
except subprocess.TimeoutExpired:
self.logger.error(f"✗ Timeout generating {fmt} SBOM for {image_name}")
except Exception as e:
self.logger.error(f"✗ Error generating {fmt} SBOM: {e}")
# 2. Extract image metadata
try:
metadata = self._extract_image_metadata(image_name)
results["metadata"] = metadata
with open(output_path / "metadata.json", 'w') as f:
json.dump(metadata, f, indent=2)
except Exception as e:
self.logger.error(f"Failed to extract metadata: {e}")
# 3. Run vulnerability scanning
if self.config["vulnerability_scanning"]["enabled"]:
for tool in self.config["vulnerability_scanning"]["tools"]:
try:
vuln_file = self._run_vulnerability_scan(image_name, tool, output_path)
if vuln_file:
results["vulnerability_reports"].append(str(vuln_file))
except Exception as e:
self.logger.error(f"Vulnerability scan failed with {tool}: {e}")
# 4. Generate analysis summary
summary_file = output_path / "analysis-summary.json"
with open(summary_file, 'w') as f:
json.dump(results, f, indent=2)
self.logger.info(f"✓ Complete analysis saved to: {output_path}")
return results
def batch_generate_sboms(self, image_list, output_dir="./batch-container-sboms"):
"""Generate SBOMs for multiple images in parallel"""
self.logger.info(f"Starting batch SBOM generation for {len(image_list)} images")
results = []
failed_images = []
with ThreadPoolExecutor(max_workers=self.config["scanning"]["parallel_scans"]) as executor:
# Submit all tasks
future_to_image = {
executor.submit(self.generate_container_sbom, image, output_dir): image
for image in image_list
}
# Process completed tasks
for future in as_completed(future_to_image):
image = future_to_image[future]
try:
result = future.result()
results.append(result)
self.logger.info(f"✓ Completed: {image}")
except Exception as e:
failed_images.append({"image": image, "error": str(e)})
self.logger.error(f"✗ Failed: {image} - {e}")
# Generate batch summary
batch_summary = {
"total_images": len(image_list),
"successful": len(results),
"failed": len(failed_images),
"results": results,
"failures": failed_images,
"timestamp": datetime.utcnow().isoformat() + "Z"
}
summary_file = Path(output_dir) / "batch-summary.json"
with open(summary_file, 'w') as f:
json.dump(batch_summary, f, indent=2)
self.logger.info(f"Batch processing complete: {len(results)} successful, {len(failed_images)} failed")
return batch_summary
def _extract_image_metadata(self, image_name):
"""Extract comprehensive image metadata"""
cmd = ["docker", "inspect", image_name]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"Failed to inspect image: {result.stderr}")
inspect_data = json.loads(result.stdout)[0]
# Extract relevant metadata
metadata = {
"id": inspect_data.get("Id"),
"created": inspect_data.get("Created"),
"architecture": inspect_data.get("Architecture"),
"os": inspect_data.get("Os"),
"size": inspect_data.get("Size"),
"virtual_size": inspect_data.get("VirtualSize"),
"config": {
"env": inspect_data.get("Config", {}).get("Env", []),
"labels": inspect_data.get("Config", {}).get("Labels", {}),
"exposed_ports": list(inspect_data.get("Config", {}).get("ExposedPorts", {}).keys()),
"volumes": list(inspect_data.get("Config", {}).get("Volumes", {}).keys()),
"working_dir": inspect_data.get("Config", {}).get("WorkingDir"),
"user": inspect_data.get("Config", {}).get("User"),
"entrypoint": inspect_data.get("Config", {}).get("Entrypoint"),
"cmd": inspect_data.get("Config", {}).get("Cmd")
},
"rootfs": inspect_data.get("RootFS"),
"layers": len(inspect_data.get("RootFS", {}).get("Layers", []))
}
return metadata
def _run_vulnerability_scan(self, image_name, tool, output_path):
"""Run vulnerability scanning with specified tool"""
if tool == "grype":
output_file = output_path / "vulnerabilities-grype.json"
cmd = ["grype", image_name, "-o", "json"]
elif tool == "trivy":
output_file = output_path / "vulnerabilities-trivy.json"
cmd = ["trivy", "image", "--format", "json", image_name]
else:
self.logger.warning(f"Unknown vulnerability scanning tool: {tool}")
return None
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
if result.returncode == 0:
with open(output_file, 'w') as f:
f.write(result.stdout)
self.logger.info(f"✓ Vulnerability scan completed with {tool}: {output_file}")
return output_file
else:
self.logger.error(f"✗ {tool} scan failed: {result.stderr}")
except subprocess.TimeoutExpired:
self.logger.error(f"✗ {tool} scan timeout for {image_name}")
except Exception as e:
self.logger.error(f"✗ {tool} scan error: {e}")
return None
def _sanitize_image_name(self, image_name):
"""Convert image name to filesystem-safe directory name"""
return image_name.replace(':', '_').replace('/', '_').replace('@', '_')
def main():
parser = argparse.ArgumentParser(description="Production Container SBOM Generator")
parser.add_argument("images", nargs="+", help="Container images to analyze")
parser.add_argument("-o", "--output", default="./container-sboms", help="Output directory")
parser.add_argument("-c", "--config", help="Configuration file")
parser.add_argument("--batch", action="store_true", help="Process images in parallel")
args = parser.parse_args()
generator = ProductionContainerSBOMGenerator(args.config)
if args.batch and len(args.images) > 1:
generator.batch_generate_sboms(args.images, args.output)
else:
for image in args.images:
generator.generate_container_sbom(image, args.output)
if __name__ == "__main__":
main()Multi-Stage Build SBOM Generation
Tracking Dependencies Across Build Stages
Advanced Multi-Stage Dockerfile with SBOM Integration# syntax=docker/dockerfile:1.4
# Multi-stage build with comprehensive SBOM tracking
# Build stage with full development environment
FROM node:18-alpine AS build-env
# Install SBOM generation tools in build stage
RUN apk add --no-cache curl bash
RUN curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
WORKDIR /app
# Copy package files first for better caching
COPY package*.json ./
RUN npm ci --only=production
# Generate build dependencies SBOM
RUN syft dir:node_modules -o spdx-json=/tmp/build-deps-sbom.spdx.json
# Copy source code and build
COPY src/ src/
COPY tsconfig.json ./
RUN npm run build
# Generate complete build SBOM including source
RUN syft dir:. -o cyclonedx-json=/tmp/build-complete-sbom.json
# Testing stage
FROM build-env AS test
RUN npm ci # Install all dependencies including dev
RUN npm test
RUN npm run lint
# Generate test dependencies SBOM
RUN syft dir:node_modules -o json=/tmp/test-deps-sbom.json
# Security scanning stage
FROM build-env AS security-scan
RUN apk add --no-cache curl
RUN curl -sSfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Scan build dependencies
RUN trivy fs --format spdx-json --output /tmp/security-scan.spdx.json /app
# Production runtime stage
FROM nginx:alpine AS runtime
# Install SBOM tools in runtime for final analysis
RUN apk add --no-cache curl bash
RUN curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
# Copy application artifacts
COPY --from=build-env /app/dist /usr/share/nginx/html
# Copy SBOMs from build stages
COPY --from=build-env /tmp/*-sbom.* /opt/sboms/
COPY --from=test /tmp/*-sbom.* /opt/sboms/
COPY --from=security-scan /tmp/security-scan.spdx.json /opt/sboms/
# Custom nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Generate final runtime SBOM
RUN syft dir:/ -o spdx-json=/opt/sboms/runtime-final.spdx.json
# Generate combined SBOM
RUN apk add --no-cache nodejs npm && \
npm install -g @cyclonedx/cli && \
cyclonedx-cli merge -i /opt/sboms/*.json -o /opt/sboms/combined-sbom.json && \
apk del nodejs npm
# Create SBOM metadata
RUN echo "{\
\"sbom_generation\": {\
\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\
\"stages\": [\"build-env\", \"test\", \"security-scan\", \"runtime\"],\
\"files\": [\
\"/opt/sboms/build-deps-sbom.spdx.json\",\
\"/opt/sboms/build-complete-sbom.json\",\
\"/opt/sboms/test-deps-sbom.json\",\
\"/opt/sboms/security-scan.spdx.json\",\
\"/opt/sboms/runtime-final.spdx.json\",\
\"/opt/sboms/combined-sbom.json\"\
]\
}\
}" > /opt/sboms/metadata.json
# Health check that includes SBOM verification
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost/ && test -f /opt/sboms/combined-sbom.json || exit 1
EXPOSE 80
# Custom entrypoint that can serve SBOMs via API
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]#!/bin/bash
# docker-entrypoint.sh - Enhanced entrypoint with SBOM serving capability
set -e
# Function to start SBOM API server
start_sbom_api() {
cat > /tmp/sbom-server.py << 'EOF'
#!/usr/bin/env python3
import http.server
import socketserver
import json
import os
from urllib.parse import urlparse, parse_qs
class SBOMHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory="/opt/sboms", **kwargs)
def do_GET(self):
parsed_path = urlparse(self.path)
if parsed_path.path == '/api/sbom/list':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
sbom_files = []
for file in os.listdir('/opt/sboms'):
if file.endswith(('.json', '.spdx.json')):
sbom_files.append({
'name': file,
'size': os.path.getsize(f'/opt/sboms/{file}'),
'type': 'spdx' if 'spdx' in file else 'cyclonedx'
})
self.wfile.write(json.dumps({
'sboms': sbom_files,
'total': len(sbom_files)
}).encode())
elif parsed_path.path == '/api/health':
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps({
'status': 'healthy',
'sboms_available': len([f for f in os.listdir('/opt/sboms') if f.endswith('.json')])
}).encode())
else:
super().do_GET()
PORT = 8080
Handler = SBOMHandler
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"SBOM API server running on port {PORT}")
httpd.serve_forever()
EOF
python3 /tmp/sbom-server.py &
SBOM_API_PID=$!
echo "Started SBOM API server (PID: $SBOM_API_PID)"
}
# Function to validate SBOMs
validate_sboms() {
echo "Validating generated SBOMs..."
for sbom_file in /opt/sboms/*.json; do
if [ -f "$sbom_file" ]; then
if jq empty "$sbom_file" 2>/dev/null; then
echo "✓ Valid JSON: $(basename "$sbom_file")"
else
echo "✗ Invalid JSON: $(basename "$sbom_file")"
fi
fi
done
}
# Function to generate runtime metrics
generate_metrics() {
cat > /opt/sboms/runtime-metrics.json << EOF
{
"container_startup": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"sbom_files_count": $(ls -1 /opt/sboms/*.json 2>/dev/null | wc -l),
"total_sbom_size": $(du -sb /opt/sboms/ | cut -f1),
"environment": {
"hostname": "$(hostname)",
"platform": "$(uname -m)",
"os": "$(cat /etc/os-release | grep PRETTY_NAME | cut -d'"' -f2)"
}
}
EOF
}
# Main execution
echo "=== Container Startup with SBOM Integration ==="
# Validate SBOMs
validate_sboms
# Generate runtime metrics
generate_metrics
# Start SBOM API if requested
if [ "${ENABLE_SBOM_API:-false}" = "true" ]; then
start_sbom_api
fi
# Execute the main command
exec "$@"#!/bin/bash
# analyze-multistage-build.sh
DOCKERFILE="${1:-Dockerfile}"
IMAGE_NAME="${2:-myapp:latest}"
if [ ! -f "$DOCKERFILE" ]; then
echo "Dockerfile not found: $DOCKERFILE"
exit 1
fi
echo "=== Multi-Stage Build SBOM Analysis ==="
# Extract build stages
echo "1. Analyzing build stages..."
stages=$(grep -n "^FROM.*AS" "$DOCKERFILE" | cut -d: -f2 | awk '{print $NF}')
if [ -z "$stages" ]; then
echo "No multi-stage build detected"
stages="single-stage"
fi
echo "Detected stages: $stages"
# Build image with build-time SBOM generation
echo "2. Building image with SBOM generation..."
docker build -t "$IMAGE_NAME" -f "$DOCKERFILE" .
# Extract SBOMs from built image
echo "3. Extracting SBOMs from image..."
mkdir -p ./multistage-analysis
# Create temporary container to extract SBOMs
container_id=$(docker create "$IMAGE_NAME")
docker cp "$container_id:/opt/sboms/." ./multistage-analysis/ 2>/dev/null || echo "No /opt/sboms found in image"
docker rm "$container_id"
# Generate external SBOM for comparison
echo "4. Generating external SBOM for comparison..."
syft "$IMAGE_NAME" -o spdx-json=./multistage-analysis/external-sbom.spdx.json
# Analyze differences between build stages
echo "5. Analyzing stage differences..."
python3 << 'PYTHON_SCRIPT'
import json
import os
from pathlib import Path
analysis_dir = Path("./multistage-analysis")
sbom_files = list(analysis_dir.glob("*.json"))
print(f"Found {len(sbom_files)} SBOM files")
stage_components = {}
for sbom_file in sbom_files:
stage_name = sbom_file.stem
try:
with open(sbom_file) as f:
sbom = json.load(f)
# Extract components based on SBOM format
components = []
if 'artifacts' in sbom: # Syft format
components = [(c.get('name', ''), c.get('version', ''), c.get('type', '')) for c in sbom['artifacts']]
elif 'components' in sbom: # CycloneDX format
components = [(c.get('name', ''), c.get('version', ''), c.get('type', '')) for c in sbom['components']]
elif 'packages' in sbom: # SPDX format
components = [(c.get('name', ''), c.get('versionInfo', ''), 'package') for c in sbom['packages']]
stage_components[stage_name] = set(components)
print(f"{stage_name}: {len(components)} components")
except Exception as e:
print(f"Error processing {sbom_file}: {e}")
# Compare stages
if len(stage_components) > 1:
print("\nStage Comparisons:")
stages = list(stage_components.keys())
for i, stage1 in enumerate(stages):
for stage2 in stages[i+1:]:
common = stage_components[stage1] & stage_components[stage2]
unique1 = stage_components[stage1] - stage_components[stage2]
unique2 = stage_components[stage2] - stage_components[stage1]
print(f"\n{stage1} vs {stage2}:")
print(f" Common components: {len(common)}")
print(f" Unique to {stage1}: {len(unique1)}")
print(f" Unique to {stage2}: {len(unique2)}")
if unique1:
print(f" Examples unique to {stage1}:")
for comp in list(unique1)[:3]:
print(f" - {comp[0]}@{comp[1]} ({comp[2]})")
PYTHON_SCRIPT
echo ""
echo "Multi-stage analysis complete. Results in: ./multistage-analysis/"Distroless and Minimal Images
SBOM Generation for Minimal Container Images
Distroless Image SBOM Challenges# Distroless images have minimal package metadata
FROM gcr.io/distroless/java17-debian11
# Copy application
COPY app.jar /app.jar
# Distroless images lack traditional package managers
# SBOM generation requires different approaches
ENTRYPOINT ["java", "-jar", "/app.jar"]#!/bin/bash
# distroless-sbom-generator.sh
IMAGE_NAME="${1:-gcr.io/distroless/java17-debian11}"
OUTPUT_DIR="${2:-./distroless-analysis}"
echo "=== Distroless Image SBOM Analysis ==="
mkdir -p "$OUTPUT_DIR"
# 1. Standard SBOM generation (may be limited)
echo "1. Standard SBOM generation..."
syft "$IMAGE_NAME" -o spdx-json="$OUTPUT_DIR/standard-sbom.spdx.json" -v
syft "$IMAGE_NAME" -o cyclonedx-json="$OUTPUT_DIR/standard-sbom.cyclonedx.json" -v
# 2. Layer-by-layer analysis for distroless
echo "2. Layer-by-layer analysis..."
# Extract image layers manually
TEMP_DIR=$(mktemp -d)
docker save "$IMAGE_NAME" | tar -xC "$TEMP_DIR"
# Analyze manifest and layers
if [ -f "$TEMP_DIR/manifest.json" ]; then
# Get base image information
base_image=$(jq -r '.[0].Config' "$TEMP_DIR/manifest.json")
if [ -f "$TEMP_DIR/$base_image" ]; then
jq . "$TEMP_DIR/$base_image" > "$OUTPUT_DIR/image-config.json"
fi
# Analyze each layer
layer_count=0
jq -r '.[0].Layers[]' "$TEMP_DIR/manifest.json" | while read layer; do
echo " Analyzing layer $layer_count..."
layer_dir="$TEMP_DIR/layer_$layer_count"
mkdir -p "$layer_dir"
# Extract layer
tar -xf "$TEMP_DIR/$layer" -C "$layer_dir" 2>/dev/null || true
# Look for package information in layer
find "$layer_dir" -name "*.list" -o -name "status" -o -name "Packages*" -o -name "*.db" | while read pkg_file; do
echo " Found package file: $pkg_file"
cp "$pkg_file" "$OUTPUT_DIR/layer-${layer_count}-$(basename $pkg_file)" 2>/dev/null || true
done
# Generate layer SBOM
syft "dir:$layer_dir" -o json="$OUTPUT_DIR/layer-$layer_count-sbom.json" 2>/dev/null || true
layer_count=$((layer_count + 1))
done
fi
rm -rf "$TEMP_DIR"
# 3. File system analysis
echo "3. File system analysis..."
# Create container to analyze file system
container_id=$(docker create "$IMAGE_NAME")
# Extract key directories for analysis
docker cp "$container_id:/usr/" "$OUTPUT_DIR/usr/" 2>/dev/null || echo " /usr/ not accessible"
docker cp "$container_id:/lib/" "$OUTPUT_DIR/lib/" 2>/dev/null || echo " /lib/ not accessible"
docker cp "$container_id:/etc/" "$OUTPUT_DIR/etc/" 2>/dev/null || echo " /etc/ not accessible"
docker rm "$container_id"
# Analyze extracted file system
if [ -d "$OUTPUT_DIR/usr/" ]; then
echo " Analyzing extracted file system..."
syft "dir:$OUTPUT_DIR/usr/" -o json="$OUTPUT_DIR/filesystem-sbom.json" -v
fi
# 4. Distroless-specific package detection
echo "4. Distroless-specific analysis..."
python3 << 'PYTHON_SCRIPT' > "$OUTPUT_DIR/distroless-analysis.json"
import json
import os
import re
from pathlib import Path
analysis_dir = Path(os.environ.get('OUTPUT_DIR', './distroless-analysis'))
results = {
'distroless_detection': {},
'debian_packages': [],
'java_components': [],
'certificates': [],
'system_libraries': []
}
# Check if this is a known distroless variant
image_config_file = analysis_dir / 'image-config.json'
if image_config_file.exists():
with open(image_config_file) as f:
config = json.load(f)
# Check environment variables and labels for distroless indicators
env_vars = config.get('config', {}).get('Env', [])
labels = config.get('config', {}).get('Labels', {})
results['distroless_detection'] = {
'is_distroless': 'distroless' in str(config).lower(),
'base_image_likely': 'debian' if 'debian' in str(config).lower() else 'unknown',
'java_version': None,
'environment_variables': env_vars,
'labels': labels
}
# Detect Java version from environment
for env_var in env_vars:
if 'JAVA' in env_var:
java_match = re.search(r'java(\d+)', env_var.lower())
if java_match:
results['distroless_detection']['java_version'] = java_match.group(1)
# Analyze extracted directories
usr_dir = analysis_dir / 'usr'
if usr_dir.exists():
# Look for Java components
java_dirs = list(usr_dir.glob('**/java*'))
for java_dir in java_dirs:
if java_dir.is_dir():
results['java_components'].append(str(java_dir.relative_to(analysis_dir)))
# Look for system libraries
lib_files = list(usr_dir.glob('**/*.so*'))
for lib_file in lib_files[:20]: # Limit output
results['system_libraries'].append(str(lib_file.relative_to(analysis_dir)))
# Look for certificates
etc_dir = analysis_dir / 'etc'
if etc_dir.exists():
cert_files = list(etc_dir.glob('**/ca-certificates*')) + list(etc_dir.glob('**/ssl/*'))
for cert_file in cert_files:
if cert_file.is_file():
results['certificates'].append(str(cert_file.relative_to(analysis_dir)))
# Parse any found package files
for pkg_file in analysis_dir.glob('layer-*-status'):
try:
with open(pkg_file) as f:
content = f.read()
# Parse dpkg status file format
packages = []
current_package = {}
for line in content.split('\n'):
if line.startswith('Package: '):
if current_package:
packages.append(current_package)
current_package = {'name': line.split(': ', 1)[1]}
elif line.startswith('Version: ') and current_package:
current_package['version'] = line.split(': ', 1)[1]
elif line.startswith('Architecture: ') and current_package:
current_package['architecture'] = line.split(': ', 1)[1]
if current_package:
packages.append(current_package)
results['debian_packages'].extend(packages)
except Exception as e:
pass
print(json.dumps(results, indent=2))
PYTHON_SCRIPT
# 5. Generate comprehensive report
echo "5. Generating comprehensive report..."
cat > "$OUTPUT_DIR/distroless-report.md" << EOF
# Distroless Image SBOM Analysis Report
**Image:** \`$IMAGE_NAME\`
**Analysis Date:** $(date)
**Output Directory:** \`$OUTPUT_DIR\`
## Analysis Summary
### Standard SBOM Results
- SPDX packages: \$(jq '.packages | length' "$OUTPUT_DIR/standard-sbom.spdx.json" 2>/dev/null || echo "0")
- CycloneDX components: \$(jq '.components | length' "$OUTPUT_DIR/standard-sbom.cyclonedx.json" 2>/dev/null || echo "0")
### Distroless-Specific Findings
\$(cat "$OUTPUT_DIR/distroless-analysis.json" | jq -r '
if .distroless_detection.is_distroless then
"- ✓ Confirmed distroless image"
else
"- ? Distroless detection inconclusive"
end,
"- Base OS: " + .distroless_detection.base_image_likely,
"- Java version: " + (.distroless_detection.java_version // "not detected"),
"- Debian packages found: " + (.debian_packages | length | tostring),
"- Java components: " + (.java_components | length | tostring),
"- System libraries: " + (.system_libraries | length | tostring),
"- Certificates: " + (.certificates | length | tostring)
')
### Layer Analysis
$(ls -1 "$OUTPUT_DIR"/layer-*-sbom.json 2>/dev/null | wc -l) layers analyzed individually
### Recommendations
1. Distroless images provide minimal package metadata
2. Consider using base image SBOMs from Google's distroless repository
3. Focus on application-level dependencies for [vulnerability management](/guides/sbom-vulnerability-management)
4. Implement runtime security monitoring for distroless containers
## Files Generated
- Standard SBOMs: \`standard-sbom.spdx.json\`, \`standard-sbom.cyclonedx.json\`
- Distroless analysis: \`distroless-analysis.json\`
- Layer SBOMs: \`layer-*-sbom.json\`
- Image configuration: \`image-config.json\`
EOF
echo ""
echo "=== Distroless SBOM Analysis Complete ==="
echo "Analysis results: $OUTPUT_DIR"
echo "Report: $OUTPUT_DIR/distroless-report.md"Language-Specific Container SBOMs
Node.js Container SBOM Best Practices
Production Node.js Dockerfile with SBOM# Multi-stage Node.js build with comprehensive SBOM tracking
FROM node:18-alpine AS base
# Install SBOM tools
RUN apk add --no-cache curl bash
RUN curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
WORKDIR /app
# Development stage
FROM base AS development
COPY package*.json ./
RUN npm ci
COPY . .
# Generate development SBOM (includes devDependencies)
RUN syft dir:node_modules -o cyclonedx-json=/tmp/dev-sbom.json
# Production build stage
FROM base AS build
COPY package*.json ./
RUN npm ci --only=production --ignore-scripts
# Generate production dependencies SBOM
RUN syft dir:node_modules -o spdx-json=/tmp/prod-deps-sbom.spdx.json
COPY . .
RUN npm run build
# Generate complete build SBOM
RUN syft dir:. -o cyclonedx-json=/tmp/build-complete-sbom.json
# Final production stage
FROM node:18-alpine AS production
# Install runtime tools
RUN apk add --no-cache curl dumb-init
RUN curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
WORKDIR /app
# Copy production files
COPY --from=build --chown=nextjs:nodejs /app/dist ./dist
COPY --from=build --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=build --chown=nextjs:nodejs /app/package*.json ./
# Copy SBOMs from build stages
COPY --from=development /tmp/dev-sbom.json /app/sboms/
COPY --from=build /tmp/*-sbom.* /app/sboms/
# Generate final production SBOM
RUN syft dir:. -o spdx-json=/app/sboms/production-final.spdx.json
# Generate Node.js specific analysis
RUN node << 'EOF' > /app/sboms/nodejs-analysis.json
const fs = require('fs');
const path = require('path');
const analysis = {
node_version: process.version,
npm_version: process.env.npm_version || 'unknown',
architecture: process.arch,
platform: process.platform,
dependencies: {},
security_audit: {}
};
// Analyze package.json
try {
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
analysis.dependencies.production = Object.keys(pkg.dependencies || {}).length;
analysis.dependencies.dev = Object.keys(pkg.devDependencies || {}).length;
analysis.app_name = pkg.name;
analysis.app_version = pkg.version;
} catch (e) {
console.error('Could not read package.json');
}
// Simple security analysis
try {
const { execSync } = require('child_process');
const auditResult = execSync('npm audit --json', { encoding: 'utf8' });
const audit = JSON.parse(auditResult);
analysis.security_audit = {
vulnerabilities: audit.metadata?.vulnerabilities || {},
total_dependencies: audit.metadata?.totalDependencies || 0
};
} catch (e) {
analysis.security_audit.error = 'npm audit failed';
}
console.log(JSON.stringify(analysis, null, 2));
EOF
# Set user
USER nextjs
EXPOSE 3000
CMD ["dumb-init", "node", "dist/index.js"]Python Container SBOM Integration
Python Flask Application with SBOMFROM python:3.11-slim AS base
# Install SBOM tools
RUN apt-get update && apt-get install -y curl
RUN curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
WORKDIR /app
# Development stage
FROM base AS development
COPY requirements-dev.txt ./
RUN pip install -r requirements-dev.txt
# Generate development SBOM
RUN syft dir:/usr/local/lib/python3.11/site-packages -o cyclonedx-json=/tmp/dev-python-sbom.json
# Production dependencies stage
FROM base AS deps
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Generate production dependencies SBOM
RUN syft dir:/usr/local/lib/python3.11/site-packages -o spdx-json=/tmp/prod-python-sbom.spdx.json
# Generate Python-specific metadata
RUN python << 'EOF' > /tmp/python-analysis.json
import json
import sys
import pkg_resources
import platform
analysis = {
"python_version": sys.version,
"platform": platform.platform(),
"pip_packages": [],
"site_packages_location": "/usr/local/lib/python3.11/site-packages"
}
for dist in pkg_resources.working_set:
analysis["pip_packages"].append({
"name": dist.project_name,
"version": dist.version,
"location": dist.location,
"requires": [str(req) for req in dist.requires()]
})
print(json.dumps(analysis, indent=2))
EOF
# Production stage
FROM python:3.11-slim AS production
# Install runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
RUN curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
# Create non-root user
RUN useradd --create-home --shell /bin/bash app
WORKDIR /app
# Copy Python environment from deps stage
COPY --from=deps /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=deps /usr/local/bin /usr/local/bin
# Copy application
COPY --chown=app:app . .
# Copy SBOMs from previous stages
COPY --from=development /tmp/dev-python-sbom.json /app/sboms/
COPY --from=deps /tmp/prod-python-sbom.spdx.json /app/sboms/
COPY --from=deps /tmp/python-analysis.json /app/sboms/
# Generate final SBOM
RUN syft dir:. -o cyclonedx-json=/app/sboms/final-sbom.json
# Create combined SBOM with metadata
RUN python << 'EOF' > /app/sboms/combined-metadata.json
import json
import os
metadata = {
"container_type": "python-flask",
"base_image": "python:3.11-slim",
"sbom_files": [
"dev-python-sbom.json",
"prod-python-sbom.spdx.json",
"python-analysis.json",
"final-sbom.json"
],
"generation_timestamp": os.popen('date -u +%Y-%m-%dT%H:%M:%SZ').read().strip()
}
print(json.dumps(metadata, indent=2))
EOF
USER app
EXPOSE 5000
CMD ["python", "app.py"]Java Spring Boot Container SBOM
Spring Boot Application with Maven SBOM# Multi-stage Java build with comprehensive SBOM tracking
FROM maven:3.9-openjdk-17 AS build
# Install SBOM tools
RUN apt-get update && apt-get install -y curl
RUN curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin
WORKDIR /app
# Copy POM first for better caching
COPY pom.xml ./
RUN mvn dependency:go-offline
# Generate Maven dependencies SBOM
RUN syft dir:/root/.m2/repository -o cyclonedx-json=/tmp/maven-deps-sbom.json
# Copy source and build
COPY src ./src
RUN mvn clean package -DskipTests
# Generate build SBOM including compiled artifacts
RUN syft dir:. -o spdx-json=/tmp/build-sbom.spdx.json
# Extract Maven dependency information
RUN mvn dependency:tree -DoutputType=json -DoutputFile=/tmp/dependency-tree.json || true
RUN mvn dependency:list -DoutputFile=/tmp/dependency-list.txt || true
# Runtime stage
FROM openjdk:17-jre-slim AS runtime
# Install SBOM tools
RUN apt-get update && apt-get install -y curl && \
curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin && \
rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN useradd --create-home --shell /bin/bash spring
WORKDIR /app
# Copy application JAR
COPY --from=build /app/target/*.jar app.jar
# Copy SBOMs and metadata from build stage
COPY --from=build /tmp/*.json /app/sboms/
COPY --from=build /tmp/*.txt /app/sboms/
# Generate runtime SBOM
RUN syft file:app.jar -o cyclonedx-json=/app/sboms/jar-sbom.json
RUN syft dir:. -o spdx-json=/app/sboms/runtime-sbom.spdx.json
# Generate Java-specific analysis
RUN java -cp app.jar -Dloader.main=org.springframework.boot.loader.PropertiesLauncher \
org.springframework.boot.loader.JarLauncher --help > /dev/null 2>&1 || true
# Extract JAR metadata
RUN jar tf app.jar | grep -E '\.(class|jar|xml|properties)$' | head -100 > /app/sboms/jar-contents.txt
# Create Java runtime analysis
RUN cat > /app/sboms/java-analysis.json << EOF
{
"java_version": "$(java -version 2>&1 | head -n1)",
"jar_file": "app.jar",
"jar_size": "$(stat -f%z app.jar 2>/dev/null || stat -c%s app.jar)",
"spring_boot": true,
"runtime_environment": {
"os": "$(cat /etc/os-release | grep PRETTY_NAME | cut -d'\"' -f2)",
"architecture": "$(uname -m)"
},
"sbom_generation": {
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"tools_used": ["syft", "maven"]
}
}
EOF
# Set ownership
RUN chown -R spring:spring /app
USER spring
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]Container Registry Integration
Automated SBOM Upload to Container Registries
Harbor Registry SBOM Integration#!/usr/bin/env python3
"""
Harbor Registry SBOM Integration
Automatically uploads and manages SBOMs in Harbor registry
"""
import requests
import json
import base64
import argparse
from pathlib import Path
import logging
class HarborSBOMManager:
def __init__(self, harbor_url, username, password):
self.harbor_url = harbor_url.rstrip('/')
self.auth = base64.b64encode(f"{username}:{password}".encode()).decode()
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Basic {self.auth}',
'Content-Type': 'application/json'
})
self.logger = logging.getLogger(__name__)
def upload_sbom_artifact(self, project, repository, tag, sbom_file, sbom_type="application/spdx+json"):
"""Upload SBOM as OCI artifact to Harbor registry"""
# Harbor API endpoint for artifacts
artifact_url = f"{self.harbor_url}/api/v2.0/projects/{project}/repositories/{repository}/artifacts"
# Read SBOM file
with open(sbom_file, 'r') as f:
sbom_content = f.read()
# Create OCI artifact manifest for SBOM
manifest = {
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"size": 2,
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
},
"layers": [
{
"mediaType": sbom_type,
"size": len(sbom_content.encode()),
"digest": self._calculate_digest(sbom_content)
}
],
"annotations": {
"org.opencontainers.image.title": f"SBOM for {repository}:{tag}",
"org.opencontainers.image.description": "[Software Bill of Materials](/learn/what-is-sbom)",
"sbom.tool": "syft",
"sbom.format": "spdx" if "spdx" in sbom_type else "cyclonedx"
}
}
try:
# Upload SBOM blob first
blob_url = f"{self.harbor_url}/api/v2.0/projects/{project}/repositories/{repository}/blobs/uploads/"
# Start upload session
upload_response = self.session.post(blob_url)
upload_response.raise_for_status()
upload_location = upload_response.headers.get('Location')
if not upload_location:
raise Exception("No upload location returned")
# Upload SBOM content
upload_url = f"{self.harbor_url}{upload_location}"
content_response = self.session.patch(
upload_url,
data=sbom_content.encode(),
headers={'Content-Type': 'application/octet-stream'}
)
content_response.raise_for_status()
# Complete upload
digest = self._calculate_digest(sbom_content)
complete_url = f"{upload_url}&digest={digest}"
complete_response = self.session.put(complete_url)
complete_response.raise_for_status()
# Upload manifest
manifest_url = f"{artifact_url}/{tag}/sbom"
manifest_response = self.session.put(
manifest_url,
json=manifest,
headers={'Content-Type': 'application/vnd.oci.image.manifest.v1+json'}
)
manifest_response.raise_for_status()
self.logger.info(f"✓ SBOM uploaded successfully for {project}/{repository}:{tag}")
return True
except Exception as e:
self.logger.error(f"✗ Failed to upload SBOM: {e}")
return False
def associate_sbom_with_image(self, project, repository, image_tag, sbom_tag):
"""Associate SBOM artifact with container image"""
# Harbor accessory API for linking artifacts
accessory_url = f"{self.harbor_url}/api/v2.0/projects/{project}/repositories/{repository}/artifacts/{image_tag}/accessories"
payload = {
"subject_artifact": {
"repository_name": f"{project}/{repository}",
"digest": self._get_artifact_digest(project, repository, image_tag)
},
"accessory_artifact": {
"repository_name": f"{project}/{repository}",
"digest": self._get_artifact_digest(project, repository, sbom_tag)
},
"type": "sbom"
}
try:
response = self.session.post(accessory_url, json=payload)
response.raise_for_status()
self.logger.info(f"✓ SBOM associated with image {project}/{repository}:{image_tag}")
return True
except Exception as e:
self.logger.error(f"✗ Failed to associate SBOM: {e}")
return False
def get_project_sboms(self, project):
"""Retrieve all SBOMs for a project"""
repos_url = f"{self.harbor_url}/api/v2.0/projects/{project}/repositories"
try:
response = self.session.get(repos_url)
response.raise_for_status()
repositories = response.json()
all_sboms = []
for repo in repositories:
repo_name = repo['name'].split('/')[-1]
# Get artifacts for repository
artifacts_url = f"{self.harbor_url}/api/v2.0/projects/{project}/repositories/{repo_name}/artifacts"
artifacts_response = self.session.get(artifacts_url)
if artifacts_response.status_code == 200:
artifacts = artifacts_response.json()
for artifact in artifacts:
# Check if artifact is an SBOM
if self._is_sbom_artifact(artifact):
all_sboms.append({
'project': project,
'repository': repo_name,
'tag': artifact.get('tags', [{}])[0].get('name', 'latest'),
'digest': artifact['digest'],
'size': artifact.get('size', 0),
'push_time': artifact.get('push_time'),
'sbom_type': self._detect_sbom_type(artifact)
})
return all_sboms
except Exception as e:
self.logger.error(f"Failed to retrieve SBOMs for project {project}: {e}")
return []
def _calculate_digest(self, content):
"""Calculate SHA256 digest for content"""
import hashlib
return f"sha256:{hashlib.sha256(content.encode()).hexdigest()}"
def _get_artifact_digest(self, project, repository, tag):
"""Get digest for specific artifact"""
artifact_url = f"{self.harbor_url}/api/v2.0/projects/{project}/repositories/{repository}/artifacts/{tag}"
try:
response = self.session.get(artifact_url)
response.raise_for_status()
return response.json()['digest']
except:
return None
def _is_sbom_artifact(self, artifact):
"""Check if artifact is an SBOM"""
media_type = artifact.get('media_type', '')
annotations = artifact.get('annotations', {})
return (
'sbom' in media_type.lower() or
'spdx' in media_type.lower() or
'cyclone' in media_type.lower() or
'sbom' in str(annotations).lower()
)
def _detect_sbom_type(self, artifact):
"""Detect SBOM format type"""
media_type = artifact.get('media_type', '')
if 'spdx' in media_type.lower():
return 'SPDX'
elif 'cyclone' in media_type.lower():
return 'CycloneDX'
else:
return 'Unknown'
def main():
parser = argparse.ArgumentParser(description="Harbor Registry SBOM Manager")
parser.add_argument("--harbor-url", required=True, help="Harbor registry URL")
parser.add_argument("--username", required=True, help="Harbor username")
parser.add_argument("--password", required=True, help="Harbor password")
parser.add_argument("--project", required=True, help="Harbor project name")
parser.add_argument("--repository", required=True, help="Repository name")
parser.add_argument("--tag", required=True, help="Image tag")
parser.add_argument("--sbom-file", help="SBOM file to upload")
parser.add_argument("--action", choices=["upload", "list", "associate"],
default="upload", help="Action to perform")
args = parser.parse_args()
logging.basicConfig(level=logging.INFO)
manager = HarborSBOMManager(args.harbor_url, args.username, args.password)
if args.action == "upload" and args.sbom_file:
success = manager.upload_sbom_artifact(
args.project, args.repository, args.tag, args.sbom_file
)
if success:
print(f"✓ SBOM uploaded for {args.project}/{args.repository}:{args.tag}")
else:
print("✗ SBOM upload failed")
elif args.action == "list":
sboms = manager.get_project_sboms(args.project)
print(f"Found {len(sboms)} SBOMs in project {args.project}:")
for sbom in sboms:
print(f" - {sbom['repository']}:{sbom['tag']} ({sbom['sbom_type']}) - {sbom['size']} bytes")
elif args.action == "associate":
# Associate SBOM with image (requires both to exist)
success = manager.associate_sbom_with_image(
args.project, args.repository, args.tag, f"{args.tag}-sbom"
)
if success:
print(f"✓ SBOM associated with {args.project}/{args.repository}:{args.tag}")
if __name__ == "__main__":
main()This comprehensive Docker and Container SBOM guide provides enterprise-ready solutions for managing container supply chain security. The tools, scripts, and techniques covered address the unique challenges of containerized applications while ensuring compliance with modern security frameworks.