summaryrefslogtreecommitdiff
path: root/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs
blob: dd170abbffc2c08b05c6def4f7d880aa2b3c3209 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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<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}");
    }
}