use crate::catalog::{ArtifactType, HiveTarget, TriagePriority, CATALOG};
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",
HiveTarget::None => "",
}
}
fn sanitize_id(id: &str) -> String {
id.replace(['-', '.'], "_")
}
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",
};
let meaning = artifact.meaning.replace('"', "'");
let meaning_short: String = meaning.chars().take(120).collect();
let (strings_block, condition_var) = match artifact.artifact_type {
ArtifactType::RegistryKey | ArtifactType::RegistryValue => {
let full_path = if let Some(hive) = artifact.hive {
let prefix = hive_prefix(hive);
if artifact.key_path.is_empty() {
prefix.to_string()
} 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")
}
ArtifactType::File | ArtifactType::Directory => {
let path = artifact.file_path.unwrap_or(artifact.key_path);
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")
}
ArtifactType::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")
}
ArtifactType::MemoryRegion
| ArtifactType::LiveResponse
| ArtifactType::DatabaseEntry
| ArtifactType::EseDatabase => {
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)
}
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 {
use super::*;
#[test]
fn prefetch_generates_yara_rule() {
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"
);
}
}
#[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();
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") {
assert!(
rule.contains("$key_path") || rule.contains("Run"),
"Registry artifact should include key path in YARA rule"
);
}
}
}