diff options
Diffstat (limited to 'tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs')
| -rw-r--r-- | tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs b/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs new file mode 100644 index 0000000..7dd3629 --- /dev/null +++ b/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs @@ -0,0 +1,89 @@ +using System.Runtime.CompilerServices; +using System.Security.AccessControl; +using System.Diagnostics; +using Agent.Windows.Network; +using Agent.Windows.Recovery; +using Agent.Windows.Security; + +namespace Agent.Windows.Tests; + +internal static class CrashScenarioBootstrap +{ + [ModuleInitializer] + internal static void RunIfRequested() + { + string? scenario = Environment.GetEnvironmentVariable("AGENT_MCP_CRASH_SCENARIO"); + if (string.IsNullOrEmpty(scenario)) return; + string root = Environment.GetEnvironmentVariable("AGENT_MCP_CRASH_ROOT") + ?? throw new InvalidOperationException("Crash root is required."); + Guid taskId = Guid.Parse(Environment.GetEnvironmentVariable("AGENT_MCP_CRASH_TASK")!); + try + { + TaskResourceJournal journal = TaskResourceJournal.Create(Path.Combine(root, "journals"), taskId); + switch (scenario) + { + case "acl": + var acl = new TaskAclManager(AppContainerProfile.EnsureFixed().Sid, new MemorySecurityAuditSink(), journal); + _ = acl.Grant(taskId, Path.Combine(root, "resource"), FileCapability.Read | FileCapability.Write); + break; + case "acl-before": + byte[] descriptor = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(Path.Combine(root, "resource")), AccessControlSections.Access) + .GetSecurityDescriptorBinaryForm(); + _ = journal.Prepare("acl", Path.Combine(root, "resource"), + new Dictionary<string, string> { ["security_descriptor"] = Convert.ToBase64String(descriptor) }); + break; + case "file-before": + string temporary = Path.Combine(root, "resource.txt.agent-mcp-prepared.tmp"); + string backup = Path.Combine(root, "backups", taskId.ToString("N"), "prepared.backup"); + Directory.CreateDirectory(Path.GetDirectoryName(backup)!); + File.WriteAllText(temporary, "temporary"); + File.WriteAllText(backup, "before-crash"); + _ = journal.Prepare("file.replace", Path.Combine(root, "resource.txt"), new Dictionary<string, string> + { + ["existed"] = bool.TrueString, ["original_sha256"] = string.Empty, ["original_base64"] = string.Empty, + ["backup_path"] = backup, ["temporary_path"] = temporary + }); + break; + case "file-replace": + _ = new TransactionalFileEditor(journal, Path.Combine(root, "backups"), 1) + .Replace(Path.Combine(root, "resource.txt"), "after-crash"); + break; + case "process-after": + string powershell = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell", "v1.0", "powershell.exe"); + string auditPath = Path.Combine(root, "crash-audit.jsonl"); + var processAudit = new JsonLinesSecurityAuditSink(auditPath); + var launcher = new RestrictedProcessLauncher( + new ExecutablePolicy([new ExecutableApproval(powershell, ExecutablePolicy.ComputeSha256(powershell))], true), processAudit); + string escapedAudit = auditPath.Replace("'", "''", StringComparison.Ordinal); + string crashError = Path.Combine(root, "crash-error.txt").Replace("'", "''", StringComparison.Ordinal); + string watchdogScript = $"$d=[DateTime]::UtcNow.AddSeconds(10); while([DateTime]::UtcNow -lt $d){{ if((Test-Path -LiteralPath '{escapedAudit}') -and (Select-String -LiteralPath '{escapedAudit}' -SimpleMatch '\"Event\":\"process.start\"' -Quiet)){{ Stop-Process -Id {Environment.ProcessId} -Force; exit 0 }}; Start-Sleep -Milliseconds 25 }}; Set-Content -LiteralPath '{crashError}' -Value 'Sandbox process did not start before crash'; Stop-Process -Id {Environment.ProcessId} -Force"; + _ = Process.Start(new ProcessStartInfo + { + FileName = powershell, UseShellExecute = false, CreateNoWindow = true, + ArgumentList = { "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", watchdogScript } + }); + _ = launcher.Run(taskId, powershell, + ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "Start-Sleep -Seconds 30"], + Environment.SystemDirectory, TimeSpan.FromMinutes(1)); + break; + case "cleanup-mid": + var cleanupAcl = new TaskAclManager(AppContainerProfile.EnsureFixed().Sid, new MemorySecurityAuditSink(), journal); + _ = cleanupAcl.Grant(taskId, Path.Combine(root, "resource"), FileCapability.Read | FileCapability.Write); + FileEditTransaction edit = new TransactionalFileEditor(journal, Path.Combine(root, "backups"), 1) + .Replace(Path.Combine(root, "resource.txt"), "during-cleanup"); + edit.Rollback(); + break; + case "wfp": + _ = TaskNetworkPolicy.Apply(journal, AppContainerProfile.EnsureFixed().Sid, [], new MemorySecurityAuditSink()); + break; + default: + throw new InvalidOperationException($"Unknown crash scenario '{scenario}'."); + } + } + catch (Exception ex) + { + File.WriteAllText(Path.Combine(root, "crash-error.txt"), ex.ToString()); + } + Environment.FailFast($"Intentional security recovery crash point: {scenario}"); + } +} |
