1use crate::container::{
2 rebase_working_dir_or_error, ContainerError, ContainerOutput, ContainerRuntime,
3};
4use crate::sandbox::{create_workflow_sandbox_config, Sandbox, SandboxConfig, SandboxError};
5use async_trait::async_trait;
6use std::path::Path;
7use wrkflw_logging;
8
9pub struct SecureEmulationRuntime {
11 sandbox: Sandbox,
12}
13
14impl Default for SecureEmulationRuntime {
15 fn default() -> Self {
16 Self::new()
17 }
18}
19
20impl SecureEmulationRuntime {
21 pub fn new() -> Self {
23 let config = create_workflow_sandbox_config();
24 let sandbox = Sandbox::new(config).expect("Failed to create sandbox");
25
26 wrkflw_logging::info(&format!(
27 "{} Initialized secure emulation runtime with sandboxing",
28 wrkflw_logging::symbols::LOCK
29 ));
30
31 Self { sandbox }
32 }
33
34 pub fn new_with_config(config: SandboxConfig) -> Result<Self, ContainerError> {
36 let sandbox = Sandbox::new(config).map_err(|e| {
37 ContainerError::ContainerStart(format!("Failed to create sandbox: {}", e))
38 })?;
39
40 wrkflw_logging::info(&format!(
41 "{} Initialized secure emulation runtime with custom config",
42 wrkflw_logging::symbols::LOCK
43 ));
44
45 Ok(Self { sandbox })
46 }
47}
48
49#[async_trait]
50impl ContainerRuntime for SecureEmulationRuntime {
51 async fn run_container(
52 &self,
53 image: &str,
54 command: &[&str],
55 env_vars: &[(&str, &str)],
56 working_dir: &Path,
57 volumes: &[(&Path, &Path)],
58 entrypoint: Option<&str>,
59 ) -> Result<ContainerOutput, ContainerError> {
60 if let Some(ep) = entrypoint {
61 wrkflw_logging::warning(&format!(
62 "Secure emulation mode ignoring entrypoint override '{}' for image '{}'. \
63 Use --runtime docker for full Docker action support.",
64 ep, image
65 ));
66 }
67
68 wrkflw_logging::info(&format!(
69 "{} Executing sandboxed command: {} (image: {})",
70 wrkflw_logging::symbols::LOCK,
71 command.join(" "),
72 image
73 ));
74
75 let host_working_dir =
80 rebase_working_dir_or_error(working_dir, volumes, "secure_emulation")?;
81
82 let result = self
84 .sandbox
85 .execute_command(command, env_vars, &host_working_dir)
86 .await;
87
88 match result {
89 Ok(output) => {
90 wrkflw_logging::info(&format!(
91 "{} Sandboxed command completed successfully",
92 wrkflw_logging::symbols::SUCCESS
93 ));
94 Ok(output)
95 }
96 Err(SandboxError::BlockedCommand { command }) => {
97 let error_msg = format!(
98 "{} SECURITY BLOCK: Command '{}' is not allowed in secure emulation mode. \
99 This command was blocked for security reasons. \
100 If you need to run this command, please use Docker or Podman mode instead.",
101 wrkflw_logging::symbols::BLOCKED,
102 command
103 );
104 wrkflw_logging::warning(&error_msg);
105 Err(ContainerError::ContainerExecution(error_msg))
106 }
107 Err(SandboxError::DangerousPattern { pattern }) => {
108 let error_msg = format!(
109 "{} SECURITY BLOCK: Dangerous command pattern detected: '{}'. \
110 This command was blocked because it matches a known dangerous pattern. \
111 Please review your workflow for potentially harmful commands.",
112 wrkflw_logging::symbols::BLOCKED,
113 pattern
114 );
115 wrkflw_logging::warning(&error_msg);
116 Err(ContainerError::ContainerExecution(error_msg))
117 }
118 Err(SandboxError::ExecutionTimeout { seconds }) => {
119 let error_msg = format!(
120 "{} Command execution timed out after {} seconds. \
121 Consider optimizing your command or increasing timeout limits.",
122 wrkflw_logging::symbols::WARNING,
123 seconds
124 );
125 wrkflw_logging::warning(&error_msg);
126 Err(ContainerError::ContainerExecution(error_msg))
127 }
128 Err(SandboxError::PathAccessDenied { path }) => {
129 let error_msg = format!(
130 "{} Path access denied: '{}'. \
131 The sandbox restricts file system access for security.",
132 wrkflw_logging::symbols::BLOCKED,
133 path
134 );
135 wrkflw_logging::warning(&error_msg);
136 Err(ContainerError::ContainerExecution(error_msg))
137 }
138 Err(SandboxError::ResourceLimitExceeded { resource }) => {
139 let error_msg = format!(
140 "{} Resource limit exceeded: {}. \
141 Your command used too many system resources.",
142 wrkflw_logging::symbols::WARNING,
143 resource
144 );
145 wrkflw_logging::warning(&error_msg);
146 Err(ContainerError::ContainerExecution(error_msg))
147 }
148 Err(e) => {
149 let error_msg = format!("Sandbox execution failed: {}", e);
150 wrkflw_logging::error(&error_msg);
151 Err(ContainerError::ContainerExecution(error_msg))
152 }
153 }
154 }
155
156 async fn pull_image(&self, image: &str) -> Result<(), ContainerError> {
157 wrkflw_logging::info(&format!(
158 "{} Secure emulation: Pretending to pull image {}",
159 wrkflw_logging::symbols::LOCK,
160 image
161 ));
162 Ok(())
163 }
164
165 async fn build_image(
166 &self,
167 dockerfile: &Path,
168 tag: &str,
169 _context_dir: &Path,
170 ) -> Result<(), ContainerError> {
171 wrkflw_logging::info(&format!(
172 "{} Secure emulation: Pretending to build image {} from {}",
173 wrkflw_logging::symbols::LOCK,
174 tag,
175 dockerfile.display()
176 ));
177 Ok(())
178 }
179
180 async fn image_exists(&self, _tag: &str) -> Result<bool, ContainerError> {
181 Ok(false)
182 }
183
184 async fn prepare_language_environment(
185 &self,
186 language: &str,
187 version: Option<&str>,
188 _additional_packages: Option<Vec<String>>,
189 ) -> Result<String, ContainerError> {
190 let base_image = match language {
193 "python" => version.map_or("python:3.11-slim".to_string(), |v| format!("python:{}", v)),
194 "node" => version.map_or("node:20-slim".to_string(), |v| format!("node:{}", v)),
195 "java" => version.map_or("eclipse-temurin:17-jdk".to_string(), |v| {
196 format!("eclipse-temurin:{}", v)
197 }),
198 "go" => version.map_or("golang:1.21-slim".to_string(), |v| format!("golang:{}", v)),
199 "dotnet" => version.map_or("mcr.microsoft.com/dotnet/sdk:7.0".to_string(), |v| {
200 format!("mcr.microsoft.com/dotnet/sdk:{}", v)
201 }),
202 "rust" => version.map_or("rust:latest".to_string(), |v| format!("rust:{}", v)),
203 _ => {
204 return Err(ContainerError::ContainerStart(format!(
205 "Unsupported language: {}",
206 language
207 )))
208 }
209 };
210
211 Ok(base_image)
214 }
215}
216
217pub async fn handle_special_action_secure(action: &str) -> Result<(), ContainerError> {
219 let action_parts: Vec<&str> = action.split('@').collect();
221 let action_name = action_parts[0];
222 let action_version = if action_parts.len() > 1 {
223 action_parts[1]
224 } else {
225 "latest"
226 };
227
228 wrkflw_logging::info(&format!(
229 "{} Processing action in secure mode: {} @ {}",
230 wrkflw_logging::symbols::LOCK,
231 action_name,
232 action_version
233 ));
234
235 match action_name {
237 name if name.starts_with("actions/checkout") => {
239 wrkflw_logging::info(&format!(
240 "{} Checkout action - workspace files are prepared securely",
241 wrkflw_logging::symbols::SUCCESS
242 ));
243 }
244 name if name.starts_with("actions/setup-node") => {
245 wrkflw_logging::info(&format!(
246 "{} Node.js setup - using system Node.js in secure mode",
247 wrkflw_logging::symbols::WARNING
248 ));
249 check_command_available_secure("node", "Node.js", "https://nodejs.org/");
250 }
251 name if name.starts_with("actions/setup-python") => {
252 wrkflw_logging::info(&format!(
253 "{} Python setup - using system Python in secure mode",
254 wrkflw_logging::symbols::WARNING
255 ));
256 check_command_available_secure("python", "Python", "https://www.python.org/downloads/");
257 }
258 name if name.starts_with("actions/setup-java") => {
259 wrkflw_logging::info(&format!(
260 "{} Java setup - using system Java in secure mode",
261 wrkflw_logging::symbols::WARNING
262 ));
263 check_command_available_secure("java", "Java", "https://adoptium.net/");
264 }
265 name if name.starts_with("actions/cache") => {
266 wrkflw_logging::info(&format!(
267 "{} Cache action - caching disabled in secure emulation mode",
268 wrkflw_logging::symbols::WARNING
269 ));
270 }
271
272 name if name.starts_with("actions-rs/cargo") => {
274 wrkflw_logging::info(&format!(
275 "{} Rust cargo action - using system Rust in secure mode",
276 wrkflw_logging::symbols::WARNING
277 ));
278 check_command_available_secure("cargo", "Rust/Cargo", "https://rustup.rs/");
279 }
280 name if name.starts_with("actions-rs/toolchain") => {
281 wrkflw_logging::info(&format!(
282 "{} Rust toolchain action - using system Rust in secure mode",
283 wrkflw_logging::symbols::WARNING
284 ));
285 check_command_available_secure("rustc", "Rust", "https://rustup.rs/");
286 }
287 name if name.starts_with("actions-rs/fmt") => {
288 wrkflw_logging::info(&format!(
289 "{} Rust formatter action - using system rustfmt in secure mode",
290 wrkflw_logging::symbols::WARNING
291 ));
292 check_command_available_secure("rustfmt", "rustfmt", "rustup component add rustfmt");
293 }
294
295 name if name.contains("docker") || name.contains("container") => {
297 wrkflw_logging::warning(&format!(
298 "{} Docker/container action '{}' is not supported in secure emulation mode. \
299 Use Docker or Podman mode for container actions.",
300 wrkflw_logging::symbols::BLOCKED,
301 action_name
302 ));
303 }
304 name if name.contains("ssh") || name.contains("deploy") => {
305 wrkflw_logging::warning(&format!(
306 "{} SSH/deployment action '{}' is restricted in secure emulation mode. \
307 Use Docker or Podman mode for deployment actions.",
308 wrkflw_logging::symbols::BLOCKED,
309 action_name
310 ));
311 }
312
313 _ => {
315 wrkflw_logging::warning(&format!(
316 "{} Unknown action '{}' in secure emulation mode. \
317 Some functionality may be limited or unavailable.",
318 wrkflw_logging::symbols::WARNING,
319 action_name
320 ));
321 }
322 }
323
324 Ok(())
325}
326
327fn check_command_available_secure(command: &str, name: &str, install_url: &str) {
329 use std::process::Command;
330
331 let is_available = Command::new("which")
332 .arg(command)
333 .output()
334 .map(|output| output.status.success())
335 .unwrap_or(false);
336
337 if !is_available {
338 wrkflw_logging::warning(&format!(
339 "🔧 {} is required but not found on the system",
340 name
341 ));
342 wrkflw_logging::info(&format!(
343 "To use this action in secure mode, please install {}: {}",
344 name, install_url
345 ));
346 wrkflw_logging::info(&format!(
347 "Alternatively, use Docker or Podman mode for automatic {} installation",
348 name
349 ));
350 } else {
351 if let Ok(output) = Command::new(command).arg("--version").output() {
353 if output.status.success() {
354 let version = String::from_utf8_lossy(&output.stdout);
355 wrkflw_logging::info(&format!(
356 "{} Using system {} in secure mode: {}",
357 wrkflw_logging::symbols::SUCCESS,
358 name,
359 version.trim()
360 ));
361 }
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369 use crate::sandbox::create_strict_sandbox_config;
370 use std::path::PathBuf;
371
372 #[tokio::test]
373 async fn test_secure_emulation_blocks_dangerous_commands() {
374 let config = create_strict_sandbox_config();
375 let runtime = SecureEmulationRuntime::new_with_config(config).unwrap();
376
377 let result = runtime
379 .run_container(
380 "alpine:latest",
381 &["rm", "-rf", "/"],
382 &[],
383 &PathBuf::from("."),
384 &[],
385 None,
386 )
387 .await;
388
389 assert!(result.is_err());
390 let error_msg = result.unwrap_err().to_string();
391 assert!(error_msg.contains("SECURITY BLOCK"));
392 }
393
394 #[tokio::test]
395 async fn test_secure_emulation_allows_safe_commands() {
396 let runtime = SecureEmulationRuntime::new();
397
398 let result = runtime
400 .run_container(
401 "alpine:latest",
402 &["echo", "hello world"],
403 &[],
404 &PathBuf::from("."),
405 &[],
406 None,
407 )
408 .await;
409
410 assert!(result.is_ok());
411 let output = result.unwrap();
412 assert!(output.stdout.contains("hello world"));
413 assert_eq!(output.exit_code, 0);
414 }
415
416 #[cfg(not(target_os = "windows"))]
420 #[tokio::test]
421 async fn secure_emulation_rebases_container_working_dir_via_volumes() {
422 let runtime = SecureEmulationRuntime::new();
423 let host_tempdir = tempfile::tempdir().unwrap();
424
425 let host = host_tempdir.path();
426 let container = Path::new("/github/workspace");
427
428 let result = runtime
431 .run_container(
432 "alpine:latest",
433 &["pwd"],
434 &[],
435 container,
436 &[(host, container)],
437 None,
438 )
439 .await
440 .expect("secure_emulation run failed");
441 assert_eq!(result.exit_code, 0, "stderr: {}", result.stderr);
442 let canon_host = host.canonicalize().unwrap();
445 let canon_pwd = PathBuf::from(result.stdout.trim()).canonicalize().unwrap();
446 assert_eq!(canon_pwd, canon_host);
447 }
448
449 #[tokio::test]
450 async fn secure_emulation_errors_when_volumes_dont_cover_working_dir() {
451 let runtime = SecureEmulationRuntime::new();
452
453 let result = runtime
455 .run_container(
456 "alpine:latest",
457 &["echo", "nope"],
458 &[],
459 Path::new("/github/workspace"),
460 &[],
461 None,
462 )
463 .await;
464
465 let err = result.expect_err("should error when no volume covers working_dir");
466 let msg = err.to_string();
467 assert!(
468 msg.contains("not covered by any volume mount"),
469 "unexpected error: {}",
470 msg
471 );
472 }
473}