Skip to content

Tutorial: Writing a Detector

This tutorial builds a detector that checks for insecure cookie configuration — a requirement under AUTH-03 (session hijacking protection).

A detector that checks for:

  • Missing HttpOnly flag on session cookies
  • Missing Secure flag on cookies
  • Missing SameSite attribute

These map to requirement AUTH-03-R2: “The product shall set secure cookie flags (HttpOnly, Secure, SameSite).”

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
}
}
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
}
#[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());
}
}

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
}
Terminal window
cargo test --lib assessment::detectors::cookies
cargo run --bin fleet -- scan --path ./test-project --output pretty
PatternWhen to Use
FindingStatus::PassClear positive signal (e.g., file exists, config correct)
FindingStatus::FailClear violation (e.g., prohibited algorithm, hardcoded secret)
FindingStatus::NeedsReviewHeuristic match, context needed for judgment
FindingStatus::NotApplicableFeature not used (e.g., no AI -> AI-* is N/A)

Always redact potential secrets in snippets: use "[REDACTED]" instead of the actual value.