Files
Anthropic-Cybersecurity-Skills/skills/performing-container-image-hardening/scripts/agent.py
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

312 lines
11 KiB
Python

#!/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()