1#[cfg(target_os = "linux")]
34use std::collections::BTreeSet;
35use std::path::Path;
36#[cfg(target_os = "linux")]
37use std::path::PathBuf;
38
39use murmur_artifact::{DeclaredNeed, LockOrigin, ProgramNeeds};
40#[cfg(target_os = "linux")]
41use murmur_artifact::{NeedForm, SystemLocation};
42
43pub const NOT_FOUND: &str = "not found on this host";
45
46pub const PROGRAM_NOT_ON_PATH: &str = "program not found on PATH";
48
49pub const FONT_LOCATIONS: &[&str] = &[
51 "/etc/fonts",
52 "/usr/share/fonts",
53 "/usr/local/share/fonts",
54 "/usr/share/fontconfig",
55 "/var/cache/fontconfig",
56];
57
58const OS_RELEASE_PATHS: &[&str] = &["/etc/os-release", "/usr/lib/os-release"];
60
61#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct DeclaredToolNeeds {
64 pub artifact: String,
66 pub origin: LockOrigin,
67 pub programs: Vec<ProgramNeeds>,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct ResolvedProgramNeeds {
73 pub artifact: String,
74 pub program: String,
75 pub granted: Vec<ResolvedNeed>,
77 pub missing: Vec<MissingNeed>,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ResolvedNeed {
85 pub need: DeclaredNeed,
86 pub paths: Vec<NeedPath>,
88}
89
90pub type NeedPath = crate::trust_store::TrustStorePath;
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct MissingNeed {
97 pub need: DeclaredNeed,
98 pub reason: String,
100}
101
102pub fn derive_declared_needs(
115 tools: &[DeclaredToolNeeds],
116 shell_allow: &[String],
117 capsule_writable: &[&Path],
118 pinned: &[crate::pinned_programs::ResolvedPinnedProgram],
119) -> Vec<ResolvedProgramNeeds> {
120 #[cfg(target_os = "linux")]
121 {
122 let mut path_dirs = crate::pinned_programs::host_path_dirs();
123 path_dirs.extend(crate::pinned_programs::pinned_bin_dirs(pinned));
124 let host = NeedHost {
125 path_dirs,
126 search_dirs: crate::sandbox::DEFAULT_LIBRARY_SEARCH_DIRS
127 .iter()
128 .map(PathBuf::from)
129 .collect(),
130 font_locations: FONT_LOCATIONS.iter().map(PathBuf::from).collect(),
131 ca_candidates: crate::trust_store::TRUST_STORE_CANDIDATES,
132 etc_root: PathBuf::from("/etc"),
133 home: std::env::var_os("HOME")
134 .filter(|home| !home.is_empty())
135 .map(PathBuf::from),
136 capsule_writable: capsule_writable
137 .iter()
138 .map(|root| root.to_path_buf())
139 .collect(),
140 pinned_trees: crate::pinned_programs::pinned_trees(pinned),
141 };
142 derive_in(tools, shell_allow, &host)
143 }
144 #[cfg(not(target_os = "linux"))]
145 {
146 let _ = (tools, shell_allow, capsule_writable, pinned);
147 Vec::new()
148 }
149}
150
151#[cfg(target_os = "linux")]
153pub(crate) struct NeedHost<'a> {
154 pub(crate) path_dirs: Vec<PathBuf>,
156 pub(crate) search_dirs: Vec<PathBuf>,
158 pub(crate) font_locations: Vec<PathBuf>,
160 pub(crate) ca_candidates: &'a [crate::trust_store::TrustStoreCandidate<'a>],
162 pub(crate) etc_root: PathBuf,
164 pub(crate) home: Option<PathBuf>,
165 pub(crate) capsule_writable: Vec<PathBuf>,
166 pub(crate) pinned_trees: Vec<PathBuf>,
169}
170
171#[cfg(target_os = "linux")]
173pub(crate) fn derive_in(
174 tools: &[DeclaredToolNeeds],
175 shell_allow: &[String],
176 host: &NeedHost<'_>,
177) -> Vec<ResolvedProgramNeeds> {
178 let unreachable = Unreachable::of(host);
179 let mut resolved = Vec::new();
180 for tool in tools {
181 if tool.origin != LockOrigin::Operator {
182 continue;
183 }
184 for program in &tool.programs {
185 if program.needs.is_empty() || !shell_allow.contains(&program.program) {
186 continue;
187 }
188 resolved.push(resolve_program(&tool.artifact, program, host, &unreachable));
189 }
190 }
191 resolved
192}
193
194#[cfg(target_os = "linux")]
195fn resolve_program(
196 artifact: &str,
197 program: &ProgramNeeds,
198 host: &NeedHost<'_>,
199 unreachable: &Unreachable,
200) -> ResolvedProgramNeeds {
201 let binaries = crate::sandbox::resolve_exec_allowlist_in(
202 std::slice::from_ref(&program.program),
203 &host.path_dirs,
204 );
205 let mut granted = Vec::new();
206 let mut missing = Vec::new();
207 for need in &program.needs {
208 let found = match &need.form {
209 NeedForm::Library(soname) => {
210 let direct = resolve_library(soname, &binaries, host);
211 let seeds: Vec<PathBuf> = direct
212 .iter()
213 .filter(|path| unreachable.reason(path).is_none())
214 .cloned()
215 .collect();
216 let Ok(closure) = crate::sandbox::walk_dependency_closure(
217 &seeds,
218 &host.search_dirs,
219 &unreachable.walk_excluded,
220 |path| -> Result<_, std::convert::Infallible> {
221 Ok(crate::sandbox::read_elf_dependencies(path))
222 },
223 );
224 let refused = direct.into_iter().filter(|path| !seeds.contains(path));
226 Ok(closure
227 .into_iter()
228 .chain(refused)
229 .map(|path| NeedPath {
230 path,
231 directory: false,
232 })
233 .collect::<Vec<_>>())
234 }
235 NeedForm::File { file, beside } => Ok(resolve_library(beside, &binaries, host)
236 .iter()
237 .filter_map(|library| library.parent())
238 .filter_map(|dir| existing(&dir.join(file)))
239 .filter(|path| !path.directory)
240 .collect()),
241 NeedForm::ProgramFolder(relative) => {
242 if binaries.is_empty() {
243 Err(PROGRAM_NOT_ON_PATH.to_string())
244 } else {
245 Ok(binaries
246 .iter()
247 .filter_map(|binary| binary.parent())
248 .filter_map(|dir| existing(&dir.join(relative)))
249 .collect())
250 }
251 }
252 NeedForm::System(SystemLocation::Fonts) => Ok(host
253 .font_locations
254 .iter()
255 .filter_map(|location| existing(location))
256 .collect()),
257 NeedForm::System(SystemLocation::CaBundle) => {
258 let writable: Vec<&Path> =
259 host.capsule_writable.iter().map(PathBuf::as_path).collect();
260 Ok(crate::trust_store::select_in(host.ca_candidates, &writable))
261 }
262 NeedForm::Etc(name) => Ok(existing(&host.etc_root.join(name)).into_iter().collect()),
263 };
264 let paths = match found {
265 Ok(paths) if !paths.is_empty() => paths,
266 Ok(_) => {
267 missing.push(MissingNeed {
268 need: need.clone(),
269 reason: NOT_FOUND.to_string(),
270 });
271 continue;
272 }
273 Err(reason) => {
274 missing.push(MissingNeed {
275 need: need.clone(),
276 reason,
277 });
278 continue;
279 }
280 };
281 let mut kept: BTreeSet<NeedPath> = BTreeSet::new();
282 let mut refusal: Option<String> = None;
283 for path in paths {
284 match unreachable.reason(&path.path) {
285 None => {
286 kept.insert(path);
287 }
288 Some(reason) => {
289 refusal.get_or_insert(reason);
290 }
291 }
292 }
293 if !kept.is_empty() {
294 granted.push(ResolvedNeed {
295 need: need.clone(),
296 paths: kept.into_iter().collect(),
297 });
298 }
299 if let Some(reason) = refusal {
300 missing.push(MissingNeed {
301 need: need.clone(),
302 reason,
303 });
304 }
305 }
306 ResolvedProgramNeeds {
307 artifact: artifact.to_string(),
308 program: program.program.clone(),
309 granted,
310 missing,
311 }
312}
313
314#[cfg(target_os = "linux")]
318fn resolve_library(soname: &str, binaries: &[PathBuf], host: &NeedHost<'_>) -> Vec<PathBuf> {
319 let mut found = BTreeSet::new();
320 if binaries.is_empty() {
321 found.extend(crate::sandbox::resolve_soname(
322 soname,
323 Path::new("/"),
324 &[],
325 &host.search_dirs,
326 ));
327 }
328 for binary in binaries {
329 let runpaths = crate::sandbox::read_elf_dependencies(binary)
330 .map(|deps| deps.runpaths)
331 .unwrap_or_default();
332 let origin = binary.parent().unwrap_or_else(|| Path::new("/"));
333 found.extend(crate::sandbox::resolve_soname(
334 soname,
335 origin,
336 &runpaths,
337 &host.search_dirs,
338 ));
339 }
340 found.into_iter().collect()
341}
342
343#[cfg(target_os = "linux")]
345fn existing(path: &Path) -> Option<NeedPath> {
346 let canonical = std::fs::canonicalize(path).ok()?;
347 let metadata = std::fs::metadata(&canonical).ok()?;
348 (metadata.is_dir() || metadata.is_file()).then(|| NeedPath {
349 path: canonical,
350 directory: metadata.is_dir(),
351 })
352}
353
354#[cfg(target_os = "linux")]
356struct Unreachable {
357 roots: Vec<PathBuf>,
359 labels: Vec<String>,
360 pinned: Vec<PathBuf>,
362 walk_excluded: Vec<PathBuf>,
364}
365
366#[cfg(target_os = "linux")]
367impl Unreachable {
368 fn of(host: &NeedHost<'_>) -> Self {
369 let mut unreachable = Self {
370 roots: Vec::new(),
371 labels: Vec::new(),
372 pinned: host
373 .pinned_trees
374 .iter()
375 .flat_map(|tree| {
376 [
377 tree.clone(),
378 crate::protected_paths::canonicalize_existing_prefix(tree),
379 ]
380 })
381 .collect(),
382 walk_excluded: Vec::new(),
383 };
384 let mut add = |path: &Path, label: &str| {
385 for root in [
386 path.to_path_buf(),
387 crate::protected_paths::canonicalize_existing_prefix(path),
388 ] {
389 if !unreachable.roots.contains(&root) {
390 unreachable
391 .labels
392 .push(format!("{label} {}", root.display()));
393 unreachable.roots.push(root);
394 }
395 }
396 };
397 for kernel in ["/proc", "/sys", "/dev"] {
398 add(Path::new(kernel), "the kernel's");
399 }
400 if let Some(home) = &host.home {
401 add(home, "the home folder");
402 }
403 for root in &host.capsule_writable {
404 add(root, "the capsule-writable tree");
405 }
406 for location in murmur_artifact::ETC_CREDENTIAL_LOCATIONS {
407 add(&host.etc_root.join(location), "the credential location");
408 }
409 unreachable.walk_excluded = unreachable
410 .roots
411 .iter()
412 .filter(|root| !unreachable.pinned.iter().any(|tree| tree.starts_with(root)))
413 .cloned()
414 .collect();
415 unreachable
416 }
417
418 fn reason(&self, path: &Path) -> Option<String> {
420 if path == Path::new("/") {
421 return Some("resolved to /, the filesystem root".to_string());
422 }
423 if self.pinned.iter().any(|tree| path.starts_with(tree)) {
424 return None;
425 }
426 self.roots
427 .iter()
428 .zip(&self.labels)
429 .find_map(|(root, label)| {
430 let relation = if path == root {
431 "which is"
432 } else if path.starts_with(root) {
433 "beneath"
434 } else if root.starts_with(path) {
435 "which holds"
436 } else {
437 return None;
438 };
439 Some(format!(
440 "resolved to {}, {relation} {label}, which no need reaches",
441 path.display()
442 ))
443 })
444 }
445}
446
447#[must_use]
450pub fn host_distributions() -> Vec<String> {
451 OS_RELEASE_PATHS
452 .iter()
453 .find_map(|path| std::fs::read_to_string(path).ok())
454 .map(|text| distributions_from_os_release(&text))
455 .unwrap_or_default()
456}
457
458#[must_use]
461pub fn distributions_from_os_release(text: &str) -> Vec<String> {
462 let value = |key: &str| {
463 text.lines().find_map(|line| {
464 let (name, value) = line.trim().split_once('=')?;
465 (name.trim() == key).then(|| {
466 let value = value.trim();
467 value
468 .strip_prefix('"')
469 .and_then(|v| v.strip_suffix('"'))
470 .or_else(|| value.strip_prefix('\'').and_then(|v| v.strip_suffix('\'')))
471 .unwrap_or(value)
472 .to_string()
473 })
474 })
475 };
476 let mut distributions: Vec<String> = Vec::new();
477 let id = value("ID");
478 let like = value("ID_LIKE");
479 for word in id
480 .iter()
481 .chain(like.iter())
482 .flat_map(|value| value.split_whitespace())
483 {
484 if !distributions.iter().any(|known| known == word) {
485 distributions.push(word.to_string());
486 }
487 }
488 distributions
489}
490
491#[must_use]
494pub fn package_for<'a>(
495 need: &'a DeclaredNeed,
496 distributions: &'a [String],
497) -> Option<(&'a str, &'a str)> {
498 distributions.iter().find_map(|distribution| {
499 need.packages
500 .get(distribution)
501 .map(|package| (package.as_str(), distribution.as_str()))
502 })
503}
504
505#[cfg(all(test, target_os = "linux"))]
506mod tests {
507 use super::*;
508 use murmur_artifact::{parse_program_needs, NeedForm};
509 use std::os::unix::fs::symlink;
510
511 struct Scratch {
513 _temp: tempfile::TempDir,
514 root: PathBuf,
515 }
516
517 impl Scratch {
518 fn new() -> Self {
519 let temp = tempfile::tempdir().unwrap();
520 let root = std::fs::canonicalize(temp.path()).unwrap();
521 for dir in [
522 "bin/share",
523 "lib",
524 "fonts",
525 "home/user",
526 "workdir",
527 "etc/ssl/private",
528 "etc/ssl/certs",
529 "etc/fonts",
530 ] {
531 std::fs::create_dir_all(root.join(dir)).unwrap();
532 }
533 std::fs::copy(host_binary("true"), root.join("bin/fixprog")).unwrap();
534 std::fs::copy(host_library("libm.so.6"), root.join("lib/libfix.so.1")).unwrap();
535 std::fs::write(root.join("lib/libfix.chk"), b"chk").unwrap();
536 std::fs::copy(host_library("libm.so.6"), root.join("lib/libother.so.1")).unwrap();
537 std::fs::write(root.join("etc/fonts/fonts.conf"), b"<fontconfig/>").unwrap();
538 std::fs::write(root.join("etc/ssl/certs/ca.pem"), b"pem").unwrap();
539 Self { _temp: temp, root }
540 }
541
542 fn host(&self) -> NeedHost<'static> {
543 let mut search_dirs = vec![self.root.join("lib")];
544 search_dirs.extend(
545 crate::sandbox::DEFAULT_LIBRARY_SEARCH_DIRS
546 .iter()
547 .map(PathBuf::from),
548 );
549 NeedHost {
550 path_dirs: vec![self.root.join("bin")],
551 search_dirs,
552 font_locations: vec![self.root.join("fonts"), self.root.join("no-fonts")],
553 ca_candidates: &[],
554 etc_root: self.root.join("etc"),
555 home: Some(self.root.join("home/user")),
556 capsule_writable: vec![self.root.join("workdir")],
557 pinned_trees: Vec::new(),
558 }
559 }
560 }
561
562 fn host_binary(name: &str) -> PathBuf {
563 ["/bin", "/usr/bin"]
564 .iter()
565 .map(|dir| Path::new(dir).join(name))
566 .find(|path| path.is_file())
567 .expect("the host has the binary")
568 }
569
570 fn host_library(soname: &str) -> PathBuf {
571 let dirs: Vec<PathBuf> = crate::sandbox::DEFAULT_LIBRARY_SEARCH_DIRS
572 .iter()
573 .map(PathBuf::from)
574 .collect();
575 crate::sandbox::resolve_soname(soname, Path::new("/"), &[], &dirs)
576 .expect("the host has the library")
577 }
578
579 fn tool(artifact: &str, origin: LockOrigin, block: &str) -> DeclaredToolNeeds {
580 DeclaredToolNeeds {
581 artifact: artifact.to_string(),
582 origin,
583 programs: parse_program_needs(&format!(
584 "name: t\nversion: 0.1.0\nruntime: tool\n{block}"
585 ))
586 .unwrap(),
587 }
588 }
589
590 fn allow(names: &[&str]) -> Vec<String> {
591 names.iter().map(|name| (*name).to_string()).collect()
592 }
593
594 fn granted_paths(resolved: &ResolvedProgramNeeds) -> BTreeSet<PathBuf> {
595 resolved
596 .granted
597 .iter()
598 .flat_map(|need| need.paths.iter().map(|path| path.path.clone()))
599 .collect()
600 }
601
602 const FIXPROG: &str = "programs:
603 fixprog:
604 needs:
605 - library: libfix.so.1
606 - file: libfix.chk
607 beside: libfix.so.1
608 - program_folder: share
609 - system: fonts
610";
611
612 #[test]
616 fn declared_needs_a_pinned_only_program_resolves_into_its_store_copy_under_home() {
617 let scratch = Scratch::new();
618 let tree = scratch.root.join("home/user/.murmur/programs/abc");
619 std::fs::create_dir_all(tree.join("files/fixture")).unwrap();
620 std::fs::create_dir_all(tree.join("bin")).unwrap();
621 std::fs::copy(host_binary("true"), tree.join("files/fixture/pinprog")).unwrap();
622 symlink("../files/fixture/pinprog", tree.join("bin/pinprog")).unwrap();
623 std::fs::create_dir_all(scratch.root.join("home/user/elsewhere")).unwrap();
624 symlink(
625 scratch.root.join("home/user/elsewhere"),
626 tree.join("files/fixture/outside"),
627 )
628 .unwrap();
629 let block = "programs:\n pinprog:\n needs:\n - program_folder: .\n - program_folder: outside\n";
630 let tools = [tool("local/pinned-tool", LockOrigin::Operator, block)];
631
632 let unpinned = derive_in(&tools, &allow(&["pinprog"]), &scratch.host());
634 assert_eq!(unpinned[0].missing[0].reason, PROGRAM_NOT_ON_PATH);
635
636 let mut host = scratch.host();
637 host.path_dirs.push(tree.join("bin"));
638 host.pinned_trees = vec![tree.clone()];
639 let resolved = derive_in(&tools, &allow(&["pinprog"]), &host);
640 assert_eq!(
641 granted_paths(&resolved[0]),
642 BTreeSet::from([tree.join("files/fixture")])
643 );
644 assert_eq!(resolved[0].missing.len(), 1);
645 assert_eq!(
646 resolved[0].missing[0].need.form,
647 NeedForm::ProgramFolder("outside".to_string())
648 );
649 assert!(
650 resolved[0].missing[0]
651 .reason
652 .contains("beneath the home folder"),
653 "{}",
654 resolved[0].missing[0].reason
655 );
656 }
657
658 #[test]
659 fn declared_needs_resolve_to_exactly_the_declared_paths_read_only() {
660 let scratch = Scratch::new();
661 let host = scratch.host();
662 let resolved = derive_in(
663 &[tool("local/needs-tool", LockOrigin::Operator, FIXPROG)],
664 &allow(&["fixprog"]),
665 &host,
666 );
667 assert_eq!(resolved.len(), 1);
668 let program = &resolved[0];
669 assert_eq!(
670 (program.artifact.as_str(), program.program.as_str()),
671 ("local/needs-tool", "fixprog")
672 );
673 assert!(program.missing.is_empty(), "{:?}", program.missing);
674
675 let library = scratch.root.join("lib/libfix.so.1");
676 let Ok(closure) = crate::sandbox::walk_dependency_closure(
677 std::slice::from_ref(&library),
678 &host.search_dirs,
679 &[],
680 |path| -> Result<_, std::convert::Infallible> {
681 Ok(crate::sandbox::read_elf_dependencies(path))
682 },
683 );
684 assert!(closure.len() > 1, "libm's own closure: {closure:?}");
685 let mut expected: BTreeSet<PathBuf> = closure.into_iter().collect();
686 expected.insert(scratch.root.join("lib/libfix.chk"));
687 expected.insert(scratch.root.join("bin/share"));
688 expected.insert(scratch.root.join("fonts"));
689 assert_eq!(granted_paths(program), expected);
690 assert!(!expected.contains(&scratch.root.join("lib/libother.so.1")));
691 assert!(!expected.contains(&scratch.root.join("lib")));
692
693 let labels: Vec<String> = program
694 .granted
695 .iter()
696 .map(|need| need.need.form.to_string())
697 .collect();
698 assert_eq!(
699 labels,
700 [
701 "library libfix.so.1",
702 "file libfix.chk beside libfix.so.1",
703 "program_folder share",
704 "system fonts",
705 ]
706 );
707
708 let grants = crate::sandbox::resolve_declared_need_grants(&resolved);
709 assert_eq!(grants.len(), expected.len());
710 for grant in &grants {
711 assert!(!grant.executable, "{grant:?}");
712 let directory = grant.path == scratch.root.join("bin/share")
713 || grant.path == scratch.root.join("fonts");
714 assert_eq!(grant.list_dir, directory, "{grant:?}");
715 }
716 }
717
718 #[test]
719 fn declared_needs_an_unreachable_location_is_moved_to_missing() {
720 let scratch = Scratch::new();
721 let bin = scratch.root.join("bin");
722 for (link, target) in [
723 ("root_link", PathBuf::from("/")),
724 ("proc_link", PathBuf::from("/proc")),
725 ("home_link", scratch.root.join("home/user")),
726 ("home_parent", scratch.root.join("home")),
727 ("writable_link", scratch.root.join("workdir")),
728 ("cred_link", scratch.root.join("etc/ssl/private")),
729 ] {
730 symlink(&target, bin.join(link)).unwrap();
731 }
732 symlink(
733 scratch.root.join("etc/ssl/private"),
734 scratch.root.join("etc/innocent"),
735 )
736 .unwrap();
737 let block = "programs:
738 fixprog:
739 needs:
740 - program_folder: root_link
741 - program_folder: proc_link
742 - program_folder: home_link
743 - program_folder: home_parent
744 - program_folder: writable_link
745 - program_folder: cred_link
746 - etc: innocent
747 - library: libmurmur-absent.so.9
748 - program_folder: share
749 ghostprog:
750 needs:
751 - program_folder: .
752 - library: libfix.so.1
753";
754 let resolved = derive_in(
755 &[tool("local/needs-tool", LockOrigin::Operator, block)],
756 &allow(&["fixprog", "ghostprog"]),
757 &scratch.host(),
758 );
759 let fixprog = &resolved[0];
760 assert_eq!(
761 granted_paths(fixprog),
762 BTreeSet::from([scratch.root.join("bin/share")])
763 );
764 let reasons: Vec<(String, String)> = fixprog
765 .missing
766 .iter()
767 .map(|missing| (missing.need.form.to_string(), missing.reason.clone()))
768 .collect();
769 let root = scratch.root.display();
770 assert_eq!(
771 reasons,
772 [
773 (
774 "program_folder root_link".to_string(),
775 "resolved to /, the filesystem root".to_string()
776 ),
777 (
778 "program_folder proc_link".to_string(),
779 "resolved to /proc, which is the kernel's /proc, which no need reaches"
780 .to_string()
781 ),
782 (
783 "program_folder home_link".to_string(),
784 format!(
785 "resolved to {root}/home/user, which is the home folder \
786 {root}/home/user, which no need reaches"
787 )
788 ),
789 (
790 "program_folder home_parent".to_string(),
791 format!(
792 "resolved to {root}/home, which holds the home folder {root}/home/user, \
793 which no need reaches"
794 )
795 ),
796 (
797 "program_folder writable_link".to_string(),
798 format!(
799 "resolved to {root}/workdir, which is the capsule-writable tree \
800 {root}/workdir, which no need reaches"
801 )
802 ),
803 (
804 "program_folder cred_link".to_string(),
805 format!(
806 "resolved to {root}/etc/ssl/private, which is the credential location \
807 {root}/etc/ssl/private, which no need reaches"
808 )
809 ),
810 (
811 "etc innocent".to_string(),
812 format!(
813 "resolved to {root}/etc/ssl/private, which is the credential location \
814 {root}/etc/ssl/private, which no need reaches"
815 )
816 ),
817 (
818 "library libmurmur-absent.so.9".to_string(),
819 NOT_FOUND.to_string()
820 ),
821 ]
822 );
823
824 let ghost = &resolved[1];
827 assert_eq!(ghost.missing.len(), 1);
828 assert_eq!(ghost.missing[0].reason, PROGRAM_NOT_ON_PATH);
829 assert!(granted_paths(ghost).contains(&scratch.root.join("lib/libfix.so.1")));
830 }
831
832 #[test]
833 fn declared_needs_reach_only_an_operator_pin_and_a_granted_program() {
834 let scratch = Scratch::new();
835 let bin = scratch.root.join("bin");
836 std::fs::copy(host_binary("true"), bin.join("cat")).unwrap();
837 std::fs::copy(host_binary("true"), bin.join("ls")).unwrap();
838 let block = "programs:
839 cat:
840 needs:
841 - program_folder: share
842 - etc: fonts/fonts.conf
843 ls:
844 needs: []
845";
846 let host = scratch.host();
847 let operator = [tool("local/needs-tool", LockOrigin::Operator, block)];
848
849 assert_eq!(derive_in(&operator, &allow(&["ls"]), &host), Vec::new());
851
852 let both = derive_in(&operator, &allow(&["cat", "ls"]), &host);
854 assert_eq!(both.len(), 1);
855 assert_eq!(both[0].program, "cat");
856 assert_eq!(
857 granted_paths(&both[0]),
858 BTreeSet::from([
859 scratch.root.join("bin/share"),
860 scratch.root.join("etc/fonts/fonts.conf"),
861 ])
862 );
863
864 let runtime = [tool(
866 "local/needs-tool",
867 LockOrigin::Runtime {
868 session: "ses_x".to_string(),
869 },
870 block,
871 )];
872 assert_eq!(derive_in(&runtime, &allow(&["cat"]), &host), Vec::new());
873
874 assert_eq!(derive_in(&operator, &allow(&["cat"]), &host), both);
876
877 assert_eq!(
879 derive_in(&operator, &allow(&["CAT", "cat2"]), &host),
880 Vec::new()
881 );
882
883 let two = derive_in(
885 &[
886 tool("local/needs-tool", LockOrigin::Operator, block),
887 tool(
888 "local/second",
889 LockOrigin::Operator,
890 "programs:\n cat:\n needs:\n - system: fonts\n",
891 ),
892 ],
893 &allow(&["cat"]),
894 &host,
895 );
896 let owners: Vec<(&str, &str)> = two
897 .iter()
898 .map(|entry| (entry.artifact.as_str(), entry.program.as_str()))
899 .collect();
900 assert_eq!(
901 owners,
902 [("local/needs-tool", "cat"), ("local/second", "cat")]
903 );
904 }
905
906 #[test]
907 fn declared_needs_ca_bundle_is_the_trust_store_without_its_network_gate() {
908 let scratch = Scratch::new();
909 let bundle = scratch.root.join("etc/ssl/certs/ca.pem");
910 let bundle_text = bundle.to_string_lossy().into_owned();
911 let certs_text = scratch
912 .root
913 .join("etc/ssl/certs")
914 .to_string_lossy()
915 .into_owned();
916 let candidates = [
917 crate::trust_store::TrustStoreCandidate {
918 path: &bundle_text,
919 directory: false,
920 },
921 crate::trust_store::TrustStoreCandidate {
922 path: &certs_text,
923 directory: true,
924 },
925 ];
926 let mut host = scratch.host();
927 host.ca_candidates = &candidates;
928 let resolved = derive_in(
929 &[tool(
930 "local/needs-tool",
931 LockOrigin::Operator,
932 "programs:\n fixprog:\n needs:\n - system: ca_bundle\n",
933 )],
934 &allow(&["fixprog"]),
935 &host,
936 );
937 assert_eq!(
938 resolved[0].granted[0].paths,
939 [NeedPath {
940 path: scratch.root.join("etc/ssl/certs"),
941 directory: true,
942 }]
943 );
944 assert_eq!(
946 crate::trust_store::derive_in(&candidates, &allow(&["fixprog"]), &[], &[]),
947 Vec::new()
948 );
949 }
950
951 #[test]
952 fn declared_needs_os_release_names_id_then_id_like() {
953 assert_eq!(
954 distributions_from_os_release("NAME=\"Ubuntu\"\nID=ubuntu\nID_LIKE=debian\n"),
955 ["ubuntu", "debian"]
956 );
957 assert_eq!(
958 distributions_from_os_release("ID=\"rocky\"\nID_LIKE=\"rhel centos fedora\"\n"),
959 ["rocky", "rhel", "centos", "fedora"]
960 );
961 assert_eq!(
962 distributions_from_os_release("ID_LIKE=\"rhel fedora\"\nID='almalinux'\n"),
963 ["almalinux", "rhel", "fedora"]
964 );
965 assert_eq!(distributions_from_os_release(""), Vec::<String>::new());
966 assert_eq!(
967 distributions_from_os_release("ID=fedora\nVERSION_ID=40\n"),
968 ["fedora"]
969 );
970 }
971
972 #[test]
973 fn declared_needs_package_for_takes_the_first_named_distribution() {
974 let need = DeclaredNeed {
975 form: NeedForm::Library("libx.so.1".to_string()),
976 packages: [("debian", "libx1"), ("fedora", "x-libs")]
977 .into_iter()
978 .map(|(k, v)| (k.to_string(), v.to_string()))
979 .collect(),
980 };
981 let ubuntu = allow(&["ubuntu", "debian"]);
982 assert_eq!(package_for(&need, &ubuntu), Some(("libx1", "debian")));
983 assert_eq!(package_for(&need, &allow(&["arch"])), None);
984 }
985}