1use std::collections::HashSet;
2use std::env;
3use std::fs;
4use std::path::Path;
5use std::fmt;
6
7pub trait FileSystem {
9 fn file_exists(&self, path: &Path) -> bool;
10 fn read_to_string(&self, path: &Path) -> std::io::Result<String>;
11}
12
13pub struct RealFileSystem;
15
16impl FileSystem for RealFileSystem {
17 fn file_exists(&self, path: &Path) -> bool {
18 path.exists()
19 }
20
21 fn read_to_string(&self, path: &Path) -> std::io::Result<String> {
22 fs::read_to_string(path)
23 }
24}
25
26#[derive(Debug, PartialEq, Eq)]
28pub enum OperatingSystem {
29 Windows,
30 Linux,
31 MacOS,
32 Unknown(String),
33}
34
35impl fmt::Display for OperatingSystem {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 OperatingSystem::Windows => write!(f, "Windows"),
39 OperatingSystem::Linux => write!(f, "Linux"),
40 OperatingSystem::MacOS => write!(f, "macOS"),
41 OperatingSystem::Unknown(name) => write!(f, "Unknown ({})", name),
42 }
43 }
44}
45
46#[derive(Debug, PartialEq, Eq)]
48pub enum ContainerEnvironment {
49 Docker,
50 Kubernetes,
51 Podman,
52 None,
53}
54
55impl fmt::Display for ContainerEnvironment {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 ContainerEnvironment::Docker => write!(f, "Docker"),
59 ContainerEnvironment::Kubernetes => write!(f, "Kubernetes"),
60 ContainerEnvironment::Podman => write!(f, "Podman"),
61 ContainerEnvironment::None => write!(f, "None"),
62 }
63 }
64}
65
66#[derive(Debug, PartialEq, Eq)]
68pub enum VirtualizationPlatform {
69 VMware,
70 VirtualBox,
71 HyperV,
72 KVM,
73 Other(String),
74 None,
75}
76
77impl fmt::Display for VirtualizationPlatform {
78 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79 match self {
80 VirtualizationPlatform::VMware => write!(f, "VMware"),
81 VirtualizationPlatform::VirtualBox => write!(f, "VirtualBox"),
82 VirtualizationPlatform::HyperV => write!(f, "Hyper-V"),
83 VirtualizationPlatform::KVM => write!(f, "KVM"),
84 VirtualizationPlatform::Other(name) => write!(f, "Other ({})", name),
85 VirtualizationPlatform::None => write!(f, "None"),
86 }
87 }
88}
89
90#[allow(non_camel_case_types)]
92#[derive(Debug, PartialEq, Eq, Hash, Clone)]
93pub enum Capability {
94 CAP_CHOWN,
95 CAP_DAC_OVERRIDE,
96 CAP_DAC_READ_SEARCH,
97 CAP_FOWNER,
98 CAP_FSETID,
99 CAP_KILL,
100 CAP_SETGID,
101 CAP_SETUID,
102 CAP_SETPCAP,
103 CAP_LINUX_IMMUTABLE,
104 CAP_NET_BIND_SERVICE,
105 CAP_NET_BROADCAST,
106 CAP_NET_ADMIN,
107 CAP_NET_RAW,
108 CAP_IPC_LOCK,
109 CAP_IPC_OWNER,
110 CAP_SYS_MODULE,
111 CAP_SYS_RAWIO,
112 CAP_SYS_CHROOT,
113 CAP_SYS_PTRACE,
114 CAP_SYS_PACCT,
115 CAP_SYS_ADMIN,
116 CAP_SYS_BOOT,
117 CAP_SYS_NICE,
118 CAP_SYS_RESOURCE,
119 CAP_SYS_TIME,
120 CAP_SYS_TTY_CONFIG,
121 CAP_MKNOD,
122 CAP_LEASE,
123 CAP_AUDIT_WRITE,
124 CAP_AUDIT_CONTROL,
125 CAP_SETFCAP,
126 Unknown(String),
127}
128
129impl fmt::Display for Capability {
130 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131 match self {
132 Capability::CAP_CHOWN => write!(f, "CAP_CHOWN"),
133 Capability::CAP_DAC_OVERRIDE => write!(f, "CAP_DAC_OVERRIDE"),
134 Capability::CAP_DAC_READ_SEARCH => write!(f, "CAP_DAC_READ_SEARCH"),
135 Capability::CAP_FOWNER => write!(f, "CAP_FOWNER"),
136 Capability::CAP_FSETID => write!(f, "CAP_FSETID"),
137 Capability::CAP_KILL => write!(f, "CAP_KILL"),
138 Capability::CAP_SETGID => write!(f, "CAP_SETGID"),
139 Capability::CAP_SETUID => write!(f, "CAP_SETUID"),
140 Capability::CAP_SETPCAP => write!(f, "CAP_SETPCAP"),
141 Capability::CAP_LINUX_IMMUTABLE => write!(f, "CAP_LINUX_IMMUTABLE"),
142 Capability::CAP_NET_BIND_SERVICE => write!(f, "CAP_NET_BIND_SERVICE"),
143 Capability::CAP_NET_BROADCAST => write!(f, "CAP_NET_BROADCAST"),
144 Capability::CAP_NET_ADMIN => write!(f, "CAP_NET_ADMIN"),
145 Capability::CAP_NET_RAW => write!(f, "CAP_NET_RAW"),
146 Capability::CAP_IPC_LOCK => write!(f, "CAP_IPC_LOCK"),
147 Capability::CAP_IPC_OWNER => write!(f, "CAP_IPC_OWNER"),
148 Capability::CAP_SYS_MODULE => write!(f, "CAP_SYS_MODULE"),
149 Capability::CAP_SYS_RAWIO => write!(f, "CAP_SYS_RAWIO"),
150 Capability::CAP_SYS_CHROOT => write!(f, "CAP_SYS_CHROOT"),
151 Capability::CAP_SYS_PTRACE => write!(f, "CAP_SYS_PTRACE"),
152 Capability::CAP_SYS_PACCT => write!(f, "CAP_SYS_PACCT"),
153 Capability::CAP_SYS_ADMIN => write!(f, "CAP_SYS_ADMIN"),
154 Capability::CAP_SYS_BOOT => write!(f, "CAP_SYS_BOOT"),
155 Capability::CAP_SYS_NICE => write!(f, "CAP_SYS_NICE"),
156 Capability::CAP_SYS_RESOURCE => write!(f, "CAP_SYS_RESOURCE"),
157 Capability::CAP_SYS_TIME => write!(f, "CAP_SYS_TIME"),
158 Capability::CAP_SYS_TTY_CONFIG => write!(f, "CAP_SYS_TTY_CONFIG"),
159 Capability::CAP_MKNOD => write!(f, "CAP_MKNOD"),
160 Capability::CAP_LEASE => write!(f, "CAP_LEASE"),
161 Capability::CAP_AUDIT_WRITE => write!(f, "CAP_AUDIT_WRITE"),
162 Capability::CAP_AUDIT_CONTROL => write!(f, "CAP_AUDIT_CONTROL"),
163 Capability::CAP_SETFCAP => write!(f, "CAP_SETFCAP"),
164 Capability::Unknown(name) => write!(f, "Unknown ({})", name),
165 }
166 }
167}
168
169#[derive(Debug, PartialEq, Eq)]
171pub struct Capabilities {
172 pub effective: HashSet<Capability>,
173}
174
175#[derive(Debug, PartialEq, Eq)]
177pub struct EnvironmentInfo {
178 pub os: OperatingSystem,
179 pub container: ContainerEnvironment,
180 pub virtualization: VirtualizationPlatform,
181 pub capabilities: Option<Capabilities>,
182}
183
184pub fn get_os() -> OperatingSystem {
186 match env::consts::OS {
187 "windows" => OperatingSystem::Windows,
188 "linux" => OperatingSystem::Linux,
189 "macos" => OperatingSystem::MacOS,
190 other => OperatingSystem::Unknown(other.to_string()),
191 }
192}
193
194fn is_kubernetes() -> bool {
196 env::var("KUBERNETES_SERVICE_HOST").is_ok()
197}
198
199fn get_container_runtime() -> Option<String> {
202 env::var("CONTAINER_RUNTIME").ok()
204}
205
206pub fn detect_container(fs: &dyn FileSystem) -> ContainerEnvironment {
208 if is_kubernetes() {
210 return ContainerEnvironment::Kubernetes;
211 }
212
213 if fs.file_exists(Path::new("/.dockerenv")) {
215 if let Some(container_runtime) = get_container_runtime() {
217 return match container_runtime.as_str() {
218 "docker" => ContainerEnvironment::Docker,
219 "podman" => ContainerEnvironment::Podman,
220 _ => ContainerEnvironment::None,
221 };
222 }
223 return ContainerEnvironment::Docker;
224 }
225
226 if let Ok(cgroup) = fs.read_to_string(Path::new("/proc/1/cgroup")) {
228 if cgroup.contains("docker") {
229 return ContainerEnvironment::Docker;
230 } else if cgroup.contains("podman") {
231 return ContainerEnvironment::Podman;
232 }
233 }
234
235 ContainerEnvironment::None
236}
237
238const CAPABILITY_BIT_MAP: [Option<Capability>; 32] = [
240 Some(Capability::CAP_CHOWN), Some(Capability::CAP_DAC_OVERRIDE), Some(Capability::CAP_DAC_READ_SEARCH), Some(Capability::CAP_FOWNER), Some(Capability::CAP_FSETID), Some(Capability::CAP_KILL), Some(Capability::CAP_SETGID), Some(Capability::CAP_SETUID), Some(Capability::CAP_SETPCAP), Some(Capability::CAP_LINUX_IMMUTABLE), Some(Capability::CAP_NET_BIND_SERVICE), Some(Capability::CAP_NET_BROADCAST), Some(Capability::CAP_NET_ADMIN), Some(Capability::CAP_NET_RAW), Some(Capability::CAP_IPC_LOCK), Some(Capability::CAP_IPC_OWNER), Some(Capability::CAP_SYS_MODULE), Some(Capability::CAP_SYS_RAWIO), Some(Capability::CAP_SYS_CHROOT), Some(Capability::CAP_SYS_PTRACE), Some(Capability::CAP_SYS_PACCT), Some(Capability::CAP_SYS_ADMIN), Some(Capability::CAP_SYS_BOOT), Some(Capability::CAP_SYS_NICE), Some(Capability::CAP_SYS_RESOURCE), Some(Capability::CAP_SYS_TIME), Some(Capability::CAP_SYS_TTY_CONFIG), Some(Capability::CAP_MKNOD), Some(Capability::CAP_LEASE), Some(Capability::CAP_AUDIT_WRITE), Some(Capability::CAP_AUDIT_CONTROL), Some(Capability::CAP_SETFCAP), ];
273
274fn parse_capabilities(cap_eff: u64) -> Vec<Capability> {
276 let mut capabilities = Vec::new();
277 for bit in 0..32 {
278 if cap_eff & (1 << bit) != 0 {
279 if let Some(cap) = CAPABILITY_BIT_MAP[bit as usize].clone() {
280 capabilities.push(cap);
281 } else {
282 capabilities.push(Capability::Unknown(format!("Bit {}", bit)));
283 }
284 }
285 }
286 capabilities
287}
288
289fn get_capabilities(fs: &dyn FileSystem) -> Option<Capabilities> {
291 let status_path = Path::new("/proc/self/status");
292 if !fs.file_exists(status_path) {
293 return None;
294 }
295
296 let status_content = match fs.read_to_string(status_path) {
297 Ok(content) => content,
298 Err(_) => return None,
299 };
300
301 let cap_eff_line = status_content
303 .lines()
304 .find(|line| line.starts_with("CapEff:"))
305 .map(|line| line.trim_start_matches("CapEff:").trim());
306
307 let cap_eff_str = match cap_eff_line {
308 Some(s) => s,
309 None => return None,
310 };
311
312 let cap_eff = match u64::from_str_radix(cap_eff_str, 16) {
314 Ok(val) => val,
315 Err(_) => return None,
316 };
317
318 let capabilities = parse_capabilities(cap_eff);
320
321 Some(Capabilities {
322 effective: capabilities.into_iter().collect(),
323 })
324}
325
326pub fn detect_virtualization(fs: &dyn FileSystem) -> VirtualizationPlatform {
328 if detect_container(fs) != ContainerEnvironment::None {
330 return VirtualizationPlatform::None;
331 }
332
333 let mut is_hypervisor = false;
334
335 if let Ok(cpuinfo) = fs.read_to_string(Path::new("/proc/cpuinfo")) {
337 let cpuinfo_lower = cpuinfo.to_lowercase();
338 if cpuinfo_lower.contains("hypervisor") || cpuinfo_lower.contains("kvm") {
339 is_hypervisor = true;
340 }
341 }
342
343 if fs.file_exists(Path::new("/dev/kvm")) {
345 is_hypervisor = true;
346 }
347
348 if is_hypervisor {
349 for path in [
351 Path::new("/sys/class/dmi/id/product_name"),
352 Path::new("/sys/class/dmi/id/sys_vendor"),
353 ]
354 .iter()
355 {
356 if let Ok(content) = fs.read_to_string(path) {
357 let content_lower = content.to_lowercase();
358 if content_lower.contains("virtualbox") {
359 return VirtualizationPlatform::VirtualBox;
360 } else if content_lower.contains("vmware") {
361 return VirtualizationPlatform::VMware;
362 } else if content_lower.contains("hyper-v") || content_lower.contains("microsoft corporation") {
363 return VirtualizationPlatform::HyperV;
364 } else if content_lower.contains("kvm") || content_lower.contains("q35") || content_lower.contains("standard pc") {
365 return VirtualizationPlatform::KVM;
366 } else if !content.trim().is_empty() {
367 return VirtualizationPlatform::Other(content.trim().to_string());
368 }
369 }
370 }
371
372 return VirtualizationPlatform::KVM;
374 }
375
376 VirtualizationPlatform::None
377}
378
379pub fn get_environment_info() -> EnvironmentInfo {
381 let fs = RealFileSystem;
382 let container = detect_container(&fs);
383 let capabilities = if container == ContainerEnvironment::Docker {
384 get_capabilities(&fs)
385 } else {
386 None
387 };
388 EnvironmentInfo {
389 os: get_os(),
390 container,
391 virtualization: detect_virtualization(&fs),
392 capabilities,
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399 use std::collections::HashMap;
400 use std::env;
401 use std::path::PathBuf;
402
403 struct MockFileSystem {
405 existing_files: Vec<PathBuf>,
406 file_contents: HashMap<PathBuf, String>,
407 }
408
409 impl MockFileSystem {
410 fn new() -> Self {
411 Self {
412 existing_files: Vec::new(),
413 file_contents: HashMap::new(),
414 }
415 }
416
417 fn add_file(&mut self, path: PathBuf, contents: String) {
419 self.existing_files.push(path.clone());
420 self.file_contents.insert(path, contents);
421 }
422 }
423
424 impl FileSystem for MockFileSystem {
425 fn file_exists(&self, path: &Path) -> bool {
426 self.existing_files.iter().any(|p| p == path)
427 }
428
429 fn read_to_string(&self, path: &Path) -> std::io::Result<String> {
430 if let Some(content) = self.file_contents.get(path) {
431 Ok(content.clone())
432 } else {
433 Err(std::io::Error::new(
434 std::io::ErrorKind::NotFound,
435 "File not found",
436 ))
437 }
438 }
439 }
440
441 #[test]
442 fn test_get_os() {
443 let os = get_os();
444 #[cfg(target_os = "windows")]
445 assert_eq!(os, OperatingSystem::Windows);
446 #[cfg(target_os = "linux")]
447 assert_eq!(os, OperatingSystem::Linux);
448 #[cfg(target_os = "macos")]
449 assert_eq!(os, OperatingSystem::MacOS);
450 #[cfg(not(any(target_os = "windows", target_os = "linux", target_os = "macos")))]
451 match os {
452 OperatingSystem::Unknown(ref s) => println!("Unknown OS: {}", s),
453 _ => panic!("Expected Unknown OS variant"),
454 }
455 }
456
457 #[test]
458 fn test_is_kubernetes() {
459 env::set_var("KUBERNETES_SERVICE_HOST", "localhost");
461 assert!(is_kubernetes());
462
463 env::remove_var("KUBERNETES_SERVICE_HOST");
465 assert!(!is_kubernetes());
466 }
467
468 #[cfg(target_os = "linux")]
469 #[test]
470 fn test_detect_container_docker() {
471 let mut mock_fs = MockFileSystem::new();
472 mock_fs.add_file(PathBuf::from("/.dockerenv"), "".to_string());
473 env::set_var("CONTAINER_RUNTIME", "docker");
475
476 let container = detect_container(&mock_fs);
477 assert_eq!(container, ContainerEnvironment::Docker);
478
479 env::remove_var("CONTAINER_RUNTIME");
480 }
481
482 #[cfg(target_os = "linux")]
483 #[test]
484 fn test_detect_container_podman() {
485 let mut mock_fs = MockFileSystem::new();
486 mock_fs.add_file(PathBuf::from("/.dockerenv"), "".to_string());
487 env::set_var("CONTAINER_RUNTIME", "podman");
488
489 let container = detect_container(&mock_fs);
490 assert_eq!(container, ContainerEnvironment::Podman);
491
492 env::remove_var("CONTAINER_RUNTIME");
493 }
494
495 #[cfg(target_os = "linux")]
496 #[test]
497 fn test_detect_container_docker_via_cgroup() {
498 let mut mock_fs = MockFileSystem::new();
499 mock_fs.add_file(PathBuf::from("/proc/1/cgroup"), "docker".to_string());
501
502 let container = detect_container(&mock_fs);
503 assert_eq!(container, ContainerEnvironment::Docker);
504 }
505
506 #[cfg(target_os = "linux")]
507 #[test]
508 fn test_detect_container_podman_via_cgroup() {
509 let mut mock_fs = MockFileSystem::new();
510 mock_fs.add_file(PathBuf::from("/proc/1/cgroup"), "podman".to_string());
512
513 let container = detect_container(&mock_fs);
514 assert_eq!(container, ContainerEnvironment::Podman);
515 }
516
517 #[cfg(target_os = "linux")]
518 #[test]
519 fn test_detect_container_none() {
520 let mock_fs = MockFileSystem::new();
521 let container = detect_container(&mock_fs);
522 assert_eq!(container, ContainerEnvironment::None);
523 }
524
525 #[cfg(target_os = "linux")]
526 #[test]
527 fn test_detect_virtualization_virtualbox() {
528 let mut mock_fs = MockFileSystem::new();
529 mock_fs.add_file(
532 PathBuf::from("/proc/cpuinfo"),
533 "flags : hypervisor".to_string(),
534 );
535 mock_fs.add_file(
537 PathBuf::from("/sys/class/dmi/id/product_name"),
538 "VirtualBox".to_string(),
539 );
540
541 let virtualization = detect_virtualization(&mock_fs);
542 assert_eq!(virtualization, VirtualizationPlatform::VirtualBox);
543 }
544
545 #[cfg(target_os = "linux")]
546 #[test]
547 fn test_detect_virtualization_vmware() {
548 let mut mock_fs = MockFileSystem::new();
549 mock_fs.add_file(
552 PathBuf::from("/proc/cpuinfo"),
553 "flags : hypervisor".to_string(),
554 );
555 mock_fs.add_file(
557 PathBuf::from("/sys/class/dmi/id/sys_vendor"),
558 "VMware, Inc.".to_string(),
559 );
560
561 let virtualization = detect_virtualization(&mock_fs);
562 assert_eq!(virtualization, VirtualizationPlatform::VMware);
563 }
564
565 #[cfg(target_os = "linux")]
566 #[test]
567 fn test_detect_virtualization_hyperv() {
568 let mut mock_fs = MockFileSystem::new();
569 mock_fs.add_file(
572 PathBuf::from("/proc/cpuinfo"),
573 "flags : hypervisor".to_string(),
574 );
575 mock_fs.add_file(
577 PathBuf::from("/sys/class/dmi/id/sys_vendor"),
578 "Microsoft Corporation".to_string(),
579 );
580
581 let virtualization = detect_virtualization(&mock_fs);
582 assert_eq!(virtualization, VirtualizationPlatform::HyperV);
583 }
584
585 #[cfg(target_os = "linux")]
586 #[test]
587 fn test_detect_virtualization_kvm() {
588 let mut mock_fs = MockFileSystem::new();
589 mock_fs.add_file(
592 PathBuf::from("/proc/cpuinfo"),
593 "flags : hypervisor kvm".to_string(),
594 );
595 mock_fs.add_file(PathBuf::from("/dev/kvm"), "".to_string());
597
598 let virtualization = detect_virtualization(&mock_fs);
599 assert_eq!(virtualization, VirtualizationPlatform::KVM);
600 }
601
602 #[cfg(target_os = "linux")]
603 #[test]
604 fn test_get_environment_info_virtualization() {
605 let mut mock_fs = MockFileSystem::new();
606 mock_fs.add_file(
608 PathBuf::from("/proc/cpuinfo"),
609 "flags : hypervisor".to_string(),
610 );
611 mock_fs.add_file(
613 PathBuf::from("/sys/class/dmi/id/product_name"),
614 "VMware".to_string(),
615 );
616
617 let info = get_environment_info_with_fs(&mock_fs);
618 assert_eq!(info.os, OperatingSystem::Linux);
619 assert_eq!(info.container, ContainerEnvironment::None);
620 assert_eq!(info.virtualization, VirtualizationPlatform::VMware);
621 assert_eq!(info.capabilities, None);
622 }
623
624 #[cfg(target_os = "linux")]
625 #[test]
626 fn test_detect_virtualization_none() {
627 let mock_fs = MockFileSystem::new();
628 let virtualization = detect_virtualization(&mock_fs);
629 assert_eq!(virtualization, VirtualizationPlatform::None);
630 }
631
632 #[test]
633 fn test_get_environment_info() {
634 env::set_var("KUBERNETES_SERVICE_HOST", "localhost");
636 let info = get_environment_info_with_fs(&RealFileSystem);
637 #[cfg(target_os = "windows")]
638 assert_eq!(info.os, OperatingSystem::Windows);
639 #[cfg(target_os = "linux")]
640 assert_eq!(info.os, OperatingSystem::Linux);
641 #[cfg(target_os = "macos")]
642 assert_eq!(info.os, OperatingSystem::MacOS);
643 assert_eq!(info.container, ContainerEnvironment::Kubernetes);
644 assert_eq!(info.virtualization, VirtualizationPlatform::None);
645 assert_eq!(info.capabilities, None);
646 env::remove_var("KUBERNETES_SERVICE_HOST");
647 }
648
649 #[cfg(target_os = "linux")]
650 #[test]
651 fn test_get_environment_info_capabilities() {
652 let mut mock_fs = MockFileSystem::new();
653 mock_fs.add_file(PathBuf::from("/.dockerenv"), "".to_string());
655 mock_fs.add_file(
657 PathBuf::from("/proc/self/status"),
658 "CapEff: 00000000002c0000\n".to_string(),
659 );
660
661 let capabilities = get_capabilities(&mock_fs).unwrap();
662 let expected: HashSet<Capability> = vec![
663 Capability::CAP_SYS_ADMIN,
664 Capability::CAP_SYS_PTRACE,
665 Capability::CAP_SYS_CHROOT,
666 ]
667 .into_iter()
668 .collect();
669 assert_eq!(capabilities.effective, expected);
670 }
671
672 fn get_environment_info_with_fs(fs: &dyn FileSystem) -> EnvironmentInfo {
674 EnvironmentInfo {
675 os: get_os(),
676 container: detect_container(fs),
677 virtualization: detect_virtualization(fs),
678 capabilities: if detect_container(fs) == ContainerEnvironment::Docker {
679 get_capabilities(fs)
680 } else {
681 None
682 },
683 }
684 }
685}