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
8 changes: 6 additions & 2 deletions docs/logging-access-denied.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ policy denial occurrences plus diagnostic outcomes omitted from the policy file:

```json
{
"version": 2,
"version": 3,
"signatures": [
{
"signature": {
Expand Down Expand Up @@ -296,6 +296,10 @@ reason and their sanitized event properties:
converted to a safe absolute DOS or UNC path. For example,
`\Device\MountPointManager` is useful Devices-namespace evidence, but it is
not a directly authorable filesystem grant.
- `comActivation` and `comInterfaceCall` mark observed classic COM class
activation and interface-call access checks: denied under `block`, recorded
and allowed under `allow`. The CLSID or IID is retained. A missing identifier
is `missingObjectName`; an invalid one is `eventPayloadMalformed`.
- `unsupportedObjectType` means the event names a resource outside the
supported diagnostic model. Examples include `\BaseNamedObjects` as a
Directory, ALPC Ports such as
Expand All @@ -312,7 +316,7 @@ Unknown event IDs from known Learning Mode providers are classified as
PID without attempting an unsupported TDH payload decode.

Per-event TDH failures use closed diagnostic reasons:
`eventPayloadMalformed` means the payload conflicts with its declared schema,
`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
Expand Down
54 changes: 51 additions & 3 deletions src/mxc-sdk/src/core/learning_mode_core/verbose_logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ pub enum VerboseLoggingOutcomeReason {
Actionable,
/// The provider is known, but the event ID is not a supported denial schema.
UnsupportedEventSchema,
/// The event payload conflicted with its declared TDH schema.
/// The event payload was malformed or conflicted with its declared TDH schema.
EventPayloadMalformed,
/// A decoder safety bound prevented full payload processing.
DecoderLimitReached,
Expand All @@ -51,6 +51,10 @@ pub enum VerboseLoggingOutcomeReason {
UnusableResourcePath,
/// A capability event did not contain a usable capability denial.
UnresolvedCapability,
/// Classic COM class activation.
ComActivation,
/// Classic COM interface call.
ComInterfaceCall,
/// The event was valid but did not describe an actionable denial.
NotActionable,
}
Expand Down Expand Up @@ -303,7 +307,7 @@ pub struct VerboseLoggingDocumentSummary {

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

/// Builds an on-disk document from decoder aggregate state.
#[must_use]
Expand Down Expand Up @@ -466,6 +470,36 @@ mod tests {
assert_eq!(summary.actionable_overflow_occurrences, 0);
}

#[test]
fn com_signature_overflow_is_not_counted_as_actionable() {
let mut summary = VerboseLoggingSummary::default();
let mut signature: VerboseLoggingSignature = serde_json::from_value(serde_json::json!({
"provider": "kernelGeneral",
"providerGuid": "kernel",
"eventId": 0,
"reason": "unsupportedEventSchema",
"pid": 1,
"properties": []
}))
.unwrap();
for event_id in 0..MAX_VERBOSE_LOGGING_GROUPS as u16 {
signature.event_id = event_id;
summary.record(signature.clone());
}

signature.event_id = u16::MAX;
signature.reason = VerboseLoggingOutcomeReason::ComActivation;
signature.resource_type = Some(crate::learning_mode_core::ResourceType::Other);
summary.record(signature);

assert_eq!(summary.overflow_occurrences, 1);
assert_eq!(summary.actionable_overflow_occurrences, 0);
assert!(!summary
.signatures
.iter()
.any(|group| { group.signature.reason == VerboseLoggingOutcomeReason::ComActivation }));
}

#[test]
fn byte_budget_leaves_guarded_analysis_protocol_headroom() {
let mut summary = VerboseLoggingSummary::default();
Expand Down Expand Up @@ -531,12 +565,26 @@ mod tests {
summary.mark_actionable_limit_reached();

let value = serde_json::to_value(VerboseLoggingDocument::new(&summary)).unwrap();
assert_eq!(value["version"], 2);
assert_eq!(value["version"], 3);
assert_eq!(value["signatures"][0]["signature"]["reason"], "actionable");
assert_eq!(value["summary"]["actionableOverflowOccurrences"], 2);
assert_eq!(value["summary"]["actionableLimitReached"], true);
}

#[test]
fn document_serializes_distinct_nonactionable_com_reasons() {
for (reason, expected) in [
(VerboseLoggingOutcomeReason::ComActivation, "comActivation"),
(
VerboseLoggingOutcomeReason::ComInterfaceCall,
"comInterfaceCall",
),
] {
assert!(!reason.is_actionable());
assert_eq!(serde_json::to_value(reason).unwrap(), expected);
}
}

#[test]
fn sibling_path_replaces_the_actionable_extension() {
assert_eq!(
Expand Down
159 changes: 159 additions & 0 deletions src/mxc-sdk/src/core/learning_mode_windows/etl_decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1847,6 +1847,165 @@ mod tests {
assert_eq!(out[2].resource_type, ResourceType::Ui);
}

#[test]
fn com_access_checks_are_distinct_verbose_only_outcomes_in_both_modes() {
Comment thread
jacobdereje-msft marked this conversation as resolved.
let activation_clsid = "{A47979D2-C419-11D9-A5B4-001185AD2B89}";
let call_iid = "{00000132-0000-0000-C000-000000000046}";
let events = vec![
kernel_event(
14,
100,
1,
&[
("Mode", "\"Normal\""),
("ObjectType", "\"ComActivationForClass\""),
("ObjectName", activation_clsid),
("AccessMask", "0x1"),
],
),
permissive_event(
14,
101,
2,
&[
("Mode", "\"Permissive\""),
("ObjectType", "\"ComActivationForClass\""),
("ObjectName", "\"{a47979d2-c419-11d9-a5b4-001185ad2b89}\""),
("AccessMask", "0xffffffff"),
],
),
permissive_event(
4907,
102,
3,
&[
("Mode", "\"Permissive\""),
("ObjectType", "\"ComCallOnInterface\""),
("ObjectName", call_iid),
("AccessMask", "0x2"),
],
),
];

let analysis = resources_from_events(&events);

assert!(analysis.denials.is_empty());
let activation_signatures = analysis
.verbose_logging
.signatures
.iter()
.filter(|group| property(&group.signature, "ObjectType") == "ComActivationForClass")
.collect::<Vec<_>>();
assert_eq!(activation_signatures.len(), 2);
assert!(activation_signatures.iter().all(|group| {
group.signature.reason == VerboseLoggingOutcomeReason::ComActivation
&& group.signature.resource_type == Some(ResourceType::Other)
&& group.signature.access_type.is_none()
&& group.count == 1
}));
assert!(activation_signatures
.iter()
.any(|group| property(&group.signature, "ObjectName") == activation_clsid));
assert!(activation_signatures.iter().any(|group| {
group.signature.provider
== VerboseLoggingProvider::PrivacyAuditingPermissiveLearningMode
}));

let call = analysis
.verbose_logging
.signatures
.iter()
.find(|group| property(&group.signature, "ObjectType") == "ComCallOnInterface")
.expect("COM interface call should remain in verbose logging");
assert_eq!(
call.signature.reason,
VerboseLoggingOutcomeReason::ComInterfaceCall
);
assert_eq!(
call.signature.provider,
VerboseLoggingProvider::PrivacyAuditingPermissiveLearningMode
);
assert_eq!(call.signature.event_id, 4907);
assert_eq!(call.signature.resource_type, Some(ResourceType::Other));
assert!(call.signature.access_type.is_none());
assert_eq!(property(&call.signature, "ObjectName"), call_iid);
}

#[test]
fn com_checks_do_not_change_actionable_output() {
let file = || {
kernel_event(
14,
100,
2,
&[
("ObjectType", "\"File\""),
("ObjectName", r"C:\kept.txt"),
("AccessMask", "0x1"),
],
)
};
let com = |filetime| {
kernel_event(
14,
100,
filetime,
&[
("ObjectType", "\"ComActivationForClass\""),
("ObjectName", "{A47979D2-C419-11D9-A5B4-001185AD2B89}"),
],
)
};

let without = resources_from_events(&[file()]);
let with = resources_from_events(&[com(1), file(), com(3)]);

assert_eq!(without.denials.len(), 1);
assert_eq!(with.denials, without.denials);
assert_eq!(
with.verbose_logging.signatures.len(),
without.verbose_logging.signatures.len() + 1
);
assert_eq!(
with.verbose_logging.total_occurrences,
without.verbose_logging.total_occurrences + 2
);
}

#[test]
fn malformed_com_identifier_remains_classified_verbose_diagnostic() {
for object_name in [r"C:\Users\alice\secret.txt", "alice@example.com"] {
let events = vec![kernel_event(
14,
42,
1,
&[
("Mode", "\"Normal\""),
("ObjectType", "\"ComActivationForClass\""),
("ObjectName", object_name),
("AccessMask", "0x1"),
],
)];

let analysis = resources_from_events(&events);

assert!(analysis.denials.is_empty());
assert_eq!(analysis.verbose_logging.signatures.len(), 1);
let signature = &analysis.verbose_logging.signatures[0].signature;
assert_eq!(
signature.reason,
VerboseLoggingOutcomeReason::EventPayloadMalformed
);
assert_eq!(signature.resource_type, Some(ResourceType::Other));
assert!(signature.access_type.is_none());
assert_eq!(property(signature, "ObjectType"), "ComActivationForClass");
assert_eq!(
property(signature, "ObjectName"),
crate::learning_mode_windows::extractors::REDACTED_PATH
);
}
}

#[test]
fn unidentified_capability_events_are_omitted() {
let events = vec![
Expand Down
Loading
Loading