mirror of
https://github.com/bahdotsh/wrkflw.git
synced 2025-12-16 11:47:45 +01:00
Merge pull request #37 from bahdotsh/feature/secure-emulation-sandboxing
feat: Add comprehensive sandboxing for secure emulation mode
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2712,9 +2712,11 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"futures",
|
"futures",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"which",
|
"which",
|
||||||
"wrkflw-logging",
|
"wrkflw-logging",
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -443,19 +443,28 @@ jobs:
|
|||||||
### Runtime Mode Differences
|
### Runtime Mode Differences
|
||||||
- **Docker Mode**: Provides the closest match to GitHub's environment, including support for Docker container actions, service containers, and Linux-based jobs. Some advanced container configurations may still require manual setup.
|
- **Docker Mode**: Provides the closest match to GitHub's environment, including support for Docker container actions, service containers, and Linux-based jobs. Some advanced container configurations may still require manual setup.
|
||||||
- **Podman Mode**: Similar to Docker mode but uses Podman for container execution. Offers rootless container support and enhanced security. Fully compatible with Docker-based workflows.
|
- **Podman Mode**: Similar to Docker mode but uses Podman for container execution. Offers rootless container support and enhanced security. Fully compatible with Docker-based workflows.
|
||||||
- **Emulation Mode**: Runs workflows using the local system tools. Limitations:
|
- **🔒 Secure Emulation Mode**: Runs workflows on the local system with comprehensive sandboxing for security. **Recommended for local development**:
|
||||||
|
- Command validation and filtering (blocks dangerous commands like `rm -rf /`, `sudo`, etc.)
|
||||||
|
- Resource limits (CPU, memory, execution time)
|
||||||
|
- Filesystem access controls
|
||||||
|
- Process monitoring and limits
|
||||||
|
- Safe for running untrusted workflows locally
|
||||||
|
- **⚠️ Emulation Mode (Legacy)**: Runs workflows using local system tools without sandboxing. **Not recommended - use Secure Emulation instead**:
|
||||||
- Only supports local and JavaScript actions (no Docker container actions)
|
- Only supports local and JavaScript actions (no Docker container actions)
|
||||||
- No support for service containers
|
- No support for service containers
|
||||||
- No caching support
|
- No caching support
|
||||||
|
- **No security protections - can execute harmful commands**
|
||||||
- Some actions may require adaptation to work locally
|
- Some actions may require adaptation to work locally
|
||||||
- Special action handling is more limited
|
|
||||||
|
|
||||||
### Best Practices
|
### Best Practices
|
||||||
- Test workflows in both Docker and emulation modes to ensure compatibility
|
- **Use Secure Emulation mode for local development** - provides safety without container overhead
|
||||||
|
- Test workflows in multiple runtime modes to ensure compatibility
|
||||||
|
- **Use Docker/Podman mode for production** - provides maximum isolation and reproducibility
|
||||||
- Keep matrix builds reasonably sized for better performance
|
- Keep matrix builds reasonably sized for better performance
|
||||||
- Use environment variables instead of GitHub secrets when possible
|
- Use environment variables instead of GitHub secrets when possible
|
||||||
- Consider using local actions for complex custom functionality
|
- Consider using local actions for complex custom functionality
|
||||||
- Test network-dependent actions carefully in both modes
|
- **Review security warnings** - pay attention to blocked commands in secure emulation mode
|
||||||
|
- **Start with secure mode** - only fall back to legacy emulation if necessary
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ async fn execute_github_workflow(
|
|||||||
"WRKFLW_RUNTIME_MODE".to_string(),
|
"WRKFLW_RUNTIME_MODE".to_string(),
|
||||||
match config.runtime_type {
|
match config.runtime_type {
|
||||||
RuntimeType::Emulation => "emulation".to_string(),
|
RuntimeType::Emulation => "emulation".to_string(),
|
||||||
|
RuntimeType::SecureEmulation => "secure_emulation".to_string(),
|
||||||
RuntimeType::Docker => "docker".to_string(),
|
RuntimeType::Docker => "docker".to_string(),
|
||||||
RuntimeType::Podman => "podman".to_string(),
|
RuntimeType::Podman => "podman".to_string(),
|
||||||
},
|
},
|
||||||
@@ -198,6 +199,7 @@ async fn execute_gitlab_pipeline(
|
|||||||
"WRKFLW_RUNTIME_MODE".to_string(),
|
"WRKFLW_RUNTIME_MODE".to_string(),
|
||||||
match config.runtime_type {
|
match config.runtime_type {
|
||||||
RuntimeType::Emulation => "emulation".to_string(),
|
RuntimeType::Emulation => "emulation".to_string(),
|
||||||
|
RuntimeType::SecureEmulation => "secure_emulation".to_string(),
|
||||||
RuntimeType::Docker => "docker".to_string(),
|
RuntimeType::Docker => "docker".to_string(),
|
||||||
RuntimeType::Podman => "podman".to_string(),
|
RuntimeType::Podman => "podman".to_string(),
|
||||||
},
|
},
|
||||||
@@ -400,6 +402,9 @@ fn initialize_runtime(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
RuntimeType::Emulation => Ok(Box::new(emulation::EmulationRuntime::new())),
|
RuntimeType::Emulation => Ok(Box::new(emulation::EmulationRuntime::new())),
|
||||||
|
RuntimeType::SecureEmulation => Ok(Box::new(
|
||||||
|
wrkflw_runtime::secure_emulation::SecureEmulationRuntime::new(),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +413,7 @@ pub enum RuntimeType {
|
|||||||
Docker,
|
Docker,
|
||||||
Podman,
|
Podman,
|
||||||
Emulation,
|
Emulation,
|
||||||
|
SecureEmulation,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|||||||
@@ -25,3 +25,5 @@ tokio.workspace = true
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
wrkflw-utils = { path = "../utils", version = "0.6.0" }
|
wrkflw-utils = { path = "../utils", version = "0.6.0" }
|
||||||
which = "4.4"
|
which = "4.4"
|
||||||
|
regex = "1.10"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|||||||
258
crates/runtime/README_SECURITY.md
Normal file
258
crates/runtime/README_SECURITY.md
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
# Security Features in wrkflw Runtime
|
||||||
|
|
||||||
|
This document describes the security features implemented in the wrkflw runtime, particularly the sandboxing capabilities for emulation mode.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The wrkflw runtime provides multiple execution modes with varying levels of security:
|
||||||
|
|
||||||
|
1. **Docker Mode** - Uses Docker containers for isolation (recommended for production)
|
||||||
|
2. **Podman Mode** - Uses Podman containers for isolation with rootless support
|
||||||
|
3. **Secure Emulation Mode** - 🔒 **NEW**: Sandboxed execution on the host system
|
||||||
|
4. **Emulation Mode** - ⚠️ **UNSAFE**: Direct execution on the host system (deprecated)
|
||||||
|
|
||||||
|
## Security Modes
|
||||||
|
|
||||||
|
### 🔒 Secure Emulation Mode (Recommended for Local Development)
|
||||||
|
|
||||||
|
The secure emulation mode provides comprehensive sandboxing to protect your system from potentially harmful commands while still allowing legitimate workflow operations.
|
||||||
|
|
||||||
|
#### Features
|
||||||
|
|
||||||
|
- **Command Validation**: Blocks dangerous commands like `rm -rf /`, `dd`, `sudo`, etc.
|
||||||
|
- **Pattern Detection**: Uses regex patterns to detect dangerous command combinations
|
||||||
|
- **Resource Limits**: Enforces CPU, memory, and execution time limits
|
||||||
|
- **Filesystem Isolation**: Restricts file access to allowed paths only
|
||||||
|
- **Environment Sanitization**: Filters dangerous environment variables
|
||||||
|
- **Process Monitoring**: Tracks and limits spawned processes
|
||||||
|
|
||||||
|
#### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use secure emulation mode (recommended)
|
||||||
|
wrkflw run --runtime secure-emulation .github/workflows/build.yml
|
||||||
|
|
||||||
|
# Or via TUI
|
||||||
|
wrkflw tui --runtime secure-emulation
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Command Whitelist/Blacklist
|
||||||
|
|
||||||
|
**Allowed Commands (Safe):**
|
||||||
|
- Basic utilities: `echo`, `cat`, `ls`, `grep`, `sed`, `awk`
|
||||||
|
- Development tools: `cargo`, `npm`, `python`, `git`, `node`
|
||||||
|
- Build tools: `make`, `cmake`, `javac`, `dotnet`
|
||||||
|
|
||||||
|
**Blocked Commands (Dangerous):**
|
||||||
|
- System modification: `rm`, `dd`, `mkfs`, `mount`, `sudo`
|
||||||
|
- Network tools: `wget`, `curl`, `ssh`, `nc`
|
||||||
|
- Process control: `kill`, `killall`, `systemctl`
|
||||||
|
|
||||||
|
#### Resource Limits
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Default configuration
|
||||||
|
SandboxConfig {
|
||||||
|
max_execution_time: Duration::from_secs(300), // 5 minutes
|
||||||
|
max_memory_mb: 512, // 512 MB
|
||||||
|
max_cpu_percent: 80, // 80% CPU
|
||||||
|
max_processes: 10, // Max 10 processes
|
||||||
|
allow_network: false, // No network access
|
||||||
|
strict_mode: true, // Whitelist-only mode
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### ⚠️ Legacy Emulation Mode (Unsafe)
|
||||||
|
|
||||||
|
The original emulation mode executes commands directly on the host system without any sandboxing. **This mode will be deprecated and should only be used for trusted workflows.**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Legacy unsafe mode (not recommended)
|
||||||
|
wrkflw run --runtime emulation .github/workflows/build.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example: Blocked vs Allowed Commands
|
||||||
|
|
||||||
|
### ❌ Blocked Commands
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# This workflow will be blocked in secure emulation mode
|
||||||
|
steps:
|
||||||
|
- name: Dangerous command
|
||||||
|
run: rm -rf /tmp/* # BLOCKED: Dangerous file deletion
|
||||||
|
|
||||||
|
- name: System modification
|
||||||
|
run: sudo apt-get install package # BLOCKED: sudo usage
|
||||||
|
|
||||||
|
- name: Network access
|
||||||
|
run: wget https://malicious-site.com/script.sh | sh # BLOCKED: wget + shell execution
|
||||||
|
```
|
||||||
|
|
||||||
|
### ✅ Allowed Commands
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# This workflow will run successfully in secure emulation mode
|
||||||
|
steps:
|
||||||
|
- name: Build project
|
||||||
|
run: cargo build --release # ALLOWED: Development tool
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test # ALLOWED: Testing
|
||||||
|
|
||||||
|
- name: List files
|
||||||
|
run: ls -la target/ # ALLOWED: Safe file listing
|
||||||
|
|
||||||
|
- name: Format code
|
||||||
|
run: cargo fmt --check # ALLOWED: Code formatting
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Warnings and Messages
|
||||||
|
|
||||||
|
When dangerous commands are detected, wrkflw provides clear security messages:
|
||||||
|
|
||||||
|
```
|
||||||
|
🚫 SECURITY BLOCK: Command 'rm' is not allowed in secure emulation mode.
|
||||||
|
This command was blocked for security reasons.
|
||||||
|
If you need to run this command, please use Docker or Podman mode instead.
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
🚫 SECURITY BLOCK: Dangerous command pattern detected: 'rm -rf /'.
|
||||||
|
This command was blocked because it matches a known dangerous pattern.
|
||||||
|
Please review your workflow for potentially harmful commands.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration Examples
|
||||||
|
|
||||||
|
### Workflow-Friendly Configuration
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wrkflw_runtime::sandbox::create_workflow_sandbox_config;
|
||||||
|
|
||||||
|
let config = create_workflow_sandbox_config();
|
||||||
|
// - Allows network access for package downloads
|
||||||
|
// - Higher resource limits for CI/CD workloads
|
||||||
|
// - Less strict mode for development flexibility
|
||||||
|
```
|
||||||
|
|
||||||
|
### Strict Security Configuration
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wrkflw_runtime::sandbox::create_strict_sandbox_config;
|
||||||
|
|
||||||
|
let config = create_strict_sandbox_config();
|
||||||
|
// - No network access
|
||||||
|
// - Very limited command set
|
||||||
|
// - Low resource limits
|
||||||
|
// - Strict whitelist-only mode
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Configuration
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wrkflw_runtime::sandbox::{SandboxConfig, Sandbox};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
let mut config = SandboxConfig::default();
|
||||||
|
|
||||||
|
// Custom allowed commands
|
||||||
|
config.allowed_commands = ["echo", "ls", "cargo"]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Custom resource limits
|
||||||
|
config.max_execution_time = Duration::from_secs(60);
|
||||||
|
config.max_memory_mb = 256;
|
||||||
|
|
||||||
|
// Custom allowed paths
|
||||||
|
config.allowed_write_paths.insert(PathBuf::from("./target"));
|
||||||
|
config.allowed_read_paths.insert(PathBuf::from("./src"));
|
||||||
|
|
||||||
|
let sandbox = Sandbox::new(config)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### From Unsafe Emulation to Secure Emulation
|
||||||
|
|
||||||
|
1. **Change Runtime Flag**:
|
||||||
|
```bash
|
||||||
|
# Old (unsafe)
|
||||||
|
wrkflw run --runtime emulation workflow.yml
|
||||||
|
|
||||||
|
# New (secure)
|
||||||
|
wrkflw run --runtime secure-emulation workflow.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Review Workflow Commands**: Check for any commands that might be blocked and adjust if necessary.
|
||||||
|
|
||||||
|
3. **Handle Security Blocks**: If legitimate commands are blocked, consider:
|
||||||
|
- Using Docker/Podman mode for those specific workflows
|
||||||
|
- Modifying the workflow to use allowed alternatives
|
||||||
|
- Creating a custom sandbox configuration
|
||||||
|
|
||||||
|
### When to Use Each Mode
|
||||||
|
|
||||||
|
| Use Case | Recommended Mode | Reason |
|
||||||
|
|----------|------------------|---------|
|
||||||
|
| Local development | Secure Emulation | Good balance of security and convenience |
|
||||||
|
| Untrusted workflows | Docker/Podman | Maximum isolation |
|
||||||
|
| CI/CD pipelines | Docker/Podman | Consistent, reproducible environment |
|
||||||
|
| Testing workflows | Secure Emulation | Fast execution with safety |
|
||||||
|
| Trusted internal workflows | Secure Emulation | Sufficient security for known-safe code |
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Command Blocked Error
|
||||||
|
|
||||||
|
If you encounter a security block:
|
||||||
|
|
||||||
|
1. **Check if the command is necessary**: Can you achieve the same result with an allowed command?
|
||||||
|
2. **Use container mode**: Switch to Docker or Podman mode for unrestricted execution
|
||||||
|
3. **Modify the workflow**: Use safer alternatives where possible
|
||||||
|
|
||||||
|
### Resource Limit Exceeded
|
||||||
|
|
||||||
|
If your workflow hits resource limits:
|
||||||
|
|
||||||
|
1. **Optimize the workflow**: Reduce resource usage where possible
|
||||||
|
2. **Use custom configuration**: Increase limits for specific use cases
|
||||||
|
3. **Use container mode**: For resource-intensive workflows
|
||||||
|
|
||||||
|
### Path Access Denied
|
||||||
|
|
||||||
|
If file access is denied:
|
||||||
|
|
||||||
|
1. **Check allowed paths**: Ensure your workflow only accesses permitted directories
|
||||||
|
2. **Use relative paths**: Work within the project directory
|
||||||
|
3. **Use container mode**: For workflows requiring system-wide file access
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Default to Secure Mode**: Use secure emulation mode by default for local development
|
||||||
|
2. **Test Workflows**: Always test workflows in secure mode before deploying
|
||||||
|
3. **Review Security Messages**: Pay attention to security blocks and warnings
|
||||||
|
4. **Use Containers for Production**: Use Docker/Podman for production deployments
|
||||||
|
5. **Regular Updates**: Keep wrkflw updated for the latest security improvements
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
- Secure emulation mode is designed to prevent **accidental** harmful commands, not to stop **determined** attackers
|
||||||
|
- For maximum security with untrusted code, always use container modes
|
||||||
|
- The sandbox is most effective against script errors and typos that could damage your system
|
||||||
|
- Always review workflows from untrusted sources before execution
|
||||||
|
|
||||||
|
## Contributing Security Improvements
|
||||||
|
|
||||||
|
If you find security issues or have suggestions for improvements:
|
||||||
|
|
||||||
|
1. **Report Security Issues**: Use responsible disclosure for security vulnerabilities
|
||||||
|
2. **Suggest Command Patterns**: Help improve dangerous pattern detection
|
||||||
|
3. **Test Edge Cases**: Help us identify bypass techniques
|
||||||
|
4. **Documentation**: Improve security documentation and examples
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For more information, see the main [README.md](../../README.md) and [Security Policy](../../SECURITY.md).
|
||||||
@@ -24,6 +24,7 @@ pub trait ContainerRuntime {
|
|||||||
) -> Result<String, ContainerError>;
|
) -> Result<String, ContainerError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct ContainerOutput {
|
pub struct ContainerOutput {
|
||||||
pub stdout: String,
|
pub stdout: String,
|
||||||
pub stderr: String,
|
pub stderr: String,
|
||||||
|
|||||||
@@ -2,3 +2,5 @@
|
|||||||
|
|
||||||
pub mod container;
|
pub mod container;
|
||||||
pub mod emulation;
|
pub mod emulation;
|
||||||
|
pub mod sandbox;
|
||||||
|
pub mod secure_emulation;
|
||||||
|
|||||||
672
crates/runtime/src/sandbox.rs
Normal file
672
crates/runtime/src/sandbox.rs
Normal file
@@ -0,0 +1,672 @@
|
|||||||
|
use regex::Regex;
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
use wrkflw_logging;
|
||||||
|
|
||||||
|
/// Configuration for sandbox execution
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SandboxConfig {
|
||||||
|
/// Maximum execution time for commands
|
||||||
|
pub max_execution_time: Duration,
|
||||||
|
/// Maximum memory usage in MB
|
||||||
|
pub max_memory_mb: u64,
|
||||||
|
/// Maximum CPU usage percentage
|
||||||
|
pub max_cpu_percent: u64,
|
||||||
|
/// Allowed commands (whitelist)
|
||||||
|
pub allowed_commands: HashSet<String>,
|
||||||
|
/// Blocked commands (blacklist)
|
||||||
|
pub blocked_commands: HashSet<String>,
|
||||||
|
/// Allowed file system paths (read-only)
|
||||||
|
pub allowed_read_paths: HashSet<PathBuf>,
|
||||||
|
/// Allowed file system paths (read-write)
|
||||||
|
pub allowed_write_paths: HashSet<PathBuf>,
|
||||||
|
/// Whether to enable network access
|
||||||
|
pub allow_network: bool,
|
||||||
|
/// Maximum number of processes
|
||||||
|
pub max_processes: u32,
|
||||||
|
/// Whether to enable strict mode (more restrictive)
|
||||||
|
pub strict_mode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SandboxConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut allowed_commands = HashSet::new();
|
||||||
|
|
||||||
|
// Basic safe commands
|
||||||
|
allowed_commands.insert("echo".to_string());
|
||||||
|
allowed_commands.insert("printf".to_string());
|
||||||
|
allowed_commands.insert("cat".to_string());
|
||||||
|
allowed_commands.insert("head".to_string());
|
||||||
|
allowed_commands.insert("tail".to_string());
|
||||||
|
allowed_commands.insert("grep".to_string());
|
||||||
|
allowed_commands.insert("sed".to_string());
|
||||||
|
allowed_commands.insert("awk".to_string());
|
||||||
|
allowed_commands.insert("sort".to_string());
|
||||||
|
allowed_commands.insert("uniq".to_string());
|
||||||
|
allowed_commands.insert("wc".to_string());
|
||||||
|
allowed_commands.insert("cut".to_string());
|
||||||
|
allowed_commands.insert("tr".to_string());
|
||||||
|
allowed_commands.insert("which".to_string());
|
||||||
|
allowed_commands.insert("pwd".to_string());
|
||||||
|
allowed_commands.insert("env".to_string());
|
||||||
|
allowed_commands.insert("date".to_string());
|
||||||
|
allowed_commands.insert("basename".to_string());
|
||||||
|
allowed_commands.insert("dirname".to_string());
|
||||||
|
|
||||||
|
// File operations (safe variants)
|
||||||
|
allowed_commands.insert("ls".to_string());
|
||||||
|
allowed_commands.insert("find".to_string());
|
||||||
|
allowed_commands.insert("mkdir".to_string());
|
||||||
|
allowed_commands.insert("touch".to_string());
|
||||||
|
allowed_commands.insert("cp".to_string());
|
||||||
|
allowed_commands.insert("mv".to_string());
|
||||||
|
|
||||||
|
// Development tools
|
||||||
|
allowed_commands.insert("git".to_string());
|
||||||
|
allowed_commands.insert("cargo".to_string());
|
||||||
|
allowed_commands.insert("rustc".to_string());
|
||||||
|
allowed_commands.insert("rustfmt".to_string());
|
||||||
|
allowed_commands.insert("clippy".to_string());
|
||||||
|
allowed_commands.insert("npm".to_string());
|
||||||
|
allowed_commands.insert("yarn".to_string());
|
||||||
|
allowed_commands.insert("node".to_string());
|
||||||
|
allowed_commands.insert("python".to_string());
|
||||||
|
allowed_commands.insert("python3".to_string());
|
||||||
|
allowed_commands.insert("pip".to_string());
|
||||||
|
allowed_commands.insert("pip3".to_string());
|
||||||
|
allowed_commands.insert("java".to_string());
|
||||||
|
allowed_commands.insert("javac".to_string());
|
||||||
|
allowed_commands.insert("maven".to_string());
|
||||||
|
allowed_commands.insert("gradle".to_string());
|
||||||
|
allowed_commands.insert("go".to_string());
|
||||||
|
allowed_commands.insert("dotnet".to_string());
|
||||||
|
|
||||||
|
// Compression tools
|
||||||
|
allowed_commands.insert("tar".to_string());
|
||||||
|
allowed_commands.insert("gzip".to_string());
|
||||||
|
allowed_commands.insert("gunzip".to_string());
|
||||||
|
allowed_commands.insert("zip".to_string());
|
||||||
|
allowed_commands.insert("unzip".to_string());
|
||||||
|
|
||||||
|
let mut blocked_commands = HashSet::new();
|
||||||
|
|
||||||
|
// Dangerous system commands
|
||||||
|
blocked_commands.insert("rm".to_string());
|
||||||
|
blocked_commands.insert("rmdir".to_string());
|
||||||
|
blocked_commands.insert("dd".to_string());
|
||||||
|
blocked_commands.insert("mkfs".to_string());
|
||||||
|
blocked_commands.insert("fdisk".to_string());
|
||||||
|
blocked_commands.insert("mount".to_string());
|
||||||
|
blocked_commands.insert("umount".to_string());
|
||||||
|
blocked_commands.insert("sudo".to_string());
|
||||||
|
blocked_commands.insert("su".to_string());
|
||||||
|
blocked_commands.insert("passwd".to_string());
|
||||||
|
blocked_commands.insert("chown".to_string());
|
||||||
|
blocked_commands.insert("chmod".to_string());
|
||||||
|
blocked_commands.insert("chgrp".to_string());
|
||||||
|
blocked_commands.insert("chroot".to_string());
|
||||||
|
|
||||||
|
// Network and system tools
|
||||||
|
blocked_commands.insert("nc".to_string());
|
||||||
|
blocked_commands.insert("netcat".to_string());
|
||||||
|
blocked_commands.insert("wget".to_string());
|
||||||
|
blocked_commands.insert("curl".to_string());
|
||||||
|
blocked_commands.insert("ssh".to_string());
|
||||||
|
blocked_commands.insert("scp".to_string());
|
||||||
|
blocked_commands.insert("rsync".to_string());
|
||||||
|
|
||||||
|
// Process control
|
||||||
|
blocked_commands.insert("kill".to_string());
|
||||||
|
blocked_commands.insert("killall".to_string());
|
||||||
|
blocked_commands.insert("pkill".to_string());
|
||||||
|
blocked_commands.insert("nohup".to_string());
|
||||||
|
blocked_commands.insert("screen".to_string());
|
||||||
|
blocked_commands.insert("tmux".to_string());
|
||||||
|
|
||||||
|
// System modification
|
||||||
|
blocked_commands.insert("systemctl".to_string());
|
||||||
|
blocked_commands.insert("service".to_string());
|
||||||
|
blocked_commands.insert("crontab".to_string());
|
||||||
|
blocked_commands.insert("at".to_string());
|
||||||
|
blocked_commands.insert("reboot".to_string());
|
||||||
|
blocked_commands.insert("shutdown".to_string());
|
||||||
|
blocked_commands.insert("halt".to_string());
|
||||||
|
blocked_commands.insert("poweroff".to_string());
|
||||||
|
|
||||||
|
Self {
|
||||||
|
max_execution_time: Duration::from_secs(300), // 5 minutes
|
||||||
|
max_memory_mb: 512,
|
||||||
|
max_cpu_percent: 80,
|
||||||
|
allowed_commands,
|
||||||
|
blocked_commands,
|
||||||
|
allowed_read_paths: HashSet::new(),
|
||||||
|
allowed_write_paths: HashSet::new(),
|
||||||
|
allow_network: false,
|
||||||
|
max_processes: 10,
|
||||||
|
strict_mode: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sandbox error types
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum SandboxError {
|
||||||
|
#[error("Command blocked by security policy: {command}")]
|
||||||
|
BlockedCommand { command: String },
|
||||||
|
|
||||||
|
#[error("Dangerous command pattern detected: {pattern}")]
|
||||||
|
DangerousPattern { pattern: String },
|
||||||
|
|
||||||
|
#[error("Path access denied: {path}")]
|
||||||
|
PathAccessDenied { path: String },
|
||||||
|
|
||||||
|
#[error("Resource limit exceeded: {resource}")]
|
||||||
|
ResourceLimitExceeded { resource: String },
|
||||||
|
|
||||||
|
#[error("Execution timeout after {seconds} seconds")]
|
||||||
|
ExecutionTimeout { seconds: u64 },
|
||||||
|
|
||||||
|
#[error("Sandbox setup failed: {reason}")]
|
||||||
|
SandboxSetupError { reason: String },
|
||||||
|
|
||||||
|
#[error("Command execution failed: {reason}")]
|
||||||
|
ExecutionError { reason: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Secure sandbox for executing commands in emulation mode
|
||||||
|
pub struct Sandbox {
|
||||||
|
config: SandboxConfig,
|
||||||
|
workspace: TempDir,
|
||||||
|
dangerous_patterns: Vec<Regex>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sandbox {
|
||||||
|
/// Create a new sandbox with the given configuration
|
||||||
|
pub fn new(config: SandboxConfig) -> Result<Self, SandboxError> {
|
||||||
|
let workspace = tempfile::tempdir().map_err(|e| SandboxError::SandboxSetupError {
|
||||||
|
reason: format!("Failed to create sandbox workspace: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let dangerous_patterns = Self::compile_dangerous_patterns();
|
||||||
|
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"Created new sandbox with workspace: {}",
|
||||||
|
workspace.path().display()
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
config,
|
||||||
|
workspace,
|
||||||
|
dangerous_patterns,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute a command in the sandbox
|
||||||
|
pub async fn execute_command(
|
||||||
|
&self,
|
||||||
|
command: &[&str],
|
||||||
|
env_vars: &[(&str, &str)],
|
||||||
|
working_dir: &Path,
|
||||||
|
) -> Result<crate::container::ContainerOutput, SandboxError> {
|
||||||
|
if command.is_empty() {
|
||||||
|
return Err(SandboxError::ExecutionError {
|
||||||
|
reason: "Empty command".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let command_str = command.join(" ");
|
||||||
|
|
||||||
|
// Step 1: Validate command
|
||||||
|
self.validate_command(&command_str)?;
|
||||||
|
|
||||||
|
// Step 2: Setup sandbox environment
|
||||||
|
let sandbox_dir = self.setup_sandbox_environment(working_dir)?;
|
||||||
|
|
||||||
|
// Step 3: Execute with limits
|
||||||
|
self.execute_with_limits(command, env_vars, &sandbox_dir)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate that a command is safe to execute
|
||||||
|
fn validate_command(&self, command_str: &str) -> Result<(), SandboxError> {
|
||||||
|
// Check for dangerous patterns first
|
||||||
|
for pattern in &self.dangerous_patterns {
|
||||||
|
if pattern.is_match(command_str) {
|
||||||
|
wrkflw_logging::warning(&format!(
|
||||||
|
"🚫 Blocked dangerous command pattern: {}",
|
||||||
|
command_str
|
||||||
|
));
|
||||||
|
return Err(SandboxError::DangerousPattern {
|
||||||
|
pattern: command_str.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split command by shell operators to validate each part
|
||||||
|
let command_parts = self.split_shell_command(command_str);
|
||||||
|
|
||||||
|
for part in command_parts {
|
||||||
|
let part = part.trim();
|
||||||
|
if part.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the base command from this part
|
||||||
|
let base_command = part.split_whitespace().next().unwrap_or("");
|
||||||
|
let command_name = Path::new(base_command)
|
||||||
|
.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or(base_command);
|
||||||
|
|
||||||
|
// Skip shell built-ins and operators
|
||||||
|
if self.is_shell_builtin(command_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check blocked commands
|
||||||
|
if self.config.blocked_commands.contains(command_name) {
|
||||||
|
wrkflw_logging::warning(&format!("🚫 Blocked command: {}", command_name));
|
||||||
|
return Err(SandboxError::BlockedCommand {
|
||||||
|
command: command_name.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// In strict mode, only allow whitelisted commands
|
||||||
|
if self.config.strict_mode && !self.config.allowed_commands.contains(command_name) {
|
||||||
|
wrkflw_logging::warning(&format!(
|
||||||
|
"🚫 Command not in whitelist (strict mode): {}",
|
||||||
|
command_name
|
||||||
|
));
|
||||||
|
return Err(SandboxError::BlockedCommand {
|
||||||
|
command: command_name.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wrkflw_logging::info(&format!("✅ Command validation passed: {}", command_str));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split shell command by operators while preserving quoted strings
|
||||||
|
fn split_shell_command(&self, command_str: &str) -> Vec<String> {
|
||||||
|
// Simple split by common shell operators
|
||||||
|
// This is not a full shell parser but handles most cases
|
||||||
|
let separators = ["&&", "||", ";", "|"];
|
||||||
|
let mut parts = vec![command_str.to_string()];
|
||||||
|
|
||||||
|
for separator in separators {
|
||||||
|
let mut new_parts = Vec::new();
|
||||||
|
for part in parts {
|
||||||
|
let split_parts: Vec<String> = part
|
||||||
|
.split(separator)
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
new_parts.extend(split_parts);
|
||||||
|
}
|
||||||
|
parts = new_parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
parts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a command is a shell built-in
|
||||||
|
fn is_shell_builtin(&self, command: &str) -> bool {
|
||||||
|
let builtins = [
|
||||||
|
"true", "false", "test", "[", "echo", "printf", "cd", "pwd", "export", "set", "unset",
|
||||||
|
"alias", "history", "jobs", "fg", "bg", "wait", "read",
|
||||||
|
];
|
||||||
|
builtins.contains(&command)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setup isolated sandbox environment
|
||||||
|
fn setup_sandbox_environment(&self, working_dir: &Path) -> Result<PathBuf, SandboxError> {
|
||||||
|
let sandbox_root = self.workspace.path();
|
||||||
|
let sandbox_workspace = sandbox_root.join("workspace");
|
||||||
|
|
||||||
|
// Create sandbox directory structure
|
||||||
|
fs::create_dir_all(&sandbox_workspace).map_err(|e| SandboxError::SandboxSetupError {
|
||||||
|
reason: format!("Failed to create sandbox workspace: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Copy allowed files to sandbox (if working_dir exists and is allowed)
|
||||||
|
if working_dir.exists() && self.is_path_allowed(working_dir, false) {
|
||||||
|
self.copy_safe_files(working_dir, &sandbox_workspace)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"Sandbox environment ready: {}",
|
||||||
|
sandbox_workspace.display()
|
||||||
|
));
|
||||||
|
|
||||||
|
Ok(sandbox_workspace)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copy files safely to sandbox, excluding dangerous files
|
||||||
|
fn copy_safe_files(&self, source: &Path, dest: &Path) -> Result<(), SandboxError> {
|
||||||
|
for entry in fs::read_dir(source).map_err(|e| SandboxError::SandboxSetupError {
|
||||||
|
reason: format!("Failed to read source directory: {}", e),
|
||||||
|
})? {
|
||||||
|
let entry = entry.map_err(|e| SandboxError::SandboxSetupError {
|
||||||
|
reason: format!("Failed to read directory entry: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let path = entry.path();
|
||||||
|
let file_name = path.file_name().and_then(|s| s.to_str()).unwrap_or("");
|
||||||
|
|
||||||
|
// Skip dangerous or sensitive files
|
||||||
|
if self.should_skip_file(file_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dest_path = dest.join(file_name);
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
fs::copy(&path, &dest_path).map_err(|e| SandboxError::SandboxSetupError {
|
||||||
|
reason: format!("Failed to copy file: {}", e),
|
||||||
|
})?;
|
||||||
|
} else if path.is_dir() && !self.should_skip_directory(file_name) {
|
||||||
|
fs::create_dir_all(&dest_path).map_err(|e| SandboxError::SandboxSetupError {
|
||||||
|
reason: format!("Failed to create directory: {}", e),
|
||||||
|
})?;
|
||||||
|
self.copy_safe_files(&path, &dest_path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute command with resource limits and monitoring
|
||||||
|
async fn execute_with_limits(
|
||||||
|
&self,
|
||||||
|
command: &[&str],
|
||||||
|
env_vars: &[(&str, &str)],
|
||||||
|
working_dir: &Path,
|
||||||
|
) -> Result<crate::container::ContainerOutput, SandboxError> {
|
||||||
|
// Join command parts and execute via shell for proper handling of operators
|
||||||
|
let command_str = command.join(" ");
|
||||||
|
|
||||||
|
let mut cmd = Command::new("sh");
|
||||||
|
cmd.arg("-c");
|
||||||
|
cmd.arg(&command_str);
|
||||||
|
cmd.current_dir(working_dir);
|
||||||
|
cmd.stdout(Stdio::piped());
|
||||||
|
cmd.stderr(Stdio::piped());
|
||||||
|
|
||||||
|
// Set environment variables (filtered)
|
||||||
|
for (key, value) in env_vars {
|
||||||
|
if self.is_env_var_safe(key) {
|
||||||
|
cmd.env(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add sandbox-specific environment variables
|
||||||
|
cmd.env("WRKFLW_SANDBOXED", "true");
|
||||||
|
cmd.env("WRKFLW_SANDBOX_MODE", "strict");
|
||||||
|
|
||||||
|
// Execute with timeout
|
||||||
|
let timeout_duration = self.config.max_execution_time;
|
||||||
|
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"🏃 Executing sandboxed command: {} (timeout: {}s)",
|
||||||
|
command.join(" "),
|
||||||
|
timeout_duration.as_secs()
|
||||||
|
));
|
||||||
|
|
||||||
|
let start_time = std::time::Instant::now();
|
||||||
|
|
||||||
|
let result = tokio::time::timeout(timeout_duration, async {
|
||||||
|
let output = cmd.output().map_err(|e| SandboxError::ExecutionError {
|
||||||
|
reason: format!("Command execution failed: {}", e),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(crate::container::ContainerOutput {
|
||||||
|
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
|
||||||
|
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
|
||||||
|
exit_code: output.status.code().unwrap_or(-1),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let execution_time = start_time.elapsed();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(output_result) => {
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"✅ Sandboxed command completed in {:.2}s",
|
||||||
|
execution_time.as_secs_f64()
|
||||||
|
));
|
||||||
|
output_result
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
wrkflw_logging::warning(&format!(
|
||||||
|
"⏰ Sandboxed command timed out after {:.2}s",
|
||||||
|
timeout_duration.as_secs_f64()
|
||||||
|
));
|
||||||
|
Err(SandboxError::ExecutionTimeout {
|
||||||
|
seconds: timeout_duration.as_secs(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a path is allowed for access
|
||||||
|
fn is_path_allowed(&self, path: &Path, write_access: bool) -> bool {
|
||||||
|
let abs_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||||
|
|
||||||
|
if write_access {
|
||||||
|
self.config
|
||||||
|
.allowed_write_paths
|
||||||
|
.iter()
|
||||||
|
.any(|allowed| abs_path.starts_with(allowed))
|
||||||
|
} else {
|
||||||
|
self.config
|
||||||
|
.allowed_read_paths
|
||||||
|
.iter()
|
||||||
|
.any(|allowed| abs_path.starts_with(allowed))
|
||||||
|
|| self
|
||||||
|
.config
|
||||||
|
.allowed_write_paths
|
||||||
|
.iter()
|
||||||
|
.any(|allowed| abs_path.starts_with(allowed))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an environment variable is safe to pass through
|
||||||
|
fn is_env_var_safe(&self, key: &str) -> bool {
|
||||||
|
// Block dangerous environment variables
|
||||||
|
let dangerous_env_vars = [
|
||||||
|
"LD_PRELOAD",
|
||||||
|
"LD_LIBRARY_PATH",
|
||||||
|
"DYLD_INSERT_LIBRARIES",
|
||||||
|
"DYLD_LIBRARY_PATH",
|
||||||
|
"PATH",
|
||||||
|
"HOME",
|
||||||
|
"SHELL",
|
||||||
|
];
|
||||||
|
|
||||||
|
!dangerous_env_vars.contains(&key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a file should be skipped during copying
|
||||||
|
fn should_skip_file(&self, filename: &str) -> bool {
|
||||||
|
let dangerous_files = [
|
||||||
|
".ssh",
|
||||||
|
".gnupg",
|
||||||
|
".aws",
|
||||||
|
".docker",
|
||||||
|
"id_rsa",
|
||||||
|
"id_ed25519",
|
||||||
|
"credentials",
|
||||||
|
"config",
|
||||||
|
".env",
|
||||||
|
".secrets",
|
||||||
|
];
|
||||||
|
|
||||||
|
dangerous_files
|
||||||
|
.iter()
|
||||||
|
.any(|pattern| filename.contains(pattern))
|
||||||
|
|| filename.starts_with('.') && filename != ".gitignore" && filename != ".github"
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a directory should be skipped
|
||||||
|
fn should_skip_directory(&self, dirname: &str) -> bool {
|
||||||
|
let skip_dirs = [
|
||||||
|
"target",
|
||||||
|
"node_modules",
|
||||||
|
".git",
|
||||||
|
".cargo",
|
||||||
|
".npm",
|
||||||
|
".cache",
|
||||||
|
"build",
|
||||||
|
"dist",
|
||||||
|
"tmp",
|
||||||
|
"temp",
|
||||||
|
];
|
||||||
|
|
||||||
|
skip_dirs.contains(&dirname)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compile regex patterns for dangerous command detection
|
||||||
|
fn compile_dangerous_patterns() -> Vec<Regex> {
|
||||||
|
let patterns = [
|
||||||
|
r"rm\s+.*-rf?\s*/", // rm -rf /
|
||||||
|
r"dd\s+.*of=/dev/", // dd ... of=/dev/...
|
||||||
|
r">\s*/dev/sd[a-z]", // > /dev/sda
|
||||||
|
r"mkfs\.", // mkfs.ext4, etc.
|
||||||
|
r"fdisk\s+/dev/", // fdisk /dev/...
|
||||||
|
r"mount\s+.*\s+/", // mount ... /
|
||||||
|
r"chroot\s+/", // chroot /
|
||||||
|
r"sudo\s+", // sudo commands
|
||||||
|
r"su\s+", // su commands
|
||||||
|
r"bash\s+-c\s+.*rm.*-rf", // bash -c "rm -rf ..."
|
||||||
|
r"sh\s+-c\s+.*rm.*-rf", // sh -c "rm -rf ..."
|
||||||
|
r"eval\s+.*rm.*-rf", // eval "rm -rf ..."
|
||||||
|
r":\(\)\{.*;\};:", // Fork bomb
|
||||||
|
r"/proc/sys/", // /proc/sys access
|
||||||
|
r"/etc/passwd", // /etc/passwd access
|
||||||
|
r"/etc/shadow", // /etc/shadow access
|
||||||
|
r"nc\s+.*-e", // netcat with exec
|
||||||
|
r"wget\s+.*\|\s*sh", // wget ... | sh
|
||||||
|
r"curl\s+.*\|\s*sh", // curl ... | sh
|
||||||
|
];
|
||||||
|
|
||||||
|
patterns
|
||||||
|
.iter()
|
||||||
|
.filter_map(|pattern| {
|
||||||
|
Regex::new(pattern)
|
||||||
|
.map_err(|e| {
|
||||||
|
wrkflw_logging::warning(&format!(
|
||||||
|
"Invalid regex pattern {}: {}",
|
||||||
|
pattern, e
|
||||||
|
));
|
||||||
|
e
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a default sandbox configuration for CI/CD workflows
|
||||||
|
pub fn create_workflow_sandbox_config() -> SandboxConfig {
|
||||||
|
let mut allowed_read_paths = HashSet::new();
|
||||||
|
allowed_read_paths.insert(PathBuf::from("."));
|
||||||
|
|
||||||
|
let mut allowed_write_paths = HashSet::new();
|
||||||
|
allowed_write_paths.insert(PathBuf::from("."));
|
||||||
|
|
||||||
|
SandboxConfig {
|
||||||
|
max_execution_time: Duration::from_secs(1800), // 30 minutes
|
||||||
|
max_memory_mb: 2048, // 2GB
|
||||||
|
max_processes: 50,
|
||||||
|
allow_network: true,
|
||||||
|
strict_mode: false,
|
||||||
|
allowed_read_paths,
|
||||||
|
allowed_write_paths,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a strict sandbox configuration for untrusted code
|
||||||
|
pub fn create_strict_sandbox_config() -> SandboxConfig {
|
||||||
|
let mut allowed_read_paths = HashSet::new();
|
||||||
|
allowed_read_paths.insert(PathBuf::from("."));
|
||||||
|
|
||||||
|
let mut allowed_write_paths = HashSet::new();
|
||||||
|
allowed_write_paths.insert(PathBuf::from("."));
|
||||||
|
|
||||||
|
// Very limited command set
|
||||||
|
let allowed_commands = ["echo", "cat", "ls", "pwd", "date"]
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
SandboxConfig {
|
||||||
|
max_execution_time: Duration::from_secs(60), // 1 minute
|
||||||
|
max_memory_mb: 128, // 128MB
|
||||||
|
max_processes: 5,
|
||||||
|
allow_network: false,
|
||||||
|
strict_mode: true,
|
||||||
|
allowed_read_paths,
|
||||||
|
allowed_write_paths,
|
||||||
|
allowed_commands,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dangerous_pattern_detection() {
|
||||||
|
let sandbox = Sandbox::new(SandboxConfig::default()).unwrap();
|
||||||
|
|
||||||
|
// Should block dangerous commands
|
||||||
|
assert!(sandbox.validate_command("rm -rf /").is_err());
|
||||||
|
assert!(sandbox
|
||||||
|
.validate_command("dd if=/dev/zero of=/dev/sda")
|
||||||
|
.is_err());
|
||||||
|
assert!(sandbox.validate_command("sudo rm -rf /home").is_err());
|
||||||
|
assert!(sandbox.validate_command("bash -c 'rm -rf /'").is_err());
|
||||||
|
|
||||||
|
// Should allow safe commands
|
||||||
|
assert!(sandbox.validate_command("echo hello").is_ok());
|
||||||
|
assert!(sandbox.validate_command("ls -la").is_ok());
|
||||||
|
assert!(sandbox.validate_command("cargo build").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_command_whitelist() {
|
||||||
|
let config = create_strict_sandbox_config();
|
||||||
|
let sandbox = Sandbox::new(config).unwrap();
|
||||||
|
|
||||||
|
// Should allow whitelisted commands
|
||||||
|
assert!(sandbox.validate_command("echo hello").is_ok());
|
||||||
|
assert!(sandbox.validate_command("ls").is_ok());
|
||||||
|
|
||||||
|
// Should block non-whitelisted commands
|
||||||
|
assert!(sandbox.validate_command("git clone").is_err());
|
||||||
|
assert!(sandbox.validate_command("cargo build").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_file_filtering() {
|
||||||
|
let sandbox = Sandbox::new(SandboxConfig::default()).unwrap();
|
||||||
|
|
||||||
|
// Should skip dangerous files
|
||||||
|
assert!(sandbox.should_skip_file("id_rsa"));
|
||||||
|
assert!(sandbox.should_skip_file(".ssh"));
|
||||||
|
assert!(sandbox.should_skip_file("credentials"));
|
||||||
|
|
||||||
|
// Should allow safe files
|
||||||
|
assert!(!sandbox.should_skip_file("Cargo.toml"));
|
||||||
|
assert!(!sandbox.should_skip_file("README.md"));
|
||||||
|
assert!(!sandbox.should_skip_file(".gitignore"));
|
||||||
|
}
|
||||||
|
}
|
||||||
339
crates/runtime/src/secure_emulation.rs
Normal file
339
crates/runtime/src/secure_emulation.rs
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
use crate::container::{ContainerError, ContainerOutput, ContainerRuntime};
|
||||||
|
use crate::sandbox::{create_workflow_sandbox_config, Sandbox, SandboxConfig, SandboxError};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::path::Path;
|
||||||
|
use wrkflw_logging;
|
||||||
|
|
||||||
|
/// Secure emulation runtime that uses sandboxing for safety
|
||||||
|
pub struct SecureEmulationRuntime {
|
||||||
|
sandbox: Sandbox,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SecureEmulationRuntime {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SecureEmulationRuntime {
|
||||||
|
/// Create a new secure emulation runtime with default workflow-friendly configuration
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let config = create_workflow_sandbox_config();
|
||||||
|
let sandbox = Sandbox::new(config).expect("Failed to create sandbox");
|
||||||
|
|
||||||
|
wrkflw_logging::info("🔒 Initialized secure emulation runtime with sandboxing");
|
||||||
|
|
||||||
|
Self { sandbox }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new secure emulation runtime with custom sandbox configuration
|
||||||
|
pub fn new_with_config(config: SandboxConfig) -> Result<Self, ContainerError> {
|
||||||
|
let sandbox = Sandbox::new(config).map_err(|e| {
|
||||||
|
ContainerError::ContainerStart(format!("Failed to create sandbox: {}", e))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
wrkflw_logging::info("🔒 Initialized secure emulation runtime with custom config");
|
||||||
|
|
||||||
|
Ok(Self { sandbox })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl ContainerRuntime for SecureEmulationRuntime {
|
||||||
|
async fn run_container(
|
||||||
|
&self,
|
||||||
|
image: &str,
|
||||||
|
command: &[&str],
|
||||||
|
env_vars: &[(&str, &str)],
|
||||||
|
working_dir: &Path,
|
||||||
|
_volumes: &[(&Path, &Path)],
|
||||||
|
) -> Result<ContainerOutput, ContainerError> {
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"🔒 Executing sandboxed command: {} (image: {})",
|
||||||
|
command.join(" "),
|
||||||
|
image
|
||||||
|
));
|
||||||
|
|
||||||
|
// Use sandbox to execute the command safely
|
||||||
|
let result = self
|
||||||
|
.sandbox
|
||||||
|
.execute_command(command, env_vars, working_dir)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(output) => {
|
||||||
|
wrkflw_logging::info("✅ Sandboxed command completed successfully");
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
Err(SandboxError::BlockedCommand { command }) => {
|
||||||
|
let error_msg = format!(
|
||||||
|
"🚫 SECURITY BLOCK: Command '{}' is not allowed in secure emulation mode. \
|
||||||
|
This command was blocked for security reasons. \
|
||||||
|
If you need to run this command, please use Docker or Podman mode instead.",
|
||||||
|
command
|
||||||
|
);
|
||||||
|
wrkflw_logging::warning(&error_msg);
|
||||||
|
Err(ContainerError::ContainerExecution(error_msg))
|
||||||
|
}
|
||||||
|
Err(SandboxError::DangerousPattern { pattern }) => {
|
||||||
|
let error_msg = format!(
|
||||||
|
"🚫 SECURITY BLOCK: Dangerous command pattern detected: '{}'. \
|
||||||
|
This command was blocked because it matches a known dangerous pattern. \
|
||||||
|
Please review your workflow for potentially harmful commands.",
|
||||||
|
pattern
|
||||||
|
);
|
||||||
|
wrkflw_logging::warning(&error_msg);
|
||||||
|
Err(ContainerError::ContainerExecution(error_msg))
|
||||||
|
}
|
||||||
|
Err(SandboxError::ExecutionTimeout { seconds }) => {
|
||||||
|
let error_msg = format!(
|
||||||
|
"⏰ Command execution timed out after {} seconds. \
|
||||||
|
Consider optimizing your command or increasing timeout limits.",
|
||||||
|
seconds
|
||||||
|
);
|
||||||
|
wrkflw_logging::warning(&error_msg);
|
||||||
|
Err(ContainerError::ContainerExecution(error_msg))
|
||||||
|
}
|
||||||
|
Err(SandboxError::PathAccessDenied { path }) => {
|
||||||
|
let error_msg = format!(
|
||||||
|
"🚫 Path access denied: '{}'. \
|
||||||
|
The sandbox restricts file system access for security.",
|
||||||
|
path
|
||||||
|
);
|
||||||
|
wrkflw_logging::warning(&error_msg);
|
||||||
|
Err(ContainerError::ContainerExecution(error_msg))
|
||||||
|
}
|
||||||
|
Err(SandboxError::ResourceLimitExceeded { resource }) => {
|
||||||
|
let error_msg = format!(
|
||||||
|
"📊 Resource limit exceeded: {}. \
|
||||||
|
Your command used too many system resources.",
|
||||||
|
resource
|
||||||
|
);
|
||||||
|
wrkflw_logging::warning(&error_msg);
|
||||||
|
Err(ContainerError::ContainerExecution(error_msg))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
let error_msg = format!("Sandbox execution failed: {}", e);
|
||||||
|
wrkflw_logging::error(&error_msg);
|
||||||
|
Err(ContainerError::ContainerExecution(error_msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn pull_image(&self, image: &str) -> Result<(), ContainerError> {
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"🔒 Secure emulation: Pretending to pull image {}",
|
||||||
|
image
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn build_image(&self, dockerfile: &Path, tag: &str) -> Result<(), ContainerError> {
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"🔒 Secure emulation: Pretending to build image {} from {}",
|
||||||
|
tag,
|
||||||
|
dockerfile.display()
|
||||||
|
));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare_language_environment(
|
||||||
|
&self,
|
||||||
|
language: &str,
|
||||||
|
version: Option<&str>,
|
||||||
|
_additional_packages: Option<Vec<String>>,
|
||||||
|
) -> Result<String, ContainerError> {
|
||||||
|
// For secure emulation runtime, we'll use a simplified approach
|
||||||
|
// that doesn't require building custom images
|
||||||
|
let base_image = match language {
|
||||||
|
"python" => version.map_or("python:3.11-slim".to_string(), |v| format!("python:{}", v)),
|
||||||
|
"node" => version.map_or("node:20-slim".to_string(), |v| format!("node:{}", v)),
|
||||||
|
"java" => version.map_or("eclipse-temurin:17-jdk".to_string(), |v| {
|
||||||
|
format!("eclipse-temurin:{}", v)
|
||||||
|
}),
|
||||||
|
"go" => version.map_or("golang:1.21-slim".to_string(), |v| format!("golang:{}", v)),
|
||||||
|
"dotnet" => version.map_or("mcr.microsoft.com/dotnet/sdk:7.0".to_string(), |v| {
|
||||||
|
format!("mcr.microsoft.com/dotnet/sdk:{}", v)
|
||||||
|
}),
|
||||||
|
"rust" => version.map_or("rust:latest".to_string(), |v| format!("rust:{}", v)),
|
||||||
|
_ => {
|
||||||
|
return Err(ContainerError::ContainerStart(format!(
|
||||||
|
"Unsupported language: {}",
|
||||||
|
language
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// For emulation, we'll just return the base image
|
||||||
|
// The actual package installation will be handled during container execution
|
||||||
|
Ok(base_image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle special actions in secure emulation mode
|
||||||
|
pub async fn handle_special_action_secure(action: &str) -> Result<(), ContainerError> {
|
||||||
|
// Extract owner, repo and version from the action
|
||||||
|
let action_parts: Vec<&str> = action.split('@').collect();
|
||||||
|
let action_name = action_parts[0];
|
||||||
|
let action_version = if action_parts.len() > 1 {
|
||||||
|
action_parts[1]
|
||||||
|
} else {
|
||||||
|
"latest"
|
||||||
|
};
|
||||||
|
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"🔒 Processing action in secure mode: {} @ {}",
|
||||||
|
action_name, action_version
|
||||||
|
));
|
||||||
|
|
||||||
|
// In secure mode, we're more restrictive about what actions we allow
|
||||||
|
match action_name {
|
||||||
|
// Core GitHub actions that are generally safe
|
||||||
|
name if name.starts_with("actions/checkout") => {
|
||||||
|
wrkflw_logging::info("✅ Checkout action - workspace files are prepared securely");
|
||||||
|
}
|
||||||
|
name if name.starts_with("actions/setup-node") => {
|
||||||
|
wrkflw_logging::info("🟡 Node.js setup - using system Node.js in secure mode");
|
||||||
|
check_command_available_secure("node", "Node.js", "https://nodejs.org/");
|
||||||
|
}
|
||||||
|
name if name.starts_with("actions/setup-python") => {
|
||||||
|
wrkflw_logging::info("🟡 Python setup - using system Python in secure mode");
|
||||||
|
check_command_available_secure("python", "Python", "https://www.python.org/downloads/");
|
||||||
|
}
|
||||||
|
name if name.starts_with("actions/setup-java") => {
|
||||||
|
wrkflw_logging::info("🟡 Java setup - using system Java in secure mode");
|
||||||
|
check_command_available_secure("java", "Java", "https://adoptium.net/");
|
||||||
|
}
|
||||||
|
name if name.starts_with("actions/cache") => {
|
||||||
|
wrkflw_logging::info("🟡 Cache action - caching disabled in secure emulation mode");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rust-specific actions
|
||||||
|
name if name.starts_with("actions-rs/cargo") => {
|
||||||
|
wrkflw_logging::info("🟡 Rust cargo action - using system Rust in secure mode");
|
||||||
|
check_command_available_secure("cargo", "Rust/Cargo", "https://rustup.rs/");
|
||||||
|
}
|
||||||
|
name if name.starts_with("actions-rs/toolchain") => {
|
||||||
|
wrkflw_logging::info("🟡 Rust toolchain action - using system Rust in secure mode");
|
||||||
|
check_command_available_secure("rustc", "Rust", "https://rustup.rs/");
|
||||||
|
}
|
||||||
|
name if name.starts_with("actions-rs/fmt") => {
|
||||||
|
wrkflw_logging::info("🟡 Rust formatter action - using system rustfmt in secure mode");
|
||||||
|
check_command_available_secure("rustfmt", "rustfmt", "rustup component add rustfmt");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Potentially dangerous actions that we warn about
|
||||||
|
name if name.contains("docker") || name.contains("container") => {
|
||||||
|
wrkflw_logging::warning(&format!(
|
||||||
|
"🚫 Docker/container action '{}' is not supported in secure emulation mode. \
|
||||||
|
Use Docker or Podman mode for container actions.",
|
||||||
|
action_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
name if name.contains("ssh") || name.contains("deploy") => {
|
||||||
|
wrkflw_logging::warning(&format!(
|
||||||
|
"🚫 SSH/deployment action '{}' is restricted in secure emulation mode. \
|
||||||
|
Use Docker or Podman mode for deployment actions.",
|
||||||
|
action_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown actions
|
||||||
|
_ => {
|
||||||
|
wrkflw_logging::warning(&format!(
|
||||||
|
"🟡 Unknown action '{}' in secure emulation mode. \
|
||||||
|
Some functionality may be limited or unavailable.",
|
||||||
|
action_name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a command is available, with security-focused messaging
|
||||||
|
fn check_command_available_secure(command: &str, name: &str, install_url: &str) {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let is_available = Command::new("which")
|
||||||
|
.arg(command)
|
||||||
|
.output()
|
||||||
|
.map(|output| output.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !is_available {
|
||||||
|
wrkflw_logging::warning(&format!(
|
||||||
|
"🔧 {} is required but not found on the system",
|
||||||
|
name
|
||||||
|
));
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"To use this action in secure mode, please install {}: {}",
|
||||||
|
name, install_url
|
||||||
|
));
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"Alternatively, use Docker or Podman mode for automatic {} installation",
|
||||||
|
name
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
// Try to get version information
|
||||||
|
if let Ok(output) = Command::new(command).arg("--version").output() {
|
||||||
|
if output.status.success() {
|
||||||
|
let version = String::from_utf8_lossy(&output.stdout);
|
||||||
|
wrkflw_logging::info(&format!(
|
||||||
|
"✅ Using system {} in secure mode: {}",
|
||||||
|
name,
|
||||||
|
version.trim()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::sandbox::create_strict_sandbox_config;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_secure_emulation_blocks_dangerous_commands() {
|
||||||
|
let config = create_strict_sandbox_config();
|
||||||
|
let runtime = SecureEmulationRuntime::new_with_config(config).unwrap();
|
||||||
|
|
||||||
|
// Should block dangerous commands
|
||||||
|
let result = runtime
|
||||||
|
.run_container(
|
||||||
|
"alpine:latest",
|
||||||
|
&["rm", "-rf", "/"],
|
||||||
|
&[],
|
||||||
|
&PathBuf::from("."),
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
let error_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(error_msg.contains("SECURITY BLOCK"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_secure_emulation_allows_safe_commands() {
|
||||||
|
let runtime = SecureEmulationRuntime::new();
|
||||||
|
|
||||||
|
// Should allow safe commands
|
||||||
|
let result = runtime
|
||||||
|
.run_container(
|
||||||
|
"alpine:latest",
|
||||||
|
&["echo", "hello world"],
|
||||||
|
&[],
|
||||||
|
&PathBuf::from("."),
|
||||||
|
&[],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let output = result.unwrap();
|
||||||
|
assert!(output.stdout.contains("hello world"));
|
||||||
|
assert_eq!(output.exit_code, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -175,6 +175,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
RuntimeType::Emulation => RuntimeType::Emulation,
|
RuntimeType::Emulation => RuntimeType::Emulation,
|
||||||
|
RuntimeType::SecureEmulation => RuntimeType::SecureEmulation,
|
||||||
};
|
};
|
||||||
|
|
||||||
App {
|
App {
|
||||||
@@ -227,7 +228,8 @@ impl App {
|
|||||||
pub fn toggle_emulation_mode(&mut self) {
|
pub fn toggle_emulation_mode(&mut self) {
|
||||||
self.runtime_type = match self.runtime_type {
|
self.runtime_type = match self.runtime_type {
|
||||||
RuntimeType::Docker => RuntimeType::Podman,
|
RuntimeType::Docker => RuntimeType::Podman,
|
||||||
RuntimeType::Podman => RuntimeType::Emulation,
|
RuntimeType::Podman => RuntimeType::SecureEmulation,
|
||||||
|
RuntimeType::SecureEmulation => RuntimeType::Emulation,
|
||||||
RuntimeType::Emulation => RuntimeType::Docker,
|
RuntimeType::Emulation => RuntimeType::Docker,
|
||||||
};
|
};
|
||||||
self.logs
|
self.logs
|
||||||
@@ -251,7 +253,8 @@ impl App {
|
|||||||
match self.runtime_type {
|
match self.runtime_type {
|
||||||
RuntimeType::Docker => "Docker",
|
RuntimeType::Docker => "Docker",
|
||||||
RuntimeType::Podman => "Podman",
|
RuntimeType::Podman => "Podman",
|
||||||
RuntimeType::Emulation => "Emulation",
|
RuntimeType::SecureEmulation => "Secure Emulation",
|
||||||
|
RuntimeType::Emulation => "Emulation (Unsafe)",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ pub async fn execute_workflow_cli(
|
|||||||
RuntimeType::Podman
|
RuntimeType::Podman
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
RuntimeType::SecureEmulation => RuntimeType::SecureEmulation,
|
||||||
RuntimeType::Emulation => RuntimeType::Emulation,
|
RuntimeType::Emulation => RuntimeType::Emulation,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -454,6 +455,7 @@ pub fn start_next_workflow_execution(
|
|||||||
RuntimeType::Podman
|
RuntimeType::Podman
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
RuntimeType::SecureEmulation => RuntimeType::SecureEmulation,
|
||||||
RuntimeType::Emulation => RuntimeType::Emulation,
|
RuntimeType::Emulation => RuntimeType::Emulation,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ pub fn render_status_bar(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App,
|
|||||||
.bg(match app.runtime_type {
|
.bg(match app.runtime_type {
|
||||||
RuntimeType::Docker => Color::Blue,
|
RuntimeType::Docker => Color::Blue,
|
||||||
RuntimeType::Podman => Color::Cyan,
|
RuntimeType::Podman => Color::Cyan,
|
||||||
RuntimeType::Emulation => Color::Magenta,
|
RuntimeType::SecureEmulation => Color::Green,
|
||||||
|
RuntimeType::Emulation => Color::Red,
|
||||||
})
|
})
|
||||||
.fg(Color::White),
|
.fg(Color::White),
|
||||||
));
|
));
|
||||||
@@ -108,6 +109,12 @@ pub fn render_status_bar(f: &mut Frame<CrosstermBackend<io::Stdout>>, app: &App,
|
|||||||
.fg(Color::White),
|
.fg(Color::White),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
RuntimeType::SecureEmulation => {
|
||||||
|
status_items.push(Span::styled(
|
||||||
|
" 🔒SECURE ",
|
||||||
|
Style::default().bg(Color::Green).fg(Color::White),
|
||||||
|
));
|
||||||
|
}
|
||||||
RuntimeType::Emulation => {
|
RuntimeType::Emulation => {
|
||||||
// No need to check anything for emulation mode
|
// No need to check anything for emulation mode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ enum RuntimeChoice {
|
|||||||
Docker,
|
Docker,
|
||||||
/// Use Podman containers for isolation
|
/// Use Podman containers for isolation
|
||||||
Podman,
|
Podman,
|
||||||
/// Use process emulation mode (no containers)
|
/// Use process emulation mode (no containers, UNSAFE)
|
||||||
Emulation,
|
Emulation,
|
||||||
|
/// Use secure emulation mode with sandboxing (recommended for untrusted code)
|
||||||
|
SecureEmulation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<RuntimeChoice> for wrkflw_executor::RuntimeType {
|
impl From<RuntimeChoice> for wrkflw_executor::RuntimeType {
|
||||||
@@ -20,6 +22,7 @@ impl From<RuntimeChoice> for wrkflw_executor::RuntimeType {
|
|||||||
RuntimeChoice::Docker => wrkflw_executor::RuntimeType::Docker,
|
RuntimeChoice::Docker => wrkflw_executor::RuntimeType::Docker,
|
||||||
RuntimeChoice::Podman => wrkflw_executor::RuntimeType::Podman,
|
RuntimeChoice::Podman => wrkflw_executor::RuntimeType::Podman,
|
||||||
RuntimeChoice::Emulation => wrkflw_executor::RuntimeType::Emulation,
|
RuntimeChoice::Emulation => wrkflw_executor::RuntimeType::Emulation,
|
||||||
|
RuntimeChoice::SecureEmulation => wrkflw_executor::RuntimeType::SecureEmulation,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +73,7 @@ enum Commands {
|
|||||||
/// Path to workflow/pipeline file to execute
|
/// Path to workflow/pipeline file to execute
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
|
|
||||||
/// Container runtime to use (docker, podman, emulation)
|
/// Container runtime to use (docker, podman, emulation, secure-emulation)
|
||||||
#[arg(short, long, value_enum, default_value = "docker")]
|
#[arg(short, long, value_enum, default_value = "docker")]
|
||||||
runtime: RuntimeChoice,
|
runtime: RuntimeChoice,
|
||||||
|
|
||||||
@@ -92,7 +95,7 @@ enum Commands {
|
|||||||
/// Path to workflow file or directory (defaults to .github/workflows)
|
/// Path to workflow file or directory (defaults to .github/workflows)
|
||||||
path: Option<PathBuf>,
|
path: Option<PathBuf>,
|
||||||
|
|
||||||
/// Container runtime to use (docker, podman, emulation)
|
/// Container runtime to use (docker, podman, emulation, secure-emulation)
|
||||||
#[arg(short, long, value_enum, default_value = "docker")]
|
#[arg(short, long, value_enum, default_value = "docker")]
|
||||||
runtime: RuntimeChoice,
|
runtime: RuntimeChoice,
|
||||||
|
|
||||||
|
|||||||
35
tests/safe_workflow.yml
Normal file
35
tests/safe_workflow.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Safe Workflow Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
safe_operations:
|
||||||
|
name: Safe Operations
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: List files
|
||||||
|
run: ls -la
|
||||||
|
|
||||||
|
- name: Show current directory
|
||||||
|
run: pwd
|
||||||
|
|
||||||
|
- name: Echo message
|
||||||
|
run: echo "Hello, this is a safe command!"
|
||||||
|
|
||||||
|
- name: Create and read file
|
||||||
|
run: |
|
||||||
|
echo "test content" > safe-file.txt
|
||||||
|
cat safe-file.txt
|
||||||
|
rm safe-file.txt
|
||||||
|
|
||||||
|
- name: Show environment (safe)
|
||||||
|
run: echo "GITHUB_WORKSPACE=$GITHUB_WORKSPACE"
|
||||||
|
|
||||||
|
- name: Check if Rust is available
|
||||||
|
run: which rustc && rustc --version || echo "Rust not found"
|
||||||
|
continue-on-error: true
|
||||||
29
tests/security_comparison.yml
Normal file
29
tests/security_comparison.yml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Security Comparison Demo
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
safe_operations:
|
||||||
|
name: Safe Operations (Works in Both Modes)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: List files
|
||||||
|
run: ls -la
|
||||||
|
|
||||||
|
- name: Create and test file
|
||||||
|
run: |
|
||||||
|
echo "Hello World" > test.txt
|
||||||
|
cat test.txt
|
||||||
|
rm test.txt
|
||||||
|
echo "File operations completed safely"
|
||||||
|
|
||||||
|
- name: Environment check
|
||||||
|
run: |
|
||||||
|
echo "Current directory: $(pwd)"
|
||||||
|
echo "User: $(whoami)"
|
||||||
|
echo "Available commands: ls, echo, cat work fine"
|
||||||
92
tests/security_demo.yml
Normal file
92
tests/security_demo.yml
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
name: Security Demo Workflow
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
safe_commands:
|
||||||
|
name: Safe Commands (Will Pass)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: List project files
|
||||||
|
run: ls -la
|
||||||
|
|
||||||
|
- name: Show current directory
|
||||||
|
run: pwd
|
||||||
|
|
||||||
|
- name: Echo a message
|
||||||
|
run: echo "This command is safe and will execute successfully"
|
||||||
|
|
||||||
|
- name: Check Rust version (if available)
|
||||||
|
run: rustc --version || echo "Rust not installed"
|
||||||
|
|
||||||
|
- name: Build documentation
|
||||||
|
run: echo "Building docs..." && mkdir -p target/doc
|
||||||
|
|
||||||
|
- name: Show environment
|
||||||
|
run: env | grep GITHUB
|
||||||
|
|
||||||
|
dangerous_commands:
|
||||||
|
name: Dangerous Commands (Will Be Blocked)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# These commands will be blocked in secure emulation mode
|
||||||
|
- name: Dangerous file deletion
|
||||||
|
run: rm -rf /tmp/* # This will be BLOCKED
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: System modification attempt
|
||||||
|
run: sudo apt-get update # This will be BLOCKED
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Network download attempt
|
||||||
|
run: wget https://example.com/script.sh # This will be BLOCKED
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Process manipulation
|
||||||
|
run: kill -9 $$ # This will be BLOCKED
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
resource_intensive:
|
||||||
|
name: Resource Limits Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: CPU intensive task
|
||||||
|
run: |
|
||||||
|
echo "Testing resource limits..."
|
||||||
|
# This might hit CPU or time limits
|
||||||
|
for i in {1..1000}; do
|
||||||
|
echo "Iteration $i"
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
filesystem_test:
|
||||||
|
name: Filesystem Access Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create files in allowed location
|
||||||
|
run: |
|
||||||
|
mkdir -p ./test-output
|
||||||
|
echo "test content" > ./test-output/safe-file.txt
|
||||||
|
cat ./test-output/safe-file.txt
|
||||||
|
|
||||||
|
- name: Attempt to access system files
|
||||||
|
run: cat /etc/passwd # This may be blocked
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Show allowed file operations
|
||||||
|
run: |
|
||||||
|
echo "Safe file operations:"
|
||||||
|
touch ./temp-file.txt
|
||||||
|
echo "content" > ./temp-file.txt
|
||||||
|
cat ./temp-file.txt
|
||||||
|
rm ./temp-file.txt
|
||||||
|
echo "File operations completed safely"
|
||||||
Reference in New Issue
Block a user