1use http::{Request, StatusCode};
3use security_rust::Scanner;
4
5pub use security_rust::{AttackCategory, DetectionResult, ScannerBuilder, Severity};
6
7mod layer;
8pub use layer::{SecurityBodyLayer, SecurityBodyService, SecurityLayer, SecurityService};
9
10#[derive(Debug, thiserror::Error)]
11pub enum SecurityError {
12 #[error("attack blocked: {0}")]
13 AttackBlocked(String),
14 #[error("request body too large")]
16 BodyTooLarge,
17 #[error("inner error: {0}")]
18 Inner(#[from] Box<dyn std::error::Error + Send + Sync>),
19}
20
21impl SecurityError {
22 pub fn to_http_status(&self) -> StatusCode {
23 match self {
24 Self::AttackBlocked(_) => StatusCode::FORBIDDEN,
25 Self::BodyTooLarge => StatusCode::PAYLOAD_TOO_LARGE,
26 Self::Inner(_) => StatusCode::INTERNAL_SERVER_ERROR,
27 }
28 }
29}
30
31impl axum::response::IntoResponse for SecurityError {
35 fn into_response(self) -> axum::response::Response {
36 let status = self.to_http_status();
37 let body = match &self {
38 Self::AttackBlocked(types) => {
39 format!(r#"{{"error":"attack blocked","types":"{types}"}}"#)
40 }
41 Self::BodyTooLarge => r#"{"error":"request body too large"}"#.to_string(),
42 Self::Inner(e) => {
43 tracing::error!(error = %e, "security middleware internal error");
44 r#"{"error":"internal server error"}"#.to_string()
45 }
46 };
47 (status, body).into_response()
48 }
49}
50
51pub struct SecurityScanner {
53 scanner: Scanner,
54}
55
56impl SecurityScanner {
57 pub fn new() -> Self {
59 Self {
60 scanner: Scanner::default(),
61 }
62 }
63
64 pub fn scan(&self, input: &str) -> Vec<DetectionResult> {
66 self.scanner.scan(input)
67 }
68
69 pub fn scan_parts(&self, parts: &[&str]) -> Vec<DetectionResult> {
71 let mut results = Vec::with_capacity(parts.len() * 2);
72 for part in parts {
73 results.extend(self.scanner.scan(part));
74 }
75 results
76 }
77
78 pub fn scan_body(&self, body: &[u8]) -> Vec<DetectionResult> {
80 if let Ok(s) = std::str::from_utf8(body) {
81 self.scanner.scan(s)
82 } else {
83 Vec::new()
84 }
85 }
86}
87
88impl Default for SecurityScanner {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94fn evaluate(results: &[DetectionResult]) -> Option<SecurityError> {
97 let mut blocked = false;
98 for r in results {
99 tracing::warn!(
100 attack_type = %r.attack_type,
101 category = ?r.category,
102 severity = ?r.severity,
103 matched = %r.matched_pattern,
104 "attack detected"
105 );
106 if r.attack_type != "jwt_attack"
110 && matches!(r.severity, Severity::High | Severity::Critical)
111 {
112 blocked = true;
113 }
114 }
115 if blocked {
116 let attack_types: Vec<String> = results.iter().map(|r| r.attack_type.to_string()).collect();
117 return Some(SecurityError::AttackBlocked(attack_types.join(", ")));
118 }
119 None
120}
121
122fn percent_decode(s: &str) -> String {
125 let bytes = s.as_bytes();
126 let mut out = Vec::with_capacity(bytes.len());
127 let mut i = 0;
128 while i < bytes.len() {
129 if bytes[i] == b'%'
130 && i + 2 < bytes.len()
131 && let (Some(h), Some(l)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2]))
132 {
133 out.push(h * 16 + l);
134 i += 3;
135 continue;
136 }
137 out.push(bytes[i]);
138 i += 1;
139 }
140 String::from_utf8_lossy(&out).into_owned()
141}
142
143fn hex_val(b: u8) -> Option<u8> {
144 match b {
145 b'0'..=b'9' => Some(b - b'0'),
146 b'a'..=b'f' => Some(b - b'a' + 10),
147 b'A'..=b'F' => Some(b - b'A' + 10),
148 _ => None,
149 }
150}
151
152fn is_proxy_header(name: &http::header::HeaderName) -> bool {
160 matches!(
161 name.as_str(),
162 "x-forwarded-for"
163 | "x-real-ip"
164 | "forwarded"
165 | "x-forwarded-host"
166 | "x-forwarded-proto"
167 | "x-forwarded-port"
168 | "authorization"
169 )
170}
171
172fn request_parts<B>(req: &Request<B>) -> Vec<String> {
173 let mut parts: Vec<String> = Vec::new();
174 parts.push(percent_decode(&req.uri().to_string()));
175 for (name, value) in req.headers() {
176 if is_proxy_header(name) {
177 continue;
178 }
179 if let Ok(v) = value.to_str() {
180 parts.push(v.to_string());
181 }
182 }
183 parts
184}
185
186#[cfg(test)]
187mod tests;