secubox-deb/docs/superpowers/plans/2026-06-17-social-ip-literal-fix.md

8.1 KiB

social-graph IP-literal fix (#642) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Stop recording IP-literal "tracker" edges (client self-traffic to gk2 service IPs) and make the #social total_trackers_seen KPI consistent with the by_tracker_domain table.

Architecture: Two small changes in secubox_toolbox/social.py: an _is_ip guard in _record_edge_sync (the chokepoint), and computing total_trackers_seen from the existing non-IP _byd fold in aggregate.

Tech Stack: Python, sqlite3. pytest (in-memory). Issue #642. Spec: docs/superpowers/specs/2026-06-17-social-ip-literal-fix-design.md.

Conventions: worktree secubox-deb-worktrees/642-… branch fix/642-…; commits end (ref #642).

Verified facts: social.py: DB_PATH (:44), _conn() (row_factory=Row, :151), _record_edge_sync(client_mac_hash, src_site, tracker_domain, cookie_id_hash_val, ja4_hash, consent_state) does the INSERT into social_edges, wrapped in try/except. _is_ip(host) (:700) = _IP_RE.match (IPv4) or ":" in h (IPv6) — defined AFTER _record_edge_sync but resolved at call-time. aggregate() sets out["total_trackers_seen"] = COUNT(DISTINCT tracker_domain) (raw), then builds _byd (folded, if not td or _is_ip(td): continue) and out["by_tracker_domain"] = sorted(_byd.values(), ...)[:50].


Task 1: _record_edge_sync — drop IP-literal trackers

Files:

  • Modify: packages/secubox-toolbox/secubox_toolbox/social.py (_record_edge_sync)

  • Test: packages/secubox-toolbox/tests/test_social_edges.py

  • Step 1: Write the failing test

# packages/secubox-toolbox/tests/test_social_edges.py
# SPDX-License-Identifier: LicenseRef-CMSD-1.0
import pathlib
from secubox_toolbox import social


def _db(tmp_path, monkeypatch):
    monkeypatch.setattr(social, "DB_PATH", pathlib.Path(tmp_path / "toolbox.db"))
    with social._conn():
        pass
    return social


def _count(s):
    with s._conn() as c:
        return c.execute("SELECT COUNT(*) FROM social_edges").fetchone()[0]


def test_record_edge_skips_ip_literals(tmp_path, monkeypatch):
    s = _db(tmp_path, monkeypatch)
    # IPv4 + IPv6 literal trackers → NOT recorded
    s._record_edge_sync("m", "news.example", "82.67.100.75", "cid", "ja4", "none_seen")
    s._record_edge_sync("m", "news.example", "2001:db8::1", "cid", "ja4", "none_seen")
    assert _count(s) == 0
    # a real hostname tracker → recorded
    s._record_edge_sync("m", "news.example", "www.criteo.com", "cid", "ja4", "pre_consent")
    assert _count(s) == 1
  • Step 2: Run test to verify it fails

Run: cd packages/secubox-toolbox && python -m pytest tests/test_social_edges.py::test_record_edge_skips_ip_literals -q Expected: FAIL — count is 2 then 3 (IP literals currently recorded).

  • Step 3: Add the guard in _record_edge_sync

At the very top of _record_edge_sync (before the try:), add:

    # #642 — never record IP-literal "trackers" (no SNI/hostname = the client's
    # own traffic to gk2 service IPs etc.); they're noise the domain views filter.
    if _is_ip(tracker_domain):
        return

(_is_ip is defined later in the module but resolved at call-time — fine.)

  • Step 4: Run test to verify it passes

Run: cd packages/secubox-toolbox && python -m pytest tests/test_social_edges.py -q Expected: PASS (1 passed). Then full suite python -m pytest tests/ -q.

  • Step 5: Commit
git add packages/secubox-toolbox/secubox_toolbox/social.py packages/secubox-toolbox/tests/test_social_edges.py
git commit -m "fix(toolbox): social drops IP-literal tracker edges (no-SNI self-traffic) (ref #642)"

Task 2: aggregate — KPI consistency

Files:

  • Modify: packages/secubox-toolbox/secubox_toolbox/social.py (aggregate)

  • Test: packages/secubox-toolbox/tests/test_social_edges.py

  • Step 1: Write the failing test (append)

def test_aggregate_total_excludes_ip_literals(tmp_path, monkeypatch):
    s = _db(tmp_path, monkeypatch)
    # insert raw rows DIRECTLY (bypass the new guard) to simulate legacy IP rows
    import time as _t
    now = int(_t.time())
    with s._conn() as c:
        for td in ("www.criteo.com", "doubleclick.net", "82.67.100.75"):
            c.execute("INSERT INTO social_edges(ts,client_mac_hash,src_site,"
                      "tracker_domain,cookie_id_hash,ja4_hash,consent_state) "
                      "VALUES(?,?,?,?,?,?,?)",
                      (now, "m", "news.example", td, "cid", "ja4", "none_seen"))
    agg = s.aggregate(hours=24)
    assert agg["total_trackers_seen"] == 2            # IP excluded
    assert len(agg["by_tracker_domain"]) == 2
    assert agg["total_trackers_seen"] == len(agg["by_tracker_domain"])
  • Step 2: Run test to verify it fails

Run: cd packages/secubox-toolbox && python -m pytest tests/test_social_edges.py::test_aggregate_total_excludes_ip_literals -q Expected: FAIL — total_trackers_seen == 3 (raw count includes the IP).

  • Step 3: Fix aggregate

DELETE the raw assignment:

            out["total_trackers_seen"] = c.execute(
                "SELECT COUNT(DISTINCT tracker_domain) FROM social_edges "
                "WHERE ts >= ?",
                (since,),
            ).fetchone()[0]

Then, immediately AFTER the out["by_tracker_domain"] = sorted(_byd.values(), key=lambda x: -x["hits"])[:50] line, add:

            # #642 — KPI = distinct non-IP registrable trackers (matches the
            # table's universe; was a raw COUNT that surfaced IP literals).
            out["total_trackers_seen"] = len(_byd)

(Leave active_clients and everything else unchanged.)

  • Step 4: Run test to verify it passes

Run: cd packages/secubox-toolbox && python -m pytest tests/test_social_edges.py -q Expected: PASS (2 passed). Then full suite python -m pytest tests/ -q.

  • Step 5: Commit
git add packages/secubox-toolbox/secubox_toolbox/social.py packages/secubox-toolbox/tests/test_social_edges.py
git commit -m "fix(toolbox): #social total_trackers_seen = distinct non-IP registrable (KPI matches table) (ref #642)"

Task 3: changelog + gate

Files:

  • Modify: packages/secubox-toolbox/debian/changelog

  • Step 1: Bump changelog

head -3 debian/changelog (top is 2.6.50-1~bookworm1 on master). Add 2.6.51-1~bookworm1, dch format, author Gerald KERMA <devel@cybermind.fr>, dated 2026-06-17:

  * social-graph (#642): stop recording IP-literal "tracker" edges (no-SNI
    self-traffic to gk2 service IPs); #social "Trackers vus" KPI now counts
    distinct non-IP registrable trackers so it matches the Top-domains table.
  • Step 2: Gate

Run: cd packages/secubox-toolbox && python -m pytest tests/ -q (expect all green). Run: python -c "import ast; ast.parse(open('secubox_toolbox/social.py').read()); print('ok')". Run: dpkg-parsechangelog -l debian/changelog | grep Version2.6.51-1~bookworm1. Run: git status --short → empty after commit.

  • Step 3: Commit
git add packages/secubox-toolbox/debian/changelog
git commit -m "chore(toolbox): changelog 2.6.51 for social IP-literal fix (ref #642)"

Self-Review

Spec coverage: Issue B (guard in _record_edge_sync) → Task 1; Issue A (total_trackers_seen = len(_byd)) → Task 2; packaging → Task 3. ✓

Placeholder scan: none — complete code each step.

Type consistency: _is_ip(host)->bool (existing) reused in _record_edge_sync; len(_byd) is the count of the dict the table is built from → total_trackers_seen == len(by_tracker_domain) whenever distinct ≤50 (the test's case). For >50 distinct, KPI ≥ table length (correct: "N seen", table shows top 50) — not a contradiction.

Rollout: no schema change; legacy IP edges excluded from both metrics immediately + expire via 7-day retention. Deploy = portal restart (aggregate) + mitm-wg reload (record_edge). No enforcement impact.