feat(ttp): E.3.10 IntelLifter (R0054-R0058)
Per-provider verdict translator for AbuseIPDB, GreyNoise, Feodo Tracker, and ThreatFox per Appendix A.10. Each rule's predicate inspects payload fields produced by the enrich worker (no DB I/O, no decnet.intel.* imports — E.2.7 decoupling guard preserved). AbuseIPDB confidence is scaled by abuse_confidence_score / 100; categories drive per-technique fan-out. R0058 aggregate-bump is a no-op in v0 (cross-tag bump deferred to E.3.14 worker bootstrap). Per-provider null tolerance is the steady state — a missing provider column produces zero tags from that rule, never an error. Tests: - tests/ttp/test_intel_lifter.py — per-provider positive + negative + state modulation + decoupling source-import guard. - tests/ttp/rule_precision/test_intel_rules.py — xfail flipped, real precision driven over seed_intel.jsonl (R0054-R0057 H-band ≥95%; R0058 skipped as bump-only). - tests/ttp/test_lifter_absence.py — IntelLifter all-populated test flipped from xfail-strict to real assertion with realistic payload. - tests/ttp/test_lifters.py — partial-null xfail flipped to real assertion.
This commit is contained in:
@@ -1,2 +1,8 @@
|
||||
{"source_kind": "intel", "payload": {"verdict": "malicious", "provider": "abuseipdb", "categories": [18, 22]}, "expected_rule_ids": ["R0054"], "label": "abuseipdb_brute"}
|
||||
{"source_kind": "intel", "payload": {"verdict": "benign", "provider": "greynoise", "tags": []}, "expected_rule_ids": [], "label": "negative_benign"}
|
||||
{"source_kind": "intel", "payload": {"abuseipdb_score": 95, "abuseipdb_categories": [18, 22], "provider": "abuseipdb"}, "expected_rule_ids": ["R0054"], "label": "abuseipdb_brute"}
|
||||
{"source_kind": "intel", "payload": {"greynoise_classification": "scanner"}, "expected_rule_ids": ["R0055"], "label": "greynoise_scanner"}
|
||||
{"source_kind": "intel", "payload": {"greynoise_classification": "malicious", "greynoise_tags": ["cobalt_strike"]}, "expected_rule_ids": ["R0055"], "label": "greynoise_c2_tag"}
|
||||
{"source_kind": "intel", "payload": {"feodo_listed": true, "malware_family": "Emotet"}, "expected_rule_ids": ["R0056"], "label": "feodo_emotet"}
|
||||
{"source_kind": "intel", "payload": {"ioc_type": "botnet_cc", "malware_family": "sliver"}, "expected_rule_ids": ["R0057"], "label": "threatfox_botnet_cc"}
|
||||
{"source_kind": "intel", "payload": {"ioc_type": "payload_delivery", "malware_family": "asyncrat"}, "expected_rule_ids": ["R0057"], "label": "threatfox_payload"}
|
||||
{"source_kind": "intel", "payload": {"verdict": "benign", "provider": "greynoise", "greynoise_classification": "benign", "greynoise_tags": []}, "expected_rule_ids": [], "label": "negative_benign"}
|
||||
{"source_kind": "intel", "payload": {}, "expected_rule_ids": [], "label": "negative_empty"}
|
||||
|
||||
@@ -67,7 +67,45 @@ def test_r0058_is_bump_only() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _build_lifter() -> "IntelLifter":
|
||||
from decnet.ttp.impl.intel_lifter import IntelLifter
|
||||
from tests.ttp._stub_store import StubRuleStore
|
||||
|
||||
rules = [
|
||||
_parse_and_compile(Path("rules/ttp") / f"{rid}.yaml", RuleState())
|
||||
for rid in _RULE_IDS
|
||||
]
|
||||
lifter = IntelLifter(StubRuleStore(compiled=rules))
|
||||
for rule in rules:
|
||||
lifter._index.install(rule)
|
||||
return lifter
|
||||
|
||||
|
||||
@pytest.mark.parametrize("rule_id", _RULE_IDS)
|
||||
@pytest.mark.xfail(strict=True, reason="impl phase E.3.10 (IntelLifter)")
|
||||
def test_intel_rule_precision(rule_id: str) -> None:
|
||||
pytest.fail(f"{rule_id}: IntelLifter not yet shipped (E.3.10)")
|
||||
def test_intel_rule_precision(
|
||||
rule_id: str,
|
||||
corpus_loader: CohortLoader,
|
||||
) -> None:
|
||||
"""E.3.10: drive IntelLifter over the labelled corpus and assert
|
||||
per-rule precision. R0058 (bump-only) is excluded — it intentionally
|
||||
never emits a tag, so vacuous precision is irrelevant.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from tests.ttp.rule_precision.conftest import precision_for
|
||||
|
||||
if rule_id == "R0058":
|
||||
pytest.skip("R0058 is bump-only; no precision target")
|
||||
rows = corpus_loader("intel")
|
||||
if not rows:
|
||||
pytest.skip("no intel 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)
|
||||
# R0054/R0055/R0056/R0057 are H-band per Appendix C → ≥95%.
|
||||
assert precision >= 0.95, (
|
||||
f"{rule_id} precision {precision:.2f} < 0.95 on intel corpus"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user