arch_toolkit/types/sandbox.rs
1//! Sandbox-related data types for build-preflight dependency analysis.
2
3use serde::{Deserialize, Serialize};
4
5/// What: Status of one declared dependency relative to the host environment.
6///
7/// Inputs:
8/// - Produced by `sandbox::analyze_dependencies()` and the analysis entry points.
9///
10/// Output:
11/// - Installation and version-constraint status for a single dependency spec.
12///
13/// Details:
14/// - `name` keeps the full spec as declared (e.g., `foo>=1.2` or
15/// `bar: enables feature X` for optdepends) so callers can display it verbatim.
16/// - `version_satisfied` is `false` when the package is not installed; when
17/// installed without a declared constraint it is `true`.
18#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
19pub struct DependencyDelta {
20 /// Dependency spec as declared (may include version requirement or description).
21 pub name: String,
22 /// Whether this dependency is installed (or provided) on the host.
23 pub is_installed: bool,
24 /// Installed version when available (from `pacman -Q`).
25 pub installed_version: Option<String>,
26 /// Whether the installed version satisfies the declared constraint.
27 pub version_satisfied: bool,
28}
29
30/// What: Build-preflight analysis result for a package.
31///
32/// Inputs:
33/// - Produced by `sandbox::analyze_pkgbuild()` / `sandbox::analyze_srcinfo()`.
34///
35/// Output:
36/// - Per-category dependency deltas comparing the package's declared
37/// dependencies against the host.
38///
39/// Details:
40/// - Ported from Pacsea's `SandboxInfo`; answers "what would I need to install
41/// to build this AUR package?" before any build starts.
42#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
43pub struct SandboxInfo {
44 /// Package name the analysis belongs to.
45 pub package_name: String,
46 /// Runtime dependencies (`depends`).
47 pub depends: Vec<DependencyDelta>,
48 /// Build-time dependencies (`makedepends`).
49 pub makedepends: Vec<DependencyDelta>,
50 /// Test dependencies (`checkdepends`).
51 pub checkdepends: Vec<DependencyDelta>,
52 /// Optional dependencies (`optdepends`).
53 pub optdepends: Vec<DependencyDelta>,
54}
55
56impl SandboxInfo {
57 /// What: List dependency specs that are not installed on the host.
58 ///
59 /// Inputs: None.
60 ///
61 /// Output:
62 /// - Specs from `depends`, `makedepends`, and `checkdepends` that are missing.
63 ///
64 /// Details:
65 /// - Optional dependencies are excluded; they do not block a build.
66 #[must_use]
67 pub fn missing_packages(&self) -> Vec<&str> {
68 self.depends
69 .iter()
70 .chain(&self.makedepends)
71 .chain(&self.checkdepends)
72 .filter(|delta| !delta.is_installed)
73 .map(|delta| delta.name.as_str())
74 .collect()
75 }
76
77 /// What: Check whether all build-relevant dependencies are installed.
78 ///
79 /// Inputs: None.
80 ///
81 /// Output:
82 /// - `true` when every entry in `depends`, `makedepends`, and
83 /// `checkdepends` is installed on the host.
84 ///
85 /// Details:
86 /// - Optional dependencies are excluded; version constraints are reported
87 /// per-delta but do not affect this readiness check (pacman would
88 /// upgrade them during install).
89 #[must_use]
90 pub fn is_ready_to_build(&self) -> bool {
91 self.depends
92 .iter()
93 .chain(&self.makedepends)
94 .chain(&self.checkdepends)
95 .all(|delta| delta.is_installed)
96 }
97}
98
99/// What: Identify one stable static PKGBUILD threat-model rule.
100///
101/// Inputs:
102/// - Produced by [`crate::sandbox::analyze_pkgbuild_security`] when matching
103/// text is found in a PKGBUILD.
104///
105/// Output:
106/// - A stable serialized `SB00x` identifier suitable for callers to filter or
107/// present without relying on an opaque aggregate score.
108///
109/// Details:
110/// - Rules describe potentially risky shell constructs, not proof of malicious
111/// intent. They are intentionally deterministic and text-only.
112#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
113pub enum SandboxRuleId {
114 /// `SB001`: Command substitution can execute a dynamically constructed command.
115 #[serde(rename = "SB001")]
116 CommandSubstitution,
117 /// `SB002`: A download command can retrieve unreviewed remote content.
118 #[serde(rename = "SB002")]
119 RemoteDownload,
120 /// `SB003`: A privilege escalation command expands the impact of a build step.
121 #[serde(rename = "SB003")]
122 PrivilegedCommand,
123 /// `SB004`: Recursive forced removal can destroy files outside a package build tree.
124 #[serde(rename = "SB004")]
125 DestructiveRemoval,
126 /// `SB005`: Dynamic evaluation obscures the command text that will run.
127 #[serde(rename = "SB005")]
128 DynamicEvaluation,
129}
130
131impl SandboxRuleId {
132 /// What: Return the stable textual identifier for this static-analysis rule.
133 ///
134 /// Inputs: None.
135 ///
136 /// Output:
137 /// - One of `SB001` through `SB005`.
138 ///
139 /// Details:
140 /// - The value matches the enum's serde representation and is stable for
141 /// caller-side policy, fixture, and display code.
142 #[must_use]
143 pub const fn as_str(self) -> &'static str {
144 match self {
145 Self::CommandSubstitution => "SB001",
146 Self::RemoteDownload => "SB002",
147 Self::PrivilegedCommand => "SB003",
148 Self::DestructiveRemoval => "SB004",
149 Self::DynamicEvaluation => "SB005",
150 }
151 }
152}
153
154/// What: Record evidence for one deterministic static PKGBUILD finding.
155///
156/// Inputs:
157/// - Produced from one matched source line during text-only analysis.
158///
159/// Output:
160/// - Stable rule ID, one-based line number, and a bounded source excerpt.
161///
162/// Details:
163/// - Evidence is not executed, expanded, or parsed as full shell syntax.
164/// - A finding flags review-worthy text, not a proven exploit or reputation score.
165#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
166pub struct SandboxFinding {
167 /// Stable rule identifier describing the matched construct.
168 pub rule_id: SandboxRuleId,
169 /// One-based PKGBUILD line containing the evidence.
170 pub line: usize,
171 /// Bounded source excerpt retained exactly for caller review.
172 pub evidence: String,
173}
174
175/// What: State a known limitation of deterministic static PKGBUILD analysis.
176///
177/// Inputs:
178/// - Included in every [`SandboxStaticAnalysis`] result.
179///
180/// Output:
181/// - Structured, explicit scope information rather than an implied guarantee.
182///
183/// Details:
184/// - Limitations are stable categories so callers can present or persist the
185/// analysis boundary alongside findings.
186#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
187pub enum SandboxAnalysisLimitation {
188 /// Analysis reads raw text only and never executes PKGBUILD content.
189 #[serde(rename = "text-only-no-execution")]
190 TextOnlyNoExecution,
191 /// The scanner does not implement a complete Bash parser or expansion model.
192 #[serde(rename = "not-a-full-shell-parser")]
193 NotFullShellParser,
194 /// Remote reputation, signatures, and external scanner results are not included.
195 #[serde(rename = "no-external-reputation-or-scanner")]
196 NoExternalReputationOrScanner,
197 /// Findings identify review signals and can include false negatives or positives.
198 #[serde(rename = "not-proof-of-malicious-intent")]
199 NotProofOfMaliciousIntent,
200}
201
202/// What: Hold a text-only PKGBUILD threat-model analysis result.
203///
204/// Inputs:
205/// - Produced by [`crate::sandbox::analyze_pkgbuild_security`] from a package
206/// name and unexecuted PKGBUILD text.
207///
208/// Output:
209/// - Structured stable-rule findings and explicit scanner limitations.
210///
211/// Details:
212/// - No aggregate risk score is calculated.
213/// - The report can be serialized for caller-owned review workflows without
214/// granting the library authority to execute or build the PKGBUILD.
215#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
216pub struct SandboxStaticAnalysis {
217 /// Caller-provided package name associated with the analyzed text.
218 pub package_name: String,
219 /// Findings in source-line and stable-rule order.
220 pub findings: Vec<SandboxFinding>,
221 /// Explicit scope and correctness limitations of this text-only result.
222 pub limitations: Vec<SandboxAnalysisLimitation>,
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 fn delta(name: &str, installed: bool) -> DependencyDelta {
230 DependencyDelta {
231 name: name.to_string(),
232 is_installed: installed,
233 installed_version: installed.then(|| "1.0".to_string()),
234 version_satisfied: installed,
235 }
236 }
237
238 #[test]
239 /// What: Verify `missing_packages` collects non-installed build deps only.
240 ///
241 /// Inputs:
242 /// - Info with missing entries in each category including optdepends.
243 ///
244 /// Output:
245 /// - Missing depends/makedepends/checkdepends; optdepends excluded.
246 ///
247 /// Details:
248 /// - Optional dependencies never block a build.
249 fn missing_packages_excludes_optdepends() {
250 let info = SandboxInfo {
251 package_name: "pkg".to_string(),
252 depends: vec![delta("a", true), delta("b", false)],
253 makedepends: vec![delta("c", false)],
254 checkdepends: vec![delta("d", true)],
255 optdepends: vec![delta("e", false)],
256 };
257 assert_eq!(info.missing_packages(), ["b", "c"]);
258 }
259
260 #[test]
261 /// What: Verify `is_ready_to_build` requires all build deps installed.
262 ///
263 /// Inputs:
264 /// - Info variants with and without missing build dependencies.
265 ///
266 /// Output:
267 /// - `true` only when depends/makedepends/checkdepends are all installed.
268 ///
269 /// Details:
270 /// - Missing optdepends must not affect readiness.
271 fn readiness() {
272 let ready = SandboxInfo {
273 package_name: "pkg".to_string(),
274 depends: vec![delta("a", true)],
275 makedepends: vec![delta("b", true)],
276 checkdepends: vec![],
277 optdepends: vec![delta("c", false)],
278 };
279 assert!(ready.is_ready_to_build());
280
281 let not_ready = SandboxInfo {
282 makedepends: vec![delta("b", false)],
283 ..ready
284 };
285 assert!(!not_ready.is_ready_to_build());
286 }
287
288 #[test]
289 /// What: Verify serde roundtrip and Default for `SandboxInfo`.
290 ///
291 /// Inputs:
292 /// - Populated info serialized to JSON and back; `SandboxInfo::default()`.
293 ///
294 /// Output:
295 /// - Roundtrip equality; default is empty and ready to build.
296 ///
297 /// Details:
298 /// - Supports caller-side caching of analysis results.
299 fn serde_and_default() {
300 let info = SandboxInfo {
301 package_name: "pkg".to_string(),
302 depends: vec![delta("glibc>=2.38", true)],
303 ..Default::default()
304 };
305 let back: SandboxInfo =
306 serde_json::from_str(&serde_json::to_string(&info).expect("serialize"))
307 .expect("deserialize");
308 assert_eq!(back, info);
309
310 let empty = SandboxInfo::default();
311 assert!(empty.missing_packages().is_empty());
312 assert!(empty.is_ready_to_build());
313 }
314}