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
|
using System.Security.Cryptography;
namespace Agent.Windows.Security;
public sealed record ExecutableApproval(string Path, string Sha256);
public sealed record VerifiedExecutable(string CanonicalPath, string Sha256);
public sealed class ExecutablePolicy(IEnumerable<ExecutableApproval> approvals, bool allowShell)
{
private readonly IReadOnlyDictionary<string, string> _approvals = approvals.ToDictionary(
approval => CanonicalPath.Resolve(approval.Path), approval => NormalizeHash(approval.Sha256), StringComparer.OrdinalIgnoreCase);
public VerifiedExecutable Verify(string executable)
{
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: '{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: '{canonical}'. Recalculate SHA-256 and update sandbox.process.approvals.");
return new(canonical, actualHash);
}
public static string? ManagedEntryPoint(string executable, IReadOnlyList<string> arguments)
{
if (!Path.GetFileName(executable).Equals("dotnet.exe", StringComparison.OrdinalIgnoreCase) || arguments.Count == 0) return null;
if (arguments[0].Equals("exec", StringComparison.OrdinalIgnoreCase))
{
if (arguments.Count < 2 || arguments[1].StartsWith("-", StringComparison.Ordinal))
throw new SandboxPolicyException("process.managed_entry_invalid", "dotnet exec requires an explicit managed entry assembly as its next argument.");
return arguments[1];
}
return arguments[0].EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ? arguments[0] : null;
}
public static string ComputeSha256(string executable)
{
using FileStream stream = new(CanonicalPath.Resolve(executable), FileMode.Open, FileAccess.Read, FileShare.Read);
return Convert.ToHexString(SHA256.HashData(stream));
}
private static bool IsShell(string path) => Path.GetFileName(path) is string name &&
(name.Equals("cmd.exe", StringComparison.OrdinalIgnoreCase) || name.Equals("powershell.exe", StringComparison.OrdinalIgnoreCase) || name.Equals("pwsh.exe", StringComparison.OrdinalIgnoreCase));
private static string NormalizeHash(string hash)
{
string normalized = hash.Replace("-", string.Empty, StringComparison.Ordinal).Trim().ToUpperInvariant();
if (normalized.Length != 64 || !normalized.All(Uri.IsHexDigit)) throw new ArgumentException("SHA-256 approval must contain 64 hexadecimal characters.", nameof(hash));
return normalized;
}
}
|