Skip to main content

fallow_security/
rules.rs

1//! Rule-severity policy for security-aware surfaces.
2
3use fallow_config::{ResolvedConfig, RulesConfig, Severity};
4use fallow_types::results::{SecurityFinding, SecurityFindingKind};
5
6/// Enable the advisory security rules for a dedicated security-aware surface.
7///
8/// Explicit user severities are preserved. Only the default `off` state is
9/// promoted to `warn`, so `fallow security` and the viz Security lens surface
10/// candidates without overriding a deliberate configuration.
11pub fn enable_security_rules(config: &mut ResolvedConfig) {
12    if config.rules.security_client_server_leak == Severity::Off {
13        config.rules.security_client_server_leak = Severity::Warn;
14    }
15    if config.rules.security_sink == Severity::Off {
16        config.rules.security_sink = Severity::Warn;
17    }
18}
19
20/// The severity of the rule that produces findings of `kind`.
21#[must_use]
22const fn security_rule_severity(rules: &RulesConfig, kind: SecurityFindingKind) -> Severity {
23    match kind {
24        SecurityFindingKind::ClientServerLeak => rules.security_client_server_leak,
25        SecurityFindingKind::TaintedSink => rules.security_sink,
26    }
27}
28
29/// The severity of `finding` for its own path.
30///
31/// The `overrides` that match the path of the finding apply in the same way as
32/// for dead-code findings.
33#[must_use]
34pub fn resolve_security_finding_severity(
35    config: &ResolvedConfig,
36    finding: &SecurityFinding,
37) -> Severity {
38    let path = config.root.join(&finding.path);
39    security_rule_severity(&config.resolve_rules_for_path(&path), finding.kind)
40}
41
42/// Remove each finding whose rule resolves to `off` for its path.
43///
44/// A top-level `off` stops the detector before this point. This pass applies
45/// an `off` from `overrides`, so a per-path `off` drops the finding also when a
46/// security-aware surface raised the top-level rule to `warn`.
47pub fn retain_enabled_security_findings(
48    findings: &mut Vec<SecurityFinding>,
49    config: &ResolvedConfig,
50) {
51    if config.overrides.is_empty() {
52        return;
53    }
54    findings.retain(|finding| resolve_security_finding_severity(config, finding) != Severity::Off);
55}
56
57/// Whether a security rule is `error` at the top level or in an override.
58#[must_use]
59pub fn security_rules_can_error(config: &ResolvedConfig) -> bool {
60    config.rules.security_client_server_leak == Severity::Error
61        || config.rules.security_sink == Severity::Error
62        || config.overrides.iter().any(|entry| {
63            entry.rules.security_client_server_leak == Some(Severity::Error)
64                || entry.rules.security_sink == Some(Severity::Error)
65        })
66}
67
68#[cfg(test)]
69#[allow(
70    clippy::expect_used,
71    reason = "fixture setup asserts its own invariants directly"
72)]
73mod tests {
74    use super::{enable_security_rules, security_rules_can_error};
75
76    fn resolve(config: fallow_config::FallowConfig) -> fallow_config::ResolvedConfig {
77        let project = tempfile::tempdir().expect("temp dir");
78        config.resolve(
79            project.path().to_path_buf(),
80            fallow_config::OutputFormat::Human,
81            1,
82            true,
83            true,
84            None,
85        )
86    }
87
88    fn sink_override(severity: fallow_config::Severity) -> fallow_config::FallowConfig {
89        fallow_config::FallowConfig {
90            overrides: vec![fallow_config::ConfigOverride {
91                files: vec!["src/generated/**".to_owned()],
92                rules: fallow_config::PartialRulesConfig {
93                    security_sink: Some(severity),
94                    ..Default::default()
95                },
96            }],
97            ..Default::default()
98        }
99    }
100
101    #[test]
102    fn override_error_makes_security_rules_able_to_error() {
103        let mut config = resolve(sink_override(fallow_config::Severity::Error));
104        enable_security_rules(&mut config);
105
106        assert!(security_rules_can_error(&config));
107    }
108
109    #[test]
110    fn override_off_or_warn_keeps_security_rules_advisory() {
111        for severity in [fallow_config::Severity::Off, fallow_config::Severity::Warn] {
112            let mut config = resolve(sink_override(severity));
113            enable_security_rules(&mut config);
114
115            assert!(!security_rules_can_error(&config));
116        }
117    }
118
119    #[test]
120    fn enables_only_default_off_security_rules() {
121        let project = tempfile::tempdir().expect("temp dir");
122        let mut config = fallow_config::FallowConfig::default().resolve(
123            project.path().to_path_buf(),
124            fallow_config::OutputFormat::Human,
125            1,
126            true,
127            true,
128            None,
129        );
130        config.rules.security_client_server_leak = fallow_config::Severity::Off;
131        config.rules.security_sink = fallow_config::Severity::Error;
132
133        enable_security_rules(&mut config);
134
135        assert_eq!(
136            config.rules.security_client_server_leak,
137            fallow_config::Severity::Warn
138        );
139        assert_eq!(config.rules.security_sink, fallow_config::Severity::Error);
140    }
141}