1use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum AnomalySignalType {
22 PayloadSizeHigh,
24 PayloadSizeLow,
26 UnexpectedParam,
28 MissingExpectedParam,
30 ParamValueAnomaly,
32 ContentTypeMismatch,
34 RateBurst,
36 ParamCountAnomaly,
38
39 HeaderMissingRequired,
44 HeaderUnexpected,
46 HeaderValueAnomaly,
48 HeaderEntropyAnomaly,
50 HeaderLengthAnomaly,
52
53 AbnormalErrorRate,
58}
59
60impl AnomalySignalType {
61 pub fn as_str(&self) -> &'static str {
63 match self {
64 Self::PayloadSizeHigh => "payload_size_high",
65 Self::PayloadSizeLow => "payload_size_low",
66 Self::UnexpectedParam => "unexpected_param",
67 Self::MissingExpectedParam => "missing_expected_param",
68 Self::ParamValueAnomaly => "param_value_anomaly",
69 Self::ContentTypeMismatch => "content_type_mismatch",
70 Self::RateBurst => "rate_burst",
71 Self::ParamCountAnomaly => "param_count_anomaly",
72 Self::HeaderMissingRequired => "header_missing_required",
74 Self::HeaderUnexpected => "header_unexpected",
75 Self::HeaderValueAnomaly => "header_value_anomaly",
76 Self::HeaderEntropyAnomaly => "header_entropy_anomaly",
77 Self::HeaderLengthAnomaly => "header_length_anomaly",
78 Self::AbnormalErrorRate => "abnormal_error_rate",
80 }
81 }
82
83 pub fn default_severity(&self) -> u8 {
90 match self {
91 Self::PayloadSizeHigh => 5,
92 Self::PayloadSizeLow => 2,
93 Self::UnexpectedParam => 3,
94 Self::MissingExpectedParam => 2,
95 Self::ParamValueAnomaly => 4,
96 Self::ContentTypeMismatch => 5,
97 Self::RateBurst => 6,
98 Self::ParamCountAnomaly => 3,
99 Self::HeaderMissingRequired => 4, Self::HeaderUnexpected => 2, Self::HeaderValueAnomaly => 5, Self::HeaderEntropyAnomaly => 6, Self::HeaderLengthAnomaly => 4, Self::AbnormalErrorRate => 5, }
109 }
110
111 pub fn default_risk(&self) -> u16 {
118 match self {
119 Self::PayloadSizeHigh => 15,
120 Self::PayloadSizeLow => 5,
121 Self::UnexpectedParam => 8,
122 Self::MissingExpectedParam => 5,
123 Self::ParamValueAnomaly => 12,
124 Self::ContentTypeMismatch => 15,
125 Self::RateBurst => 20,
126 Self::ParamCountAnomaly => 8,
127 Self::HeaderMissingRequired => 10,
129 Self::HeaderUnexpected => 5,
130 Self::HeaderValueAnomaly => 15,
131 Self::HeaderEntropyAnomaly => 20,
132 Self::HeaderLengthAnomaly => 10,
133 Self::AbnormalErrorRate => 15, }
136 }
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct AnomalySignal {
146 pub signal_type: AnomalySignalType,
148 pub severity: u8,
150 pub detail: String,
152}
153
154impl AnomalySignal {
155 #[inline]
157 pub fn new(signal_type: AnomalySignalType, severity: u8, detail: String) -> Self {
158 Self {
159 signal_type,
160 severity: severity.min(10),
161 detail,
162 }
163 }
164
165 #[inline]
167 pub fn with_default_severity(signal_type: AnomalySignalType, detail: String) -> Self {
168 Self::new(signal_type, signal_type.default_severity(), detail)
169 }
170}
171
172#[derive(Debug, Clone, Default, Serialize, Deserialize)]
178pub struct AnomalyResult {
179 pub total_score: f64,
181 pub signals: Vec<AnomalySignal>,
183}
184
185impl AnomalyResult {
186 #[inline]
188 pub fn none() -> Self {
189 Self {
190 total_score: 0.0,
191 signals: Vec::new(),
192 }
193 }
194
195 #[inline]
197 pub fn new() -> Self {
198 Self {
199 total_score: 0.0,
200 signals: Vec::with_capacity(4), }
202 }
203
204 #[inline]
206 pub fn add(&mut self, signal_type: AnomalySignalType, severity: u8, detail: String) {
207 self.total_score += severity as f64;
208 self.signals
209 .push(AnomalySignal::new(signal_type, severity, detail));
210 }
211
212 #[inline]
214 pub fn add_signal(&mut self, signal: AnomalySignal) {
215 self.total_score += signal.severity as f64;
216 self.signals.push(signal);
217 }
218
219 #[inline]
221 pub fn has_anomalies(&self) -> bool {
222 !self.signals.is_empty()
223 }
224
225 #[inline]
227 pub fn signal_count(&self) -> usize {
228 self.signals.len()
229 }
230
231 pub fn normalize(&mut self) {
233 self.total_score = self.total_score.clamp(-10.0, 10.0);
234 }
235
236 pub fn max_severity(&self) -> u8 {
238 self.signals.iter().map(|s| s.severity).max().unwrap_or(0)
239 }
240
241 pub fn signal_types(&self) -> Vec<AnomalySignalType> {
243 self.signals.iter().map(|s| s.signal_type).collect()
244 }
245
246 pub fn merge(&mut self, other: AnomalyResult) {
248 self.total_score += other.total_score;
249 self.signals.extend(other.signals);
250 }
251}
252
253#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn test_anomaly_signal_type_as_str() {
263 assert_eq!(
264 AnomalySignalType::PayloadSizeHigh.as_str(),
265 "payload_size_high"
266 );
267 assert_eq!(AnomalySignalType::RateBurst.as_str(), "rate_burst");
268 }
269
270 #[test]
271 fn test_anomaly_signal_creation() {
272 let signal = AnomalySignal::new(
273 AnomalySignalType::PayloadSizeHigh,
274 7,
275 "Large payload detected".to_string(),
276 );
277 assert_eq!(signal.signal_type, AnomalySignalType::PayloadSizeHigh);
278 assert_eq!(signal.severity, 7);
279 }
280
281 #[test]
282 fn test_anomaly_signal_severity_clamped() {
283 let signal = AnomalySignal::new(
284 AnomalySignalType::RateBurst,
285 15, "Test".to_string(),
287 );
288 assert_eq!(signal.severity, 10); }
290
291 #[test]
292 fn test_anomaly_result_empty() {
293 let result = AnomalyResult::none();
294 assert!(!result.has_anomalies());
295 assert_eq!(result.total_score, 0.0);
296 assert_eq!(result.signal_count(), 0);
297 }
298
299 #[test]
300 fn test_anomaly_result_add() {
301 let mut result = AnomalyResult::new();
302 result.add(
303 AnomalySignalType::UnexpectedParam,
304 3,
305 "New param".to_string(),
306 );
307 result.add(AnomalySignalType::RateBurst, 6, "High rate".to_string());
308
309 assert!(result.has_anomalies());
310 assert_eq!(result.signal_count(), 2);
311 assert_eq!(result.total_score, 9.0);
312 }
313
314 #[test]
315 fn test_anomaly_result_normalize() {
316 let mut result = AnomalyResult::new();
317 for _ in 0..5 {
318 result.add(AnomalySignalType::RateBurst, 6, "Test".to_string());
319 }
320 assert_eq!(result.total_score, 30.0);
321
322 result.normalize();
323 assert_eq!(result.total_score, 10.0);
324 }
325
326 #[test]
327 fn test_anomaly_result_max_severity() {
328 let mut result = AnomalyResult::new();
329 result.add(AnomalySignalType::PayloadSizeLow, 2, "Small".to_string());
330 result.add(AnomalySignalType::RateBurst, 8, "Burst".to_string());
331 result.add(AnomalySignalType::UnexpectedParam, 3, "Param".to_string());
332
333 assert_eq!(result.max_severity(), 8);
334 }
335
336 #[test]
337 fn test_anomaly_result_signal_types() {
338 let mut result = AnomalyResult::new();
339 result.add(AnomalySignalType::PayloadSizeHigh, 5, "Test".to_string());
340 result.add(AnomalySignalType::RateBurst, 6, "Test".to_string());
341
342 let types = result.signal_types();
343 assert!(types.contains(&AnomalySignalType::PayloadSizeHigh));
344 assert!(types.contains(&AnomalySignalType::RateBurst));
345 }
346
347 #[test]
348 fn test_anomaly_result_merge() {
349 let mut result1 = AnomalyResult::new();
350 result1.add(AnomalySignalType::PayloadSizeHigh, 5, "Test1".to_string());
351
352 let mut result2 = AnomalyResult::new();
353 result2.add(AnomalySignalType::RateBurst, 6, "Test2".to_string());
354
355 result1.merge(result2);
356
357 assert_eq!(result1.signal_count(), 2);
358 assert_eq!(result1.total_score, 11.0);
359 }
360
361 #[test]
362 fn test_default_severity() {
363 assert_eq!(AnomalySignalType::PayloadSizeHigh.default_severity(), 5);
364 assert_eq!(AnomalySignalType::RateBurst.default_severity(), 6);
365 assert_eq!(AnomalySignalType::PayloadSizeLow.default_severity(), 2);
366 }
367
368 #[test]
369 fn test_signal_with_default_severity() {
370 let signal = AnomalySignal::with_default_severity(
371 AnomalySignalType::RateBurst,
372 "Test burst".to_string(),
373 );
374 assert_eq!(signal.severity, 6);
375 }
376
377 #[test]
382 fn test_header_anomaly_signal_types_as_str() {
383 assert_eq!(
384 AnomalySignalType::HeaderMissingRequired.as_str(),
385 "header_missing_required"
386 );
387 assert_eq!(
388 AnomalySignalType::HeaderUnexpected.as_str(),
389 "header_unexpected"
390 );
391 assert_eq!(
392 AnomalySignalType::HeaderValueAnomaly.as_str(),
393 "header_value_anomaly"
394 );
395 assert_eq!(
396 AnomalySignalType::HeaderEntropyAnomaly.as_str(),
397 "header_entropy_anomaly"
398 );
399 assert_eq!(
400 AnomalySignalType::HeaderLengthAnomaly.as_str(),
401 "header_length_anomaly"
402 );
403 }
404
405 #[test]
406 fn test_header_anomaly_default_severities() {
407 assert_eq!(
408 AnomalySignalType::HeaderMissingRequired.default_severity(),
409 4
410 );
411 assert_eq!(AnomalySignalType::HeaderUnexpected.default_severity(), 2);
412 assert_eq!(AnomalySignalType::HeaderValueAnomaly.default_severity(), 5);
413 assert_eq!(
414 AnomalySignalType::HeaderEntropyAnomaly.default_severity(),
415 6
416 );
417 assert_eq!(AnomalySignalType::HeaderLengthAnomaly.default_severity(), 4);
418 }
419
420 #[test]
421 fn test_header_anomaly_default_risks() {
422 assert_eq!(AnomalySignalType::HeaderMissingRequired.default_risk(), 10);
423 assert_eq!(AnomalySignalType::HeaderUnexpected.default_risk(), 5);
424 assert_eq!(AnomalySignalType::HeaderValueAnomaly.default_risk(), 15);
425 assert_eq!(AnomalySignalType::HeaderEntropyAnomaly.default_risk(), 20);
426 assert_eq!(AnomalySignalType::HeaderLengthAnomaly.default_risk(), 10);
427 }
428
429 #[test]
430 fn test_header_anomaly_in_result() {
431 let mut result = AnomalyResult::new();
432 result.add(
433 AnomalySignalType::HeaderMissingRequired,
434 4,
435 "Missing Authorization header".to_string(),
436 );
437 result.add(
438 AnomalySignalType::HeaderEntropyAnomaly,
439 6,
440 "High entropy in X-Token".to_string(),
441 );
442
443 assert!(result.has_anomalies());
444 assert_eq!(result.signal_count(), 2);
445 assert_eq!(result.total_score, 10.0); let types = result.signal_types();
448 assert!(types.contains(&AnomalySignalType::HeaderMissingRequired));
449 assert!(types.contains(&AnomalySignalType::HeaderEntropyAnomaly));
450 }
451}