truestack 0.2.0

Security-aware technology fingerprinting — detects what is really running, not what the version string claims
Documentation
//! WAF detection via `wafrift-detect`.
//!
//! Thin integration layer that delegates WAF identification to the
//! [`wafrift_detect`] crate and converts results into truestack's
//! [`Technology`] type.

use crate::{TechCategory, Technology};

/// Detect the WAF protecting a target from response data.
///
/// Wraps [`wafrift_detect::detect`] and maps the result into a
/// [`Technology`] with the WAF's name, confidence score, and
/// `Security` category.
///
/// # Arguments
///
/// * `status` — HTTP response status code.
/// * `headers` — Response headers as `(name, value)` pairs.
/// * `body` — Raw response body bytes (only the first 4 KiB are inspected).
#[must_use]
pub fn detect(status: u16, headers: &[(String, String)], body: &[u8]) -> Option<Technology> {
    let detected = wafrift_detect::detect(status, headers, body)
        .into_iter()
        .next()?;

    Some(Technology {
        name: detected.name.to_string(),
        version: None,
        category: TechCategory::Security,
        confidence: (detected.confidence * 100.0).round().min(100.0) as u8,
    })
}

/// Returns the names of all WAFs that can be detected.
#[must_use]
pub fn supported_wafs() -> Vec<String> {
    wafrift_detect::supported_wafs()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cloudflare_waf_detected() {
        let headers = vec![
            ("server".to_string(), "cloudflare".to_string()),
            ("cf-ray".to_string(), "abc123".to_string()),
        ];
        let result = detect(403, &headers, b"cloudflare ray id");
        assert!(result.is_some(), "should detect Cloudflare WAF");
        let tech = result.unwrap();
        assert_eq!(tech.name, "Cloudflare");
        assert_eq!(tech.category, TechCategory::Security);
        assert!(tech.confidence > 50);
    }

    #[test]
    fn no_waf_returns_none() {
        let headers = vec![("server".to_string(), "nginx/1.21.0".to_string())];
        let result = detect(200, &headers, b"<html>hello</html>");
        assert!(result.is_none());
    }

    #[test]
    fn supported_wafs_nonempty() {
        let wafs = supported_wafs();
        assert!(
            wafs.len() >= 15,
            "should have at least 15 WAF detectors, got {}",
            wafs.len()
        );
    }
}