feat(ttp): E.3.8 R0031-R0040 behavioral cohort

10 YAMLs for the behavioral / cross-event cohort per Appendix B:
beaconing, data destruction, ransom note, web exfil, DB mass-read,
credentials-in-files, k8s SA token harvest, Docker host escape,
LLMNR poisoning, TFTP router-config retrieval.

Every rule is lifter-bound (BehavioralLifter / IdentityLifter) —
the v0 RuleEngine cannot count, aggregate, or compose cross-event
signals, so these YAMLs declare the technique mappings the lifter
will consume by rule_id at E.3.9. Their match specs use a
'kind: lifter:*' shape inert to the regex matcher.

test_behavioral_rules.py asserts each YAML compiles, none fire
from the v0 engine (FP regression guard against a YAML drifting
into a regex), and an xfail(strict=True, reason='impl phase E.3.9')
precision case that will flip green when the lifter lands.
This commit is contained in:
2026-05-01 09:18:27 -04:00
parent b1fe1f9403
commit 806301e179
11 changed files with 304 additions and 0 deletions

23
rules/ttp/R0031.yaml Normal file
View File

@@ -0,0 +1,23 @@
rule_id: R0031
rule_version: 1
name: beaconing
description: |
Periodic outbound activity with low jitter — classic C2 beacon.
Read from AttackerBehavior.beacon_interval_s / .beacon_jitter_pct
by the BehavioralLifter (E.3.9).
applies_to:
- session
match:
kind: lifter:beaconing
max_jitter_pct: 0.15
min_interval_s: 10
emits:
- tactic: TA0011
technique_id: T1071
confidence: 0.8
- tactic: TA0011
technique_id: T1029
confidence: 0.85
evidence_fields:
- beacon_interval_s
- beacon_jitter_pct

24
rules/ttp/R0032.yaml Normal file
View File

@@ -0,0 +1,24 @@
rule_id: R0032
rule_version: 1
name: data_destruction
description: |
Mass destructive ops: Redis FLUSHALL, SQL DROP DATABASE, MongoDB
dropDatabase(), bulk DELETE without WHERE. Cross-event because we
want to confirm the verb landed on real data, not just a parse.
applies_to:
- session
match:
kind: lifter:data_destruction
patterns:
- 'FLUSHALL'
- 'DROP\\s+DATABASE'
- 'TRUNCATE\\s+TABLE'
- 'dropDatabase\\(\\)'
- 'DELETE\\s+/\\_all'
emits:
- tactic: TA0040
technique_id: T1485
confidence: 0.95
evidence_fields:
- matched_op
- target

26
rules/ttp/R0033.yaml Normal file
View File

@@ -0,0 +1,26 @@
rule_id: R0033
rule_version: 1
name: ransom_note_pattern
description: |
Bitcoin/Monero address + payment-demand language inserted into a
honeydoc, mail body, or DB collection. EmailLifter (R0033 fires
from email_body too) and BehavioralLifter share the same rule_id.
applies_to:
- session
- email
match:
kind: lifter:ransom_note
require_btc_or_xmr: true
payment_keywords:
- bitcoin
- btc
- monero
- ransom
- decrypt
emits:
- tactic: TA0040
technique_id: T1486
confidence: 0.9
evidence_fields:
- btc_address
- matched_keywords

21
rules/ttp/R0034.yaml Normal file
View File

@@ -0,0 +1,21 @@
rule_id: R0034
rule_version: 1
name: exfil_over_web
description: |
Outbound data exfil to a web service (POST with large body /
Content-Length, or many GETs encoding payload in path). Read
from session aggregates, not single requests.
applies_to:
- session
match:
kind: lifter:exfil_over_web
min_payload_bytes: 1048576
request_threshold: 50
emits:
- tactic: TA0010
technique_id: T1567
confidence: 0.85
evidence_fields:
- bytes_out
- request_count
- target_host

21
rules/ttp/R0035.yaml Normal file
View File

@@ -0,0 +1,21 @@
rule_id: R0035
rule_version: 1
name: db_mass_read
description: |
SELECT/COPY/RETR pulling many rows or whole tables from a DB
honeypot. BehavioralLifter (E.3.9) reads the per-session
query-byte aggregate.
applies_to:
- session
match:
kind: lifter:db_mass_read
min_rows: 10000
min_bytes: 5242880
emits:
- tactic: TA0009
technique_id: T1213
confidence: 0.85
evidence_fields:
- service
- rows_read
- bytes_read

25
rules/ttp/R0036.yaml Normal file
View File

@@ -0,0 +1,25 @@
rule_id: R0036
rule_version: 1
name: credentials_in_files
description: |
Reading .env / .git/config / cloud credential files —
credential-from-file harvesting. Lifter-driven so the rule
composes the file-access signal with the path discriminator.
applies_to:
- session
- http_request
match:
kind: lifter:credentials_in_files
paths:
- '\\.env'
- '\\.git/config'
- '\\.aws/credentials'
- '\\.ssh/id_rsa'
- 'wp-config\\.php'
emits:
- tactic: TA0006
technique_id: T1552
sub_technique_id: T1552.001
confidence: 0.9
evidence_fields:
- matched_path

22
rules/ttp/R0037.yaml Normal file
View File

@@ -0,0 +1,22 @@
rule_id: R0037
rule_version: 1
name: k8s_service_account_token
description: |
Reading /api/v1/namespaces/*/secrets or /var/run/secrets/k8s.io/
serviceaccount/token — kube SA harvest.
applies_to:
- session
- http_request
match:
kind: lifter:k8s_sa_token
paths:
- '/api/v1/namespaces/[^/]+/secrets'
- '/var/run/secrets/kubernetes\\.io/serviceaccount'
emits:
- tactic: TA0006
technique_id: T1552
sub_technique_id: T1552.007
confidence: 0.95
evidence_fields:
- matched_path
- namespace

23
rules/ttp/R0038.yaml Normal file
View File

@@ -0,0 +1,23 @@
rule_id: R0038
rule_version: 1
name: docker_host_escape
description: |
Privileged container creation or bind-mount of host /, /etc, or
/var/run/docker.sock — host-escape primitive. Lifter inspects
the structured Docker-API event payload.
applies_to:
- session
match:
kind: lifter:docker_escape
signals:
- 'privileged:true'
- 'bind:/:/'
- 'bind:/etc'
- 'bind:/var/run/docker.sock'
emits:
- tactic: TA0004
technique_id: T1611
confidence: 0.95
evidence_fields:
- matched_signal
- container_image

19
rules/ttp/R0039.yaml Normal file
View File

@@ -0,0 +1,19 @@
rule_id: R0039
rule_version: 1
name: llmnr_poisoning
description: |
Responder-style LLMNR/NBT-NS spoofed reply pattern observed
on the network sniffer. BehavioralLifter (E.3.9) reads the
network-event aggregate.
applies_to:
- session
match:
kind: lifter:llmnr_poisoning
emits:
- tactic: TA0009
technique_id: T1557
sub_technique_id: T1557.001
confidence: 0.9
evidence_fields:
- poisoned_query
- victim_host

23
rules/ttp/R0040.yaml Normal file
View File

@@ -0,0 +1,23 @@
rule_id: R0040
rule_version: 1
name: tftp_router_config_retrieval
description: |
TFTP RRQ for a router-config-shaped filename (*-confg, *.cfg,
startup-config, running-config). Per Appendix A.4.
applies_to:
- session
match:
kind: lifter:tftp_router_config
filename_patterns:
- '.*-confg$'
- '.*\\.cfg$'
- 'startup-config'
- 'running-config'
emits:
- tactic: TA0009
technique_id: T1602
sub_technique_id: T1602.002
confidence: 0.9
evidence_fields:
- tftp_filename
- source_host

View File

@@ -0,0 +1,77 @@
"""R0031-R0040 — behavioral / cross-event cohort.
Every rule here is consumed by the BehavioralLifter (or an
identity-rollup variant) at E.3.9. The v0 :class:`RuleEngine` has no
counter / aggregator — it can only regex over a single event
payload — so these rules cannot fire from the engine alone. Their
``match.kind`` keys (``lifter:beaconing`` etc.) are inert to the
regex matcher by design.
This file asserts:
* every R003N has a YAML on disk that compiles
* the v0 engine NEVER fires any of them (regression guard against a
YAML drifting into a regex match)
* the precision target test is :pyfunc:`pytest.xfail`-gated until
the BehavioralLifter ships, matching the CDD pattern at
``development/TTP_TAGGING.md:2450``.
"""
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(31, 41)]
@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:
"""Behavioral rules MUST NOT fire from the regex engine.
Walks both the behavioral and the command corpora — if any event
in either set lights up a behavioral rule, a YAML drifted into a
regex match.spec.
"""
fired: set[str] = set()
for cohort in ("behavioral", "commands"):
for row in corpus_loader(cohort):
tags = await precision_engine.evaluate(make_event(row))
fired.update(tag.rule_id for tag in tags)
assert rule_id not in fired, (
f"{rule_id} is lifter-bound but fired from the regex engine"
)
@pytest.mark.parametrize("rule_id", _RULE_IDS)
@pytest.mark.xfail(strict=True, reason="impl phase E.3.9 (BehavioralLifter)")
def test_behavioral_rule_precision(rule_id: str) -> None:
"""Will live once the BehavioralLifter ships at E.3.9.
The lifter consumes ``AttackerBehavior`` / session aggregates and
emits one tag per matching rule_id. This test will then load the
behavioral corpus, drive the lifter, and assert the per-rule
precision target. Until that day this xfails strict so the suite
flips green automatically when E.3.9 wires it up.
"""
pytest.fail(f"{rule_id}: BehavioralLifter not yet shipped (E.3.9)")