#!/usr/bin/env python3 """Agent for auditing and generating Calico Kubernetes network policies.""" import json import argparse import subprocess from datetime import datetime def kubectl_get(resource, namespace=None, label_selector=None): """Get Kubernetes resources via kubectl.""" cmd = ["kubectl", "get", resource, "-o", "json"] if namespace: cmd.extend(["-n", namespace]) else: cmd.append("--all-namespaces") if label_selector: cmd.extend(["-l", label_selector]) result = subprocess.run(cmd, capture_output=True, text=True, timeout=60) if result.returncode != 0: return {"error": result.stderr.strip()} return json.loads(result.stdout) if result.stdout.strip() else {} def list_network_policies(): """List all NetworkPolicy and Calico GlobalNetworkPolicy resources.""" k8s_np = kubectl_get("networkpolicy") calico_gnp = kubectl_get("globalnetworkpolicy") return { "k8s_network_policies": k8s_np.get("items", []) if isinstance(k8s_np, dict) else [], "calico_global_policies": calico_gnp.get("items", []) if isinstance(calico_gnp, dict) else [], } def audit_network_policies(policies_path): """Audit network policies for security gaps.""" with open(policies_path) as f: data = json.load(f) policies = data if isinstance(data, list) else data.get("items", data.get("policies", [])) findings = [] namespaces_covered = set() for policy in policies: metadata = policy.get("metadata", {}) spec = policy.get("spec", {}) ns = metadata.get("namespace", "default") namespaces_covered.add(ns) policy_types = spec.get("policyTypes", []) if "Ingress" not in policy_types and "Egress" not in policy_types: findings.append({ "policy": metadata.get("name", ""), "namespace": ns, "issue": "No policyTypes defined (defaults to ingress-only)", "severity": "MEDIUM", }) if "Egress" not in policy_types: findings.append({ "policy": metadata.get("name", ""), "namespace": ns, "issue": "No egress policy - all outbound traffic allowed", "severity": "HIGH", }) ingress_rules = spec.get("ingress", []) for rule in ingress_rules: if not rule.get("from"): findings.append({ "policy": metadata.get("name", ""), "issue": "Ingress rule allows from all sources", "severity": "HIGH", }) egress_rules = spec.get("egress", []) for rule in egress_rules: if not rule.get("to"): findings.append({ "policy": metadata.get("name", ""), "issue": "Egress rule allows to all destinations", "severity": "MEDIUM", }) return {"findings": findings, "namespaces_covered": list(namespaces_covered), "total_policies": len(policies)} def generate_default_deny(namespace): """Generate a default deny-all NetworkPolicy for a namespace.""" return { "apiVersion": "networking.k8s.io/v1", "kind": "NetworkPolicy", "metadata": {"name": "default-deny-all", "namespace": namespace}, "spec": { "podSelector": {}, "policyTypes": ["Ingress", "Egress"], }, } def generate_allow_dns_egress(namespace): """Generate a policy allowing DNS egress to kube-dns.""" return { "apiVersion": "networking.k8s.io/v1", "kind": "NetworkPolicy", "metadata": {"name": "allow-dns-egress", "namespace": namespace}, "spec": { "podSelector": {}, "policyTypes": ["Egress"], "egress": [{ "to": [{"namespaceSelector": {"matchLabels": { "kubernetes.io/metadata.name": "kube-system"}}}], "ports": [{"protocol": "UDP", "port": 53}, {"protocol": "TCP", "port": 53}], }], }, } def generate_app_policy(namespace, app_label, allowed_ingress_labels=None, allowed_egress_ports=None): """Generate a Calico-style network policy for an application.""" policy = { "apiVersion": "networking.k8s.io/v1", "kind": "NetworkPolicy", "metadata": {"name": f"allow-{app_label}", "namespace": namespace}, "spec": { "podSelector": {"matchLabels": {"app": app_label}}, "policyTypes": ["Ingress", "Egress"], "ingress": [], "egress": [], }, } if allowed_ingress_labels: for label in allowed_ingress_labels: policy["spec"]["ingress"].append({ "from": [{"podSelector": {"matchLabels": {"app": label}}}], }) if allowed_egress_ports: for port_info in allowed_egress_ports: policy["spec"]["egress"].append({ "ports": [{"protocol": port_info.get("protocol", "TCP"), "port": port_info["port"]}], }) return policy def check_unprotected_namespaces(): """Find namespaces without any network policies.""" namespaces = kubectl_get("namespaces") policies = kubectl_get("networkpolicy") if isinstance(namespaces, dict) and "error" not in namespaces: all_ns = {item["metadata"]["name"] for item in namespaces.get("items", [])} else: return {"error": "Cannot list namespaces"} protected_ns = set() if isinstance(policies, dict) and "error" not in policies: for item in policies.get("items", []): protected_ns.add(item.get("metadata", {}).get("namespace", "default")) system_ns = {"kube-system", "kube-public", "kube-node-lease"} unprotected = all_ns - protected_ns - system_ns return { "total_namespaces": len(all_ns), "protected": len(protected_ns), "unprotected": list(unprotected), "severity": "HIGH" if unprotected else "INFO", } def main(): parser = argparse.ArgumentParser(description="Calico Network Policy Agent") parser.add_argument("--audit", help="Network policies JSON to audit") parser.add_argument("--namespace", help="Namespace for policy generation") parser.add_argument("--app", help="App label for policy generation") parser.add_argument("--action", choices=["audit", "generate", "check", "full"], default="full") parser.add_argument("--output", default="calico_netpol_report.json") args = parser.parse_args() report = {"generated_at": datetime.utcnow().isoformat(), "results": {}} if args.action in ("audit", "full") and args.audit: result = audit_network_policies(args.audit) report["results"]["audit"] = result print(f"[+] Audit: {len(result['findings'])} findings across {result['total_policies']} policies") if args.action in ("generate", "full") and args.namespace: policies = [ generate_default_deny(args.namespace), generate_allow_dns_egress(args.namespace), ] if args.app: policies.append(generate_app_policy(args.namespace, args.app)) report["results"]["generated"] = policies print(f"[+] Generated {len(policies)} policies for {args.namespace}") if args.action in ("check", "full"): result = check_unprotected_namespaces() report["results"]["unprotected"] = result print(f"[+] Unprotected namespaces: {result.get('unprotected', [])}") with open(args.output, "w") as f: json.dump(report, f, indent=2, default=str) print(f"[+] Report saved to {args.output}") if __name__ == "__main__": main()