Container security is a game of layers. Your app code could be bulletproof, but if your Dockerfile runs as root, exposes ports you don’t need, or bases on an unpatched image with 47 known CVEs, you’re handing the keys to attackers. Most container scanning tools tell you what’s wrong—they find missing updates, exposed secrets, bad configurations—but they don’t tell you why or give you guidance on fixing it.
In this article, we’re building a container security reviewer with Claude Code that goes beyond basic scanning. It reads your Dockerfiles, understands your build context, checks base image provenance, validates runtime configurations, and gives you actionable feedback in plain English. No jargon, no false positives—just “here’s what you should fix and why.”
Why Container Security Matters (and Why It’s Hard)
Here’s the thing about container security: most exploits don’t target the container runtime itself. They target what’s inside it—vulnerabilities in dependencies, secrets hardcoded in layers, unnecessary privileges granted at startup. A single misconfigured Dockerfile can undermine your entire deployment.
The challenge is context. A tool can tell you that nginx 1.19 has a vulnerability, but it doesn’t know:
- Is this a public-facing service or internal-only?
- Is the vulnerability exploitable in your use case?
- Are you planning to update soon?
- What are the tradeoffs of upgrading?
A security reviewer augmented with Claude can answer those questions. It can:
- Understand intent: Read the Dockerfile and infer what the application does
- Contextualize risk: Weigh vulnerabilities against exposure and usability
- Suggest fixes: Propose concrete changes with rationale
- Learn patterns: Remember what you’ve approved and what you’ve fixed
- Scale reviews: Handle dozens of Dockerfiles in minutes, not days
The Hidden Layer: Why Most Container Scanning Fails
When we talk about container security, we’re usually talking about vulnerability scanning. Tools like Trivy, Snyk, and Grype scan images and report CVEs. But here’s what they can’t do: understand whether that vulnerability actually matters in your specific context.
A tool might flag a critical CVE in a library you never use. It might miss a subtle configuration issue that cascades into a security failure. It might not understand the relationship between your base image choices, your runtime configuration, and your actual threat model.
This is where AI-augmented security review becomes powerful. Instead of wading through hundreds of false positives, you get a security analyst that understands your application, your infrastructure, and your risk tolerance. It asks context questions. It prioritizes remediations by impact and effort. It explains the “why” behind recommendations, not just the “what.”
Over time, as you review findings and approve or reject recommendations, the system learns your organization’s security patterns. What’s acceptable in your dev environment becomes unacceptable in production. The system learns these distinctions and stops wasting your time on false positives while catching real issues.
Understanding the Threat Model
Before we build a scanner, let’s clarify what we’re actually protecting against. Container security isn’t monolithic. Different threats apply in different contexts:
Supply chain attacks: Someone compromises a base image or dependency, injecting malicious code upstream. Your image inherits the compromise.
Runtime exploitation: An attacker gains RCE through an unpatched vulnerability in your application or dependencies. If the container runs as root, they own the host.
Data exfiltration: Secrets are hardcoded in the image or layer history. An attacker pulls the image and extracts them.
Privilege escalation: The container runs with unnecessary Linux capabilities or Docker privileges. A compromise that would be contained becomes a system-wide disaster.
Denial of service: Misconfigured resource limits allow a single container to consume all CPU, memory, or disk, crashing the host or neighboring containers.
Each threat requires different defensive measures. A secure base image doesn’t help if you’ve hardcoded an API key. Pinned versions don’t matter if you’re running as root. Our scanner needs to address all of these dimensions, not just scan for CVEs.
Architecture: From Dockerfile to Security Report
Let’s sketch what we’re building:
Dockerfile
↓
Claude Code security reviewer
├─ Parse Dockerfile and extract metadata
├─ Fetch base image details (provenance, CVEs, tags)
├─ Analyze layers for secrets, permissions, unnecessary tools
├─ Check runtime config (uid/gid, capabilities, mounts)
├─ Cross-reference with vulnerability databases
├─ Generate detailed security report
└─ Suggest remediations with reasoning
↓
Security report + recommendations
Unlike off-the-shelf scanners that are all-or-nothing, this system is collaborative: you review its findings, provide context, and it learns what’s acceptable in your environment.
The Economics of Container Security
Let me be blunt: container security is an economic problem, not just a technical one. The cost of a security breach—cleanup, forensics, legal liability, reputation damage, lost customers—can be millions. The cost of preventing that breach through proper security practices is relatively small.
But here’s the hidden economics: without automated security review, the cost of security practices is high. Each Dockerfile review takes time. Each image scan takes time. Each remediation requires human judgment and effort. Most organizations skip these steps because they’re expensive.
Automated security review changes the economics. You can scan hundreds of images in minutes. You can prioritize findings by real risk (not just CVE score). You can track remediation progress across your organization. The cost per image drops dramatically, making comprehensive security coverage economically feasible.
This is why Claude Code for container security matters: it doesn’t just make you more secure, it makes security cost-effective. You can have both strong security posture and reasonable operational overhead.
The Trust and Compliance Story
Beyond direct risk, there’s another economic factor: trust. When you’re evaluating security posture to investors, enterprise customers, or potential acquirers, having evidence of systematic security practices matters. A documented security review process for all containers shows due diligence.
If you get breached and an investigation shows you never reviewed security or updated images, liability increases. If an investigation shows you had a systematic process, documented findings, and demonstrated good-faith remediation efforts, you’re in a much stronger position.
This isn’t to encourage theater over substance. Real security matters. But real security is more valuable when it’s documented and defensible.
Building the Container Security Scanner
Before we dive into code, let’s talk about what makes a container security scanner valuable. The best security tools don’t just find problems—they educate. They teach engineers about attack surfaces, security principles, and why constraints matter. A Dockerfile review that ends with “Add USER directive” is useless without explanation. But a review that says “Running as root means any container escape gives attackers full system access” helps engineers internalize the principle.
This is where Claude Code shines. It can read code, understand context, and explain reasoning. When your reviewer flags a missing index on a database query, it understands that this index matters because the query filters on that column frequently. When it flags a hardcoded secret, it understands that secrets in images get leaked in registries, in pull logs, in image history. It’s not just checking boxes—it’s providing genuine security analysis.
The scanner we’re building has several key components: a Dockerfile parser that understands the image structure, a base image analyzer that checks for vulnerabilities and best practices, a secrets detector that finds hardcoded credentials, a runtime configuration validator that ensures proper privilege separation, and a synthesizer that uses Claude to generate a prioritized remediation plan.
Each component serves a purpose. The parser gives us structured data. The analyzers check specific dimensions. The synthesizer stitches findings together and explains them. By separating concerns, we can improve each component independently. If you discover a new type of security issue, you add an analyzer. If you want to tune prioritization, you adjust the synthesizer prompt.
The Architecture Advantage
One key insight: the architecture should support learning. As you use the system, you’ll discover false positives that don’t actually matter in your environment. You’ll discover patterns that are common across your fleet. You’ll learn what remediations are actually practical versus idealistic.
A good security framework captures these learnings. You can maintain an “approved exceptions” list for known false positives. You can track which remediations have been successfully implemented across your fleet, creating templates for new findings. You can build a knowledge base of “for this issue in our Java services, we typically do X, and it’s proven effective.”
This is the organizational learning layer. It’s not in the code—it’s in how you use the code over time.
Let’s write TypeScript code that does the heavy lifting:
interface DockerfileAnalysis {
baseImage: string;
layers: DockerfileLayer[];
exposedPorts: string[];
environmentVariables: Record<string, string>;
volumes: string[];
entrypoint: string | null;
user: string | null;
workdir: string;
}
interface DockerfileLayer {
instruction: string;
arguments: string[];
lineNumber: number;
riskLevel: "critical" | "high" | "medium" | "low" | "info";
}
interface SecurityFinding {
severity: "critical" | "high" | "medium" | "low" | "info";
category: string;
finding: string;
affectedLines: number[];
recommendation: string;
rationale: string;
}
interface SecurityReport {
dockerfile: string;
baseImage: string;
findings: SecurityFinding[];
riskScore: number; // 0-100, higher is worse
overallAssessment: string;
prioritizedRemediations: string[];
}
class DockerfileSecurityReviewer {
private client: Anthropic;
private vulnerabilityCache: Map<string, string> = new Map();
constructor() {
this.client = new Anthropic();
}
async reviewDockerfile(dockerfilePath: string): Promise<SecurityReport> {
console.log(`\n📋 Reviewing Dockerfile: ${dockerfilePath}`);
// Step 1: Read and parse the Dockerfile
const dockerfileContent = fs.readFileSync(dockerfilePath, "utf-8");
const analysis = this.parseDockerfile(dockerfileContent);
// Step 2: Analyze base image for vulnerabilities
console.log(`Analyzing base image: ${analysis.baseImage}`);
const baseImageRisks = await this.analyzeBaseImage(analysis.baseImage);
// Step 3: Check for secrets and bad practices
const codeIssues = await this.analyzeDockerfileContent(
dockerfileContent,
analysis,
);
// Step 4: Validate runtime configuration
const runtimeRisks = this.analyzeRuntimeConfiguration(analysis);
// Step 5: Synthesize findings
const allFindings = [
...baseImageRisks,
...codeIssues,
...runtimeRisks,
].sort((a, b) => {
const severity = {
critical: 4,
high: 3,
medium: 2,
low: 1,
info: 0,
};
return severity[b.severity] - severity[a.severity];
});
// Step 6: Get Claude's synthesis and prioritized recommendations
const report = await this.synthesizeReport(
dockerfilePath,
analysis,
allFindings,
);
return report;
}
private parseDockerfile(content: string): DockerfileAnalysis {
const lines = content.split("\n");
const analysis: DockerfileAnalysis = {
baseImage: "",
layers: [],
exposedPorts: [],
environmentVariables: {},
volumes: [],
entrypoint: null,
user: null,
workdir: "/",
};
lines.forEach((line, idx) => {
const trimmed = line.trim();
if (trimmed.startsWith("#") || trimmed === "") return;
const match = trimmed.match(/^([A-Z]+)\s+(.*)$/);
if (!match) return;
const [, instruction, args] = match;
const layer: DockerfileLayer = {
instruction,
arguments: [args],
lineNumber: idx + 1,
riskLevel: "info",
};
switch (instruction) {
case "FROM":
analysis.baseImage = args.trim();
break;
case "EXPOSE":
analysis.exposedPorts.push(args.trim());
break;
case "ENV":
const envMatch = args.match(/^(\w+)=(.*)$/);
if (envMatch) {
analysis.environmentVariables[envMatch[1]] = envMatch[2];
}
break;
case "VOLUME":
analysis.volumes.push(args.trim());
break;
case "ENTRYPOINT":
analysis.entrypoint = args.trim();
break;
case "USER":
analysis.user = args.trim();
break;
case "WORKDIR":
analysis.workdir = args.trim();
break;
case "RUN":
if (args.includes("root") || args.includes("sudo")) {
layer.riskLevel = "medium";
}
break;
}
analysis.layers.push(layer);
});
return analysis;
}
private async analyzeBaseImage(
baseImage: string,
): Promise<SecurityFinding[]> {
const findings: SecurityFinding[] = [];
// Check for scratch image (safest option)
if (baseImage === "scratch") {
return [
{
severity: "info",
category: "base_image",
finding: "Using 'scratch' base image",
affectedLines: [],
recommendation:
"Excellent security choice for single-binary deployments",
rationale:
"Scratch contains nothing but your application, minimizing attack surface",
},
];
}
// Parse base image name and tag
const [imageName, tag = "latest"] = baseImage.split(":");
// Check for common anti-patterns
if (!tag || tag === "latest") {
findings.push({
severity: "high",
category: "base_image",
finding: `Using ${imageName}${tag ? ":" + tag : ""} without pinned version`,
affectedLines: [1], // Usually FROM is line 1
recommendation: `Pin to a specific version: ${imageName}:1.24.0 (or hash: ${imageName}@sha256:abc123...)`,
rationale:
"Latest tags change over time, making deployments unpredictable and potentially insecure. Always pin versions.",
});
}
// Simulate CVE lookup (in production, query actual vulnerability DB)
const cveData = await this.fetchImageCVEs(imageName);
if (cveData.criticalCount > 0) {
findings.push({
severity: "critical",
category: "base_image_vulnerabilities",
finding: `${cveData.criticalCount} critical CVEs found in ${imageName}:${tag}`,
affectedLines: [1],
recommendation: `Upgrade to ${cveData.recommendedTag} which has ${cveData.reducedCount} known vulnerabilities`,
rationale: `${cveData.description}`,
});
}
// Check for official image status
if (!imageName.includes("/") || imageName.startsWith("library/")) {
findings.push({
severity: "info",
category: "base_image",
finding: `${imageName} is an official Docker image`,
affectedLines: [1],
recommendation: "No action needed",
rationale:
"Official images are maintained by Docker and have security scanning",
});
} else if (!imageName.includes(".") && !imageName.includes("/")) {
findings.push({
severity: "medium",
category: "base_image",
finding: `${imageName} is a third-party image without explicit registry`,
affectedLines: [1],
recommendation: `Specify full registry path: docker.io/${imageName} or verify provenance`,
rationale:
"Third-party images should be explicitly trusted and pinned by hash when possible",
});
}
return findings;
}
private async fetchImageCVEs(imageName: string): Promise<any> {
// Mock implementation - would call actual CVE database
// (e.g., Trivy API, Snyk, or registry scan results)
const cveData: Record<string, any> = {
nginx: {
criticalCount: 2,
reducedCount: 0,
recommendedTag: "1.25.4-alpine",
description:
"Recent versions of nginx 1.24 have a buffer overflow vulnerability (CVE-2024-1234) and privilege escalation issue (CVE-2024-1235)",
},
ubuntu: {
criticalCount: 5,
reducedCount: 1,
recommendedTag: "22.04.3-LTS",
description:
"Ubuntu 20.04 has 5 critical vulnerabilities in OpenSSL, OpenSSH, and kernel",
},
alpine: {
criticalCount: 0,
reducedCount: 0,
recommendedTag: "3.19.1",
description:
"Alpine is minimalist, but keep updated for security patches",
},
};
const baseImageName = imageName.split("/").pop()!.split(":")[0];
return (
cveData[baseImageName] || {
criticalCount: 0,
reducedCount: 0,
recommendedTag: imageName,
description: "No specific CVE data available; verify independently",
}
);
}
private async analyzeDockerfileContent(
content: string,
analysis: DockerfileAnalysis,
): Promise<SecurityFinding[]> {
const findings: SecurityFinding[] = [];
const lines = content.split("\n");
// Check for embedded secrets
const secretPatterns = [
{ regex: /password\s*=\s*[^\s]+/, name: "password" },
{ regex: /api[_-]?key\s*=\s*[^\s]+/, name: "API key" },
{ regex: /secret\s*=\s*[^\s]+/, name: "secret" },
{ regex: /token\s*=\s*[^\s]+/, name: "token" },
{ regex: /aws[_-]?secret[_-]?access[_-]?key\s*=/, name: "AWS secret" },
];
lines.forEach((line, idx) => {
secretPatterns.forEach((pattern) => {
if (pattern.regex.test(line.toLowerCase())) {
findings.push({
severity: "critical",
category: "hardcoded_secrets",
finding: `Potential ${pattern.name} hardcoded in Dockerfile`,
affectedLines: [idx + 1],
recommendation:
"Use Docker secrets or build-time arguments (--build-arg), never hardcoded values",
rationale:
"Secrets in Dockerfiles are baked into the image and leaked in registries. Use Docker Compose secrets or external secret management.",
});
}
});
// Check for RUN with apt-get/yum without cleanup
if (
line.includes("RUN") &&
line.includes("apt-get install") &&
!line.includes("apt-get clean")
) {
findings.push({
severity: "low",
category: "image_optimization",
finding: `apt-get install not cleaned up (line ${idx + 1})`,
affectedLines: [idx + 1],
recommendation:
"Combine RUN commands and clean apt cache: RUN apt-get update && apt-get install -y pkg && rm -rf /var/lib/apt/lists/*",
rationale:
"Unnecessary package manager caches inflate image size and provide no runtime value",
});
}
// Check for multiple FROM statements (multi-stage build)
if (line.trim().startsWith("FROM")) {
// Multi-stage is good, nothing to report
}
// Check for COPY /** and ADD /**
if (line.includes("COPY") && line.includes("/*")) {
findings.push({
severity: "medium",
category: "layer_efficiency",
finding: `Copying entire source tree into image (line ${idx + 1})`,
affectedLines: [idx + 1],
recommendation:
"Use .dockerignore to exclude unnecessary files (node_modules, .git, test files, etc.)",
rationale:
"Large layers slow down pushes/pulls and increase image size. .dockerignore filters unwanted files.",
});
}
});
return findings;
}
private analyzeRuntimeConfiguration(
analysis: DockerfileAnalysis,
): SecurityFinding[] {
const findings: SecurityFinding[] = [];
// Check if running as root
if (!analysis.user || analysis.user === "root") {
findings.push({
severity: "high",
category: "privilege_management",
finding: "Container runs as root (uid=0)",
affectedLines: [],
recommendation:
"Add USER directive: USER appuser (create non-root user in Dockerfile)",
rationale:
"Running as root means any container escape or RCE gives attacker full system access. Always use least privilege.",
});
}
// Check for exposed ports
if (analysis.exposedPorts.length > 0) {
// Expose is just metadata, but worth reviewing
findings.push({
severity: "info",
category: "exposed_ports",
finding: `Container exposes ports: ${analysis.exposedPorts.join(", ")}`,
affectedLines: [],
recommendation:
"Verify all exposed ports are intentional and documented. Use port mapping (docker run -p) for runtime control.",
rationale:
"EXPOSE is informational but documents the service interface. Ensure no unintended ports are exposed.",
});
}
// Check for unnecessary volumes
if (analysis.volumes.includes("/") || analysis.volumes.includes("/*")) {
findings.push({
severity: "high",
category: "mount_points",
finding: "Broad VOLUME mount defined",
affectedLines: [],
recommendation: 'Be specific: VOLUME ["/data", "/logs"]',
rationale:
"Broad volumes can leak sensitive host data. Mount only required directories.",
});
}
// Check workdir
if (analysis.workdir === "/" || analysis.workdir.startsWith("/usr")) {
findings.push({
severity: "medium",
category: "working_directory",
finding: `WORKDIR is ${analysis.workdir} (system directory)`,
affectedLines: [],
recommendation: "Use a dedicated directory: WORKDIR /app",
rationale:
"Working in system directories risks conflicts and makes debugging harder. Use /app or /home/app.",
});
}
return findings;
}
private async synthesizeReport(
dockerfilePath: string,
analysis: DockerfileAnalysis,
findings: SecurityFinding[],
): Promise<SecurityReport> {
const prompt = `
You are a container security expert. Analyze these security findings from a Dockerfile review:
Dockerfile: ${dockerfilePath}
Base Image: ${analysis.baseImage}
FINDINGS:
${findings
.map(
(f) =>
`[${f.severity.toUpperCase()}] ${f.category}: ${f.finding}
Recommendation: ${f.recommendation}
Rationale: ${f.rationale}`,
)
.join("\n\n")}
Provide a JSON response with:
{
"overallAssessment": "1-2 sentence summary of security posture",
"riskScore": 0-100,
"prioritizedRemediations": [
"1. Fix critical issue (biggest impact, easiest first)",
"2. Fix high severity issue",
"..."
]
}
Focus on actionable items that meaningfully improve security.
`;
const message = await this.client.messages.create({
model: "claude-opus-4-1",
max_tokens: 1024,
messages: [
{
role: "user",
content: prompt,
},
],
});
try {
const responseText =
message.content[0].type === "text" ? message.content[0].text : "{}";
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
const synthesis = jsonMatch ? JSON.parse(jsonMatch[0]) : {};
return {
dockerfile: dockerfilePath,
baseImage: analysis.baseImage,
findings,
riskScore: synthesis.riskScore || 0,
overallAssessment:
synthesis.overallAssessment || "Unable to generate assessment",
prioritizedRemediations: synthesis.prioritizedRemediations || [],
};
} catch (error) {
console.error("Failed to synthesize report:", error);
return {
dockerfile: dockerfilePath,
baseImage: analysis.baseImage,
findings,
riskScore: 75, // Conservative estimate if synthesis fails
overallAssessment:
"Review findings above. High risk due to multiple security issues.",
prioritizedRemediations: findings
.filter((f) => ["critical", "high"].includes(f.severity))
.map((f) => f.recommendation),
};
}
}
}
// Example usage
async function reviewDockerfiles(directory: string): Promise<void> {
const reviewer = new DockerfileSecurityReviewer();
const dockerfiles = [
path.join(directory, "Dockerfile"),
path.join(directory, "Dockerfile.prod"),
].filter((f) => fs.existsSync(f));
for (const dockerfile of dockerfiles) {
const report = await reviewer.reviewDockerfile(dockerfile);
console.log("\n" + "=".repeat(80));
console.log(`📄 SECURITY REPORT: ${report.dockerfile}`);
console.log("=".repeat(80));
console.log(`Base Image: ${report.baseImage}`);
console.log(`Risk Score: ${report.riskScore}/100`);
console.log(`\nAssessment: ${report.overallAssessment}`);
if (report.findings.length > 0) {
console.log(`\n🔍 Findings (${report.findings.length} total):`);
report.findings.forEach((finding, idx) => {
const icon =
{
critical: "🚨",
high: "⚠️",
medium: "⚡",
low: "ℹ️",
info: "ℹ️",
}[finding.severity] || "•";
console.log(
`\n${icon} [${finding.severity.toUpperCase()}] ${finding.category}`,
);
console.log(` Finding: ${finding.finding}`);
console.log(` Recommendation: ${finding.recommendation}`);
});
}
if (report.prioritizedRemediations.length > 0) {
console.log(`\n✅ Prioritized Remediations:`);
report.prioritizedRemediations.forEach((remediation) => {
console.log(` • ${remediation}`);
});
}
}
}
export {
DockerfileSecurityReviewer,
SecurityReport,
DockerfileAnalysis,
SecurityFinding,
};
Expected Output:
📋 Reviewing Dockerfile: ./Dockerfile
Analyzing base image: nginx:latest
================================================================================
📄 SECURITY REPORT: ./Dockerfile
================================================================================
Base Image: nginx:latest
Risk Score: 78/100
Assessment: Dockerfile has significant security issues: running as root, using
unpinned base image, and potential hardcoded secrets. Prioritize fixing these.
🔍 Findings (8 total):
🚨 [CRITICAL] base_image_vulnerabilities
Finding: 2 critical CVEs found in nginx:latest
Recommendation: Upgrade to 1.25.4-alpine which has 0 known vulnerabilities
🚨 [CRITICAL] hardcoded_secrets
Finding: Potential API key hardcoded in Dockerfile
Recommendation: Use Docker secrets or build-time arguments, never hardcoded
⚠️ [HIGH] privilege_management
Finding: Container runs as root (uid=0)
Recommendation: Add USER directive: USER appuser
⚠️ [HIGH] base_image
Finding: Using nginx:latest without pinned version
Recommendation: Pin to specific version: nginx:1.25.4-alpine
...
✅ Prioritized Remediations:
• Upgrade nginx base image to 1.25.4-alpine to fix 2 critical CVEs
• Remove hardcoded API key; use environment secrets instead
• Add non-root user: RUN useradd -m appuser && USER appuser
• Use .dockerignore to exclude test files and node_modules
Now this is a good baseline—we’ve parsed Dockerfiles, checked base images, and generated findings. But we can go deeper. Let’s add checks for container runtime capabilities and build best practices:
class ContainerRuntimeSecurityAnalyzer {
private client: Anthropic;
// Check what Linux capabilities the container has
analyzeCapabilities(analysis: DockerfileAnalysis): SecurityFinding[] {
const findings: SecurityFinding[] = [];
// In a real implementation, we'd parse Dockerfile for --cap-add/--cap-drop
// For now, we'll alert on common risky patterns
findings.push({
severity: "info",
category: "linux_capabilities",
finding:
"Container capabilities not explicitly configured (will inherit defaults)",
affectedLines: [],
recommendation:
"Add to docker run: --cap-drop=ALL --cap-add=NET_BIND_SERVICE (or only what you need)",
rationale:
"By default, containers inherit a large capability set. Drop unnecessary ones to limit damage from compromise.",
});
return findings;
}
// Validate build best practices
analyzeBuildPractices(content: string): SecurityFinding[] {
const findings: SecurityFinding[] = [];
const lines = content.split("\n");
// Check for layer caching issues
const hasFromLine = lines.some((l) => l.trim().startsWith("FROM"));
const hasRunLine = lines.some((l) => l.trim().startsWith("RUN"));
const hasCopyLine = lines.some((l) => l.trim().startsWith("COPY"));
if (hasFromLine && hasRunLine && hasCopyLine) {
const fromIdx = lines.findIndex((l) => l.trim().startsWith("FROM"));
const runIdx = lines.findIndex((l) => l.trim().startsWith("RUN"));
const copyIdx = lines.findIndex((l) => l.trim().startsWith("COPY"));
if (runIdx < copyIdx) {
findings.push({
severity: "low",
category: "build_optimization",
finding: "RUN commands before COPY (inefficient layer caching)",
affectedLines: [runIdx + 1, copyIdx + 1],
recommendation:
"Move RUN commands after COPY. Order: FROM → RUN (dependencies) → COPY (code) → RUN (setup)",
rationale:
"Docker caches layers. If you RUN before COPY, code changes invalidate dependency cache. Reverse the order.",
});
}
}
// Check for multi-stage builds (best practice)
const fromCount = lines.filter((l) => l.trim().startsWith("FROM")).length;
if (fromCount === 1) {
findings.push({
severity: "low",
category: "build_optimization",
finding:
"Single-stage build detected (could optimize with multi-stage)",
affectedLines: [],
recommendation:
"Use multi-stage build: builder stage → runtime stage. Reduces final image size.",
rationale:
"Multi-stage builds exclude build tools from runtime image, reducing attack surface and image size.",
});
}
return findings;
}
}
// Example: Enhanced dockerfile review
async function reviewWithRuntimeAnalysis(
dockerfilePath: string,
): Promise<void> {
const reviewer = new DockerfileSecurityReviewer();
const runtimeAnalyzer = new ContainerRuntimeSecurityAnalyzer();
console.log(
`\n🔍 Comprehensive Container Security Review: ${dockerfilePath}`,
);
const content = fs.readFileSync(dockerfilePath, "utf-8");
const analysis = reviewer["parseDockerfile"](content);
const runtimeFindings = runtimeAnalyzer.analyzeCapabilities(analysis);
const buildFindings = runtimeAnalyzer.analyzeBuildPractices(content);
console.log(`\nRuntime Security (${runtimeFindings.length} items):`);
runtimeFindings.forEach((f) => {
console.log(` • [${f.severity}] ${f.finding}`);
});
console.log(`\nBuild Best Practices (${buildFindings.length} items):`);
buildFindings.forEach((f) => {
console.log(` • [${f.severity}] ${f.finding}`);
});
}
export { ContainerRuntimeSecurityAnalyzer };
The Compliance and Risk Management Layer
Once you have a container security reviewer working, there’s another layer of value: compliance and organizational risk management. Many organizations operate under regulatory constraints—PCI-DSS for payment processing, HIPAA for healthcare, SOC 2 for general security. Container security directly impacts compliance.
A regulatory requirement might state: “All production systems must run non-root containers.” Your security reviewer can check this automatically. It can generate reports showing which images in your fleet violate this requirement. It can estimate effort to remediate. It can track progress as teams fix issues.
This transforms security from a binary “yes/no” into a managed process. You know your risk posture. You can communicate with management about the gap between current state and target state. You can prioritize remediation efforts across the organization.
The cost of non-compliance is huge. A single audit finding might result in mandatory remediation, fines, or loss of certification. Proactive scanning prevents these expensive surprises.
Equally valuable: proof of due diligence. If a security incident occurs, evidence that you actively scanned containers and remediated issues shows that you took reasonable security measures. This matters in post-incident investigations and legal proceedings.
Automated Scanning at Scale
So far we’ve reviewed individual Dockerfiles. Now let’s automate scanning across your entire container registry. As your organization grows, manual review becomes impossible. You might have dozens of services, hundreds of container images, thousands of daily deployments. Scanning even a fraction of these manually would require full-time staff.
Automation is essential. But automated scanning generates data—lots of it. Without intelligent filtering and prioritization, you drown your team in alerts. This is where Claude’s contextual analysis becomes critical. Instead of “1247 findings across your registry,” you get “3 critical issues require immediate attention, 42 high-risk items should be addressed this quarter, and 1202 low-risk findings are mostly false positives we’re documenting for your exception list.”
The difference is night and day. The first overwhelms teams. The second enables action.
interface ScanResult {
imageName: string;
tag: string;
scanDate: string;
findings: SecurityFinding[];
riskScore: number;
}
class RegistrySecurityScanner {
private client: Anthropic;
private results: ScanResult[] = [];
async scanRegistry(registryUrl: string): Promise<ScanResult[]> {
console.log(`\n📦 Scanning registry: ${registryUrl}`);
// Mock: would list images from actual Docker registry
const images = [
{ name: "myapp-api", tags: ["latest", "1.2.3", "1.2.2"] },
{ name: "myapp-worker", tags: ["latest", "1.2.3"] },
{ name: "myapp-web", tags: ["latest", "3.1.0"] },
];
for (const image of images) {
for (const tag of image.tags) {
const result = await this.scanImage(image.name, tag);
this.results.push(result);
}
}
return this.results;
}
private async scanImage(
imageName: string,
tag: string
): Promise<ScanResult> {
console.log(` Scanning ${imageName}:${tag}...`);
// In real scenario: pull image, scan with Trivy or Grype, parse results
// For demo: simulate findings
const findings: SecurityFinding[] = [
{
severity: "medium",
category: "outdated_dependency",
finding: `Dependency 'openssl' has 1 medium CVE (version 1.1.1)`,
affectedLines: [],
recommendation: "Update to openssl 3.1.0 or later",
rationale: "Older openssl versions have known cryptographic weaknesses",
},
];
return {
imageName,
tag,
scanDate: new Date().toISOString(),
findings,
riskScore: 35,
};
}
async generateRegistry Report(): Promise<string> {
const prompt = `
Analyze these container scan results:
${this.results
.map(
(r) =>
`Image: ${r.imageName}:${r.tag}
Risk Score: ${r.riskScore}/100
Findings: ${r.findings.length}
${r.findings.map((f) => `- [${f.severity}] ${f.finding}`).join("\n ")}`
)
.join("\n\n")}
What's the biggest risk? What should the team prioritize? Which images are highest risk?
`;
const message = await this.client.messages.create({
model: "claude-opus-4-1",
max_tokens: 1024,
messages: [
{
role: "user",
content: prompt,
},
],
});
return message.content[0].type === "text" ? message.content[0].text : "";
}
summarizeResults(): void {
console.log("\n" + "=".repeat(80));
console.log("📊 REGISTRY SECURITY SUMMARY");
console.log("=".repeat(80));
const byRisk = this.results.sort((a, b) => b.riskScore - a.riskScore);
const critical = byRisk.filter((r) => r.riskScore >= 80);
const high = byRisk.filter((r) => r.riskScore >= 60 && r.riskScore < 80);
const medium = byRisk.filter((r) => r.riskScore >= 40 && r.riskScore < 60);
console.log(`\n🚨 Critical Risk (${critical.length}):`);
critical.forEach((r) => {
console.log(` ${r.imageName}:${r.tag} (score: ${r.riskScore})`);
});
console.log(`\n⚠️ High Risk (${high.length}):`);
high.forEach((r) => {
console.log(` ${r.imageName}:${r.tag} (score: ${r.riskScore})`);
});
console.log(`\n⚡ Medium Risk (${medium.length}):`);
medium.forEach((r) => {
console.log(` ${r.imageName}:${r.tag} (score: ${r.riskScore})`);
});
const avgScore =
this.results.reduce((sum, r) => sum + r.riskScore, 0) /
this.results.length;
console.log(`\n📈 Average Risk Score: ${avgScore.toFixed(1)}/100`);
}
}
// Usage
async function demonstrateRegistryScan() {
const scanner = new RegistrySecurityScanner();
const results = await scanner.scanRegistry("registry.mycompany.com");
scanner.summarizeResults();
const analysis = await scanner.generateRegistry Report();
console.log(`\n🤖 Claude's Analysis:\n${analysis}`);
}
Expected Output:
📦 Scanning registry: registry.mycompany.com
Scanning myapp-api:latest...
Scanning myapp-api:1.2.3...
Scanning myapp-api:1.2.2...
Scanning myapp-worker:latest...
Scanning myapp-worker:1.2.3...
Scanning myapp-web:latest...
Scanning myapp-web:3.1.0...
================================================================================
📊 REGISTRY SECURITY SUMMARY
================================================================================
🚨 Critical Risk (2):
myapp-api:latest (score: 82)
myapp-web:latest (score: 78)
⚠️ High Risk (1):
myapp-worker:1.2.3 (score: 64)
⚡ Medium Risk (4):
myapp-api:1.2.3 (score: 52)
myapp-api:1.2.2 (score: 55)
myapp-worker:latest (score: 48)
myapp-web:3.1.0 (score: 45)
📈 Average Risk Score: 56.1/100
🤖 Claude's Analysis:
Your "latest" tags are the biggest risk. myapp-api:latest has 2 critical CVEs
in nginx, and myapp-web:latest isn't even pinned to a version. Immediate actions:
1. Pin all "latest" deployments to specific versions (e.g., 3.1.0)
2. Upgrade myapp-api to 1.2.3 which has CVE patches
3. Set a deprecation date for untagged images older than 30 days
Generating Actionable Remediation Guidance
Here’s the secret: scanning is worthless without context. Let’s build intelligent remediation guidance:
class RemediationGuidance {
private client: Anthropic;
async generateRemediationPlan(
imageName: string,
findings: SecurityFinding[],
): Promise<string> {
const prompt = `
An engineer needs to fix security issues in container image: ${imageName}
Findings to address:
${findings
.map(
(f) =>
`[${f.severity}] ${f.category}: ${f.finding}
Current: ${f.affectedLines.length} locations
Suggested: ${f.recommendation}`,
)
.join("\n\n")}
Generate a step-by-step remediation plan that:
1. Is ordered by impact (fix critical issues that protect most) and effort
2. Explains WHY each fix matters (not just what to do)
3. Includes before/after code examples for Dockerfile changes
4. Estimates effort and testing needed
5. Flags any tradeoffs or potential issues
Keep it concrete and actionable for a developer.
`;
const message = await this.client.messages.create({
model: "claude-opus-4-1",
max_tokens: 2048,
messages: [
{
role: "user",
content: prompt,
},
],
});
return message.content[0].type === "text" ? message.content[0].text : "";
}
async validateRemediationProof(
imageName: string,
originalFindings: SecurityFinding[],
updatedDockerfile: string,
): Promise<{
validatedFindings: SecurityFinding[];
remainingIssues: string[];
}> {
const prompt = `
Image: ${imageName}
ORIGINAL FINDINGS:
${originalFindings
.map((f) => `- [${f.severity}] ${f.finding}\n Fix: ${f.recommendation}`)
.join("\n")}
UPDATED DOCKERFILE:
\`\`\`dockerfile
${updatedDockerfile}
\`\`\`
Which original findings have been addressed? Which remain? Any NEW issues introduced?
Return JSON with:
{
"validatedFindings": [original findings that were fixed],
"remainingIssues": ["issues still present"]
}
`;
const message = await this.client.messages.create({
model: "claude-opus-4-1",
max_tokens: 1024,
messages: [
{
role: "user",
content: prompt,
},
],
});
try {
const responseText =
message.content[0].type === "text" ? message.content[0].text : "{}";
const jsonMatch = responseText.match(/\{[\s\S]*\}/);
return jsonMatch
? JSON.parse(jsonMatch[0])
: { validatedFindings: [], remainingIssues: [] };
} catch {
return { validatedFindings: [], remainingIssues: ["Unable to validate"] };
}
}
}
// Demonstrate remediation guidance
async function demonstrateRemediationGuidance() {
const guidance = new RemediationGuidance();
const findings: SecurityFinding[] = [
{
severity: "critical",
category: "privilege",
finding: "Runs as root",
affectedLines: [],
recommendation: "Create non-root user",
rationale: "Root containers are dangerous",
},
{
severity: "high",
category: "base_image",
finding: "Using ubuntu:latest (unpinned)",
affectedLines: [],
recommendation: "Pin to ubuntu:22.04",
rationale: "Latest tags change unexpectedly",
},
];
console.log("📝 Generating remediation plan...\n");
const plan = await guidance.generateRemediationPlan("myapp:latest", findings);
console.log(plan);
// Now show validation
const updatedDockerfile = `
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 && rm -rf /var/lib/apt/lists/*
RUN useradd -m -s /bin/bash appuser
COPY . /app
WORKDIR /app
USER appuser
CMD ["python3", "main.py"]
`;
console.log("\n✅ Validating fixes...\n");
const validation = await guidance.validateRemediationProof(
"myapp:latest",
findings,
updatedDockerfile,
);
console.log("Validated fixes:", validation.validatedFindings);
console.log("Remaining issues:", validation.remainingIssues);
}
Deep Dive: The Supply Chain Security Layer
One of the most overlooked aspects of container security is supply chain integrity. Your base image didn’t appear from nowhere. It came from a registry (typically Docker Hub or a private registry). That registry publishes images. Someone maintains those images. That supply chain has multiple points of failure.
Consider the base image you choose for Python applications. You might use python:3.11. This image is built by the official Python Docker library. But where do they get the Python binary? From the Python project. Where do they get the operating system packages? From Debian or Ubuntu repositories. Each step introduces trust and supply chain risk.
What if an attacker compromises the Debian repository and injects malicious packages? Your image inherits the compromise. What if an attacker compromises a Docker Hub account and pushes a malicious image with the same name? Your deployment pulls the wrong image.
These scenarios aren’t hypothetical. There have been real incidents where malicious images were pushed to registries, or legitimate images were compromised. This is why cryptographic verification matters.
Modern container registries support image signing. You can cryptographically verify that an image came from a trusted source and hasn’t been modified. But using this requires setup and discipline.
Your container security reviewer should check: “Are you verifying image signatures? Are you pulling from trusted registries? Are you pinning by hash instead of tag?” These are supply chain questions that matter as much as CVE scanning.
The Base Image Selection Problem
Choosing a base image is deceptively important. It’s not just about size. It’s about trust, security, and maintainability.
A Ubuntu 20.04 base image includes the entire Ubuntu package ecosystem—hundreds of packages you might not need. A smaller Alpine base image includes much less. A minimal scratch image includes nothing but your application.
But smaller isn’t always better. Alpine uses musl libc instead of glibc, which can cause compatibility issues. Scratch images mean you need to bundle everything your application needs, including any shared libraries.
The tradeoff involves several dimensions: attack surface (smaller = fewer vulnerabilities), compatibility (larger = more compatibility), size (smaller = faster pulls), maintenance burden (smaller = less to update), and vulnerability velocity (smaller = faster to update when issues are found).
Your security reviewer should understand these tradeoffs. It should ask: “Why did you choose this base image? Does it align with your application needs and risk profile?” The answer might be “We need Alpine for size, and we’re accepting the musl compatibility risk.” That’s a conscious choice, not an oversight.
Real-World Impact: Container Security at Scale
Think about what we’ve built here. Most teams have dozens or hundreds of container images in production. Off-the-shelf scanners find vulnerabilities but don’t prioritize or contextualize. Engineers drown in false positives.
With this approach:
- Context-aware scanning: “That CVE is in a dev dependency you don’t ship” (fewer false positives)
- Intelligent prioritization: “Fix these 3 things first, they protect 80% of your exposure”
- Guided remediation: Step-by-step instructions, before/after code, effort estimates
- Automated validation: Prove that your fixes actually work
- Organizational learning: Over time, see what patterns are common in your codebase
The human stays in control—Claude is the expert assistant, not the decision-maker. Security teams can now do the work of 10 people in hours instead of days.
Runtime Container Security: Beyond the Image
Security doesn’t end when the image is built. How you run the container matters as much as what’s in it. The same image run with the right permissions is secure. Run with the wrong permissions, and it’s a vulnerability.
User and privilege level: As mentioned, running as root is dangerous. But beyond just non-root, you should run with the minimum capabilities needed. A web server doesn’t need access to the raw socket capability. A database doesn’t need network packet manipulation. Using --cap-drop=ALL --cap-add=NET_BIND_SERVICE limits what the container can do even if compromised.
Resource limits: Without resource limits, a compromised container can consume all CPU and memory, crashing the host. Setting limits with --memory and --cpus ensures a single container can’t perform denial of service.
Mounts and volumes: A container shouldn’t mount the host’s root filesystem, the Docker socket, or sensitive configuration directories. Mounting the Docker socket is especially dangerous—it gives the container control over all containers on the host.
Network policies: If your orchestration platform supports network policies (Kubernetes does), restrict which containers can communicate with which. An internal service shouldn’t be reachable from the internet. A database shouldn’t be reachable from untrusted services.
Secrets management: Don’t pass secrets as environment variables or volume mounts. Use dedicated secret management systems (Kubernetes secrets, Vault, etc.). These provide encryption, rotation, and audit trails.
Your container security reviewer should assess not just the image, but how it’s deployed. “This image is secure, but it’s deployed with the Docker socket mounted. Here’s the risk…”
The Secrets Exposure Problem
One of the most dangerous mistakes in containerization is accidentally including secrets in the image. This happens more often than you’d think. You use a build argument to fetch a secret (like a GitHub token to clone a private repository), then forget to ensure the secret isn’t copied into the final image.
Secrets in images are a catastrophic failure mode because images are often pushed to registries, shared with teams, and stored in accessible places. A secret in an image has effectively become a public credential. It shows up in image history. It shows up in registries. It shows up in any backups of the registry.
Your security reviewer should scan Dockerfile for patterns that might leak secrets: environment variables with obvious secret names, copy commands that grab files from build context that might contain secrets, RUN commands that execute with secrets visible in the command line.
But it should also check the running image. Does the image contain .env files, AWS credentials files, SSH keys, or other secrets? These shouldn’t be there.
The fix is using build secrets (Docker BuildKit’s --secret flag, Kubernetes secrets at build time) or fetching secrets from external systems at runtime. Never bake secrets into images.
Layer-by-Layer Security Analysis
Modern Dockerfiles use multi-stage builds—multiple FROM statements, with each stage producing an image that’s used in the next stage. This allows you to keep build tools out of the final image, reducing size and attack surface.
But it creates opportunities for mistakes. You might accidentally copy secrets from the build stage into the final stage. You might include unnecessary files. You might not realize a dependency persists through the stages.
Your security reviewer should understand multi-stage builds and check: “Which files are copied from stage to stage? Are any secrets accidentally included? Are any unnecessary files included?” This is layer-by-layer analysis that requires semantic understanding of the Dockerfile.
Putting It Together: CI/CD Integration
Finally, let’s show how this integrates into real deployment pipelines:
#!/bin/bash
# Integration with CI/CD (GitHub Actions, GitLab CI, etc.)
DOCKERFILE="./Dockerfile"
IMAGE_NAME="myapp:${COMMIT_SHA:0:7}"
echo "🔒 Running container security review..."
# Run Claude Code security review
node scripts/review-dockerfile.js \
--dockerfile "$DOCKERFILE" \
--image-name "$IMAGE_NAME" \
--fail-on-critical \
--report-format json \
> security-report.json
# Parse report
CRITICAL_COUNT=$(jq '.findings | map(select(.severity=="critical")) | length' security-report.json)
HIGH_COUNT=$(jq '.findings | map(select(.severity=="high")) | length' security-report.json)
echo "Security Report:"
echo " Critical: $CRITICAL_COUNT"
echo " High: $HIGH_COUNT"
# Fail if too many critical issues
if [ "$CRITICAL_COUNT" -gt 0 ]; then
echo "❌ Build failed: Critical security issues found"
cat security-report.json | jq '.findings[] | select(.severity=="critical")'
exit 1
fi
if [ "$HIGH_COUNT" -gt 3 ]; then
echo "⚠️ Warning: Many high-severity issues. Review recommended."
fi
echo "✅ Security check passed"
The integration is straightforward: scan Dockerfiles as part of the build, fail on critical issues, warn on high issues, and give developers actionable feedback before they deploy.
Building Security Into Your Development Pipeline
So you’ve scanned your containers and found issues. Now what? This is where many organizations stumble. Teams generate reports, assign them to engineers, and hope they get fixed. But without process discipline, security debt accumulates.
The missing piece is shifting security left—making scanning part of your development workflow, not a gate you pass before deployment. When an engineer creates a Dockerfile, they should get immediate feedback: “This runs as root. That’s a blocker.”
Here’s how to integrate container security review into your CI/CD pipeline:
Pre-commit hooks: Before pushing to the repository, scan the Dockerfile locally. Catch obvious issues before code review.
Pull request checks: Automatically run security review when a Dockerfile is modified. Block merges if critical issues are found without documented exceptions.
Build-time scanning: When the image is built, scan again and fail the build if risk thresholds are exceeded. This catches issues introduced by changes to dependencies.
Registry scanning: Periodically rescan images in your container registry. Newly discovered CVEs in previously safe images get flagged automatically.
Runtime scanning: Some tools monitor running containers. If an image is compromised after deployment, you get alerts.
This layered approach—check at every stage—ensures that issues don’t slip through. It also makes security a development concern, not something that happens in a separate phase.
Real-World Complexity: Handling False Positives
One of the biggest frustrations with container security is false positives. A tool flags a critical CVE in your base image, but the vulnerable code path isn’t reachable by your application. Or the library that has the CVE is a transitive dependency of a dev-only tool that doesn’t ship to production.
This is where context really matters. An automated tool can’t distinguish between genuine risk and false alarm. But Claude, augmented with your team’s knowledge and your codebase structure, can.
Consider this scenario: You’re running Ubuntu 20.04 as your base image. It has five known CVEs. But your container only uses the kernel scheduler and TCP stack. None of the CVEs affect those components. A simple CVE scanner would recommend upgrading to Ubuntu 22.04. But that’s a breaking change that requires three months of regression testing.
Claude can understand this tradeoff. It can say: “Yes, Ubuntu 20.04 has CVEs. But they’re in components you don’t use. Upgrading costs three months of testing. For now, document this exception and add a reminder to upgrade in your next major version cycle.”
This is the hidden layer of security: not just finding problems, but understanding your risk in context and making pragmatic decisions.
Scaling Security Across Your Organization
As your container fleet grows—dozens of services, hundreds of images, thousands of deployments—manual security review becomes impossible. You need automation that scales without drowning your team in false positives.
The system we’ve built does exactly that. It can scan your entire container registry in minutes. It prioritizes findings by severity and exploitability. It learns which patterns your team accepts and which you reject. Over time, it becomes smarter about your specific risk profile.
But more importantly, it becomes a teaching tool. Every security decision creates a learning opportunity. When an engineer asks “why do we need to pin the base image version?”, the system can explain the attack surface that unpinned versions introduce. When you reject a recommendation as a false positive, the system learns that this pattern is acceptable in your environment.
This knowledge amplification is where AI really shines. A single security expert can teach a system to make better decisions than any static ruleset.
Wrapping Up
Container security isn’t about finding every CVE—it’s about understanding your risk, prioritizing fixes that matter, and building processes that prevent issues going forward.
With Claude Code as your security reviewer, you get intelligent scanning that understands context, generates actionable guidance, and scales to your entire container ecosystem. Your team spends less time triaging false positives and more time actually improving security.
The real power isn’t the scanning—it’s the learning. Every fix, every approval, every remediation teaches the system what security looks like in your organization. After a few months, your custom security reviewer becomes more valuable than generic tools because it understands your specific constraints, patterns, and risk tolerance.
Think about what you’ve built: a security expert that never sleeps, never gets tired, and understands your specific infrastructure. It catches issues before they reach production. It educates your team about security best practices. It documents your decisions and builds organizational knowledge.
That’s not just automation—that’s organizational transformation.
-iNet