Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
8c0f5b7
PLM split PR1: audit-mode plumbing and trace lifecycle
lilybarkley-msft Jun 27, 2026
6e255c2
Remove round-N review markers from CI workflow comments
lilybarkley-msft Jun 27, 2026
3e0478c
PLM CI: restrict integration tests to filesystem fixtures (headless)
lilybarkley-msft Jun 27, 2026
a7f10e2
PLM PR1 fix #1: build plm cross-platform via no-op stub main
lilybarkley-msft Jun 30, 2026
7d7f4a8
PLM PR1 review fixes: quiet --audit, w! macro, chrono scope, WPR race
lilybarkley-msft Jul 2, 2026
94de54b
PLM PR1 review batch 2 fixes: comment cleanup, MIT headers, workspace…
lilybarkley-msft Jul 2, 2026
18fe0fd
Gate plm_ctrl_handler on target_os = "windows"
lilybarkley-msft Jul 2, 2026
72606c7
Regenerate dev schema to match wire model
lilybarkley-msft Jul 2, 2026
f09b340
Soften README claim about plm stop parsing (not yet wired on this PR)
lilybarkley-msft Jul 2, 2026
badb6b2
Move --with-bfs to filesystem PR
lilybarkley-msft Jul 2, 2026
5222d79
Drop debug_assertions branch on permissiveLearningMode gate
lilybarkley-msft Jul 2, 2026
7b1dcbe
Shorten audit-mode README section; link to plm readme
lilybarkley-msft Jul 2, 2026
0e25d9b
Byte-verify staged plm.wprp against embedded profile before rename
lilybarkley-msft Jul 2, 2026
2da4ff1
Verify wpr.exe Authenticode signature before every plm invocation
lilybarkley-msft Jul 2, 2026
64d1838
Remove inline PLM integration test step from Build.Windows.Job
lilybarkley-msft Jul 2, 2026
a980d44
Drop serde_json preserve_order feature
lilybarkley-msft Jul 2, 2026
16fd29c
Remove Tessera codename from docs
lilybarkley-msft Jul 2, 2026
5424928
plm: address PR584 review β€” capture wpr/plm stdio, harden singleton, …
lilybarkley-msft Jul 2, 2026
6e46795
plm: move PLM_TRACE_ACTIVE ownership onto AcquiredSingleton
lilybarkley-msft Jul 2, 2026
d277890
plm: replace WinVerifyTrust with existence check for wpr.exe
lilybarkley-msft Jul 2, 2026
af003bb
plm: suppress `wpr -stop` progress bar in `plm log` happy path
lilybarkley-msft Jul 2, 2026
cdf6d21
plm: spawn wpr.exe with CREATE_NO_WINDOW to suppress its progress bar
lilybarkley-msft Jul 2, 2026
0fd93c4
plm(pr1): remove forward-looking future-PR language in module docs
lilybarkley-msft Jul 3, 2026
2feb58e
wxc-exec: auto-elevate --audit via UAC self-relaunch
lilybarkley-msft Jul 3, 2026
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
9 changes: 9 additions & 0 deletions .azure-pipelines/templates/Rust.Build.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,15 @@ jobs:
displayName: Build linux-test-proxy
condition: eq('${{ item.os }}', 'linux')

# plm (Permissive Learning Mode) is functionally Windows-only, but it
# builds cross-platform via a no-op stub `fn main()` so it sits in the
# workspace default-members. Tests run on x64 only (arm64 emulation in
# 1ES windows-11-arm is too slow for the WPR-touching helpers).
- script: |
cargo test --release --target $(triplet) -p plm --manifest-path $(Build.SourcesDirectory)/src/Cargo.toml
displayName: Test plm
condition: and(eq('${{ item.os }}', 'windows'), eq('${{ item.arch }}', 'x64'))
Comment thread
MGudgin marked this conversation as resolved.

- task: EsrpCodeSigning@5
displayName: Code Sign
condition: and(eq(${{ parameters.isOfficialBuild }}, true), eq('${{ item.os }}', 'windows'))
Expand Down
15 changes: 15 additions & 0 deletions .github/workflows/Build.Linux.Job.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ jobs:
run: cargo test --locked --release --target ${{ matrix.target }}
-p wxc_e2e_tests

# PLM (Permissive Learning Mode) is functionally Windows-only, but the
# crate builds cross-platform: the lib's helper modules compile on every
# target, and the binary has a no-op stub `fn main()` for non-Windows so
# the crate lives in the workspace `default-members` list. This gate
# keeps the cross-platform/Windows-only split a CI-enforced contract β€” if
# someone adds a `use windows::…` without a `#[cfg]` gate the Linux
# build fails fast rather than rotting silently.
- name: Build plm (cross-platform stub + lib)
working-directory: src
run: cargo build --locked --release --target ${{ matrix.target }} -p plm

- name: Test plm (cross-platform modules)
working-directory: src
run: cargo test --locked --release --target ${{ matrix.target }} -p plm
Comment thread
lilybarkley-msft marked this conversation as resolved.

# linux_test_proxy is a separate workspace member, not a dep of lxc.
- name: Build linux-test-proxy
working-directory: src
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,16 @@ wxc-exec.exe --debug config.json

See [docs/diagnostics.md](docs/diagnostics.md) for full diagnostics reference.

### Audit Mode (Permissive Learning Mode)
Comment thread
lilybarkley-msft marked this conversation as resolved.

`--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/core/plm/readme.md](src/core/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 …`).

```bash
wxc-exec.exe --audit policy.json
```

> **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.

## Documentation

| Document | Description |
Expand Down
16 changes: 16 additions & 0 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ if "%BUILD_ALL%"=="0" if "%BUILD_ARCH%"=="" (
:: Build flags
set "CARGO_FLAGS=--target"
if "%BUILD_CONFIG%"=="release" set "CARGO_FLAGS=--release --target"
:: plm is a standalone Windows-only binary that does not consume any of the
:: workspace feature flags above, so it uses its own profile/target-only flags.
set "PLM_FLAGS=--target"
if "%BUILD_CONFIG%"=="release" set "PLM_FLAGS=--release --target"
if "%WITH_NANVIX%"=="1" set "CARGO_FLAGS=--features microvm %CARGO_FLAGS%"
if "%WITH_WSLC%"=="1" set "CARGO_FLAGS=--features wslc %CARGO_FLAGS%"
if "%WITH_ISOLATION_SESSION%"=="1" set "CARGO_FLAGS=--features isolation_session %CARGO_FLAGS%"
Expand All @@ -66,11 +70,14 @@ if not errorlevel 1 (
if "%BUILD_ALL%"=="1" (
echo Target: x86_64-pc-windows-msvc
cargo build %CARGO_FLAGS% x86_64-pc-windows-msvc || goto :error
cargo build -p plm %PLM_FLAGS% x86_64-pc-windows-msvc || goto :error
echo Target: aarch64-pc-windows-msvc
cargo build %CARGO_FLAGS% aarch64-pc-windows-msvc || goto :error
cargo build -p plm %PLM_FLAGS% aarch64-pc-windows-msvc || goto :error
) else (
echo Target: %BUILD_ARCH%
cargo build %CARGO_FLAGS% %BUILD_ARCH% || goto :error
cargo build -p plm %PLM_FLAGS% %BUILD_ARCH% || goto :error
)
echo Check formatting
cargo fmt --all -- --check || goto :error
Expand Down Expand Up @@ -108,6 +115,10 @@ for %%T in (x86_64-pc-windows-msvc aarch64-pc-windows-msvc) do (
copy /Y "!BIN_DIR!\wxc-host-prep.exe" "sdk\bin\!SDK_ARCH!\" >nul
echo Copied !SDK_ARCH!\wxc-host-prep.exe
)
if exist "!BIN_DIR!\plm.exe" (
copy /Y "!BIN_DIR!\plm.exe" "sdk\bin\!SDK_ARCH!\" >nul
echo Copied !SDK_ARCH!\plm.exe
)
if "%WITH_NANVIX%"=="1" (
for %%B in (nanvixd.exe nanvix_rootfs.img python3.initrd) do (
if exist "!BIN_DIR!\%%B" (
Expand Down Expand Up @@ -147,6 +158,11 @@ popd
echo.
echo Building SDK integration tests...
pushd sdk\tests\integration
:: npm caches `file:` deps by package.json version. The local SDK version
:: rarely bumps between builds, so a plain `npm install` keeps reusing the
:: stale packed copy. Force a refresh of the @microsoft/mxc-sdk link so
:: type-checking sees the dist we just rebuilt above.
if exist node_modules\@microsoft\mxc-sdk rmdir /s /q node_modules\@microsoft\mxc-sdk
call npm install & call npm run build
popd

Expand Down
20 changes: 10 additions & 10 deletions sdk/tests/integration/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions src/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"core/mxc-sdk",
"core/mxc_pty",
"core/mxc_build_common",
"core/plm",
Comment thread
MGudgin marked this conversation as resolved.
"core/generated/base_container_specification",
"backends/appcontainer/common",
"backends/windows_sandbox/daemon",
Expand Down Expand Up @@ -82,6 +83,8 @@ windows = { version = "0.62", features = [
"Win32_System_SystemServices",
"Win32_System_SystemInformation",
"Win32_System_JobObjects",
"Win32_Security_WinTrust",
"Win32_Security_Cryptography",
] }
windows-core = "0.62"
serde = { version = "1", features = ["derive"] }
Expand All @@ -90,6 +93,8 @@ thiserror = "2"
anyhow = "1"
base64 = "0.22"
clap = { version = "4", features = ["derive"] }
chrono = { version = "0.4", default-features = false, features = ["std", "clock"] }
roxmltree = "0.20"
wxc_common = { path = "core/wxc_common" }
appcontainer_common = { path = "backends/appcontainer/common" }
windows_sandbox_common = { path = "backends/windows_sandbox/common" }
Expand Down
10 changes: 3 additions & 7 deletions src/backends/appcontainer/common/src/appcontainer_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -442,19 +442,15 @@ impl AppContainerScriptRunner {
// --- Validate permissiveLearningMode ---
for cap in &request.policy.capabilities {
if cap == "permissiveLearningMode" {
#[cfg(debug_assertions)]
{
if request.audit {
logger.log_line("*** SECURITY WARNING ***");
logger.log_line(
"permissiveLearningMode is ENABLED. \
Container will learn and record access patterns.",
);
}
#[cfg(not(debug_assertions))]
{
} else {
return Err(WxcError::Validation(
"SECURITY: permissiveLearningMode not allowed in release builds"
.to_string(),
"SECURITY: permissiveLearningMode requires --audit".to_string(),
));
}
}
Expand Down
36 changes: 36 additions & 0 deletions src/core/plm/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "plm"
version = "0.1.0"
edition.workspace = true

[lib]
name = "plm"
path = "src/lib.rs"

[[bin]]
name = "plm"
path = "src/main.rs"

[dependencies]
clap.workspace = true
anyhow.workspace = true
# Portable deps (config / access_event / event_parser) must compile on
# every target so their unit tests run in cross-platform CI. The
# `windows` crate stays target-gated below.
serde_json.workspace = true
chrono.workspace = true
roxmltree.workspace = true

[target.'cfg(target_os = "windows")'.dependencies]
windows = { workspace = true, features = [
"Win32_System_EventLog",
"Win32_System_SystemInformation",
"Win32_System_Threading",
] }
wxc_common = { workspace = true }

[build-dependencies]
mxc_build_common.workspace = true

[dev-dependencies]
tempfile.workspace = true
8 changes: 8 additions & 0 deletions src/core/plm/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Build script for plm β€” embeds Windows VersionInfo.

fn main() {
mxc_build_common::embed_version_info("MXC permissive learning mode", "plm.exe");
}
83 changes: 83 additions & 0 deletions src/core/plm/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# PLM β€” Permissive Learning Mode

`plm.exe` is the Windows-only trace driver for permissive learning mode. Long-form, it captures the access-denied events emitted by Windows' permissive sandbox layer, decodes them into structured findings, and merges those findings back into an MXC container config so the next enforcing run succeeds.

This PR introduces the **trace-lifecycle skeleton only**: WPR start/stop, the host-wide singleton mutex, the embedded `plm.wprp` materializer, and the `wxc-exec --audit` plumbing. Event parsing, capability extraction, filesystem/UI merging, and the adjusted-config writer arrive in subsequent PRs.

PLM is invoked automatically by [`wxc-exec --audit`](../../../README.md#audit-mode-permissive-learning-mode); the standalone CLI documented here is for capturing traces, interactive iteration, and (later) debugging the parser itself.

## How it works (skeleton)

1. **Capture** β€” `plm start` calls `wpr -start <plm.wprp>!AccessFailureProfile -filemode`, enabling the `Microsoft-Windows-Privacy-Auditing-PermissiveLearningMode` and `Microsoft-Windows-Kernel-General` ETW providers in a secure realtime collector.
2. **Run** β€” the operator runs the workload. The OS-side permissive sandbox logs `EventID=14` / `EventID=27` for every access that *would* have been denied.
3. **Stop** β€” `plm stop` calls `wpr -stop <trace.etl>` and records the captured trace location.
4. **Parse / Merge** β€” *(arrives in later PRs)* the `.etl` is walked with `EvtQuery` / `EvtRender` and findings are merged into a copy of the input config as `Adjusted_<name>.json`.

## Layout (this PR)

| File | Role |
|-----------------------|-------------------------------------------------------------------------------------|
| `src/main.rs` | `clap` dispatch for `plm start` / `plm stop` / `plm log` (`extract-caps` lands later) |
| `src/start.rs` | `wpr -cancel` (best-effort) + `wpr -start …!AccessFailureProfile -filemode` |
| `src/stop.rs` | `wpr -stop` (or skip with `--trace-file`); parse + merge arrive in later PRs |
| `src/log.rs` | Interactive mode: Enter to start, Enter to stop; preview arrives in later PRs |
| `src/coordination.rs` | Cross-process singleton named-mutex + bypass-env-var coordination for `plm log` |
| `src/wpr_path.rs` | Resolves `wpr.exe` to its absolute `%SystemRoot%\System32` path (PATH-spoof-safe) |
| `src/profile_gen.rs` | Inline WPR profile (`EMBEDDED_WPRP`) + run-time writer that drops `plm.wprp` next to `plm.exe` when missing |

## CLI

### `plm start`

Cancels any in-progress WPR session and starts a new permissive-learning-mode trace.

```powershell
plm.exe start [--wprp <path>]
```

| Flag | Default | Purpose |
|------------|------------------------|---------------------------------------------------------------|
| `--wprp` | `<exe dir>\plm.wprp` | Override the WPR profile path. By default `plm` materializes its embedded profile next to the exe on first use; an existing `plm.wprp` is never overwritten, so operator hand-edits are preserved. |

### `plm stop`

Stops the active trace (or accepts a previously captured one).

```powershell
plm.exe stop [--config-path <path>] [--log-dir <path>] [--bin-path <path>]
[--adjusted-config-path <path>] [--trace-file <path>]
[--verbose-logging]
```

`--config-path` / `--adjusted-config-path` are accepted today so `wxc-exec --audit` can pass them through; the merge that consumes them arrives in subsequent PRs.

### `plm log`

Interactive iteration mode: press Enter to start a trace, run the workload, press Enter again to stop. The "diff against a blank config" preview arrives in later PRs.

```powershell
plm.exe log [--wprp <path>] [--verbose-logging]
```

## Building

PLM is part of the MXC workspace but excluded from `default-members` because it's Windows-only. Build it explicitly:

```powershell
cd C:\src\mxc\src
cargo build -p plm --target x86_64-pc-windows-msvc
# or for release:
cargo build -p plm --target x86_64-pc-windows-msvc --release
```

The WPR profile is embedded into `plm.exe` itself (see `src/profile_gen.rs`); on first use of `plm start` / `plm log`, `profile_gen::ensure_wprp_next_to_exe` writes it to disk next to the binary if no `plm.wprp` is already present. `build.bat` from the repo root builds `plm.exe` and stages it next to `wxc-exec.exe` for the `--audit` integration.

## Limitations

- **Windows-only.** Uses `wpr.exe` and Job-Object UI-limit semantics that have no portable equivalent.
- **No parse-and-merge yet.** `plm stop` writes the captured `.etl` to the log directory but does not yet produce an `Adjusted_*.json`. Later PRs add file-path extraction, capability extraction, UI-policy extraction, and the adjusted-config writer.

## See also

- [`docs/base-process-container/guide.md`](../../../docs/base-process-container/guide.md) β€” process-container backend overview
- [README β†’ Debugging β†’ Audit Mode](../../../README.md#audit-mode-permissive-learning-mode) β€” `wxc-exec --audit` integration
Loading
Loading