Adding Permissive Learning Mode Tool#552
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new Windows-only Permissive Learning Mode (PLM) Rust tool (plm.exe) plus wxc-exec --audit integration to capture permissive audit events, decode them, and merge required relaxations back into an adjusted MXC config. It also introduces a plmtester harness + sample configs for exercising UI-limit and other surfaces end-to-end.
Changes:
- New
core/plmcrate implementing WPR capture (start), ETL parsing/decoding (stop), and interactive workflows (log,extract-caps). wxc-exec --auditsupport to injectpermissiveLearningModeand runplm start/stoparound the workload.- New
testing/plm_testercrate and newtests/configs/PLMConfigs/*configs plus doc/build updates.
Reviewed changes
Copilot reviewed 38 out of 40 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/configs/PLMConfigs/system_param_set_basic.json | Adds a PLMTester config for SystemParameters/SetSysColors scenario. |
| tests/configs/PLMConfigs/screenshot_basic.json | Adds a PLMTester config for WinRT screenshot capture via picker. |
| tests/configs/PLMConfigs/injection_child_window_basic.json | Adds a PLMTester config for SendInput injection probe. |
| tests/configs/PLMConfigs/find_window_basic.json | Adds a PLMTester config for FindWindow/UI isolation probe. |
| tests/configs/PLMConfigs/display_settings_basic.json | Adds a PLMTester config for ChangeDisplaySettingsW validation. |
| tests/configs/PLMConfigs/clipboard_roundtrip_basic.json | Adds a PLMTester config for clipboard set/get roundtrip. |
| tests/configs/PLMConfigs/README.md | Documents what each PLMConfigs JSON file is intended to test. |
| tests/configs/aaaa.json | Adds an extra standalone processcontainer config (appears to be scratch). |
| src/testing/plm_tester/src/uiisolation.rs | Implements FindWindow-based UI isolation probe. |
| src/testing/plm_tester/src/system_param.rs | Implements SystemParametersInfoW + SetSysColors probe CLI. |
| src/testing/plm_tester/src/screenshot.rs | Implements WinRT GraphicsCapturePicker capture pipeline. |
| src/testing/plm_tester/src/screenshot_simple.rs | Implements GDI-based screenshot via win-screenshot crate. |
| src/testing/plm_tester/src/main.rs | Adds CLI parsing/dispatch for PLMTester subcommands. |
| src/testing/plm_tester/src/injection.rs | Implements SendInput injection probe (ConsoleControl + SetForegroundWindow). |
| src/testing/plm_tester/src/display_settings.rs | Implements ChangeDisplaySettingsW probe with CDS_TEST/--apply. |
| src/testing/plm_tester/src/clipboard.rs | Implements clipboard set/get/roundtrip + diagnostics helpers. |
| src/testing/plm_tester/README.md | Documents PLMTester purpose, usage, and surfaces tested. |
| src/testing/plm_tester/Cargo.toml | Adds plmtester crate dependencies (windows/image/win-screenshot). |
| src/testing/plm_tester/.gitignore | Ignores local target dir for the plm_tester crate. |
| src/core/wxc/src/main.rs | Adds --audit flag, injects permissiveLearningMode, and shells out to plm start/stop. |
| src/core/wxc_common/src/models.rs | Adds audit: bool to ExecutionRequest. |
| src/core/wxc_common/src/config_parser.rs | Adds extra diagnostics around permissiveLearningMode injection/presence; initializes audit default. |
| src/core/plm/src/stop.rs | Implements plm stop: stop/parse trace and merge findings into adjusted config. |
| src/core/plm/src/start.rs | Implements plm start: cancel existing WPR trace and start PLM profile. |
| src/core/plm/src/plm.wprp | Adds WPR profile for permissive learning mode providers. |
| src/core/plm/src/main.rs | Adds plm CLI entry with start/stop/log/extract-caps subcommands. |
| src/core/plm/src/log.rs | Implements interactive trace start/stop and “diff vs blank config” printing. |
| src/core/plm/src/extract_caps.rs | Implements capability SID resolution from DACL ACE blobs. |
| src/core/plm/src/event_parser.rs | Implements ETL event parsing and decoding for EventID 14/27 payloads. |
| src/core/plm/src/config.rs | Implements JSON merge logic for filesystem paths, capabilities, and UI relaxations. |
| src/core/plm/src/access_event.rs | Defines parsed access event struct used by the merge pipeline. |
| src/core/plm/readme.md | Adds comprehensive PLM tool documentation and workflows. |
| src/core/plm/Cargo.toml | Adds the new plm crate to the workspace. |
| src/core/plm/build.rs | Stages plm.wprp next to plm.exe and embeds version info. |
| src/Cargo.toml | Adds core/plm + testing/plm_tester to workspace members; keeps them out of default-members. |
| src/Cargo.lock | Updates lockfile for new crates/dependencies (including new windows/image deps). |
| src/backends/appcontainer/common/src/appcontainer_runner.rs | Allows permissiveLearningMode in release only when request.audit is set (with warning). |
| sdk/tests/integration/package-lock.json | Updates SDK integration test lockfile for new SDK version/build behavior. |
| README.md | Adds top-level “Audit Mode (Permissive Learning Mode)” documentation. |
| build.bat | Adds --with-bfs option, builds/stages plm, and forces SDK dep refresh for integration tests. |
Files not reviewed (1)
- sdk/tests/integration/package-lock.json: Generated file
| if !ui.contains_key("disable") { | ||
| ui.insert("disable".into(), Value::Bool(true)); | ||
| } else { | ||
| ui.insert("disable".into(), Value::Bool(false)); | ||
| } | ||
| println!("Enabling access to GUI subsystem "); |
| fn parent_for_write(file_path: &str) -> Option<String> { | ||
| let p = Path::new(file_path); | ||
| if p.is_file() { | ||
| return p.parent().map(|s| s.to_string_lossy().into_owned()); | ||
| } | ||
| if p.is_dir() { | ||
| return Some(file_path.to_string()); | ||
| } | ||
| None | ||
| } |
| [target.'cfg(target_os = "windows")'.dependencies] | ||
| serde = { workspace = true } | ||
| serde_json = { version = "1", features = ["preserve_order"] } | ||
| chrono = "0.4" | ||
| roxmltree = "0.20" | ||
| windows = { version = "0.58", features = [ | ||
| "Win32_Foundation", | ||
| "Win32_Security", | ||
| "Win32_Security_Authorization", | ||
| "Win32_System_EventLog", | ||
| "Win32_System_Diagnostics_Etw", | ||
| ] } |
| [dependencies] | ||
| anyhow = "1" | ||
| clap = { version = "4", features = ["derive"] } | ||
| image = { version = "0.25", default-features = false, features = ["png"] } | ||
| win-screenshot = "4" | ||
|
|
||
| [dependencies.windows] | ||
| version = "0.58" | ||
| features = [ | ||
| "Foundation", | ||
| "Graphics_Capture", |
| "node_modules/@types/node": { | ||
| "version": "20.19.39", | ||
| "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", | ||
| "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", | ||
| "dev": true, | ||
| "license": "MIT", | ||
| "dependencies": { | ||
| "undici-types": "~6.21.0" | ||
| } | ||
| }, |
| { | ||
| "version": "0.4.0-alpha", | ||
| "containerId": "CLI-BasicProcessContainer-Test", | ||
| "containment": "processcontainer", | ||
| "process": { |
| All configs assume `plmtester.exe` is at | ||
| `C:\Users\AdminUser\Desktop\lily\plmtester.exe`. | ||
|
|
|
Thanks @lilybarkley-msft, the work on the learning mode is greatly appreciated, especially converting what you had in the past to rust! That said, do you think we'd be able to break this one up into multiple vertical PRs? That would help with getting this in faster. Also does this have anything to do with #558 that one mentions being a predecessor for a learning mode orchestrator but I'm not sure if that one and this one are related. |
- config.rs: fix inverted branch in set_ui_subsystem_enabled (always set ui.disable=false on CONVERT_TO_GUI violation, not true when missing). - config.rs: make parent_for_write purely syntactic so trace paths that don't yet exist on the host aren't silently dropped. - plm: switch to workspace windows 0.62 dep + add Win32_System_EventLog; migrate EvtQuery/EvtRender/LocalFree/LookupAccountSidW to Option- wrapped handle signatures. - plmtester: keep windows 0.58 pin (Graphics_Capture/DXGI/clipboard APIs have breaking changes in 0.62; migration tracked separately). - plm_configs: drop hardcoded c:\users\adminuser\desktop\lily path, rely on plmtester.exe being on PATH; update README. - sdk/tests/integration: regenerate package-lock.json with resolved/ integrity entries. - tests/configs/aaaa.json: remove scratch file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sts)
Fixes derived from a 6-axis adversarial review of the PR.
Correctness:
- config.rs: path_starts_with_any is now component-aware (was a literal
string starts_with, which treated C:\\foobar as nested under C:\\foo
and silently mishandled siblings sharing a name prefix). Match now
requires equality or a separator boundary, with trailing-separator
tolerance and case-insensitive comparison.
- config.rs: parent_for_write refuses to widen to a bare drive root.
A write to C:\\hiberfil.sys / C:\\.git previously inserted ''C:\\''
into readwritePaths, granting write to the entire volume; we now
fall back to the file path itself in that case.
- config.rs: update_from_access_events re-checks deniedPaths against
the parent_for_write result. Previously, writing a non-denied
sibling inside a directory holding a denied file silently widened
the parent, transitively granting write to the denied file.
Reliability:
- event_parser.rs: EvtNext loop distinguishes ERROR_NO_MORE_ITEMS
(real EOF) from mid-stream errors. The previous
''next_ok.is_err() || returned == 0'' collapsed both into a silent
break, producing a partial Adjusted_*.json that looked complete.
- event_parser.rs: EVT_HANDLE wrapped in an RAII guard (EvtHandleOwned)
so a panic / early return no longer leaks kernel ETW handles.
- config.rs: initialize_filesystem, resolve_containment_key,
merge_capabilities, set_ui_subsystem_enabled, apply_ui_operation_flags
now return Result and validate JSON node kinds instead of panicking
via unwrap()/expect() on adversarial config shapes
(''ui'': null, ''readwritePaths'': ''C:\\foo'', non-object root, etc.).
stop.rs and log.rs propagate the new errors via ?.
Performance:
- extract_caps.rs: invoke_ace_walk_with_table / extract_caps_with_table
added; event_parser.rs builds the capability table once up-front and
reuses it across the per-event loop. Previously every access event
rebuilt the ~150-entry table (~150 DeriveCapabilitySidsFromName
syscalls + LocalAlloc/LocalFree pairs per event), which dominated
parse time on large traces.
- config.rs: update_from_access_events maintains pre-lowercased shadow
vectors of readwritePaths/readonlyPaths/deniedPaths outside the loop
and only appends to them. The previous code re-cloned the JSON array
and lowercased every element on every event (O(N*M) String allocs).
- event_parser.rs: EvtNext batch size bumped 16 -> 256; render_event_xml
reuses a caller-owned scratch buffer rather than allocating a fresh
Vec<u8> per event.
Tests:
- config.rs: 23 new unit tests covering parent_for_write edge cases
(file vs directory, drive root), path_starts_with_any sibling
rejection, deny-bypass via parent, idempotency on already-writable
paths, read-under-existing-readonly dedup, typed-error rejection of
non-object root / non-object ui / non-array path fields,
set_ui_subsystem_enabled always-false invariant, apply_ui_operation_flags
bit-to-field mapping for clipboard / IME / desktop+exitwindows /
injection (closing uncovered branches), and merge_capabilities
case-insensitive dedup.
Verified: cargo build -p plm, cargo test -p plm (23 passed),
cargo clippy -p plm --all-targets -- -D warnings (clean).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… stop logging when audit mode is passed in
- config.rs: fix inverted branch in set_ui_subsystem_enabled (always set ui.disable=false on CONVERT_TO_GUI violation, not true when missing). - config.rs: make parent_for_write purely syntactic so trace paths that don't yet exist on the host aren't silently dropped. - plm: switch to workspace windows 0.62 dep + add Win32_System_EventLog; migrate EvtQuery/EvtRender/LocalFree/LookupAccountSidW to Option- wrapped handle signatures. - plmtester: keep windows 0.58 pin (Graphics_Capture/DXGI/clipboard APIs have breaking changes in 0.62; migration tracked separately). - plm_configs: drop hardcoded c:\users\adminuser\desktop\lily path, rely on plmtester.exe being on PATH; update README. - sdk/tests/integration: regenerate package-lock.json with resolved/ integrity entries. - tests/configs/aaaa.json: remove scratch file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sts)
Fixes derived from a 6-axis adversarial review of the PR.
Correctness:
- config.rs: path_starts_with_any is now component-aware (was a literal
string starts_with, which treated C:\\foobar as nested under C:\\foo
and silently mishandled siblings sharing a name prefix). Match now
requires equality or a separator boundary, with trailing-separator
tolerance and case-insensitive comparison.
- config.rs: parent_for_write refuses to widen to a bare drive root.
A write to C:\\hiberfil.sys / C:\\.git previously inserted ''C:\\''
into readwritePaths, granting write to the entire volume; we now
fall back to the file path itself in that case.
- config.rs: update_from_access_events re-checks deniedPaths against
the parent_for_write result. Previously, writing a non-denied
sibling inside a directory holding a denied file silently widened
the parent, transitively granting write to the denied file.
Reliability:
- event_parser.rs: EvtNext loop distinguishes ERROR_NO_MORE_ITEMS
(real EOF) from mid-stream errors. The previous
''next_ok.is_err() || returned == 0'' collapsed both into a silent
break, producing a partial Adjusted_*.json that looked complete.
- event_parser.rs: EVT_HANDLE wrapped in an RAII guard (EvtHandleOwned)
so a panic / early return no longer leaks kernel ETW handles.
- config.rs: initialize_filesystem, resolve_containment_key,
merge_capabilities, set_ui_subsystem_enabled, apply_ui_operation_flags
now return Result and validate JSON node kinds instead of panicking
via unwrap()/expect() on adversarial config shapes
(''ui'': null, ''readwritePaths'': ''C:\\foo'', non-object root, etc.).
stop.rs and log.rs propagate the new errors via ?.
Performance:
- extract_caps.rs: invoke_ace_walk_with_table / extract_caps_with_table
added; event_parser.rs builds the capability table once up-front and
reuses it across the per-event loop. Previously every access event
rebuilt the ~150-entry table (~150 DeriveCapabilitySidsFromName
syscalls + LocalAlloc/LocalFree pairs per event), which dominated
parse time on large traces.
- config.rs: update_from_access_events maintains pre-lowercased shadow
vectors of readwritePaths/readonlyPaths/deniedPaths outside the loop
and only appends to them. The previous code re-cloned the JSON array
and lowercased every element on every event (O(N*M) String allocs).
- event_parser.rs: EvtNext batch size bumped 16 -> 256; render_event_xml
reuses a caller-owned scratch buffer rather than allocating a fresh
Vec<u8> per event.
Tests:
- config.rs: 23 new unit tests covering parent_for_write edge cases
(file vs directory, drive root), path_starts_with_any sibling
rejection, deny-bypass via parent, idempotency on already-writable
paths, read-under-existing-readonly dedup, typed-error rejection of
non-object root / non-object ui / non-array path fields,
set_ui_subsystem_enabled always-false invariant, apply_ui_operation_flags
bit-to-field mapping for clipboard / IME / desktop+exitwindows /
injection (closing uncovered branches), and merge_capabilities
case-insensitive dedup.
Verified: cargo build -p plm, cargo test -p plm (23 passed),
cargo clippy -p plm --all-targets -- -D warnings (clean).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
b889e60 to
3675afc
Compare
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
📖 Description
Adds a Permissive Learning Mode (PLM) tool that captures the Windows OS-side permissive sandbox audit events emitted while a workload runs, decodes them into structured findings, and merges those findings back into an MXC container config so the next enforcing run succeeds without changes to the workload. This is a Rust port and significant expansion of the existing
learning_mode/*.ps1PowerShell scripts.New crate:
core/plm(Windows-only, excluded fromdefault-members)plm start— wrapswpr -start <PLM.wprp>!AccessFailureProfileto begin capture.plm stop— wrapswpr -stop, parses the.etlwithEvtQuery/EvtRender, and merges findings into a config.--trace-fileallows re-processing an existing.etlwithout an active WPR session;--log-dir,--bin-path,--adjusted-config-path,--verbose-logginground out the surface.plm log— interactive iteration mode (Enter to start/stop, prints diff vs. blank config).plm extract-caps— standalone capability-ACE decoder for parser debugging.Decoders
EventID=14file-access events →filesystem.readwritePaths/readonlyPaths(split by access mask), with capability names resolved from the embedded ACE DACL viaDeriveCapabilitySidsFromName.EventID=27UI violations → classified byCategory:CONVERT_TO_GUI (1)flipsui.disabletofalse.UI_OPERATION (2)accumulatesJOB_OBJECT_UILIMIT_*bits into a mask and maps each bit to the rightui.*field perUIPolicy_Schema.md(e.g.WRITECLIPBOARDwidensui.clipboard,HANDLESrelaxesui.isolationfromcontainer→atoms→…→desktop,INJECTIONsetsui.injection=true). Undecodable payloads are surfaced only in--verbose-loggingand never trigger relaxations.CONVERT_TO_GUI,UI_OPERATION, all 10JOB_OBJECT_UILIMIT_*flags) exported fromevent_parser.rswith aui_limit_name()helper for diagnostics.wxc-exec --auditintegration--auditflag onwxc-exec: injectspermissiveLearningMode(gated by a security warning in release), runs the workload in permissive mode, and bookends it withplm start/plm stop --config-path <file>so anAdjusted_*.jsonlands next to the trace.permissiveLearningModewhenrequest.auditis set, even in release builds (matched by a security warning).Test scaffolding
testing/plm_tester(the existinglearning_mode/Rust_test/source, integrated into the workspace under its previous package nameplmtester) — aPLMTester.exebinary that deliberately trips each UI limit (clipboard, handles, system parameters, display settings, IME, injection, screenshot/screen capture) for end-to-end validation of the parsing + relaxation pipeline.tests/configs/PLMConfigs/*.jsonpolicy configs that pair with theplm_testersubcommands.Documentation
src/core/plm/readme.mdcovering the full pipeline, every CLI flag, three workflow examples, the ETW event reference, the 4-layer event-filtering chain, and known limitations.README.mdunder Debugging, with a link to the PLM readme.build.batnow wipessdk/tests/integration/node_modules/@microsoft/mxc-sdkbefore reinstalling, so the npmfile:cache picks up freshly built SDK type definitions instead of a stale pack.Limitations
wpr.exe,EvtQuery,DeriveCapabilitySidsFromName, Job-Object UI-limit semantics).wxc-exec --auditonly plumbs file-form configs through toplm stop; base64 configs get the detection summary but noAdjusted_*.json(re-runplm stop --trace-file …manually).extract_caps::KNOWN_CAPABILITIES.🔗 References
docs/base-process-container/UIPolicy_Schema.md— schema mapping consumed byapply_ui_operation_flags.docs/base-process-container/guide.md— process-container backend overview.learning_mode/*.ps1(this PR is the Rust port).🔍 Validation
Build & lint (verified locally on Windows x64):
cargo build -p plm --target x86_64-pc-windows-msvccargo clippy -p plm --target x86_64-pc-windows-msvc --all-targets -- -D warningscargo fmt -p plm -- --checkcargo build -p plmtester --target x86_64-pc-windows-msvccargo buildfrom the workspace root (default members) still succeeds without pullingplm/plmtesterin — preserves cross-platform CI.npm run buildinsdk/tests/integrationafter thebuild.batreinstall fix (previously failed withProperty 'seatbelt' does not exist on type 'ContainerConfig').Functional
PLMTester.exe handles(HANDLES violation), confirmed:category=0x00000002 (UI_OPERATION) detail=0x00000001 (HANDLES).Adjusted_*.jsonwritesui.isolation = "atoms"from a default"container"baseline, and"desktop"when applied to a pre-existing"handles"config — both match the schema-table inversion.plm stop --trace-file C:\Tessera\temp\trace.etlre-processes an existing.etlwithout invokingwpr -stop, and that UI-only traces now produce anAdjusted_*.json(previously bailed out before the merge step).wxc-exec --auditinvokesplm start/plm stop --config-path <file>and that the merged config lands in<exe dir>\logs\<timestamp>\Adjusted_*.json.--verbose-loggingoutput and no longer flipui.disable.Manual coverage of UI policy options via the 12 existing configs in
tests/configs/base_container_ui_configs/plus the newtests/configs/PLMConfigs/. Note:ui.isolation = "handles"/"atoms"andui.systemSettings = "parameters"/"display"are reachable via the relaxation pipeline but don't yet have a direct enforcing-mode test config — happy to add four configs if reviewers want pre-relaxation coverage.✅ Checklist
📋 Issue Type
GitHub Actions runs the PR validation build automatically. The ADO pipeline
(
MXC-PR-Build) is the official build pipeline that signs the binaries; itruns on merge to
mainand nightly, and Microsoft reviewers can trigger iton a PR with
/azp run. See docs/pull-requests.md.Microsoft Reviewers: Open in CodeFlow