Practical guide to generating Software Bills of Materials for Docker containers, from local images to registry workflows and CI/CD automation.

Updated:

Docker SBOM Guide for Container Images and Security

Table of Contents

  1. Introduction
  2. Understanding Container SBOMs
  3. Docker SBOM Fundamentals
  4. Essential Tools for Container SBOMs
  5. Syft for Docker Images
  6. Multi-Stage Build SBOM Generation
  7. Distroless and Minimal Images
  8. Language-Specific Container SBOMs
  9. Container Registry Integration
  10. CI/CD Pipeline Integration
  11. Security Scanning & Vulnerability Management
  12. Enterprise Container SBOM Management
  13. Best Practices
  14. 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
Multi-Scope SBOM Generation
  • 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)
2. Multi-Stage Build Complexity
# 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
3. Base Image Inheritance
# Inherited vulnerabilities and dependencies
FROM ubuntu:22.04  # Inherits all Ubuntu packages
# Your application adds more components

Docker 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
Comprehensive Docker Analysis Script
#!/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' ' ')"
fi

Essential 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 table

Syft 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;"]
Enhanced Docker Entrypoint with SBOM API
#!/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 "$@"
Multi-Stage Build Analysis Script
#!/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"]
Enhanced Distroless SBOM Generation
#!/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 SBOM
FROM 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.