mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-10 21:24:56 +03:00
200 lines
6.2 KiB
Python
200 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Helm Chart Security Scanner - Render Helm templates and scan
|
|
for security misconfigurations in Kubernetes manifests.
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import argparse
|
|
import re
|
|
from pathlib import Path
|
|
|
|
|
|
SECURITY_CHECKS = [
|
|
{
|
|
"id": "HELM-001",
|
|
"name": "Container runs as root",
|
|
"severity": "HIGH",
|
|
"pattern": r"runAsNonRoot:\s*false|runAsUser:\s*0",
|
|
"remediation": "Set securityContext.runAsNonRoot: true and runAsUser to non-zero",
|
|
},
|
|
{
|
|
"id": "HELM-002",
|
|
"name": "Privileged container",
|
|
"severity": "CRITICAL",
|
|
"pattern": r"privileged:\s*true",
|
|
"remediation": "Set securityContext.privileged: false",
|
|
},
|
|
{
|
|
"id": "HELM-003",
|
|
"name": "Allow privilege escalation",
|
|
"severity": "HIGH",
|
|
"pattern": r"allowPrivilegeEscalation:\s*true",
|
|
"remediation": "Set securityContext.allowPrivilegeEscalation: false",
|
|
},
|
|
{
|
|
"id": "HELM-004",
|
|
"name": "No resource limits",
|
|
"severity": "MEDIUM",
|
|
"pattern": r"resources:\s*\{\}|resources:\s*null",
|
|
"remediation": "Set resources.limits.cpu and resources.limits.memory",
|
|
},
|
|
{
|
|
"id": "HELM-005",
|
|
"name": "Uses latest image tag",
|
|
"severity": "MEDIUM",
|
|
"pattern": r"image:.*:latest|image:\s*[^:]+\s*$",
|
|
"remediation": "Use specific image tag or digest instead of :latest",
|
|
},
|
|
{
|
|
"id": "HELM-006",
|
|
"name": "HostPath volume mount",
|
|
"severity": "HIGH",
|
|
"pattern": r"hostPath:",
|
|
"remediation": "Avoid hostPath volumes; use PersistentVolumeClaim instead",
|
|
},
|
|
{
|
|
"id": "HELM-007",
|
|
"name": "Host network enabled",
|
|
"severity": "HIGH",
|
|
"pattern": r"hostNetwork:\s*true",
|
|
"remediation": "Set hostNetwork: false",
|
|
},
|
|
{
|
|
"id": "HELM-008",
|
|
"name": "Host PID namespace",
|
|
"severity": "HIGH",
|
|
"pattern": r"hostPID:\s*true",
|
|
"remediation": "Set hostPID: false",
|
|
},
|
|
{
|
|
"id": "HELM-009",
|
|
"name": "Service account token auto-mounted",
|
|
"severity": "MEDIUM",
|
|
"pattern": r"automountServiceAccountToken:\s*true",
|
|
"remediation": "Set automountServiceAccountToken: false unless needed",
|
|
},
|
|
{
|
|
"id": "HELM-010",
|
|
"name": "Writable root filesystem",
|
|
"severity": "MEDIUM",
|
|
"pattern": r"readOnlyRootFilesystem:\s*false",
|
|
"remediation": "Set securityContext.readOnlyRootFilesystem: true",
|
|
},
|
|
]
|
|
|
|
|
|
def render_chart(chart_path: str, values_file: str = None, release_name: str = "scan") -> str:
|
|
"""Render Helm chart templates."""
|
|
cmd = ["helm", "template", release_name, chart_path]
|
|
if values_file:
|
|
cmd.extend(["-f", values_file])
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
print(f"Helm template error: {result.stderr}", file=sys.stderr)
|
|
sys.exit(1)
|
|
return result.stdout
|
|
|
|
|
|
def scan_rendered(content: str) -> list:
|
|
"""Scan rendered YAML for security issues."""
|
|
findings = []
|
|
lines = content.split("\n")
|
|
for check in SECURITY_CHECKS:
|
|
for i, line in enumerate(lines, 1):
|
|
if re.search(check["pattern"], line):
|
|
findings.append({
|
|
"id": check["id"],
|
|
"name": check["name"],
|
|
"severity": check["severity"],
|
|
"line": i,
|
|
"content": line.strip(),
|
|
"remediation": check["remediation"],
|
|
})
|
|
return findings
|
|
|
|
|
|
def lint_chart(chart_path: str) -> dict:
|
|
"""Run helm lint on chart."""
|
|
result = subprocess.run(
|
|
["helm", "lint", chart_path, "--strict"],
|
|
capture_output=True, text=True,
|
|
)
|
|
return {
|
|
"passed": result.returncode == 0,
|
|
"output": result.stdout + result.stderr,
|
|
}
|
|
|
|
|
|
def generate_report(findings: list, chart_path: str) -> str:
|
|
"""Generate security scan report."""
|
|
severity_counts = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0}
|
|
for f in findings:
|
|
severity_counts[f["severity"]] = severity_counts.get(f["severity"], 0) + 1
|
|
|
|
report = f"""# Helm Chart Security Scan Report
|
|
|
|
**Chart:** `{chart_path}`
|
|
**Findings:** {len(findings)}
|
|
|
|
## Summary
|
|
|
|
| Severity | Count |
|
|
|----------|-------|
|
|
| CRITICAL | {severity_counts['CRITICAL']} |
|
|
| HIGH | {severity_counts['HIGH']} |
|
|
| MEDIUM | {severity_counts['MEDIUM']} |
|
|
| LOW | {severity_counts['LOW']} |
|
|
|
|
## Findings
|
|
|
|
| ID | Severity | Finding | Line | Remediation |
|
|
|----|----------|---------|------|-------------|
|
|
"""
|
|
for f in sorted(findings, key=lambda x: {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3}[x["severity"]]):
|
|
report += f"| {f['id']} | {f['severity']} | {f['name']} | {f['line']} | {f['remediation']} |\n"
|
|
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Helm Chart Security Scanner")
|
|
parser.add_argument("chart", help="Path to Helm chart")
|
|
parser.add_argument("--values", "-f", help="Values file path")
|
|
parser.add_argument("--report", "-r", help="Output report file")
|
|
parser.add_argument("--lint", action="store_true", help="Run helm lint")
|
|
parser.add_argument("--fail-on", choices=["critical", "high", "medium"],
|
|
default="high", help="Fail threshold")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.lint:
|
|
lint_result = lint_chart(args.chart)
|
|
print(lint_result["output"])
|
|
if not lint_result["passed"]:
|
|
print("Lint FAILED")
|
|
sys.exit(1)
|
|
|
|
rendered = render_chart(args.chart, args.values)
|
|
findings = scan_rendered(rendered)
|
|
report = generate_report(findings, args.chart)
|
|
|
|
if args.report:
|
|
Path(args.report).write_text(report)
|
|
print(f"Report written to {args.report}")
|
|
else:
|
|
print(report)
|
|
|
|
threshold = {"critical": ["CRITICAL"], "high": ["CRITICAL", "HIGH"],
|
|
"medium": ["CRITICAL", "HIGH", "MEDIUM"]}
|
|
blocking = [f for f in findings if f["severity"] in threshold[args.fail_on]]
|
|
if blocking:
|
|
print(f"\nFAILED: {len(blocking)} findings at or above {args.fail_on} severity")
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|