mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 06:04:56 +03:00
c47eed6a64
- 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
312 lines
11 KiB
Python
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()
|