devsecops

Performing Container Image Hardening

This skill covers hardening container images by minimizing attack surface, removing unnecessary packages, implementing multi-stage builds, configuring non-root users, and applying CIS Docker Benchmark recommendations to produce secure production-ready images.

cicdcis-benchmarkcontainer-hardeningdevsecopsdockersecure-sdlc
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When building production container images that need minimal attack surface
  • When compliance requires CIS Docker Benchmark adherence for container configurations
  • When reducing image size to minimize vulnerability exposure from unused packages
  • When implementing defense-in-depth for containerized workloads
  • When migrating from fat base images to distroless or minimal images

Do not use for runtime container security monitoring (use Falco), for host-level Docker daemon hardening (use CIS Docker Benchmark host checks), or for container orchestration security (use Kubernetes security scanning).

Prerequisites

  • Docker or BuildKit for multi-stage builds
  • Base image options: distroless, Alpine, slim, or scratch
  • Container scanning tool (Trivy) for validation
  • CIS Docker Benchmark reference

Workflow

Step 1: Use Multi-Stage Builds to Minimize Image Size

# Build stage with all dependencies
FROM python:3.12-bookworm AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
COPY src/ ./src/
RUN python -m compileall src/
 
# Production stage with minimal base
FROM python:3.12-slim-bookworm AS production
RUN apt-get update && \
    apt-get install -y --no-install-recommends libpq5 && \
    rm -rf /var/lib/apt/lists/* && \
    apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false
 
COPY --from=builder /install /usr/local
COPY --from=builder /build/src /app/src
 
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
RUN chown -R appuser:appuser /app
 
USER appuser
WORKDIR /app
 
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" || exit 1
 
EXPOSE 8080
ENTRYPOINT ["python", "-m", "src.main"]

Step 2: Use Distroless Base Images

# Go application with distroless
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /server .
 
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]

Step 3: Remove Unnecessary Components

# Hardened image checklist
FROM ubuntu:24.04 AS base
 
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      ca-certificates \
      libssl3 && \
    # Remove package manager to prevent runtime package installation
    apt-get purge -y --auto-remove apt dpkg && \
    rm -rf /var/lib/apt/lists/* \
           /var/cache/apt/* \
           /tmp/* \
           /var/tmp/* \
           /usr/share/doc/* \
           /usr/share/man/* \
           /usr/share/info/* \
           /root/.cache
 
# Remove shells if not needed
RUN rm -f /bin/sh /bin/bash /usr/bin/sh 2>/dev/null || true
 
# Remove setuid/setgid binaries
RUN find / -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null || true

Step 4: Configure Read-Only Filesystem

# Kubernetes deployment with read-only root filesystem
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hardened-app
spec:
  template:
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 65534
        fsGroup: 65534
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: app
          image: app:hardened
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp
            - name: cache
              mountPath: /app/cache
      volumes:
        - name: tmp
          emptyDir:
            sizeLimit: 100Mi
        - name: cache
          emptyDir:
            sizeLimit: 50Mi

Step 5: Pin Base Image by Digest

# Pin to exact image digest for reproducibility
FROM python:3.12-slim-bookworm@sha256:abcdef1234567890 AS production
# This ensures the exact same base image is used every time

Step 6: Validate Hardening with Automated Scanning

# Scan hardened image with Trivy
trivy image --severity HIGH,CRITICAL hardened-app:latest
 
# Check CIS Docker Benchmark compliance
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/docker-bench-security
 
# Verify no root processes
docker run --rm hardened-app:latest whoami
# Expected: appuser (NOT root)
 
# Verify read-only filesystem
docker run --rm hardened-app:latest touch /test 2>&1
# Expected: Read-only file system error

Key Concepts

Term Definition
Multi-Stage Build Docker build technique using multiple FROM stages to separate build and runtime, reducing final image size
Distroless Google-maintained minimal container images containing only the application and runtime dependencies
Non-Root User Running container processes as unprivileged user to limit impact of container escape exploits
Read-Only Root Mounting the container root filesystem as read-only to prevent runtime modification
Image Digest SHA256 hash uniquely identifying an exact image version, more precise than mutable tags
Scratch Image Empty Docker base image used for statically compiled binaries requiring no OS
Security Context Kubernetes pod/container-level security settings controlling privileges, filesystem, and capabilities

Tools & Systems

  • Docker BuildKit: Advanced Docker build engine supporting multi-stage builds and build secrets
  • Distroless Images: Google's minimal container base images (static, base, java, python, nodejs)
  • docker-bench-security: Script checking CIS Docker Benchmark compliance
  • Trivy: Container image vulnerability and misconfiguration scanner
  • Hadolint: Dockerfile linter enforcing best practices

Common Scenarios

Scenario: Reducing a 1.2GB Python Image to Under 150MB

Context: A data science team uses python:3.12 as base image (1.2GB) with scientific computing packages. The image has 200+ known CVEs from unnecessary system packages.

Approach:

  1. Switch to python:3.12-slim-bookworm as base (150MB) and install only required system libraries
  2. Use multi-stage build: compile C extensions in builder stage, copy wheels to production
  3. Pin numpy, pandas, and scipy to pre-built wheels to avoid build dependencies in production
  4. Remove pip, setuptools, and wheel from the final image
  5. Create non-root user and set filesystem permissions
  6. Validate with Trivy: expect CVE count to drop from 200+ to under 20

Pitfalls: Some Python packages require shared libraries at runtime (libgomp, libstdc++). Test the application thoroughly after removing system packages. Alpine-based images use musl libc which can cause compatibility issues with numpy and pandas.

Output Format

Container Image Hardening Report
==================================
Image: app:hardened
Base: python:3.12-slim-bookworm
Date: 2026-02-23
 
SIZE COMPARISON:
  Before hardening: 1,247 MB (python:3.12)
  After hardening:  143 MB  (python:3.12-slim + multi-stage)
  Reduction: 88.5%
 
SECURITY CHECKS:
  [PASS] Non-root user configured (appuser:1000)
  [PASS] HEALTHCHECK instruction present
  [PASS] No setuid/setgid binaries found
  [PASS] Package manager removed
  [PASS] Base image pinned by digest
  [PASS] No shell access (/bin/sh removed)
  [WARN] /tmp writable (emptyDir mounted)
 
VULNERABILITY COMPARISON:
  Before: 234 CVEs (12 Critical, 45 High)
  After:  18 CVEs (0 Critical, 3 High)
  Reduction: 92.3%
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md5.1 KB

API Reference: Container Image Hardening Audit

Libraries Used

Library Purpose
subprocess Execute Trivy, Docker, and hadolint CLI commands
json Parse vulnerability scan and inspection results
re Analyze Dockerfile instructions
pathlib Handle Dockerfile and image paths

Installation

# Trivy vulnerability scanner
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
 
# Hadolint Dockerfile linter
wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
chmod +x /usr/local/bin/hadolint
 
# Docker CLI (already installed in most environments)

Trivy Image Scanning

Scan Image for Vulnerabilities

import subprocess
import json
 
def scan_image(image_name, severity="CRITICAL,HIGH"):
    cmd = [
        "trivy", "image",
        "--format", "json",
        "--severity", severity,
        "--exit-code", "0",
        image_name,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    return json.loads(result.stdout) if result.stdout else {}

Scan for Secrets in Image

def scan_secrets(image_name):
    cmd = [
        "trivy", "image",
        "--format", "json",
        "--scanners", "secret",
        image_name,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    return json.loads(result.stdout) if result.stdout else {}

Scan for Misconfigurations

def scan_misconfig(image_name):
    cmd = [
        "trivy", "image",
        "--format", "json",
        "--scanners", "misconfig",
        image_name,
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    return json.loads(result.stdout) if result.stdout else {}

Docker Image Inspection

Inspect Image Metadata

def inspect_image(image_name):
    cmd = ["docker", "inspect", image_name]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    data = json.loads(result.stdout)[0]
    config = data.get("Config", {})
    return {
        "image": image_name,
        "user": config.get("User", "root"),
        "exposed_ports": list(config.get("ExposedPorts", {}).keys()),
        "env_vars": config.get("Env", []),
        "entrypoint": config.get("Entrypoint"),
        "cmd": config.get("Cmd"),
        "labels": config.get("Labels", {}),
        "layers": len(data.get("RootFS", {}).get("Layers", [])),
        "size_mb": round(data.get("Size", 0) / 1048576, 1),
    }

Check for Root User

def check_non_root(image_name):
    inspection = inspect_image(image_name)
    user = inspection["user"]
    return {
        "image": image_name,
        "runs_as_root": user in ("", "root", "0"),
        "user": user or "root (default)",
        "severity": "high" if user in ("", "root", "0") else "pass",
    }

Hadolint Dockerfile Linting

def lint_dockerfile(dockerfile_path):
    cmd = [
        "hadolint",
        "--format", "json",
        str(dockerfile_path),
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
    findings = json.loads(result.stdout) if result.stdout else []
    return [
        {
            "line": f["line"],
            "code": f["code"],
            "level": f["level"],
            "message": f["message"],
        }
        for f in findings
    ]

Hardening Checks

Common Dockerfile Issues

def audit_dockerfile(dockerfile_path):
    findings = []
    with open(dockerfile_path) as f:
        lines = f.readlines()
 
    has_user = False
    has_healthcheck = False
 
    for i, line in enumerate(lines, 1):
        stripped = line.strip()
        if stripped.startswith("USER") and stripped.split()[-1] not in ("root", "0"):
            has_user = True
        if stripped.startswith("HEALTHCHECK"):
            has_healthcheck = True
        if stripped.startswith("FROM") and ":latest" in stripped:
            findings.append({
                "line": i, "severity": "medium",
                "issue": "Using :latest tag — pin specific version",
            })
        if "ADD" in stripped and ("http://" in stripped or "https://" in stripped):
            findings.append({
                "line": i, "severity": "high",
                "issue": "ADD with remote URL — use COPY + curl for verification",
            })
 
    if not has_user:
        findings.append({"line": 0, "severity": "high", "issue": "No USER instruction — runs as root"})
    if not has_healthcheck:
        findings.append({"line": 0, "severity": "low", "issue": "No HEALTHCHECK instruction"})
 
    return findings

Output Format

{
  "image": "myapp:v1.2.3",
  "vulnerabilities": {
    "critical": 2,
    "high": 8,
    "medium": 15,
    "low": 23
  },
  "runs_as_root": false,
  "size_mb": 142.5,
  "layers": 12,
  "dockerfile_issues": 3,
  "secrets_found": 0,
  "findings": [
    {
      "type": "vulnerability",
      "package": "openssl",
      "installed": "3.0.2",
      "fixed": "3.0.13",
      "severity": "CRITICAL",
      "cve": "CVE-2024-0727"
    }
  ]
}
standards.md0.9 KB

Standards Reference: Container Image Hardening

CIS Docker Benchmark v1.6.0 - Image Controls

  • 4.1: Create a user for the container
  • 4.2: Use trusted base images
  • 4.3: Do not install unnecessary packages
  • 4.4: Scan and rebuild images for security patches
  • 4.6: Add HEALTHCHECK instruction
  • 4.7: Do not use update instructions alone
  • 4.9: Use COPY instead of ADD
  • 4.10: Do not store secrets in Dockerfiles
  • 4.11: Install verified packages only

NIST SP 800-190 Application Container Security Guide

Image Hardening (Section 4.1)

  • Use minimal base images to reduce attack surface
  • Remove unnecessary tools, shells, and package managers
  • Scan images for vulnerabilities before deployment
  • Sign images for integrity verification

OWASP Docker Security Cheat Sheet

  • Use multi-stage builds
  • Run as non-root user
  • Use read-only root filesystem
  • Pin base images to digests
  • Drop all Linux capabilities
  • Enable seccomp profile
workflows.md1.8 KB

Workflow Reference: Container Image Hardening

Hardening Pipeline

Base Image Selection


┌──────────────────┐
│ Multi-Stage Build │
│ (builder + prod)  │
└──────┬───────────┘


┌──────────────────┐
│ Remove Packages  │
│ + Set User/Perms │
└──────┬───────────┘


┌──────────────────┐
│ Trivy Scan       │
│ + Hadolint       │
└──────┬───────────┘


┌──────────────────┐
│ CIS Benchmark    │
│ Validation       │
└──────────────────┘

Base Image Selection Guide

Base Image Size Use Case Packages
scratch 0 MB Static Go/Rust binaries None
distroless/static 2 MB Static binaries + CA certs ca-certificates
distroless/base 20 MB Dynamic binaries glibc, libssl
alpine:3.19 7 MB General minimal musl, busybox
debian:bookworm-slim 80 MB Debian ecosystem apt, glibc
ubuntu:24.04 78 MB Ubuntu ecosystem apt, glibc

Dockerfile Hardening Checklist

  • Multi-stage build separates build and runtime
  • Minimal base image selected
  • Non-root USER instruction present
  • HEALTHCHECK instruction defined
  • Base image pinned by digest
  • COPY used instead of ADD
  • No secrets in ENV or ARG
  • Package cache cleaned (rm -rf /var/lib/apt/lists/*)
  • Unnecessary packages removed
  • setuid/setgid bits cleared
  • Shell removed (if not needed)

Scripts 2

agent.py11.3 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Container image hardening audit agent.

Audits Docker container images for security hardening best practices
using Trivy for vulnerability scanning, Dockle for CIS Docker Benchmark
compliance, and Dockerfile analysis for security anti-patterns.
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone


def find_binary(name):
    """Locate a binary on the system PATH."""
    for ext in ["", ".exe"]:
        for directory in os.environ.get("PATH", "").split(os.pathsep):
            full_path = os.path.join(directory, name + ext)
            if os.path.isfile(full_path):
                return full_path
    return None


def run_trivy_image_scan(image, severity="CRITICAL,HIGH", ignore_unfixed=True):
    """Scan a container image with Trivy for vulnerabilities."""
    trivy_bin = find_binary("trivy")
    if not trivy_bin:
        print("[!] trivy not found. Install: https://github.com/aquasecurity/trivy",
              file=sys.stderr)
        return None

    cmd = [trivy_bin, "image", "--format", "json", "--severity", severity]
    if ignore_unfixed:
        cmd.append("--ignore-unfixed")
    cmd.append(image)

    print(f"[*] Running Trivy scan: {' '.join(cmd)}")
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
    if result.returncode != 0 and not result.stdout:
        print(f"[!] Trivy error: {result.stderr[:300]}", file=sys.stderr)
        return None
    try:
        return json.loads(result.stdout)
    except json.JSONDecodeError:
        return None


def run_dockle_scan(image):
    """Scan a container image with Dockle for CIS benchmark compliance."""
    dockle_bin = find_binary("dockle")
    if not dockle_bin:
        print("[*] dockle not found, skipping CIS benchmark check", file=sys.stderr)
        return None

    cmd = [dockle_bin, "--format", "json", image]
    print(f"[*] Running Dockle scan: {' '.join(cmd)}")
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
    try:
        return json.loads(result.stdout)
    except json.JSONDecodeError:
        return None


def analyze_dockerfile(dockerfile_path):
    """Analyze a Dockerfile for security anti-patterns."""
    findings = []
    if not os.path.isfile(dockerfile_path):
        return findings

    with open(dockerfile_path, "r") as f:
        lines = f.readlines()

    runs_as_root = True
    has_healthcheck = False
    uses_latest_tag = False

    for i, line in enumerate(lines, 1):
        stripped = line.strip()
        upper = stripped.upper()

        if upper.startswith("FROM") and ":latest" in stripped:
            uses_latest_tag = True
            findings.append({
                "check": "FROM uses :latest tag",
                "line": i,
                "severity": "HIGH",
                "description": "Pin image to a specific version for reproducibility and security",
                "content": stripped,
            })

        if upper.startswith("FROM") and "scratch" not in stripped.lower():
            base = stripped.split()[-1] if stripped.split() else ""
            if "alpine" not in base.lower() and "distroless" not in base.lower():
                findings.append({
                    "check": "Non-minimal base image",
                    "line": i,
                    "severity": "MEDIUM",
                    "description": "Consider using Alpine or distroless base for smaller attack surface",
                    "content": stripped,
                })

        if upper.startswith("USER") and stripped.split()[-1] not in ("root", "0"):
            runs_as_root = False

        if upper.startswith("HEALTHCHECK"):
            has_healthcheck = True

        if upper.startswith("RUN") and ("chmod 777" in stripped or "chmod -R 777" in stripped):
            findings.append({
                "check": "Overly permissive chmod 777",
                "line": i,
                "severity": "HIGH",
                "description": "chmod 777 grants world-writable permissions; use specific permissions",
                "content": stripped,
            })

        if upper.startswith("RUN") and "curl" in stripped and "| sh" in stripped:
            findings.append({
                "check": "Pipe to shell from curl",
                "line": i,
                "severity": "CRITICAL",
                "description": "Piping curl output to shell is risky; download, verify, then execute",
                "content": stripped,
            })

        if upper.startswith("ENV") and any(kw in upper for kw in ["PASSWORD", "SECRET", "TOKEN", "API_KEY"]):
            findings.append({
                "check": "Secrets in ENV instruction",
                "line": i,
                "severity": "CRITICAL",
                "description": "Never embed secrets in Dockerfile; use build args or secrets mount",
                "content": stripped,
            })

        if upper.startswith("ADD") and not stripped.endswith(".tar.gz"):
            findings.append({
                "check": "ADD instead of COPY",
                "line": i,
                "severity": "LOW",
                "description": "Use COPY unless you need ADD's auto-extraction; COPY is more explicit",
                "content": stripped,
            })

        if upper.startswith("EXPOSE") and any(p in stripped for p in ["22", "23", "3389"]):
            findings.append({
                "check": "Exposed management port",
                "line": i,
                "severity": "HIGH",
                "description": "SSH/Telnet/RDP ports should not be exposed in containers",
                "content": stripped,
            })

    if runs_as_root:
        findings.append({
            "check": "Container runs as root",
            "line": 0,
            "severity": "HIGH",
            "description": "Add a USER instruction to run as non-root for least privilege",
        })

    if not has_healthcheck:
        findings.append({
            "check": "Missing HEALTHCHECK",
            "line": 0,
            "severity": "LOW",
            "description": "Add HEALTHCHECK to enable container health monitoring",
        })

    return findings


def extract_trivy_findings(trivy_data):
    """Extract vulnerability findings from Trivy JSON output."""
    findings = []
    if not trivy_data:
        return findings
    results = trivy_data.get("Results", [])
    for result in results:
        target = result.get("Target", "")
        for vuln in result.get("Vulnerabilities", []):
            findings.append({
                "source": "trivy",
                "target": target,
                "vulnerability_id": vuln.get("VulnerabilityID", ""),
                "pkg_name": vuln.get("PkgName", ""),
                "installed_version": vuln.get("InstalledVersion", ""),
                "fixed_version": vuln.get("FixedVersion", ""),
                "severity": vuln.get("Severity", "UNKNOWN"),
                "title": vuln.get("Title", ""),
                "description": vuln.get("Description", "")[:200],
            })
    return findings


def extract_dockle_findings(dockle_data):
    """Extract CIS benchmark findings from Dockle JSON output."""
    findings = []
    if not dockle_data:
        return findings
    for detail in dockle_data.get("details", []):
        severity_map = {"FATAL": "CRITICAL", "WARN": "HIGH", "INFO": "MEDIUM", "SKIP": "LOW"}
        findings.append({
            "source": "dockle",
            "code": detail.get("code", ""),
            "title": detail.get("title", ""),
            "level": detail.get("level", "INFO"),
            "severity": severity_map.get(detail.get("level", "INFO"), "MEDIUM"),
            "alerts": detail.get("alerts", []),
        })
    return findings


def format_summary(image, trivy_findings, dockle_findings, dockerfile_findings):
    """Print combined audit summary."""
    all_findings = trivy_findings + dockle_findings + dockerfile_findings
    print(f"\n{'='*60}")
    print(f"  Container Image Hardening Audit")
    print(f"{'='*60}")
    print(f"  Image          : {image}")
    print(f"  Vulnerabilities: {len(trivy_findings)} (Trivy)")
    print(f"  CIS Benchmark  : {len(dockle_findings)} (Dockle)")
    print(f"  Dockerfile     : {len(dockerfile_findings)}")
    print(f"  Total Findings : {len(all_findings)}")

    severity_counts = {}
    for f in all_findings:
        sev = f.get("severity", "UNKNOWN")
        severity_counts[sev] = severity_counts.get(sev, 0) + 1

    print(f"\n  By Severity:")
    for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"]:
        count = severity_counts.get(sev, 0)
        if count > 0:
            print(f"    {sev:10s}: {count}")

    if trivy_findings:
        print(f"\n  Top Vulnerabilities:")
        for f in trivy_findings[:10]:
            print(f"    {f['vulnerability_id']:16s} | {f['severity']:8s} | "
                  f"{f['pkg_name']}:{f['installed_version']} -> {f.get('fixed_version', 'N/A')}")

    if dockerfile_findings:
        print(f"\n  Dockerfile Issues:")
        for f in dockerfile_findings:
            print(f"    [{f['severity']:8s}] Line {f.get('line', 0):3d}: {f['check']}")

    return severity_counts


def main():
    parser = argparse.ArgumentParser(
        description="Container image hardening audit agent"
    )
    parser.add_argument("--image", required=True, help="Container image to scan (e.g., nginx:1.25)")
    parser.add_argument("--dockerfile", help="Path to Dockerfile for static analysis")
    parser.add_argument("--severity", default="CRITICAL,HIGH",
                        help="Trivy severity filter (default: CRITICAL,HIGH)")
    parser.add_argument("--skip-trivy", action="store_true", help="Skip Trivy scan")
    parser.add_argument("--skip-dockle", action="store_true", help="Skip Dockle scan")
    parser.add_argument("--output", "-o", help="Output JSON report path")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    trivy_findings = []
    dockle_findings = []
    dockerfile_findings = []

    if not args.skip_trivy:
        trivy_data = run_trivy_image_scan(args.image, args.severity)
        trivy_findings = extract_trivy_findings(trivy_data)

    if not args.skip_dockle:
        dockle_data = run_dockle_scan(args.image)
        dockle_findings = extract_dockle_findings(dockle_data)

    if args.dockerfile:
        dockerfile_findings = analyze_dockerfile(args.dockerfile)

    severity_counts = format_summary(
        args.image, trivy_findings, dockle_findings, dockerfile_findings
    )

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "Container Hardening Audit",
        "image": args.image,
        "severity_counts": severity_counts,
        "trivy_findings": trivy_findings,
        "dockle_findings": dockle_findings,
        "dockerfile_findings": dockerfile_findings,
        "total_findings": len(trivy_findings) + len(dockle_findings) + len(dockerfile_findings),
        "risk_level": (
            "CRITICAL" if severity_counts.get("CRITICAL", 0) > 0
            else "HIGH" if severity_counts.get("HIGH", 0) > 0
            else "MEDIUM" if severity_counts.get("MEDIUM", 0) > 0
            else "LOW"
        ),
    }

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
process.py6.9 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""
Container Image Hardening Validation Script

Validates container images against hardening best practices,
checks for non-root user, minimal packages, and CIS compliance.

Usage:
    python process.py --image myapp:latest --output hardening-report.json
"""

import argparse
import json
import os
import subprocess
import sys
from dataclasses import dataclass, field
from datetime import datetime, timezone


@dataclass
class HardeningCheck:
    check_id: str
    name: str
    passed: bool
    details: str
    severity: str


def run_docker_inspect(image: str) -> dict:
    """Inspect a Docker image and return configuration."""
    cmd = ["docker", "inspect", image]
    try:
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
        if proc.returncode == 0:
            data = json.loads(proc.stdout)
            return data[0] if data else {}
        return {"error": proc.stderr}
    except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError) as e:
        return {"error": str(e)}


def check_non_root_user(config: dict) -> HardeningCheck:
    """Check if image runs as non-root user."""
    user = config.get("Config", {}).get("User", "")
    if user and user != "0" and user != "root":
        return HardeningCheck("CIS-4.1", "Non-root user configured", True,
                              f"User: {user}", "HIGH")
    return HardeningCheck("CIS-4.1", "Non-root user configured", False,
                          "Image runs as root. Add USER instruction.", "HIGH")


def check_healthcheck(config: dict) -> HardeningCheck:
    """Check if HEALTHCHECK is defined."""
    healthcheck = config.get("Config", {}).get("Healthcheck")
    if healthcheck and healthcheck.get("Test"):
        return HardeningCheck("CIS-4.6", "HEALTHCHECK defined", True,
                              f"Test: {healthcheck['Test']}", "MEDIUM")
    return HardeningCheck("CIS-4.6", "HEALTHCHECK defined", False,
                          "No HEALTHCHECK instruction found.", "MEDIUM")


def check_exposed_ports(config: dict) -> HardeningCheck:
    """Check for unnecessary exposed ports."""
    ports = config.get("Config", {}).get("ExposedPorts", {})
    port_list = list(ports.keys()) if ports else []
    if len(port_list) <= 2:
        return HardeningCheck("HARD-1", "Minimal ports exposed", True,
                              f"Ports: {port_list}", "LOW")
    return HardeningCheck("HARD-1", "Minimal ports exposed", False,
                          f"Many ports exposed: {port_list}. Minimize exposed ports.", "LOW")


def check_image_size(image: str) -> HardeningCheck:
    """Check image size against recommended limits."""
    cmd = ["docker", "image", "inspect", image, "--format", "{{.Size}}"]
    try:
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
        size_bytes = int(proc.stdout.strip())
        size_mb = size_bytes / (1024 * 1024)
        if size_mb < 200:
            return HardeningCheck("HARD-2", "Image size within limits", True,
                                  f"Size: {size_mb:.0f} MB (< 200 MB)", "MEDIUM")
        elif size_mb < 500:
            return HardeningCheck("HARD-2", "Image size within limits", False,
                                  f"Size: {size_mb:.0f} MB (> 200 MB, consider optimizing)", "MEDIUM")
        else:
            return HardeningCheck("HARD-2", "Image size within limits", False,
                                  f"Size: {size_mb:.0f} MB (> 500 MB, use multi-stage build)", "HIGH")
    except (subprocess.TimeoutExpired, ValueError):
        return HardeningCheck("HARD-2", "Image size within limits", False,
                              "Could not determine image size", "LOW")


def check_env_secrets(config: dict) -> HardeningCheck:
    """Check for potential secrets in environment variables."""
    env_vars = config.get("Config", {}).get("Env", [])
    secret_keywords = ["password", "secret", "key", "token", "credential", "api_key"]
    suspicious = []
    for env in env_vars:
        name = env.split("=")[0].lower()
        if any(kw in name for kw in secret_keywords):
            suspicious.append(env.split("=")[0])
    if not suspicious:
        return HardeningCheck("CIS-4.10", "No secrets in ENV", True,
                              "No suspicious environment variables found", "HIGH")
    return HardeningCheck("CIS-4.10", "No secrets in ENV", False,
                          f"Suspicious ENV vars: {suspicious}. Use secrets manager.", "HIGH")


def check_shell_available(image: str) -> HardeningCheck:
    """Check if shell is available in the image."""
    cmd = ["docker", "run", "--rm", "--entrypoint", "", image, "sh", "-c", "echo shell_available"]
    try:
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
        if "shell_available" in proc.stdout:
            return HardeningCheck("HARD-3", "Shell removed from image", False,
                                  "Shell (/bin/sh) is available. Consider removing for production.", "LOW")
        return HardeningCheck("HARD-3", "Shell removed from image", True,
                              "No shell available in image", "LOW")
    except (subprocess.TimeoutExpired, FileNotFoundError):
        return HardeningCheck("HARD-3", "Shell removed from image", True,
                              "Could not test shell access (likely unavailable)", "LOW")


def main():
    parser = argparse.ArgumentParser(description="Container Image Hardening Validation")
    parser.add_argument("--image", required=True, help="Docker image to validate")
    parser.add_argument("--output", default="hardening-report.json")
    parser.add_argument("--fail-on-findings", action="store_true")
    args = parser.parse_args()

    print(f"[*] Validating hardening: {args.image}")

    config = run_docker_inspect(args.image)
    if "error" in config:
        print(f"[ERROR] {config['error']}")
        sys.exit(2)

    checks = [
        check_non_root_user(config),
        check_healthcheck(config),
        check_exposed_ports(config),
        check_image_size(args.image),
        check_env_secrets(config),
        check_shell_available(args.image),
    ]

    passed = sum(1 for c in checks if c.passed)
    failed = sum(1 for c in checks if not c.passed)

    report = {
        "metadata": {"image": args.image, "date": datetime.now(timezone.utc).isoformat()},
        "summary": {"total": len(checks), "passed": passed, "failed": failed},
        "checks": [
            {"id": c.check_id, "name": c.name, "passed": c.passed,
             "details": c.details, "severity": c.severity}
            for c in checks
        ]
    }

    output_path = os.path.abspath(args.output)
    with open(output_path, "w") as f:
        json.dump(report, f, indent=2)

    for c in checks:
        status = "PASS" if c.passed else "FAIL"
        print(f"  [{status}] {c.check_id}: {c.name} - {c.details}")

    print(f"\n[*] {passed}/{len(checks)} checks passed")
    if args.fail_on_findings and failed > 0:
        sys.exit(1)


if __name__ == "__main__":
    main()

Assets 1

template.mdtext/markdown · 1.4 KB
Keep exploring