⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/pkg/
testing.rs

1//! Package Testing and Security Scanning
2//!
3//! Provides automated package validation through test definition, execution
4//! framework, and pre-install security scanning. The test runner validates
5//! test definitions; actual process spawning is deferred to user-space.
6//! The security scanner checks package file paths and requested capabilities
7//! against known-suspicious patterns before installation.
8//!
9//! NOTE: Many types in this module are forward declarations for user-space
10//! APIs. They will be exercised when user-space process execution is
11//! functional. See TODO(user-space) markers for specific activation points.
12
13// User-space API forward declarations -- see NOTE above
14
15#[cfg(feature = "alloc")]
16extern crate alloc;
17
18#[cfg(feature = "alloc")]
19use alloc::{string::String, vec::Vec};
20
21// ============================================================================
22// Package Test Framework
23// ============================================================================
24
25/// Classification of package tests.
26#[cfg(feature = "alloc")]
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum TestType {
29    /// Quick smoke tests to verify basic functionality.
30    Smoke,
31    /// Unit tests for individual components.
32    Unit,
33    /// Integration tests across components.
34    Integration,
35}
36
37#[cfg(feature = "alloc")]
38impl TestType {
39    /// Parse a test type from a string identifier.
40    pub fn parse(s: &str) -> Self {
41        match s {
42            "smoke" => Self::Smoke,
43            "unit" => Self::Unit,
44            "integration" => Self::Integration,
45            _ => Self::Unit,
46        }
47    }
48
49    /// Return a human-readable name.
50    pub fn as_str(self) -> &'static str {
51        match self {
52            Self::Smoke => "smoke",
53            Self::Unit => "unit",
54            Self::Integration => "integration",
55        }
56    }
57}
58
59/// Definition of a single package test.
60#[cfg(feature = "alloc")]
61#[derive(Debug, Clone)]
62pub struct PackageTest {
63    /// Name of the test.
64    pub test_name: String,
65    /// Classification of the test.
66    pub test_type: TestType,
67    /// Command to execute for this test.
68    pub command: String,
69    /// Maximum execution time in milliseconds.
70    pub timeout_ms: u64,
71    /// Expected process exit code (0 = success).
72    pub expected_exit: i32,
73}
74
75#[cfg(feature = "alloc")]
76impl PackageTest {
77    /// Create a new package test definition.
78    pub fn new(
79        test_name: String,
80        test_type: TestType,
81        command: String,
82        timeout_ms: u64,
83        expected_exit: i32,
84    ) -> Self {
85        Self {
86            test_name,
87            test_type,
88            command,
89            timeout_ms,
90            expected_exit,
91        }
92    }
93
94    /// Validate that this test definition is well-formed.
95    pub fn validate(&self) -> Result<(), crate::error::KernelError> {
96        if self.test_name.is_empty() {
97            return Err(crate::error::KernelError::InvalidArgument {
98                name: "test_name",
99                value: "empty",
100            });
101        }
102        if self.command.is_empty() {
103            return Err(crate::error::KernelError::InvalidArgument {
104                name: "command",
105                value: "empty",
106            });
107        }
108        if self.timeout_ms == 0 {
109            return Err(crate::error::KernelError::InvalidArgument {
110                name: "timeout_ms",
111                value: "zero",
112            });
113        }
114        Ok(())
115    }
116}
117
118/// Result of executing a single package test.
119#[cfg(feature = "alloc")]
120#[derive(Debug, Clone)]
121pub struct TestResult {
122    /// Name of the test that was executed.
123    pub test_name: String,
124    /// Whether the test passed.
125    pub passed: bool,
126    /// Execution duration in milliseconds.
127    pub duration_ms: u64,
128    /// Exit code returned by the test process.
129    pub exit_code: i32,
130    /// Standard output captured from the test.
131    pub stdout: String,
132    /// Standard error captured from the test.
133    pub stderr: String,
134}
135
136#[cfg(feature = "alloc")]
137impl TestResult {
138    /// Create a placeholder result for a test that has not yet been executed.
139    ///
140    /// Used when actual process spawning is deferred to user-space.
141    fn deferred(test_name: &str) -> Self {
142        Self {
143            test_name: String::from(test_name),
144            passed: true,
145            duration_ms: 0,
146            exit_code: 0,
147            stdout: String::from("TODO(user-space): test execution deferred"),
148            stderr: String::new(),
149        }
150    }
151}
152
153/// Test runner that manages and executes package tests.
154///
155/// Actual process spawning is deferred to user-space. The runner validates
156/// test definitions and creates placeholder results.
157#[cfg(feature = "alloc")]
158pub struct TestRunner {
159    /// Registered test definitions.
160    tests: Vec<PackageTest>,
161    /// Accumulated test results.
162    results: Vec<TestResult>,
163}
164
165#[cfg(feature = "alloc")]
166impl TestRunner {
167    /// Create a new empty test runner.
168    pub fn new() -> Self {
169        Self {
170            tests: Vec::new(),
171            results: Vec::new(),
172        }
173    }
174
175    /// Add a test definition to the runner.
176    ///
177    /// Returns an error if the test definition is invalid.
178    pub fn add_test(&mut self, test: PackageTest) -> Result<(), crate::error::KernelError> {
179        test.validate()?;
180        self.tests.push(test);
181        Ok(())
182    }
183
184    /// Run all registered tests and return results.
185    ///
186    /// NOTE: Actual process spawning is deferred to user-space. This method
187    /// validates test definitions and creates placeholder `TestResult` entries
188    /// with `TODO(user-space)` markers. When user-space process execution is
189    /// available, this will spawn test processes and capture real output.
190    pub fn run_all(&mut self) -> Vec<TestResult> {
191        self.results.clear();
192
193        for test in &self.tests {
194            // TODO(user-space): Spawn process from test.command, capture
195            // stdout/stderr, enforce test.timeout_ms, and compare exit code
196            // against test.expected_exit.
197            let result = TestResult::deferred(&test.test_name);
198            self.results.push(result);
199        }
200
201        self.results.clone()
202    }
203
204    /// Run a single test by name and return its result.
205    ///
206    /// Returns `None` if no test with the given name is registered.
207    pub fn run_single(&mut self, name: &str) -> Option<TestResult> {
208        let test = self.tests.iter().find(|t| t.test_name == name)?;
209        // TODO(user-space): Spawn process from test.command
210        let result = TestResult::deferred(&test.test_name);
211        self.results.push(result.clone());
212        Some(result)
213    }
214
215    /// Return the number of registered tests.
216    pub fn test_count(&self) -> usize {
217        self.tests.len()
218    }
219
220    /// Return accumulated test results.
221    pub fn results(&self) -> &[TestResult] {
222        &self.results
223    }
224
225    /// Count how many tests passed.
226    pub fn pass_count(&self) -> usize {
227        self.results.iter().filter(|r| r.passed).count()
228    }
229
230    /// Count how many tests failed.
231    pub fn fail_count(&self) -> usize {
232        self.results.iter().filter(|r| !r.passed).count()
233    }
234}
235
236#[cfg(feature = "alloc")]
237impl Default for TestRunner {
238    fn default() -> Self {
239        Self::new()
240    }
241}
242
243/// Run all tests defined for a package.
244///
245/// Looks up test definitions for the given package name and executes them
246/// through a `TestRunner`. Returns an empty list if the package has no tests.
247#[cfg(feature = "alloc")]
248pub fn run_package_tests(package: &str) -> Vec<TestResult> {
249    // TODO(user-space): Load test definitions from package metadata or
250    // test manifest file at /usr/local/packages/<package>/tests.toml.
251    // For now, create a default smoke test.
252    let mut runner = TestRunner::new();
253
254    let smoke_test = PackageTest::new(
255        alloc::format!("{}-smoke", package),
256        TestType::Smoke,
257        alloc::format!("/usr/local/packages/{}/bin/test-smoke", package),
258        5000,
259        0,
260    );
261
262    if runner.add_test(smoke_test).is_ok() {
263        runner.run_all()
264    } else {
265        Vec::new()
266    }
267}
268
269// ============================================================================
270// Package Security Scanner
271// ============================================================================
272
273/// Severity level for package security scan findings.
274///
275/// Distinct from `repository::Severity` which is used for repository-level
276/// vulnerability tracking. This type is for pre-install package scanning.
277#[cfg(feature = "alloc")]
278#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
279pub enum ScanSeverity {
280    /// Low-risk finding (informational).
281    Low,
282    /// Medium-risk finding (review recommended).
283    Medium,
284    /// High-risk finding (should be addressed).
285    High,
286    /// Critical-risk finding (blocks installation).
287    Critical,
288}
289
290#[cfg(feature = "alloc")]
291impl ScanSeverity {
292    /// Return a human-readable label.
293    pub fn as_str(self) -> &'static str {
294        match self {
295            Self::Low => "low",
296            Self::Medium => "medium",
297            Self::High => "high",
298            Self::Critical => "critical",
299        }
300    }
301}
302
303/// Classification of scan pattern types.
304#[cfg(feature = "alloc")]
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum ScanPatternType {
307    /// File path that accesses sensitive system locations.
308    SuspiciousPath,
309    /// Capability request that is excessively broad.
310    ExcessiveCapability,
311    /// File whose hash matches a known-bad sample.
312    KnownBadHash,
313    /// Code pattern that is potentially unsafe.
314    UnsafePattern,
315}
316
317/// A pattern used to detect suspicious content in a package.
318#[cfg(feature = "alloc")]
319#[derive(Debug, Clone)]
320pub struct ScanPattern {
321    /// Human-readable name for this pattern.
322    pub name: String,
323    /// What kind of pattern this is.
324    pub pattern_type: ScanPatternType,
325    /// Description of why this pattern is suspicious.
326    pub description: String,
327    /// Severity if this pattern is matched.
328    pub severity: ScanSeverity,
329}
330
331/// A security finding produced by the package scanner.
332#[cfg(feature = "alloc")]
333#[derive(Debug, Clone)]
334pub struct SecurityFinding {
335    /// Severity of the finding.
336    pub severity: ScanSeverity,
337    /// Description of the issue.
338    pub description: String,
339    /// File path that triggered the finding (empty for capability findings).
340    pub file_path: String,
341    /// Name of the pattern that was matched.
342    pub pattern_name: String,
343}
344
345/// Pre-install package security scanner.
346///
347/// Scans package file paths and requested capabilities against a set of
348/// suspicious patterns before the package is installed. This is distinct
349/// from `repository::SecurityScanner` which operates at the repository level.
350#[cfg(feature = "alloc")]
351pub struct PackageSecurityScanner {
352    /// Registered scan patterns.
353    patterns: Vec<ScanPattern>,
354}
355
356#[cfg(feature = "alloc")]
357impl PackageSecurityScanner {
358    /// Create a new scanner pre-loaded with default suspicious patterns.
359    pub fn new() -> Self {
360        let mut scanner = Self {
361            patterns: Vec::new(),
362        };
363        scanner.load_default_patterns();
364        scanner
365    }
366
367    /// Register an additional scan pattern.
368    pub fn add_pattern(&mut self, pattern: ScanPattern) {
369        self.patterns.push(pattern);
370    }
371
372    /// Return the number of registered patterns.
373    pub fn pattern_count(&self) -> usize {
374        self.patterns.len()
375    }
376
377    /// Scan a list of file paths against suspicious-path patterns.
378    ///
379    /// Checks each file path against all `ScanPatternType::SuspiciousPath`
380    /// and `ScanPatternType::UnsafePattern` patterns.
381    pub fn scan_paths(&self, file_paths: &[&str]) -> Vec<SecurityFinding> {
382        let mut findings = Vec::new();
383
384        for path in file_paths {
385            for pattern in &self.patterns {
386                let matches = match pattern.pattern_type {
387                    ScanPatternType::SuspiciousPath => path.contains(pattern.name.as_str()),
388                    ScanPatternType::UnsafePattern => path.contains(pattern.name.as_str()),
389                    _ => false,
390                };
391
392                if matches {
393                    findings.push(SecurityFinding {
394                        severity: pattern.severity,
395                        description: pattern.description.clone(),
396                        file_path: String::from(*path),
397                        pattern_name: pattern.name.clone(),
398                    });
399                }
400            }
401        }
402
403        findings
404    }
405
406    /// Scan requested capabilities against excessive-capability patterns.
407    ///
408    /// Checks each requested capability against all
409    /// `ScanPatternType::ExcessiveCapability` patterns.
410    pub fn scan_capabilities(&self, requested_caps: &[&str]) -> Vec<SecurityFinding> {
411        let mut findings = Vec::new();
412
413        for cap in requested_caps {
414            for pattern in &self.patterns {
415                if pattern.pattern_type != ScanPatternType::ExcessiveCapability {
416                    continue;
417                }
418                if *cap == pattern.name.as_str() {
419                    findings.push(SecurityFinding {
420                        severity: pattern.severity,
421                        description: pattern.description.clone(),
422                        file_path: String::new(),
423                        pattern_name: pattern.name.clone(),
424                    });
425                }
426            }
427        }
428
429        findings
430    }
431
432    /// Scan file hashes against known-bad hash patterns.
433    ///
434    /// `file_hashes` is a list of `(file_path, hex_hash)` pairs.
435    pub fn scan_hashes(&self, file_hashes: &[(&str, &str)]) -> Vec<SecurityFinding> {
436        let mut findings = Vec::new();
437
438        for (path, hash) in file_hashes {
439            for pattern in &self.patterns {
440                if pattern.pattern_type != ScanPatternType::KnownBadHash {
441                    continue;
442                }
443                if *hash == pattern.name.as_str() {
444                    findings.push(SecurityFinding {
445                        severity: pattern.severity,
446                        description: pattern.description.clone(),
447                        file_path: String::from(*path),
448                        pattern_name: pattern.name.clone(),
449                    });
450                }
451            }
452        }
453
454        findings
455    }
456
457    /// Check if any finding is at or above the given severity threshold.
458    pub fn has_findings_at_severity(
459        findings: &[SecurityFinding],
460        min_severity: ScanSeverity,
461    ) -> bool {
462        findings.iter().any(|f| f.severity >= min_severity)
463    }
464
465    /// Populate the scanner with well-known suspicious patterns.
466    fn load_default_patterns(&mut self) {
467        // Suspicious file paths (high severity)
468        let suspicious_paths: &[(&str, &str)] = &[
469            ("/etc/shadow", "Access to password shadow file"),
470            ("/dev/mem", "Direct physical memory access"),
471            ("/proc/kcore", "Kernel memory image access"),
472            ("/dev/kmem", "Kernel memory device access"),
473        ];
474        for (path, desc) in suspicious_paths {
475            self.patterns.push(ScanPattern {
476                name: String::from(*path),
477                pattern_type: ScanPatternType::SuspiciousPath,
478                description: String::from(*desc),
479                severity: ScanSeverity::High,
480            });
481        }
482
483        // Excessive capability requests (medium-high severity)
484        let dangerous_caps: &[(&str, &str, ScanSeverity)] = &[
485            (
486                "CAP_SYS_ADMIN",
487                "Broad administrative capability",
488                ScanSeverity::High,
489            ),
490            (
491                "CAP_NET_RAW",
492                "Raw network socket capability",
493                ScanSeverity::Medium,
494            ),
495            (
496                "CAP_SYS_RAWIO",
497                "Raw I/O port access capability",
498                ScanSeverity::High,
499            ),
500        ];
501        for (cap, desc, severity) in dangerous_caps {
502            self.patterns.push(ScanPattern {
503                name: String::from(*cap),
504                pattern_type: ScanPatternType::ExcessiveCapability,
505                description: String::from(*desc),
506                severity: *severity,
507            });
508        }
509
510        // Unsafe permission patterns (medium severity)
511        let unsafe_patterns: &[(&str, &str)] = &[
512            ("setuid", "Setuid binary detected"),
513            ("world-writable", "World-writable file detected"),
514        ];
515        for (pat, desc) in unsafe_patterns {
516            self.patterns.push(ScanPattern {
517                name: String::from(*pat),
518                pattern_type: ScanPatternType::UnsafePattern,
519                description: String::from(*desc),
520                severity: ScanSeverity::Medium,
521            });
522        }
523    }
524}
525
526#[cfg(feature = "alloc")]
527impl Default for PackageSecurityScanner {
528    fn default() -> Self {
529        Self::new()
530    }
531}