diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk.Sample/Program.cs b/sdk/dotnet/Microsoft.Mxc.Sdk.Sample/Program.cs
index fa02237f8..bd97359a0 100644
--- a/sdk/dotnet/Microsoft.Mxc.Sdk.Sample/Program.cs
+++ b/sdk/dotnet/Microsoft.Mxc.Sdk.Sample/Program.cs
@@ -27,6 +27,21 @@
TimeoutMs = 30_000,
};
+// Set MXC_SAMPLE_CAPTURE_DENIALS=1 to opt into Windows denial capture without
+// changing the sample's default host requirements or generating traces by default.
+if (OperatingSystem.IsWindows()
+ && string.Equals(
+ Environment.GetEnvironmentVariable("MXC_SAMPLE_CAPTURE_DENIALS"),
+ "1",
+ StringComparison.Ordinal))
+{
+ policy.CaptureDenials = new CaptureDenialsPolicy
+ {
+ Mode = CaptureDenialsMode.Block,
+ RetainEtl = false,
+ };
+}
+
Console.WriteLine($"Running: {command}");
try
@@ -39,6 +54,23 @@
{
Console.WriteLine($"stderr : {result.Stderr.TrimEnd()}");
}
+ foreach (var warning in result.Warnings)
+ {
+ Console.WriteLine($"warning : {warning}");
+ }
+ if (result.OutputMetadata?.CaptureDenials is { } capture)
+ {
+ Console.WriteLine($"denials : {capture.OutputPath}");
+ if (capture.EtlPath is not null)
+ {
+ Console.WriteLine($"ETL : {capture.EtlPath}");
+ }
+ }
+ if (result.OutputMetadata?.CaptureDenialsError is { } captureError)
+ {
+ Console.WriteLine($"capture error: {captureError.Message}");
+ Console.WriteLine($"retained ETL : {captureError.EtlPath}");
+ }
// Streaming variant: spawn the same command as a live process and stream
// its stdout as it is produced, then wait for exit.
@@ -59,6 +91,19 @@
var streamResult = proc.Wait();
Console.WriteLine($"streamed exit code : {streamResult.ExitCode}");
+ if (proc.OutputMetadata?.CaptureDenials is { } streamCapture)
+ {
+ Console.WriteLine($"stream denials : {streamCapture.OutputPath}");
+ if (streamCapture.EtlPath is not null)
+ {
+ Console.WriteLine($"stream ETL : {streamCapture.EtlPath}");
+ }
+ }
+ if (proc.OutputMetadata?.CaptureDenialsError is { } streamCaptureError)
+ {
+ Console.WriteLine($"stream capture error: {streamCaptureError.Message}");
+ Console.WriteLine($"stream retained ETL : {streamCaptureError.EtlPath}");
+ }
}
// State-aware lifecycle: provision -> start -> exec -> stop -> deprovision.
diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcSandboxTests.cs b/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcSandboxTests.cs
index 6b76268c6..8b8dca47c 100644
--- a/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcSandboxTests.cs
+++ b/sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcSandboxTests.cs
@@ -50,13 +50,15 @@ public void SandboxPolicy_SerializesToCamelCaseJson()
TimeoutMs = 5000,
Filesystem = new FilesystemPolicy { ReadwritePaths = { "/tmp" } },
Ui = new UiPolicy { AllowWindows = true, Clipboard = ClipboardPolicy.Read },
+ CaptureDenials = new CaptureDenialsPolicy
+ {
+ Mode = CaptureDenialsMode.Allow,
+ OutputPath = @"C:\logs\denials.json",
+ RetainEtl = true,
+ },
};
- var options = new JsonSerializerOptions
- {
- Converters = { new System.Text.Json.Serialization.JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
- };
- var json = JsonSerializer.Serialize(policy, options);
+ var json = MxcSandbox.SerializePolicy(policy);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
@@ -65,6 +67,35 @@ public void SandboxPolicy_SerializesToCamelCaseJson()
Assert.Equal("/tmp", root.GetProperty("filesystem").GetProperty("readwritePaths")[0].GetString());
Assert.Equal("read", root.GetProperty("ui").GetProperty("clipboard").GetString());
Assert.True(root.GetProperty("ui").GetProperty("allowWindows").GetBoolean());
+ var capture = root.GetProperty("captureDenials");
+ Assert.Equal("allow", capture.GetProperty("mode").GetString());
+ Assert.Equal(@"C:\logs\denials.json", capture.GetProperty("outputPath").GetString());
+ Assert.True(capture.GetProperty("retainEtl").GetBoolean());
+ }
+
+ [Fact]
+ public void CaptureDenialsPolicy_DefaultsToBlockAndOmitsOutputPath()
+ {
+ var policy = new SandboxPolicy
+ {
+ Version = "0.8.0-alpha",
+ CaptureDenials = new CaptureDenialsPolicy(),
+ };
+ using var doc = JsonDocument.Parse(MxcSandbox.SerializePolicy(policy));
+ var capture = doc.RootElement.GetProperty("captureDenials");
+
+ Assert.Equal("block", capture.GetProperty("mode").GetString());
+ Assert.False(capture.GetProperty("retainEtl").GetBoolean());
+ Assert.False(capture.TryGetProperty("outputPath", out _));
+ }
+
+ [Fact]
+ public void SandboxPolicy_OmitsCaptureDenialsWhenNotConfigured()
+ {
+ var policy = new SandboxPolicy { Version = "0.8.0-alpha" };
+ using var doc = JsonDocument.Parse(MxcSandbox.SerializePolicy(policy));
+
+ Assert.False(doc.RootElement.TryGetProperty("captureDenials", out _));
}
[Fact]
diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/MxcSandbox.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/MxcSandbox.cs
index 6c5b50f07..abac34d5c 100644
--- a/sdk/dotnet/Microsoft.Mxc.Sdk/MxcSandbox.cs
+++ b/sdk/dotnet/Microsoft.Mxc.Sdk/MxcSandbox.cs
@@ -57,7 +57,7 @@ public static RunResult Run(SandboxPolicy policy, string command)
ArgumentNullException.ThrowIfNull(policy);
ArgumentNullException.ThrowIfNull(command);
- var policyJson = JsonSerializer.Serialize(policy, PolicyJsonOptions);
+ var policyJson = SerializePolicy(policy);
var policyBuf = ToNullTerminatedUtf8(policyJson);
var commandBuf = ToNullTerminatedUtf8(command);
@@ -122,7 +122,7 @@ public static MxcSandboxProcess Spawn(SandboxPolicy policy, string command)
ArgumentNullException.ThrowIfNull(policy);
ArgumentNullException.ThrowIfNull(command);
- var policyJson = JsonSerializer.Serialize(policy, PolicyJsonOptions);
+ var policyJson = SerializePolicy(policy);
var policyBuf = ToNullTerminatedUtf8(policyJson);
var commandBuf = ToNullTerminatedUtf8(command);
@@ -157,6 +157,12 @@ private static byte[] ToNullTerminatedUtf8(string value)
return buffer;
}
+ internal static string SerializePolicy(SandboxPolicy policy)
+ {
+ ArgumentNullException.ThrowIfNull(policy);
+ return JsonSerializer.Serialize(policy, PolicyJsonOptions);
+ }
+
private static unsafe string? PtrToString(byte* p) =>
p is null ? null : Marshal.PtrToStringUTF8((IntPtr)p);
diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxOutputMetadata.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxOutputMetadata.cs
index 398bd5e26..2e79bb181 100644
--- a/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxOutputMetadata.cs
+++ b/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxOutputMetadata.cs
@@ -24,7 +24,10 @@ public sealed class CaptureDenialsErrorOutput
[JsonPropertyName("message")]
public string Message { get; init; } = string.Empty;
- /// Absolute path to the retained ETL trace.
+ ///
+ /// Absolute path to the retained ETL trace. Delete this file after use; do not delete its
+ /// parent directory unless the caller independently owns or positively recognizes it.
+ ///
[JsonPropertyName("etlPath")]
public string EtlPath { get; init; } = string.Empty;
}
@@ -52,7 +55,11 @@ public sealed class CaptureDenialsOutput
[JsonPropertyName("deniedResourcesTruncated")]
public bool DeniedResourcesTruncated { get; init; }
- /// Absolute path to the retained ETL trace, when requested.
+ ///
+ /// Absolute path to the retained ETL trace, when requested. Delete this file after use; do
+ /// not delete its parent directory unless the caller independently owns or positively
+ /// recognizes it.
+ ///
[JsonPropertyName("etlPath")]
public string? EtlPath { get; init; }
}
diff --git a/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicy.cs b/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicy.cs
index b8fb1e819..d5a2ae12e 100644
--- a/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicy.cs
+++ b/sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicy.cs
@@ -28,11 +28,67 @@ public sealed class SandboxPolicy
[JsonPropertyName("ui")]
public UiPolicy? Ui { get; set; }
+ ///
+ /// Windows ProcessContainer denial capture. Its presence enables capture;
+ /// Linux and macOS backends ignore it.
+ ///
+ [JsonPropertyName("captureDenials")]
+ public CaptureDenialsPolicy? CaptureDenials { get; set; }
+
/// Execution timeout in milliseconds (null = no timeout).
[JsonPropertyName("timeoutMs")]
public uint? TimeoutMs { get; set; }
}
+///
+/// How captureDenials handles each ungranted access check while recording it.
+///
+public enum CaptureDenialsMode
+{
+ ///
+ /// Keep the access denied and record the denial, preserving deny-by-default containment.
+ ///
+ Block,
+
+ ///
+ /// Allow and record the access. This relaxes containment for the run and emits a warning.
+ /// and
+ /// expose that
+ /// warning; streaming processes returned by
+ /// currently do not.
+ ///
+ Allow,
+}
+
+///
+/// Windows ProcessContainer denial-capture settings. The presence of this section enables
+/// capture and reports the resulting document through .
+///
+public sealed class CaptureDenialsPolicy
+{
+ /// How ungranted access checks are handled while recording them.
+ [JsonPropertyName("mode")]
+ public CaptureDenialsMode Mode { get; set; } = CaptureDenialsMode.Block;
+
+ ///
+ /// Optional absolute path for the JSON denials document. The parent directory must exist.
+ /// MXC inserts a per-run identifier into the file stem and reports the actual path through
+ /// output metadata. When omitted, MXC uses a run-unique file in the system temporary
+ /// directory.
+ ///
+ [JsonPropertyName("outputPath")]
+ public string? OutputPath { get; set; }
+
+ ///
+ /// Preserve the sealed ETL trace and report its path through output metadata. Retained traces
+ /// can contain sensitive paths and identifiers; callers must delete the reported trace after
+ /// use. Do not delete its parent directory unless the caller independently owns or positively
+ /// recognizes that directory.
+ ///
+ [JsonPropertyName("retainEtl")]
+ public bool RetainEtl { get; set; }
+}
+
/// Filesystem section of a .
public sealed class FilesystemPolicy
{
diff --git a/sdk/dotnet/README.md b/sdk/dotnet/README.md
index 790c251e7..cf4d700e4 100644
--- a/sdk/dotnet/README.md
+++ b/sdk/dotnet/README.md
@@ -44,14 +44,61 @@ generated JSON document and carries its summary. When ETL retention is enabled,
`OutputMetadata.CaptureDenials.EtlPath` identifies the retained trace. If
capture finalization fails after retaining the trace,
`OutputMetadata.CaptureDenialsError` carries both the failure and its path.
-After deleting a retained ETL, also remove its now-empty per-run parent
-directory.
+Delete every reported ETL file after use. Do not delete its parent directory
+unless your application independently owns or positively recognizes that
+directory; it may be a shared location such as the system temporary directory
+or the configured output directory.
`RunResult.Warnings` carries security warnings raised during the run — notably
when `permissiveLearningMode` disabled deny-by-default. MXC never writes these
to the host's stderr, so inspecting `Warnings` is the only way to learn that
containment was relaxed.
+### Denial capture (Windows)
+
+Set `SandboxPolicy.CaptureDenials` to record Windows ProcessContainer accesses
+that the policy does not grant:
+
+```csharp
+var policy = new SandboxPolicy
+{
+ Version = "0.8.0-alpha",
+ CaptureDenials = new CaptureDenialsPolicy
+ {
+ // Block is the safe default: deny the access and record it.
+ // Allow records what would have been denied but relaxes containment.
+ Mode = CaptureDenialsMode.Block,
+ // Optional absolute destination. MXC inserts a per-run ID into the stem.
+ OutputPath = @"C:\logs\denials.json",
+ // Retained ETLs can contain sensitive paths and identifiers.
+ RetainEtl = false,
+ },
+};
+
+RunResult result = MxcSandbox.Run(policy, "cmd /c type C:\\blocked.txt");
+if (result.OutputMetadata?.CaptureDenials is { } capture)
+{
+ Console.WriteLine($"denials: {capture.OutputPath}");
+ Console.WriteLine($"unique denials: {capture.TotalDenials}");
+}
+
+foreach (string warning in result.Warnings)
+{
+ Console.Error.WriteLine(warning);
+}
+```
+
+`OutputPath`'s parent directory must already exist. When it is omitted, MXC
+uses a run-unique file in the system temporary directory. Delete every reported
+ETL file after use, including an ETL reported through
+`CaptureDenialsError.EtlPath`; do not delete its parent directory unless your
+application independently owns or positively recognizes that directory.
+`CaptureDenialsMode.Allow` intentionally weakens deny-by-default containment;
+always inspect `RunResult.Warnings`. Streaming processes currently do not
+expose warnings, so use `Run` or `RunAsync` when warning inspection is required.
+Linux and macOS accept the policy for cross-platform parity but do not act on
+it.
+
### Streaming
`MxcSandbox.Spawn(policy, command)` returns a live `MxcSandboxProcess` you can