Tutorial: Writing a Detector
This tutorial builds a detector that checks for insecure cookie configuration — a requirement under AUTH-03 (session hijacking protection).
What We’re Building
Section titled “What We’re Building”A detector that checks for:
- Missing
HttpOnlyflag on session cookies - Missing
Secureflag on cookies - Missing
SameSiteattribute
These map to requirement AUTH-03-R2: “The product shall set secure cookie flags (HttpOnly, Secure, SameSite).”
Step 1: Create the Detector File
Section titled “Step 1: Create the Detector File”Create src/assessment/detectors/cookies.rs:
use regex::Regex;use crate::assessment::catalog::CatalogIndex;use crate::assessment::detect::*;
pub struct CookieDetector;
impl Detector for CookieDetector { fn name(&self) -> &str { "cookies" } fn handles_prefixes(&self) -> &[&str] { &["AUTH-03"] }
fn detect( &self, _catalog: &CatalogIndex, _context: &ProjectContext, files: &ProjectFiles, ) -> Vec<Finding> { let mut findings = Vec::new();
for (relative, content) in super::walk_source_files(files.root()) { if !super::is_source_file(std::path::Path::new(&relative)) { continue; } findings.extend(check_cookie_flags(&content, &relative)); }
findings }}Step 2: Add Detection Logic
Section titled “Step 2: Add Detection Logic”fn check_cookie_flags(content: &str, file: &str) -> Vec<Finding> { let mut findings = Vec::new();
// Detect cookie-setting patterns let cookie_set = Regex::new( r"(?i)(?:set-cookie|cookie\s*\(|setCookie|res\.cookie|response\.set_cookie)" ).unwrap();
for (line_num, line) in content.lines().enumerate() { let trimmed = line.trim(); if trimmed.starts_with("//") || trimmed.starts_with('#') { continue; }
if cookie_set.is_match(line) { let lower = line.to_lowercase();
// Check for missing HttpOnly if !lower.contains("httponly") { findings.push(Finding { requirement_id: "AUTH-03-R2".to_string(), risk_id: "AUTH-03".to_string(), status: FindingStatus::NeedsReview, confidence: 0.65, detector: "cookies".to_string(), message: "Cookie set without HttpOnly flag. Session cookies should use HttpOnly.".to_string(), source_locations: vec![SourceLocation { file: file.to_string(), line: line_num + 1, snippet: trimmed.to_string(), }], }); }
// Check for missing Secure flag if !lower.contains("secure") { findings.push(Finding { requirement_id: "AUTH-03-R2".to_string(), risk_id: "AUTH-03".to_string(), status: FindingStatus::NeedsReview, confidence: 0.65, detector: "cookies".to_string(), message: "Cookie set without Secure flag. Cookies should only be sent over HTTPS.".to_string(), source_locations: vec![SourceLocation { file: file.to_string(), line: line_num + 1, snippet: trimmed.to_string(), }], }); } } }
findings}Step 3: Write Tests
Section titled “Step 3: Write Tests”#[cfg(test)]mod tests { use super::*;
#[test] fn detects_missing_httponly() { let code = r#"res.cookie('session', token, { maxAge: 3600000 })"#; let findings = check_cookie_flags(code, "src/auth.js"); assert!(findings.iter().any(|f| f.message.contains("HttpOnly"))); }
#[test] fn passes_with_secure_flags() { let code = r#"res.cookie('session', token, { httpOnly: true, secure: true, sameSite: 'strict' })"#; let findings = check_cookie_flags(code, "src/auth.js"); // Should not flag HttpOnly or Secure since both are present assert!(findings.iter().all(|f| !f.message.contains("HttpOnly"))); assert!(findings.iter().all(|f| !f.message.contains("Secure"))); }
#[test] fn skips_comments() { let code = "// res.cookie('session', token)"; let findings = check_cookie_flags(code, "src/auth.js"); assert!(findings.is_empty()); }}Step 4: Register the Detector
Section titled “Step 4: Register the Detector”In src/assessment/detectors/mod.rs:
pub mod cookies; // Add this line
pub fn register_all(registry: &mut DetectorRegistry) { // ... existing detectors ... registry.register(Box::new(cookies::CookieDetector)); // Add this}Step 5: Build and Test
Section titled “Step 5: Build and Test”cargo test --lib assessment::detectors::cookiescargo run --bin fleet -- scan --path ./test-project --output prettyKey Patterns
Section titled “Key Patterns”| Pattern | When to Use |
|---|---|
FindingStatus::Pass | Clear positive signal (e.g., file exists, config correct) |
FindingStatus::Fail | Clear violation (e.g., prohibited algorithm, hardcoded secret) |
FindingStatus::NeedsReview | Heuristic match, context needed for judgment |
FindingStatus::NotApplicable | Feature not used (e.g., no AI -> AI-* is N/A) |
Always redact potential secrets in snippets: use "[REDACTED]" instead of the actual value.