Files
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- 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
2026-03-19 13:26:49 +01:00

211 lines
7.7 KiB
Python

#!/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()