docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment guidelines: delete redundant restatements, decorative banners, change narration, and end-of-line comments; keep and tighten the crucial ones (invariants, bug rationale, SAFETY blocks, ported-source attribution). No functional code changed. Every edit is proven comment-only against the prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a separate doctest-fence check. Where removing a comment made rustfmt or clippy want to re-lay-out adjacent code, the minimal triggering comment is restored so code tokens stay byte-identical. Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy --workspace --all-targets (0 warnings). Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for these guidelines (flags banners, end-of-line comments, change narration, and commented-out code).
This commit is contained in:
@@ -18,7 +18,6 @@ use kigi_sandbox::{ProfileName, SandboxManager};
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
// Parse profile from args (default: workspace).
|
||||
let profile_name = std::env::args()
|
||||
.nth(1)
|
||||
.unwrap_or_else(|| "workspace".to_string());
|
||||
@@ -28,7 +27,6 @@ fn main() {
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
// Check platform support before applying
|
||||
let support = SandboxManager::support_info();
|
||||
println!(
|
||||
"Platform support: {}",
|
||||
@@ -47,7 +45,6 @@ fn main() {
|
||||
println!("\nProfile: {profile}");
|
||||
println!("Workspace: {}", workspace.display());
|
||||
|
||||
// Apply the sandbox
|
||||
println!("\n--- Applying sandbox ---");
|
||||
let mut sandbox = SandboxManager::new(profile, &workspace);
|
||||
match sandbox.apply(&workspace) {
|
||||
@@ -68,39 +65,40 @@ fn main() {
|
||||
sandbox.restrict_child_network()
|
||||
);
|
||||
|
||||
// Test operations
|
||||
println!("\n--- Testing filesystem operations ---\n");
|
||||
|
||||
// Test 1: Read CWD (should always work)
|
||||
// Nothing below asserts; each comment is the outcome the operator should
|
||||
// see on stdout for the profile named.
|
||||
|
||||
// Always allowed.
|
||||
test_read("Read CWD", &workspace);
|
||||
|
||||
// Test 2: Read /tmp (should work for workspace/read-only)
|
||||
// Allowed for workspace/read-only.
|
||||
test_read("Read /tmp", Path::new("/tmp"));
|
||||
|
||||
// Test 3: Read home directory (should work for workspace/read-only, blocked for strict)
|
||||
// Allowed for workspace/read-only, blocked for strict.
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
test_read("Read ~/", &home);
|
||||
}
|
||||
|
||||
// Test 4: Write to CWD (should work for workspace/strict, blocked for read-only)
|
||||
// Allowed for workspace/strict, blocked for read-only.
|
||||
let test_file = workspace.join(".sandbox-test-write");
|
||||
test_write("Write to CWD", &test_file);
|
||||
// Clean up
|
||||
let _ = std::fs::remove_file(&test_file);
|
||||
|
||||
// Test 5: Write to /tmp (should work for workspace/strict, blocked for read-only)
|
||||
// Allowed for workspace/strict, blocked for read-only.
|
||||
let tmp_test = Path::new("/tmp/.kigi-sandbox-test");
|
||||
test_write("Write to /tmp", tmp_test);
|
||||
let _ = std::fs::remove_file(tmp_test);
|
||||
|
||||
// Test 6: Write outside workspace (should be blocked for all active profiles)
|
||||
// Blocked for all active profiles.
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let outside = home.join(".sandbox-test-blocked");
|
||||
test_write("Write to ~/", &outside);
|
||||
let _ = std::fs::remove_file(&outside);
|
||||
}
|
||||
|
||||
// Test 7: Read ~/.ssh (a custom profile's `deny` list could block this)
|
||||
// A custom profile's `deny` list could block this.
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let ssh = home.join(".ssh");
|
||||
if ssh.exists() {
|
||||
@@ -108,7 +106,6 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
println!("\n--- Sandbox event log ---");
|
||||
let events = sandbox.logger().take_events();
|
||||
for event in &events {
|
||||
|
||||
@@ -15,7 +15,8 @@ pub unsafe fn install_child_network_filter() -> std::io::Result<()> {
|
||||
|
||||
const SECCOMP_RET_ALLOW: u32 = 0x7fff_0000;
|
||||
const SECCOMP_RET_ERRNO: u32 = 0x0005_0000;
|
||||
const EPERM_VAL: u32 = 1; // libc::EPERM
|
||||
// libc::EPERM
|
||||
const EPERM_VAL: u32 = 1;
|
||||
|
||||
macro_rules! bpf_stmt {
|
||||
($code:expr, $k:expr) => {
|
||||
@@ -39,7 +40,8 @@ pub unsafe fn install_child_network_filter() -> std::io::Result<()> {
|
||||
};
|
||||
}
|
||||
|
||||
const NR_OFFSET: u32 = 0; // seccomp_data.nr offset
|
||||
// seccomp_data.nr offset
|
||||
const NR_OFFSET: u32 = 0;
|
||||
|
||||
let blocked_syscalls: &[i64] = &[
|
||||
SYS_connect,
|
||||
@@ -54,24 +56,22 @@ pub unsafe fn install_child_network_filter() -> std::io::Result<()> {
|
||||
let mut filter: Vec<sock_filter> = Vec::new();
|
||||
let total_checks = blocked_syscalls.len();
|
||||
|
||||
// 1. Load syscall number
|
||||
filter.push(bpf_stmt!(BPF_LD | BPF_W | BPF_ABS, NR_OFFSET));
|
||||
|
||||
// 2. Check each blocked syscall
|
||||
for (i, &syscall) in blocked_syscalls.iter().enumerate() {
|
||||
let remaining = total_checks - i - 1;
|
||||
filter.push(bpf_jump!(
|
||||
BPF_JMP | BPF_JEQ | BPF_K,
|
||||
syscall,
|
||||
remaining as u8 + 1, // match: jump to ERRNO
|
||||
0 // no match: check next
|
||||
// match: jump to ERRNO
|
||||
remaining as u8 + 1,
|
||||
// no match: check next
|
||||
0
|
||||
));
|
||||
}
|
||||
|
||||
// 3. Default: ALLOW
|
||||
filter.push(bpf_stmt!(BPF_RET | BPF_K, SECCOMP_RET_ALLOW));
|
||||
|
||||
// 4. Blocked: ERRNO(EPERM)
|
||||
filter.push(bpf_stmt!(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM_VAL));
|
||||
|
||||
let prog = sock_fprog {
|
||||
|
||||
@@ -25,7 +25,7 @@ pub(crate) fn is_glob(entry: &str) -> bool {
|
||||
|
||||
/// Split a profile's raw deny entries into exact paths (handled by the literal /
|
||||
/// subpath kernel-deny flow) and glob patterns. Non-glob entries are returned
|
||||
/// unchanged so their exact-path enforcement is preserved with no regression.
|
||||
/// `unchanged` so their exact-path enforcement is preserved with no regression.
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
pub(crate) fn partition_deny_entries(deny: &[PathBuf]) -> (Vec<PathBuf>, Vec<String>) {
|
||||
let mut exact = Vec::new();
|
||||
@@ -170,12 +170,15 @@ fn glob_tail_to_regex(tail: &str) -> String {
|
||||
chars.next();
|
||||
if chars.peek() == Some(&'/') {
|
||||
chars.next();
|
||||
out.push_str("(.*/)?"); // `**/` spans zero or more dirs
|
||||
// `**/` spans zero or more dirs
|
||||
out.push_str("(.*/)?");
|
||||
} else {
|
||||
out.push_str(".*"); // `**` spans anything, incl. `/`
|
||||
// `**` spans anything, incl. `/`
|
||||
out.push_str(".*");
|
||||
}
|
||||
} else {
|
||||
out.push_str("[^/]*"); // `*` stops at a path separator
|
||||
// `*` stops at a path separator
|
||||
out.push_str("[^/]*");
|
||||
}
|
||||
}
|
||||
'?' => out.push_str("[^/]"),
|
||||
@@ -622,7 +625,8 @@ mod tests {
|
||||
}
|
||||
for p in &patterns {
|
||||
if validate_deny_glob(p).is_err() {
|
||||
continue; // rejected patterns aren't enforced on either platform
|
||||
// rejected patterns aren't enforced on either platform
|
||||
continue;
|
||||
}
|
||||
let regexes = glob_to_seatbelt_regexes(Path::new("/ws"), p);
|
||||
assert_eq!(regexes.len(), 1, "expected one regex for {p:?}");
|
||||
@@ -732,7 +736,8 @@ mod tests {
|
||||
let _g = TmpTree(ws.clone());
|
||||
std::fs::create_dir_all(ws.join("sub/dir")).unwrap();
|
||||
std::fs::write(ws.join("sub/dir/key.pem"), "x").unwrap();
|
||||
std::fs::write(ws.join(".env"), "x").unwrap(); // hidden + usually gitignored
|
||||
// hidden + usually gitignored
|
||||
std::fs::write(ws.join(".env"), "x").unwrap();
|
||||
std::fs::write(ws.join("readable.txt"), "x").unwrap();
|
||||
let globs = vec!["**/*.pem".to_string(), "**/.env".to_string()];
|
||||
let out = expand_deny_globs(&ws, &globs, 64, 4096, 200_000).expect("should expand");
|
||||
|
||||
@@ -60,11 +60,9 @@ struct GlobalSandboxState {
|
||||
logger: SandboxLogger,
|
||||
applied: bool,
|
||||
}
|
||||
/// Whether child subprocesses should have network blocked via seccomp.
|
||||
pub fn should_restrict_child_network() -> bool {
|
||||
RESTRICT_CHILD_NETWORK.load(Ordering::Relaxed)
|
||||
}
|
||||
/// Whether bash commands should be auto-approved when the sandbox is active.
|
||||
pub fn should_auto_allow_bash() -> bool {
|
||||
AUTO_ALLOW_BASH.load(Ordering::Relaxed) && is_active()
|
||||
}
|
||||
@@ -75,7 +73,6 @@ pub fn set_auto_allow_bash(enabled: bool) {
|
||||
pub fn set_configured_profile(name: impl Into<String>) {
|
||||
let _ = CONFIGURED_PROFILE.set(name.into());
|
||||
}
|
||||
/// Resolved sandbox profile from startup, or `None` if `set_configured_profile` was never called.
|
||||
pub fn configured_profile_name() -> Option<&'static str> {
|
||||
CONFIGURED_PROFILE.get().map(|s| s.as_str())
|
||||
}
|
||||
@@ -83,15 +80,13 @@ pub fn configured_profile_name() -> Option<&'static str> {
|
||||
pub fn is_active() -> bool {
|
||||
SANDBOX.get().is_some_and(|s| s.applied)
|
||||
}
|
||||
/// The active sandbox profile name, or `None` if sandbox is not applied.
|
||||
pub fn profile_name() -> Option<&'static str> {
|
||||
SANDBOX
|
||||
.get()
|
||||
.filter(|s| s.applied)
|
||||
.map(|s| s.profile.as_str())
|
||||
}
|
||||
/// Log a sandbox violation. Immediately flushed to disk.
|
||||
/// No-op if sandbox is not active.
|
||||
/// Log a sandbox violation, flushing it to disk immediately.
|
||||
pub fn log_violation(target: &str, operation: &str) {
|
||||
if let Some(state) = SANDBOX.get() {
|
||||
state.logger.log(SandboxEvent::fs_violation(
|
||||
@@ -102,7 +97,6 @@ pub fn log_violation(target: &str, operation: &str) {
|
||||
let _ = state.logger.flush_to_disk();
|
||||
}
|
||||
}
|
||||
/// Flush sandbox events to disk. No-op if not initialized.
|
||||
pub fn flush() {
|
||||
if let Some(state) = SANDBOX.get()
|
||||
&& let Err(e) = state.logger.flush_to_disk()
|
||||
@@ -110,7 +104,6 @@ pub fn flush() {
|
||||
tracing::warn!(error = % e, "Failed to flush sandbox events to disk");
|
||||
}
|
||||
}
|
||||
/// Violation metrics, or `None` if sandbox is not active.
|
||||
pub fn metrics() -> Option<&'static SandboxMetrics> {
|
||||
SANDBOX.get().map(|s| s.logger.metrics())
|
||||
}
|
||||
@@ -122,7 +115,6 @@ pub struct SandboxManager {
|
||||
applied: bool,
|
||||
}
|
||||
impl SandboxManager {
|
||||
/// Create a sandbox manager. Does not apply until `apply()` is called.
|
||||
pub fn new(profile: ProfileName, _workspace: &Path) -> Self {
|
||||
let net_restricted = profile.restricts_network();
|
||||
Self {
|
||||
@@ -210,24 +202,19 @@ impl SandboxManager {
|
||||
applied: self.applied,
|
||||
});
|
||||
}
|
||||
/// Check whether the current platform supports sandboxing.
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
pub fn support_info() -> nono::SupportInfo {
|
||||
Sandbox::support_info()
|
||||
}
|
||||
/// Whether the sandbox was successfully applied.
|
||||
pub fn is_applied(&self) -> bool {
|
||||
self.applied
|
||||
}
|
||||
/// Whether child subprocesses should have network blocked.
|
||||
pub fn restrict_child_network(&self) -> bool {
|
||||
self.applied && self.net_restricted
|
||||
}
|
||||
/// The active profile name.
|
||||
pub fn profile(&self) -> &ProfileName {
|
||||
&self.profile
|
||||
}
|
||||
/// Access the sandbox event logger (before `install()`).
|
||||
pub fn logger(&self) -> &SandboxLogger {
|
||||
&self.logger
|
||||
}
|
||||
@@ -480,7 +467,7 @@ pub fn bwrap_reexec_for_profile(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serial_test::serial;
|
||||
/// Save, set/remove, and auto-restore an env var on drop.
|
||||
/// Restores the previous value of the env var on drop.
|
||||
struct EnvGuard {
|
||||
key: &'static str,
|
||||
prev: Option<String>,
|
||||
@@ -612,8 +599,8 @@ mod tests {
|
||||
set_configured_profile("read-only");
|
||||
assert_eq!(configured_profile_name(), Some("read-only"));
|
||||
}
|
||||
/// Create a temp workspace whose `.kigi/sandbox.toml` contains `toml_body`.
|
||||
/// Returns the workspace path (caller removes it).
|
||||
/// Returns a workspace whose `.kigi/sandbox.toml` holds `toml_body`; the
|
||||
/// caller is responsible for removing it.
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn temp_workspace_with_sandbox_toml(tag: &str, toml_body: &str) -> PathBuf {
|
||||
let nanos = std::time::SystemTime::now()
|
||||
@@ -626,9 +613,8 @@ mod tests {
|
||||
std::fs::write(kigi.join("sandbox.toml"), toml_body).unwrap();
|
||||
ws
|
||||
}
|
||||
/// Create a temp workspace defining a `denytest` profile (extends `workspace`)
|
||||
/// with the given `deny` list. `deny_toml` is the raw TOML array body
|
||||
/// (e.g. `"\".env\""`).
|
||||
/// Defines a `denytest` profile extending `workspace`. `deny_toml` is the
|
||||
/// raw TOML array body, e.g. `"\".env\""`.
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
fn temp_workspace_with_deny(tag: &str, deny_toml: &str) -> PathBuf {
|
||||
temp_workspace_with_sandbox_toml(
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
//! Sandbox event logger.
|
||||
//!
|
||||
//! Records sandbox events (profile applied, violations, bypasses) for
|
||||
//! telemetry and debugging. Events are kept in memory and can be flushed
|
||||
//! to a JSONL file at `~/.kigi/sandbox-events.jsonl`.
|
||||
//! Events (profile applied, violations, bypasses) are buffered in memory and
|
||||
//! flushed as JSONL to `~/.kigi/sandbox-events.jsonl`.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::types::{SandboxEvent, SandboxEventType, SandboxMetrics};
|
||||
|
||||
/// Logger that collects sandbox events and maintains violation counters.
|
||||
pub struct SandboxLogger {
|
||||
events: Mutex<Vec<SandboxEvent>>,
|
||||
metrics: SandboxMetrics,
|
||||
@@ -23,7 +21,6 @@ impl SandboxLogger {
|
||||
}
|
||||
}
|
||||
|
||||
/// Record an event, updating metrics counters as appropriate.
|
||||
pub fn log(&self, event: SandboxEvent) {
|
||||
match &event.event_type {
|
||||
SandboxEventType::FsViolation => self.metrics.inc_fs_violation(),
|
||||
@@ -46,12 +43,11 @@ impl SandboxLogger {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the metrics counters.
|
||||
pub fn metrics(&self) -> &SandboxMetrics {
|
||||
&self.metrics
|
||||
}
|
||||
|
||||
/// Take all accumulated events, draining the internal buffer.
|
||||
/// Drains the buffer.
|
||||
pub fn take_events(&self) -> Vec<SandboxEvent> {
|
||||
self.events
|
||||
.lock()
|
||||
@@ -59,8 +55,6 @@ impl SandboxLogger {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Flush accumulated events to the JSONL log file.
|
||||
/// Each event is written as a single JSON line.
|
||||
pub fn flush_to_disk(&self) -> anyhow::Result<()> {
|
||||
let events = self.take_events();
|
||||
if events.is_empty() {
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// ── Kigi state directory ────────────────────────────────────────────────────
|
||||
// Kigi state directory
|
||||
|
||||
/// Kigi state directory — always writable (`$KIGI_SHARE_DIR` or `~/.kigi`).
|
||||
pub(crate) fn kigi_home() -> PathBuf {
|
||||
kigi_config::kigi_home()
|
||||
}
|
||||
|
||||
// ── Device files & directories ──────────────────────────────────────────────
|
||||
// Device files & directories
|
||||
|
||||
/// Device files that need write access for normal tool operation.
|
||||
///
|
||||
@@ -27,22 +27,30 @@ pub(crate) fn kigi_home() -> PathBuf {
|
||||
/// `/dev/pts` is a directory (PTY slaves on Linux) so it uses `allow_path`.
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
pub(crate) const DEVICE_FILES: &[&str] = &[
|
||||
"/dev/null", // output sink — used by virtually every CLI tool
|
||||
"/dev/zero", // zero source — used by memory allocators
|
||||
"/dev/random", // entropy — used by crypto/TLS
|
||||
"/dev/urandom", // entropy — used by crypto/TLS
|
||||
"/dev/tty", // controlling terminal — used by git, ssh, gpg
|
||||
"/dev/ptmx", // PTY allocation — used by terminal spawning
|
||||
"/dev/fd", // file descriptor access (symlink to /proc/self/fd on Linux)
|
||||
// output sink — used by virtually every CLI tool
|
||||
"/dev/null",
|
||||
// zero source — used by memory allocators
|
||||
"/dev/zero",
|
||||
// entropy — used by crypto/TLS
|
||||
"/dev/random",
|
||||
// entropy — used by crypto/TLS
|
||||
"/dev/urandom",
|
||||
// controlling terminal — used by git, ssh, gpg
|
||||
"/dev/tty",
|
||||
// PTY allocation — used by terminal spawning
|
||||
"/dev/ptmx",
|
||||
// file descriptor access (symlink to /proc/self/fd on Linux)
|
||||
"/dev/fd",
|
||||
];
|
||||
|
||||
/// Device directories that need write access.
|
||||
#[cfg(all(feature = "enforce", unix))]
|
||||
pub(crate) const DEVICE_DIRS: &[&str] = &[
|
||||
"/dev/pts", // PTY slaves (Linux)
|
||||
// PTY slaves (Linux)
|
||||
"/dev/pts",
|
||||
];
|
||||
|
||||
// ── Temporary directories ───────────────────────────────────────────────────
|
||||
// Temporary directories
|
||||
|
||||
/// Temporary directories that need write access.
|
||||
///
|
||||
@@ -77,7 +85,7 @@ pub(crate) fn temp_writable_paths() -> Vec<PathBuf> {
|
||||
paths
|
||||
}
|
||||
|
||||
// ── Essential writable paths ────────────────────────────────────────────────
|
||||
// Essential writable paths
|
||||
|
||||
/// Writable directory paths for profiles that allow workspace writes (workspace, devbox, strict).
|
||||
/// Device files are handled separately via `allow_file` in `to_capability_set_with_config`.
|
||||
|
||||
@@ -367,10 +367,11 @@ impl ProfileName {
|
||||
"/run",
|
||||
// NSS/SSSD (and similar) under /var — needed beyond resolv.conf alone
|
||||
"/var",
|
||||
// macOS-specific paths (filtered by exists() below)
|
||||
"/System", // Security framework, dylibs, TLS certificates
|
||||
"/Library", // System-wide frameworks
|
||||
"/private", // Real path behind /etc, /tmp, /var symlinks
|
||||
// macOS-specific paths, filtered by exists() below:
|
||||
// /System (security framework, dylibs, TLS certs),
|
||||
// /Library (system-wide frameworks), /private (real path
|
||||
// behind the /etc, /tmp, /var symlinks).
|
||||
"/System", "/Library", "/private",
|
||||
]
|
||||
.iter()
|
||||
.map(PathBuf::from)
|
||||
|
||||
@@ -4,14 +4,13 @@ use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// A recorded sandbox event for telemetry and debugging.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SandboxEvent {
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub event_type: SandboxEventType,
|
||||
pub profile: String,
|
||||
|
||||
// Context fields — present on ProfileApplied/ApplyFailed
|
||||
// Only ProfileApplied and ApplyFailed carry these.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub workspace: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -27,7 +26,6 @@ pub struct SandboxEvent {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deny_paths: Option<Vec<String>>,
|
||||
|
||||
// Violation/error fields
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub operation: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -61,7 +59,6 @@ impl SandboxEvent {
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a "profile applied" event with full context.
|
||||
pub fn profile_applied(
|
||||
profile: &str,
|
||||
workspace: &std::path::Path,
|
||||
@@ -108,7 +105,6 @@ impl SandboxEvent {
|
||||
event
|
||||
}
|
||||
|
||||
/// Create an "apply failed" event with context.
|
||||
pub fn apply_failed(
|
||||
profile: &str,
|
||||
workspace: &std::path::Path,
|
||||
@@ -130,7 +126,6 @@ impl SandboxEvent {
|
||||
event
|
||||
}
|
||||
|
||||
/// Create a filesystem violation event.
|
||||
pub fn fs_violation(profile: &str, target: &str, operation: &str) -> Self {
|
||||
let mut event = Self::base(SandboxEventType::FsViolation, profile);
|
||||
event.operation = Some(operation.to_string());
|
||||
@@ -138,7 +133,6 @@ impl SandboxEvent {
|
||||
event
|
||||
}
|
||||
|
||||
/// Create a network violation event.
|
||||
pub fn net_violation(profile: &str, target: &str) -> Self {
|
||||
let mut event = Self::base(SandboxEventType::NetViolation, profile);
|
||||
event.operation = Some("connect".to_string());
|
||||
@@ -157,7 +151,6 @@ pub enum SandboxEventType {
|
||||
BypassDenied,
|
||||
}
|
||||
|
||||
/// Counters for sandbox activity, used for telemetry dashboards.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SandboxMetrics {
|
||||
pub fs_violations: AtomicU64,
|
||||
|
||||
@@ -134,7 +134,8 @@ fn assert_write_denied(label: &str, path: &Path) {
|
||||
fn assert_rename_bypass_blocked(label: &str, path: &Path, workspace: &Path) {
|
||||
let name = path.file_name().unwrap().to_string_lossy();
|
||||
let moved = workspace.join(format!("exfil-{name}"));
|
||||
let _ = fs::rename(path, &moved); // expected to fail; bytes must not leak
|
||||
// expected to fail; bytes must not leak
|
||||
let _ = fs::rename(path, &moved);
|
||||
match fs::read_to_string(&moved) {
|
||||
Ok(c) if c.contains(MARKER) => {
|
||||
eprintln!("FAIL: {label} rename bypass exposed MARKER");
|
||||
@@ -160,7 +161,7 @@ fn profile_from_env() -> kigi_sandbox::ProfileName {
|
||||
kigi_sandbox::ProfileName::Custom(std::env::var(PROFILE_ENV).expect(PROFILE_ENV))
|
||||
}
|
||||
|
||||
// ── Subprocess entry point ──────────────────────────────────────────────
|
||||
// Subprocess entry point
|
||||
|
||||
/// `#[ignore]`d — only runs when invoked by the parent test via `run_scenario`.
|
||||
#[test]
|
||||
@@ -186,7 +187,8 @@ fn subprocess_entry() {
|
||||
match kigi_sandbox::bwrap_reexec_for_profile(&profile_from_env(), workspace) {
|
||||
Some(mut cmd) => {
|
||||
use std::os::unix::process::CommandExt;
|
||||
let err = cmd.exec(); // returns only if exec failed
|
||||
// returns only if exec failed
|
||||
let err = cmd.exec();
|
||||
eprintln!("bwrap re-exec failed: {err}");
|
||||
std::process::exit(2);
|
||||
}
|
||||
@@ -280,7 +282,7 @@ fn subprocess_entry() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Parent test cases ───────────────────────────────────────────────────
|
||||
// Parent test cases
|
||||
|
||||
/// Drive one deny case end-to-end: define a custom profile whose `deny` list is
|
||||
/// `deny_entries` (exact paths and/or globs), create each `target` (with the
|
||||
@@ -418,7 +420,8 @@ fn deny_exact_paths_block_read_write_rename() {
|
||||
&[".env", "src/server.pem", "secretdir"],
|
||||
&[".env", "src/server.pem", "secretdir/inner.pem"],
|
||||
&["readable.txt"],
|
||||
&[], // exact paths have no runtime/post-launch coverage to assert
|
||||
// exact paths have no runtime/post-launch coverage to assert
|
||||
&[],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ fn test_sandbox_logger() {
|
||||
let events = logger.take_events();
|
||||
assert_eq!(events.len(), 3);
|
||||
|
||||
// Buffer is now empty
|
||||
// Buffer is empty
|
||||
let events2 = logger.take_events();
|
||||
assert!(events2.is_empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user