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.
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
$ 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 immediatelySetup
pip install anthropic click rich python-dotenvCore: Claude-Powered Analysis
# 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
# 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
# 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.jsonExample Dockerfile That Gets Flagged
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 MEDIUMAfter 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
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
Build an AI-Powered Dockerfile Optimizer Using Claude API
Feed any Dockerfile to Claude and get back a production-ready version with smaller image size, better layer caching, security fixes, and an explanation of every change.
Build an AI-Powered CI/CD Pipeline Failure Analyzer with LangChain
Build a tool that automatically reads CI/CD failure logs, uses LangChain + Claude to diagnose the root cause, and posts a clear explanation with fix suggestions to your PR.
Build a DevSecOps Pipeline from Scratch (2026 Project Walkthrough)
A complete end-to-end DevSecOps pipeline with SAST, container scanning, secrets detection, DAST, and supply chain security using open-source tools.