Skip to main content

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}