1use crate::{
11 doctor::System,
12 history,
13 reliability::{Assessment, Evidence},
14 updates::Install,
15};
16use helixforge_core::{Action, Entry, NixAction, PackageManager, printable};
17use helixforge_platform::LinuxInfo;
18use serde::Serialize;
19use sha2::{Digest, Sha256};
20use std::path::{Path, PathBuf};
21
22#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
24#[serde(rename_all = "kebab-case")]
25pub enum Standing {
26 Provenance,
28 Checked,
30 Pinned,
32 Unverified,
34 Unknown,
36}
37
38impl Standing {
39 pub fn label(self) -> &'static str {
40 match self {
41 Standing::Provenance => "Release provenance",
42 Standing::Checked => "Integrity checked",
43 Standing::Pinned => "Pinned revision",
44 Standing::Unverified => "Unverified",
45 Standing::Unknown => "Unknown",
46 }
47 }
48}
49
50#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
52pub struct Fact {
53 pub standing: Standing,
54 pub subject: String,
56 pub text: String,
57}
58
59fn fact(standing: Standing, subject: impl Into<String>, text: impl Into<String>) -> Fact {
60 Fact {
61 standing,
62 subject: subject.into(),
63 text: printable(&text.into()),
64 }
65}
66
67#[derive(Clone, Debug)]
69pub struct Build {
70 pub version: String,
71 pub release: bool,
73 pub commit: Option<String>,
75 pub install: Install,
76}
77
78#[derive(Clone, Debug, Serialize)]
80pub struct Inspection {
81 pub id: String,
82 pub name: String,
83 pub kind: &'static str,
85 pub origin: String,
87 pub file: Option<String>,
89 pub sha256: Option<String>,
90 pub licence: Option<String>,
92 pub software_licence: Option<String>,
94 pub provenance: Vec<Fact>,
96 pub integrity: Vec<Fact>,
98 pub downloads: Vec<Fact>,
100 pub privileges: Vec<String>,
102 pub validation: Assessment,
103}
104
105pub fn inspect(
107 entry: &Entry,
108 root: &Path,
109 platform: &LinuxInfo,
110 build: &Build,
111 system: &dyn System,
112 evidence: &Evidence,
113 history: &[history::Record],
114) -> Inspection {
115 let mut inspection = Inspection {
116 id: entry.id.clone(),
117 name: entry.name.clone(),
118 kind: "command",
119 origin: "the HelixForge catalog".to_string(),
120 file: None,
121 sha256: None,
122 licence: Some("MIT (HelixForge)".to_string()),
123 software_licence: None,
124 provenance: provenance(build),
125 integrity: Vec::new(),
126 downloads: Vec::new(),
127 privileges: privileges(entry),
128 validation: evidence.assess(entry, history),
129 };
130 match &entry.action {
131 Action::Package { .. } => {
132 inspection.kind = "packages";
133 inspection.origin =
134 "commands HelixForge builds from the package names in its catalog".to_string();
135 }
136 Action::Script { file, .. } => {
137 inspection.kind = "script";
138 let scripts = root.join("scripts");
139 let relative = file
140 .strip_prefix(&scripts)
141 .unwrap_or(file)
142 .display()
143 .to_string();
144 inspection.sha256 = sum(file);
145 let (origin, licence) = script_origin(&relative);
146 inspection.origin = origin;
147 inspection.licence = Some(licence);
148 inspection.file = Some(printable(&relative));
149 let texts = script_texts(file);
150 inspection.downloads = downloads(&texts, &scripts);
151 inspection.privileges.extend(
152 elevated_commands(&texts)
153 .into_iter()
154 .map(|command| format!("runs {command} as root")),
155 );
156 }
157 Action::Command { line } => {
158 inspection.kind = "command";
159 inspection.origin = format!("a command line in the catalog: {}", printable(line));
160 }
161 Action::Nix { action } => {
162 inspection.kind = "nix";
163 let (what, text) = match action {
164 NixAction::ProfileInstall { installable } => (
165 installable,
166 "Nix builds or downloads it and checks every store path's hash; a flake's lock file pins its inputs",
167 ),
168 NixAction::HomeManagerSwitch { flake } => (
169 flake,
170 "Home Manager switches to this flake; Nix checks every store path's hash, and the flake's lock file pins its inputs",
171 ),
172 };
173 inspection.origin = format!("Nix: {}", printable(what));
174 inspection
175 .integrity
176 .push(fact(Standing::Checked, "Nix", text));
177 }
178 Action::Native { action } => {
179 inspection.kind = "native";
180 inspection.origin = format!(
181 "HelixForge itself (helixforge native {}), reading the system",
182 action.id()
183 );
184 }
185 Action::Profile { entries } => {
186 inspection.kind = "profile";
187 inspection.origin = format!(
188 "a profile of {} entries; inspect each one: {}",
189 entries.len(),
190 printable(&entries.join(", "))
191 );
192 }
193 }
194 if let Some(identity) = entry.identity() {
195 inspection.integrity.extend(integrity(
196 identity.packages.for_manager_any(platform.manager),
197 identity.aur,
198 identity.flatpak,
199 platform,
200 system,
201 ));
202 inspection.software_licence = software_licence(
203 identity.packages.for_manager_any(platform.manager),
204 identity.flatpak,
205 platform,
206 system,
207 );
208 }
209 inspection.integrity.sort_by_key(|fact| fact.standing);
210 inspection
211 .downloads
212 .sort_by_key(|fact| std::cmp::Reverse(fact.standing));
213 inspection
214}
215
216fn provenance(build: &Build) -> Vec<Fact> {
218 let subject = format!("HelixForge {}", build.version);
219 let commit = build
220 .commit
221 .as_deref()
222 .map(|commit| format!(" of commit {}", commit.chars().take(12).collect::<String>()))
223 .unwrap_or_default();
224 if !build.release {
225 return vec![fact(
226 Standing::Unknown,
227 subject,
228 match build.install {
229 Install::Cargo => "built on this machine by cargo install; crates.io checked the source's checksums, and the catalog and scripts came with it, but there is no build provenance for this copy".to_string(),
230 _ => "built from source, not by the release workflow, so there is no build provenance; the catalog and scripts are those of the checkout".to_string(),
231 },
232 )];
233 }
234 let mut facts = vec![fact(
235 Standing::Provenance,
236 subject.clone(),
237 format!(
238 "a release build{commit}: the catalog and every installer ship inside it, and each release file has a signed build provenance attestation; gh attestation verify <file> --repo Commanderx-code/Helix-Forge shows which workflow built it"
239 ),
240 )];
241 match build.install {
242 Install::Launcher | Install::Download => facts.push(fact(
243 Standing::Checked,
244 subject,
245 "run from where the launcher or start.sh keeps it; both check the download against the release's SHA256SUMS before running it",
246 )),
247 Install::Package => facts.push(fact(
248 Standing::Checked,
249 subject,
250 "installed from the release's package; its checksum is in the release's SHA256SUMS",
251 )),
252 _ => {}
253 }
254 facts
255}
256
257fn script_origin(relative: &str) -> (String, String) {
259 if relative.starts_with("installers/commander/") {
260 (
261 "Commander Toolbox's installers, maintained in the HelixForge repository (github.com/Commanderx-code/Helix-Forge)".to_string(),
262 "MIT (HelixForge)".to_string(),
263 )
264 } else if relative.starts_with("installers/") {
265 (
266 "Linutil's catalog (github.com/ChrisTitusTech/linutil), carried into the HelixForge repository".to_string(),
267 "MIT (Linutil; scripts/installers/LICENSE-linutil)".to_string(),
268 )
269 } else {
270 (
271 "the HelixForge repository".to_string(),
272 "MIT (HelixForge)".to_string(),
273 )
274 }
275}
276
277fn sum(path: &Path) -> Option<String> {
278 let bytes = std::fs::read(path).ok()?;
279 Some(
280 Sha256::digest(&bytes)
281 .iter()
282 .map(|byte| format!("{byte:02x}"))
283 .collect(),
284 )
285}
286
287fn script_texts(file: &Path) -> Vec<String> {
290 let mut texts = Vec::new();
291 let Ok(text) = std::fs::read_to_string(file) else {
292 return texts;
293 };
294 let directory = file.parent().unwrap_or(Path::new("."));
295 let mut seen: Vec<PathBuf> = vec![file.to_path_buf()];
296 for word in text
297 .split(|character: char| !(character.is_ascii_alphanumeric() || "-_.".contains(character)))
298 {
299 if !word.ends_with(".sh") || word.starts_with("common") {
300 continue;
301 }
302 let other = directory.join(word);
303 if !seen.contains(&other)
304 && other.is_file()
305 && let Ok(more) = std::fs::read_to_string(&other)
306 {
307 seen.push(other);
308 texts.push(more);
309 }
310 }
311 texts.insert(0, text);
312 texts
313}
314
315fn pins(scripts: &Path) -> Vec<(String, String, String)> {
317 let Ok(text) = std::fs::read_to_string(scripts.join("installers/commander/common.sh")) else {
318 return Vec::new();
319 };
320 text.lines()
321 .filter_map(|line| {
322 let (name, rest) = line.trim().split_once(") ")?;
323 let revision = rest
324 .split("revision=")
325 .nth(1)?
326 .split(|c: char| !c.is_ascii_hexdigit())
327 .next()?;
328 if revision.len() != 40 || name.contains(['*', '|', ' ']) {
329 return None;
330 }
331 let repository = rest
332 .split("source_repo=")
333 .nth(1)
334 .and_then(|value| value.split(';').next())
335 .map(str::to_string)
336 .unwrap_or_else(|| format!("Commanderx-code/{name}"));
337 Some((name.to_string(), repository, revision.to_string()))
338 })
339 .collect()
340}
341
342fn downloads(texts: &[String], scripts: &Path) -> Vec<Fact> {
344 let mut facts = Vec::new();
345 let all = texts.join("\n");
346 let pins = pins(scripts);
347 for (name, repository, revision) in &pins {
348 if all.contains(&format!("fetch_source {name}"))
349 || (name == "dotfiles" || name == "Myfish")
350 && all.contains("fetch_source \"$source_name\"")
351 {
352 facts.push(fact(
353 Standing::Pinned,
354 "Download",
355 format!(
356 "github.com/{repository} at commit {}, checked after the download",
357 &revision[..12]
358 ),
359 ));
360 }
361 }
362 let checks = [
363 "sha256sum",
364 "sha512sum",
365 "gpg --verify",
366 "minisign",
367 "cosign verify",
368 ]
369 .into_iter()
370 .filter(|tool| all.contains(tool))
371 .collect::<Vec<_>>();
372 let mut urls: Vec<String> = Vec::new();
373 for line in all.lines() {
374 let line = line.trim();
375 if line.starts_with('#') {
376 continue;
377 }
378 for (start, _) in line.match_indices("http") {
379 let rest = &line[start..];
380 if !(rest.starts_with("https://") || rest.starts_with("http://")) {
381 continue;
382 }
383 let url: String = rest
384 .chars()
385 .take_while(|character| {
386 !character.is_whitespace() && !"\"'()<>|;`".contains(*character)
387 })
388 .collect();
389 if url.len() < 12 || urls.contains(&url) {
390 continue;
391 }
392 urls.push(url.clone());
393 let piped = line.contains("| sh") || line.contains("| bash") || line.contains("| sudo");
394 let (standing, text) = if url.starts_with("http://") {
395 (
396 Standing::Unverified,
397 format!("{url} over plain HTTP, which anyone on the network can change"),
398 )
399 } else if piped {
400 (
401 Standing::Unverified,
402 format!("{url}, run straight into a shell without a check"),
403 )
404 } else if !checks.is_empty() {
405 (
406 Standing::Checked,
407 format!(
408 "{url}; the script checks downloads with {}",
409 checks.join(", ")
410 ),
411 )
412 } else {
413 (
414 Standing::Unverified,
415 format!("{url}, over HTTPS but with no checksum or signature check"),
416 )
417 };
418 facts.push(fact(standing, "Download", text));
419 }
420 }
421 if all.contains("git clone") && !all.contains("fetch_source") {
422 facts.push(fact(
423 Standing::Unverified,
424 "Download",
425 "clones a Git repository at whatever its branch holds when it runs",
426 ));
427 }
428 facts
429}
430
431fn elevated_commands(texts: &[String]) -> Vec<String> {
433 let mut commands: Vec<String> = Vec::new();
434 for text in texts {
435 for line in text.lines() {
436 if line.trim_start().starts_with('#') {
437 continue;
438 }
439 for marker in ["sudo ", "\"$ESCALATION_TOOL\" ", "$ESCALATION_TOOL "] {
440 for (start, _) in line.match_indices(marker) {
441 let word: String = line[start + marker.len()..]
442 .trim_start()
443 .trim_start_matches('"')
444 .chars()
445 .take_while(|character| {
446 character.is_ascii_alphanumeric() || "-_.$".contains(*character)
447 })
448 .collect();
449 let word = match word.as_str() {
450 "$PACKAGER" | "${PACKAGER}" => "the package manager".to_string(),
451 _ => word,
452 };
453 if !word.is_empty() && !word.starts_with('-') && !commands.contains(&word) {
454 commands.push(word);
455 }
456 }
457 }
458 }
459 }
460 commands.sort();
461 commands
462}
463
464fn privileges(entry: &Entry) -> Vec<String> {
466 let mut found = Vec::new();
467 if helixforge_core::privileged(&entry.flags, &entry.action) {
468 found.push("asks for your password (sudo) while it runs".to_string());
469 } else {
470 found.push("runs as your user, without asking for a password".to_string());
471 }
472 let flags: Vec<&str> = entry.flags.split_whitespace().collect();
473 for (flag, text) in [
474 ("D", "changes disks, such as partitions"),
475 ("K", "changes the kernel"),
476 ("SI", "installs a whole system"),
477 ("SS", "starts, stops or enables services"),
478 ("PFM", "changes system files"),
479 ("FM", "changes your files"),
480 ("RP", "removes packages"),
481 ] {
482 if flags.contains(&flag) {
483 found.push(text.to_string());
484 }
485 }
486 for path in &entry.writes {
487 found.push(format!("writes {}", printable(path)));
488 }
489 found
490}
491
492fn integrity(
494 native: Option<(PackageManager, &str)>,
495 aur: Option<&str>,
496 flatpak: Option<&str>,
497 platform: &LinuxInfo,
498 system: &dyn System,
499) -> Vec<Fact> {
500 let mut facts = Vec::new();
501 if let Some((manager, name)) = native {
502 facts.push(match manager {
503 PackageManager::Arch => {
504 let level = system
505 .read(Path::new("/etc/pacman.conf"))
506 .and_then(|text| sig_level(&text))
507 .unwrap_or_else(|| "Required DatabaseOptional".to_string());
508 if level.contains("Never") || level.contains("TrustAll") {
509 fact(
510 Standing::Unverified,
511 format!("pacman ({name})"),
512 format!(
513 "pacman.conf has SigLevel = {level}, so package signatures are not required"
514 ),
515 )
516 } else {
517 fact(
518 Standing::Checked,
519 format!("pacman ({name})"),
520 format!(
521 "pacman checks each package's signature against the distribution's keyring (SigLevel = {level})"
522 ),
523 )
524 }
525 }
526 PackageManager::Debian => {
527 let trusted = ["/etc/apt/sources.list"]
528 .iter()
529 .map(PathBuf::from)
530 .chain(
531 system
532 .list(Path::new("/etc/apt/sources.list.d"))
533 .into_iter()
534 .map(|name| PathBuf::from("/etc/apt/sources.list.d").join(name)),
535 )
536 .filter_map(|path| system.read(&path))
537 .any(|text| text.contains("trusted=yes") || text.contains("Trusted: yes"));
538 if trusted {
539 fact(
540 Standing::Unverified,
541 format!("apt ({name})"),
542 "a repository is marked trusted=yes, so apt does not check its signature",
543 )
544 } else {
545 fact(
546 Standing::Checked,
547 format!("apt ({name})"),
548 "apt checks the repository's signed Release file and each package's checksum",
549 )
550 }
551 }
552 PackageManager::Fedora => {
553 let off = system
554 .read(Path::new("/etc/dnf/dnf.conf"))
555 .is_some_and(|text| {
556 text.lines().any(|line| {
557 let line = line.replace(' ', "").to_lowercase();
558 line == "gpgcheck=0" || line == "gpgcheck=false"
559 })
560 });
561 if off {
562 fact(
563 Standing::Unverified,
564 format!("dnf ({name})"),
565 "dnf.conf turns gpgcheck off, so package signatures are not checked",
566 )
567 } else {
568 fact(
569 Standing::Checked,
570 format!("dnf ({name})"),
571 "dnf checks each package's GPG signature (gpgcheck)",
572 )
573 }
574 }
575 PackageManager::Suse => fact(
576 Standing::Checked,
577 format!("zypper ({name})"),
578 "zypper checks the repository's and each package's GPG signature",
579 ),
580 other => fact(
581 Standing::Checked,
582 format!("{} ({name})", other.executable()),
583 "the distribution's package manager checks its packages' signatures",
584 ),
585 });
586 }
587 if let Some(name) = aur.filter(|_| platform.manager == Some(PackageManager::Arch)) {
588 facts.push(fact(
589 Standing::Unverified,
590 format!("AUR ({name})"),
591 "if pacman has no package, the AUR's build script is used: users submit them and nobody reviews them; paru or yay shows it before building, and checks the sums it lists for its downloads",
592 ));
593 }
594 if let Some(id) = flatpak {
595 facts.push(fact(
596 Standing::Checked,
597 format!("Flathub ({id})"),
598 "Flatpak checks Flathub's signed commits; who publishes the app on Flathub is shown on its Flathub page",
599 ));
600 }
601 facts
602}
603
604fn sig_level(text: &str) -> Option<String> {
606 let mut options = false;
607 for line in text.lines() {
608 let line = line.trim();
609 if line.starts_with('[') {
610 options = line == "[options]";
611 } else if options
612 && let Some(value) = line.strip_prefix("SigLevel")
613 && let Some(value) = value.trim_start().strip_prefix('=')
614 {
615 return Some(value.trim().to_string());
616 }
617 }
618 None
619}
620
621fn software_licence(
623 native: Option<(PackageManager, &str)>,
624 flatpak: Option<&str>,
625 _platform: &LinuxInfo,
626 system: &dyn System,
627) -> Option<String> {
628 let field = |text: &str, key: &str| {
629 text.lines().find_map(|line| {
630 let (name, value) = line.split_once(':')?;
631 (name.trim() == key).then(|| printable(value.trim()))
632 })
633 };
634 if let Some((manager, name)) = native {
635 let found = match manager {
636 PackageManager::Arch => system
637 .run(&["pacman", "-Qi", name])
638 .filter(|(ok, _)| *ok)
639 .or_else(|| system.run(&["pacman", "-Si", name]).filter(|(ok, _)| *ok))
640 .and_then(|(_, text)| field(&text, "Licenses")),
641 PackageManager::Fedora | PackageManager::Suse => system
642 .run(&["rpm", "-q", "--qf", "%{LICENSE}", name])
643 .filter(|(ok, _)| *ok)
644 .map(|(_, text)| printable(text.trim())),
645 PackageManager::Debian => {
646 let copyright = PathBuf::from(format!("/usr/share/doc/{name}/copyright"));
647 system
648 .exists(©right)
649 .then(|| format!("in {}", copyright.display()))
650 }
651 _ => None,
652 };
653 if found.as_deref().is_some_and(|text| !text.is_empty()) {
654 return found;
655 }
656 }
657 let id = flatpak?;
658 system
659 .run(&["flatpak", "info", id])
660 .filter(|(ok, _)| *ok)
661 .and_then(|(_, text)| field(&text, "License"))
662}
663
664trait ForManagerAny {
666 fn for_manager_any(&self, manager: Option<PackageManager>) -> Option<(PackageManager, &str)>;
667}
668
669impl ForManagerAny for helixforge_core::PackageNames {
670 fn for_manager_any(&self, manager: Option<PackageManager>) -> Option<(PackageManager, &str)> {
671 let manager = manager?;
672 self.for_manager(manager).map(|name| (manager, name))
673 }
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679 use helixforge_catalog::Catalog;
680 use std::collections::HashMap;
681
682 #[derive(Default)]
684 struct Fake {
685 files: HashMap<PathBuf, String>,
686 commands: HashMap<String, String>,
687 }
688
689 impl System for Fake {
690 fn read(&self, path: &Path) -> Option<String> {
691 self.files.get(path).cloned()
692 }
693 fn exists(&self, path: &Path) -> bool {
694 self.files.contains_key(path)
695 }
696 fn list(&self, _: &Path) -> Vec<String> {
697 Vec::new()
698 }
699 fn link(&self, _: &Path) -> Option<PathBuf> {
700 None
701 }
702 fn run(&self, command: &[&str]) -> Option<(bool, String)> {
703 self.commands
704 .get(&command.join(" "))
705 .map(|text| (true, text.clone()))
706 }
707 fn resolve(&self, _: &str) -> Result<Vec<String>, String> {
708 Err("offline".into())
709 }
710 fn space(&self, _: &Path) -> Option<(u64, u64)> {
711 None
712 }
713 fn owner(&self, _: &Path) -> Option<u32> {
714 None
715 }
716 fn is_link(&self, _: &Path) -> bool {
717 false
718 }
719 fn canonical(&self, _: &Path) -> Option<PathBuf> {
720 None
721 }
722 fn user(&self) -> (u32, u32, PathBuf) {
723 (1000, 1000, PathBuf::from("/home/me"))
724 }
725 fn on_path(&self, _: &str) -> bool {
726 false
727 }
728 fn processes(&self) -> Vec<String> {
729 Vec::new()
730 }
731 }
732
733 fn arch() -> LinuxInfo {
734 LinuxInfo {
735 id: "arch".into(),
736 pretty_name: "Arch Linux".into(),
737 id_like: vec![],
738 manager: Some(PackageManager::Arch),
739 }
740 }
741
742 fn release() -> Build {
743 Build {
744 version: "1.2.0".into(),
745 release: true,
746 commit: Some("0123456789abcdef0123456789abcdef01234567".into()),
747 install: Install::Package,
748 }
749 }
750
751 #[test]
752 fn a_package_entry_shows_each_sources_checks() {
753 let catalog = Catalog::builtin(false).unwrap();
754 let mut fake = Fake::default();
755 fake.files.insert(
756 PathBuf::from("/etc/pacman.conf"),
757 "[options]\nSigLevel = Required DatabaseOptional\n[core]\n".into(),
758 );
759 fake.commands.insert(
760 "pacman -Qi ripgrep".into(),
761 "Name : ripgrep\nLicenses : MIT Unlicense\n".into(),
762 );
763 let found = inspect(
764 catalog.find("ripgrep").unwrap(),
765 catalog.root(),
766 &arch(),
767 &release(),
768 &fake,
769 &Evidence::default(),
770 &[],
771 );
772 assert_eq!(found.kind, "packages");
773 assert_eq!(found.software_licence.as_deref(), Some("MIT Unlicense"));
774 assert_eq!(found.integrity[0].standing, Standing::Checked);
775 assert!(found.integrity[0].text.contains("SigLevel = Required"));
776 assert_eq!(found.provenance[0].standing, Standing::Provenance);
777 assert!(found.provenance[0].text.contains("0123456789ab"));
778 assert!(found.privileges[0].contains("password"));
779
780 fake.files.insert(
782 PathBuf::from("/etc/pacman.conf"),
783 "[options]\nSigLevel = Never\n".into(),
784 );
785 let found = inspect(
786 catalog.find("ripgrep").unwrap(),
787 catalog.root(),
788 &arch(),
789 &release(),
790 &fake,
791 &Evidence::default(),
792 &[],
793 );
794 assert_eq!(found.integrity[0].standing, Standing::Unverified);
795 }
796
797 #[test]
798 fn scripts_show_their_origin_downloads_and_root_commands() {
799 let catalog = Catalog::builtin(false).unwrap();
800 let fake = Fake::default();
801 let inspect_id = |id: &str, build: &Build| {
802 inspect(
803 catalog.find(id).unwrap(),
804 catalog.root(),
805 &arch(),
806 build,
807 &fake,
808 &Evidence::default(),
809 &[],
810 )
811 };
812 let sddm = inspect_id("sddm", &release());
813 assert_eq!(sddm.kind, "script");
814 assert!(sddm.origin.contains("Commander"));
815 assert_eq!(sddm.sha256.as_ref().map(String::len), Some(64));
816 assert!(
817 sddm.downloads
818 .iter()
819 .any(|fact| fact.standing == Standing::Pinned
820 && fact.text.contains("uiriansan/SilentSDDM")),
821 "{:?}",
822 sddm.downloads
823 );
824 let ghostty = inspect_id("dotfiles-ghostty", &release());
826 assert!(
827 ghostty
828 .downloads
829 .iter()
830 .any(|fact| fact.text.contains("Commanderx-code/dotfiles")),
831 "{:?}",
832 ghostty.downloads
833 );
834 let dank = inspect_id("dank-shell", &release());
836 assert!(dank.origin.contains("Linutil"));
837 assert_eq!(dank.downloads[0].standing, Standing::Unverified);
838 assert!(dank.downloads[0].text.contains("install.danklinux.com"));
839 assert!(dank.downloads[0].text.contains("shell"));
840 let local = Build {
842 release: false,
843 commit: None,
844 install: Install::Other,
845 ..release()
846 };
847 let resolve = inspect_id("resolve-native", &local);
848 assert_eq!(resolve.provenance[0].standing, Standing::Unknown);
849 assert!(!resolve.privileges.is_empty());
850 }
851
852 #[test]
853 fn elevated_commands_are_read_from_the_script() {
854 let texts = vec![
855 "sudo pacman -S x\n\"$ESCALATION_TOOL\" systemctl enable sddm\n# sudo rm -rf /\n$ESCALATION_TOOL \"$PACKAGER\" install y\n".to_string(),
856 ];
857 assert_eq!(
858 elevated_commands(&texts),
859 ["pacman", "systemctl", "the package manager"]
860 );
861 }
862}