Closes #1144. Builds a fleet audit pipeline that detects duplicate agent identities, ghost accounts, and authorship ambiguity across all machines. Deliverables: bin/fleet_audit.py — Full audit tool with four checks: - Identity registry validation (one name per machine, unique gitea_user) - Git authorship audit (detects ambiguous committers from branch names) - Gitea org member audit (finds ghost accounts with zero activity) - Cross-reference registry vs fleet-routing.json (orphan/location mismatch) fleet/identity-registry.yaml — Canonical identity registry: - 8 active agents (timmy, allegro, ezra, bezalel, bilbobagginshire, fenrir, substratum, claw-code) - 7 ghost/deprecated accounts marked inactive - Rules: one identity per machine, unique gitea_user, required fields tests/test_fleet_audit.py — 11 tests covering all validation rules. Usage: python3 bin/fleet_audit.py # full audit -> JSON python3 bin/fleet_audit.py --identity-check # registry only python3 bin/fleet_audit.py --git-authors # authorship only python3 bin/fleet_audit.py --report out.json # write to file
144 lines
6.2 KiB
Python
144 lines
6.2 KiB
Python
"""Tests for fleet_audit — Deduplicate Agents, One Identity Per Machine."""
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import yaml
|
|
|
|
# Adjust import path
|
|
import sys
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "bin"))
|
|
|
|
from fleet_audit import (
|
|
AuditFinding,
|
|
validate_registry,
|
|
cross_reference_registry_agents,
|
|
audit_git_authors,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Identity registry validation tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestValidateRegistry:
|
|
"""Test identity registry validation rules."""
|
|
|
|
def _make_registry(self, agents):
|
|
return {"version": 1, "agents": agents, "rules": {"one_identity_per_machine": True}}
|
|
|
|
def test_clean_registry_passes(self):
|
|
registry = self._make_registry([
|
|
{"name": "allegro", "machine": "167.99.126.228", "role": "burn", "gitea_user": "allegro"},
|
|
{"name": "ezra", "machine": "143.198.27.163", "role": "triage", "gitea_user": "ezra"},
|
|
])
|
|
findings = validate_registry(registry)
|
|
critical = [f for f in findings if f.severity == "critical"]
|
|
assert len(critical) == 0
|
|
|
|
def test_same_name_on_different_machines_detected(self):
|
|
registry = self._make_registry([
|
|
{"name": "allegro", "machine": "167.99.126.228", "role": "burn"},
|
|
{"name": "allegro", "machine": "104.131.15.18", "role": "burn"},
|
|
])
|
|
findings = validate_registry(registry)
|
|
critical = [f for f in findings if f.severity == "critical" and f.category == "duplicate"]
|
|
# Two findings: one for name-on-multiple-machines, one for duplicate name
|
|
assert len(critical) >= 1
|
|
machine_findings = [f for f in critical if "registered on" in f.description]
|
|
assert len(machine_findings) == 1
|
|
assert "167.99.126.228" in machine_findings[0].description
|
|
assert "104.131.15.18" in machine_findings[0].description
|
|
|
|
def test_multiple_agents_same_machine_ok(self):
|
|
# Multiple different agents on the same VPS is normal.
|
|
registry = self._make_registry([
|
|
{"name": "allegro", "machine": "167.99.126.228", "role": "burn"},
|
|
{"name": "bilbo", "machine": "167.99.126.228", "role": "queries"},
|
|
])
|
|
findings = validate_registry(registry)
|
|
critical = [f for f in findings if f.severity == "critical"]
|
|
assert len(critical) == 0
|
|
|
|
def test_duplicate_name_detected(self):
|
|
registry = self._make_registry([
|
|
{"name": "bezalel", "machine": "104.131.15.18", "role": "ci"},
|
|
{"name": "bezalel", "machine": "167.99.126.228", "role": "ci"},
|
|
])
|
|
findings = validate_registry(registry)
|
|
name_dupes = [f for f in findings if f.severity == "critical" and "bezalel" in f.description.lower() and "registered on" in f.description.lower()]
|
|
assert len(name_dupes) == 1
|
|
|
|
def test_duplicate_gitea_user_detected(self):
|
|
registry = self._make_registry([
|
|
{"name": "agent-a", "machine": "host1", "role": "x", "gitea_user": "shared"},
|
|
{"name": "agent-b", "machine": "host2", "role": "x", "gitea_user": "shared"},
|
|
])
|
|
findings = validate_registry(registry)
|
|
gitea_dupes = [f for f in findings if "Gitea user 'shared'" in f.description]
|
|
assert len(gitea_dupes) == 1
|
|
assert "agent-a" in gitea_dupes[0].affected
|
|
assert "agent-b" in gitea_dupes[0].affected
|
|
|
|
def test_missing_required_fields(self):
|
|
registry = self._make_registry([
|
|
{"name": "incomplete-agent"},
|
|
])
|
|
findings = validate_registry(registry)
|
|
missing = [f for f in findings if f.category == "orphan"]
|
|
assert len(missing) >= 1
|
|
assert "machine" in missing[0].description or "role" in missing[0].description
|
|
|
|
def test_empty_registry_passes(self):
|
|
registry = self._make_registry([])
|
|
findings = validate_registry(registry)
|
|
assert len(findings) == 0
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Cross-reference tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestCrossReference:
|
|
"""Test registry vs fleet-routing.json cross-reference."""
|
|
|
|
def test_orphan_in_fleet_not_registry(self):
|
|
reg_agents = [{"name": "allegro", "machine": "x", "role": "y"}]
|
|
fleet_agents = [{"name": "allegro", "location": "x"}, {"name": "unknown-agent", "location": "y"}]
|
|
findings = cross_reference_registry_agents(reg_agents, fleet_agents)
|
|
orphans = [f for f in findings if f.category == "orphan" and "unknown-agent" in f.description]
|
|
assert len(orphans) == 1
|
|
|
|
def test_location_mismatch_detected(self):
|
|
reg_agents = [{"name": "allegro", "machine": "167.99.126.228", "role": "y"}]
|
|
fleet_agents = [{"name": "allegro", "location": "totally-different-host"}]
|
|
findings = cross_reference_registry_agents(reg_agents, fleet_agents)
|
|
mismatches = [f for f in findings if f.category == "duplicate" and "different locations" in f.description]
|
|
assert len(mismatches) == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Integration test against actual registry
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestRealRegistry:
|
|
"""Test against the actual identity-registry.yaml in the repo."""
|
|
|
|
def test_registry_loads(self):
|
|
reg_path = Path(__file__).resolve().parent.parent / "fleet" / "identity-registry.yaml"
|
|
if reg_path.exists():
|
|
with open(reg_path) as f:
|
|
registry = yaml.safe_load(f)
|
|
assert registry["version"] == 1
|
|
assert len(registry["agents"]) > 0
|
|
|
|
def test_registry_no_critical_findings(self):
|
|
reg_path = Path(__file__).resolve().parent.parent / "fleet" / "identity-registry.yaml"
|
|
if reg_path.exists():
|
|
with open(reg_path) as f:
|
|
registry = yaml.safe_load(f)
|
|
findings = validate_registry(registry)
|
|
critical = [f for f in findings if f.severity == "critical"]
|
|
assert len(critical) == 0, f"Critical findings: {[f.description for f in critical]}"
|