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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
//! YARA rule template generator.
//!
//! Generates YARA rule skeletons from catalog artifact metadata and
//! container signatures. Output is valid YARA syntax that analysts
//! can refine and deploy.
use crate::catalog::{ArtifactLocation, HiveTarget, TriagePriority, CATALOG};
/// Return the Windows registry root prefix string for a given hive target.
fn hive_prefix(hive: HiveTarget) -> &'static str {
match hive {
HiveTarget::HklmSystem => r"HKEY_LOCAL_MACHINE\SYSTEM",
HiveTarget::HklmSoftware => r"HKEY_LOCAL_MACHINE\SOFTWARE",
HiveTarget::HklmSam => r"HKEY_LOCAL_MACHINE\SAM",
HiveTarget::HklmSecurity => r"HKEY_LOCAL_MACHINE\SECURITY",
HiveTarget::NtUser => r"HKEY_CURRENT_USER",
HiveTarget::UsrClass => r"HKEY_CURRENT_USER\Software\Classes",
HiveTarget::Amcache => {
r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatCache"
}
HiveTarget::Bcd => r"HKEY_LOCAL_MACHINE\BCD00000000",
// `None` and any future `#[non_exhaustive]` HiveTarget variant have no
// registry root prefix.
_ => "",
}
}
/// Sanitize an artifact id into a valid YARA rule name identifier.
///
/// YARA identifiers may contain letters, digits, and underscores. Hyphens and
/// dots are replaced with underscores.
fn sanitize_id(id: &str) -> String {
id.replace(['-', '.'], "_")
}
/// Generate a YARA rule skeleton for a catalog artifact.
///
/// Uses the artifact's key_path, file_path, and MITRE techniques to produce a
/// valid YARA rule template that analysts can refine and deploy.
///
/// Returns `None` only if the artifact is not found in the catalog.
pub fn yara_rule_template(artifact_id: &str) -> Option<String> {
let artifact = CATALOG.by_id(artifact_id)?;
let rule_name = sanitize_id(artifact.id);
let mitre = artifact
.mitre_techniques
.first()
.copied()
.unwrap_or("(none)");
let priority = match artifact.triage_priority {
TriagePriority::Critical => "critical",
TriagePriority::High => "high",
TriagePriority::Medium => "medium",
TriagePriority::Low => "low",
// Forward-compat for future `#[non_exhaustive]` TriagePriority variants.
_ => "unknown", // cov:unreachable: forward-compat for future #[non_exhaustive] TriagePriority variants
};
// Truncate meaning and sanitize quotes so the YARA string literal stays valid.
let meaning = artifact.meaning.replace('"', "'");
let meaning_short: String = meaning.chars().take(120).collect();
// Build the strings block depending on artifact type.
let (strings_block, condition_var) = match artifact.artifact_type {
ArtifactLocation::RegistryKey | ArtifactLocation::RegistryValue => {
// Construct full registry path: HKEY_... \ key_path
let full_path = if let Some(hive) = artifact.hive {
let prefix = hive_prefix(hive);
if artifact.key_path.is_empty() {
prefix.to_string() // cov:unreachable: no shipped registry artifact has a hive set with an empty key_path
} else {
format!(r"{}\{}", prefix, artifact.key_path)
}
} else {
artifact.key_path.to_string()
};
let block =
format!(" strings:\n $key_path = \"{full_path}\" nocase wide ascii");
(block, "$key_path")
}
ArtifactLocation::File | ArtifactLocation::Directory => {
// Prefer file_path; fall back to key_path.
let path = artifact.file_path.unwrap_or(artifact.key_path);
// Use the filename portion for a compact, focused string match.
let filename = path.rsplit(['\\', '/']).next().unwrap_or(path);
let target = if filename.is_empty() { path } else { filename };
let block =
format!(" strings:\n $file_path = \"{target}\" nocase wide ascii");
(block, "$file_path")
}
ArtifactLocation::EventLog => {
let path = artifact.file_path.unwrap_or(artifact.key_path);
let filename = path.rsplit(['\\', '/']).next().unwrap_or(path);
let block =
format!(" strings:\n $evtx_file = \"{filename}\" nocase wide ascii");
(block, "$evtx_file")
}
// MemoryRegion / LiveResponse / DatabaseEntry / EseDatabase, plus any
// future `#[non_exhaustive]` ArtifactLocation variant: fall back to a
// generic name-based string match.
_ => {
let block = format!(
" strings:\n $artifact = \"{}\" nocase wide ascii",
artifact.name
);
(block, "$artifact")
}
};
let rule = format!(
"rule {rule_name}\n{{\n meta:\n description = \"{meaning_short}\"\n mitre = \"{mitre}\"\n triage_priority = \"{priority}\"\n{strings_block}\n condition:\n {condition_var}\n}}"
);
Some(rule)
}
/// Generate YARA rules for all catalog artifacts.
///
/// Returns a vec of `(artifact_id, rule_string)` pairs. Every catalog entry
/// produces a rule; the caller receives the artifact id alongside the rule so
/// results can be filtered or indexed by id.
pub fn all_yara_templates() -> Vec<(&'static str, String)> {
CATALOG
.list()
.iter()
.filter_map(|d| yara_rule_template(d.id).map(|rule| (d.id, rule)))
.collect()
}
#[cfg(test)]
mod tests {
#[test]
fn hive_prefix_bcd() {
use crate::catalog::HiveTarget;
assert_eq!(
hive_prefix(HiveTarget::Bcd),
r"HKEY_LOCAL_MACHINE\BCD00000000"
);
}
use super::*;
#[test]
fn prefetch_generates_yara_rule() {
// prefetch_file should have a container signature with magic bytes
if let Some(rule) = yara_rule_template("prefetch_file") {
assert!(
rule.contains("rule prefetch_file"),
"Rule name should be prefetch_file"
);
assert!(rule.contains("meta:"), "Should have meta block");
assert!(rule.contains("condition:"), "Should have condition block");
assert!(
rule.contains("T1059") || rule.contains("mitre"),
"Should reference MITRE"
);
} // cov:unreachable: yara_rule_template("prefetch_file") always resolves in the shipped catalog
// It's OK if prefetch has no container signature — test that it doesn't panic
}
#[test]
fn nonexistent_artifact_returns_none() {
assert!(yara_rule_template("this_does_not_exist").is_none());
}
#[test]
fn all_templates_returns_nonempty() {
let templates = all_yara_templates();
// At minimum, registry key artifacts with key_path should produce templates
assert!(
!templates.is_empty(),
"all_yara_templates() should return at least some templates"
);
}
#[test]
fn generated_rule_has_valid_structure() {
let templates = all_yara_templates();
for (id, rule) in &templates {
assert!(
rule.contains("rule "),
"Rule for '{id}' missing 'rule' keyword"
);
assert!(
rule.contains("meta:"),
"Rule for '{id}' missing 'meta:' block"
);
assert!(
rule.contains("condition:"),
"Rule for '{id}' missing 'condition:' block"
);
}
}
#[test]
fn rule_name_is_valid_identifier() {
let templates = all_yara_templates();
for (id, rule) in &templates {
let expected_name = id.replace(['-', '.'], "_");
assert!(
rule.contains(&format!("rule {expected_name}")),
"Rule for '{id}' should use identifier '{expected_name}'"
);
}
}
#[test]
fn run_key_generates_registry_string() {
if let Some(rule) = yara_rule_template("run_key_hklm") {
// Registry artifacts should have key_path strings
assert!(
rule.contains("$key_path") || rule.contains("Run"),
"Registry artifact should include key path in YARA rule"
);
} // cov:unreachable: yara_rule_template("run_key_hklm") always resolves in the shipped catalog
}
}