feat(ttp): E.3.8 R0054-R0058 intel cohort + mark step done
5 YAMLs for the intel-verdict cohort per Appendix B / A.10: AbuseIPDB category mapping, GreyNoise classification, Feodo Tracker hit, ThreatFox IOC type, aggregate-malicious bump-only. IntelLifter (E.3.10) consumes by rule_id and tolerates absence silently (null provider column → no tag). R0058 is the meta bump-only rule — emits a single confidence=0.0 sentinel so it validates and surfaces in the catalogue, but the repository's sub-0.3 drop ensures no fresh tag persists if the fanout fires accidentally. test_intel_rules.py pins that zero-confidence invariant. Marks E.3.8 done in development/TTP_TAGGING.md with the cohort- split summary.
This commit is contained in:
@@ -2969,7 +2969,20 @@ Order:
|
|||||||
test per Appendix C in the same commit. The corpus for
|
test per Appendix C in the same commit. The corpus for
|
||||||
precision testing comes from a labelled holdout fixture under
|
precision testing comes from a labelled holdout fixture under
|
||||||
`tests/ttp/rule_precision/corpus/` — that fixture is itself a
|
`tests/ttp/rule_precision/corpus/` — that fixture is itself a
|
||||||
sub-step (commit) before any rule lands.
|
sub-step (commit) before any rule lands. ✅ done. Cohorts shipped
|
||||||
|
in 6 commits: corpus+harness, then command (R0001–R0030),
|
||||||
|
behavioral (R0031–R0040), email (R0041–R0048), canary
|
||||||
|
(R0049–R0053), intel (R0054–R0058). Live precision asserts on
|
||||||
|
R0007–R0029 (regex-on-payload-field). Lifter-bound rules
|
||||||
|
(R0001–R0006, R0030, R0031–R0058) are inert under the v0 engine
|
||||||
|
by design — their YAMLs declare technique mappings the
|
||||||
|
E.3.9–E.3.13 lifters consume by `rule_id`, with each precision
|
||||||
|
case `xfail(strict=True)`-gated on the matching lifter step.
|
||||||
|
R0058 emits at confidence 0.0 (bump-only meta-rule; repository
|
||||||
|
drops sub-0.3 anyway). The corpus extractor lives at
|
||||||
|
`tests/ttp/rule_precision/_build_corpus.py`; operator-built
|
||||||
|
corpora are gitignored, only synthetic `seed_*.jsonl` is
|
||||||
|
committed.
|
||||||
9. **BehavioralLifter** — read `AttackerBehavior` /
|
9. **BehavioralLifter** — read `AttackerBehavior` /
|
||||||
`Credential` / `CredentialReuse`, emit per Appendix A behavior
|
`Credential` / `CredentialReuse`, emit per Appendix A behavior
|
||||||
tables. Tests in `test_lifter_absence.py` and a new
|
tables. Tests in `test_lifter_absence.py` and a new
|
||||||
|
|||||||
25
rules/ttp/R0054.yaml
Normal file
25
rules/ttp/R0054.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
rule_id: R0054
|
||||||
|
rule_version: 1
|
||||||
|
name: abuseipdb_category
|
||||||
|
description: |
|
||||||
|
AbuseIPDB category → ATT&CK technique mapping per Appendix A.10.
|
||||||
|
IntelLifter reads AttackerIntel.abuseipdb_categories and emits
|
||||||
|
one tag per matching category code.
|
||||||
|
applies_to:
|
||||||
|
- intel
|
||||||
|
match:
|
||||||
|
kind: lifter:intel_abuseipdb
|
||||||
|
provider: abuseipdb
|
||||||
|
emits:
|
||||||
|
- tactic: TA0006
|
||||||
|
technique_id: T1110
|
||||||
|
confidence: 0.7
|
||||||
|
- tactic: TA0001
|
||||||
|
technique_id: T1190
|
||||||
|
confidence: 0.7
|
||||||
|
- tactic: TA0001
|
||||||
|
technique_id: T1566
|
||||||
|
confidence: 0.7
|
||||||
|
evidence_fields:
|
||||||
|
- abuseipdb_categories
|
||||||
|
- abuse_confidence_score
|
||||||
23
rules/ttp/R0055.yaml
Normal file
23
rules/ttp/R0055.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
rule_id: R0055
|
||||||
|
rule_version: 1
|
||||||
|
name: greynoise_classification
|
||||||
|
description: |
|
||||||
|
GreyNoise classification + tag → ATT&CK technique per A.10.
|
||||||
|
IntelLifter reads AttackerIntel.greynoise_classification and
|
||||||
|
greynoise_tags.
|
||||||
|
applies_to:
|
||||||
|
- intel
|
||||||
|
match:
|
||||||
|
kind: lifter:intel_greynoise
|
||||||
|
provider: greynoise
|
||||||
|
emits:
|
||||||
|
- tactic: TA0043
|
||||||
|
technique_id: T1595
|
||||||
|
sub_technique_id: T1595.002
|
||||||
|
confidence: 0.7
|
||||||
|
- tactic: TA0011
|
||||||
|
technique_id: T1071
|
||||||
|
confidence: 0.7
|
||||||
|
evidence_fields:
|
||||||
|
- greynoise_classification
|
||||||
|
- greynoise_tags
|
||||||
23
rules/ttp/R0056.yaml
Normal file
23
rules/ttp/R0056.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
rule_id: R0056
|
||||||
|
rule_version: 1
|
||||||
|
name: feodo_tracker_hit
|
||||||
|
description: |
|
||||||
|
Source IP listed by abuse.ch Feodo Tracker — known C2 infra,
|
||||||
|
family attribution attached.
|
||||||
|
applies_to:
|
||||||
|
- intel
|
||||||
|
match:
|
||||||
|
kind: lifter:intel_feodo
|
||||||
|
provider: feodo
|
||||||
|
emits:
|
||||||
|
- tactic: TA0011
|
||||||
|
technique_id: T1071
|
||||||
|
sub_technique_id: T1071.001
|
||||||
|
confidence: 0.85
|
||||||
|
- tactic: TA0042
|
||||||
|
technique_id: T1588
|
||||||
|
sub_technique_id: T1588.001
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- malware_family
|
||||||
|
- first_seen_feodo
|
||||||
23
rules/ttp/R0057.yaml
Normal file
23
rules/ttp/R0057.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
rule_id: R0057
|
||||||
|
rule_version: 1
|
||||||
|
name: threatfox_ioc
|
||||||
|
description: |
|
||||||
|
abuse.ch ThreatFox IOC type → ATT&CK technique mapping with
|
||||||
|
family attribution.
|
||||||
|
applies_to:
|
||||||
|
- intel
|
||||||
|
match:
|
||||||
|
kind: lifter:intel_threatfox
|
||||||
|
provider: threatfox
|
||||||
|
emits:
|
||||||
|
- tactic: TA0011
|
||||||
|
technique_id: T1071
|
||||||
|
confidence: 0.8
|
||||||
|
- tactic: TA0042
|
||||||
|
technique_id: T1588
|
||||||
|
sub_technique_id: T1588.001
|
||||||
|
confidence: 0.8
|
||||||
|
evidence_fields:
|
||||||
|
- ioc_type
|
||||||
|
- malware_family
|
||||||
|
- threat_type
|
||||||
23
rules/ttp/R0058.yaml
Normal file
23
rules/ttp/R0058.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
rule_id: R0058
|
||||||
|
rule_version: 1
|
||||||
|
name: aggregate_malicious_verdict_bump
|
||||||
|
description: |
|
||||||
|
Aggregate intel verdict = "malicious" with no specific provider
|
||||||
|
mapping. Per Appendix B: confidence-bump existing tags only,
|
||||||
|
never emits a fresh tag. emits is intentionally a single
|
||||||
|
zero-confidence sentinel so the rule still validates and the
|
||||||
|
catalogue surfaces it; the IntelLifter inspects rule_id and
|
||||||
|
bumps existing tags' confidence rather than calling the engine
|
||||||
|
fanout.
|
||||||
|
applies_to:
|
||||||
|
- intel
|
||||||
|
match:
|
||||||
|
kind: lifter:intel_aggregate_bump
|
||||||
|
bump_amount: 0.05
|
||||||
|
emits:
|
||||||
|
- tactic: TA0042
|
||||||
|
technique_id: T1588
|
||||||
|
confidence: 0.0
|
||||||
|
evidence_fields:
|
||||||
|
- aggregate_verdict
|
||||||
|
- bumped_rule_ids
|
||||||
73
tests/ttp/rule_precision/test_intel_rules.py
Normal file
73
tests/ttp/rule_precision/test_intel_rules.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
"""R0054-R0058 — intel verdict cohort.
|
||||||
|
|
||||||
|
IntelLifter (E.3.10) reads ``AttackerIntel`` provider columns
|
||||||
|
(AbuseIPDB, GreyNoise, Feodo, ThreatFox) and emits per the per-
|
||||||
|
provider mapping tables in Appendix A.10. Per Appendix B every
|
||||||
|
intel rule tolerates absence silently — a null provider column is
|
||||||
|
"no tag from this rule", never an error. R0058 is the
|
||||||
|
confidence-bump-only meta-rule (no fresh tag emission); the
|
||||||
|
lifter inspects rule_id and bumps existing tags.
|
||||||
|
|
||||||
|
The v0 :class:`RuleEngine` cannot navigate the intel envelope —
|
||||||
|
the rules are inert under regex.
|
||||||
|
"""
|
||||||
|
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(54, 59)]
|
||||||
|
|
||||||
|
|
||||||
|
@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("intel"):
|
||||||
|
tags = await precision_engine.evaluate(make_event(row))
|
||||||
|
fired.update(tag.rule_id for tag in tags)
|
||||||
|
assert rule_id not in fired
|
||||||
|
|
||||||
|
|
||||||
|
def test_r0058_is_bump_only() -> None:
|
||||||
|
"""R0058's only emit is a zero-confidence sentinel.
|
||||||
|
|
||||||
|
Per Appendix B the aggregate-malicious rule must not emit a fresh
|
||||||
|
tag — it bumps existing rule confidences. The repository drops
|
||||||
|
tags below 0.3 confidence, so even if the lifter accidentally
|
||||||
|
drove the engine fanout the tag would never persist. This test
|
||||||
|
pins that defense-in-depth property: any future edit pushing the
|
||||||
|
R0058 emit confidence above 0 would fire here.
|
||||||
|
"""
|
||||||
|
compiled = _parse_and_compile(
|
||||||
|
Path("rules/ttp/R0058.yaml"), RuleState(),
|
||||||
|
)
|
||||||
|
assert all(emit[3] == 0.0 for emit in compiled.emits), (
|
||||||
|
"R0058 must keep all emit confidences at 0.0 (bump-only rule)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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)")
|
||||||
Reference in New Issue
Block a user