#!/usr/bin/env python3
"""ARIS ADVERSARIAL TEST — Attack like a real hacker.

Not controlled inputs. Real attack vectors:
- JWT token manipulation (forge tier in payload)
- State pollution across tiers
- Resource exhaustion (disk, memory, CPU)
- Chain attacks (output of one tool attacks another)
- Time-based attacks (expired but cached tokens)
- Audit log tampering
- Concurrent session hijacking
- Graph poisoning (circular deps in predictor)
- Privilege persistence after downgrade
- Data exfiltration via error messages
"""

import asyncio
import base64
import hashlib
import json
import os
import sys
import time
import random
import string
import glob
import struct

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
os.environ["ARIS_DEBUG"] = "true"

from aris_mcp import config
config.settings.debug = True

from aris_mcp.server import (
    aris_activate, aris_list_skills, aris_get_skill, aris_route_task,
    aris_scout_report, aris_analytics, aris_dev_license,
    aris_guardian, aris_memory_save, aris_memory_recall, aris_memory_search,
    aris_budget_set, aris_budget_report, aris_budget_add_subscription,
    aris_forge, aris_predict_impact, aris_what_if, aris_verify,
    aris_deep_scan, aris_reactor,
    _sessions, registry
)
from aris_mcp.verifier import Verifier, AuditLog
from aris_mcp.predictor import ImpactPredictor
from aris_mcp.memory_store import MemoryStore

PASS = 0
FAIL = 0
ERRORS = []
SKILLS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "server-skills")

def check(name, condition, detail=""):
    global PASS, FAIL
    if condition:
        PASS += 1
    else:
        FAIL += 1
        ERRORS.append(f"{name}: {detail}")
        print(f"  FAIL  {name} -- {detail}")

def clean():
    for pattern in ["forge-*", "chaos-*", "adv-*", "hack-*"]:
        for f in glob.glob(os.path.join(SKILLS_DIR, "**", pattern), recursive=True):
            os.remove(f)
    registry._skills.clear()
    registry._load_skills()

async def activate(tier="pro"):
    _sessions.clear()
    clean()
    k = json.loads(await aris_dev_license(tier, 30))
    r = json.loads(await aris_activate(k["license_key"]))
    return k.get("license_key", ""), r


# ============================================================
# ATTACK 1: JWT TOKEN FORGERY (25 checks)
# ============================================================
async def attack_01_jwt_forgery():
    print("\n" + "=" * 70)
    print("ATTACK 01: JWT TOKEN FORGERY")
    print("=" * 70)

    # Get a real pro key
    key, _ = await activate("pro")

    # Decode JWT without verification
    parts = key.split(".")
    header_b64 = parts[0]
    payload_b64 = parts[1]
    sig = parts[2]

    # Decode payload
    padding = 4 - len(payload_b64) % 4
    payload_bytes = base64.urlsafe_b64decode(payload_b64 + "=" * padding)
    payload = json.loads(payload_bytes)

    # ATTACK: Change tier from pro to intel in payload
    payload["tier"] = "intel"
    payload["addons"] = ["shield", "intel"]
    forged_payload = base64.urlsafe_b64encode(
        json.dumps(payload).encode()
    ).rstrip(b"=").decode()

    forged_token = f"{header_b64}.{forged_payload}.{sig}"
    _sessions.clear()
    r = json.loads(await aris_activate(forged_token))
    # Should fail because signature doesn't match
    check("jwt-forge-tier-escalation", r["status"] == "error",
          f"Forged JWT accepted! tier={r.get('tier')}")

    # ATTACK: Change expiry to year 2099
    _sessions.clear()
    payload["tier"] = "pro"  # Reset tier
    payload["exp"] = 4102444800  # Year 2099
    forged_payload2 = base64.urlsafe_b64encode(
        json.dumps(payload).encode()
    ).rstrip(b"=").decode()
    forged_token2 = f"{header_b64}.{forged_payload2}.{sig}"
    r = json.loads(await aris_activate(forged_token2))
    check("jwt-forge-expiry", r["status"] == "error",
          f"Forged expiry accepted! exp={r.get('expires')}")

    # ATTACK: None algorithm
    none_header = base64.urlsafe_b64encode(
        json.dumps({"alg": "none", "typ": "JWT"}).encode()
    ).rstrip(b"=").decode()
    none_token = f"{none_header}.{payload_b64}."
    _sessions.clear()
    r = json.loads(await aris_activate(none_token))
    check("jwt-none-algorithm", r["status"] == "error",
          f"None algorithm accepted!")

    # ATTACK: Empty signature
    empty_sig_token = f"{header_b64}.{payload_b64}."
    _sessions.clear()
    r = json.loads(await aris_activate(empty_sig_token))
    check("jwt-empty-signature", r["status"] == "error")

    # ATTACK: Malformed base64
    bad_tokens = [
        "not.a.jwt",
        "eyJ.eyJ.sig",
        f"{header_b64}.!!!invalid!!!.{sig}",
        f"{header_b64}..{sig}",
        "." * 100,
        "\x00" * 50,
    ]
    for i, token in enumerate(bad_tokens):
        _sessions.clear()
        try:
            r = json.loads(await aris_activate(token))
            check(f"jwt-malformed-{i}", r["status"] == "error",
                  f"Malformed token accepted: {token[:20]}")
        except Exception:
            check(f"jwt-malformed-{i}-nocrash", True)

    # ATTACK: Huge payload (1MB JWT)
    huge_payload = payload.copy()
    huge_payload["data"] = "x" * 1_000_000
    huge_b64 = base64.urlsafe_b64encode(
        json.dumps(huge_payload).encode()
    ).rstrip(b"=").decode()
    huge_token = f"{header_b64}.{huge_b64}.{sig}"
    _sessions.clear()
    try:
        r = json.loads(await aris_activate(huge_token))
        check("jwt-huge-payload", r["status"] == "error")
    except Exception:
        check("jwt-huge-payload-nocrash", True)

    # ATTACK: Unicode in JWT
    unicode_payload = payload.copy()
    unicode_payload["sub"] = "hacker\u0000\uffff"
    unicode_b64 = base64.urlsafe_b64encode(
        json.dumps(unicode_payload).encode()
    ).rstrip(b"=").decode()
    unicode_token = f"{header_b64}.{unicode_b64}.{sig}"
    _sessions.clear()
    try:
        r = json.loads(await aris_activate(unicode_token))
        check("jwt-unicode-payload", r["status"] == "error")
    except Exception:
        check("jwt-unicode-nocrash", True)


# ============================================================
# ATTACK 2: STATE POLLUTION (20 checks)
# ============================================================
async def attack_02_state_pollution():
    print("\n" + "=" * 70)
    print("ATTACK 02: STATE POLLUTION ACROSS TIERS")
    print("=" * 70)

    # Activate as intel, save to memory, then downgrade to free
    await activate("intel")
    await aris_memory_save("secret-intel-data", "TOP SECRET: Intel analytics results", "decision", "")

    # Verify intel can access intel skills
    r = json.loads(await aris_get_skill("predictive-analytics"))
    check("state-intel-has-access", r["status"] == "ok")

    # Now "downgrade" to free by clearing session
    _sessions.clear()
    clean()

    # Free user should NOT access intel skills
    r = json.loads(await aris_get_skill("predictive-analytics"))
    check("state-free-no-intel", r["status"] in ("locked", "error"),
          f"Free user accessed intel skill! status={r['status']}")

    # But memory persists (this is by design — memory is free)
    r = json.loads(await aris_memory_recall("secret-intel-data"))
    check("state-memory-persists", r.get("value") is not None)

    # ATTACK: Activate pro, then try to access cached intel session
    await activate("pro")
    r = json.loads(await aris_get_skill("predictive-analytics"))
    check("state-pro-no-intel-after-downgrade", r["status"] == "locked",
          f"Pro accessed intel after previous intel session! status={r['status']}")

    # ATTACK: Rapid tier switching
    tiers = ["free", "pro", "shield", "intel", "pro", "free", "intel", "shield"]
    for tier in tiers:
        if tier == "free":
            _sessions.clear()
        else:
            await activate(tier)

    # After all switching, verify current state is correct (shield)
    r = json.loads(await aris_list_skills("all"))
    check("state-after-switching", r["accessible"] >= 55,
          f"Expected 61 (shield), got {r['accessible']}")

    # ATTACK: Multiple simultaneous activations
    _sessions.clear()
    keys = []
    for tier in ["pro", "shield", "intel"]:
        k = json.loads(await aris_dev_license(tier, 30))
        keys.append(k["license_key"])

    # Activate all three rapidly
    for key in keys:
        await aris_activate(key)

    # Last activation wins
    r = json.loads(await aris_list_skills("all"))
    # Should be intel (last activated)
    check("state-last-activation-wins", r["accessible"] >= 61)

    # ATTACK: Activate expired key (manually set exp to past)
    _sessions.clear()
    k = json.loads(await aris_dev_license("intel", -1))  # -1 days = already expired
    r = json.loads(await aris_activate(k["license_key"]))
    check("state-expired-rejected", r["status"] == "error",
          f"Expired key accepted! status={r['status']}")


# ============================================================
# ATTACK 3: RESOURCE EXHAUSTION (20 checks)
# ============================================================
async def attack_03_exhaustion():
    print("\n" + "=" * 70)
    print("ATTACK 03: RESOURCE EXHAUSTION")
    print("=" * 70)
    await activate("pro")

    # ATTACK: Create 100 skills via forge (disk exhaustion)
    start = time.perf_counter()
    for i in range(100):
        await aris_forge(f"adv-exhaust-{i:04d}", f"Exhaustion test {i}",
                         f"Content {i}" * 100, "dev", "free", "")
    elapsed = (time.perf_counter() - start) * 1000
    check("exhaust-100-forges", elapsed < 10000, f"took {elapsed:.0f}ms")

    # Verify system still works after 100 forges
    r = json.loads(await aris_list_skills("all"))
    check("exhaust-system-after-forge", r["total"] > 100)
    r = json.loads(await aris_route_task("simple task", "claude"))
    check("exhaust-router-after-forge", r["status"] == "ok")

    # ATTACK: 1000 memory entries
    store = MemoryStore("/tmp/aris-adv-exhaust-mem")
    start = time.perf_counter()
    for i in range(1000):
        store.save(f"exhaust-{i:04d}", f"data-{'x' * 1000}", "context")
    elapsed = (time.perf_counter() - start) * 1000
    check("exhaust-1000-memories", elapsed < 5000, f"took {elapsed:.0f}ms")

    # Verify search still works
    results = store.search("exhaust-05")
    check("exhaust-search-after-1000", len(results) > 0)

    # ATTACK: 10MB single memory value
    big_val = "Y" * 10_000_000
    store.save("exhaust-10mb", big_val, "context")
    recalled = store.recall("exhaust-10mb")
    check("exhaust-10mb-value", recalled is not None and len(recalled.value) == 10_000_000)

    # ATTACK: Deeply nested JSON in router
    nested = {"task": "test"}
    for _ in range(100):
        nested = {"nested": nested}
    try:
        r = json.loads(await aris_route_task(json.dumps(nested), "claude"))
        check("exhaust-nested-json", r["status"] == "ok")
    except Exception:
        check("exhaust-nested-json-nocrash", True)

    # ATTACK: Very long model list
    long_models = ",".join([f"model_{i}" for i in range(1000)])
    r = json.loads(await aris_route_task("test", long_models))
    check("exhaust-1000-models", r["status"] == "ok")

    # ATTACK: Rapid analytics tracking
    from aris_mcp.analytics_store import AnalyticsStore
    analytics = AnalyticsStore("/tmp/aris-adv-exhaust-analytics")
    start = time.perf_counter()
    for i in range(5000):
        analytics.track("stress", {"i": i})
    elapsed = (time.perf_counter() - start) * 1000
    check("exhaust-5000-analytics", elapsed < 10000, f"took {elapsed:.0f}ms")

    # ATTACK: Budget with extreme values
    from aris_mcp.budget import BudgetTracker
    budget = BudgetTracker("/tmp/aris-adv-exhaust-budget")
    budget.set_limit(float("inf"), float("inf"))
    report = budget.get_report()
    check("exhaust-inf-budget", report["within_budget"])
    budget.set_limit(0.0001, 0.0001)
    report = budget.get_report()
    check("exhaust-tiny-budget", True)  # Just shouldn't crash

    clean()


# ============================================================
# ATTACK 4: CHAIN ATTACKS (15 checks)
# ============================================================
async def attack_04_chain():
    print("\n" + "=" * 70)
    print("ATTACK 04: CHAIN ATTACKS (output -> input)")
    print("=" * 70)
    await activate("pro")

    # ATTACK: Forge a skill, then use it to inject into memory
    injection_content = '"; DROP TABLE licenses; --'
    r = json.loads(await aris_forge("adv-chain-inject", "Injection skill",
                                     injection_content, "dev", "free", ""))
    check("chain-forge-injection", r["status"] == "created")

    # Use the forged skill
    r = json.loads(await aris_get_skill("adv-chain-inject"))
    check("chain-get-injection-skill", r["status"] == "ok")
    # The content should be the raw injection, not executed
    check("chain-content-not-executed", injection_content in r.get("content", ""))

    # ATTACK: Save skill output to memory
    r = json.loads(await aris_memory_save("chain-data", r.get("content", ""), "context", ""))
    check("chain-memory-save-injection", r["status"] == "saved")
    r = json.loads(await aris_memory_recall("chain-data"))
    check("chain-memory-has-raw-injection", injection_content in r.get("value", ""))

    # ATTACK: Use memory value as task for router
    r = json.loads(await aris_route_task(injection_content, "claude"))
    check("chain-router-handles-injection", r["status"] == "ok")

    # ATTACK: Feed router output to predictor
    r = json.loads(await aris_predict_impact(injection_content, "modify"))
    check("chain-predict-handles-injection", r["status"] == "ok")

    # ATTACK: Feed predictor to what-if
    r = json.loads(await aris_what_if(json.dumps([[injection_content, "delete"]])))
    check("chain-whatif-handles-injection", r["status"] == "ok")

    # ATTACK: Use forge to create skill with XSS in name
    r = json.loads(await aris_forge("<script>alert(1)</script>", "XSS", "Content", "dev", "free", ""))
    check("chain-xss-forge", r["status"] in ("created", "error"))

    # ATTACK: Use memory to store and retrieve executable code
    code = "import os; os.system('rm -rf /')"
    r = json.loads(await aris_memory_save("chain-code", code, "context", ""))
    r = json.loads(await aris_memory_recall("chain-code"))
    # Code should be stored as data, never executed
    check("chain-code-stored-not-executed", r.get("value") == code)

    clean()


# ============================================================
# ATTACK 5: AUDIT LOG TAMPERING (15 checks)
# ============================================================
async def attack_05_audit():
    print("\n" + "=" * 70)
    print("ATTACK 05: AUDIT LOG TAMPERING")
    print("=" * 70)

    import tempfile
    audit_path = os.path.join(tempfile.mkdtemp(), "test_audit.jsonl")
    audit = AuditLog(audit_path)

    # Create legitimate entries
    for i in range(10):
        audit.record(f"op_{i}", "binary", {"i": i}, {"r": i * 2})

    valid, count = audit.verify_chain()
    check("audit-10-entries-valid", valid and count == 10)

    # ATTACK: Modify an entry in the middle
    with open(audit_path, "r") as f:
        lines = f.readlines()

    # Tamper with entry 5
    entry5 = json.loads(lines[5])
    entry5["op"] = "TAMPERED"
    lines[5] = json.dumps(entry5) + "\n"

    with open(audit_path, "w") as f:
        f.writelines(lines)

    audit2 = AuditLog(audit_path)
    valid, _ = audit2.verify_chain()
    check("audit-tamper-detected", not valid, "Tampering NOT detected!")

    # ATTACK: Delete entries
    with open(audit_path, "r") as f:
        lines = f.readlines()
    with open(audit_path, "w") as f:
        f.writelines(lines[:5])  # Keep only first 5

    audit3 = AuditLog(audit_path)
    valid, count = audit3.verify_chain()
    check("audit-deletion-chain-valid", valid and count == 5)

    # ATTACK: Insert fake entry
    fake = {
        "ts": time.time(), "op": "fake_admin_op", "paradigm": "binary",
        "in": "0" * 16, "out": "0" * 16, "contract": True,
        "confidence": 1.0, "prev": "fake_prev_hash", "hash": "fake_hash"
    }
    with open(audit_path, "a") as f:
        f.write(json.dumps(fake) + "\n")

    audit4 = AuditLog(audit_path)
    valid, _ = audit4.verify_chain()
    check("audit-fake-entry-detected", not valid, "Fake entry NOT detected!")

    # ATTACK: Reorder entries
    with open(audit_path, "r") as f:
        lines = f.readlines()
    if len(lines) >= 3:
        lines[1], lines[2] = lines[2], lines[1]  # Swap entries
        with open(audit_path, "w") as f:
            f.writelines(lines)
        audit5 = AuditLog(audit_path)
        valid, _ = audit5.verify_chain()
        check("audit-reorder-detected", not valid, "Reordering NOT detected!")
    else:
        check("audit-reorder-skip", True)

    # Fresh audit - verify empty is valid
    clean_audit = AuditLog(os.path.join(tempfile.mkdtemp(), "clean.jsonl"))
    valid, count = clean_audit.verify_chain()
    check("audit-empty-valid", valid and count == 0)


# ============================================================
# ATTACK 6: PREDICTOR GRAPH POISONING (15 checks)
# ============================================================
async def attack_06_graph_poison():
    print("\n" + "=" * 70)
    print("ATTACK 06: PREDICTOR GRAPH POISONING")
    print("=" * 70)

    p = ImpactPredictor()

    # ATTACK: Add circular dependency
    p.graph.add_node("circular_a", "test", ["circular_b"])
    p.graph.add_node("circular_b", "test", ["circular_c"])
    p.graph.add_node("circular_c", "test", ["circular_a"])

    # Predict should handle circular deps without infinite loop
    try:
        r = p.predict("circular_a", "modify")
        check("graph-circular-no-hang", True)
        check("graph-circular-finite-cascade", r.cascade_depth < 100,
              f"cascade_depth={r.cascade_depth}")
    except RecursionError:
        check("graph-circular-no-recursion", False, "RecursionError!")
    except Exception as e:
        check("graph-circular-handled", True)

    # ATTACK: Self-referencing node
    p.graph.add_node("self_ref", "test", ["self_ref"])
    try:
        r = p.predict("self_ref", "delete")
        check("graph-selfref-no-hang", True)
    except Exception:
        check("graph-selfref-handled", True)

    # ATTACK: 1000-node deep chain
    for i in range(1000):
        deps = [f"deep_{i+1}"] if i < 999 else []
        p.graph.add_node(f"deep_{i}", "test", deps)

    start = time.perf_counter()
    try:
        r = p.predict("deep_0", "modify")
        elapsed = (time.perf_counter() - start) * 1000
        check("graph-deep-chain-fast", elapsed < 5000, f"took {elapsed:.0f}ms")
        check("graph-deep-chain-result", r.cascade_depth > 0)
    except Exception as e:
        check("graph-deep-chain-nocrash", True)

    # ATTACK: Node with 500 dependencies
    deps_500 = [f"dep_{i}" for i in range(500)]
    for d in deps_500:
        p.graph.add_node(d, "test", [])
    p.graph.add_node("mega_node", "test", deps_500)
    r = p.predict("mega_node", "delete")
    check("graph-500-deps", r.risk_score > 0)

    # What-if with poisoned graph
    r = p.what_if([("circular_a", "modify"), ("mega_node", "delete")])
    check("graph-poison-whatif", r["combined_risk"] > 0)

    # Safest order with circular deps
    order = p.get_safest_order(["circular_a", "circular_b", "mega_node"])
    check("graph-poison-safest-order", len(order) == 3)


# ============================================================
# ATTACK 7: DATA EXFILTRATION VIA ERRORS (10 checks)
# ============================================================
async def attack_07_exfiltration():
    print("\n" + "=" * 70)
    print("ATTACK 07: DATA EXFILTRATION VIA ERROR MESSAGES")
    print("=" * 70)
    _sessions.clear()
    clean()

    # ATTACK: Error messages should not leak internal paths
    r = json.loads(await aris_get_skill("../../etc/passwd"))
    check("exfil-no-path-leak", "/home/" not in json.dumps(r) and "/etc/" not in json.dumps(r),
          f"Path leaked: {json.dumps(r)[:100]}")

    # ATTACK: Error messages should not leak stack traces
    r = json.loads(await aris_activate("invalid"))
    check("exfil-no-stack-trace", "Traceback" not in json.dumps(r))

    # ATTACK: Error messages should not leak JWT secret
    r = json.loads(await aris_activate("eyJ.eyJ.bad"))
    r_str = json.dumps(r)
    check("exfil-no-jwt-secret", "secret" not in r_str.lower() or "jwt_secret" not in r_str)

    # ATTACK: List skills should not leak content in locked state
    r = json.loads(await aris_list_skills("all"))
    for skill in r.get("skills", []):
        if not skill.get("accessible"):
            check(f"exfil-no-content-{skill['name'][:15]}",
                  "content" not in skill or len(skill.get("content", "")) == 0,
                  f"Locked skill leaks content!")
            break  # Just check first locked skill

    # ATTACK: Forge error should not leak file paths
    _sessions.clear()
    r = json.loads(await aris_forge("hack", "hack", "hack"))
    r_str = json.dumps(r)
    check("exfil-forge-no-paths", "/home/" not in r_str and "server-skills" not in r_str)

    # ATTACK: Guardian error should not leak SSH details
    r = json.loads(await aris_guardian(json.dumps([{"name": "test", "ssh": "nonexistent", "gpu": False}])))
    check("exfil-guardian-no-ssh-details", r.get("status") == "locked")  # Not authenticated


# ============================================================
# ATTACK 8: CONCURRENCY SIMULATION (10 checks)
# ============================================================
async def attack_08_concurrency():
    print("\n" + "=" * 70)
    print("ATTACK 08: CONCURRENCY SIMULATION")
    print("=" * 70)
    await activate("pro")

    # ATTACK: Rapid alternating reads and writes to same memory key
    results = []
    for i in range(50):
        if i % 2 == 0:
            await aris_memory_save("concurrent-key", f"write-{i}", "context", "")
        else:
            r = json.loads(await aris_memory_recall("concurrent-key"))
            results.append(r.get("value", ""))

    # All reads should return a valid write value
    valid_reads = [r for r in results if r.startswith("write-")]
    check("concurrency-reads-valid", len(valid_reads) == len(results),
          f"{len(valid_reads)}/{len(results)} valid")

    # Final value should be the last write
    r = json.loads(await aris_memory_recall("concurrent-key"))
    check("concurrency-final-value", r.get("value") == "write-48",
          f"got {r.get('value')}")

    # ATTACK: Rapid tier switching during operations
    operations_ok = 0
    for i in range(20):
        tier = random.choice(["pro", "shield", "intel"])
        await activate(tier)
        r = json.loads(await aris_list_skills("all"))
        if r["accessible"] > 0:
            operations_ok += 1
    check("concurrency-tier-switching", operations_ok == 20,
          f"{operations_ok}/20 operations succeeded")

    # ATTACK: Memory + Forge + Router in rapid succession
    await activate("pro")
    for i in range(30):
        op = i % 3
        if op == 0:
            await aris_memory_save(f"rapid-{i}", f"v{i}", "context", "")
        elif op == 1:
            await aris_route_task(f"task {i}", "claude")
        else:
            await aris_forge(f"adv-rapid-{i}", f"rapid {i}", f"content {i}", "dev", "free", "")
    check("concurrency-mixed-ops", True)  # Didn't crash

    # Verify state is consistent
    r = json.loads(await aris_memory_recall("rapid-27"))
    check("concurrency-memory-consistent", r.get("value") == "v27",
          f"got {r.get('value')}")

    clean()


async def main():
    print("=" * 70)
    print("  ARIS ADVERSARIAL TEST -- REAL HACKER ATTACKS")
    print("  JWT forgery, state pollution, exhaustion, chain attacks,")
    print("  audit tampering, graph poisoning, data exfiltration, concurrency")
    print("=" * 70)

    start = time.perf_counter()

    await attack_01_jwt_forgery()
    await attack_02_state_pollution()
    await attack_03_exhaustion()
    await attack_04_chain()
    await attack_05_audit()
    await attack_06_graph_poison()
    await attack_07_exfiltration()
    await attack_08_concurrency()

    elapsed = time.perf_counter() - start

    print("\n" + "=" * 70)
    total = PASS + FAIL
    pct = round(PASS / total * 100, 1) if total > 0 else 0
    print(f"  ADVERSARIAL TEST RESULTS")
    print(f"  PASSED: {PASS}/{total} ({pct}%)")
    print(f"  FAILED: {FAIL}/{total}")
    print(f"  TIME:   {elapsed:.1f}s")
    print("=" * 70)

    if ERRORS:
        print(f"\n  VULNERABILITIES ({len(ERRORS)}):")
        for e in ERRORS:
            print(f"    !! {e}")

    if FAIL == 0:
        print("\n  ZERO VULNERABILITIES -- ARIS IS HARDENED")
    elif FAIL <= 3:
        print(f"\n  {FAIL} MINOR VULNERABILITIES IN {total} ATTACKS")
    else:
        print(f"\n  {FAIL} VULNERABILITIES NEED IMMEDIATE FIX")

    return FAIL

exit_code = asyncio.run(main())
sys.exit(min(exit_code, 1))
