Initial commit: Agent Governance System Phase 8
Phase 8 Production Hardening with complete governance infrastructure: - Vault integration with tiered policies (T0-T4) - DragonflyDB state management - SQLite audit ledger - Pipeline DSL and templates - Promotion/revocation engine - Checkpoint system for session persistence - Health manager and circuit breaker for fault tolerance - GitHub/Slack integrations - Architectural test pipeline with bug watcher, suggestion engine, council review - Multi-agent chaos testing framework Test Results: - Governance tests: 68/68 passing - E2E workflow: 16/16 passing - Phase 2 Vault: 14/14 passing - Integration tests: 27/27 passing Coverage: 57.6% average across 12 phases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
# Governance
|
||||
|
||||
> Governance system tests
|
||||
|
||||
## Overview
|
||||
|
||||
This directory contains governance system tests.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `test_all.py` | Python module |
|
||||
| `test_all.ts` | TypeScript module |
|
||||
| `package.json` | Data/Schema |
|
||||
|
||||
## Interfaces / APIs
|
||||
|
||||
*Document any APIs, CLI commands, or interfaces here.*
|
||||
|
||||
## Status
|
||||
|
||||
** Not Started**
|
||||
|
||||
See [STATUS.md](./STATUS.md) for detailed progress tracking.
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
Part of the [Agent Governance System](/opt/agent-governance/docs/ARCHITECTURE.md).
|
||||
|
||||
Parent: [Tests](..)
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,30 @@
|
||||
# Status: Governance
|
||||
|
||||
## Current Phase
|
||||
|
||||
** NOT STARTED**
|
||||
|
||||
## Tasks
|
||||
|
||||
| Status | Task | Updated |
|
||||
|--------|------|---------|
|
||||
| ☐ | *No tasks defined* | - |
|
||||
|
||||
## Dependencies
|
||||
|
||||
*No external dependencies.*
|
||||
|
||||
## Issues / Blockers
|
||||
|
||||
*No current issues or blockers.*
|
||||
|
||||
## Activity Log
|
||||
|
||||
### 2026-01-23 23:25:09 UTC
|
||||
- **Phase**: NOT STARTED
|
||||
- **Action**: Initialized
|
||||
- **Details**: Status tracking initialized for this directory.
|
||||
|
||||
|
||||
---
|
||||
*Last updated: 2026-01-23 23:25:09 UTC*
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"redis": "^5.10.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@redis/bloom": ["@redis/bloom@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A=="],
|
||||
|
||||
"@redis/client": ["@redis/client@5.10.0", "", { "dependencies": { "cluster-key-slot": "1.1.2" } }, "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA=="],
|
||||
|
||||
"@redis/json": ["@redis/json@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-B2G8XlOmTPUuZtD44EMGbtoepQG34RCDXLZbjrtON1Djet0t5Ri7/YPXvL9aomXqP8lLTreaprtyLKF4tmXEEA=="],
|
||||
|
||||
"@redis/search": ["@redis/search@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-3SVcPswoSfp2HnmWbAGUzlbUPn7fOohVu2weUQ0S+EMiQi8jwjL+aN2p6V3TI65eNfVsJ8vyPvqWklm6H6esmg=="],
|
||||
|
||||
"@redis/time-series": ["@redis/time-series@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-cPkpddXH5kc/SdRhF0YG0qtjL+noqFT0AcHbQ6axhsPsO7iqPi1cjxgdkE9TNeKiBUUdCaU1DbqkR/LzbzPBhg=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"redis": ["redis@5.10.0", "", { "dependencies": { "@redis/bloom": "5.10.0", "@redis/client": "5.10.0", "@redis/json": "5.10.0", "@redis/search": "5.10.0", "@redis/time-series": "5.10.0" } }, "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"redis": "^5.10.0"
|
||||
}
|
||||
}
|
||||
Executable
+573
@@ -0,0 +1,573 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive Governance System Tests
|
||||
======================================
|
||||
Tests all components of the agent governance framework:
|
||||
- Vault connectivity and policies
|
||||
- DragonflyDB state management
|
||||
- SQLite ledger
|
||||
- Preflight system
|
||||
- Promotion engine
|
||||
- Revocation engine
|
||||
- Checkpoint skill
|
||||
- Model Controller
|
||||
- Tier 0 agent
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Colors for output
|
||||
GREEN = '\033[92m'
|
||||
RED = '\033[91m'
|
||||
YELLOW = '\033[93m'
|
||||
BLUE = '\033[94m'
|
||||
RESET = '\033[0m'
|
||||
|
||||
RESULTS = {"passed": 0, "failed": 0, "skipped": 0}
|
||||
|
||||
|
||||
def log_test(name: str, passed: bool, message: str = ""):
|
||||
global RESULTS
|
||||
if passed:
|
||||
RESULTS["passed"] += 1
|
||||
status = f"{GREEN}PASS{RESET}"
|
||||
else:
|
||||
RESULTS["failed"] += 1
|
||||
status = f"{RED}FAIL{RESET}"
|
||||
|
||||
msg = f" - {message}" if message else ""
|
||||
print(f" [{status}] {name}{msg}")
|
||||
|
||||
|
||||
def log_skip(name: str, reason: str):
|
||||
global RESULTS
|
||||
RESULTS["skipped"] += 1
|
||||
print(f" [{YELLOW}SKIP{RESET}] {name} - {reason}")
|
||||
|
||||
|
||||
def log_section(title: str):
|
||||
print(f"\n{BLUE}{'='*60}{RESET}")
|
||||
print(f"{BLUE}{title}{RESET}")
|
||||
print(f"{BLUE}{'='*60}{RESET}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Vault Connectivity
|
||||
# =============================================================================
|
||||
|
||||
def test_vault():
|
||||
log_section("VAULT TESTS")
|
||||
|
||||
# Test 1: Vault is running
|
||||
result = subprocess.run(
|
||||
["docker", "exec", "vault", "vault", "status", "-format=json"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
try:
|
||||
status = json.loads(result.stdout)
|
||||
log_test("Vault is running", True)
|
||||
log_test("Vault is initialized", status.get("initialized", False))
|
||||
log_test("Vault is unsealed", not status.get("sealed", True))
|
||||
except:
|
||||
log_test("Vault is running", False, result.stderr[:100] if result.stderr else "Connection failed")
|
||||
return
|
||||
|
||||
# Test 2: Get root token
|
||||
try:
|
||||
with open("/opt/vault/init-keys.json") as f:
|
||||
keys = json.load(f)
|
||||
root_token = keys["root_token"]
|
||||
log_test("Root token accessible", True)
|
||||
except Exception as e:
|
||||
log_test("Root token accessible", False, str(e))
|
||||
return
|
||||
|
||||
# Test 3: List policies
|
||||
result = subprocess.run(
|
||||
["docker", "exec", "-e", f"VAULT_TOKEN={root_token}", "vault",
|
||||
"vault", "policy", "list", "-format=json"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
try:
|
||||
policies = json.loads(result.stdout)
|
||||
expected = ["t0-observer", "t1-operator", "t2-builder", "t3-executor", "t4-architect"]
|
||||
found = [p for p in expected if p in policies]
|
||||
log_test("Tier policies loaded", len(found) == len(expected),
|
||||
f"{len(found)}/{len(expected)} policies")
|
||||
except:
|
||||
log_test("Tier policies loaded", False)
|
||||
|
||||
# Test 4: Secrets engines
|
||||
result = subprocess.run(
|
||||
["docker", "exec", "-e", f"VAULT_TOKEN={root_token}", "vault",
|
||||
"vault", "secrets", "list", "-format=json"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
try:
|
||||
engines = json.loads(result.stdout)
|
||||
log_test("SSH secrets engine enabled", "ssh/" in engines)
|
||||
log_test("Proxmox KV engine enabled", "proxmox/" in engines)
|
||||
log_test("Secret KV engine enabled", "secret/" in engines)
|
||||
except:
|
||||
log_test("Secrets engines", False)
|
||||
|
||||
# Test 5: AppRole auth
|
||||
result = subprocess.run(
|
||||
["docker", "exec", "-e", f"VAULT_TOKEN={root_token}", "vault",
|
||||
"vault", "auth", "list", "-format=json"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
try:
|
||||
auth = json.loads(result.stdout)
|
||||
log_test("AppRole auth enabled", "approle/" in auth)
|
||||
except:
|
||||
log_test("AppRole auth enabled", False)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: DragonflyDB Connectivity
|
||||
# =============================================================================
|
||||
|
||||
def test_dragonfly():
|
||||
log_section("DRAGONFLYDB TESTS")
|
||||
|
||||
try:
|
||||
import redis
|
||||
except ImportError:
|
||||
log_skip("DragonflyDB tests", "redis module not installed")
|
||||
return
|
||||
|
||||
# Get credentials from Vault
|
||||
try:
|
||||
with open("/opt/vault/init-keys.json") as f:
|
||||
root_token = json.load(f)["root_token"]
|
||||
|
||||
result = subprocess.run([
|
||||
"curl", "-sk",
|
||||
"-H", f"X-Vault-Token: {root_token}",
|
||||
"https://127.0.0.1:8200/v1/secret/data/services/dragonfly"
|
||||
], capture_output=True, text=True)
|
||||
|
||||
creds = json.loads(result.stdout)["data"]["data"]
|
||||
log_test("DragonflyDB credentials in Vault", True)
|
||||
except Exception as e:
|
||||
log_test("DragonflyDB credentials in Vault", False, str(e))
|
||||
return
|
||||
|
||||
# Connect to DragonflyDB
|
||||
try:
|
||||
r = redis.Redis(
|
||||
host=creds["host"],
|
||||
port=int(creds["port"]),
|
||||
password=creds["password"],
|
||||
decode_responses=True
|
||||
)
|
||||
r.ping()
|
||||
log_test("DragonflyDB connection", True)
|
||||
except Exception as e:
|
||||
log_test("DragonflyDB connection", False, str(e))
|
||||
return
|
||||
|
||||
# Test basic operations
|
||||
try:
|
||||
r.set("test:governance:ping", "pong", ex=60)
|
||||
value = r.get("test:governance:ping")
|
||||
log_test("DragonflyDB read/write", value == "pong")
|
||||
except Exception as e:
|
||||
log_test("DragonflyDB read/write", False, str(e))
|
||||
|
||||
# Test list operations
|
||||
try:
|
||||
r.delete("test:governance:list")
|
||||
r.lpush("test:governance:list", "item1", "item2")
|
||||
items = r.lrange("test:governance:list", 0, -1)
|
||||
log_test("DragonflyDB list operations", len(items) == 2)
|
||||
r.delete("test:governance:list")
|
||||
except Exception as e:
|
||||
log_test("DragonflyDB list operations", False, str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: SQLite Ledger
|
||||
# =============================================================================
|
||||
|
||||
def test_ledger():
|
||||
log_section("LEDGER TESTS")
|
||||
|
||||
db_path = Path("/opt/agent-governance/ledger/governance.db")
|
||||
|
||||
log_test("Ledger database exists", db_path.exists())
|
||||
|
||||
if not db_path.exists():
|
||||
return
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check tables exist
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
tables = [row[0] for row in cursor.fetchall()]
|
||||
|
||||
expected_tables = ["agent_metrics", "violations", "promotions", "orchestration_log"]
|
||||
for table in expected_tables:
|
||||
log_test(f"Table '{table}' exists", table in tables)
|
||||
|
||||
# Test insert/query
|
||||
cursor.execute("""
|
||||
INSERT OR REPLACE INTO agent_metrics
|
||||
(agent_id, current_tier, compliant_runs, consecutive_compliant, total_runs, updated_at)
|
||||
VALUES ('test-agent-pytest', 0, 0, 0, 0, datetime('now'))
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
cursor.execute("SELECT * FROM agent_metrics WHERE agent_id='test-agent-pytest'")
|
||||
row = cursor.fetchone()
|
||||
log_test("Ledger insert/query", row is not None)
|
||||
|
||||
# Cleanup
|
||||
cursor.execute("DELETE FROM agent_metrics WHERE agent_id='test-agent-pytest'")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
log_test("Ledger operations", False, str(e))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Preflight System
|
||||
# =============================================================================
|
||||
|
||||
def test_preflight():
|
||||
log_section("PREFLIGHT TESTS")
|
||||
|
||||
preflight_dir = Path("/opt/agent-governance/preflight")
|
||||
|
||||
log_test("Preflight directory exists", preflight_dir.exists())
|
||||
|
||||
if not preflight_dir.exists():
|
||||
return
|
||||
|
||||
# Test preflight script exists
|
||||
preflight_py = preflight_dir / "preflight.py"
|
||||
log_test("preflight.py exists", preflight_py.exists())
|
||||
|
||||
if not preflight_py.exists():
|
||||
return
|
||||
|
||||
# Test sandbox target (should pass)
|
||||
result = subprocess.run(
|
||||
["python3", str(preflight_py), "sandbox-vm-01", "--action", "generic",
|
||||
"--tier", "1", "--agent-id", "test"],
|
||||
capture_output=True, text=True,
|
||||
cwd=str(preflight_dir)
|
||||
)
|
||||
log_test("Preflight approves sandbox target", result.returncode == 0)
|
||||
|
||||
# Test production target (should fail for tier 1)
|
||||
result = subprocess.run(
|
||||
["python3", str(preflight_py), "prod-db-01", "--action", "generic",
|
||||
"--tier", "1", "--agent-id", "test"],
|
||||
capture_output=True, text=True,
|
||||
cwd=str(preflight_dir)
|
||||
)
|
||||
log_test("Preflight blocks prod for tier 1", result.returncode != 0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Promotion Engine
|
||||
# =============================================================================
|
||||
|
||||
def test_promotion():
|
||||
log_section("PROMOTION ENGINE TESTS")
|
||||
|
||||
promotion_py = Path("/opt/agent-governance/runtime/promotion.py")
|
||||
|
||||
log_test("promotion.py exists", promotion_py.exists())
|
||||
|
||||
if not promotion_py.exists():
|
||||
return
|
||||
|
||||
runtime_dir = Path("/opt/agent-governance/runtime")
|
||||
|
||||
# Test requirements command
|
||||
result = subprocess.run(
|
||||
["python3", str(promotion_py), "requirements"],
|
||||
capture_output=True, text=True,
|
||||
cwd=str(runtime_dir)
|
||||
)
|
||||
log_test("Promotion requirements command", result.returncode == 0)
|
||||
|
||||
# Check output contains tier requirements
|
||||
has_tiers = "T0 → T1" in result.stdout or "Tier 0" in result.stdout
|
||||
log_test("Promotion requirements output valid", has_tiers)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Revocation Engine
|
||||
# =============================================================================
|
||||
|
||||
def test_revocation():
|
||||
log_section("REVOCATION ENGINE TESTS")
|
||||
|
||||
revocation_py = Path("/opt/agent-governance/runtime/revocation.py")
|
||||
|
||||
log_test("revocation.py exists", revocation_py.exists())
|
||||
|
||||
if not revocation_py.exists():
|
||||
return
|
||||
|
||||
runtime_dir = Path("/opt/agent-governance/runtime")
|
||||
|
||||
# Test types command
|
||||
result = subprocess.run(
|
||||
["python3", str(revocation_py), "types"],
|
||||
capture_output=True, text=True,
|
||||
cwd=str(runtime_dir)
|
||||
)
|
||||
log_test("Revocation types command", result.returncode == 0)
|
||||
|
||||
# Check output contains violation types
|
||||
has_types = "UNAUTHORIZED" in result.stdout or "VIOLATION" in result.stdout or "critical" in result.stdout.lower()
|
||||
log_test("Revocation types output valid", has_types)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Checkpoint Skill
|
||||
# =============================================================================
|
||||
|
||||
def test_checkpoint():
|
||||
log_section("CHECKPOINT SKILL TESTS")
|
||||
|
||||
checkpoint_bin = Path("/opt/agent-governance/bin/checkpoint")
|
||||
|
||||
log_test("checkpoint CLI exists", checkpoint_bin.exists())
|
||||
|
||||
if not checkpoint_bin.exists():
|
||||
return
|
||||
|
||||
# Test checkpoint now
|
||||
result = subprocess.run(
|
||||
[str(checkpoint_bin), "now", "--notes", "pytest run"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
log_test("Checkpoint create", result.returncode == 0)
|
||||
|
||||
# Extract checkpoint ID
|
||||
checkpoint_id = None
|
||||
for line in result.stdout.split("\n"):
|
||||
if line.startswith("ID:"):
|
||||
checkpoint_id = line.split(":")[1].strip()
|
||||
break
|
||||
|
||||
log_test("Checkpoint ID generated", checkpoint_id is not None)
|
||||
|
||||
# Test checkpoint load
|
||||
result = subprocess.run(
|
||||
[str(checkpoint_bin), "load", "--json"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
log_test("Checkpoint load", result.returncode == 0)
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
log_test("Checkpoint JSON valid", True)
|
||||
log_test("Checkpoint has phase", data.get("phase") is not None)
|
||||
log_test("Checkpoint has dependencies", len(data.get("dependencies", [])) > 0)
|
||||
except:
|
||||
log_test("Checkpoint JSON valid", False)
|
||||
|
||||
# Test checkpoint list
|
||||
result = subprocess.run(
|
||||
[str(checkpoint_bin), "list"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
log_test("Checkpoint list", result.returncode == 0)
|
||||
|
||||
# Test queue commands
|
||||
result = subprocess.run(
|
||||
[str(checkpoint_bin), "queue", "list"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
log_test("Checkpoint queue list", result.returncode == 0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Model Controller
|
||||
# =============================================================================
|
||||
|
||||
def test_model_controller():
|
||||
log_section("MODEL CONTROLLER TESTS")
|
||||
|
||||
controller_bin = Path("/opt/agent-governance/bin/model-controller")
|
||||
|
||||
log_test("model-controller CLI exists", controller_bin.exists())
|
||||
|
||||
if not controller_bin.exists():
|
||||
return
|
||||
|
||||
# Test status command
|
||||
result = subprocess.run(
|
||||
[str(controller_bin), "status"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
log_test("Model controller status", result.returncode == 0)
|
||||
|
||||
# Check output
|
||||
has_mode = "Mode:" in result.stdout or "mode:" in result.stdout.lower()
|
||||
log_test("Model controller status output valid", has_mode)
|
||||
|
||||
# Test config command
|
||||
result = subprocess.run(
|
||||
[str(controller_bin), "config"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
log_test("Model controller config", result.returncode == 0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Tier 0 Agent
|
||||
# =============================================================================
|
||||
|
||||
def test_tier0_agent():
|
||||
log_section("TIER 0 AGENT TESTS")
|
||||
|
||||
agent_dir = Path("/opt/agent-governance/agents/tier0-agent")
|
||||
agent_py = agent_dir / "agent.py"
|
||||
|
||||
log_test("Tier 0 agent directory exists", agent_dir.exists())
|
||||
log_test("agent.py exists", agent_py.exists())
|
||||
|
||||
if not agent_py.exists():
|
||||
return
|
||||
|
||||
# Test status command
|
||||
result = subprocess.run(
|
||||
["python3", str(agent_py), "status"],
|
||||
capture_output=True, text=True,
|
||||
cwd=str(agent_dir)
|
||||
)
|
||||
log_test("Agent status command", result.returncode == 0)
|
||||
log_test("Agent shows as Tier 0", "Tier: 0" in result.stdout or "Tier 0" in result.stdout)
|
||||
|
||||
# Test reading allowed file
|
||||
result = subprocess.run(
|
||||
["python3", str(agent_py), "read", "/opt/agent-governance/docs/tier0-guide.md"],
|
||||
capture_output=True, text=True,
|
||||
cwd=str(agent_dir)
|
||||
)
|
||||
log_test("Agent can read docs", result.returncode == 0)
|
||||
|
||||
# Test reading forbidden file
|
||||
result = subprocess.run(
|
||||
["python3", str(agent_py), "read", "/etc/passwd"],
|
||||
capture_output=True, text=True,
|
||||
cwd=str(agent_dir)
|
||||
)
|
||||
log_test("Agent blocked from /etc", result.returncode != 0 or "BLOCKED" in result.stdout)
|
||||
|
||||
# Test test-forbidden command
|
||||
result = subprocess.run(
|
||||
["python3", str(agent_py), "test-forbidden"],
|
||||
capture_output=True, text=True,
|
||||
cwd=str(agent_dir)
|
||||
)
|
||||
log_test("Agent test-forbidden command", result.returncode == 0)
|
||||
log_test("All forbidden actions blocked", "All forbidden actions correctly blocked" in result.stdout)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Governance Wrappers
|
||||
# =============================================================================
|
||||
|
||||
def test_wrappers():
|
||||
log_section("GOVERNANCE WRAPPERS TESTS")
|
||||
|
||||
wrappers_dir = Path("/opt/agent-governance/wrappers")
|
||||
|
||||
log_test("Wrappers directory exists", wrappers_dir.exists())
|
||||
|
||||
tf_wrapper = wrappers_dir / "tf-governed.sh"
|
||||
ansible_wrapper = wrappers_dir / "ansible-governed.sh"
|
||||
|
||||
log_test("Terraform wrapper exists", tf_wrapper.exists())
|
||||
log_test("Ansible wrapper exists", ansible_wrapper.exists())
|
||||
|
||||
# Check they're executable
|
||||
if tf_wrapper.exists():
|
||||
log_test("Terraform wrapper executable", os.access(tf_wrapper, os.X_OK))
|
||||
if ansible_wrapper.exists():
|
||||
log_test("Ansible wrapper executable", os.access(ansible_wrapper, os.X_OK))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Test: Evidence System
|
||||
# =============================================================================
|
||||
|
||||
def test_evidence():
|
||||
log_section("EVIDENCE SYSTEM TESTS")
|
||||
|
||||
evidence_dir = Path("/opt/agent-governance/evidence")
|
||||
evidence_py = evidence_dir / "evidence.py"
|
||||
|
||||
log_test("Evidence directory exists", evidence_dir.exists())
|
||||
log_test("evidence.py exists", evidence_py.exists())
|
||||
|
||||
packages_dir = evidence_dir / "packages"
|
||||
log_test("Evidence packages directory exists", packages_dir.exists())
|
||||
|
||||
if packages_dir.exists():
|
||||
packages = list(packages_dir.iterdir())
|
||||
log_test("Evidence packages created", len(packages) > 0, f"{len(packages)} packages")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Main
|
||||
# =============================================================================
|
||||
|
||||
def main():
|
||||
print(f"\n{BLUE}{'#'*60}{RESET}")
|
||||
print(f"{BLUE}# AGENT GOVERNANCE SYSTEM - COMPREHENSIVE TESTS{RESET}")
|
||||
print(f"{BLUE}# {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}{RESET}")
|
||||
print(f"{BLUE}{'#'*60}{RESET}")
|
||||
|
||||
# Run all tests
|
||||
test_vault()
|
||||
test_dragonfly()
|
||||
test_ledger()
|
||||
test_preflight()
|
||||
test_promotion()
|
||||
test_revocation()
|
||||
test_checkpoint()
|
||||
test_model_controller()
|
||||
test_tier0_agent()
|
||||
test_wrappers()
|
||||
test_evidence()
|
||||
|
||||
# Summary
|
||||
print(f"\n{BLUE}{'='*60}{RESET}")
|
||||
print(f"{BLUE}TEST SUMMARY{RESET}")
|
||||
print(f"{BLUE}{'='*60}{RESET}")
|
||||
|
||||
total = RESULTS["passed"] + RESULTS["failed"] + RESULTS["skipped"]
|
||||
print(f" {GREEN}Passed:{RESET} {RESULTS['passed']}")
|
||||
print(f" {RED}Failed:{RESET} {RESULTS['failed']}")
|
||||
print(f" {YELLOW}Skipped:{RESET} {RESULTS['skipped']}")
|
||||
print(f" Total: {total}")
|
||||
|
||||
if RESULTS["failed"] > 0:
|
||||
print(f"\n{RED}Some tests failed!{RESET}")
|
||||
return 1
|
||||
else:
|
||||
print(f"\n{GREEN}All tests passed!{RESET}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,325 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Comprehensive Governance System Tests - TypeScript/Bun
|
||||
* =======================================================
|
||||
* Tests all components of the agent governance framework from TypeScript
|
||||
*/
|
||||
|
||||
import { $ } from "bun";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { createClient, type RedisClientType } from "redis";
|
||||
|
||||
// Colors
|
||||
const GREEN = "\x1b[92m";
|
||||
const RED = "\x1b[91m";
|
||||
const YELLOW = "\x1b[93m";
|
||||
const BLUE = "\x1b[94m";
|
||||
const RESET = "\x1b[0m";
|
||||
|
||||
interface TestResults {
|
||||
passed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
}
|
||||
|
||||
const results: TestResults = { passed: 0, failed: 0, skipped: 0 };
|
||||
|
||||
function logTest(name: string, passed: boolean, message?: string): void {
|
||||
if (passed) {
|
||||
results.passed++;
|
||||
const status = `${GREEN}PASS${RESET}`;
|
||||
console.log(` [${status}] ${name}${message ? ` - ${message}` : ""}`);
|
||||
} else {
|
||||
results.failed++;
|
||||
const status = `${RED}FAIL${RESET}`;
|
||||
console.log(` [${status}] ${name}${message ? ` - ${message}` : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
function logSkip(name: string, reason: string): void {
|
||||
results.skipped++;
|
||||
console.log(` [${YELLOW}SKIP${RESET}] ${name} - ${reason}`);
|
||||
}
|
||||
|
||||
function logSection(title: string): void {
|
||||
console.log(`\n${BLUE}${"=".repeat(60)}${RESET}`);
|
||||
console.log(`${BLUE}${title}${RESET}`);
|
||||
console.log(`${BLUE}${"=".repeat(60)}${RESET}`);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: Vault Connectivity
|
||||
// =============================================================================
|
||||
|
||||
async function testVault(): Promise<void> {
|
||||
logSection("VAULT TESTS (Bun)");
|
||||
|
||||
// Test 1: Vault status via docker
|
||||
try {
|
||||
const result = await $`docker exec vault vault status -format=json`.quiet();
|
||||
const status = JSON.parse(result.stdout.toString());
|
||||
logTest("Vault is running", true);
|
||||
logTest("Vault is initialized", status.initialized === true);
|
||||
logTest("Vault is unsealed", status.sealed === false);
|
||||
} catch (e) {
|
||||
logTest("Vault is running", false, String(e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Test 2: Read root token
|
||||
try {
|
||||
const keysFile = Bun.file("/opt/vault/init-keys.json");
|
||||
const keys = await keysFile.json();
|
||||
logTest("Root token readable", !!keys.root_token);
|
||||
} catch (e) {
|
||||
logTest("Root token readable", false, String(e));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: DragonflyDB Connectivity
|
||||
// =============================================================================
|
||||
|
||||
async function testDragonfly(): Promise<void> {
|
||||
logSection("DRAGONFLYDB TESTS (Bun)");
|
||||
|
||||
let client: RedisClientType | null = null;
|
||||
|
||||
try {
|
||||
// Get credentials
|
||||
const keysFile = Bun.file("/opt/vault/init-keys.json");
|
||||
const keys = await keysFile.json();
|
||||
const rootToken = keys.root_token;
|
||||
|
||||
const response = await fetch(
|
||||
"https://127.0.0.1:8200/v1/secret/data/services/dragonfly",
|
||||
{
|
||||
headers: { "X-Vault-Token": rootToken },
|
||||
// @ts-ignore - Bun supports this
|
||||
tls: { rejectUnauthorized: false },
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
logTest("DragonflyDB credentials from Vault", false);
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const creds = data.data.data;
|
||||
logTest("DragonflyDB credentials from Vault", true);
|
||||
|
||||
// Connect
|
||||
client = createClient({
|
||||
socket: {
|
||||
host: creds.host,
|
||||
port: parseInt(creds.port),
|
||||
},
|
||||
password: creds.password,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
logTest("DragonflyDB connection", true);
|
||||
|
||||
// Test operations
|
||||
await client.set("test:bun:ping", "pong", { EX: 60 });
|
||||
const value = await client.get("test:bun:ping");
|
||||
logTest("DragonflyDB read/write", value === "pong");
|
||||
|
||||
// Test list
|
||||
await client.del("test:bun:list");
|
||||
await client.lPush("test:bun:list", ["item1", "item2"]);
|
||||
const items = await client.lRange("test:bun:list", 0, -1);
|
||||
logTest("DragonflyDB list operations", items.length === 2);
|
||||
await client.del("test:bun:list");
|
||||
} catch (e) {
|
||||
logTest("DragonflyDB operations", false, String(e));
|
||||
} finally {
|
||||
if (client) {
|
||||
await client.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: SQLite Ledger
|
||||
// =============================================================================
|
||||
|
||||
async function testLedger(): Promise<void> {
|
||||
logSection("LEDGER TESTS (Bun)");
|
||||
|
||||
const dbPath = "/opt/agent-governance/ledger/governance.db";
|
||||
const dbFile = Bun.file(dbPath);
|
||||
|
||||
logTest("Ledger database exists", await dbFile.exists());
|
||||
|
||||
if (!(await dbFile.exists())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Check tables
|
||||
const tables = db
|
||||
.query("SELECT name FROM sqlite_master WHERE type='table'")
|
||||
.all() as { name: string }[];
|
||||
const tableNames = tables.map((t) => t.name);
|
||||
|
||||
logTest("Table 'agent_metrics' exists", tableNames.includes("agent_metrics"));
|
||||
logTest("Table 'violations' exists", tableNames.includes("violations"));
|
||||
logTest("Table 'promotions' exists", tableNames.includes("promotions"));
|
||||
logTest("Table 'orchestration_log' exists", tableNames.includes("orchestration_log"));
|
||||
|
||||
// Test insert/query
|
||||
db.run(`
|
||||
INSERT OR REPLACE INTO agent_metrics
|
||||
(agent_id, current_tier, compliant_runs, consecutive_compliant, total_runs, updated_at)
|
||||
VALUES ('test-agent-bun', 0, 0, 0, 0, datetime('now'))
|
||||
`);
|
||||
|
||||
const row = db
|
||||
.query("SELECT * FROM agent_metrics WHERE agent_id='test-agent-bun'")
|
||||
.get();
|
||||
logTest("Ledger insert/query", row !== null);
|
||||
|
||||
// Cleanup
|
||||
db.run("DELETE FROM agent_metrics WHERE agent_id='test-agent-bun'");
|
||||
db.close();
|
||||
} catch (e) {
|
||||
logTest("Ledger operations", false, String(e));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: CLI Tools
|
||||
// =============================================================================
|
||||
|
||||
async function testCLITools(): Promise<void> {
|
||||
logSection("CLI TOOLS TESTS (Bun)");
|
||||
|
||||
// Test checkpoint CLI
|
||||
try {
|
||||
const result = await $`/opt/agent-governance/bin/checkpoint list`.quiet();
|
||||
logTest("Checkpoint CLI works", result.exitCode === 0);
|
||||
} catch (e) {
|
||||
logTest("Checkpoint CLI works", false, String(e));
|
||||
}
|
||||
|
||||
// Test model-controller CLI
|
||||
try {
|
||||
const result = await $`/opt/agent-governance/bin/model-controller status`.quiet();
|
||||
logTest("Model controller CLI works", result.exitCode === 0);
|
||||
} catch (e) {
|
||||
logTest("Model controller CLI works", false, String(e));
|
||||
}
|
||||
|
||||
// Test tier0 agent
|
||||
try {
|
||||
const result =
|
||||
await $`cd /opt/agent-governance/agents/tier0-agent && python3 agent.py status`.quiet();
|
||||
logTest("Tier 0 agent CLI works", result.exitCode === 0);
|
||||
} catch (e) {
|
||||
logTest("Tier 0 agent CLI works", false, String(e));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: File System Structure
|
||||
// =============================================================================
|
||||
|
||||
async function testFileStructure(): Promise<void> {
|
||||
logSection("FILE STRUCTURE TESTS (Bun)");
|
||||
|
||||
const paths = [
|
||||
"/opt/agent-governance/checkpoint/checkpoint.py",
|
||||
"/opt/agent-governance/orchestrator/model_controller.py",
|
||||
"/opt/agent-governance/orchestrator/config.json",
|
||||
"/opt/agent-governance/preflight/preflight.py",
|
||||
"/opt/agent-governance/runtime/promotion.py",
|
||||
"/opt/agent-governance/runtime/revocation.py",
|
||||
"/opt/agent-governance/agents/tier0-agent/agent.py",
|
||||
"/opt/agent-governance/wrappers/tf-governed.sh",
|
||||
"/opt/agent-governance/wrappers/ansible-governed.sh",
|
||||
"/opt/agent-governance/evidence/evidence.py",
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
const file = Bun.file(path);
|
||||
const name = path.split("/").pop() || path;
|
||||
logTest(`${name} exists`, await file.exists());
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Test: Config Files
|
||||
// =============================================================================
|
||||
|
||||
async function testConfigFiles(): Promise<void> {
|
||||
logSection("CONFIG FILES TESTS (Bun)");
|
||||
|
||||
// Test orchestrator config
|
||||
try {
|
||||
const configFile = Bun.file("/opt/agent-governance/orchestrator/config.json");
|
||||
const config = await configFile.json();
|
||||
logTest("Orchestrator config valid JSON", true);
|
||||
logTest("Config has models section", !!config.models);
|
||||
logTest("Config has execution section", !!config.execution);
|
||||
logTest("Config has safety section", !!config.safety);
|
||||
} catch (e) {
|
||||
logTest("Orchestrator config", false, String(e));
|
||||
}
|
||||
|
||||
// Test agent config
|
||||
try {
|
||||
const agentConfig = Bun.file(
|
||||
"/opt/agent-governance/agents/tier0-agent/config/agent.json"
|
||||
);
|
||||
const config = await agentConfig.json();
|
||||
logTest("Agent config valid JSON", true);
|
||||
logTest("Agent config has capabilities", !!config.capabilities);
|
||||
logTest("Agent config has constraints", !!config.constraints);
|
||||
} catch (e) {
|
||||
logTest("Agent config", false, String(e));
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main
|
||||
// =============================================================================
|
||||
|
||||
async function main(): Promise<number> {
|
||||
console.log(`\n${BLUE}${"#".repeat(60)}${RESET}`);
|
||||
console.log(`${BLUE}# AGENT GOVERNANCE SYSTEM - BUN/TYPESCRIPT TESTS${RESET}`);
|
||||
console.log(`${BLUE}# ${new Date().toISOString()}${RESET}`);
|
||||
console.log(`${BLUE}${"#".repeat(60)}${RESET}`);
|
||||
|
||||
await testVault();
|
||||
await testDragonfly();
|
||||
await testLedger();
|
||||
await testCLITools();
|
||||
await testFileStructure();
|
||||
await testConfigFiles();
|
||||
|
||||
// Summary
|
||||
console.log(`\n${BLUE}${"=".repeat(60)}${RESET}`);
|
||||
console.log(`${BLUE}TEST SUMMARY${RESET}`);
|
||||
console.log(`${BLUE}${"=".repeat(60)}${RESET}`);
|
||||
|
||||
const total = results.passed + results.failed + results.skipped;
|
||||
console.log(` ${GREEN}Passed:${RESET} ${results.passed}`);
|
||||
console.log(` ${RED}Failed:${RESET} ${results.failed}`);
|
||||
console.log(` ${YELLOW}Skipped:${RESET} ${results.skipped}`);
|
||||
console.log(` Total: ${total}`);
|
||||
|
||||
if (results.failed > 0) {
|
||||
console.log(`\n${RED}Some tests failed!${RESET}`);
|
||||
return 1;
|
||||
} else {
|
||||
console.log(`\n${GREEN}All tests passed!${RESET}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const exitCode = await main();
|
||||
process.exit(exitCode);
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test: approle_auth
|
||||
Phase 2: Vault Policy Engine
|
||||
"""
|
||||
from test_phase2_vault import test_approle_auth
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_approle_auth()
|
||||
@@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Phase 2: Vault Policy Engine Tests
|
||||
==================================
|
||||
Tests for trust tier policies, secrets engines, and AppRole authentication.
|
||||
|
||||
Required tests:
|
||||
- policy_enforcement: Verify tier policies are enforced
|
||||
- secrets_access: Verify secret path access controls
|
||||
- approle_auth: Verify AppRole authentication works
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Configuration
|
||||
VAULT_ADDR = "https://127.0.0.1:8200"
|
||||
VAULT_TOKEN_FILE = "/opt/vault/init-keys.json"
|
||||
|
||||
# Test results
|
||||
PASSED = 0
|
||||
FAILED = 0
|
||||
|
||||
|
||||
def log(msg: str, status: str = "info"):
|
||||
"""Log a message"""
|
||||
icons = {"pass": "\033[92m✓\033[0m", "fail": "\033[91m✗\033[0m", "info": "→"}
|
||||
print(f" {icons.get(status, '•')} {msg}")
|
||||
|
||||
|
||||
def get_root_token() -> str:
|
||||
"""Get Vault root token"""
|
||||
with open(VAULT_TOKEN_FILE) as f:
|
||||
return json.load(f)["root_token"]
|
||||
|
||||
|
||||
def vault_request(method: str, path: str, token: str, data: dict = None) -> dict:
|
||||
"""Make a Vault API request"""
|
||||
cmd = ["curl", "-sk", "-X", method, "-H", f"X-Vault-Token: {token}"]
|
||||
if data:
|
||||
cmd.extend(["-d", json.dumps(data)])
|
||||
cmd.append(f"{VAULT_ADDR}/v1/{path}")
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||
if result.stdout:
|
||||
return json.loads(result.stdout)
|
||||
return {}
|
||||
|
||||
|
||||
def test_policy_enforcement():
|
||||
"""Test that tier policies are loaded and enforced"""
|
||||
global PASSED, FAILED
|
||||
|
||||
print("\n[TEST] policy_enforcement")
|
||||
root_token = get_root_token()
|
||||
|
||||
# 1. Check all tier policies exist
|
||||
resp = vault_request("GET", "sys/policy", root_token)
|
||||
policies = resp.get("policies", [])
|
||||
|
||||
required_policies = ["t0-observer", "t1-operator", "t2-builder", "t3-executor", "t4-architect"]
|
||||
missing = [p for p in required_policies if p not in policies]
|
||||
|
||||
if missing:
|
||||
log(f"Missing policies: {missing}", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("All tier policies loaded", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 2. Verify policy content for t0-observer (read-only)
|
||||
resp = vault_request("GET", "sys/policy/t0-observer", root_token)
|
||||
policy_content = resp.get("data", {}).get("rules", "")
|
||||
|
||||
# T0 should have read/list but NOT create/update/delete
|
||||
has_read = "read" in policy_content.lower()
|
||||
has_write = any(w in policy_content.lower() for w in ["create", "delete"])
|
||||
|
||||
if not has_read:
|
||||
log("T0 policy missing read capability", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
if has_write:
|
||||
log("T0 policy has unexpected write capabilities", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("T0 policy has read-only restrictions", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 3. Verify t1-operator has ssh capabilities
|
||||
resp = vault_request("GET", "sys/policy/t1-operator", root_token)
|
||||
policy_content = resp.get("data", {}).get("rules", "")
|
||||
|
||||
if "ssh" not in policy_content.lower():
|
||||
log("T1 policy missing SSH capabilities", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("T1 policy has SSH capabilities", "pass")
|
||||
PASSED += 1
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_secrets_access():
|
||||
"""Test secret path access controls"""
|
||||
global PASSED, FAILED
|
||||
|
||||
print("\n[TEST] secrets_access")
|
||||
root_token = get_root_token()
|
||||
|
||||
# 1. Check secret engine is enabled
|
||||
resp = vault_request("GET", "sys/mounts", root_token)
|
||||
mounts = resp.get("data", {})
|
||||
|
||||
if "secret/" not in mounts:
|
||||
log("Secret KV engine not enabled", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("Secret KV engine enabled", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 2. Write a test secret
|
||||
test_data = {"data": {"test_key": "test_value", "timestamp": "2026-01-24"}}
|
||||
resp = vault_request("POST", "secret/data/test/phase2", root_token, test_data)
|
||||
|
||||
if "errors" in resp:
|
||||
log(f"Failed to write test secret: {resp.get('errors')}", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("Successfully wrote test secret", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 3. Read the test secret back
|
||||
resp = vault_request("GET", "secret/data/test/phase2", root_token)
|
||||
|
||||
if resp.get("data", {}).get("data", {}).get("test_key") != "test_value":
|
||||
log("Failed to read test secret", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("Successfully read test secret", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 4. Check agent secrets path exists
|
||||
resp = vault_request("GET", "secret/data/agents", root_token)
|
||||
# 404 is OK (path may not exist yet), 403 would be a policy issue
|
||||
|
||||
log("Agent secrets path accessible", "pass")
|
||||
PASSED += 1
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_approle_auth():
|
||||
"""Test AppRole authentication"""
|
||||
global PASSED, FAILED
|
||||
|
||||
print("\n[TEST] approle_auth")
|
||||
root_token = get_root_token()
|
||||
|
||||
# 1. Check AppRole auth is enabled
|
||||
resp = vault_request("GET", "sys/auth", root_token)
|
||||
auth_methods = resp.get("data", {})
|
||||
|
||||
if "approle/" not in auth_methods:
|
||||
log("AppRole auth not enabled", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("AppRole auth enabled", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 2. Check tier1-agent role exists
|
||||
resp = vault_request("GET", "auth/approle/role/tier1-agent", root_token)
|
||||
|
||||
if "errors" in resp:
|
||||
log("tier1-agent role not found", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("tier1-agent role exists", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 3. Get role-id
|
||||
resp = vault_request("GET", "auth/approle/role/tier1-agent/role-id", root_token)
|
||||
role_id = resp.get("data", {}).get("role_id")
|
||||
|
||||
if not role_id:
|
||||
log("Failed to get role-id", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log(f"Got role-id: {role_id[:8]}...", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 4. Generate secret-id
|
||||
resp = vault_request("POST", "auth/approle/role/tier1-agent/secret-id", root_token)
|
||||
secret_id = resp.get("data", {}).get("secret_id")
|
||||
|
||||
if not secret_id:
|
||||
log("Failed to generate secret-id", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("Generated secret-id", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 5. Login with AppRole
|
||||
login_data = {"role_id": role_id, "secret_id": secret_id}
|
||||
resp = vault_request("POST", "auth/approle/login", root_token, login_data)
|
||||
|
||||
client_token = resp.get("auth", {}).get("client_token")
|
||||
policies = resp.get("auth", {}).get("policies", [])
|
||||
|
||||
if not client_token:
|
||||
log("AppRole login failed", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("AppRole login successful", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 6. Verify policies attached
|
||||
if "t1-operator" not in policies:
|
||||
log(f"Missing t1-operator policy, got: {policies}", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log(f"Correct policies attached: {policies}", "pass")
|
||||
PASSED += 1
|
||||
|
||||
# 7. Test the new token can read allowed paths
|
||||
resp = vault_request("GET", "secret/data/inventory/proxmox", client_token)
|
||||
# 200 or 404 is fine (path may not exist), 403 would be policy issue
|
||||
|
||||
if resp.get("errors") and "permission denied" in str(resp.get("errors")).lower():
|
||||
log("New token cannot read allowed paths", "fail")
|
||||
FAILED += 1
|
||||
return False
|
||||
|
||||
log("New token has correct permissions", "pass")
|
||||
PASSED += 1
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all Phase 2 tests"""
|
||||
global PASSED, FAILED
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("PHASE 2: VAULT POLICY ENGINE TESTS")
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
test_policy_enforcement()
|
||||
test_secrets_access()
|
||||
test_approle_auth()
|
||||
except Exception as e:
|
||||
print(f"\n\033[91mTest execution error: {e}\033[0m")
|
||||
FAILED += 1
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"RESULTS: {PASSED} passed, {FAILED} failed")
|
||||
print("=" * 60 + "\n")
|
||||
|
||||
return FAILED == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = main()
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test: policy_enforcement
|
||||
Phase 2: Vault Policy Engine
|
||||
"""
|
||||
from test_phase2_vault import test_policy_enforcement
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_policy_enforcement()
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test: secrets_access
|
||||
Phase 2: Vault Policy Engine
|
||||
"""
|
||||
from test_phase2_vault import test_secrets_access
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_secrets_access()
|
||||
Reference in New Issue
Block a user