diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-13 18:20:33 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-13 18:20:33 +0900 |
| commit | 76fdcb43585a4c4d288c1026d318a0ddbe3e26ab (patch) | |
| tree | f58296832d37b035d3e787e0f270c77b59c89d77 /tests/Agent.Windows.Tests/SecurityPolicyTests.cs | |
| parent | 78fe349073e3a2e89f0bf14b134ae0955aa7182a (diff) | |
fix(sandbox): correct cmd descendant handling and protected ACL policy
Replace the earlier cmd-wide descendant incompatibility diagnosis with the verified distinction between drive-qualified paths and PATH/workspace-relative invocation. Document the cmd YAML configuration, executable resolution, --no-runtime approval flow, and safe descendant invocation examples in the user README.
Keep the System32 protected DACL intact for both the OS Runtime Root and approved entry files nested below it. Skip redundant file-level ACL rewrites for approved System32 executables while retaining LPAC confinement, resolved-path checks, SHA-256 approval, WFP policy, and Job Object enforcement.
Expand Windows coverage with capability positive/negative matrices, protected-DACL regression tests, cmd/PowerShell/venv Python script execution, nine Entry-to-Descendant combinations, network none/local probes, and randomized ACL round-trip testing. Add read-only CLI/README verification and published HTTP MCP agent_run verification scripts.
Update Design, Audit Report, and Requirement Traceability with the corrected root cause, current evidence, withdrawn diagnostic conclusions, and the remaining partial item for per-combination descendant self-recorded LPAC SID/Job evidence.
Verified: Release build 0 warnings/errors; Core 103 PASS; Planner 10 PASS; Windows 75 PASS with skip 0; README operational suite 19 PASS; published HTTP MCP agent_run completed 3/3 with cmd, curl, C:\tmp\.venv Python, network-none enforcement, ACL restoration, and CMD_HTTP_MCP_OK.
Diffstat (limited to 'tests/Agent.Windows.Tests/SecurityPolicyTests.cs')
| -rw-r--r-- | tests/Agent.Windows.Tests/SecurityPolicyTests.cs | 77 |
1 files changed, 72 insertions, 5 deletions
diff --git a/tests/Agent.Windows.Tests/SecurityPolicyTests.cs b/tests/Agent.Windows.Tests/SecurityPolicyTests.cs index 09ed61d..d0c32a8 100644 --- a/tests/Agent.Windows.Tests/SecurityPolicyTests.cs +++ b/tests/Agent.Windows.Tests/SecurityPolicyTests.cs @@ -431,8 +431,10 @@ public sealed class SecurityPolicyTests } } - [Fact] - public void Lpac_baseline_allows_registry_read_but_denies_registry_write_and_com_activation() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Lpac_capability_matrix_preserves_registry_and_com_denials(bool allowNetwork) { AppContainerProfile profile = AppContainerProfile.EnsureFixed(); using TestDirectory directory = new(); @@ -448,7 +450,7 @@ public sealed class SecurityPolicyTests { var launcher = new RestrictedProcessLauncher( new EntryExecutablePolicy([new ApprovedEntryExecutable("powershell", powershell, EntryExecutablePolicy.ComputeSha256(powershell), true, null)], [], []), - audit, profile.Sid, new SandboxJobLimits(2, 256UL * 1024 * 1024)); + audit, profile.Sid, new SandboxJobLimits(2, 256UL * 1024 * 1024), allowNetwork); string script = $"$ErrorActionPreference='Stop'; $p=(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion').ProductName; " + $"$writeDenied=$false; try {{ New-Item -Path 'HKCU:\\Software\\{registryName}' -Force | Out-Null }} catch {{ $writeDenied=$true }}; " + "$comDenied=$false; try { $null=New-Object -ComObject Scripting.FileSystemObject } catch { $comDenied=$true }; " + @@ -464,6 +466,9 @@ public sealed class SecurityPolicyTests Assert.Contains("REGISTRY_WRITE_DENIED=True", result.StandardOutput, StringComparison.Ordinal); Assert.Contains("COM_DENIED=True", result.StandardOutput, StringComparison.Ordinal); Assert.Contains("BROKER_SECRET_INHERITED=False", result.StandardOutput, StringComparison.Ordinal); + Assert.Contains(audit.Events, item => item.Event == "process.start" && + item.Data["baseline_capabilities"] == "registryRead,lpacInstrumentation" && + item.Data["network_capabilities"] == (allowNetwork ? "internetClient,privateNetworkClientServer" : "none")); } finally { @@ -507,6 +512,31 @@ public sealed class SecurityPolicyTests } [Fact] + public void Approved_cmd_starts_unapproved_descendant_by_unqualified_name() + { + AppContainerProfile profile = AppContainerProfile.EnsureFixed(); + using TestDirectory directory = new(); + string cmd = Environment.GetEnvironmentVariable("ComSpec")!; + var audit = new MemorySecurityAuditSink(); + TaskAclManager acl = new(profile.Sid, audit); + Guid taskId = Guid.NewGuid(); + AclSnapshot snapshot = acl.Grant(taskId, directory.Path, FileCapability.Read | FileCapability.Write); + try + { + var launcher = new RestrictedProcessLauncher( + new EntryExecutablePolicy([new ApprovedEntryExecutable("cmd", cmd, EntryExecutablePolicy.ComputeSha256(cmd), true, null)], [], []), + audit, profile.Sid, new SandboxJobLimits(4, 512UL * 1024 * 1024)); + RestrictedProcessResult result = launcher.Run(taskId, "cmd", ["/d", "/c", "cmd.exe /d /c exit 0"], + directory.Path, TimeSpan.FromSeconds(30), null, null, TestContext.Current.CancellationToken); + Assert.Equal(0u, result.ExitCode); + Assert.Contains(audit.Events, item => item.Event == "process.start" && + item.Data["token_is_app_container"] == bool.TrueString && item.Data["process_in_job"] == bool.TrueString && + item.Data["descendant_policy"] == "lpac_inherited"); + } + finally { acl.Restore(taskId, snapshot); } + } + + [Fact] public void Approved_python314_starts_python_subprocess_inside_lpac_job() { AppContainerProfile profile = AppContainerProfile.EnsureFixed(); @@ -656,8 +686,8 @@ public sealed class SecurityPolicyTests snapshots.Add(acl.Grant(taskId, directory.Path, FileCapability.Read | FileCapability.Write | FileCapability.Delete | FileCapability.Execute)); snapshots.Add(acl.Grant(taskId, runtimeRoot, FileCapability.Read | FileCapability.Execute)); - // System32 owns a protected DACL. Do not rewrite it; successful cmd descendants below - // are the enforcement check for its existing LPAC read/execute access. + // System32 owns an OS-managed protected DACL. Do not rewrite it; descendant + // execution relies on its existing LPAC read/execute access. var launcher = new RestrictedProcessLauncher( new EntryExecutablePolicy([new ApprovedEntryExecutable("dotnet", dotnet, EntryExecutablePolicy.ComputeSha256(dotnet), false, runtimeRoot)], [runtimeRoot], [directory.Path]), audit, profile.Sid, new SandboxJobLimits(16, 1024UL * 1024 * 1024)); @@ -975,6 +1005,43 @@ public sealed class SecurityPolicyTests } [Fact] + public async Task Os_runtime_root_protected_dacl_is_never_modified() + { + using TestDirectory directory = new(); + string system32 = ResolvedPath.Resolve(Environment.SystemDirectory); + string cmd = ResolvedPath.Resolve(Environment.GetEnvironmentVariable("ComSpec")!); + DirectorySecurity security = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(system32), AccessControlSections.Access); + var raw = new RawSecurityDescriptor(security.GetSecurityDescriptorBinaryForm(), 0); + Assert.True(raw.ControlFlags.HasFlag(ControlFlags.DiscretionaryAclProtected)); + string before = Convert.ToHexString(security.GetSecurityDescriptorBinaryForm()); + string cmdBefore = Convert.ToHexString(FileSystemAclExtensions.GetAccessControl( + new FileInfo(cmd), AccessControlSections.Access).GetSecurityDescriptorBinaryForm()); + var audit = new MemorySecurityAuditSink(); + var settings = new EffectiveSandboxPolicy( + [new FilePolicyEntry(cmd, FileCapability.Read | FileCapability.Execute, "approved_shell_executable"), + new FilePolicyEntry(system32, FileCapability.Read | FileCapability.Execute, "os_runtime_root")], + [new ApprovedEntryExecutable("cmd", cmd, EntryExecutablePolicy.ComputeSha256(cmd), true, null)], [], [], + new SandboxJobLimits(), "none", [], Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"), + 16 * 1024 * 1024, 1024 * 1024, 1000, 10000, BaselineCapabilities: ["registryRead", "lpacInstrumentation"]); + var backend = new WindowsToolBackend(settings, audit); + Guid taskId = Guid.NewGuid(); + + await backend.BeginTaskAsync(taskId, Guid.NewGuid(), TestContext.Current.CancellationToken); + await backend.EndTaskAsync(taskId, true, TestContext.Current.CancellationToken); + + string after = Convert.ToHexString(FileSystemAclExtensions.GetAccessControl( + new DirectoryInfo(system32), AccessControlSections.Access).GetSecurityDescriptorBinaryForm()); + string cmdAfter = Convert.ToHexString(FileSystemAclExtensions.GetAccessControl( + new FileInfo(cmd), AccessControlSections.Access).GetSecurityDescriptorBinaryForm()); + Assert.Equal(before, after); + Assert.Equal(cmdBefore, cmdAfter); + Assert.DoesNotContain(audit.Events, item => item.Event is ("acl.apply" or "acl.restore") && + item.Data.TryGetValue("path", out string? path) && + (string.Equals(path, system32, StringComparison.OrdinalIgnoreCase) || + string.Equals(path, cmd, StringComparison.OrdinalIgnoreCase))); + } + + [Fact] public async Task Dir_mkdir_creates_directory_within_approved_root_and_cleans_up() { using TestDirectory directory = new(); |
