1use std::fmt;
7
8use pedant_types::{Capability, CapabilityFinding};
9use serde::Serialize;
10
11use crate::check_config::GateConfig;
12use crate::check_config::GateRuleOverride;
13use crate::ir::DataFlowFact;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
17#[serde(rename_all = "lowercase")]
18pub enum GateSeverity {
19 Deny,
21 Warn,
23 Info,
25}
26
27impl fmt::Display for GateSeverity {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Self::Deny => f.write_str("deny"),
31 Self::Warn => f.write_str("warn"),
32 Self::Info => f.write_str("info"),
33 }
34 }
35}
36
37#[derive(Serialize)]
39pub struct GateVerdict {
40 pub rule: &'static str,
42 pub severity: GateSeverity,
44 pub rationale: &'static str,
46}
47
48pub struct GateRuleInfo {
50 pub name: &'static str,
52 pub default_severity: GateSeverity,
54 pub description: &'static str,
56}
57
58struct BuiltinRule {
60 name: &'static str,
61 default_severity: GateSeverity,
62 description: &'static str,
63 rationale: &'static str,
64 predicate: fn(&[CapabilityFinding]) -> bool,
65}
66
67fn has_capability(
68 findings: &[CapabilityFinding],
69 cap: Capability,
70 build_script_only: bool,
71) -> bool {
72 findings
73 .iter()
74 .any(|f| f.capability == cap && (!build_script_only || f.build_script))
75}
76
77const BUILTIN_RULES: &[BuiltinRule] = &[
78 BuiltinRule {
80 name: "build-script-network",
81 default_severity: GateSeverity::Deny,
82 description: "Build script with network access",
83 rationale: "Build scripts should not make network requests",
84 predicate: |f| has_capability(f, Capability::Network, true),
85 },
86 BuiltinRule {
87 name: "build-script-exec",
88 default_severity: GateSeverity::Warn,
89 description: "Build script spawning processes",
90 rationale: "Build scripts spawning processes is common (cc, pkg-config) but risky",
91 predicate: |f| has_capability(f, Capability::ProcessExec, true),
92 },
93 BuiltinRule {
94 name: "build-script-download-exec",
95 default_severity: GateSeverity::Deny,
96 description: "Build script with network access and process execution",
97 rationale: "Download-and-execute in build script — classic supply chain attack",
98 predicate: |f| {
99 has_capability(f, Capability::Network, true)
100 && has_capability(f, Capability::ProcessExec, true)
101 },
102 },
103 BuiltinRule {
104 name: "build-script-file-write",
105 default_severity: GateSeverity::Warn,
106 description: "Build script with filesystem write access",
107 rationale: "Build scripts writing outside OUT_DIR is suspicious",
108 predicate: |f| has_capability(f, Capability::FileWrite, true),
109 },
110 BuiltinRule {
112 name: "proc-macro-network",
113 default_severity: GateSeverity::Deny,
114 description: "Proc macro with network access",
115 rationale: "Proc macros have no legitimate reason for network access",
116 predicate: |f| {
117 has_capability(f, Capability::ProcMacro, false)
118 && has_capability(f, Capability::Network, false)
119 },
120 },
121 BuiltinRule {
122 name: "proc-macro-exec",
123 default_severity: GateSeverity::Deny,
124 description: "Proc macro spawning processes",
125 rationale: "Proc macros have no legitimate reason to spawn processes",
126 predicate: |f| {
127 has_capability(f, Capability::ProcMacro, false)
128 && has_capability(f, Capability::ProcessExec, false)
129 },
130 },
131 BuiltinRule {
132 name: "proc-macro-file-write",
133 default_severity: GateSeverity::Deny,
134 description: "Proc macro with filesystem write access",
135 rationale: "Proc macros should not write to the filesystem",
136 predicate: |f| {
137 has_capability(f, Capability::ProcMacro, false)
138 && has_capability(f, Capability::FileWrite, false)
139 },
140 },
141 BuiltinRule {
143 name: "env-access-network",
144 default_severity: GateSeverity::Info,
145 description: "Environment variable access with network capability",
146 rationale: "Reading environment variables and accessing network — review for credential harvesting",
147 predicate: |f| {
148 has_capability(f, Capability::EnvAccess, false)
149 && has_capability(f, Capability::Network, false)
150 },
151 },
152 BuiltinRule {
153 name: "key-material-network",
154 default_severity: GateSeverity::Warn,
155 description: "Embedded key material with network access",
156 rationale: "Embedded key material with network access — verify intent",
157 predicate: |f| has_capability(f, Capability::Network, false) && has_key_material(f),
158 },
159];
160
161fn has_key_material(findings: &[CapabilityFinding]) -> bool {
167 findings
168 .iter()
169 .any(|f| f.capability == Capability::Crypto && !f.evidence.contains("::"))
170}
171
172struct FlowRule {
174 name: &'static str,
175 default_severity: GateSeverity,
176 description: &'static str,
177 rationale: &'static str,
178 predicate: fn(&[DataFlowFact]) -> bool,
179}
180
181fn has_flow(data_flows: &[DataFlowFact], source: Capability, sink: Capability) -> bool {
182 data_flows
183 .iter()
184 .any(|f| f.source_capability == source && f.sink_capability == sink)
185}
186
187const FLOW_RULES: &[FlowRule] = &[
188 FlowRule {
189 name: "env-to-network",
190 default_severity: GateSeverity::Deny,
191 description: "Data flows from environment variable to network sink",
192 rationale: "Environment variable value reaches a network call — potential credential exfiltration",
193 predicate: |f| has_flow(f, Capability::EnvAccess, Capability::Network),
194 },
195 FlowRule {
196 name: "file-to-network",
197 default_severity: GateSeverity::Deny,
198 description: "Data flows from file read to network sink",
199 rationale: "File content reaches a network call — potential data exfiltration",
200 predicate: |f| has_flow(f, Capability::FileRead, Capability::Network),
201 },
202 FlowRule {
203 name: "network-to-exec",
204 default_severity: GateSeverity::Deny,
205 description: "Data flows from network source to process execution",
206 rationale: "Network-sourced data reaches process execution — remote code execution risk",
207 predicate: |f| has_flow(f, Capability::Network, Capability::ProcessExec),
208 },
209];
210
211pub fn all_gate_rules() -> Box<[GateRuleInfo]> {
213 BUILTIN_RULES
214 .iter()
215 .map(|r| GateRuleInfo {
216 name: r.name,
217 default_severity: r.default_severity,
218 description: r.description,
219 })
220 .chain(FLOW_RULES.iter().map(|r| GateRuleInfo {
221 name: r.name,
222 default_severity: r.default_severity,
223 description: r.description,
224 }))
225 .collect()
226}
227
228pub fn evaluate_gate_rules(
233 findings: &[CapabilityFinding],
234 data_flows: &[DataFlowFact],
235 config: &GateConfig,
236) -> Box<[GateVerdict]> {
237 if !config.enabled {
238 return Box::new([]);
239 }
240
241 let capability_verdicts = BUILTIN_RULES.iter().filter_map(|rule| {
242 let severity = resolve_severity(rule.name, rule.default_severity, config)?;
243 (rule.predicate)(findings).then_some(GateVerdict {
244 rule: rule.name,
245 severity,
246 rationale: rule.rationale,
247 })
248 });
249
250 let flow_verdicts = FLOW_RULES.iter().filter_map(|rule| {
251 let severity = resolve_severity(rule.name, rule.default_severity, config)?;
252 (rule.predicate)(data_flows).then_some(GateVerdict {
253 rule: rule.name,
254 severity,
255 rationale: rule.rationale,
256 })
257 });
258
259 capability_verdicts.chain(flow_verdicts).collect()
260}
261
262fn resolve_severity(
264 name: &str,
265 default: GateSeverity,
266 config: &GateConfig,
267) -> Option<GateSeverity> {
268 match config.overrides.get(name) {
269 Some(GateRuleOverride::Disabled) => None,
270 Some(GateRuleOverride::Severity(s)) => Some(*s),
271 None => Some(default),
272 }
273}