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-inheritance": var inheritedAcl = new TaskAclManager( new System.Security.Principal.SecurityIdentifier("S-1-15-2-330000001-330000002-330000003-330000004-330000005-330000006-330000007"), new MemorySecurityAuditSink(), journal); _ = inheritedAcl.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 { ["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 { ["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 EntryExecutablePolicy([new ApprovedEntryExecutable("powershell", powershell, EntryExecutablePolicy.ComputeSha256(powershell), true, null)], [], []), 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.ApplyAsync(new DirectElevatedNetworkSession(), journal, AppContainerProfile.EnsureFixed().Sid, "none", [], new MemorySecurityAuditSink(), null, CancellationToken.None).GetAwaiter().GetResult(); 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}"); } }