Skip to main content

nyx_scanner/utils/
detector_options.rs

1//! Per-detector runtime options.
2//!
3//! Mirrors the install/current pattern in [`crate::utils::analysis_options`]
4//! but for detector-class knobs that live under `[detectors.*]` in
5//! `nyx.conf`.  Engine code that wants to consult a detector option calls
6//! [`current`]; the CLI installs a resolved value before the scan starts.
7//!
8//! The first knobs covered here are the [`Cap::DATA_EXFIL`][crate::labels::Cap::DATA_EXFIL]
9//! suppression layers:
10//!
11//! * `enabled` — turn the cap off entirely per-project so legitimate
12//!   forwarding pipelines don't surface findings.
13//! * `trusted_destinations` — destination URL prefixes that suppress the
14//!   cap when a sink's URL argument has a static prefix matching one of
15//!   them.  Uses the same prefix-lock plumbing the SSRF suppression has.
16//!
17//! Defaults are conservative: detector enabled, no trusted destinations.
18
19use serde::{Deserialize, Serialize};
20use std::sync::RwLock;
21
22/// Options for the `Cap::DATA_EXFIL` suppression layers.
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(default)]
25pub struct DataExfilDetectorOptions {
26    /// When `false`, the entire data-exfiltration detector class is
27    /// suppressed for the project.  Sink-time filters drop
28    /// [`crate::labels::Cap::DATA_EXFIL`] from sink caps before event
29    /// emission, so no `taint-data-exfiltration` findings reach output.
30    pub enabled: bool,
31    /// URL prefixes treated as trusted destinations for outbound
32    /// requests.  When a sink's destination argument has a proven static
33    /// prefix (from the abstract string domain or an inline literal)
34    /// that begins with one of these entries, the
35    /// [`crate::labels::Cap::DATA_EXFIL`] bit is dropped before event
36    /// emission.  Mirrors the SSRF prefix-lock semantics.
37    pub trusted_destinations: Vec<String>,
38}
39
40impl Default for DataExfilDetectorOptions {
41    fn default() -> Self {
42        Self {
43            enabled: true,
44            trusted_destinations: Vec::new(),
45        }
46    }
47}
48
49/// Top-level `[detectors]` block.
50#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
51#[serde(default)]
52pub struct DetectorOptions {
53    pub data_exfil: DataExfilDetectorOptions,
54}
55
56static RUNTIME: RwLock<Option<DetectorOptions>> = RwLock::new(None);
57
58/// Install the process-wide detector options.  First-wins: subsequent calls
59/// are a no-op and return `false`.  The CLI calls this once per process at
60/// scan start; library consumers that never install pick up
61/// [`DetectorOptions::default`] via [`current`].
62pub fn install(opts: DetectorOptions) -> bool {
63    let mut guard = RUNTIME.write().expect("detector options RwLock poisoned");
64    if guard.is_some() {
65        return false;
66    }
67    *guard = Some(opts);
68    true
69}
70
71/// Replace the installed options unconditionally.  Mirrors
72/// [`crate::utils::analysis_options::reinstall`] for the server's
73/// per-request resolution path.
74pub fn reinstall(opts: DetectorOptions) {
75    *RUNTIME.write().expect("detector options RwLock poisoned") = Some(opts);
76}
77
78/// Read the active options.  Returns the installed runtime when present,
79/// otherwise [`DetectorOptions::default`].
80pub fn current() -> DetectorOptions {
81    RUNTIME
82        .read()
83        .expect("detector options RwLock poisoned")
84        .clone()
85        .unwrap_or_default()
86}
87
88/// Test helper: clear the installed runtime so a subsequent [`install`]
89/// takes effect.  Used only in tests that exercise different detector
90/// configurations within the same process.
91#[doc(hidden)]
92pub fn _reset_for_tests() {
93    *RUNTIME.write().expect("detector options RwLock poisoned") = None;
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn defaults_match_documented() {
102        let o = DetectorOptions::default();
103        assert!(o.data_exfil.enabled);
104        assert!(o.data_exfil.trusted_destinations.is_empty());
105    }
106
107    #[test]
108    fn toml_roundtrip() {
109        let opts = DetectorOptions {
110            data_exfil: DataExfilDetectorOptions {
111                enabled: false,
112                trusted_destinations: vec![
113                    "https://api.internal/".into(),
114                    "https://telemetry.".into(),
115                ],
116            },
117        };
118        let s = toml::to_string(&opts).unwrap();
119        let back: DetectorOptions = toml::from_str(&s).unwrap();
120        assert_eq!(opts, back);
121    }
122
123    #[test]
124    fn missing_section_uses_defaults() {
125        let toml_str = r#"# empty"#;
126        let cfg: DetectorOptions = toml::from_str(toml_str).unwrap();
127        assert!(cfg.data_exfil.enabled);
128    }
129}