kql_panopticon/pack/
processing.rs

1//! Processing configuration for pack execution
2//!
3//! Processing runs after acquisition completes, transforming and analyzing
4//! the collected data. Currently supports scoring; future versions may add
5//! Polars-based transformations.
6
7use serde::{Deserialize, Serialize};
8
9/// Processing configuration
10///
11/// Contains processing steps that run after acquisition.
12/// Results are available in reporting as `{{ processing.step_name.* }}`.
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct Processing {
15    /// Processing steps
16    #[serde(default)]
17    pub steps: Vec<ProcessingStep>,
18}
19
20impl Processing {
21    /// Create empty processing config
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Check if processing has any steps
27    pub fn is_empty(&self) -> bool {
28        self.steps.is_empty()
29    }
30
31    /// Get step by name
32    pub fn get_step(&self, name: &str) -> Option<&ProcessingStep> {
33        self.steps.iter().find(|s| s.name == name)
34    }
35}
36
37/// A single processing step
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct ProcessingStep {
40    /// Step name (unique identifier, used as `processing.{name}` in templates)
41    pub name: String,
42
43    /// Processing step type and configuration
44    #[serde(flatten)]
45    pub config: ProcessingStepConfig,
46
47    /// Condition for executing this step
48    #[serde(default)]
49    pub when: Option<String>,
50}
51
52/// Processing step type configuration
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(tag = "type", rename_all = "lowercase")]
55pub enum ProcessingStepConfig {
56    /// Risk scoring based on indicators and thresholds
57    Scoring(ScoringConfig),
58    // Future: Polars, Aggregate, etc.
59}
60
61/// Scoring configuration
62///
63/// Evaluates indicators against step data to produce a risk score.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct ScoringConfig {
66    /// Weighted indicators that contribute to the score
67    #[serde(default)]
68    pub indicators: Vec<ScoringIndicator>,
69
70    /// Score thresholds that determine risk level
71    #[serde(default)]
72    pub thresholds: Vec<ScoringThreshold>,
73}
74
75/// A scoring indicator
76///
77/// When the condition matches, the weight is added to the total score.
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct ScoringIndicator {
80    /// Indicator name
81    pub name: String,
82
83    /// Condition expression (e.g., "step_name.any(column > 10)")
84    pub condition: String,
85
86    /// Weight (positive = risk, negative = benign)
87    pub weight: i32,
88
89    /// Human-readable description
90    #[serde(default)]
91    pub description: Option<String>,
92}
93
94/// A score threshold
95///
96/// Defines risk levels based on accumulated score.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct ScoringThreshold {
99    /// Level name (e.g., "CRITICAL", "HIGH", "MEDIUM", "LOW")
100    pub level: String,
101
102    /// Minimum score for this level
103    pub min_score: i32,
104
105    /// Summary message template (supports Tera)
106    #[serde(default)]
107    pub summary: Option<String>,
108
109    /// Recommendation template (supports Tera)
110    #[serde(default)]
111    pub recommendation: Option<String>,
112}
113
114/// Result of scoring execution
115#[derive(Debug, Clone, Default, Serialize, Deserialize)]
116pub struct ScoringResult {
117    /// Total accumulated score
118    pub score: i32,
119
120    /// Matched risk level
121    pub level: String,
122
123    /// Indicators that matched
124    pub matched_indicators: Vec<MatchedIndicator>,
125
126    /// Summary from threshold (rendered)
127    pub summary: Option<String>,
128
129    /// Recommendation from threshold (rendered)
130    pub recommendation: Option<String>,
131}
132
133/// An indicator that matched during scoring
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct MatchedIndicator {
136    /// Indicator name
137    pub name: String,
138
139    /// Weight that was added
140    pub weight: i32,
141
142    /// Description
143    pub description: Option<String>,
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_empty_processing() {
152        let proc = Processing::new();
153        assert!(proc.is_empty());
154    }
155
156    #[test]
157    fn test_scoring_config_deserialize() {
158        let yaml = r#"
159name: risk_score
160type: scoring
161indicators:
162  - name: high_failures
163    condition: "signins.any(FailedCount > 10)"
164    weight: 25
165thresholds:
166  - level: HIGH
167    min_score: 50
168"#;
169        let step: ProcessingStep = serde_yaml::from_str(yaml).unwrap();
170        assert_eq!(step.name, "risk_score");
171
172        match step.config {
173            ProcessingStepConfig::Scoring(cfg) => {
174                assert_eq!(cfg.indicators.len(), 1);
175                assert_eq!(cfg.thresholds.len(), 1);
176            }
177        }
178    }
179}