Detection rules › Kusto
Detect changes to Connect Sync Application
This detection flags any changes happening on the Connect Sync Application in Entra ID. Since this is a very interesting account for attackers to abuse when moving laterally between AD DS and Entra ID, any change to the account should be investigated. We try to exclude legitimate certificate renewal processes, and a new account onboarding in the detection rule.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078.004 Valid Accounts: Cloud Accounts |
| Persistence | T1078.004 Valid Accounts: Cloud Accounts, T1098 Account Manipulation, T1556.007 Modify Authentication Process: Hybrid Identity |
| Privilege Escalation | T1078.004 Valid Accounts: Cloud Accounts, T1098 Account Manipulation |
| Stealth | T1078.004 Valid Accounts: Cloud Accounts |
| Defense Impairment | T1556.007 Modify Authentication Process: Hybrid Identity |
| Credential Access | T1556.007 Modify Authentication Process: Hybrid Identity |
References
Event coverage
| Provider | Event | Title |
|---|---|---|
| Entra-AuditLogs | _catch_all | Entra ID audit event (any operation) |
Rule body yaml
// Flag everything except a renewal process and onboarding
let base = materialize (
AuditLogs
// Search events happening on the Sync Account
| extend AppName = tostring(TargetResources[0].displayName)
| where AppName startswith "ConnectSyncProvisioning_"
// Only get cretificate or secret changes
| where OperationName contains "Update application – Certificates and secrets management"
// Expand the target resources and modified properties, and only use events ralted to KeyDescription
| mv-expand TargetResources
| extend ModifiedProperties = TargetResources.modifiedProperties
| mv-expand ModifiedProperties
| where ModifiedProperties.displayName == "KeyDescription"
// Save the old and new values of the secrets on the application
| extend OldValue = tostring(ModifiedProperties.oldValue),
NewValue = tostring(ModifiedProperties.newValue)
// Save the old and new credential names in an array
| extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue),
NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue)
);
let newCredsAdd = (
base
// Flag when there are more new credentials than old ones
| where array_length(OldCredentialNames) < array_length(NewCredentialNames)
| project-rename NewCredAddTimeGenerated = TimeGenerated
);
let credRemove = (
base
// Get events where credentials are being removed
| where array_length(OldCredentialNames) > array_length(NewCredentialNames)
| project-rename CredRemoveTimeGenerated = TimeGenerated
);
let credRenewal = (
// Find legitimate credential renewals
newCredsAdd
| join kind=leftouter credRemove on AppName
| where CredRemoveTimeGenerated - NewCredAddTimeGenerated <= 1m
);
AuditLogs
| extend AppName = tostring(TargetResources[0].displayName)
| where AppName startswith "ConnectSyncProvisioning_"
| join kind=leftanti credRenewal on CorrelationId
// Exclude cred removes (duplicate / not interesting)
| join kind=leftanti credRemove on CorrelationId
// Exclude new deployment
| where OperationName !in ("Add service principal", "Add application")
Stages and Predicates
Let binding: base
let base = materialize (
AuditLogs
| extend AppName = tostring(TargetResources[0].displayName)
| where AppName startswith "ConnectSyncProvisioning_"
| where OperationName contains "Update application – Certificates and secrets management"
| mv-expand TargetResources
| extend ModifiedProperties = TargetResources.modifiedProperties
| mv-expand ModifiedProperties
| where ModifiedProperties.displayName == "KeyDescription"
| extend OldValue = tostring(ModifiedProperties.oldValue),
NewValue = tostring(ModifiedProperties.newValue)
| extend OldCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), OldValue),
NewCredentialNames = extract_all(@"DisplayName=([^\]]+)", dynamic([1]), NewValue)
);
Let binding: newCredsAdd
let newCredsAdd = (
base
| where array_length(OldCredentialNames) < array_length(NewCredentialNames)
| project-rename NewCredAddTimeGenerated = TimeGenerated
);
Derived from base.
Let binding: credRemove
let credRemove = (
base
| where array_length(OldCredentialNames) > array_length(NewCredentialNames)
| project-rename CredRemoveTimeGenerated = TimeGenerated
);
Derived from base.
Let binding: credRenewal
let credRenewal = (
newCredsAdd
| join kind=leftouter credRemove on AppName
| where CredRemoveTimeGenerated - NewCredAddTimeGenerated <= 1m
);
Derived from newCredsAdd, credRemove.
Stage 1: source
AuditLogs
Stage 2: extend
| extend AppName = tostring(TargetResources[0].displayName)
Stage 3: where
| where AppName startswith "ConnectSyncProvisioning_"
Stage 4: join (negated)
| join kind=leftanti credRenewal on CorrelationId
Stage 5: join (negated)
| join kind=leftanti credRemove on CorrelationId
Stage 6: where
| where OperationName !in ("Add service principal", "Add application")
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
AppName | starts_with | ConnectSyncProvisioning_ |
OldCredentialNames | cross_field_compare | NewCredentialNames |
OldCredentialNames | cross_field_compare | NewCredentialNames |
OperationName | contains | Update application – Certificates and secrets management |
displayName | eq | KeyDescription |
AppName | starts_with | ConnectSyncProvisioning_ |
OldCredentialNames | cross_field_compare | NewCredentialNames |
OperationName | contains | Update application – Certificates and secrets management |
displayName | eq | KeyDescription |
OperationName | in | Add application, Add service principal |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
AppName | starts_with |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
AppName | extend |