#!/usr/bin/env python3 """Open Policy Agent (OPA) policy-as-code agent. Evaluates security policies against infrastructure configurations using the OPA REST API or CLI. Supports evaluating Rego policies for Kubernetes admission control, Terraform plans, IAM policies, and custom security rules. """ import argparse import json import os import subprocess import sys from datetime import datetime, timezone try: import requests except ImportError: requests = None def find_opa_binary(): """Locate the OPA binary on the system.""" custom_path = os.environ.get("OPA_PATH") if custom_path and os.path.isfile(custom_path): return custom_path for name in ["opa", "opa.exe"]: for directory in os.environ.get("PATH", "").split(os.pathsep): full_path = os.path.join(directory, name) if os.path.isfile(full_path): return full_path return None def eval_policy_cli(opa_bin, policy_path, input_path, data_path=None, query="data"): """Evaluate a Rego policy using OPA CLI.""" cmd = [opa_bin, "eval", "--format", "json"] cmd.extend(["--bundle", policy_path]) if input_path: cmd.extend(["--input", input_path]) if data_path: cmd.extend(["--data", data_path]) cmd.append(query) print(f"[*] Running: {' '.join(cmd)}") result = subprocess.run( cmd, capture_output=True, text=True, timeout=120, ) if result.returncode != 0: print(f"[!] OPA error: {result.stderr}", file=sys.stderr) return None try: return json.loads(result.stdout) except json.JSONDecodeError: print(f"[!] Failed to parse OPA output", file=sys.stderr) return None def eval_policy_api(opa_url, policy_path, input_data): """Evaluate a policy via OPA REST API.""" if not requests: print("[!] 'requests' library required for API mode", file=sys.stderr) sys.exit(1) url = f"{opa_url}/v1/data/{policy_path.replace('.', '/')}" print(f"[*] Querying OPA API: {url}") resp = requests.post( url, json={"input": input_data}, timeout=30, ) resp.raise_for_status() return resp.json() def test_policies(opa_bin, policy_dir): """Run OPA test suite against policy directory.""" cmd = [opa_bin, "test", "--format", "json", policy_dir, "-v"] print(f"[*] Running policy tests: {' '.join(cmd)}") result = subprocess.run( cmd, capture_output=True, text=True, timeout=120, ) try: test_results = json.loads(result.stdout) except json.JSONDecodeError: test_results = [] return test_results, result.returncode def check_policy_syntax(opa_bin, policy_path): """Check Rego policy syntax.""" cmd = [opa_bin, "check", "--format", "json", policy_path] result = subprocess.run( cmd, capture_output=True, text=True, timeout=30, ) if result.returncode == 0: print(f"[+] Policy syntax valid: {policy_path}") return True, [] try: errors = json.loads(result.stdout) except json.JSONDecodeError: errors = [{"message": result.stderr}] print(f"[!] Syntax errors in {policy_path}") return False, errors def extract_violations(eval_result, violation_key="violations"): """Extract policy violations from OPA evaluation result.""" violations = [] if not eval_result: return violations results = eval_result.get("result", []) if isinstance(results, list): for entry in results: bindings = entry.get("bindings", {}) expressions = entry.get("expressions", []) for expr in expressions: value = expr.get("value", {}) if isinstance(value, dict): for key, val in value.items(): if key == violation_key and isinstance(val, list): violations.extend(val) elif isinstance(val, dict): nested_violations = val.get(violation_key, []) if isinstance(nested_violations, list): violations.extend(nested_violations) elif isinstance(results, dict): violations = results.get(violation_key, []) return violations def format_summary(violations, test_results, policy_path, input_path): """Print evaluation summary.""" print(f"\n{'='*60}") print(f" OPA Policy Evaluation Report") print(f"{'='*60}") print(f" Policy : {policy_path}") print(f" Input : {input_path or 'N/A'}") print(f" Violations: {len(violations)}") if test_results: passed = sum(1 for t in test_results if t.get("pass", t.get("result") == "pass")) failed = len(test_results) - passed print(f" Tests : {passed} passed, {failed} failed") if violations: severity_counts = {} for v in violations: sev = "HIGH" if isinstance(v, dict): sev = v.get("severity", v.get("level", "HIGH")) severity_counts[sev] = severity_counts.get(sev, 0) + 1 print(f"\n Violations by Severity:") for sev, count in sorted(severity_counts.items()): print(f" {sev:10s}: {count}") print(f"\n Violation Details:") for v in violations[:20]: if isinstance(v, dict): msg = v.get("msg", v.get("message", str(v))) resource = v.get("resource", v.get("name", "")) sev = v.get("severity", "HIGH") print(f" [{sev:6s}] {resource:30s} | {msg[:60]}") else: print(f" {str(v)[:80]}") return len(violations) def main(): parser = argparse.ArgumentParser( description="Open Policy Agent policy-as-code evaluation agent" ) sub = parser.add_subparsers(dest="command", help="Action") p_eval = sub.add_parser("eval", help="Evaluate policy against input") p_eval.add_argument("--policy", required=True, help="Path to Rego policy or bundle dir") p_eval.add_argument("--input", dest="input_file", help="Path to input JSON") p_eval.add_argument("--data", help="Path to external data JSON") p_eval.add_argument("--query", default="data", help="OPA query (default: data)") p_eval.add_argument("--violation-key", default="violations", help="Key in result containing violations") p_api = sub.add_parser("api", help="Evaluate via OPA REST API") p_api.add_argument("--url", default="http://localhost:8181", help="OPA server URL") p_api.add_argument("--policy-path", required=True, help="OPA document path (e.g., authz.allow)") p_api.add_argument("--input", dest="input_file", required=True, help="Input JSON file") p_test = sub.add_parser("test", help="Run OPA test suite") p_test.add_argument("--policy-dir", required=True, help="Directory containing policies and tests") p_check = sub.add_parser("check", help="Check Rego syntax") p_check.add_argument("--policy", required=True, help="Policy file or directory") parser.add_argument("--output", "-o", help="Output JSON report path") parser.add_argument("--verbose", "-v", action="store_true") args = parser.parse_args() if not args.command: parser.print_help() sys.exit(1) opa_bin = find_opa_binary() violations = [] test_results = [] if args.command == "eval": if not opa_bin: print("[!] OPA binary not found", file=sys.stderr) sys.exit(1) eval_result = eval_policy_cli( opa_bin, args.policy, args.input_file, args.data, args.query ) violations = extract_violations(eval_result, args.violation_key) format_summary(violations, [], args.policy, args.input_file) elif args.command == "api": with open(args.input_file, "r") as f: input_data = json.load(f) eval_result = eval_policy_api(args.url, args.policy_path, input_data) violations = extract_violations(eval_result) format_summary(violations, [], args.policy_path, args.input_file) elif args.command == "test": if not opa_bin: print("[!] OPA binary not found", file=sys.stderr) sys.exit(1) test_results, returncode = test_policies(opa_bin, args.policy_dir) format_summary([], test_results, args.policy_dir, None) elif args.command == "check": if not opa_bin: print("[!] OPA binary not found", file=sys.stderr) sys.exit(1) valid, errors = check_policy_syntax(opa_bin, args.policy) if not valid: for e in errors: print(f" Error: {e}") report = { "timestamp": datetime.now(timezone.utc).isoformat(), "tool": "Open Policy Agent", "command": args.command, "violations_count": len(violations), "violations": violations, "test_results": test_results, "risk_level": ( "CRITICAL" if len(violations) > 10 else "HIGH" if len(violations) > 0 else "LOW" ), } if args.output: with open(args.output, "w") as f: json.dump(report, f, indent=2) print(f"\n[+] Report saved to {args.output}") elif args.verbose: print(json.dumps(report, indent=2)) if __name__ == "__main__": main()