summaryrefslogtreecommitdiff
path: root/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs')
-rw-r--r--tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs89
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}");
+ }
+}