use super::{Coverage, parent_technique};
pub(crate) use crate::commands::navigator::to_pretty_json;
use crate::commands::navigator::{DOMAIN, Gradient, Layer, NavTechnique, Versions};
const MAX_COMMENT_TITLES: usize = 8;
pub(crate) fn build_layer(coverage: &Coverage, name: &str) -> Layer {
let mut max_score = 1u64;
let mut techniques = Vec::with_capacity(coverage.techniques.len());
for (id, agg) in &coverage.techniques {
let score = agg.rule_count() as u64;
max_score = max_score.max(score);
let show_subtechniques = !id.contains('.')
&& coverage
.techniques
.keys()
.any(|k| parent_technique(k) == Some(id.as_str()));
techniques.push(NavTechnique {
technique_id: id.clone(),
score,
comment: comment_for(&agg.titles()),
enabled: true,
show_subtechniques,
});
}
Layer {
name: name.to_string(),
versions: Versions::current(),
domain: DOMAIN,
description: format!(
"Rule coverage generated by rsigma; score = number of rules per technique ({} techniques).",
coverage.techniques.len()
),
sorting: 3, hide_disabled: false,
gradient: Gradient {
colors: vec!["#ffffcc", "#fd8d3c", "#bd0026"],
min_value: 0,
max_value: max_score,
},
techniques,
}
}
fn comment_for(titles: &[String]) -> String {
let total = titles.len();
let shown: Vec<&str> = titles
.iter()
.take(MAX_COMMENT_TITLES)
.map(|s| s.as_str())
.collect();
let mut comment = shown.join(", ");
if total > MAX_COMMENT_TITLES {
comment.push_str(&format!(", (+{} more)", total - MAX_COMMENT_TITLES));
}
comment
}
#[cfg(test)]
mod tests {
use super::*;
use crate::commands::coverage::Coverage;
fn coverage_from(yaml: &str) -> Coverage {
let collection = rsigma_parser::parse_sigma_yaml(yaml).expect("rules parse");
Coverage::from_collection(&collection)
}
#[test]
fn layer_scores_by_rule_count_and_expands_parents() {
let yaml = r#"
title: A
id: 00000000-0000-0000-0000-0000000000a1
logsource: {category: process_creation, product: windows}
detection:
sel: {Image|endswith: '\a.exe'}
condition: sel
tags:
- attack.t1059
- attack.execution
---
title: B
id: 00000000-0000-0000-0000-0000000000a2
logsource: {category: process_creation, product: windows}
detection:
sel: {Image|endswith: '\b.exe'}
condition: sel
tags:
- attack.t1059
- attack.t1059.001
"#;
let cov = coverage_from(yaml);
let layer = build_layer(&cov, "test");
let t1059 = layer
.techniques
.iter()
.find(|t| t.technique_id == "T1059")
.expect("T1059 present");
assert_eq!(t1059.score, 2); assert!(t1059.show_subtechniques); assert_eq!(layer.gradient.max_value, 2);
}
#[test]
fn layer_serializes_format_4_5_header() {
let cov = coverage_from(
r#"
title: A
id: 00000000-0000-0000-0000-0000000000a1
logsource: {category: test, product: test}
detection: {sel: {Image: x}, condition: sel}
tags: [attack.t1003]
"#,
);
let json = to_pretty_json(&build_layer(&cov, "n"));
assert!(json.contains("\"layer\": \"4.5\""));
assert!(json.contains("\"techniqueID\": \"T1003\""));
}
}