Skip to content
Closed
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
127 changes: 97 additions & 30 deletions docs/learning-mode/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,25 +207,60 @@ sandbox policy:
- Analysis retains at most 10,000 unique denials and processes at most
1,000,000 ETW events. Reaching the unique-denial bound stops adding policy
entries but continues bounded diagnostic accounting; reaching either bound
sets `summary.deniedResourcesTruncated` to `true`.
sets `summary.deniedResourcesTruncated` to `true`. Missing schemas for
supported denial events also set this flag. Incomplete results do not produce
policy previews or adjusted configurations.
- `resource` is the user-visible identifier for the denied resource,
interpreted by `resourceType`: an absolute `C:\…` path for `file`, the
AppContainer **capability name** (e.g. `internetClient`) for `capability`,
and the raw resource identifier otherwise. Well-known capability SIDs are
resolved to their policy name; custom (hashed) capability SIDs that can't be
reversed fall back to the `S-1-15-3-…` SID string. Named Section,
SymbolicLink, and Timer checks are verbose-only because the config has no
corresponding policy grants. Event 28 is
schema-discriminated: UI-shaped `Category`/`Detail` payloads emit `ui`
resources instead of treating the package SID as a capability.
SymbolicLink, Timer, and COM checks are verbose-only because the config has
no corresponding policy grants. Event 28 is schema-discriminated: UI-shaped
`Category`/`Detail` payloads emit `ui` resources instead of treating the
package SID as a capability.
- `resourceType` is one of `file`, `ui`, `network`, `capability`, `other`;
`accessType` is one of `read`, `write`, `execute`, `unknown`. Capability
denials are recorded under `block`; current `allow` traces expose capability
checks as empty-`ObjectType` access events that are omitted because they do
not carry a stable capability identifier.
denials are recorded under `block`; `allow` traces can expose capability
checks as empty-`ObjectType` access events. Known capabilities can be recovered
from their DACL payload; unresolved checks remain verbose diagnostics.
- `filetime` is a decimal string containing the Windows `FILETIME` value, so
JavaScript consumers retain all 64 bits without numeric precision loss.

### COM access checks

On Windows builds with COM Learning Mode metadata, Kernel-General access-check
event 14 reports two additional object types:

| `ObjectType` | Verbose reason | Identifier |
| ------------ | -------------- | ---------- |
| `ComActivationForClass` | `comActivation` | Activation CLSID |
| `ComCallOnInterface` | `comInterfaceCall` | Called interface IID |

Both `captureDenials.mode: "block"` and `captureDenials.mode: "allow"` decode
these records identically. The mode still controls whether the denied operation
remains blocked or is permitted; it does not change the output classification.
MXC validates that the identifier is GUID-shaped.

COM records do not enter the caller-facing `denials` array because MXC does not
expose an authorable COM grant. They are retained in the local verbose sibling
with `resourceType: "other"`, no `accessType`, and the original CLSID/IID in the
sanitized `ObjectName` property. The distinct reasons make COM activation and
interface calls recognizable without treating them as unknown object types or
policy-actionable denials.

The `MXC.VerboseDenials` telemetry projection retains the COM reason and count
but removes all verbose properties, including the CLSID/IID.

This coverage applies to classic COM activation checks instrumented in RPCSS and
COM interface-call checks instrumented in COMBASE. The separate WinRT
`CheckActivationPermissions` path does not currently attach this COM Learning
Mode metadata, so an MXC decoder-only change cannot identify those WinRT
activation denials as `ComActivationForClass`. `RPC Interface` events are
lower-layer LRPC diagnostics and remain `unsupportedObjectType`; they are not a
substitute for the COM permission event.

### Verbose logging event signatures

Every successful decode also writes a deterministic sibling file:
Expand All @@ -235,7 +270,7 @@ policy denial occurrences plus diagnostic outcomes omitted from the policy file:

```json
{
"version": 2,
"version": 3,
"signatures": [
{
"signature": {
Expand Down Expand Up @@ -266,24 +301,33 @@ policy denial occurrences plus diagnostic outcomes omitted from the policy file:
```

Signatures are keyed by symbolic provider category, provider GUID,
provider-scoped event ID, closed outcome reason, PID, and sorted sanitized
properties. SIDs, capability names, GUIDs, PIDs/process identifiers, and
provider-scoped event ID, optional schema `eventName`, closed outcome reason,
PID, and sorted sanitized properties. The schema name distinguishes TraceLogging
events that share ID 0 and is separate from any payload field named `EventName`.
It is sanitized and bounded like other values, and is omitted when unavailable.
SIDs, capability names, GUIDs, PIDs/process identifiers, and
non-file resource values are retained. Complete file paths are replaced with
`<REDACTED>`; standalone user/account names remain replaced with
`<redacted-user>`.
`<REDACTED>`; a path inside a command line or rendered array causes that entire
property value to be redacted. Standalone user/account names remain replaced
with `<redacted-user>`. Properties whose names contain file paths are omitted.
Exact header timestamps and timestamp-like properties are omitted so otherwise
identical events deduplicate, and free-form decoder errors are never serialized.
This is an outcome summary, not an ordered event ledger: repeats become a count,
and one source event can produce several capability-denial outcomes.

Every valid actionable denial is classified as `actionable` in the verbose
file, including its first occurrence, later duplicates, and candidates observed
after the actionable file's unique-denial bound. Those occurrences deduplicate
under the same signature and increment its count. `accessType` and
Every valid policy denial is classified as `actionable` in the verbose file.
Its first occurrence, later duplicates, and candidates observed after the
actionable file's unique-denial bound are all retained. Those occurrences
deduplicate under the same signature and increment its count. `accessType` and
`resourceType` are included when denial extraction determined them; diagnostic
outcomes without those classifications omit the fields.

Candidates excluded from the actionable output retain a closed diagnostic
reason and their sanitized event properties:

- `comActivation` and `comInterfaceCall` identify recognized classic COM
permission decisions. They include `resourceType: "other"` and omit
`accessType`; their CLSID/IID remains only in the local verbose properties.
- `notActionable` includes registry writes, registry checks whose access mask
cannot be classified as a read, and recognized Section, SymbolicLink, and
Timer checks. MXC has no corresponding policy grants, so reporting them in
Expand All @@ -297,34 +341,54 @@ reason and their sanitized event properties:
- `unsupportedObjectType` means the event names a resource outside the
supported diagnostic model. Examples include `\BaseNamedObjects` as a
Directory, ALPC Ports such as
`ubpmtaskhostchannel`, and RPC Interface GUIDs.
`ubpmtaskhostchannel`, and lower-layer RPC Interface GUIDs. The explicit COM
object types documented above are supported and do not use this outcome.

Property values longer than 256 characters retain bounded prefix and suffix
context plus a SHA-256 digest of the complete sanitized value. This keeps long
named-object resources individually identifiable when they share a prefix
without exceeding the per-property bound. Redaction occurs before the digest is computed, so neither retained context nor
a digest is derived from a sensitive value.

Unknown event IDs from known Learning Mode providers are classified as
`unsupportedEventSchema`; the real ETL path retains their provider GUID and
PID without attempting an unsupported TDH payload decode.
Unknown providers and event IDs receive best-effort TDH decoding. Events with no
actionable extractor use `unsupportedEventSchema` and retain their sanitized
properties. Unknown providers use `provider: "other"` with their actual GUID
in the local file. Different provider GUIDs remain separate deduplication keys.
They do not create new actionable policy grants.

Per-event TDH failures use closed diagnostic reasons:
`eventPayloadMalformed` means the payload conflicts with its declared schema,
`decoderLimitReached` means a nesting/element/work safety bound stopped
decoding, and `unsupportedPropertyEncoding` means the decoder cannot consume
that property shape. When TDH exposes it, the schema-declared name is retained
as the bounded `EventName` signature property. Free-form decoder errors are
never serialized. Failure to obtain the event schema remains a fatal analysis
error rather than being represented as a verbose logging signature.
in the optional `eventName` metadata field, with no partial payload properties.
Free-form decoder errors are
never serialized. Failure to obtain the event schema is retained as
`schemaUnavailable` rather than aborting the analysis, and marks actionable
results incomplete when the event belongs to a supported denial schema.
For brokered Event 28, scoped analysis marks the result incomplete even when
the missing schema prevents reading its workload PID; unattributed event
contents are not retained.

To keep diagnostics bounded, verbose logging retains at most 4,096 distinct
signatures, 24 sorted properties per signature, and 256 characters per property
value. `overflowOccurrences` and `aggregateGroupsTruncated` indicate that
signatures and 16 MiB of compact signature data, with 24 sorted properties per
signature and 256 characters per property value.
`overflowOccurrences` and `aggregateGroupsTruncated` indicate that
additional diagnostic groups were omitted. `actionableOverflowOccurrences`
counts omitted actionable-denial occurrences, while `processedEventsTruncated`
indicates that the 1,000,000-event limit prevented complete accounting. The
actionable file itself is never reduced to make room for verbose logging.
The existing 64 MiB guarded analysis frame can also move verbose groups into
overflow accounting. These bounds are retained; the file does not claim complete
event-by-event coverage.

Guarded WPR keeps only events from the exact job-attested process lifetimes,
including brokered capability attribution, regardless of provider. Its generated
relogging header is excluded from analysis so it does not add a diagnostic that
was absent from the source. A schema lookup failure while scoping brokered
Event 28 fails the capture, because its workload PID cannot be established.
Neither the unattributed event nor a potentially incomplete filtered trace is
returned. The host-wide source ETL is not transferred.

The actionable and verbose logging files fail together: MXC stages both and reports
capture failure unless both final artifacts are committed. The verbose logging path
Expand All @@ -336,7 +400,9 @@ When stable telemetry is enabled and authorized, MXC may validate, compact, and
send this redacted verbose document through `Microsoft.MXC/MXC.VerboseDenials`. Each
event contains a valid JSON array of complete signatures and document
reconstruction metadata. Before emission, MXC derives provider GUIDs from the
closed provider enum and drops every verbose property name and value. MXC does
closed provider enum and drops every verbose property name and value. For `other`,
the telemetry GUID is empty. Matching groups are combined again after these
values and the schema `eventName` are removed. MXC does
not send the actionable denials file, workload-derived properties, or raw ETL
through telemetry. See [MXC telemetry](../telemetry/telemetry.md).

Expand Down Expand Up @@ -372,9 +438,10 @@ WPR's source ETL is host-wide, so the elevated guarded-WPR helper never
transfers that file across the privilege boundary for `captureDenials` or
`--audit`. After the sandbox process tree terminates, the helper uses the
retained, job-attested process handles and their exact PID/creation/exit
`FILETIME` ranges to relog a second ETL. The
retained ETL contains only supported Learning Mode events whose event header
falls inside one of those attested process generations. Guarded analysis and
`FILETIME` ranges to relog a second ETL. The retained ETL contains events from
any provider attributed to those process generations, plus required ETL metadata.
Brokered capability events use their payload `ProcessId` rather than the
broker's header PID. Guarded analysis and
retention both consume that same filtered ETL; filtering failure transfers no
trace. The host-wide source remains in protected elevated scratch and is
deleted with that scratch. The unelevated caller writes the filtered retained
Expand Down
2 changes: 2 additions & 0 deletions src/Cargo.lock

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

Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,7 @@ mod tests {
let output_path = directory.path().join("denials.json");
let mut analysis = AnalysisResult::complete(Vec::new());
analysis.verbose_logging.record(VerboseLoggingSignature {
event_name: None,
provider: VerboseLoggingProvider::KernelGeneral,
provider_guid: "{A68CA8B7-004F-D7B6-A698-07E2DE0F1F5D}".to_string(),
event_id: 14,
Expand Down
4 changes: 4 additions & 0 deletions src/core/learning_mode_core/src/analyze.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ mod tests {
let signature = |pid| VerboseLoggingAggregate {
signature: VerboseLoggingSignature {
provider: VerboseLoggingProvider::KernelGeneral,
event_name: None,
provider_guid: "provider".to_string(),
event_id: 14,
reason: VerboseLoggingOutcomeReason::Actionable,
Expand Down Expand Up @@ -351,6 +352,7 @@ mod tests {
signatures: vec![VerboseLoggingAggregate {
signature: VerboseLoggingSignature {
provider: VerboseLoggingProvider::KernelGeneral,
event_name: None,
provider_guid: "provider".to_string(),
event_id: 14,
reason: VerboseLoggingOutcomeReason::Actionable,
Expand Down Expand Up @@ -395,6 +397,7 @@ mod tests {
signature: VerboseLoggingSignature {
provider: VerboseLoggingProvider::KernelGeneral,
provider_guid: "provider".to_string(),
event_name: None,
event_id: 14,
reason: VerboseLoggingOutcomeReason::Actionable,
pid: 1,
Expand Down Expand Up @@ -428,6 +431,7 @@ mod tests {
.map(|pid| VerboseLoggingAggregate {
signature: VerboseLoggingSignature {
provider: VerboseLoggingProvider::KernelGeneral,
event_name: None,
provider_guid: "provider".to_string(),
event_id: 14,
reason: VerboseLoggingOutcomeReason::MissingObjectName,
Expand Down
Loading
Loading