mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 13:44:56 +03:00
309 lines
11 KiB
Python
309 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Kubernetes Pod Security Standards Compliance Checker
|
|
|
|
Audits namespaces and workloads for PSS enforcement levels
|
|
and identifies non-compliant pods.
|
|
"""
|
|
|
|
import subprocess
|
|
import json
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
|
|
|
|
@dataclass
|
|
class PSSFinding:
|
|
namespace: str
|
|
resource: str
|
|
level: str # baseline, restricted
|
|
violation: str
|
|
severity: str # CRITICAL, HIGH, MEDIUM
|
|
remediation: str
|
|
|
|
|
|
@dataclass
|
|
class PSSReport:
|
|
findings: list = field(default_factory=list)
|
|
namespaces_audited: int = 0
|
|
pods_audited: int = 0
|
|
compliant_pods: int = 0
|
|
non_compliant_pods: int = 0
|
|
|
|
|
|
def run_kubectl(args: list) -> tuple:
|
|
"""Execute kubectl command and return parsed JSON."""
|
|
cmd = ["kubectl"] + args + ["-o", "json"]
|
|
try:
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
|
if result.returncode != 0:
|
|
return None, result.stderr
|
|
return json.loads(result.stdout), None
|
|
except (subprocess.TimeoutExpired, json.JSONDecodeError) as e:
|
|
return None, str(e)
|
|
|
|
|
|
def get_namespace_pss_labels(ns_data: dict) -> dict:
|
|
"""Extract PSS labels from namespace metadata."""
|
|
labels = ns_data.get("metadata", {}).get("labels", {})
|
|
return {
|
|
"enforce": labels.get("pod-security.kubernetes.io/enforce", "not-set"),
|
|
"audit": labels.get("pod-security.kubernetes.io/audit", "not-set"),
|
|
"warn": labels.get("pod-security.kubernetes.io/warn", "not-set"),
|
|
"enforce-version": labels.get("pod-security.kubernetes.io/enforce-version", "not-set"),
|
|
}
|
|
|
|
|
|
def check_restricted_compliance(pod_spec: dict, pod_name: str, namespace: str) -> list:
|
|
"""Check pod spec against restricted PSS profile."""
|
|
findings = []
|
|
containers = pod_spec.get("containers", []) + pod_spec.get("initContainers", [])
|
|
pod_security_context = pod_spec.get("securityContext", {})
|
|
|
|
# Check pod-level runAsNonRoot
|
|
pod_run_as_non_root = pod_security_context.get("runAsNonRoot", False)
|
|
pod_run_as_user = pod_security_context.get("runAsUser")
|
|
|
|
# Check pod-level seccomp
|
|
pod_seccomp = pod_security_context.get("seccompProfile", {})
|
|
pod_seccomp_type = pod_seccomp.get("type", "")
|
|
|
|
# Check hostNetwork, hostPID, hostIPC
|
|
for host_ns in ["hostNetwork", "hostPID", "hostIPC"]:
|
|
if pod_spec.get(host_ns, False):
|
|
findings.append(PSSFinding(
|
|
namespace=namespace,
|
|
resource=pod_name,
|
|
level="baseline",
|
|
violation=f"{host_ns} is enabled",
|
|
severity="CRITICAL",
|
|
remediation=f"Set {host_ns}: false in pod spec"
|
|
))
|
|
|
|
# Check hostPath volumes
|
|
for vol in pod_spec.get("volumes", []):
|
|
if "hostPath" in vol:
|
|
findings.append(PSSFinding(
|
|
namespace=namespace,
|
|
resource=pod_name,
|
|
level="baseline",
|
|
violation=f"hostPath volume '{vol.get('name')}' detected",
|
|
severity="HIGH",
|
|
remediation="Replace hostPath with emptyDir, PVC, or configMap"
|
|
))
|
|
|
|
for container in containers:
|
|
c_name = container.get("name", "unknown")
|
|
sc = container.get("securityContext", {})
|
|
|
|
# Check privileged
|
|
if sc.get("privileged", False):
|
|
findings.append(PSSFinding(
|
|
namespace=namespace,
|
|
resource=f"{pod_name}/{c_name}",
|
|
level="baseline",
|
|
violation="Container runs in privileged mode",
|
|
severity="CRITICAL",
|
|
remediation="Set privileged: false and use specific capabilities"
|
|
))
|
|
|
|
# Check allowPrivilegeEscalation
|
|
if sc.get("allowPrivilegeEscalation", True):
|
|
findings.append(PSSFinding(
|
|
namespace=namespace,
|
|
resource=f"{pod_name}/{c_name}",
|
|
level="restricted",
|
|
violation="allowPrivilegeEscalation is not explicitly false",
|
|
severity="HIGH",
|
|
remediation="Set allowPrivilegeEscalation: false"
|
|
))
|
|
|
|
# Check runAsNonRoot
|
|
c_run_as_non_root = sc.get("runAsNonRoot")
|
|
c_run_as_user = sc.get("runAsUser")
|
|
if not (c_run_as_non_root or pod_run_as_non_root):
|
|
if not (c_run_as_user and c_run_as_user > 0) and not (pod_run_as_user and pod_run_as_user > 0):
|
|
findings.append(PSSFinding(
|
|
namespace=namespace,
|
|
resource=f"{pod_name}/{c_name}",
|
|
level="restricted",
|
|
violation="Container may run as root (runAsNonRoot not set)",
|
|
severity="HIGH",
|
|
remediation="Set runAsNonRoot: true and runAsUser to non-zero value"
|
|
))
|
|
|
|
# Check capabilities
|
|
caps = sc.get("capabilities", {})
|
|
drop_caps = [c.upper() for c in (caps.get("drop") or [])]
|
|
add_caps = [c.upper() for c in (caps.get("add") or [])]
|
|
|
|
if "ALL" not in drop_caps:
|
|
findings.append(PSSFinding(
|
|
namespace=namespace,
|
|
resource=f"{pod_name}/{c_name}",
|
|
level="restricted",
|
|
violation="Capabilities not dropped (missing drop: ALL)",
|
|
severity="HIGH",
|
|
remediation="Add capabilities: drop: ['ALL'] to security context"
|
|
))
|
|
|
|
allowed_add = {"NET_BIND_SERVICE"}
|
|
extra_caps = set(add_caps) - allowed_add
|
|
if extra_caps:
|
|
findings.append(PSSFinding(
|
|
namespace=namespace,
|
|
resource=f"{pod_name}/{c_name}",
|
|
level="restricted",
|
|
violation=f"Disallowed capabilities added: {extra_caps}",
|
|
severity="HIGH",
|
|
remediation="Only NET_BIND_SERVICE may be added in restricted profile"
|
|
))
|
|
|
|
# Check seccomp
|
|
c_seccomp = sc.get("seccompProfile", {})
|
|
c_seccomp_type = c_seccomp.get("type", pod_seccomp_type)
|
|
if c_seccomp_type not in ("RuntimeDefault", "Localhost"):
|
|
findings.append(PSSFinding(
|
|
namespace=namespace,
|
|
resource=f"{pod_name}/{c_name}",
|
|
level="restricted",
|
|
violation=f"Seccomp profile not set or is '{c_seccomp_type}'",
|
|
severity="MEDIUM",
|
|
remediation="Set seccompProfile.type to RuntimeDefault or Localhost"
|
|
))
|
|
|
|
# Check readOnlyRootFilesystem (recommended, not required by PSS)
|
|
if not sc.get("readOnlyRootFilesystem", False):
|
|
findings.append(PSSFinding(
|
|
namespace=namespace,
|
|
resource=f"{pod_name}/{c_name}",
|
|
level="restricted",
|
|
violation="Root filesystem is not read-only (recommended)",
|
|
severity="MEDIUM",
|
|
remediation="Set readOnlyRootFilesystem: true and use emptyDir for writable paths"
|
|
))
|
|
|
|
return findings
|
|
|
|
|
|
def audit_cluster(report: PSSReport):
|
|
"""Audit all namespaces and pods for PSS compliance."""
|
|
# Get all namespaces
|
|
ns_data, err = run_kubectl(["get", "namespaces"])
|
|
if ns_data is None:
|
|
print(f"[!] Failed to get namespaces: {err}")
|
|
return
|
|
|
|
namespaces = ns_data.get("items", [])
|
|
print(f"[*] Found {len(namespaces)} namespaces")
|
|
|
|
for ns in namespaces:
|
|
ns_name = ns["metadata"]["name"]
|
|
pss_labels = get_namespace_pss_labels(ns)
|
|
report.namespaces_audited += 1
|
|
|
|
enforce_level = pss_labels["enforce"]
|
|
print(f"\n[*] Namespace: {ns_name} (enforce={enforce_level})")
|
|
|
|
# Flag namespaces without PSS enforcement
|
|
if enforce_level == "not-set":
|
|
report.findings.append(PSSFinding(
|
|
namespace=ns_name,
|
|
resource="namespace",
|
|
level="baseline",
|
|
violation="No PSS enforcement label set",
|
|
severity="HIGH" if ns_name not in ("kube-system", "kube-public", "kube-node-lease") else "MEDIUM",
|
|
remediation=f"kubectl label namespace {ns_name} pod-security.kubernetes.io/enforce=baseline"
|
|
))
|
|
|
|
# Get pods in namespace
|
|
pods_data, err = run_kubectl(["get", "pods", "-n", ns_name])
|
|
if pods_data is None:
|
|
continue
|
|
|
|
pods = pods_data.get("items", [])
|
|
for pod in pods:
|
|
pod_name = pod["metadata"]["name"]
|
|
pod_spec = pod.get("spec", {})
|
|
report.pods_audited += 1
|
|
|
|
pod_findings = check_restricted_compliance(pod_spec, pod_name, ns_name)
|
|
if pod_findings:
|
|
report.non_compliant_pods += 1
|
|
report.findings.extend(pod_findings)
|
|
else:
|
|
report.compliant_pods += 1
|
|
|
|
|
|
def print_report(report: PSSReport):
|
|
"""Print audit results."""
|
|
print("\n" + "=" * 70)
|
|
print("KUBERNETES POD SECURITY STANDARDS AUDIT REPORT")
|
|
print("=" * 70)
|
|
print(f"Namespaces audited: {report.namespaces_audited}")
|
|
print(f"Pods audited: {report.pods_audited}")
|
|
print(f"Compliant pods: {report.compliant_pods}")
|
|
print(f"Non-compliant pods: {report.non_compliant_pods}")
|
|
print(f"Total findings: {len(report.findings)}")
|
|
|
|
if report.pods_audited > 0:
|
|
compliance_rate = (report.compliant_pods / report.pods_audited) * 100
|
|
print(f"Compliance rate: {compliance_rate:.1f}%")
|
|
|
|
print("=" * 70)
|
|
|
|
# Group by severity
|
|
for severity in ["CRITICAL", "HIGH", "MEDIUM"]:
|
|
severity_findings = [f for f in report.findings if f.severity == severity]
|
|
if severity_findings:
|
|
print(f"\n{severity} FINDINGS ({len(severity_findings)}):")
|
|
print("-" * 70)
|
|
for f in severity_findings:
|
|
print(f" [{f.namespace}] {f.resource}")
|
|
print(f" Level: {f.level} | Violation: {f.violation}")
|
|
print(f" Fix: {f.remediation}")
|
|
print()
|
|
|
|
|
|
def main():
|
|
print("[*] Kubernetes Pod Security Standards Compliance Checker")
|
|
print("[*] Checking against restricted profile\n")
|
|
|
|
report = PSSReport()
|
|
audit_cluster(report)
|
|
print_report(report)
|
|
|
|
# Save JSON report
|
|
output = {
|
|
"summary": {
|
|
"namespaces": report.namespaces_audited,
|
|
"pods_audited": report.pods_audited,
|
|
"compliant": report.compliant_pods,
|
|
"non_compliant": report.non_compliant_pods,
|
|
},
|
|
"findings": [
|
|
{
|
|
"namespace": f.namespace,
|
|
"resource": f.resource,
|
|
"level": f.level,
|
|
"violation": f.violation,
|
|
"severity": f.severity,
|
|
"remediation": f.remediation,
|
|
}
|
|
for f in report.findings
|
|
],
|
|
}
|
|
|
|
with open("pss_audit_report.json", "w") as f:
|
|
json.dump(output, f, indent=2)
|
|
print("[*] Report saved to pss_audit_report.json")
|
|
|
|
critical_high = [f for f in report.findings if f.severity in ("CRITICAL", "HIGH")]
|
|
if critical_high:
|
|
print(f"\n[!] {len(critical_high)} CRITICAL/HIGH findings require attention")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|