termivar_scanner/defense/
policy.rs1use super::state::{DefensePosture, DefenseState};
11use super::transition::DefenseTransition;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18#[non_exhaustive]
19pub enum DefenseResponse {
20 Proceed,
22 Observe,
25 Backoff,
28 Reconsider,
31 Halt,
33}
34
35pub fn recommend(state: &DefenseState, transition: Option<&DefenseTransition>) -> DefenseResponse {
46 if transition.is_some_and(DefenseTransition::is_newly_blocking) {
47 return DefenseResponse::Reconsider;
48 }
49 if state.posture() == DefensePosture::Blocking {
50 return DefenseResponse::Halt;
51 }
52 if state.is_rate_limited() {
53 return DefenseResponse::Backoff;
54 }
55 if state.posture() == DefensePosture::Suspected {
56 return DefenseResponse::Observe;
57 }
58 DefenseResponse::Proceed
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 fn state(status: u16, headers: &[(&str, &str)], body: &str) -> DefenseState {
66 DefenseState::observe(status, headers, body)
67 }
68
69 #[test]
70 fn an_open_response_proceeds() {
71 let open = state(200, &[("Server", "nginx")], "ok");
72 assert_eq!(recommend(&open, None), DefenseResponse::Proceed);
73 }
74
75 #[test]
76 fn a_present_but_non_blocking_defense_is_observed() {
77 let suspected = state(200, &[("x-amzn-requestid", "id")], "ok");
78 assert_eq!(recommend(&suspected, None), DefenseResponse::Observe);
79 }
80
81 #[test]
82 fn rate_limiting_recommends_backoff() {
83 let limited = state(429, &[], "slow down");
84 assert_eq!(recommend(&limited, None), DefenseResponse::Backoff);
85 }
86
87 #[test]
88 fn a_standing_block_halts() {
89 let blocked = state(403, &[], "forbidden");
90 assert_eq!(recommend(&blocked, None), DefenseResponse::Halt);
91 }
92
93 #[test]
94 fn a_candidate_provoked_block_is_reconsidered_not_halted() {
95 let control = state(200, &[], "ok");
96 let candidate = state(403, &[], "forbidden");
97 let transition = DefenseTransition::between(&control, &candidate);
98 assert_eq!(
101 recommend(&candidate, Some(&transition)),
102 DefenseResponse::Reconsider
103 );
104 }
105
106 #[test]
107 fn a_pre_existing_block_still_halts_even_with_a_transition() {
108 let control = state(403, &[], "forbidden");
109 let candidate = state(403, &[], "forbidden");
110 let transition = DefenseTransition::between(&control, &candidate);
111 assert_eq!(
113 recommend(&candidate, Some(&transition)),
114 DefenseResponse::Halt
115 );
116 }
117
118 #[test]
119 fn responses_order_by_restrictiveness() {
120 assert!(DefenseResponse::Halt > DefenseResponse::Reconsider);
121 assert!(DefenseResponse::Reconsider > DefenseResponse::Backoff);
122 assert!(DefenseResponse::Backoff > DefenseResponse::Observe);
123 assert!(DefenseResponse::Observe > DefenseResponse::Proceed);
124 }
125
126 #[test]
127 fn recommendation_is_deterministic() {
128 let control = state(200, &[], "ok");
129 let candidate = state(403, &[("CF-RAY", "x")], "access denied");
130 let transition = DefenseTransition::between(&control, &candidate);
131 let first = recommend(&candidate, Some(&transition));
132 let second = recommend(&candidate, Some(&transition));
133 assert_eq!(first, second);
134 }
135}