Skip to content
Merged
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
3 changes: 2 additions & 1 deletion docs/development/architecture/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ Emitted when a telemetry-enabled ProcessContainer run successfully produces a
Learning Mode `captureDenials` verbose logging artifact. MXC reads the
versioned `*.verbose.json` sibling, validates it as a
`VerboseLoggingDocument`, derives each provider GUID from the document's
closed provider enum, drops every verbose property name and value, and
closed provider enum, drops every verbose property name and value and the
schema name, sums the counts of signatures that become identical, and
serializes that telemetry-specific projection as compact JSON. The event never
contains the actionable denials file, raw ETL, commands, sandbox output, or
general logger text.
Expand Down
30 changes: 19 additions & 11 deletions docs/logging-access-denied.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,9 @@ sandbox policy:
`summary.totalDenials` equals `denials.length`.
- 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`.
entries but continues bounded diagnostic accounting; reaching either bound, or
failing to read a non-network event's schema, sets
`summary.deniedResourcesTruncated` to `true`.
- `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`,
Expand Down Expand Up @@ -237,7 +238,7 @@ policy denial occurrences plus diagnostic outcomes omitted from the policy file:

```json
{
"version": 3,
"version": 4,
"signatures": [
{
"signature": {
Expand Down Expand Up @@ -268,8 +269,8 @@ 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, schema name, closed outcome reason, PID, and sorted
sanitized properties. 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>`.
Expand Down Expand Up @@ -311,18 +312,25 @@ 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.
Only Learning Mode events are decoded: Kernel-General events 14, 27, and 28,
PermissiveLearningMode events 14, 27, and 4907, and NetworkDecision event 1.
Comment thread
jacobdereje-msft marked this conversation as resolved.
Other provider and event ID pairs are ignored. `unsupportedEventSchema` means
the event has no actionable extractor. NetworkDecision records are kept only by
unscoped analysis, with PID 0 and that reason; the local file keeps their
sanitized properties, including remote endpoints, while telemetry drops them.

Per-event TDH failures use closed diagnostic reasons:
`eventPayloadMalformed` means the payload is malformed or 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.
as the bounded `eventName` signature field. Free-form decoder errors are
never serialized. `schemaUnavailable` means the event schema could not be
obtained. Analysis continues but sets `deniedResourcesTruncated` because the
unreadable event may have been a denial; network decisions are not denials, so
they do not. Schema failures
remain fatal for raw decoding and for scoping brokered capability events in
guarded traces.

To keep diagnostics bounded, verbose logging retains at most 4,096 distinct
signatures, 24 sorted properties per signature, and 256 characters per property
Expand Down
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/mxc-sdk/src/core/learning_mode_core/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 @@ -429,6 +432,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
46 changes: 44 additions & 2 deletions src/mxc-sdk/src/core/learning_mode_core/verbose_logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ pub enum VerboseLoggingProvider {
KernelGeneral,
/// Microsoft-Windows-Privacy-Auditing-PermissiveLearningMode.
PrivacyAuditingPermissiveLearningMode,
/// Microsoft-Windows-LearningMode-NetworkDecision.
LearningModeNetworkDecision,
Comment thread
jacobdereje-msft marked this conversation as resolved.
}

/// Closed reason describing how a decoder outcome was handled.
Expand All @@ -35,6 +37,8 @@ pub enum VerboseLoggingOutcomeReason {
Actionable,
/// The provider is known, but the event ID is not a supported denial schema.
UnsupportedEventSchema,
/// TDH could not resolve the event schema.
SchemaUnavailable,
/// The event payload was malformed or conflicted with its declared TDH schema.
EventPayloadMalformed,
/// A decoder safety bound prevented full payload processing.
Expand Down Expand Up @@ -77,6 +81,9 @@ pub struct VerboseLoggingSignature {
pub provider_guid: String,
/// Provider-scoped ETW schema identifier.
pub event_id: u16,
/// Sanitized schema name.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub event_name: Option<String>,
/// Closed exclusion category.
pub reason: VerboseLoggingOutcomeReason,
/// Process identifier from the event header.
Expand Down Expand Up @@ -307,7 +314,7 @@ pub struct VerboseLoggingDocumentSummary {

impl VerboseLoggingDocument {
/// Current verbose logging document schema version.
pub const VERSION: u32 = 3;
pub const VERSION: u32 = 4;
Comment thread
jacobdereje-msft marked this conversation as resolved.

/// Builds an on-disk document from decoder aggregate state.
#[must_use]
Expand Down Expand Up @@ -375,6 +382,7 @@ mod tests {
fn aggregates_and_sorts_sanitized_signatures() {
let mut summary = VerboseLoggingSummary::default();
let signature = VerboseLoggingSignature {
event_name: None,
provider: VerboseLoggingProvider::KernelGeneral,
provider_guid: "{A68CA8B7-004F-D7B6-A698-07E2DE0F1F5D}".to_string(),
event_id: 14,
Expand Down Expand Up @@ -411,6 +419,7 @@ mod tests {
for event_id in 0..MAX_VERBOSE_LOGGING_GROUPS as u16 {
summary.record(VerboseLoggingSignature {
provider: VerboseLoggingProvider::KernelGeneral,
event_name: None,
provider_guid: "kernel".to_string(),
event_id,
reason: VerboseLoggingOutcomeReason::UnsupportedEventSchema,
Expand All @@ -422,6 +431,7 @@ mod tests {
}
summary.record(VerboseLoggingSignature {
provider: VerboseLoggingProvider::PrivacyAuditingPermissiveLearningMode,
event_name: None,
provider_guid: "privacy".to_string(),
event_id: u16::MAX,
reason: VerboseLoggingOutcomeReason::UnsupportedEventSchema,
Expand All @@ -443,6 +453,7 @@ mod tests {
summary.record(VerboseLoggingSignature {
provider: VerboseLoggingProvider::KernelGeneral,
provider_guid: "kernel".to_string(),
event_name: None,
event_id,
reason: VerboseLoggingOutcomeReason::UnsupportedEventSchema,
pid: 1,
Expand All @@ -456,6 +467,7 @@ mod tests {
provider_guid: "kernel".to_string(),
event_id: u16::MAX,
reason: VerboseLoggingOutcomeReason::Actionable,
event_name: None,
pid: 1,
access_type: Some(crate::learning_mode_core::AccessType::Read),
resource_type: Some(crate::learning_mode_core::ResourceType::File),
Expand Down Expand Up @@ -510,6 +522,7 @@ mod tests {
for pid in 0..MAX_VERBOSE_LOGGING_GROUPS as u32 {
summary.record_with_byte_budget(
VerboseLoggingSignature {
event_name: None,
provider: VerboseLoggingProvider::KernelGeneral,
provider_guid: "{A68CA8B7-004F-D7B6-A698-07E2DE0F1F5D}".to_string(),
event_id: 14,
Expand Down Expand Up @@ -548,13 +561,42 @@ mod tests {
assert_eq!(parsed, document);
}

#[test]
fn schema_name_is_optional_and_separate_from_payload() {
let old = serde_json::json!({
"provider": "learningModeNetworkDecision",
"providerGuid": "provider",
"eventId": 0,
"reason": "schemaUnavailable",
"pid": 42,
"properties": [["EventName", "payload-name"]]
});
let mut signature: VerboseLoggingSignature = serde_json::from_value(old).unwrap();
assert!(signature.event_name.is_none());
assert!(serde_json::to_value(&signature)
.unwrap()
.get("eventName")
.is_none());
signature.event_name = Some("schema-name".into());
let json = serde_json::to_value(&signature).unwrap();
assert_eq!(json["provider"], "learningModeNetworkDecision");
assert_eq!(json["reason"], "schemaUnavailable");
assert_eq!(json["eventName"], "schema-name");
assert_eq!(json["properties"][0][1], "payload-name");
assert_eq!(
serde_json::from_value::<VerboseLoggingSignature>(json).unwrap(),
signature
);
}

#[test]
fn document_uses_actionable_vocabulary() {
let mut summary = VerboseLoggingSummary::default();
summary.record(VerboseLoggingSignature {
provider: VerboseLoggingProvider::KernelGeneral,
provider_guid: "kernel".to_string(),
event_id: 14,
event_name: None,
reason: VerboseLoggingOutcomeReason::Actionable,
pid: 1,
access_type: Some(crate::learning_mode_core::AccessType::Read),
Expand All @@ -565,7 +607,7 @@ mod tests {
summary.mark_actionable_limit_reached();

let value = serde_json::to_value(VerboseLoggingDocument::new(&summary)).unwrap();
assert_eq!(value["version"], 3);
assert_eq!(value["version"], 4);
assert_eq!(value["signatures"][0]["signature"]["reason"], "actionable");
assert_eq!(value["summary"]["actionableOverflowOccurrences"], 2);
assert_eq!(value["summary"]["actionableLimitReached"], true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,7 @@ mod tests {
fn parts(name: &str, value: String) -> DecodedEventParts {
DecodedEventParts {
provider: PRIVACY_LEARNING_MODE_PROVIDER,
event_name: None,
event_id: ACCESS_CHECK_EVENT_ID,
props: vec![
("ObjectType".to_string(), "\"\"".to_string()),
Expand Down Expand Up @@ -854,6 +855,7 @@ mod tests {
#[test]
fn ignores_non_permissive_provider() {
let event = DecodedEventParts {
event_name: None,
provider: GUID::zeroed(),
event_id: ACCESS_CHECK_EVENT_ID,
props: Vec::new(),
Expand Down
Loading
Loading