mod load;
mod validate;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub use load::{
load_detector_cache, load_detectors, load_detectors_from_str, load_detectors_with_gate,
save_detector_cache,
};
pub use validate::{validate_detector, QualityIssue};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetadataSpec {
pub name: String,
pub json_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DetectorSpec {
pub id: String,
pub name: String,
pub service: String,
pub severity: Severity,
pub patterns: Vec<PatternSpec>,
#[serde(default)]
pub companions: Vec<CompanionSpec>,
pub verify: Option<VerifySpec>,
#[serde(default)]
pub keywords: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatternSpec {
pub regex: String,
pub description: Option<String>,
pub group: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompanionSpec {
pub name: String,
pub regex: String,
pub within_lines: usize,
#[serde(default)]
pub required: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct VerifySpec {
#[serde(default)]
pub service: String,
pub method: Option<HttpMethod>,
pub url: Option<String>,
pub auth: Option<AuthSpec>,
#[serde(default)]
pub headers: Vec<HeaderSpec>,
pub body: Option<String>,
pub success: Option<SuccessSpec>,
#[serde(default)]
pub metadata: Vec<MetadataSpec>,
pub timeout_ms: Option<u64>,
#[serde(default)]
pub steps: Vec<StepSpec>,
#[serde(default)]
pub allowed_domains: Vec<String>,
pub oob: Option<OobSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OobSpec {
pub protocol: OobProtocol,
#[serde(default)]
pub timeout_secs: Option<u64>,
#[serde(default)]
pub policy: OobPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OobProtocol {
Dns,
Http,
Smtp,
Any,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum OobPolicy {
#[default]
OobAndHttp,
OobOnly,
OobOptional,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepSpec {
pub name: String,
pub method: HttpMethod,
pub url: String,
pub auth: AuthSpec,
#[serde(default)]
pub headers: Vec<HeaderSpec>,
pub body: Option<String>,
pub success: SuccessSpec,
#[serde(default)]
pub extract: Vec<MetadataSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HeaderSpec {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AuthSpec {
None,
Bearer {
field: String,
},
Basic {
username: String,
password: String,
},
Header {
name: String,
template: String,
},
Query {
param: String,
field: String,
},
#[serde(rename = "aws_v4")]
AwsV4 {
access_key: String,
secret_key: String,
region: String,
service: String,
session_token: Option<String>,
},
Script {
engine: String,
code: String,
},
}
impl AuthSpec {
pub fn service_name(&self) -> Option<&str> {
match self {
AuthSpec::AwsV4 { service, .. } => Some(service),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SuccessSpec {
#[serde(default)]
pub status: Option<u16>,
#[serde(default)]
pub status_not: Option<u16>,
#[serde(default)]
pub body_contains: Option<String>,
#[serde(default)]
pub body_not_contains: Option<String>,
#[serde(default)]
pub json_path: Option<String>,
#[serde(default)]
pub equals: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
#[default]
Info,
Low,
Medium,
High,
Critical,
}
impl Severity {
pub fn to_severity(&self) -> Self {
*self
}
pub fn downgrade_one(self) -> Self {
match self {
Severity::Critical => Severity::High,
Severity::High => Severity::Medium,
Severity::Medium => Severity::Low,
Severity::Low => Severity::Info,
Severity::Info => Severity::Info,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum HttpMethod {
#[serde(rename = "GET")]
Get,
#[serde(rename = "POST")]
Post,
#[serde(rename = "PUT")]
Put,
#[serde(rename = "DELETE")]
Delete,
#[serde(rename = "PATCH")]
Patch,
#[serde(rename = "HEAD")]
Head,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DetectorFile {
pub detector: DetectorSpec,
}
#[derive(Debug, Error)]
#[allow(clippy::result_large_err)] pub enum SpecError {
#[error(
"failed to read detector file {path}: {source}. Fix: check the detector path exists and that the file is readable TOML"
)]
ReadFile {
path: String,
source: std::io::Error,
},
#[error("invalid TOML in detector {path}: {source}. Fix: repair the TOML syntax in the detector file")]
InvalidToml {
path: std::path::PathBuf,
source: toml::de::Error,
},
}
#[cfg(test)]
mod tests {
use super::Severity;
#[test]
fn severity_downgrade_walks_one_step() {
assert_eq!(Severity::Critical.downgrade_one(), Severity::High);
assert_eq!(Severity::High.downgrade_one(), Severity::Medium);
assert_eq!(Severity::Medium.downgrade_one(), Severity::Low);
assert_eq!(Severity::Low.downgrade_one(), Severity::Info);
}
#[test]
fn severity_downgrade_floors_at_info() {
assert_eq!(Severity::Info.downgrade_one(), Severity::Info);
}
#[test]
fn severity_downgrade_is_monotonic() {
let mut s = Severity::Critical;
for _ in 0..10 {
let next = s.downgrade_one();
assert!(next <= s);
s = next;
}
assert_eq!(s, Severity::Info);
}
}