Ctrl K
ComunidadGuías…

test_private_data_guard.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
# ═══════════════════════════════════════════════════════════════════
#   ✦  T L A M A T I N I  ✦   —   "one who knows"
#
#   Created by  Angela López Mendoza   ·   @angelahack1
#   Developer · Architect · Creator of Tlamatini
#
#   Every line of this file was written by Angela López Mendoza.
# ═══════════════════════════════════════════════════════════════════
#   Tlamatini Author Banner — do not remove (releases scrub the name automatically)
"""Automated tests for the PRIVATE DATA GUARD mechanism.

Two mechanisms are verified:

  A. GIT-HISTORY INTEGRITY GUARD
     The promise is: sensitive data is removed ONLY in new forward commits,
     and history / tags / pushes are NEVER rewritten. These tests fail loudly
     if anyone ever rebases, amends, resets, filter-repos, or force-pushes:
       - the ROOT commit SHA is immutable,
       - the commit count only ever grows,
       - every previously-published tag still exists,
       - the forward-only scrub still holds (no private PII in tracked files).

  B. GLOBAL BANNER GUARD
     A SessionStart hook prints the guard IN CAPS for EVERY Claude Code session
     of this user, in any directory on this machine:
       - the banner script exists,
       - ~/.claude/settings.json wires it as a SessionStart hook,
       - running it prints the required CAPS keywords and exits 0.

NO private data is hardcoded in this file. The deep PII scan is DELEGATED to
``check_private_data.py`` (which takes targets at runtime) and is SKIPPED unless
a local, gitignored targets file / env var is present.

Run:  python -m unittest test_private_data_guard -v
"""
from __future__ import annotations

import json
import os
import re
import subprocess
import sys
import unicodedata
import unittest
from pathlib import Path

REPO = Path(__file__).resolve().parent

# --- Git invariants captured when the guard was installed (2026-06-27) --------
# The root (very first) commit. Rewriting history necessarily changes this SHA.
ROOT_COMMIT = "137a0d70e1466fc69b50a8c3ac1740cf5b57c41d"
# History only ever grows; it must never be shorter than this floor.
MIN_COMMITS = 523
# Anchor tags that must never disappear (first, and the privacy-scrub releases).
# NOTE: v1.31.0 was NEVER published — the release sequence skipped that patch
# (v1.30.1 -> v1.31.1), verified absent both locally AND on origin and never in
# the reflog. Removing it is a correction of a phantom entry, NOT a dropped
# pushed tag (which would be forbidden).
REQUIRED_TAGS = ("v1.0.0", "v1.30.1", "v1.31.1")
MIN_TAG_COUNT = 49

# Placeholders the forward-only scrub wrote (positive, PII-free assertions).
PLACEHOLDER_CHECKS = (
    (REPO / "Tlamatini/agent/agents/telegrammer/config.yaml", "@your_telegram_username"),
    (REPO / "Tlamatini/agent/agents/emailer/config.yaml", "you@example.com"),
    # Angela's directive REVERSED the old "hide the maintainer" scrub — her name
    # must be NAMED in the self-knowledge doc, not replaced by a generic phrase.
    # So this sentinel now asserts her authorship is PRESENT, not that it's hidden.
    (REPO / "Tlamatini/agent/Tlamatini.md", "Ángela López Mendoza"),
)

# A structural (NOT personal) shape: Meta/WhatsApp access tokens. Must be absent
# from tracked files. This is a regex for a token format, not anyone's data.
# A token stands ALONE: "EAA..." inside a longer base64 run (an embedded PNG, font or wasm blob) is
# image data that merely contains those letters, so the run must not continue on either side.
META_TOKEN_RE = re.compile(r"(?<![A-Za-z0-9+/=_-])EAA[A-Za-z0-9]{30,}(?![A-Za-z0-9+/=_-])")
# Third-party code vendored under these paths is minified and ships base64 data blobs line by line;
# it is not ours to scrub and holds no token of ours.
TOKEN_SCAN_EXCLUDED_PREFIXES = ("Tlamatini/agent/static/agent/vendor/",)

# The deep scan looks for PRIVATE TARGETS (a person's name, e-mail, phone, handle) in TRACKED files.
# The maintainer is credited BY NAME in this repository on purpose (see PLACEHOLDER_CHECKS above), so
# her own name is never a leak, whatever the local targets file lists.
PUBLIC_MAINTAINER_NAMES = frozenset({"angela lopez mendoza"})

USER_CLAUDE = Path.home() / ".claude"
BANNER_SCRIPT = USER_CLAUDE / "hooks" / "private_data_guard_banner.py"
USER_SETTINGS = USER_CLAUDE / "settings.json"


def _git(*args: str) -> str:
    out = subprocess.run(
        ["git", "-C", str(REPO), *args],
        capture_output=True, text=True, timeout=30,
    )
    if out.returncode != 0:
        raise RuntimeError(f"git {' '.join(args)} failed: {out.stderr.strip()}")
    return out.stdout.strip()


def _tracked_files() -> list[str]:
    return [p for p in _git("ls-files").splitlines() if p]


def _fold(text: str) -> str:
    """Lower-case, accent-free, single-spaced: 'Ángela  López' == 'angela lopez'."""
    decomposed = unicodedata.normalize("NFKD", str(text))
    plain = "".join(ch for ch in decomposed if not unicodedata.combining(ch))
    return " ".join(plain.lower().split())


def _mask(value: str) -> str:
    """Never print a private value, not even in a failing test's message."""
    value = str(value)
    return f"{value[:2]}***{value[-2:]}" if len(value) > 6 else "***"


class GitHistoryIntegrityGuardTests(unittest.TestCase):
    """The history must only ever move FORWARD — never be rewritten."""

    def test_root_commit_is_immutable(self):
        root = _git("rev-list", "--max-parents=0", "HEAD")
        # There may be more than one root in theory; ours is single.
        self.assertIn(
            ROOT_COMMIT, root.split(),
            "ROOT COMMIT CHANGED -> history was rewritten. This is forbidden.",
        )

    def test_history_only_grows(self):
        count = int(_git("rev-list", "--count", "HEAD"))
        self.assertGreaterEqual(
            count, MIN_COMMITS,
            f"Commit count {count} < {MIN_COMMITS} -> commits were dropped "
            "(reset/rebase/filter). Forbidden.",
        )

    def test_all_published_tags_still_exist(self):
        tags = set(_git("tag").splitlines())
        for t in REQUIRED_TAGS:
            self.assertIn(t, tags, f"Tag {t} disappeared -> a pushed tag was deleted. Forbidden.")
        self.assertGreaterEqual(
            len(tags), MIN_TAG_COUNT,
            f"Only {len(tags)} tags (< {MIN_TAG_COUNT}) -> tags were deleted. Forbidden.",
        )

    def test_scrub_holds_placeholders_present(self):
        for path, placeholder in PLACEHOLDER_CHECKS:
            self.assertTrue(path.exists(), f"missing file {path}")
            text = path.read_text(encoding="utf-8", errors="replace")
            self.assertIn(
                placeholder, text,
                f"placeholder {placeholder!r} missing from {path} -> scrub regressed.",
            )

    def test_no_meta_token_shape_in_tracked_files(self):
        offenders = []
        for rel in _tracked_files():
            if rel.startswith(TOKEN_SCAN_EXCLUDED_PREFIXES):
                continue
            p = REPO / rel
            try:
                if p.stat().st_size > 5_000_000:
                    continue
                data = p.read_text(encoding="utf-8", errors="ignore")
            except OSError:
                continue
            if META_TOKEN_RE.search(data):
                offenders.append(rel)
        self.assertEqual([], offenders, f"Meta/WhatsApp token shape in tracked files: {offenders}")

    def test_deep_pii_scan_delegated_to_auditor(self):
        """Run check_private_data.py against a LOCAL targets file if one exists.

        Targets are NEVER stored in the repo. Provide them via the gitignored
        file ``.private_targets.json`` or env ``CHECK_PRIVATE_DATA_TARGETS`` to
        activate this check; otherwise it is skipped (so the committed test
        carries no private data).
        """
        auditor = REPO / "check_private_data.py"
        targets_file = REPO / ".private_targets.json"
        has_env = bool(os.environ.get("CHECK_PRIVATE_DATA_TARGETS"))
        if not auditor.exists():
            self.skipTest("check_private_data.py not present")
        if not (targets_file.exists() or has_env):
            self.skipTest(
                "no local targets (.private_targets.json / env) -> deep scan skipped by design"
            )
        # The report goes under the gitignored Temp/ (never the repo root) and is removed afterwards.
        report_dir = REPO / "Temp"
        report_dir.mkdir(exist_ok=True)
        report_path = report_dir / "private_data_audit_guard.json"
        report_path.unlink(missing_ok=True)
        cmd = [sys.executable, str(auditor), "--local", "--no-llm", "--repo", str(REPO),
               "--output", str(report_path)]
        if targets_file.exists():
            cmd += ["--targets-file", str(targets_file)]
        res = subprocess.run(cmd, capture_output=True, text=True, timeout=600, cwd=str(REPO))
        # The auditor exits 0 = nothing found, 1 = findings, 2 = no targets. Its findings also count
        # structural noise (high-entropy strings, "kyber" keywords) that says nothing about PRIVATE
        # TARGETS, so the verdict is read from the report: a TARGET found in a TRACKED file.
        try:
            self.assertIn(res.returncode, (0, 1), f"check_private_data.py did not run (rc={res.returncode}).\n{res.stderr[-800:]}")
            self.assertTrue(report_path.is_file(), "check_private_data.py wrote no report")
            report = json.loads(report_path.read_text(encoding="utf-8"))
        finally:
            report_path.unlink(missing_ok=True)
        tracked = set(_tracked_files())
        offenders = set()
        for scan in report.get("scans", []):
            for finding in (scan.get("result") or {}).get("findings", []):
                rel = str((finding.get("forensics") or {}).get("relpath") or "").replace("\\", "/")
                if rel not in tracked:   # gitignored / untracked local files never reach the repository
                    continue
                for match in finding.get("matches", []):
                    target = match.get("target")
                    if not target or _fold(target) in PUBLIC_MAINTAINER_NAMES:
                        continue
                    offenders.add((rel, str(match.get("category") or "target"), _mask(target)))
        self.assertEqual(
            [], sorted(offenders),
            "a private target (masked here) is present in tracked files: (path, category, target). "
            "Remove it going forward (edit the file, then a NEW forward commit); never rewrite history.",
        )


class GlobalBannerGuardTests(unittest.TestCase):
    """The CAPS banner must fire for every Claude Code session on this machine."""

    def test_banner_script_exists(self):
        self.assertTrue(
            BANNER_SCRIPT.exists(),
            f"banner script missing at {BANNER_SCRIPT} -> the global hook target is gone.",
        )

    def test_user_settings_wires_sessionstart_hook(self):
        self.assertTrue(USER_SETTINGS.exists(), f"{USER_SETTINGS} missing")
        cfg = json.loads(USER_SETTINGS.read_text(encoding="utf-8"))
        sessionstart = (cfg.get("hooks") or {}).get("SessionStart") or []
        commands = [
            h.get("command", "")
            for group in sessionstart
            for h in (group.get("hooks") or [])
        ]
        self.assertTrue(
            any("private_data_guard_banner" in c for c in commands),
            "no SessionStart hook references private_data_guard_banner in user settings.json",
        )

    def test_banner_prints_caps_keywords(self):
        if not BANNER_SCRIPT.exists():
            self.skipTest("banner script missing")
        res = subprocess.run(
            [sys.executable, str(BANNER_SCRIPT)],
            capture_output=True, text=True, timeout=30,
        )
        self.assertEqual(res.returncode, 0, "banner must exit 0 so it can't break a session")
        out = res.stdout
        for needle in (
            "PRIVATE DATA GUARD: ON",
            "NEVER REWRITE GIT HISTORY",
            "TAGS, PUSHES",
        ):
            self.assertIn(needle, out, f"banner output missing required CAPS line: {needle!r}")
        # The hook prints ONE JSON line ("systemMessage" reaches Angela's screen, "additionalContext"
        # reaches Claude): judge the banner TEXT inside it, never the JSON line with its lowercase keys.
        try:
            payload = json.loads(out)
        except ValueError:
            payload = None
        if isinstance(payload, dict):
            on_screen = str(payload.get("systemMessage") or "")
            to_claude = str((payload.get("hookSpecificOutput") or {}).get("additionalContext") or "")
            for channel, text in (("systemMessage", on_screen), ("additionalContext", to_claude)):
                for needle in ("PRIVATE DATA GUARD: ON", "NEVER REWRITE GIT HISTORY", "TAGS, PUSHES"):
                    self.assertIn(needle, text, f"the {channel} channel lost the required line: {needle!r}")
            banner = on_screen
        else:
            banner = out   # an older plain-text banner
        # The headline must actually be uppercase.
        headline = next((ln for ln in banner.splitlines() if "PRIVATE DATA GUARD" in ln), "")
        self.assertTrue(headline, "no headline line carries PRIVATE DATA GUARD")
        self.assertEqual(headline, headline.upper(), "headline banner line must be ALL CAPS")


if __name__ == "__main__":
    unittest.main(verbosity=2)