1use serde::{Deserialize, Deserializer, Serialize, Serializer};
15use std::collections::BTreeMap;
16use thiserror::Error;
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20pub struct BehavioralSignature {
21 pub signature_id: String,
22 pub signal_type: String,
23 pub indicators: Vec<BehaviorIndicator>,
24 #[serde(with = "positive_unit_interval_f32")]
26 pub threshold: f32,
27 #[serde(with = "unit_interval_f32")]
29 pub confidence: f32,
30}
31
32impl BehavioralSignature {
33 pub fn validate(&self) -> Result<(), BehavioralSignatureValidationError> {
35 if self.signature_id.trim().is_empty() {
36 return Err(BehavioralSignatureValidationError::BlankSignatureId);
37 }
38 if self.signal_type.trim().is_empty() {
39 return Err(BehavioralSignatureValidationError::BlankSignalType);
40 }
41 if self.indicators.is_empty() {
42 return Err(BehavioralSignatureValidationError::NoIndicators);
43 }
44 if !normalized(self.threshold) || self.threshold == 0.0 {
45 return Err(BehavioralSignatureValidationError::InvalidThreshold);
46 }
47 if !normalized(self.confidence) {
48 return Err(BehavioralSignatureValidationError::InvalidConfidence);
49 }
50 for (index, indicator) in self.indicators.iter().enumerate() {
51 if !indicator.is_valid() {
52 return Err(BehavioralSignatureValidationError::InvalidIndicator { index });
53 }
54 }
55 Ok(())
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
61pub enum BehavioralSignatureValidationError {
62 #[error("behavioral signature ID must not be blank")]
63 BlankSignatureId,
64 #[error("behavioral signal type must not be blank")]
65 BlankSignalType,
66 #[error("behavioral signature must contain at least one indicator")]
67 NoIndicators,
68 #[error("behavioral signature threshold must be finite and within (0, 1]")]
69 InvalidThreshold,
70 #[error("behavioral signature confidence must be finite and within 0..=1")]
71 InvalidConfidence,
72 #[error("behavioral signature indicator {index} is contradictory or invalid")]
73 InvalidIndicator { index: usize },
74}
75
76#[derive(Debug, Clone, PartialEq)]
78pub struct BehaviorIndicator {
79 pub indicator_type: IndicatorType,
80 pub metric: String,
81 pub operator: ComparisonOperator,
82 pub value: f32,
83 pub weight: f32,
84}
85
86impl BehaviorIndicator {
87 fn has_wire_safe_numbers(&self) -> bool {
88 let value_is_valid = self.value.is_finite()
89 && self.value >= 0.0
90 && (self.indicator_type != IndicatorType::Consistency || normalized(self.value));
91 value_is_valid && normalized(self.weight) && self.weight > 0.0
92 }
93
94 fn is_valid(&self) -> bool {
95 let metric_matches_type = matches!(
96 (self.indicator_type, self.metric.as_str()),
97 (IndicatorType::Timing, "response_time")
98 | (IndicatorType::Size, "response_size")
99 | (IndicatorType::Pattern, "unique_patterns")
100 | (IndicatorType::Error, "error_keywords")
101 | (IndicatorType::Consistency, "consistency")
102 );
103 metric_matches_type && self.has_wire_safe_numbers()
104 }
105}
106
107const BEHAVIOR_INDICATOR_WIRE_ERROR: &str =
108 "behavior indicator values must be finite and within their documented ranges";
109
110#[derive(Serialize)]
111struct BehaviorIndicatorRef<'a> {
112 indicator_type: IndicatorType,
113 metric: &'a str,
114 operator: ComparisonOperator,
115 value: f32,
116 weight: f32,
117}
118
119#[derive(Deserialize)]
120struct BehaviorIndicatorWire {
121 indicator_type: IndicatorType,
122 metric: String,
123 operator: ComparisonOperator,
124 value: f32,
125 weight: f32,
126}
127
128impl Serialize for BehaviorIndicator {
129 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
130 where
131 S: Serializer,
132 {
133 if !self.has_wire_safe_numbers() {
134 return Err(serde::ser::Error::custom(BEHAVIOR_INDICATOR_WIRE_ERROR));
135 }
136 BehaviorIndicatorRef {
137 indicator_type: self.indicator_type,
138 metric: &self.metric,
139 operator: self.operator,
140 value: self.value,
141 weight: self.weight,
142 }
143 .serialize(serializer)
144 }
145}
146
147impl<'de> Deserialize<'de> for BehaviorIndicator {
148 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
149 where
150 D: Deserializer<'de>,
151 {
152 let wire = BehaviorIndicatorWire::deserialize(deserializer)?;
153 let indicator = Self {
154 indicator_type: wire.indicator_type,
155 metric: wire.metric,
156 operator: wire.operator,
157 value: wire.value,
158 weight: wire.weight,
159 };
160 if indicator.has_wire_safe_numbers() {
161 Ok(indicator)
162 } else {
163 Err(serde::de::Error::custom(BEHAVIOR_INDICATOR_WIRE_ERROR))
164 }
165 }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
170pub enum IndicatorType {
171 #[serde(rename = "timing")]
172 Timing,
173 #[serde(rename = "size")]
174 Size,
175 #[serde(rename = "pattern")]
176 Pattern,
177 #[serde(rename = "error")]
178 Error,
179 #[serde(rename = "consistency")]
180 Consistency,
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
185pub enum ComparisonOperator {
186 #[serde(rename = "greater_than")]
187 GreaterThan,
188 #[serde(rename = "less_than")]
189 LessThan,
190 #[serde(rename = "equals")]
191 Equals,
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196pub struct BehavioralAnalysisData {
197 #[serde(with = "nonnegative_f32")]
198 pub response_time_ms: f32,
199 pub response_size_bytes: u32,
200 pub error_keywords_count: u32,
201 pub unique_patterns: u32,
202 #[serde(with = "unit_interval_f32")]
203 pub consistency_score: f32,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
208pub enum TechniqueCategory {
209 Encoding,
210 Transformation,
211 Fragmentation,
212 Normalization,
213 Timing,
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
218pub struct TechniqueRecord {
219 pub technique_id: String,
220 pub technique_name: String,
221 pub category: TechniqueCategory,
222 pub description: String,
223 #[serde(with = "unit_interval_f32")]
224 pub reported_effectiveness: f32,
225 #[serde(with = "unit_interval_f32")]
226 pub reported_false_positive_rate: f32,
227 pub method_labels: Vec<String>,
228}
229
230impl TechniqueRecord {
231 pub fn validate(&self) -> Result<(), CatalogRecordError> {
232 if self.technique_id.trim().is_empty() {
233 return Err(CatalogRecordError::BlankId);
234 }
235 if self.technique_name.trim().is_empty() {
236 return Err(CatalogRecordError::BlankName);
237 }
238 if !normalized(self.reported_effectiveness)
239 || !normalized(self.reported_false_positive_rate)
240 {
241 return Err(CatalogRecordError::InvalidReportedScore);
242 }
243 Ok(())
244 }
245}
246
247#[derive(Debug, Default)]
249pub struct TechniqueCatalog {
250 records: BTreeMap<String, TechniqueRecord>,
251}
252
253impl TechniqueCatalog {
254 #[must_use]
255 pub fn new() -> Self {
256 Self::default()
257 }
258
259 pub fn insert(&mut self, record: TechniqueRecord) -> Result<(), CatalogRecordError> {
260 record.validate()?;
261 if self.records.contains_key(&record.technique_id) {
262 return Err(CatalogRecordError::DuplicateId);
263 }
264 self.records.insert(record.technique_id.clone(), record);
265 Ok(())
266 }
267
268 #[must_use]
270 pub fn ranked(&self, category: TechniqueCategory) -> Vec<&TechniqueRecord> {
271 let mut records: Vec<_> = self
272 .records
273 .values()
274 .filter(|record| record.category == category)
275 .collect();
276 records.sort_by(|left, right| {
277 right
278 .reported_effectiveness
279 .total_cmp(&left.reported_effectiveness)
280 .then_with(|| left.technique_id.cmp(&right.technique_id))
281 });
282 records
283 }
284
285 #[must_use]
286 pub fn len(&self) -> usize {
287 self.records.len()
288 }
289
290 #[must_use]
291 pub fn is_empty(&self) -> bool {
292 self.records.is_empty()
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
298pub enum TransformationType {
299 Encoding,
300 Manipulation,
301 Noise,
302 Other,
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
307pub struct TransformationRule {
308 pub rule_id: String,
309 pub target_label: String,
310 pub transformation_label: String,
311 pub transformation_type: TransformationType,
312 #[serde(with = "unit_interval_f32")]
313 pub reported_effectiveness: f32,
314}
315
316impl TransformationRule {
317 pub fn validate(&self) -> Result<(), CatalogRecordError> {
318 if self.rule_id.trim().is_empty() {
319 return Err(CatalogRecordError::BlankId);
320 }
321 if self.target_label.trim().is_empty() || self.transformation_label.trim().is_empty() {
322 return Err(CatalogRecordError::BlankName);
323 }
324 if !normalized(self.reported_effectiveness) {
325 return Err(CatalogRecordError::InvalidReportedScore);
326 }
327 Ok(())
328 }
329}
330
331#[derive(Debug, Default)]
333pub struct TransformationRuleCatalog {
334 records: BTreeMap<String, TransformationRule>,
335}
336
337impl TransformationRuleCatalog {
338 #[must_use]
339 pub fn new() -> Self {
340 Self::default()
341 }
342
343 pub fn insert(&mut self, record: TransformationRule) -> Result<(), CatalogRecordError> {
344 record.validate()?;
345 if self.records.contains_key(&record.rule_id) {
346 return Err(CatalogRecordError::DuplicateId);
347 }
348 self.records.insert(record.rule_id.clone(), record);
349 Ok(())
350 }
351
352 #[must_use]
353 pub fn for_target(&self, target: &str) -> Vec<&TransformationRule> {
354 self.records
355 .values()
356 .filter(|record| record.target_label == target)
357 .collect()
358 }
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
363pub enum CatalogRecordError {
364 #[error("catalog record ID must not be blank")]
365 BlankId,
366 #[error("catalog record name/label must not be blank")]
367 BlankName,
368 #[error("reported scores must be finite and within 0..=1")]
369 InvalidReportedScore,
370 #[error("catalog record ID already exists")]
371 DuplicateId,
372}
373
374fn normalized(value: f32) -> bool {
375 value.is_finite() && (0.0..=1.0).contains(&value)
376}
377
378mod unit_interval_f32 {
379 use serde::{Deserialize, Deserializer, Serializer};
380
381 const ERROR: &str = "score must be finite and within 0..=1";
382
383 pub fn serialize<S>(value: &f32, serializer: S) -> Result<S::Ok, S::Error>
384 where
385 S: Serializer,
386 {
387 if super::normalized(*value) {
388 serializer.serialize_f32(*value)
389 } else {
390 Err(serde::ser::Error::custom(ERROR))
391 }
392 }
393
394 pub fn deserialize<'de, D>(deserializer: D) -> Result<f32, D::Error>
395 where
396 D: Deserializer<'de>,
397 {
398 let value = f32::deserialize(deserializer)?;
399 if super::normalized(value) {
400 Ok(value)
401 } else {
402 Err(serde::de::Error::custom(ERROR))
403 }
404 }
405}
406
407mod positive_unit_interval_f32 {
408 use serde::{Deserialize, Deserializer, Serializer};
409
410 const ERROR: &str = "score must be finite and within (0, 1]";
411
412 pub fn serialize<S>(value: &f32, serializer: S) -> Result<S::Ok, S::Error>
413 where
414 S: Serializer,
415 {
416 if super::normalized(*value) && *value > 0.0 {
417 serializer.serialize_f32(*value)
418 } else {
419 Err(serde::ser::Error::custom(ERROR))
420 }
421 }
422
423 pub fn deserialize<'de, D>(deserializer: D) -> Result<f32, D::Error>
424 where
425 D: Deserializer<'de>,
426 {
427 let value = f32::deserialize(deserializer)?;
428 if super::normalized(value) && value > 0.0 {
429 Ok(value)
430 } else {
431 Err(serde::de::Error::custom(ERROR))
432 }
433 }
434}
435
436mod nonnegative_f32 {
437 use serde::{Deserialize, Deserializer, Serializer};
438
439 const ERROR: &str = "value must be finite and non-negative";
440
441 pub fn serialize<S>(value: &f32, serializer: S) -> Result<S::Ok, S::Error>
442 where
443 S: Serializer,
444 {
445 if value.is_finite() && *value >= 0.0 {
446 serializer.serialize_f32(*value)
447 } else {
448 Err(serde::ser::Error::custom(ERROR))
449 }
450 }
451
452 pub fn deserialize<'de, D>(deserializer: D) -> Result<f32, D::Error>
453 where
454 D: Deserializer<'de>,
455 {
456 let value = f32::deserialize(deserializer)?;
457 if value.is_finite() && value >= 0.0 {
458 Ok(value)
459 } else {
460 Err(serde::de::Error::custom(ERROR))
461 }
462 }
463}
464
465#[cfg(test)]
466mod tests {
467 use super::*;
468
469 fn valid_signature() -> BehavioralSignature {
470 BehavioralSignature {
471 signature_id: "signal-1".to_string(),
472 signal_type: "timing-deviation".to_string(),
473 indicators: vec![BehaviorIndicator {
474 indicator_type: IndicatorType::Timing,
475 metric: "response_time".to_string(),
476 operator: ComparisonOperator::GreaterThan,
477 value: 500.0,
478 weight: 1.0,
479 }],
480 threshold: 0.5,
481 confidence: 0.8,
482 }
483 }
484
485 #[test]
486 fn signal_validation_rejects_type_metric_mismatch() {
487 assert_eq!(valid_signature().validate(), Ok(()));
488 let mut invalid = valid_signature();
489 invalid.indicators[0].metric = "response_size".to_string();
490 assert_eq!(
491 invalid.validate(),
492 Err(BehavioralSignatureValidationError::InvalidIndicator { index: 0 })
493 );
494 }
495
496 #[test]
497 fn catalogs_reject_invalid_scores_duplicates_and_sort_ties_by_id() {
498 let record = |id: &str, score: f32| TechniqueRecord {
499 technique_id: id.to_string(),
500 technique_name: "fixture".to_string(),
501 category: TechniqueCategory::Encoding,
502 description: "caller record".to_string(),
503 reported_effectiveness: score,
504 reported_false_positive_rate: 0.1,
505 method_labels: vec!["inert-marker".to_string()],
506 };
507 let mut catalog = TechniqueCatalog::new();
508 assert_eq!(catalog.insert(record("b", 0.5)), Ok(()));
509 assert_eq!(catalog.insert(record("a", 0.5)), Ok(()));
510 assert_eq!(
511 catalog.insert(record("c", f32::NAN)),
512 Err(CatalogRecordError::InvalidReportedScore)
513 );
514 assert_eq!(
515 catalog.insert(record("a", 0.4)),
516 Err(CatalogRecordError::DuplicateId)
517 );
518 assert_eq!(
519 catalog
520 .ranked(TechniqueCategory::Encoding)
521 .into_iter()
522 .map(|item| item.technique_id.as_str())
523 .collect::<Vec<_>>(),
524 vec!["a", "b"]
525 );
526 }
527
528 #[test]
529 fn transformation_catalog_stores_only_valid_records() {
530 let mut catalog = TransformationRuleCatalog::new();
531 assert_eq!(
532 catalog.insert(TransformationRule {
533 rule_id: "rule-1".to_string(),
534 target_label: "host-marker".to_string(),
535 transformation_label: "inert-marker".to_string(),
536 transformation_type: TransformationType::Encoding,
537 reported_effectiveness: 0.4,
538 }),
539 Ok(())
540 );
541 assert_eq!(catalog.for_target("host-marker").len(), 1);
542 }
543
544 #[test]
545 fn advanced_detection_wire_rejects_unsafe_numeric_values() {
546 let mut invalid_signature = valid_signature();
547 invalid_signature.threshold = f32::NAN;
548 assert!(serde_json::to_string(&invalid_signature).is_err());
549
550 let mut signature_json =
551 serde_json::to_value(valid_signature()).expect("valid signature serializes");
552 signature_json["confidence"] = serde_json::json!(1.01);
553 assert!(serde_json::from_value::<BehavioralSignature>(signature_json).is_err());
554
555 let invalid_indicator = BehaviorIndicator {
556 indicator_type: IndicatorType::Timing,
557 metric: "response_time".to_string(),
558 operator: ComparisonOperator::GreaterThan,
559 value: -1.0,
560 weight: 0.5,
561 };
562 assert!(serde_json::to_string(&invalid_indicator).is_err());
563
564 let invalid_consistency_indicator = serde_json::json!({
565 "indicator_type": "consistency",
566 "metric": "consistency",
567 "operator": "equals",
568 "value": 1.01,
569 "weight": 0.5
570 });
571 assert!(
572 serde_json::from_value::<BehaviorIndicator>(invalid_consistency_indicator).is_err()
573 );
574
575 let invalid_analysis = BehavioralAnalysisData {
576 response_time_ms: 1.0,
577 response_size_bytes: 0,
578 error_keywords_count: 0,
579 unique_patterns: 0,
580 consistency_score: f32::INFINITY,
581 };
582 assert!(serde_json::to_string(&invalid_analysis).is_err());
583
584 let invalid_technique = TechniqueRecord {
585 technique_id: "technique-1".to_string(),
586 technique_name: "fixture".to_string(),
587 category: TechniqueCategory::Encoding,
588 description: "caller record".to_string(),
589 reported_effectiveness: 0.5,
590 reported_false_positive_rate: -0.01,
591 method_labels: Vec::new(),
592 };
593 assert!(serde_json::to_string(&invalid_technique).is_err());
594
595 let invalid_rule = serde_json::json!({
596 "rule_id": "rule-1",
597 "target_label": "host-marker",
598 "transformation_label": "inert-marker",
599 "transformation_type": "Other",
600 "reported_effectiveness": 1.01
601 });
602 assert!(serde_json::from_value::<TransformationRule>(invalid_rule).is_err());
603 }
604
605 #[test]
606 fn advanced_detection_records_round_trip_without_execution() {
607 let signature = valid_signature();
608 let encoded = serde_json::to_string(&signature).expect("signature serializes");
609 let decoded: BehavioralSignature =
610 serde_json::from_str(&encoded).expect("signature deserializes");
611 assert_eq!(decoded, signature);
612
613 let analysis = BehavioralAnalysisData {
614 response_time_ms: 12.5,
615 response_size_bytes: u32::MAX,
616 error_keywords_count: 2,
617 unique_patterns: 3,
618 consistency_score: 0.75,
619 };
620 let encoded = serde_json::to_string(&analysis).expect("analysis record serializes");
621 let decoded: BehavioralAnalysisData =
622 serde_json::from_str(&encoded).expect("analysis record deserializes");
623 assert_eq!(decoded, analysis);
624
625 let rule = TransformationRule {
626 rule_id: "rule-1".to_string(),
627 target_label: "host-marker".to_string(),
628 transformation_label: "inert-marker".to_string(),
629 transformation_type: TransformationType::Other,
630 reported_effectiveness: 0.25,
631 };
632 let encoded = serde_json::to_string(&rule).expect("rule record serializes");
633 let decoded: TransformationRule =
634 serde_json::from_str(&encoded).expect("rule record deserializes");
635 assert_eq!(decoded, rule);
636 }
637}