admin / Syanpse-Vanguard
public
Syanpse-Vanguard / synapse-vanguard-v3 / scripts / smoke.py
6011 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | """End-to-end smoke test with no external services (memory broker + DuckDB). Exercises the real code paths for Milestones 1-3: 1. storage: schema init + bulk write + parameterized search 2. ingest: agent enrollment (dev bearer) + broker push 3. pipeline: normalizer parse + durable write + ack 4. query: RBAC-guarded, tenant-isolated search 5. rbac: role matrix enforcement Run: python scripts/smoke.py """ from __future__ import annotations import asyncio import json import os import tempfile # Configure a self-contained environment BEFORE importing sv.* _tmp = tempfile.mkdtemp(prefix="sv-smoke-") os.environ.update( SV_STORAGE_BACKEND="duckdb", SV_DUCKDB_PATH=os.path.join(_tmp, "smoke.duckdb"), SV_COLD_DIR=os.path.join(_tmp, "cold"), SV_BROKER="memory", SV_MTLS_ENABLED="false", SV_VAULT_KEY="11" * 32, SV_JWT_SECRET="smoke-secret", SV_ENROLLMENT_TOKEN="smoke-enroll", SV_BOOTSTRAP_ADMIN_USER="admin", SV_BOOTSTRAP_ADMIN_PASSWORD="admin-pw", ) from sv.auth.rbac import Permission, Role, can # noqa: E402 from sv.auth.session import issue_token # noqa: E402 from sv.auth.users import get_user_by_username, verify_password # noqa: E402 from sv.bootstrap import shutdown, startup # noqa: E402 from sv.ingest import registry # noqa: E402 from sv.models.agent import AgentRegisterRequest # noqa: E402 from sv.pipeline.normalizer import _parse_lines # noqa: E402 from sv.query.builder import SearchRequest, build_search # noqa: E402 from sv.config import DEFAULT_TENANT_ID # noqa: E402 PASS, FAIL = "\033[92mPASS\033[0m", "\033[91mFAIL\033[0m" _ok = True def check(name: str, cond: bool) -> None: global _ok _ok = _ok and cond print(f" [{PASS if cond else FAIL}] {name}") async def main() -> int: engine, broker = await startup(with_broker=True) try: # 1. bootstrap admin seeded admin = await get_user_by_username(engine, "admin") check("bootstrap admin seeded", admin is not None) check("admin password verifies", verify_password("admin-pw", admin["password_hash"])) check("admin is TENANT_ADMIN", admin["role"] == "TENANT_ADMIN") # 2. RBAC matrix check("auditor can read logs", can(Role.READ_ONLY_VIEWER, Permission.LOG_READ)) check("auditor CANNOT triage", not can(Role.READ_ONLY_VIEWER, Permission.ALERT_TRIAGE)) check("analyst can triage", can(Role.SECURITY_ANALYST, Permission.ALERT_TRIAGE)) check("analyst CANNOT manage users", not can(Role.SECURITY_ANALYST, Permission.USER_MANAGE)) check("admin can manage agents", can(Role.TENANT_ADMIN, Permission.AGENT_MANAGE)) # 3. JWT round-trips tok = issue_token(admin["id"], "admin", Role.TENANT_ADMIN, DEFAULT_TENANT_ID) check("jwt issued", isinstance(tok, str) and tok.count(".") == 2) # 4. agent enrollment (dev bearer) req = AgentRegisterRequest(hostname="testhost", platform="linux", agent_version="0.1.0", channels=["journald"]) agent_id, token = await registry.enroll(engine, req, cert_cn=None) check("agent enrolled with dev token", token is not None) found = await registry.get_agent(engine, agent_id) check("agent retrievable", found is not None and not found["revoked"]) # 5. ingest -> broker -> normalizer -> storage lines = [ json.dumps({"source_type": "winlog", "host": "testhost", "message": "An account was successfully logged on", "fields": {"EventID": 4624, "Channel": "Security", "Level": 4, "TargetUserName": "alice", "Keywords": "Audit Success"}}), json.dumps({"source_type": "journald", "host": "testhost", "message": "sshd accepted password for bob", "fields": {"PRIORITY": "6", "_COMM": "sshd"}}), "{ this is not valid json }", # must be tolerated & dropped ] await broker.push_raw(agent_id, seq=2, payload="\n".join(lines).encode()) msgs = await broker.read_group("smoke", count=10, block_ms=500) check("broker delivered 1 message", len(msgs) == 1) events = [] for m in msgs: events.extend(_parse_lines(m.payload)) await broker.ack(m.msg_id) check("2 valid events parsed (1 bad dropped)", len(events) == 2) written = await engine.write_batch(events) check("2 events written to storage", written == 2) # 6. normalization correctness win = next(e for e in events if e.source_type == "winlog") check("winlog EventID parsed", win.event_id == 4624) check("winlog outcome=success", win.outcome == "success") check("winlog user parsed", win.user_name == "alice") # 7. RBAC- & tenant-scoped search q = build_search(SearchRequest(equals={"source_type": "winlog"}, limit=10), tenant_id=DEFAULT_TENANT_ID) table = await engine.execute(q) check("search returns the winlog event", table.num_rows == 1) check("tenant filter present in SQL", "tenant_id = ?" in q.text) # 8. filter allowlist rejects unknown fields (injection guard) try: build_search(SearchRequest(equals={"evil; DROP TABLE events": 1}), tenant_id=DEFAULT_TENANT_ID) check("unknown filter rejected", False) except ValueError: check("unknown filter rejected", True) finally: await shutdown(engine, broker) print() print("RESULT:", "ALL PASS" if _ok else "FAILURES PRESENT") return 0 if _ok else 1 if __name__ == "__main__": raise SystemExit(asyncio.run(main())) |