summaryrefslogtreecommitdiff
path: root/src/Agent.Windows/Tools
diff options
context:
space:
mode:
Diffstat (limited to 'src/Agent.Windows/Tools')
-rw-r--r--src/Agent.Windows/Tools/WindowsToolBackend.cs60
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;
}