🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Articles

Build an AI Dockerfile Security Scanner with Claude API

Tutorial to build a tool that analyzes Dockerfiles for security vulnerabilities, bad practices, and layer optimization issues — using Claude API to generate specific, actionable fixes with a corrected Dockerfile.

Shubham6 min read
Share:Tweet

A Dockerfile that installs software as root, copies in .env files, uses latest tags, and has 20 unnecessary layers is depressingly common. Most teams write Dockerfiles once and never revisit them.

An AI Dockerfile scanner that catches these issues before CI builds them into production images is a practical tool that can run in pre-commit hooks or as a pull request check.

What We're Building

A Python tool + GitHub Actions workflow that:

  1. Reads a Dockerfile
  2. Sends it to Claude API for security and best practice analysis
  3. Returns specific issues with severity ratings
  4. Outputs a corrected Dockerfile with all critical issues fixed
  5. Can block CI if critical issues are found

Setup

bash
pip install anthropic python-dotenv

Step 1: Dockerfile Parser

python
import anthropic
import json
import os
import sys
import re
from pathlib import Path
from typing import Optional
 
 
def parse_dockerfile(content: str) -> dict:
    """Extract key information from Dockerfile for context-aware analysis."""
    lines = [l.strip() for l in content.split('\n') if l.strip() and not l.strip().startswith('#')]
 
    context = {
        "base_images": [],
        "exposed_ports": [],
        "run_commands": [],
        "copy_paths": [],
        "user_instructions": [],
        "environment_vars": [],
        "has_healthcheck": False,
        "is_multi_stage": False,
        "total_layers": 0
    }
 
    for line in lines:
        upper = line.upper()
        if upper.startswith("FROM"):
            context["base_images"].append(line)
            context["total_layers"] += 1
            if len(context["base_images"]) > 1:
                context["is_multi_stage"] = True
 
        elif upper.startswith("EXPOSE"):
            ports = re.findall(r'\d+', line)
            context["exposed_ports"].extend(ports)
 
        elif upper.startswith("RUN"):
            context["run_commands"].append(line[:200])
            context["total_layers"] += 1
 
        elif upper.startswith("COPY") or upper.startswith("ADD"):
            context["copy_paths"].append(line)
            context["total_layers"] += 1
 
        elif upper.startswith("USER"):
            context["user_instructions"].append(line)
 
        elif upper.startswith("ENV"):
            var_name = line.split('=')[0].replace('ENV', '').strip()
            context["environment_vars"].append(var_name)
 
        elif upper.startswith("HEALTHCHECK"):
            context["has_healthcheck"] = True
 
    return context
 
 
def detect_obvious_issues(content: str, context: dict) -> list[str]:
    """Fast pre-scan for obvious patterns before sending to Claude."""
    issues = []
 
    if not context["user_instructions"]:
        issues.append("CRITICAL: No USER instruction — container runs as root by default")
 
    if any("latest" in img.lower() for img in context["base_images"]):
        issues.append("HIGH: Using :latest tag — non-reproducible builds")
 
    # Check for copied sensitive files
    sensitive_patterns = [".env", "*.pem", "*.key", "id_rsa", "credentials", ".aws"]
    for path in context["copy_paths"]:
        for pattern in sensitive_patterns:
            if pattern.replace("*", "") in path.lower():
                issues.append(f"CRITICAL: Potentially copying sensitive file: {path}")
 
    if not context["has_healthcheck"]:
        issues.append("MEDIUM: No HEALTHCHECK instruction")
 
    if context["total_layers"] > 20:
        issues.append(f"MEDIUM: High layer count ({context['total_layers']}) — consider layer optimization")
 
    return issues

Step 2: Claude API Analysis

python
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
 
 
def analyze_dockerfile_with_claude(
    dockerfile_content: str,
    context: dict,
    pre_scan_issues: list[str]
) -> dict:
    """
    Send Dockerfile to Claude for comprehensive security and optimization analysis.
    """
    dockerfile_block = "```dockerfile\n" + dockerfile_content + "\n```"
    pre_scan_text = "\n".join(pre_scan_issues) if pre_scan_issues else "None"
 
    prompt = (
        "You are a Docker security expert reviewing a Dockerfile for production deployment.\n\n"
        "## Dockerfile to Review\n\n"
        + dockerfile_block + "\n\n"
        "## Pre-scan Findings\n" + pre_scan_text + "\n\n"
        "## Context\n"
        f"- Base images: {context['base_images']}\n"
        f"- Is multi-stage: {context['is_multi_stage']}\n"
        f"- Exposed ports: {context['exposed_ports']}\n"
        f"- Has USER instruction: {bool(context['user_instructions'])}\n"
        f"- Has HEALTHCHECK: {context['has_healthcheck']}\n"
        f"- Total layers: {context['total_layers']}\n"
        f"- Environment vars: {context['environment_vars']}\n\n"
        "---\n\n"
        'Provide a comprehensive analysis in this exact JSON structure:\n\n'
        '{"overall_score": 0-100, "production_ready": true/false, '
        '"security_issues": [{"severity": "critical|high|medium|low", '
        '"line_hint": "...", "issue": "...", "attack_vector": "...", "fix": "..."}], '
        '"optimization_issues": [{"severity": "high|medium|low", "issue": "...", '
        '"impact": "...", "fix": "..."}], '
        '"positive_practices": ["..."], '
        '"corrected_dockerfile": "Complete corrected Dockerfile", '
        '"summary": "2-3 sentence verdict"}\n\n'
        "Security: check for root user, sensitive files in COPY, hardcoded secrets, :latest tags, no HEALTHCHECK.\n"
        "Optimization: unchained RUN commands, COPY . . before deps, oversized base image, dev deps in prod.\n"
        "Return ONLY valid JSON."
    )
 
    response = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=4000,
        messages=[{"role": "user", "content": prompt}]
    )
 
    result_text = response.content[0].text.strip()
    if result_text.startswith("```"):
        result_text = result_text.split("```")[1]
        if result_text.startswith("json"):
            result_text = result_text[4:]
        if result_text.endswith("```"):
            result_text = result_text[:-3]
 
    return json.loads(result_text)

Step 3: CLI with Exit Codes for CI

python
def format_report(analysis: dict, filepath: str) -> str:
    """Format analysis as terminal-friendly report."""
    score = analysis.get("overall_score", 0)
    production_ready = analysis.get("production_ready", False)
 
    status_icon = "✅" if production_ready else "❌"
    score_color = "🟢" if score >= 80 else "🟡" if score >= 60 else "🔴"
 
    lines = [
        f"\n{'='*60}",
        f"  DOCKERFILE SECURITY SCAN: {filepath}",
        f"{'='*60}",
        f"{status_icon}  Score: {score}/100 — {'Production Ready' if production_ready else 'NOT Production Ready'}",
        f"",
    ]
 
    security_issues = analysis.get("security_issues", [])
    if security_issues:
        lines.append("🔒  SECURITY ISSUES")
        for issue in security_issues:
            sev = issue.get("severity", "").upper()
            icon = "🔴" if sev == "CRITICAL" else "🟠" if sev == "HIGH" else "🟡"
            lines.append(f"  {icon} [{sev}] {issue.get('line_hint', '')}:")
            lines.append(f"     {issue.get('issue', '')}")
            lines.append(f"     Attack vector: {issue.get('attack_vector', '')}")
            lines.append(f"     Fix: {issue.get('fix', '')}")
        lines.append("")
 
    opt_issues = analysis.get("optimization_issues", [])
    if opt_issues:
        lines.append("⚡  OPTIMIZATION ISSUES")
        for issue in opt_issues:
            sev = issue.get("severity", "").upper()
            lines.append(f"  [{sev}] {issue.get('issue', '')}")
            lines.append(f"     Fix: {issue.get('fix', '')}")
        lines.append("")
 
    if analysis.get("positive_practices"):
        lines.append("✅  GOOD PRACTICES")
        for p in analysis["positive_practices"]:
            lines.append(f"  • {p}")
        lines.append("")
 
    lines.append(f"📋  VERDICT")
    lines.append(f"  {analysis.get('summary', '')}")
 
    return "\n".join(lines)
 
 
def scan_dockerfile(
    dockerfile_path: str,
    save_fixed: bool = False,
    fail_on_critical: bool = True
) -> int:
    """
    Scan a Dockerfile. Returns exit code:
    0 = no critical issues
    1 = critical or high security issues found
    2 = file not found or parse error
    """
    try:
        content = Path(dockerfile_path).read_text()
    except FileNotFoundError:
        print(f"Error: {dockerfile_path} not found")
        return 2
 
    context = parse_dockerfile(content)
    pre_scan = detect_obvious_issues(content, context)
 
    print(f"Analyzing {dockerfile_path}...")
    analysis = analyze_dockerfile_with_claude(content, context, pre_scan)
 
    print(format_report(analysis, dockerfile_path))
 
    if save_fixed and analysis.get("corrected_dockerfile"):
        fixed_path = dockerfile_path.replace("Dockerfile", "Dockerfile.fixed")
        Path(fixed_path).write_text(analysis["corrected_dockerfile"])
        print(f"\n💾  Fixed Dockerfile saved to: {fixed_path}")
 
    # Determine exit code for CI
    has_critical = any(
        i.get("severity") in ["critical", "high"]
        for i in analysis.get("security_issues", [])
    )
 
    if fail_on_critical and has_critical:
        print("\n❌  CRITICAL/HIGH security issues found. Failing CI.")
        return 1
 
    return 0
 
 
if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="AI Dockerfile security scanner")
    parser.add_argument("dockerfile", nargs="?", default="Dockerfile")
    parser.add_argument("--save-fixed", action="store_true")
    parser.add_argument("--no-fail", action="store_true", help="Don't exit 1 on critical issues")
    args = parser.parse_args()
 
    from dotenv import load_dotenv
    load_dotenv()
 
    sys.exit(scan_dockerfile(args.dockerfile, args.save_fixed, not args.no_fail))

GitHub Actions Integration

yaml
# .github/workflows/dockerfile-scan.yml
name: Dockerfile Security Scan
 
on:
  pull_request:
    paths:
      - '**/Dockerfile'
      - '**/Dockerfile.*'
 
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
 
      - name: Install dependencies
        run: pip install anthropic
 
      - name: Scan all Dockerfiles
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
        run: |
          EXIT_CODE=0
          for dockerfile in $(find . -name "Dockerfile*" -not -path "./.git/*"); do
            python dockerfile_scanner.py "$dockerfile" || EXIT_CODE=1
          done
          exit $EXIT_CODE

Example Output

============================================================
  DOCKERFILE SECURITY SCAN: Dockerfile
============================================================
❌  Score: 32/100 — NOT Production Ready

🔒  SECURITY ISSUES
  🔴 [CRITICAL] No USER instruction:
     Container runs as root, giving attackers full system access if container is compromised
     Attack vector: If any RUN command downloads malicious content, attacker has root
     Fix: Add "USER appuser" and create the user: RUN addgroup -S app && adduser -S appuser -G app

  🔴 [CRITICAL] COPY . . before npm install:
     .env file in project root will be copied into the image
     Attack vector: Anyone with image access can extract all environment secrets
     Fix: Add .dockerignore with .env, *.pem, .git; use Secrets at runtime not build time

🎯  VERDICT
  This Dockerfile has critical security vulnerabilities — running as root and potentially
  leaking secrets through the build context. Not suitable for production. Fix the USER
  instruction and .dockerignore before deployment.

More AI security tools? Read our AI Kubernetes YAML explainer and validator and Dockerfile security best practices.

🔧

Today I Fixed

Short real fixes from production — posted daily

Browse fixes
Newsletter

Stay ahead of the curve

Get the latest DevOps, Kubernetes, AWS, and AI/ML guides delivered straight to your inbox. No spam — just practical engineering content.

Related Articles

Comments