#!/usr/bin/env python3 """Agent for auditing HashiCorp Boundary zero trust access configuration.""" import os import subprocess import json import argparse from datetime import datetime, timezone def run_boundary_cmd(args_list, addr, token): """Execute a boundary CLI command and return parsed JSON.""" env_vars = {"BOUNDARY_ADDR": addr, "BOUNDARY_TOKEN": token} cmd = ["boundary"] + args_list + ["-format=json"] result = subprocess.run(cmd, capture_output=True, text=True, env={**dict(os.environ), **env_vars}, timeout=30) if result.returncode != 0: print(f" [-] Error: {result.stderr.strip()[:200]}") return {} return json.loads(result.stdout) if result.stdout else {} def list_scopes(addr, token): """List organization and project scopes.""" data = run_boundary_cmd(["scopes", "list", "-scope-id=global"], addr, token) scopes = data.get("items", []) print(f"[*] Scopes: {len(scopes)}") for s in scopes: print(f" {s.get('id')}: {s.get('name', 'unnamed')} (type={s.get('type')})") return scopes def list_targets(addr, token, scope_id): """List targets (resources users can connect to) in a scope.""" data = run_boundary_cmd(["targets", "list", f"-scope-id={scope_id}"], addr, token) targets = data.get("items", []) findings = [] print(f"\n[*] Targets in scope {scope_id}: {len(targets)}") for t in targets: session_max = t.get("session_max_seconds", 0) conn_limit = t.get("session_connection_limit", -1) print(f" {t.get('name')}: type={t.get('type')}, " f"max_sec={session_max}, conn_limit={conn_limit}") if conn_limit == -1: findings.append({"target": t.get("name"), "issue": "Unlimited connections", "severity": "MEDIUM"}) if session_max == 0 or session_max > 28800: findings.append({"target": t.get("name"), "issue": f"Long session timeout ({session_max}s)", "severity": "HIGH"}) return targets, findings def list_host_catalogs(addr, token, scope_id): """List host catalogs (static or dynamic).""" data = run_boundary_cmd(["host-catalogs", "list", f"-scope-id={scope_id}"], addr, token) catalogs = data.get("items", []) print(f"\n[*] Host Catalogs in scope {scope_id}: {len(catalogs)}") for c in catalogs: print(f" {c.get('id')}: {c.get('name', 'unnamed')} (type={c.get('type')})") return catalogs def list_credential_stores(addr, token, scope_id): """List credential stores (Vault integration check).""" data = run_boundary_cmd(["credential-stores", "list", f"-scope-id={scope_id}"], addr, token) stores = data.get("items", []) print(f"\n[*] Credential Stores in scope {scope_id}: {len(stores)}") vault_stores = [s for s in stores if s.get("type") == "vault"] static_stores = [s for s in stores if s.get("type") == "static"] print(f" Vault-backed: {len(vault_stores)}, Static: {len(static_stores)}") findings = [] if static_stores: for s in static_stores: findings.append({"store": s.get("name"), "type": "static", "issue": "Static credentials (not Vault-brokered)", "severity": "MEDIUM"}) return stores, findings def list_sessions(addr, token, scope_id): """List active sessions for audit.""" data = run_boundary_cmd(["sessions", "list", f"-scope-id={scope_id}"], addr, token) sessions = data.get("items", []) print(f"\n[*] Active Sessions in scope {scope_id}: {len(sessions)}") for s in sessions[:10]: print(f" {s.get('id')[:12]}... user={s.get('user_id', 'N/A')} " f"target={s.get('target_id', 'N/A')} status={s.get('status')}") return sessions def check_auth_methods(addr, token, scope_id="global"): """Audit configured authentication methods.""" data = run_boundary_cmd(["auth-methods", "list", f"-scope-id={scope_id}"], addr, token) methods = data.get("items", []) findings = [] print(f"\n[*] Auth Methods: {len(methods)}") for m in methods: mtype = m.get("type", "unknown") print(f" {m.get('name', 'unnamed')}: type={mtype}") if mtype == "password": findings.append({"method": m.get("name"), "type": mtype, "issue": "Password-only auth (use OIDC for zero trust)", "severity": "HIGH"}) return methods, findings def generate_report(target_findings, cred_findings, auth_findings, output_path): """Generate Boundary audit report.""" all_findings = target_findings + cred_findings + auth_findings report = { "audit_date": datetime.now(timezone.utc).isoformat(), "total_findings": len(all_findings), "target_findings": target_findings, "credential_findings": cred_findings, "auth_findings": auth_findings, } with open(output_path, "w") as f: json.dump(report, f, indent=2, default=str) print(f"\n[*] Report saved to {output_path}") def main(): parser = argparse.ArgumentParser(description="HashiCorp Boundary Zero Trust Audit Agent") parser.add_argument("action", choices=["scopes", "targets", "hosts", "creds", "sessions", "auth", "full-audit"]) parser.add_argument("--addr", required=True, help="Boundary controller address") parser.add_argument("--token", required=True, help="Boundary auth token") parser.add_argument("--scope-id", default="global", help="Scope ID to audit") parser.add_argument("-o", "--output", default="boundary_audit.json") args = parser.parse_args() if args.action == "scopes": list_scopes(args.addr, args.token) elif args.action == "targets": list_targets(args.addr, args.token, args.scope_id) elif args.action == "hosts": list_host_catalogs(args.addr, args.token, args.scope_id) elif args.action == "creds": list_credential_stores(args.addr, args.token, args.scope_id) elif args.action == "sessions": list_sessions(args.addr, args.token, args.scope_id) elif args.action == "auth": check_auth_methods(args.addr, args.token) elif args.action == "full-audit": scopes = list_scopes(args.addr, args.token) all_tf, all_cf, all_af = [], [], [] _, af = check_auth_methods(args.addr, args.token) all_af.extend(af) for s in scopes: sid = s.get("id") _, tf = list_targets(args.addr, args.token, sid) all_tf.extend(tf) _, cf = list_credential_stores(args.addr, args.token, sid) all_cf.extend(cf) list_sessions(args.addr, args.token, sid) generate_report(all_tf, all_cf, all_af, args.output) if __name__ == "__main__": main()