1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
//! Native event-signature → MITRE ATT&CK technique knowledge.
//!
//! Pure facts: "Windows event ID *X* (optionally with logon type *Y*) is
//! consistent with ATT&CK technique *Z* under tactic *W*." No detection logic,
//! no severity judgment, no thresholds, no I/O — those are analyzer decisions
//! and live in the tool that *reads* this table.
//!
//! This is the **behavioral-signature** refinement layer, complementary to the
//! broad [`crate::eventids`] enrichment table: it is logon-type aware (so a
//! type-10 RDP logon resolves to the specific lateral-movement technique
//! `T1021.001` rather than the generic `T1078`) and it carries the ATT&CK
//! `tactic`, which `eventids` does not. Findings derived from it are
//! observations *consistent with* the named technique — never a verdict.
/// A native event signature mapped to the ATT&CK technique it is consistent
/// with. `logon_type == None` matches any logon type (or events that have none).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct NativeEventTechnique {
/// Windows event ID, e.g. `4624`, `7045`.
pub event_id: u32,
/// Required logon type (e.g. `10` = RemoteInteractive/RDP), or `None` for any.
pub logon_type: Option<u32>,
/// ATT&CK technique ID, e.g. `"T1021.001"`.
pub technique: &'static str,
/// ATT&CK tactic in lowercase snake form, e.g. `"initial_access"`.
pub tactic: &'static str,
/// Short forensic description of the signature.
pub description: &'static str,
}
/// Per-event behavioral signatures. The first entry whose `event_id` matches
/// and whose `logon_type` constraint is satisfied wins (see [`technique_for`]).
pub static NATIVE_EVENT_TECHNIQUES: &[NativeEventTechnique] = &[
NativeEventTechnique {
event_id: 4624,
logon_type: Some(10),
technique: "T1021.001",
tactic: "initial_access",
description: "Type-10 (RemoteInteractive/RDP) successful logon",
},
NativeEventTechnique {
event_id: 7045,
logon_type: None,
technique: "T1543.003",
tactic: "persistence",
description: "New Windows service installed (7045)",
},
NativeEventTechnique {
event_id: 4672,
logon_type: None,
technique: "T1078",
tactic: "privilege_escalation",
description: "Privileged (admin-equivalent) logon assigned (4672)",
},
];
/// The technique a *burst* of failed logons (4625) is consistent with. The
/// burst threshold is a tuning decision the analyzer owns, not a fact, so it is
/// deliberately absent here.
pub const FAILED_LOGON_BURST: NativeEventTechnique = NativeEventTechnique {
event_id: 4625,
logon_type: None,
technique: "T1110",
tactic: "initial_access",
description: "Repeated failed logons (4625) — consistent with password brute force",
};
/// Look up the behavioral technique consistent with a single event signature.
///
/// Returns the first matching entry: `event_id` must match and, if the entry
/// constrains `logon_type`, the supplied `logon_type` must equal it.
#[must_use]
pub fn technique_for(
event_id: u32,
logon_type: Option<u32>,
) -> Option<&'static NativeEventTechnique> {
NATIVE_EVENT_TECHNIQUES
.iter()
.find(|t| t.event_id == event_id && t.logon_type.map_or(true, |lt| logon_type == Some(lt)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rdp_type_10_resolves_to_t1021_001() {
let t = technique_for(4624, Some(10)).expect("4624 type-10 must resolve");
assert_eq!(t.technique, "T1021.001");
assert_eq!(t.tactic, "initial_access");
}
#[test]
fn console_type_2_logon_is_not_rdp() {
assert!(technique_for(4624, Some(2)).is_none());
}
#[test]
fn service_install_resolves_to_t1543_003() {
let t = technique_for(7045, None).expect("7045 must resolve");
assert_eq!(t.technique, "T1543.003");
assert_eq!(t.tactic, "persistence");
}
#[test]
fn privileged_logon_resolves_to_t1078() {
let t = technique_for(4672, None).expect("4672 must resolve");
assert_eq!(t.technique, "T1078");
assert_eq!(t.tactic, "privilege_escalation");
}
#[test]
fn unknown_event_resolves_to_none() {
assert!(technique_for(4634, None).is_none());
}
#[test]
fn failed_logon_burst_is_t1110_initial_access() {
assert_eq!(FAILED_LOGON_BURST.event_id, 4625);
assert_eq!(FAILED_LOGON_BURST.technique, "T1110");
assert_eq!(FAILED_LOGON_BURST.tactic, "initial_access");
}
}