mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 06:04:56 +03:00
177 lines
6.9 KiB
Python
177 lines
6.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Container Image Hardening Validation Script
|
|
|
|
Validates container images against hardening best practices,
|
|
checks for non-root user, minimal packages, and CIS compliance.
|
|
|
|
Usage:
|
|
python process.py --image myapp:latest --output hardening-report.json
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
@dataclass
|
|
class HardeningCheck:
|
|
check_id: str
|
|
name: str
|
|
passed: bool
|
|
details: str
|
|
severity: str
|
|
|
|
|
|
def run_docker_inspect(image: str) -> dict:
|
|
"""Inspect a Docker image and return configuration."""
|
|
cmd = ["docker", "inspect", image]
|
|
try:
|
|
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
if proc.returncode == 0:
|
|
data = json.loads(proc.stdout)
|
|
return data[0] if data else {}
|
|
return {"error": proc.stderr}
|
|
except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError) as e:
|
|
return {"error": str(e)}
|
|
|
|
|
|
def check_non_root_user(config: dict) -> HardeningCheck:
|
|
"""Check if image runs as non-root user."""
|
|
user = config.get("Config", {}).get("User", "")
|
|
if user and user != "0" and user != "root":
|
|
return HardeningCheck("CIS-4.1", "Non-root user configured", True,
|
|
f"User: {user}", "HIGH")
|
|
return HardeningCheck("CIS-4.1", "Non-root user configured", False,
|
|
"Image runs as root. Add USER instruction.", "HIGH")
|
|
|
|
|
|
def check_healthcheck(config: dict) -> HardeningCheck:
|
|
"""Check if HEALTHCHECK is defined."""
|
|
healthcheck = config.get("Config", {}).get("Healthcheck")
|
|
if healthcheck and healthcheck.get("Test"):
|
|
return HardeningCheck("CIS-4.6", "HEALTHCHECK defined", True,
|
|
f"Test: {healthcheck['Test']}", "MEDIUM")
|
|
return HardeningCheck("CIS-4.6", "HEALTHCHECK defined", False,
|
|
"No HEALTHCHECK instruction found.", "MEDIUM")
|
|
|
|
|
|
def check_exposed_ports(config: dict) -> HardeningCheck:
|
|
"""Check for unnecessary exposed ports."""
|
|
ports = config.get("Config", {}).get("ExposedPorts", {})
|
|
port_list = list(ports.keys()) if ports else []
|
|
if len(port_list) <= 2:
|
|
return HardeningCheck("HARD-1", "Minimal ports exposed", True,
|
|
f"Ports: {port_list}", "LOW")
|
|
return HardeningCheck("HARD-1", "Minimal ports exposed", False,
|
|
f"Many ports exposed: {port_list}. Minimize exposed ports.", "LOW")
|
|
|
|
|
|
def check_image_size(image: str) -> HardeningCheck:
|
|
"""Check image size against recommended limits."""
|
|
cmd = ["docker", "image", "inspect", image, "--format", "{{.Size}}"]
|
|
try:
|
|
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
|
size_bytes = int(proc.stdout.strip())
|
|
size_mb = size_bytes / (1024 * 1024)
|
|
if size_mb < 200:
|
|
return HardeningCheck("HARD-2", "Image size within limits", True,
|
|
f"Size: {size_mb:.0f} MB (< 200 MB)", "MEDIUM")
|
|
elif size_mb < 500:
|
|
return HardeningCheck("HARD-2", "Image size within limits", False,
|
|
f"Size: {size_mb:.0f} MB (> 200 MB, consider optimizing)", "MEDIUM")
|
|
else:
|
|
return HardeningCheck("HARD-2", "Image size within limits", False,
|
|
f"Size: {size_mb:.0f} MB (> 500 MB, use multi-stage build)", "HIGH")
|
|
except (subprocess.TimeoutExpired, ValueError):
|
|
return HardeningCheck("HARD-2", "Image size within limits", False,
|
|
"Could not determine image size", "LOW")
|
|
|
|
|
|
def check_env_secrets(config: dict) -> HardeningCheck:
|
|
"""Check for potential secrets in environment variables."""
|
|
env_vars = config.get("Config", {}).get("Env", [])
|
|
secret_keywords = ["password", "secret", "key", "token", "credential", "api_key"]
|
|
suspicious = []
|
|
for env in env_vars:
|
|
name = env.split("=")[0].lower()
|
|
if any(kw in name for kw in secret_keywords):
|
|
suspicious.append(env.split("=")[0])
|
|
if not suspicious:
|
|
return HardeningCheck("CIS-4.10", "No secrets in ENV", True,
|
|
"No suspicious environment variables found", "HIGH")
|
|
return HardeningCheck("CIS-4.10", "No secrets in ENV", False,
|
|
f"Suspicious ENV vars: {suspicious}. Use secrets manager.", "HIGH")
|
|
|
|
|
|
def check_shell_available(image: str) -> HardeningCheck:
|
|
"""Check if shell is available in the image."""
|
|
cmd = ["docker", "run", "--rm", "--entrypoint", "", image, "sh", "-c", "echo shell_available"]
|
|
try:
|
|
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
|
if "shell_available" in proc.stdout:
|
|
return HardeningCheck("HARD-3", "Shell removed from image", False,
|
|
"Shell (/bin/sh) is available. Consider removing for production.", "LOW")
|
|
return HardeningCheck("HARD-3", "Shell removed from image", True,
|
|
"No shell available in image", "LOW")
|
|
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
return HardeningCheck("HARD-3", "Shell removed from image", True,
|
|
"Could not test shell access (likely unavailable)", "LOW")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Container Image Hardening Validation")
|
|
parser.add_argument("--image", required=True, help="Docker image to validate")
|
|
parser.add_argument("--output", default="hardening-report.json")
|
|
parser.add_argument("--fail-on-findings", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
print(f"[*] Validating hardening: {args.image}")
|
|
|
|
config = run_docker_inspect(args.image)
|
|
if "error" in config:
|
|
print(f"[ERROR] {config['error']}")
|
|
sys.exit(2)
|
|
|
|
checks = [
|
|
check_non_root_user(config),
|
|
check_healthcheck(config),
|
|
check_exposed_ports(config),
|
|
check_image_size(args.image),
|
|
check_env_secrets(config),
|
|
check_shell_available(args.image),
|
|
]
|
|
|
|
passed = sum(1 for c in checks if c.passed)
|
|
failed = sum(1 for c in checks if not c.passed)
|
|
|
|
report = {
|
|
"metadata": {"image": args.image, "date": datetime.now(timezone.utc).isoformat()},
|
|
"summary": {"total": len(checks), "passed": passed, "failed": failed},
|
|
"checks": [
|
|
{"id": c.check_id, "name": c.name, "passed": c.passed,
|
|
"details": c.details, "severity": c.severity}
|
|
for c in checks
|
|
]
|
|
}
|
|
|
|
output_path = os.path.abspath(args.output)
|
|
with open(output_path, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
|
|
for c in checks:
|
|
status = "PASS" if c.passed else "FAIL"
|
|
print(f" [{status}] {c.check_id}: {c.name} - {c.details}")
|
|
|
|
print(f"\n[*] {passed}/{len(checks)} checks passed")
|
|
if args.fail_on_findings and failed > 0:
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|