1mod load;
5mod validate;
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10pub use load::{
11 load_detector_cache, load_detectors, load_detectors_from_str, load_detectors_with_gate,
12 save_detector_cache,
13};
14pub use validate::{validate_detector, QualityIssue};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct MetadataSpec {
19 pub name: String,
21 pub json_path: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
27pub struct DetectorSpec {
28 pub id: String,
30 pub name: String,
32 pub service: String,
34 pub severity: Severity,
36 pub patterns: Vec<PatternSpec>,
38 #[serde(default)]
40 pub companions: Vec<CompanionSpec>,
41 pub verify: Option<VerifySpec>,
43 #[serde(default)]
45 pub keywords: Vec<String>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct PatternSpec {
51 pub regex: String,
53 pub description: Option<String>,
55 pub group: Option<usize>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct CompanionSpec {
62 pub name: String,
64 pub regex: String,
66 pub within_lines: usize,
68 #[serde(default)]
70 pub required: bool,
71}
72
73#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct VerifySpec {
76 #[serde(default)]
78 pub service: String,
79 pub method: Option<HttpMethod>,
81 pub url: Option<String>,
83 pub auth: Option<AuthSpec>,
85 #[serde(default)]
87 pub headers: Vec<HeaderSpec>,
88 pub body: Option<String>,
90 pub success: Option<SuccessSpec>,
92 #[serde(default)]
94 pub metadata: Vec<MetadataSpec>,
95 pub timeout_ms: Option<u64>,
97 #[serde(default)]
99 pub steps: Vec<StepSpec>,
100 #[serde(default)]
110 pub allowed_domains: Vec<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct StepSpec {
116 pub name: String,
117 pub method: HttpMethod,
118 pub url: String,
119 pub auth: AuthSpec,
120 #[serde(default)]
121 pub headers: Vec<HeaderSpec>,
122 pub body: Option<String>,
123 pub success: SuccessSpec,
124 #[serde(default)]
125 pub extract: Vec<MetadataSpec>,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct HeaderSpec {
131 pub name: String,
132 pub value: String,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
137#[serde(tag = "type", rename_all = "snake_case")]
138pub enum AuthSpec {
139 None,
140 Bearer {
141 field: String,
142 },
143 Basic {
144 username: String,
145 password: String,
146 },
147 Header {
148 name: String,
149 template: String,
150 },
151 Query {
152 param: String,
153 field: String,
154 },
155 #[serde(rename = "aws_v4")]
156 AwsV4 {
157 access_key: String,
158 secret_key: String,
159 region: String,
160 service: String,
161 session_token: Option<String>,
162 },
163 Script {
164 engine: String,
165 code: String,
166 },
167}
168
169impl AuthSpec {
170 pub fn service_name(&self) -> Option<&str> {
171 match self {
172 AuthSpec::AwsV4 { service, .. } => Some(service),
173 _ => None,
174 }
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize, Default)]
180pub struct SuccessSpec {
181 #[serde(default)]
182 pub status: Option<u16>,
184 #[serde(default)]
185 pub status_not: Option<u16>,
187 #[serde(default)]
188 pub body_contains: Option<String>,
190 #[serde(default)]
191 pub body_not_contains: Option<String>,
193 #[serde(default)]
194 pub json_path: Option<String>,
196 #[serde(default)]
197 pub equals: Option<String>,
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
203#[serde(rename_all = "lowercase")]
204pub enum Severity {
205 #[default]
206 Info,
207 Low,
208 Medium,
209 High,
210 Critical,
211}
212
213impl Severity {
214 pub fn to_severity(&self) -> Self {
215 *self
216 }
217
218 pub fn downgrade_one(self) -> Self {
227 match self {
228 Severity::Critical => Severity::High,
229 Severity::High => Severity::Medium,
230 Severity::Medium => Severity::Low,
231 Severity::Low => Severity::Info,
232 Severity::Info => Severity::Info,
233 }
234 }
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub enum HttpMethod {
240 #[serde(rename = "GET")]
241 Get,
242 #[serde(rename = "POST")]
243 Post,
244 #[serde(rename = "PUT")]
245 Put,
246 #[serde(rename = "DELETE")]
247 Delete,
248 #[serde(rename = "PATCH")]
249 Patch,
250 #[serde(rename = "HEAD")]
251 Head,
252}
253
254#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct DetectorFile {
257 pub detector: DetectorSpec,
258}
259
260#[derive(Debug, Error)]
262pub enum SpecError {
263 #[error(
264 "failed to read detector file {path}: {source}. Fix: check the detector path exists and that the file is readable TOML"
265 )]
266 ReadFile {
267 path: String,
268 source: std::io::Error,
269 },
270 #[error("invalid TOML in detector {path}: {source}. Fix: repair the TOML syntax in the detector file")]
271 InvalidToml {
272 path: std::path::PathBuf,
273 source: toml::de::Error,
274 },
275}
276
277#[cfg(test)]
278mod tests {
279 use super::Severity;
280
281 #[test]
282 fn severity_downgrade_walks_one_step() {
283 assert_eq!(Severity::Critical.downgrade_one(), Severity::High);
284 assert_eq!(Severity::High.downgrade_one(), Severity::Medium);
285 assert_eq!(Severity::Medium.downgrade_one(), Severity::Low);
286 assert_eq!(Severity::Low.downgrade_one(), Severity::Info);
287 }
288
289 #[test]
290 fn severity_downgrade_floors_at_info() {
291 assert_eq!(Severity::Info.downgrade_one(), Severity::Info);
292 }
293
294 #[test]
295 fn severity_downgrade_is_monotonic() {
296 let mut s = Severity::Critical;
298 for _ in 0..10 {
299 let next = s.downgrade_one();
300 assert!(next <= s);
301 s = next;
302 }
303 assert_eq!(s, Severity::Info);
304 }
305}