Skip to content

Commit 500ed40

Browse files
richiemsftCopilot
andauthored
Inject and validate the two learning-mode capabilities (#622)
* Inject and validate the two learning-mode capabilities Recognize learningMode (deny-and-record) and permissiveLearningMode (audit / allow-all) as distinct learning-mode capabilities in the AppContainer runner (shared by classic AppContainer and BaseContainer). Add a LearningModeCapability enum plus a learning_mode_capability_diagnostics helper that emits an informational note for learningMode and a security warning for permissiveLearningMode. Permissive mode is now allowed in every build, including release, since it is a legitimate auditing tool; it only warns rather than hard-erroring. This replaces the previous release-build rejection of permissiveLearningMode. Capability injection itself rides the existing generic capability -> SID plumbing, so no new injection path is needed. Add unit tests for classification and diagnostics, and document the two capabilities in docs/learning-mode/capabilities.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Honor both learning-mode capabilities in all build configs The config parser previously stripped permissiveLearningMode in release builds and mapped the processContainer.learningMode boolean to permissive (debug only). This contradicted the runner, which now permits permissiveLearningMode in every build and only warns. Stop stripping permissiveLearningMode in release, and map the learningMode boolean to the deny-and-record learningMode capability in every build (restrictions stay enforced; denials are recorded). Update the parser unit tests to cover both capabilities across build configs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Address PR #622 review: honor release security guarantees for permissiveLearningMode Reverts the config-path weakening flagged by review and hardens the permissiveLearningMode gate across both AppContainer-family backends. - config_parser: restore the release-build strip of permissiveLearningMode requested via the config `capabilities` array (case-insensitive). The learningMode boolean still maps to the safe deny-and-record capability in all builds. permissiveLearningMode is reachable only via --audit. - appcontainer_runner / base_container_runner: match the capability case-insensitively (Windows derives the SID case-insensitively) and share a single permissive_learning_mode_requires_audit() gate so BaseContainer now emits the same diagnostics and enforces the same --audit requirement as AppContainer (previously it passed permissive through silently). - main: make the --audit injection dedup check case-insensitive. - docs + dev schema: document that permissiveLearningMode is --audit-only and rejected from config in release; correct the learningMode description. - tests: release-only strip tests (incl. mis-cased), case-insensitive classification, and --audit gate tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Regenerate SDK wire types for learningMode doc change The wire.rs learning_mode doc comment feeds the generated SDK TypeScript wire types; regenerate sdk/src/generated/wire.ts to satisfy the SDK wire-types codegen gate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Use correct OS capability token learningModeLogging for deny-and-record The deny-and-record learning-mode capability was named learningMode, but the OS token that actually produces block-and-log behavior is learningModeLogging (a bare learningMode token yields no logged denials, VM-verified). Rename the recognized capability string, the config learningMode-boolean mapping, diagnostics, tests, and docs accordingly. The processContainer.learningMode config field and permissiveLearningMode are unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Make permissiveLearningMode available in release (drop --audit gate) Permissive learning mode is now on parity with learningModeLogging: it is accepted from the config capabilities array in every build and the runners enforce it via the capability string, emitting a security warning. Removes the runner-side --audit gate at both AppContainer-family runners, the release-build config strip, and the now-unused request.audit model field. --audit is unchanged as the developer inner-loop flow (injects permissiveLearningMode + drives the WPR/ETW PLM trace). Documents the three learning-mode flows in capabilities.md and updates the README --audit warning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Surface permissive learning mode warnings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec * Reserve learning mode capability entry points Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec * Document reserved learning mode capabilities Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec * Expose deny-and-record learning mode in Node SDK Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec * Migrate reserved learning mode config fixtures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec * Restrict audit mode to ProcessContainer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec * Update learning mode capture names Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec * Reject comma-packed capability entries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: dfea4d42-e8a5-42ef-978a-6434e8bf89ec
1 parent 3b910d9 commit 500ed40

24 files changed

Lines changed: 766 additions & 135 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,13 @@ See [docs/diagnostics.md](docs/diagnostics.md) for full diagnostics reference.
219219

220220
### Audit Mode (Permissive Learning Mode)
221221

222-
`--audit` runs the policy in **permissive** mode — denied operations are logged but allowed to proceed — and starts a Permissive Learning Mode (PLM) ETW trace alongside the workload. See [src/host/plm/readme.md](src/host/plm/readme.md) for the full PLM tool reference, including standalone `plm.exe` invocation (e.g. re-processing an existing `.etl` with `plm stop --trace-file …`).
222+
`--audit` runs a Windows **ProcessContainer** policy in permissive mode — denied operations are logged but allowed to proceed — and starts a Permissive Learning Mode (PLM) ETW trace alongside the workload. It is rejected for Windows Sandbox, WSLC, IsolationSession, and every other containment backend because those paths do not honor the AppContainer learning-mode capability. It is the developer inner-loop flow for discovering the capabilities and paths a workload needs. See [src/host/plm/readme.md](src/host/plm/readme.md) for the full PLM tool reference, including standalone `plm.exe` invocation (e.g. re-processing an existing `.etl` with `plm stop --trace-file …`).
223223

224224
```bash
225225
wxc-exec.exe --audit policy.json
226226
```
227227

228-
> **Warning:** In release builds, `--audit` relaxes the rejection of `permissiveLearningMode` — AppContainer restrictions are **not** enforced for the duration of the run. Use only for policy authoring.
228+
> **Warning:** `--audit` injects `permissiveLearningMode` — AppContainer restrictions are **not** enforced for the duration of the run. Use only for policy authoring. `learningModeLogging` and `permissiveLearningMode` are reserved internal capability names and are rejected in `processContainer.capabilities`; use `processContainer.learningMode: true` for deny-and-record mode or `--audit` for permissive mode. See [docs/learning-mode/capabilities.md](docs/learning-mode/capabilities.md) for the three learning-mode flows.
229229
230230
## Telemetry (Experimental)
231231

docs/learning-mode/capabilities.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Learning-mode capabilities
2+
3+
MXC sandboxes are **deny-by-default**: when a workload touches a file, registry
4+
key, or other resource the policy does not grant, the access is blocked and the
5+
OS returns the usual "Access is denied" error. For non-trivial workloads this is
6+
operationally fragile — the author must enumerate every path the workload will
7+
ever touch up front, or hand the operator a stack trace and ask them to guess.
8+
9+
**Learning mode** turns those denied accesses into observable events. It is
10+
enabled per-run through two Windows-specific policy capabilities. These
11+
capabilities are the *inputs* to learning mode; the machinery that collects and
12+
surfaces the resulting denial events is layered on top in later work.
13+
14+
> **Platform support.** Learning-mode capabilities are **Windows-only** and
15+
> apply to the AppContainer-based backends (classic AppContainer and
16+
> BaseContainer, which share `backends/appcontainer/common`). On other platforms
17+
> the capability strings are ignored.
18+
19+
## The two capabilities
20+
21+
The two capabilities are **semantically distinct and must not be conflated**:
22+
23+
| Capability | Behavior | Enforcement |
24+
| ----------------------- | ----------------------------------------------------- | ------------------------------------ |
25+
| `learningModeLogging` | Logs every **failed** access check (deny-and-record). | **Unchanged** — accesses stay denied. |
26+
| `permissiveLearningMode`| Logs **every** access check and **allows** it (audit / allow-all). | **Weakened** — the container no longer enforces deny-by-default. |
27+
28+
### `learningModeLogging` — deny-and-record
29+
30+
The OS records each access check that *would have been denied*, but the access
31+
is **still denied**. Containment is unchanged, so this is safe to use as a
32+
diagnostic aid: the workload behaves exactly as it would without learning mode,
33+
while producing a record of what it tried and failed to reach.
34+
35+
### `permissiveLearningMode` — audit / allow-all
36+
37+
The OS records **every** access check and **allows** it. This is an audit mode:
38+
it answers "what would this workload touch if nothing were blocked?" but it does
39+
so by **not enforcing deny-by-default** for the duration of the run.
40+
41+
Because it relaxes containment, `permissiveLearningMode` is **security-sensitive**:
42+
whenever it is present, both the AppContainer and BaseContainer runners emit an
43+
always-visible **security warning** on the host's stderr. In-process Rust callers
44+
can also inspect it through `Sandbox::warnings()` or `Output::warnings`. It is a
45+
reserved internal capability enabled by the dedicated audit/capture entry points.
46+
47+
The parser rejects both learning-mode capability names in
48+
`processContainer.capabilities`, case-insensitively. This prevents a policy from
49+
selecting contradictory modes or bypassing the security-sensitive entry points.
50+
51+
## How to enable them
52+
53+
Enable deny-and-record through the dedicated `learningMode` setting:
54+
55+
```jsonc
56+
{
57+
"processContainer": {
58+
"learningMode": true
59+
}
60+
}
61+
```
62+
63+
Enable permissive audit mode through the CLI:
64+
65+
```text
66+
wxc-exec --audit --config <config>
67+
```
68+
69+
These entry points inject the reserved capability strings internally; users
70+
must not add them directly to `processContainer.capabilities`.
71+
When either learning-mode capability is in effect the runner emits a diagnostic
72+
describing the mode (informational logging for `learningModeLogging`, an
73+
always-visible stderr security warning for `permissiveLearningMode`).
74+
75+
## Three learning-mode flows
76+
77+
Learning-mode telemetry is consumed through three distinct flows. They differ in
78+
*who* runs them, *how* the capability is supplied, and *whether* deny-by-default
79+
stays enforced:
80+
81+
| Flow | Audience | Entry point | Enforcement |
82+
| ---- | -------- | ----------- | ----------- |
83+
| **Developer inner-loop** | The author bringing a workload up | `--audit` CLI flag | Relaxed (allow-all) |
84+
| **App / user-configurable** | Apps that let end users tune their own config | `captureDenials` (`mode: "block"`) / `learningModeLogging` | Enforced (deny-and-record) |
85+
| **Fleet auditing** | IT admins | `captureDenials` (`mode: "allow"`) / `permissiveLearningMode` | Relaxed (allow-all) |
86+
87+
1. **Developer inner-loop (`--audit`).** A developer runs `wxc-exec --audit`
88+
with ProcessContainer containment to discover the capabilities and paths
89+
their process needs. `--audit` is rejected for every other Windows backend.
90+
It triggers UAC, injects `permissiveLearningMode`, and drives a WPR/ETW
91+
permissive-learning-mode trace for the run. This is typically a static
92+
config the developer iterates on locally.
93+
94+
```
95+
wxc-exec --audit --config <config>
96+
```
97+
98+
2. **App / user-configurable (`captureDenials` block / `learningModeLogging`).**
99+
An app wants to let its users "configure" their own sandbox. Each user
100+
workflow differs, so the app records what was blocked, presents it through its
101+
own UX, and re-generates the config with the new paths/capabilities.
102+
Deny-by-default stays enforced — the workload behaves exactly as it would in
103+
production while the denials are recorded.
104+
105+
3. **Fleet auditing (`captureDenials` allow / `permissiveLearningMode`).**
106+
IT admins audit access checks across a fleet by running MXC instances in
107+
permissive learning mode. This flow does **not** trigger UAC: the capability
108+
is supplied through config and takes effect directly, allowing and recording
109+
every access check.
110+
111+
## Relationship to denial capture
112+
113+
Injecting these capabilities makes the OS *emit* learning-mode events. The
114+
Windows-only `captureDenials` config switch drives collecting those events and
115+
surfacing the resulting denials to the caller. Its `mode` selects how each
116+
ungranted access is handled while it is recorded:
117+
118+
- `mode: "block"` (default) maps onto `learningModeLogging`
119+
(deny-and-record) — the app / user-configurable flow.
120+
- `mode: "allow"` maps onto `permissiveLearningMode` (allow-and-record)
121+
— the fleet-auditing flow.
122+
123+
The capture pipeline is delivered incrementally and is documented separately as
124+
it lands.

schemas/dev/mxc-config.schema.0.8.0-dev.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@
681681
"description": "ProcessContainer-specific settings.",
682682
"properties": {
683683
"capabilities": {
684-
"description": "AppContainer capabilities (e.g. `internetClient`, `registryRead`).",
684+
"description": "AppContainer capabilities (e.g. `internetClient`, `registryRead`). Each array entry must contain exactly one capability name; commas are rejected because BaseContainer uses commas as its wire delimiter. `learningModeLogging` and `permissiveLearningMode` are reserved and rejected here; use `learningMode`, `--audit`, or the dedicated denial capture configuration instead.",
685685
"items": {
686686
"type": "string"
687687
},
@@ -702,7 +702,7 @@
702702
"description": "Windows denial capture. When present, the runner records the sandboxed process's access attempts to a learning-mode ETL trace for later inspection. Requires a host that exposes the learning-mode OS API."
703703
},
704704
"learningMode": {
705-
"description": "AppContainer permissive learning mode.",
705+
"description": "AppContainer learning mode (deny-and-record): failed access checks are logged for diagnostics while the accesses stay denied; containment is unchanged. Distinct from the allow-all `permissiveLearningMode` capability, which is injected internally by the `--audit` CLI flag or dedicated denial-capture configuration.",
706706
"type": [
707707
"boolean",
708708
"null"

scripts/versioning/validate-configs.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ const exemptions = existsSync(exemptionsPath)
4242

4343
const ajv = new Ajv({ allErrors: true, strict: false });
4444
const validate = ajv.compile(schema);
45+
const RESERVED_LEARNING_MODE_CAPABILITIES = [
46+
"learningModeLogging",
47+
"permissiveLearningMode",
48+
];
4549

4650
// Recursively collect repo-root-relative paths of *.json files under `dir`, so
4751
// configs in nested directories are not silently skipped.
@@ -107,6 +111,24 @@ for (const rel of files) {
107111
.map((e) => ` ${e.instancePath || "/"} ${e.message}`)
108112
.join("\n");
109113
unexpectedInvalidDetails.push(`${relNorm}:\n${msgs}`);
114+
} else if (ok && !isExempt) {
115+
const processContainer = data.processContainer ?? data.appContainer;
116+
const capabilities = processContainer?.capabilities;
117+
if (Array.isArray(capabilities)) {
118+
const reserved = capabilities.find(
119+
(capability) =>
120+
typeof capability === "string" &&
121+
RESERVED_LEARNING_MODE_CAPABILITIES.some(
122+
(name) => name.toLowerCase() === capability.toLowerCase()
123+
)
124+
);
125+
if (reserved !== undefined) {
126+
unexpectedInvalid++;
127+
unexpectedInvalidDetails.push(
128+
`${relNorm}: processContainer.capabilities contains reserved learning-mode capability '${reserved}'`
129+
);
130+
}
131+
}
110132
}
111133
}
112134

sdk/node/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,12 @@ Backend-specific tuning lives on the returned `ContainerConfig`. The full set of
217217

218218
Open the schema file matching your `policy.version` (e.g. `mxc-config.schema.0.6.0-alpha.json`) and look up `processContainer`, `lxc`, `experimental.wslc`, `experimental.windows_sandbox`, etc.
219219

220+
For Windows ProcessContainer configs, `processContainer.learningMode: true`
221+
enables deny-and-record learning mode: failed accesses are logged but remain
222+
denied. The internal `learningModeLogging` and `permissiveLearningMode`
223+
capability names are reserved and must not be added directly to
224+
`processContainer.capabilities`.
225+
220226
</details>
221227

222228
## State-Aware Sandboxes

sdk/node/src/generated/wire.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,15 +315,15 @@ export interface Process {
315315
*/
316316
export interface ProcessContainer {
317317
/**
318-
* AppContainer capabilities (e.g. `internetClient`, `registryRead`).
318+
* AppContainer capabilities (e.g. `internetClient`, `registryRead`). Each array entry must contain exactly one capability name; commas are rejected because BaseContainer uses commas as its wire delimiter. `learningModeLogging` and `permissiveLearningMode` are reserved and rejected here; use `learningMode`, `--audit`, or the dedicated denial capture configuration instead.
319319
*/
320320
capabilities?: string[] | null;
321321
/**
322322
* Windows denial capture. When present, the runner records the sandboxed process's access attempts to a learning-mode ETL trace for later inspection. Requires a host that exposes the learning-mode OS API.
323323
*/
324324
captureDenials?: CaptureDenials | null;
325325
/**
326-
* AppContainer permissive learning mode.
326+
* AppContainer learning mode (deny-and-record): failed access checks are logged for diagnostics while the accesses stay denied; containment is unchanged. Distinct from the allow-all `permissiveLearningMode` capability, which is injected internally by the `--audit` CLI flag or dedicated denial-capture configuration.
327327
*/
328328
learningMode?: boolean | null;
329329
/**

sdk/node/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
* MXC SDK - TypeScript SDK for Microsoft eXecution Containers
66
*
77
* This package provides a Node.js interface for spawning sandboxed containers.
8+
* For direct Windows ProcessContainer configs, set
9+
* `processContainer.learningMode: true` to enable deny-and-record learning
10+
* mode. Learning-mode capability names are reserved and must not be supplied
11+
* directly in `processContainer.capabilities`.
812
*
913
* @example
1014
* ```typescript

sdk/node/src/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,16 @@ export interface ProcessContainerConfig {
156156
name?: string;
157157
/** Use least privilege mode with PROCESS_CREATION_ALL_APPLICATION_PACKAGES_OPT_OUT (default: false) */
158158
leastPrivilege?: boolean;
159-
/** Additional AppContainer capabilities (e.g., "registryRead", "internetClient") */
159+
/**
160+
* Enable deny-and-record learning mode. Failed access checks are logged while
161+
* accesses remain denied and containment remains enforced.
162+
*/
163+
learningMode?: boolean;
164+
/**
165+
* Additional AppContainer capabilities (e.g., "registryRead", "internetClient").
166+
* Each entry must contain one capability name and must not contain a comma.
167+
* The reserved learning-mode capability names must not be supplied directly.
168+
*/
160169
capabilities?: string[];
161170
/** BaseProcess-specific UI settings (Windows only) */
162171
ui?: BaseProcessUiConfig;

sdk/node/tests/unit/wire-conformance.test.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -199,14 +199,8 @@ type _WslcWireKeys = AssertTrue<Equivalent<OnlyInWire<WslcConfig, WireWslc>, nev
199199
type _PortMappingWireKeys = AssertTrue<Equivalent<OnlyInWire<PublicPortMapping, WirePortMapping>, never>>;
200200
type _LxcWireKeys = AssertTrue<Equivalent<OnlyInWire<LxcConfig, WireLxc>, never>>;
201201

202-
// `processContainer.learningMode` and `processContainer.captureDenials` are wire
203-
// fields the SDK does not yet expose (the AppContainer learning-mode features are
204-
// not surfaced through the policy API).
205202
type _ProcessContainerWireKeys = AssertTrue<
206-
Equivalent<
207-
OnlyInWire<ProcessContainerConfig, WireProcessContainer>,
208-
'learningMode' | 'captureDenials'
209-
>
203+
Equivalent<OnlyInWire<ProcessContainerConfig, WireProcessContainer>, 'captureDenials'>
210204
>;
211205

212206
// `seatbelt.guiAccess` and `seatbelt.launchMethod` are wire fields the one-shot

0 commit comments

Comments
 (0)