summaryrefslogtreecommitdiff
path: root/src/Agent.Windows/Security
diff options
context:
space:
mode:
Diffstat (limited to 'src/Agent.Windows/Security')
-rw-r--r--src/Agent.Windows/Security/ExecutablePolicy.cs4
-rw-r--r--src/Agent.Windows/Security/PathPolicy.cs9
-rw-r--r--src/Agent.Windows/Security/RestrictedProcessLauncher.cs15
-rw-r--r--src/Agent.Windows/Security/SecurityAudit.cs6
4 files changed, 21 insertions, 13 deletions
diff --git a/src/Agent.Windows/Security/ExecutablePolicy.cs b/src/Agent.Windows/Security/ExecutablePolicy.cs
index 3d20bfa..1f36164 100644
--- a/src/Agent.Windows/Security/ExecutablePolicy.cs
+++ b/src/Agent.Windows/Security/ExecutablePolicy.cs
@@ -14,11 +14,11 @@ public sealed class ExecutablePolicy(IEnumerable<ExecutableApproval> approvals,
{
string canonical = CanonicalPath.Resolve(executable);
if (!allowShell && IsShell(canonical)) throw new SandboxPolicyException("process.shell_denied", "Shell execution is disabled.");
- if (!_approvals.TryGetValue(canonical, out string? approvedHash)) throw new SandboxPolicyException("process.path_denied", "Executable path is not approved.");
+ if (!_approvals.TryGetValue(canonical, out string? approvedHash)) throw new SandboxPolicyException("process.path_denied", $"Executable path is not approved: '{canonical}'. Add its canonical path and SHA-256 to sandbox.process.approvals.");
using FileStream stream = new(canonical, FileMode.Open, FileAccess.Read, FileShare.Read);
string actualHash = Convert.ToHexString(SHA256.HashData(stream));
if (!CryptographicOperations.FixedTimeEquals(Convert.FromHexString(approvedHash), Convert.FromHexString(actualHash)))
- throw new SandboxPolicyException("process.hash_denied", "Executable SHA-256 does not match its approval.");
+ throw new SandboxPolicyException("process.hash_denied", $"Executable SHA-256 does not match its approval: '{canonical}'. Recalculate SHA-256 and update sandbox.process.approvals.");
return new(canonical, actualHash);
}
diff --git a/src/Agent.Windows/Security/PathPolicy.cs b/src/Agent.Windows/Security/PathPolicy.cs
index 32a949c..4f6eec4 100644
--- a/src/Agent.Windows/Security/PathPolicy.cs
+++ b/src/Agent.Windows/Security/PathPolicy.cs
@@ -17,7 +17,8 @@ public sealed class PathPolicy(IEnumerable<FilePolicyEntry> entries)
public void Demand(string path, FileCapability required)
{
- if (!IsAllowed(path, required)) throw new SandboxPolicyException("policy.deny", $"'{path}' is not approved for {required}.");
+ if (!IsAllowed(path, required))
+ throw new SandboxPolicyException("policy.deny", $"'{path}' is not approved for {required}. Add the canonical path to sandbox.filesystem.{required.ToString().ToLowerInvariant()} and restart the server.");
}
private static bool IsAtOrBelow(string candidate, string root) =>
@@ -39,7 +40,11 @@ public static class CanonicalPath
current = Path.Combine(current, segment);
if (!File.Exists(current) && !Directory.Exists(current)) continue;
if ((File.GetAttributes(current) & FileAttributes.ReparsePoint) != 0)
- throw new SandboxPolicyException("path.reparse", $"Reparse points are not allowed: '{current}'.");
+ {
+ FileSystemInfo info = Directory.Exists(current) ? new DirectoryInfo(current) : new FileInfo(current);
+ string? target = info.ResolveLinkTarget(returnFinalTarget: true)?.FullName ?? info.LinkTarget;
+ throw new SandboxPolicyException("path.reparse", $"Reparse points are not allowed: component='{current}'. Approve the resolved canonical target{(string.IsNullOrWhiteSpace(target) ? string.Empty : $" '{Path.GetFullPath(target)}'")} instead.");
+ }
}
if (requireExisting && !File.Exists(fullPath) && !Directory.Exists(fullPath)) throw new FileNotFoundException("Approved path does not exist.", fullPath);
return Path.TrimEndingDirectorySeparator(fullPath);
diff --git a/src/Agent.Windows/Security/RestrictedProcessLauncher.cs b/src/Agent.Windows/Security/RestrictedProcessLauncher.cs
index 0786150..687af94 100644
--- a/src/Agent.Windows/Security/RestrictedProcessLauncher.cs
+++ b/src/Agent.Windows/Security/RestrictedProcessLauncher.cs
@@ -19,7 +19,8 @@ public sealed unsafe class RestrictedProcessLauncher(ExecutablePolicy executable
private const uint ProcessCreationChildProcessRestricted = 0x1;
private readonly SandboxJobLimits _jobLimits = jobLimits ?? new();
- public RestrictedProcessResult Run(Guid taskId, string executable, IReadOnlyList<string> arguments, string workingDirectory, TimeSpan timeout, string? standardInput = null)
+ public RestrictedProcessResult Run(Guid taskId, string executable, IReadOnlyList<string> arguments, string workingDirectory, TimeSpan timeout,
+ string? standardInput = null, Guid? sessionId = null)
{
VerifiedExecutable verified;
VerifiedExecutable? managedEntry = null;
@@ -31,7 +32,7 @@ public sealed unsafe class RestrictedProcessLauncher(ExecutablePolicy executable
}
catch (SandboxPolicyException ex)
{
- audit.Write(new(DateTimeOffset.UtcNow, taskId, "policy.deny", "denied", new Dictionary<string, string> { ["code"] = ex.Code, ["executable"] = executable }));
+ audit.Write(new(DateTimeOffset.UtcNow, taskId, "policy.deny", "denied", new Dictionary<string, string> { ["code"] = ex.Code, ["executable"] = executable }, SessionId: sessionId, ErrorCode: ex.Code));
throw;
}
@@ -142,7 +143,7 @@ public sealed unsafe class RestrictedProcessLauncher(ExecutablePolicy executable
audit.Write(new(DateTimeOffset.UtcNow, taskId, "policy.deny", "denied", new Dictionary<string, string>
{
["code"] = ex.Code, ["executable"] = executable, ["stage"] = "post_create_pre_resume"
- }));
+ }, SessionId: sessionId, ErrorCode: ex.Code));
throw;
}
if (PInvoke.ResumeThread(process.hThread) == uint.MaxValue) throw LastError("ResumeThread");
@@ -157,23 +158,23 @@ public sealed unsafe class RestrictedProcessLauncher(ExecutablePolicy executable
processAudit["managed_entry_path"] = managedEntry.CanonicalPath;
processAudit["managed_entry_sha256"] = managedEntry.Sha256;
}
- audit.Write(new(DateTimeOffset.UtcNow, taskId, "process.start", "allowed", processAudit));
+ audit.Write(new(DateTimeOffset.UtcNow, taskId, "process.start", "allowed", processAudit, SessionId: sessionId));
WAIT_EVENT wait = PInvoke.WaitForSingleObject(process.hProcess, checked((uint)Math.Clamp(timeout.TotalMilliseconds, 1, uint.MaxValue - 1)));
if (wait == WAIT_EVENT.WAIT_TIMEOUT)
{
_ = PInvoke.TerminateJobObject(job, 1460);
- audit.Write(new(DateTimeOffset.UtcNow, taskId, "process.timeout", "terminated", new Dictionary<string, string> { ["pid"] = process.dwProcessId.ToString() }));
+ audit.Write(new(DateTimeOffset.UtcNow, taskId, "process.timeout", "terminated", new Dictionary<string, string> { ["pid"] = process.dwProcessId.ToString() }, SessionId: sessionId));
throw new TimeoutException("Sandbox process exceeded its timeout.");
}
if (!PInvoke.GetExitCodeProcess(processView, out uint exitCode)) throw LastError("GetExitCodeProcess");
- audit.Write(new(DateTimeOffset.UtcNow, taskId, "process.exit", "observed", new Dictionary<string, string> { ["pid"] = process.dwProcessId.ToString(), ["exit_code"] = exitCode.ToString() }));
+ audit.Write(new(DateTimeOffset.UtcNow, taskId, "process.exit", "observed", new Dictionary<string, string> { ["pid"] = process.dwProcessId.ToString(), ["exit_code"] = exitCode.ToString() }, SessionId: sessionId));
if (exitCode != 0)
{
audit.Write(new(DateTimeOffset.UtcNow, taskId, "policy.deny", "observed", new Dictionary<string, string>
{
["code"] = "restricted_process_failure", ["pid"] = process.dwProcessId.ToString(),
["child_process_policy"] = "restricted", ["exit_code"] = exitCode.ToString()
- }));
+ }, SessionId: sessionId, ErrorCode: "process.child_denied"));
}
return new(process.dwProcessId, exitCode, ReadCapture(stdout), ReadCapture(stderr));
}
diff --git a/src/Agent.Windows/Security/SecurityAudit.cs b/src/Agent.Windows/Security/SecurityAudit.cs
index 8858c2b..616b08f 100644
--- a/src/Agent.Windows/Security/SecurityAudit.cs
+++ b/src/Agent.Windows/Security/SecurityAudit.cs
@@ -2,7 +2,8 @@ using System.Text.Json;
namespace Agent.Windows.Security;
-public sealed record SecurityAuditEvent(DateTimeOffset Timestamp, Guid TaskId, string Event, string Outcome, IReadOnlyDictionary<string, string> Data);
+public sealed record SecurityAuditEvent(DateTimeOffset Timestamp, Guid TaskId, string Event, string Outcome,
+ IReadOnlyDictionary<string, string> Data, Guid? SessionId = null, string? ErrorCode = null);
public interface ISecurityAuditSink
{
@@ -12,11 +13,12 @@ public interface ISecurityAuditSink
public sealed class JsonLinesSecurityAuditSink(string path) : ISecurityAuditSink
{
private readonly object _sync = new();
+ private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web);
public void Write(SecurityAuditEvent auditEvent)
{
string? directory = Path.GetDirectoryName(Path.GetFullPath(path));
if (directory is not null) Directory.CreateDirectory(directory);
- string line = JsonSerializer.Serialize(auditEvent);
+ string line = JsonSerializer.Serialize(auditEvent, Json);
lock (_sync) File.AppendAllText(path, line + Environment.NewLine);
}
}