feat(ttp): E.3.8 R0041-R0048 email cohort
8 YAMLs for the email cohort per Appendix B: open-relay abuse, mass phishing, phishing-kit X-Mailer signatures, IDN/punycode URLs, sender masquerade, malicious attachment, BEC, encoded payload in body. EmailLifter (E.3.12) consumes by rule_id. test_email_rules.py: YAML-present + inert-in-v0 + xfail(strict) precision case gated on E.3.12.
This commit is contained in:
24
rules/ttp/R0041.yaml
Normal file
24
rules/ttp/R0041.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
rule_id: R0041
|
||||||
|
rule_version: 1
|
||||||
|
name: open_relay_abuse
|
||||||
|
description: |
|
||||||
|
High RCPT count with a foreign From — abuse of an open relay to
|
||||||
|
push outbound mail. EmailLifter (E.3.12).
|
||||||
|
applies_to:
|
||||||
|
- email
|
||||||
|
match:
|
||||||
|
kind: lifter:open_relay
|
||||||
|
rcpt_threshold: 10
|
||||||
|
require_foreign_from: true
|
||||||
|
emits:
|
||||||
|
- tactic: TA0040
|
||||||
|
technique_id: T1496
|
||||||
|
confidence: 0.85
|
||||||
|
- tactic: TA0042
|
||||||
|
technique_id: T1586
|
||||||
|
sub_technique_id: T1586.002
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- rcpt_count
|
||||||
|
- from_domain
|
||||||
|
- mail_from_domain
|
||||||
19
rules/ttp/R0042.yaml
Normal file
19
rules/ttp/R0042.yaml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
rule_id: R0042
|
||||||
|
rule_version: 1
|
||||||
|
name: mass_phishing_campaign
|
||||||
|
description: |
|
||||||
|
RCPT count above threshold + body simhash matching across N
|
||||||
|
recipients in a window. EmailLifter (E.3.12).
|
||||||
|
applies_to:
|
||||||
|
- email
|
||||||
|
match:
|
||||||
|
kind: lifter:email_mass_phish
|
||||||
|
rcpt_threshold: 25
|
||||||
|
body_simhash_window_h: 24
|
||||||
|
emits:
|
||||||
|
- tactic: TA0001
|
||||||
|
technique_id: T1566
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- rcpt_count
|
||||||
|
- body_simhash
|
||||||
22
rules/ttp/R0043.yaml
Normal file
22
rules/ttp/R0043.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
rule_id: R0043
|
||||||
|
rule_version: 1
|
||||||
|
name: phishing_kit_xmailer
|
||||||
|
description: |
|
||||||
|
X-Mailer header matches a curated phishing-kit signature DB
|
||||||
|
(PHPMailer-of-known-kits, GreatHorn known-bad, etc.).
|
||||||
|
applies_to:
|
||||||
|
- email
|
||||||
|
match:
|
||||||
|
kind: lifter:email_xmailer_kit
|
||||||
|
catalog: phishing_kits
|
||||||
|
emits:
|
||||||
|
- tactic: TA0001
|
||||||
|
technique_id: T1566
|
||||||
|
confidence: 0.9
|
||||||
|
- tactic: TA0042
|
||||||
|
technique_id: T1588
|
||||||
|
sub_technique_id: T1588.001
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- x_mailer
|
||||||
|
- matched_kit
|
||||||
23
rules/ttp/R0044.yaml
Normal file
23
rules/ttp/R0044.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
rule_id: R0044
|
||||||
|
rule_version: 1
|
||||||
|
name: idn_homoglyph_url
|
||||||
|
description: |
|
||||||
|
IDN / Punycode (xn--) URL in email body. Two emits: masquerade
|
||||||
|
(T1036.005) and credential-harvest landing-page (T1566.002).
|
||||||
|
applies_to:
|
||||||
|
- email
|
||||||
|
match:
|
||||||
|
kind: lifter:email_idn_url
|
||||||
|
punycode_prefix: 'xn--'
|
||||||
|
emits:
|
||||||
|
- tactic: TA0005
|
||||||
|
technique_id: T1036
|
||||||
|
sub_technique_id: T1036.005
|
||||||
|
confidence: 0.9
|
||||||
|
- tactic: TA0001
|
||||||
|
technique_id: T1566
|
||||||
|
sub_technique_id: T1566.002
|
||||||
|
confidence: 0.9
|
||||||
|
evidence_fields:
|
||||||
|
- matched_url
|
||||||
|
- decoded_idn
|
||||||
25
rules/ttp/R0045.yaml
Normal file
25
rules/ttp/R0045.yaml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
rule_id: R0045
|
||||||
|
rule_version: 1
|
||||||
|
name: sender_masquerade
|
||||||
|
description: |
|
||||||
|
From / Return-Path / MAIL FROM domain mismatch, or DKIM/SPF
|
||||||
|
fail signal in Authentication-Results. Lifter composes the three
|
||||||
|
header signals into a single masquerade verdict.
|
||||||
|
applies_to:
|
||||||
|
- email
|
||||||
|
match:
|
||||||
|
kind: lifter:email_sender_masquerade
|
||||||
|
signals:
|
||||||
|
- from_returnpath_mismatch
|
||||||
|
- from_mailfrom_mismatch
|
||||||
|
- dkim_fail
|
||||||
|
- spf_fail
|
||||||
|
emits:
|
||||||
|
- tactic: TA0005
|
||||||
|
technique_id: T1036
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- from_domain
|
||||||
|
- return_path_domain
|
||||||
|
- mail_from_domain
|
||||||
|
- auth_results
|
||||||
33
rules/ttp/R0046.yaml
Normal file
33
rules/ttp/R0046.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
rule_id: R0046
|
||||||
|
rule_version: 1
|
||||||
|
name: malicious_attachment
|
||||||
|
description: |
|
||||||
|
Macro-bearing Office doc, .lnk, .iso/.img, password-protected
|
||||||
|
archive, or HTML-smuggling pattern. Lifter inspects the
|
||||||
|
attachment table (file_type + ole_macros + maldoc verdict).
|
||||||
|
applies_to:
|
||||||
|
- email
|
||||||
|
match:
|
||||||
|
kind: lifter:email_malicious_attachment
|
||||||
|
triggers:
|
||||||
|
- office_macro
|
||||||
|
- lnk
|
||||||
|
- iso
|
||||||
|
- img
|
||||||
|
- protected_archive
|
||||||
|
- html_smuggling
|
||||||
|
- mal_hash_match
|
||||||
|
emits:
|
||||||
|
- tactic: TA0002
|
||||||
|
technique_id: T1204
|
||||||
|
sub_technique_id: T1204.002
|
||||||
|
confidence: 0.9
|
||||||
|
- tactic: TA0001
|
||||||
|
technique_id: T1566
|
||||||
|
sub_technique_id: T1566.001
|
||||||
|
confidence: 0.9
|
||||||
|
evidence_fields:
|
||||||
|
- filename
|
||||||
|
- mime_type
|
||||||
|
- matched_trigger
|
||||||
|
- file_hash
|
||||||
31
rules/ttp/R0047.yaml
Normal file
31
rules/ttp/R0047.yaml
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
rule_id: R0047
|
||||||
|
rule_version: 1
|
||||||
|
name: bec_pattern
|
||||||
|
description: |
|
||||||
|
Business email compromise: urgent-wire / CEO-impersonation
|
||||||
|
language + a request-for-action shape (gift cards, wire,
|
||||||
|
payroll change). Subject + body composite signal.
|
||||||
|
applies_to:
|
||||||
|
- email
|
||||||
|
match:
|
||||||
|
kind: lifter:email_bec
|
||||||
|
subject_keywords:
|
||||||
|
- urgent
|
||||||
|
- wire
|
||||||
|
- invoice
|
||||||
|
- payroll
|
||||||
|
- gift card
|
||||||
|
body_action_keywords:
|
||||||
|
- send
|
||||||
|
- transfer
|
||||||
|
- update banking
|
||||||
|
- confidential
|
||||||
|
emits:
|
||||||
|
- tactic: TA0001
|
||||||
|
technique_id: T1566
|
||||||
|
sub_technique_id: T1566.003
|
||||||
|
confidence: 0.85
|
||||||
|
evidence_fields:
|
||||||
|
- subject
|
||||||
|
- matched_subject_kw
|
||||||
|
- matched_body_kw
|
||||||
23
rules/ttp/R0048.yaml
Normal file
23
rules/ttp/R0048.yaml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
rule_id: R0048
|
||||||
|
rule_version: 1
|
||||||
|
name: encoded_payload_in_body
|
||||||
|
description: |
|
||||||
|
Base64-encoded blob ≥ N bytes embedded in the email body —
|
||||||
|
classic obfuscated-payload smuggling.
|
||||||
|
applies_to:
|
||||||
|
- email
|
||||||
|
match:
|
||||||
|
kind: lifter:email_encoded_payload
|
||||||
|
min_bytes: 4096
|
||||||
|
encoding: base64
|
||||||
|
emits:
|
||||||
|
- tactic: TA0011
|
||||||
|
technique_id: T1071
|
||||||
|
sub_technique_id: T1071.003
|
||||||
|
confidence: 0.85
|
||||||
|
- tactic: TA0005
|
||||||
|
technique_id: T1027
|
||||||
|
confidence: 0.9
|
||||||
|
evidence_fields:
|
||||||
|
- encoded_bytes
|
||||||
|
- decoded_preview
|
||||||
54
tests/ttp/rule_precision/test_email_rules.py
Normal file
54
tests/ttp/rule_precision/test_email_rules.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""R0041-R0048 — email cohort.
|
||||||
|
|
||||||
|
EmailLifter (E.3.12) consumes these by rule_id. The v0
|
||||||
|
:class:`RuleEngine` cannot parse SMTP envelopes, walk attachment
|
||||||
|
trees, or compose header / body / attachment signals — so these
|
||||||
|
rules are inert under the regex matcher.
|
||||||
|
|
||||||
|
Asserts each YAML compiles, none fire from the v0 engine, and a
|
||||||
|
strict-xfail precision case that flips green when E.3.12 lands.
|
||||||
|
"""
|
||||||
|
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(41, 49)]
|
||||||
|
|
||||||
|
|
||||||
|
@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("email"):
|
||||||
|
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.12 (EmailLifter)")
|
||||||
|
def test_email_rule_precision(rule_id: str) -> None:
|
||||||
|
pytest.fail(f"{rule_id}: EmailLifter not yet shipped (E.3.12)")
|
||||||
Reference in New Issue
Block a user