diff options
Diffstat (limited to 'src/Agent.Windows/Tools')
| -rw-r--r-- | src/Agent.Windows/Tools/WindowsToolBackend.cs | 60 |
1 files changed, 36 insertions, 24 deletions
diff --git a/src/Agent.Windows/Tools/WindowsToolBackend.cs b/src/Agent.Windows/Tools/WindowsToolBackend.cs index 5cbf853..af4c906 100644 --- a/src/Agent.Windows/Tools/WindowsToolBackend.cs +++ b/src/Agent.Windows/Tools/WindowsToolBackend.cs @@ -10,12 +10,13 @@ using Agent.Windows.Security; namespace Agent.Windows.Tools; -public sealed record WindowsSandboxSettings( +public sealed record EffectiveSandboxPolicy( IReadOnlyList<FilePolicyEntry> Files, - IReadOnlyList<ExecutableApproval> Executables, - bool AllowShell, + IReadOnlyList<ApprovedEntryExecutable> EntryExecutables, + IReadOnlyList<string> RuntimeRoots, + IReadOnlyList<string> ArtifactRoots, SandboxJobLimits JobLimits, - string NetworkMode, + string NetworkAccess, IReadOnlyList<(string Host, IReadOnlyList<int> Ports)> NetworkAllowances, string JournalDirectory, string BackupDirectory, @@ -23,17 +24,22 @@ public sealed record WindowsSandboxSettings( long WholeReadLimitBytes, int SearchResultLimit, int TailLineLimit, - int ProcessTimeoutSeconds = 300); + int ProcessTimeoutSeconds = 300, + string ResourceProfile = "normal", + IReadOnlyList<string>? BaselineCapabilities = null); -public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurityAuditSink audit, ITaskUpdateHandler? taskUpdates = null) : IInternalToolBackend +public sealed class WindowsToolBackend(EffectiveSandboxPolicy settings, ISecurityAuditSink audit, ITaskUpdateHandler? taskUpdates = null) : IInternalToolBackend { private readonly ConcurrentDictionary<Guid, TaskContext> _contexts = new(); private readonly PathPolicy _paths = new(settings.Files); - private readonly ExecutablePolicy _executables = new(settings.Executables, settings.AllowShell); + private readonly EntryExecutablePolicy _entryExecutables = new(settings.EntryExecutables, settings.RuntimeRoots, settings.ArtifactRoots); private readonly ITaskUpdateHandler _taskUpdates = taskUpdates ?? new TaskGraphToolHandler(); public async Task BeginTaskAsync(Guid taskId, Guid sessionId, CancellationToken cancellationToken) { + if (settings.BaselineCapabilities is not null && + !settings.BaselineCapabilities.SequenceEqual(["registryRead", "lpacInstrumentation"], StringComparer.Ordinal)) + throw new InvalidOperationException("Effective Sandbox Policy baseline capabilities do not match the fixed LPAC baseline."); if (_contexts.Count != 0) throw new InvalidOperationException("Only one Task Resource scope may be active."); AppContainerProfile profile = AppContainerProfile.EnsureFixed(); TaskResourceJournal journal = TaskResourceJournal.Create(settings.JournalDirectory, taskId); @@ -42,23 +48,29 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit _taskUpdates.BeginTask(taskId, sessionId); try { - foreach ((string path, FileCapability capabilities) in settings.Files - .GroupBy(static entry => CanonicalPath.Resolve(entry.Path, false), StringComparer.OrdinalIgnoreCase) - .Select(static group => (group.Key, group.Aggregate(FileCapability.None, static (value, item) => value | item.Capabilities)))) + foreach ((string path, FileCapability capabilities, bool osRuntimeRoot) in settings.Files + .GroupBy(static entry => ResolvedPath.Resolve(entry.Path, false), StringComparer.OrdinalIgnoreCase) + .Select(static group => (group.Key, + group.Aggregate(FileCapability.None, static (value, item) => value | item.Capabilities), + group.All(static item => item.Origin == "os_runtime_root")))) { + // System32 has a protected OS-owned DACL. Its existing LPAC access is verified by + // real process-tree tests; changing or unprotecting that DACL is forbidden. + if (osRuntimeRoot) continue; // OS/runtime executables may already grant AppContainer execute through their protected system ACL. // Do not rewrite a file ACL when execute is the only requested capability; CreateProcess remains - // constrained by AppContainer access plus canonical path/SHA-256 approval. + // constrained by LPAC access plus Resolved Path/SHA-256 Entry approval. if (File.Exists(path) && capabilities == FileCapability.Execute) continue; // A single inheritable ACE on an approved directory covers existing and newly-created // descendants. Snapshotting and rewriting every child ACL is both redundant and // prohibitively expensive for runtime trees such as Python installations. context.Acls.Add(acl.Grant(taskId, path, capabilities)); } - IReadOnlyList<NetworkEndpoint> allowances = settings.NetworkMode == "allow" + IReadOnlyList<NetworkEndpoint> allowances = settings.NetworkAccess != "none" ? await NetworkAllowanceSnapshot.ResolveAsync(settings.NetworkAllowances, cancellationToken).ConfigureAwait(false) : []; - context.Network = TaskNetworkPolicy.Apply(journal, profile.Sid, allowances, audit, sessionId); + context.Network = TaskNetworkPolicy.Apply(journal, profile.Sid, allowances, audit, sessionId, + enableLoopback: settings.NetworkAccess != "none"); if (!_contexts.TryAdd(taskId, context)) throw new InvalidOperationException("Task context already exists."); audit.Write(new(DateTimeOffset.UtcNow, taskId, "task.start", "running", new Dictionary<string, string>(), SessionId: sessionId)); } @@ -82,7 +94,7 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit "file_search" => Task.FromResult(Search(arguments)), "file_read" => Task.FromResult(Read(arguments)), "file_edit" => Task.FromResult(Edit(context, arguments)), - "process_run" => Task.FromResult(RunProcess(context, arguments)), + "process_run" => Task.FromResult(RunProcess(context, arguments, cancellationToken)), "task_update" => Task.FromResult(_taskUpdates.Execute(taskId, arguments)), "dir" => Task.FromResult(DirectoryOp(context, arguments)), _ => throw new ToolValidationException($"Unknown internal tool '{toolName}'.") @@ -128,7 +140,7 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit { string root = arguments.GetProperty("root").GetString()!; _paths.Demand(root, FileCapability.Read); - string canonical = CanonicalPath.Resolve(root); + string canonical = ResolvedPath.Resolve(root); string mode = arguments.GetProperty("mode").GetString()!; string query = arguments.GetProperty("query").GetString()!; int max = arguments.TryGetProperty("max_results", out JsonElement maxElement) ? maxElement.GetInt32() : 100; @@ -164,10 +176,10 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit { string path = arguments.GetProperty("path").GetString()!; _paths.Demand(path, FileCapability.Read); - string canonical = CanonicalPath.Resolve(path); + string canonical = ResolvedPath.Resolve(path); string mode = arguments.GetProperty("mode").GetString()!; FileInfo info = new(canonical); - if (mode == "metadata") return Success(JsonSerializer.Serialize(new { path = canonical, info.Length, info.LastWriteTimeUtc, sha256 = ExecutablePolicy.ComputeSha256(canonical) })); + if (mode == "metadata") return Success(JsonSerializer.Serialize(new { path = canonical, info.Length, info.LastWriteTimeUtc, sha256 = EntryExecutablePolicy.ComputeSha256(canonical) })); if (mode == "whole") { if (info.Length > settings.WholeReadLimitBytes) throw new IOException("Whole read exceeds configured limit; use range mode."); @@ -245,7 +257,7 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit if (operation == "list") { _paths.Demand(path, FileCapability.Read); - string canonical = CanonicalPath.Resolve(path); + string canonical = ResolvedPath.Resolve(path); var entries = Directory.EnumerateFileSystemEntries(canonical, "*", SearchOption.TopDirectoryOnly) .Select(Path.GetFullPath) .ToArray(); @@ -254,20 +266,20 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit throw new ToolValidationException($"Unsupported dir operation '{operation}'."); } - private ToolExecutionResult RunProcess(TaskContext context, JsonElement arguments) + private ToolExecutionResult RunProcess(TaskContext context, JsonElement arguments, CancellationToken cancellationToken) { Guid taskId = context.Journal.TaskId; string executable = arguments.GetProperty("executable").GetString()!; string cwd = arguments.GetProperty("cwd").GetString()!; - _paths.Demand(executable, FileCapability.Execute); _paths.Demand(cwd, FileCapability.Read); string[] args = arguments.GetProperty("args").EnumerateArray().Select(static item => item.GetString()!).ToArray(); - string? managedEntryPoint = ExecutablePolicy.ManagedEntryPoint(executable, args); + string? managedEntryPoint = _entryExecutables.ManagedEntryPoint(executable, args, cwd); if (managedEntryPoint is not null) _paths.Demand(managedEntryPoint, FileCapability.Execute); int seconds = ResolveProcessTimeout(arguments, settings.ProcessTimeoutSeconds); string? stdin = arguments.TryGetProperty("stdin", out JsonElement stdinElement) && stdinElement.ValueKind == JsonValueKind.String ? stdinElement.GetString() : null; - RestrictedProcessResult result = new RestrictedProcessLauncher(_executables, audit, AppContainerProfile.EnsureFixed().Sid, settings.JobLimits) - .Run(taskId, executable, args, cwd, TimeSpan.FromSeconds(seconds), stdin, context.SessionId); + RestrictedProcessResult result = new RestrictedProcessLauncher(_entryExecutables, audit, AppContainerProfile.EnsureFixed().Sid, settings.JobLimits, + allowNetwork: settings.NetworkAccess != "none") + .Run(taskId, executable, args, cwd, TimeSpan.FromSeconds(seconds), stdin, context.SessionId, cancellationToken); string output = $"Process exited with code {result.ExitCode} (PID {result.ProcessId}).\nstdout:\n{result.StandardOutput}\nstderr:\n{result.StandardError}"; return new(result.ExitCode == 0, output, true, result.ExitCode == 0 ? null : $"process:{executable}:{result.ExitCode}"); @@ -278,7 +290,7 @@ public sealed class WindowsToolBackend(WindowsSandboxSettings settings, ISecurit int seconds = arguments.TryGetProperty("timeout_seconds", out JsonElement timeout) ? timeout.GetInt32() : configuredTimeoutSeconds; if (seconds <= 0) throw new ToolValidationException("timeout_seconds must be positive."); if (seconds > configuredTimeoutSeconds) - throw new ToolValidationException($"timeout_seconds exceeds configured sandbox.resources.process_timeout_seconds ({configuredTimeoutSeconds})."); + throw new ToolValidationException($"timeout_seconds exceeds configured sandbox.resources.execution_timeout_seconds ({configuredTimeoutSeconds})."); return seconds; } |
