Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk.Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand Down
41 changes: 36 additions & 5 deletions sdk/dotnet/Microsoft.Mxc.Sdk.Tests/MxcSandboxTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: didn't realize we hadn't done this yet but we probably want to hook these tests up to the PR pipelines in the future.

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]
Expand Down
10 changes: 8 additions & 2 deletions sdk/dotnet/Microsoft.Mxc.Sdk/MxcSandbox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
11 changes: 9 additions & 2 deletions sdk/dotnet/Microsoft.Mxc.Sdk/SandboxOutputMetadata.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ public sealed class CaptureDenialsErrorOutput
[JsonPropertyName("message")]
public string Message { get; init; } = string.Empty;

/// <summary>Absolute path to the retained ETL trace.</summary>
/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("etlPath")]
public string EtlPath { get; init; } = string.Empty;
}
Expand Down Expand Up @@ -52,7 +55,11 @@ public sealed class CaptureDenialsOutput
[JsonPropertyName("deniedResourcesTruncated")]
public bool DeniedResourcesTruncated { get; init; }

/// <summary>Absolute path to the retained ETL trace, when requested.</summary>
/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("etlPath")]
public string? EtlPath { get; init; }
}
56 changes: 56 additions & 0 deletions sdk/dotnet/Microsoft.Mxc.Sdk/SandboxPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,67 @@ public sealed class SandboxPolicy
[JsonPropertyName("ui")]
public UiPolicy? Ui { get; set; }

/// <summary>
/// Windows ProcessContainer denial capture. Its presence enables capture;
/// Linux and macOS backends ignore it.
/// </summary>
[JsonPropertyName("captureDenials")]
public CaptureDenialsPolicy? CaptureDenials { get; set; }

/// <summary>Execution timeout in milliseconds (<c>null</c> = no timeout).</summary>
[JsonPropertyName("timeoutMs")]
public uint? TimeoutMs { get; set; }
}

/// <summary>
/// How <c>captureDenials</c> handles each ungranted access check while recording it.
/// </summary>
public enum CaptureDenialsMode
{
/// <summary>
/// Keep the access denied and record the denial, preserving deny-by-default containment.
/// </summary>
Block,

/// <summary>
/// Allow and record the access. This relaxes containment for the run and emits a warning.
/// <see cref="MxcSandbox.Run(SandboxPolicy, string)"/> and
/// <see cref="MxcSandbox.RunAsync(SandboxPolicy, string, CancellationToken)"/> expose that
/// warning; streaming processes returned by
/// <see cref="MxcSandbox.Spawn(SandboxPolicy, string)"/> currently do not.
/// </summary>
Allow,
}

/// <summary>
/// Windows ProcessContainer denial-capture settings. The presence of this section enables
/// capture and reports the resulting document through <see cref="SandboxOutputMetadata"/>.
/// </summary>
public sealed class CaptureDenialsPolicy
{
/// <summary>How ungranted access checks are handled while recording them.</summary>
[JsonPropertyName("mode")]
public CaptureDenialsMode Mode { get; set; } = CaptureDenialsMode.Block;

/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("outputPath")]
public string? OutputPath { get; set; }

/// <summary>
/// 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.
/// </summary>
[JsonPropertyName("retainEtl")]
public bool RetainEtl { get; set; }
}

/// <summary>Filesystem section of a <see cref="SandboxPolicy"/>.</summary>
public sealed class FilesystemPolicy
{
Expand Down
51 changes: 49 additions & 2 deletions sdk/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading