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

Build an AI-Powered Dockerfile Security Scanner with Claude

Build a tool that scans Dockerfiles for security issues using Claude API — finds hardcoded secrets, root users, unscanned base images, and missing security best practices.

DevOpsBoysJun 2, 20265 min read
Share:Tweet

Dockerfile security mistakes are common and expensive to fix in production. Hardcoded credentials, running as root, outdated base images — a simple scan catches them before they ship.

This tool uses Claude to do deep security analysis, not just pattern matching.


What We're Building

bash
$ docker-scan ./Dockerfile
 
🔍 Scanning Dockerfile...
 
CRITICAL (2):
  [Line 8]  Hardcoded AWS credentials detected: AWS_ACCESS_KEY_ID=AKIA...
  [Line 23] Container runs as root use USER instruction to set non-root user
 
HIGH (3):
  [Line 3]  Base image 'node:14' uses EOL Node.js version. Use node:20-alpine
  [Line 15] RUN apt-get install without --no-install-recommends increases image size and attack surface
  [Line 19] curl | bash pipe pattern - running untrusted scripts
 
MEDIUM (2):
  [Line 1]  No specific image digest use 'node:20-alpine@sha256:...' for reproducibility
  [Line 31] HEALTHCHECK not defined container health can't be monitored
 
Score: 34/100 — Fix CRITICAL issues immediately

Setup

bash
pip install anthropic click rich python-dotenv

Core: Claude-Powered Analysis

python
# scanner.py
import anthropic
import json
import os
from dataclasses import dataclass
 
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))
 
@dataclass
class SecurityFinding:
    severity: str  # CRITICAL, HIGH, MEDIUM, LOW, INFO
    line: int
    issue: str
    recommendation: str
    cve_related: bool = False
 
SYSTEM_PROMPT = """You are a Docker security expert and DevSecOps specialist.
Analyze the provided Dockerfile for security vulnerabilities and best practice violations.
 
Check for:
1. CRITICAL: Hardcoded secrets/credentials, exposed private keys, passwords in ENV/ARG
2. CRITICAL: Running as root (no USER instruction or USER root)
3. HIGH: EOL/outdated base images, known vulnerable base images
4. HIGH: Unsafe practices: curl|bash, wget|sh, arbitrary code execution
5. HIGH: Excessive privileges (--privileged, --cap-add ALL)
6. HIGH: Secrets passed as build args
7. MEDIUM: Large attack surface (no --no-install-recommends)
8. MEDIUM: No HEALTHCHECK instruction
9. MEDIUM: Using :latest tag (not reproducible)
10. MEDIUM: COPY . . (copies everything including .env, .git)
11. LOW: Missing .dockerignore usage indicators
12. LOW: Non-minimal base image (using ubuntu when alpine works)
13. INFO: Image size optimization opportunities
 
For each finding, provide:
- Exact line number
- Severity (CRITICAL/HIGH/MEDIUM/LOW/INFO)
- Clear description of the issue
- Specific fix recommendation
 
Return as JSON array:
[{"line": 5, "severity": "CRITICAL", "issue": "...", "recommendation": "...", "cve_related": false}]
 
Return ONLY the JSON array, no other text."""
 
def scan_dockerfile(content: str) -> list[SecurityFinding]:
    """Scan a Dockerfile for security issues using Claude."""
    
    # Add line numbers to content for Claude to reference
    numbered_content = "\n".join(
        f"{i+1:3}: {line}" 
        for i, line in enumerate(content.split('\n'))
    )
    
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=2000,
        system=SYSTEM_PROMPT,
        messages=[{
            "role": "user",
            "content": f"Scan this Dockerfile:\n\n{numbered_content}"
        }]
    )
    
    try:
        findings_data = json.loads(response.content[0].text)
        return [SecurityFinding(**f) for f in findings_data]
    except json.JSONDecodeError:
        return []
 
def calculate_score(findings: list[SecurityFinding]) -> int:
    """Calculate security score 0-100."""
    deductions = {
        "CRITICAL": 25,
        "HIGH": 15,
        "MEDIUM": 8,
        "LOW": 3,
        "INFO": 0
    }
    total_deduction = sum(deductions.get(f.severity, 0) for f in findings)
    return max(0, 100 - total_deduction)

CLI Interface

python
# cli.py
import click
import sys
from pathlib import Path
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich import box
from scanner import scan_dockerfile, calculate_score, SecurityFinding
 
console = Console()
 
SEVERITY_COLORS = {
    "CRITICAL": "red",
    "HIGH": "orange3",
    "MEDIUM": "yellow",
    "LOW": "blue",
    "INFO": "dim",
}
 
SEVERITY_ICONS = {
    "CRITICAL": "🔴",
    "HIGH": "🟠",
    "MEDIUM": "🟡",
    "LOW": "🔵",
    "INFO": "⚪",
}
 
def score_color(score: int) -> str:
    if score >= 80: return "green"
    if score >= 60: return "yellow"
    if score >= 40: return "orange3"
    return "red"
 
def score_label(score: int) -> str:
    if score >= 80: return "Secure"
    if score >= 60: return "Needs Improvement"
    if score >= 40: return "At Risk"
    return "Critical Issues — Fix Immediately"
 
@click.command()
@click.argument("dockerfile", default="Dockerfile")
@click.option("--min-severity", default="LOW", 
              type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]),
              help="Minimum severity to report")
@click.option("--json-output", is_flag=True, help="Output results as JSON")
@click.option("--fail-on", default="CRITICAL",
              type=click.Choice(["CRITICAL", "HIGH", "MEDIUM", "LOW"]),
              help="Exit with error if issues of this severity found")
def main(dockerfile, min_severity, json_output, fail_on):
    """Scan a Dockerfile for security vulnerabilities."""
    
    path = Path(dockerfile)
    if not path.exists():
        console.print(f"[red]Error: {dockerfile} not found[/red]")
        sys.exit(1)
    
    content = path.read_text()
    
    if not json_output:
        console.print(f"\n[dim]🔍 Scanning {dockerfile}...[/dim]\n")
    
    findings = scan_dockerfile(content)
    score = calculate_score(findings)
    
    # Filter by severity
    severity_order = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]
    min_idx = severity_order.index(min_severity)
    filtered = [f for f in findings if severity_order.index(f.severity) <= min_idx]
    
    if json_output:
        import json
        output = {
            "score": score,
            "findings": [
                {"line": f.line, "severity": f.severity, 
                 "issue": f.issue, "recommendation": f.recommendation}
                for f in filtered
            ]
        }
        print(json.dumps(output, indent=2))
    else:
        # Group by severity
        by_severity = {}
        for f in filtered:
            by_severity.setdefault(f.severity, []).append(f)
        
        for sev in severity_order:
            group = by_severity.get(sev, [])
            if not group:
                continue
            
            color = SEVERITY_COLORS[sev]
            icon = SEVERITY_ICONS[sev]
            console.print(f"[{color} bold]{icon} {sev} ({len(group)})[/{color} bold]")
            
            for finding in group:
                console.print(f"  [dim][Line {finding.line}][/dim] {finding.issue}")
                console.print(f"  [green]  → Fix:[/green] {finding.recommendation}\n")
        
        # Score
        s_color = score_color(score)
        s_label = score_label(score)
        console.print(Panel(
            f"[{s_color} bold]Security Score: {score}/100[/{s_color} bold]\n[dim]{s_label}[/dim]",
            box=box.ROUNDED
        ))
    
    # Exit code for CI
    fail_severities = severity_order[:severity_order.index(fail_on) + 1]
    has_blocking = any(f.severity in fail_severities for f in findings)
    
    if has_blocking:
        if not json_output:
            console.print(f"\n[red]❌ Blocking {fail_on}+ issues found. Fix before deploying.[/red]")
        sys.exit(1)
 
if __name__ == "__main__":
    main()

CI/CD Integration

yaml
# GitHub Actions
- name: Dockerfile Security Scan
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
  run: |
    pip install anthropic click rich
    python cli.py Dockerfile --fail-on HIGH --min-severity MEDIUM
 
# GitLab CI
dockerfile-scan:
  script:
    - pip install anthropic click rich
    - python cli.py Dockerfile --fail-on CRITICAL --json-output > scan-results.json
  artifacts:
    reports:
      junit: scan-results.json

Example Dockerfile That Gets Flagged

dockerfile
FROM ubuntu:latest                    # MEDIUM: use specific version + alpine
 
ARG AWS_KEY=AKIAIOSFODNN7EXAMPLE      # CRITICAL: credential in build arg
ENV AWS_SECRET=wJalrXUtnFEMI/K7MDENG # CRITICAL: credential in ENV
 
RUN apt-get update && apt-get install -y curl nodejs  # MEDIUM: no --no-install-recommends
 
RUN curl -sSL https://scripts.example.com/install.sh | bash  # HIGH: curl|bash
 
COPY . /app                           # MEDIUM: copies .env, .git, node_modules
 
WORKDIR /app
RUN npm install
 
EXPOSE 3000
CMD ["node", "server.js"]
# Missing: USER instruction (running as root!) CRITICAL
# Missing: HEALTHCHECK                           MEDIUM

After scanning, the tool tells you exactly what to fix.


Cost Per Scan

~500–800 input tokens per Dockerfile = ~$0.002 per scan at Claude Sonnet pricing.
At 100 scans/day in CI = ~$0.20/day. Negligible.

Get your Anthropic API key to start building. Pair with Trivy for vulnerability scanning of the built image — Claude catches Dockerfile mistakes, Trivy catches CVEs in packages.

🔧

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