Skip to main content

keyhog_core/
spec.rs

1//! Detector specification: TOML-based pattern definitions with regex, keywords,
2//! verification endpoints, and companion patterns.
3
4mod load;
5mod validate;
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10pub use load::{
11    load_detector_cache, load_detectors, load_detectors_with_gate, save_detector_cache,
12};
13pub use validate::{QualityIssue, validate_detector};
14
15/// Metadata field specification for verification results.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct MetadataSpec {
18    /// Field name in the finding metadata map.
19    pub name: String,
20    /// GJSON path to extract from the verification response body.
21    pub json_path: String,
22}
23
24/// A complete detector definition loaded from a TOML file.
25#[derive(Debug, Clone, Serialize, Deserialize, Default)]
26pub struct DetectorSpec {
27    /// Unique stable identifier (e.g. `aws-access-key`).
28    pub id: String,
29    /// Human-readable name.
30    pub name: String,
31    /// Target service (e.g. `aws`, `stripe`).
32    pub service: String,
33    /// Default severity for findings.
34    pub severity: Severity,
35    /// List of regex patterns to match.
36    pub patterns: Vec<PatternSpec>,
37    /// Secondary patterns required to confirm a match.
38    #[serde(default)]
39    pub companions: Vec<CompanionSpec>,
40    /// Live verification configuration.
41    pub verify: Option<VerifySpec>,
42    /// High-performance pre-filtering keywords.
43    #[serde(default)]
44    pub keywords: Vec<String>,
45}
46
47/// A regex pattern with optional capture group and description.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct PatternSpec {
50    /// Regular expression string (Rust flavor).
51    pub regex: String,
52    /// Optional context description.
53    pub description: Option<String>,
54    /// Optional capture group index containing the secret.
55    pub group: Option<usize>,
56}
57
58/// Secondary pattern used to confirm a primary match or provide extra context.
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct CompanionSpec {
61    /// Field name used in verification templates (e.g. `{{companion.secret_key}}`).
62    pub name: String,
63    /// Regex to find the companion value nearby.
64    pub regex: String,
65    /// Maximum line distance from the primary match.
66    pub within_lines: usize,
67    /// Whether this companion must be found to report the finding.
68    #[serde(default)]
69    pub required: bool,
70}
71
72/// Live verification configuration for a detector.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct VerifySpec {
75    /// Target service identifier (defaults to detector's service if omitted).
76    #[serde(default)]
77    pub service: String,
78    /// HTTP method (default: GET).
79    pub method: Option<HttpMethod>,
80    /// Endpoint URL with optional `{{match}}` or `{{companion.<name>}}` placeholders.
81    pub url: Option<String>,
82    /// Authentication scheme.
83    pub auth: Option<AuthSpec>,
84    /// Custom HTTP headers.
85    #[serde(default)]
86    pub headers: Vec<HeaderSpec>,
87    /// Optional request body template.
88    pub body: Option<String>,
89    /// Criteria for a successful verification.
90    pub success: Option<SuccessSpec>,
91    /// Metadata to extract from the response.
92    #[serde(default)]
93    pub metadata: Vec<MetadataSpec>,
94    /// Optional request timeout override.
95    pub timeout_ms: Option<u64>,
96    /// Multi-step verification flow.
97    #[serde(default)]
98    pub steps: Vec<StepSpec>,
99}
100
101/// A single step in a multi-step verification flow.
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct StepSpec {
104    pub name: String,
105    pub method: HttpMethod,
106    pub url: String,
107    pub auth: AuthSpec,
108    #[serde(default)]
109    pub headers: Vec<HeaderSpec>,
110    pub body: Option<String>,
111    pub success: SuccessSpec,
112    #[serde(default)]
113    pub extract: Vec<MetadataSpec>,
114}
115
116/// Custom HTTP header specification.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct HeaderSpec {
119    pub name: String,
120    pub value: String,
121}
122
123/// Authentication scheme for verification requests.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(tag = "type", rename_all = "snake_case")]
126pub enum AuthSpec {
127    None,
128    Bearer {
129        field: String,
130    },
131    Basic {
132        username: String,
133        password: String,
134    },
135    Header {
136        name: String,
137        template: String,
138    },
139    Query {
140        param: String,
141        field: String,
142    },
143    #[serde(rename = "aws_v4")]
144    AwsV4 {
145        access_key: String,
146        secret_key: String,
147        region: String,
148        service: String,
149        session_token: Option<String>,
150    },
151    Script {
152        engine: String,
153        code: String,
154    },
155}
156
157impl AuthSpec {
158    pub fn service_name(&self) -> Option<&str> {
159        match self {
160            AuthSpec::AwsV4 { service, .. } => Some(service),
161            _ => None,
162        }
163    }
164}
165
166/// Criteria for a successful verification response.
167#[derive(Debug, Clone, Serialize, Deserialize, Default)]
168pub struct SuccessSpec {
169    #[serde(default)]
170    /// Required HTTP status code.
171    pub status: Option<u16>,
172    #[serde(default)]
173    /// Reject if this status code is returned.
174    pub status_not: Option<u16>,
175    #[serde(default)]
176    /// Response body must contain this substring.
177    pub body_contains: Option<String>,
178    #[serde(default)]
179    /// Response body must NOT contain this substring.
180    pub body_not_contains: Option<String>,
181    #[serde(default)]
182    /// GJSON path to check in response body.
183    pub json_path: Option<String>,
184    #[serde(default)]
185    /// Expected value at `json_path`.
186    pub equals: Option<String>,
187}
188
189/// Severity level for a finding.
190#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)]
191#[serde(rename_all = "lowercase")]
192pub enum Severity {
193    #[default]
194    Info,
195    Low,
196    Medium,
197    High,
198    Critical,
199}
200
201impl Severity {
202    pub fn to_severity(&self) -> Self {
203        *self
204    }
205}
206
207/// HTTP method for verification requests.
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub enum HttpMethod {
210    #[serde(rename = "GET")]
211    Get,
212    #[serde(rename = "POST")]
213    Post,
214    #[serde(rename = "PUT")]
215    Put,
216    #[serde(rename = "DELETE")]
217    Delete,
218    #[serde(rename = "PATCH")]
219    Patch,
220    #[serde(rename = "HEAD")]
221    Head,
222}
223
224/// Wrapping struct for a detector TOML file.
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct DetectorFile {
227    pub detector: DetectorSpec,
228}
229
230/// Errors returned while loading or validating detector specifications.
231#[derive(Debug, Error)]
232pub enum SpecError {
233    #[error(
234        "failed to read detector file {path}: {source}. Fix: check the detector path exists and that the file is readable TOML"
235    )]
236    ReadFile {
237        path: String,
238        source: std::io::Error,
239    },
240}