Files
DECNET/tests/ttp/rule_precision/test_canary_rules.py
anti f2b3393669 chore: relicense to AGPL-3.0-or-later and add SPDX headers
Replaces LICENSE (GPLv3 -> AGPLv3) and prepends
`SPDX-License-Identifier: AGPL-3.0-or-later` to every source file
across decnet/, decnet_web/, tests/, scripts/, and tools/.

Rationale: closes the GPLv3 ASP loophole so any party operating a
modified DECNET as a network service must offer their modified
source. Personal copyright (Samuel Paschuan) + inbound=outbound
contributions make a future unilateral relicense infeasible.

- LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt)
- COPYRIGHT: project copyright notice
- tools/add_spdx_headers.py: idempotent header injector
  (shebang- and PEP 263-aware)

Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh).
No behavior change; comments only.
2026-05-22 21:04:16 -04:00

90 lines
2.9 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""R0049-R0053 — canary fingerprint cohort.
CanaryFingerprintLifter (E.3.11) parses the fingerprint payload
(navigator/webdriver flag, canvas/audio/WebGL hashes, WebRTC IPs,
TZ/language/geo composite) and emits per Appendix A.9. The v0
:class:`RuleEngine` cannot navigate a structured fingerprint blob —
these rules are inert under the regex matcher.
"""
from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
import pytest
from decnet.ttp.impl.rule_engine import RuleEngine
from decnet.ttp.store.base import RuleState
from decnet.ttp.store.impl.filesystem import _parse_and_compile
from tests.ttp.rule_precision.conftest import CorpusRow, make_event
CohortLoader = Callable[[str], list[CorpusRow]]
_RULE_IDS = [f"R{n:04d}" for n in range(49, 54)]
@pytest.mark.parametrize("rule_id", _RULE_IDS)
def test_rule_yaml_present(rule_id: str) -> None:
path = Path("rules/ttp") / f"{rule_id}.yaml"
assert path.exists(), f"missing YAML: {path}"
compiled = _parse_and_compile(path, RuleState())
assert compiled.rule_id == rule_id
@pytest.mark.parametrize("rule_id", _RULE_IDS)
async def test_lifter_bound_inert_in_v0(
rule_id: str,
precision_engine: RuleEngine,
corpus_loader: CohortLoader,
) -> None:
fired: set[str] = set()
for row in corpus_loader("canary"):
tags = await precision_engine.evaluate(make_event(row))
fired.update(tag.rule_id for tag in tags)
assert rule_id not in fired
def _build_lifter() -> "CanaryFingerprintLifter":
from decnet.ttp.impl.canary_fingerprint_lifter import (
CanaryFingerprintLifter,
)
from tests.ttp._stub_store import StubRuleStore
rules = [
_parse_and_compile(Path("rules/ttp") / f"{rid}.yaml", RuleState())
for rid in _RULE_IDS
]
lifter = CanaryFingerprintLifter(StubRuleStore(compiled=rules))
for rule in rules:
lifter._index.install(rule)
return lifter
@pytest.mark.parametrize("rule_id", _RULE_IDS)
def test_canary_rule_precision(
rule_id: str,
corpus_loader: CohortLoader,
) -> None:
"""E.3.11 — drive CanaryFingerprintLifter over the labelled corpus
and assert per-rule precision (H-band rules → ≥95%, M-band → ≥80%).
R0052 is M-band (0.7 confidence); the rest are H-band.
"""
import asyncio
from tests.ttp.rule_precision.conftest import precision_for
rows = corpus_loader("canary")
if not rows:
pytest.skip("no canary corpus available")
lifter = _build_lifter()
fired: dict[str, list[str]] = {}
for row in rows:
tags = asyncio.run(lifter.tag(make_event(row)))
fired[row.label] = [tag.rule_id for tag in tags]
precision, _tp, _fp = precision_for(rule_id, rows, fired)
threshold = 0.80 if rule_id == "R0052" else 0.95
assert precision >= threshold, (
f"{rule_id} precision {precision:.2f} < {threshold} on canary corpus"
)