Skip to content

Adding Permissive Learning Mode Tool#552

Draft
lilybarkley-msft wants to merge 31 commits into
mainfrom
user/lilybarkley/PermissiveLearningModeTool
Draft

Adding Permissive Learning Mode Tool#552
lilybarkley-msft wants to merge 31 commits into
mainfrom
user/lilybarkley/PermissiveLearningModeTool

Conversation

@lilybarkley-msft

@lilybarkley-msft lilybarkley-msft commented Jun 24, 2026

Copy link
Copy Markdown

📖 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/*.ps1 PowerShell scripts.

New crate: core/plm (Windows-only, excluded from default-members)

  • plm start — wraps wpr -start <PLM.wprp>!AccessFailureProfile to begin capture.
  • plm stop — wraps wpr -stop, parses the .etl with EvtQuery/EvtRender, and merges findings into a config. --trace-file allows re-processing an existing .etl without an active WPR session; --log-dir, --bin-path, --adjusted-config-path, --verbose-logging round 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=14 file-access events → filesystem.readwritePaths / readonlyPaths (split by access mask), with capability names resolved from the embedded ACE DACL via DeriveCapabilitySidsFromName.
  • EventID=27 UI violations → classified by Category:
    • CONVERT_TO_GUI (1) flips ui.disable to false.
    • UI_OPERATION (2) accumulates JOB_OBJECT_UILIMIT_* bits into a mask and maps each bit to the right ui.* field per UIPolicy_Schema.md (e.g. WRITECLIPBOARD widens ui.clipboard, HANDLES relaxes ui.isolation from containeratoms→…→desktop, INJECTION sets ui.injection=true). Undecodable payloads are surfaced only in --verbose-logging and never trigger relaxations.
  • Full constant set (CONVERT_TO_GUI, UI_OPERATION, all 10 JOB_OBJECT_UILIMIT_* flags) exported from event_parser.rs with a ui_limit_name() helper for diagnostics.

wxc-exec --audit integration

  • New --audit flag on wxc-exec: injects permissiveLearningMode (gated by a security warning in release), runs the workload in permissive mode, and bookends it with plm start / plm stop --config-path <file> so an Adjusted_*.json lands next to the trace.
  • AppContainer runner now allows permissiveLearningMode when request.audit is set, even in release builds (matched by a security warning).

Test scaffolding

  • New crate testing/plm_tester (the existing learning_mode/Rust_test/ source, integrated into the workspace under its previous package name plmtester) — a PLMTester.exe binary 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.
  • 6 new tests/configs/PLMConfigs/*.json policy configs that pair with the plm_tester subcommands.

Documentation

  • Rewritten src/core/plm/readme.md covering the full pipeline, every CLI flag, three workflow examples, the ETW event reference, the 4-layer event-filtering chain, and known limitations.
  • New Audit Mode subsection in the top-level README.md under Debugging, with a link to the PLM readme.
  • build.bat now wipes sdk/tests/integration/node_modules/@microsoft/mxc-sdk before reinstalling, so the npm file: cache picks up freshly built SDK type definitions instead of a stale pack.

Limitations

  • Windows-only (uses wpr.exe, EvtQuery, DeriveCapabilitySidsFromName, Job-Object UI-limit semantics).
  • Only operations gated by the OS permissive-auditing layer produce events — Win32 access checks outside the AppContainer envelope are invisible.
  • wxc-exec --audit only plumbs file-form configs through to plm stop; base64 configs get the detection summary but no Adjusted_*.json (re-run plm stop --trace-file … manually).
  • Capability resolution is limited to the named list in extract_caps::KNOWN_CAPABILITIES.

🔗 References

🔍 Validation

Build & lint (verified locally on Windows x64):

  • cargo build -p plm --target x86_64-pc-windows-msvc
  • cargo clippy -p plm --target x86_64-pc-windows-msvc --all-targets -- -D warnings
  • cargo fmt -p plm -- --check
  • cargo build -p plmtester --target x86_64-pc-windows-msvc
  • cargo build from the workspace root (default members) still succeeds without pulling plm / plmtester in — preserves cross-platform CI.
  • npm run build in sdk/tests/integration after the build.bat reinstall fix (previously failed with Property 'seatbelt' does not exist on type 'ContainerConfig').

Functional

  • Captured a trace from PLMTester.exe handles (HANDLES violation), confirmed:
    • Summary shows category=0x00000002 (UI_OPERATION) detail=0x00000001 (HANDLES).
    • Adjusted_*.json writes ui.isolation = "atoms" from a default "container" baseline, and "desktop" when applied to a pre-existing "handles" config — both match the schema-table inversion.
  • Confirmed plm stop --trace-file C:\Tessera\temp\trace.etl re-processes an existing .etl without invoking wpr -stop, and that UI-only traces now produce an Adjusted_*.json (previously bailed out before the merge step).
  • Confirmed wxc-exec --audit invokes plm start / plm stop --config-path <file> and that the merged config lands in <exe dir>\logs\<timestamp>\Adjusted_*.json.
  • Confirmed undecodable EventID=27 records are noted only in --verbose-logging output and no longer flip ui.disable.

Manual coverage of UI policy options via the 12 existing configs in tests/configs/base_container_ui_configs/ plus the new tests/configs/PLMConfigs/. Note: ui.isolation = "handles" / "atoms" and ui.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

  • Bug fix
  • Feature
  • Task

GitHub Actions runs the PR validation build automatically. The ADO pipeline
(MXC-PR-Build) is the official build pipeline that signs the binaries; it
runs on merge to main and nightly, and Microsoft reviewers can trigger it
on a PR with /azp run. See docs/pull-requests.md.

Microsoft Reviewers: Open in CodeFlow

Copilot AI review requested due to automatic review settings June 24, 2026 04:43
@lilybarkley-msft lilybarkley-msft requested a review from a team as a code owner June 24, 2026 04:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/plm crate implementing WPR capture (start), ETL parsing/decoding (stop), and interactive workflows (log, extract-caps).
  • wxc-exec --audit support to inject permissiveLearningMode and run plm start/stop around the workload.
  • New testing/plm_tester crate and new tests/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

Comment thread src/core/plm/src/config.rs Outdated
Comment on lines +301 to +306
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 ");
Comment on lines +119 to +128
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
}
Comment thread src/core/plm/Cargo.toml Outdated
Comment on lines +14 to +25
[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",
] }
Comment on lines +10 to +20
[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",
Comment on lines 33 to 40
"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"
}
},
Comment thread tests/configs/aaaa.json Outdated
Comment on lines +1 to +5
{
"version": "0.4.0-alpha",
"containerId": "CLI-BasicProcessContainer-Test",
"containment": "processcontainer",
"process": {
Comment thread tests/configs/PLMConfigs/README.md Outdated
Comment on lines +7 to +9
All configs assume `plmtester.exe` is at
`C:\Users\AdminUser\Desktop\lily\plmtester.exe`.

@bbonaby

bbonaby commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

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.

@lilybarkley-msft lilybarkley-msft marked this pull request as draft June 25, 2026 19:37
lilybarkley-msft added a commit that referenced this pull request Jun 25, 2026
- 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>
lilybarkley-msft added a commit that referenced this pull request Jun 25, 2026
…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>
lilybarkley-msft and others added 20 commits June 26, 2026 16:12
- 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>
@lilybarkley-msft lilybarkley-msft force-pushed the user/lilybarkley/PermissiveLearningModeTool branch from b889e60 to 3675afc Compare June 26, 2026 23:15
@lilybarkley-msft

Copy link
Copy Markdown
Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants