Detection rules › Kusto
F&O - Bank account change following network alias reassignment
Identifies changes to user accounts where the network alias was modified to a new value. Shortly afterwards, the updated alias is used to update a bank account number.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation | T1078 Valid Accounts |
| Credential Access | T1556 Modify Authentication Process |
| Persistence | T0859 Valid Accounts |
| Lateral Movement | T0859 Valid Accounts |
Rule body kusto
id: dccbdb5b-2ce7-4931-bfbe-f1ad6523ee64
kind: Scheduled
name: F&O - Bank account change following network alias reassignment
description: Identifies changes to user accounts where the network alias was modified
to a new value. Shortly afterwards, the updated alias is used to update a bank account
number.
severity: Low
status: Available
requiredDataConnectors:
- connectorId: Dynamics365Finance
dataTypes:
- FinanceOperationsActivity_CL
queryFrequency: 15m
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- CredentialAccess
- LateralMovement
- PrivilegeEscalation
relevantTechniques:
- T1556
- T0859
- T1078
query: |
let query_frequency = 15m;
FinanceOperationsActivity_CL
| where LogType == "Update" and TableName == "UserInfo"
| extend UserId = tostring(parse_json(tostring(FormattedData.["03::id"])).NewData)
| extend NetworkAlias = parse_json(tostring(FormattedData.networkAlias))
| extend
CurrentAlias = tostring(NetworkAlias.NewData),
PreviousAlias = tostring(NetworkAlias.OldData)
| where CurrentAlias != PreviousAlias
| extend
AliasUpdated = LogCreatedDateTime,
AliasChangedBy = Username
| join kind=inner(FinanceOperationsActivity_CL
| where TimeGenerated >= ago (query_frequency)
| where LogType == "Update" and TableName == "BankAccountTable"
| extend AccountId = tostring(parse_json(tostring(FormattedData.AccountID)).NewData)
| extend AccountNum = parse_json(tostring(FormattedData.AccountNum))
| extend
CurrentAccountNum = tostring(AccountNum.NewData),
OldAccountNum = tostring(AccountNum.OldData)
| where CurrentAccountNum != OldAccountNum
| extend BankUpdated = LogCreatedDateTime)
on $left.UserId == $right.Username
| where BankUpdated > AliasUpdated
| extend
FinOpsAppId = 32780,
AccountName = tostring(split(CurrentAlias, "@")[0]),
UPNSuffix = tostring(split(CurrentAlias, "@")[1])
| project
AliasUpdated,
AliasChangedBy,
Username,
AccountId,
CurrentAccountNum,
OldAccountNum,
CurrentAlias,
PreviousAlias,
FinOpsAppId,
AccountName,
UPNSuffix
eventGroupingSettings:
aggregationKind: SingleAlert
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: AliasChangedBy
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: UPNSuffix
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Username
alertDetailsOverride:
alertDisplayNameFormat: F&O - Suspicious bank account changes
alertDescriptionFormat: A user account alias was reassigned for {{Username}} by
{{AliasChangedBy}} and shortly afterwards, bank account {{AccountId}} was modified.
version: 3.2.0
Stages and Predicates
Parameters
let query_frequency = 15m;
Stage 1: source
FinanceOperationsActivity_CL
Stage 2: where
| where LogType == "Update" and TableName == "UserInfo"
Stage 3: extend (3 consecutive steps)
| extend UserId = tostring(parse_json(tostring(FormattedData.["03::id"])).NewData)
| extend NetworkAlias = parse_json(tostring(FormattedData.networkAlias))
| extend
CurrentAlias = tostring(NetworkAlias.NewData),
PreviousAlias = tostring(NetworkAlias.OldData)
Stage 4: where
| where CurrentAlias != PreviousAlias
Stage 5: extend
| extend
AliasUpdated = LogCreatedDateTime,
AliasChangedBy = Username
Stage 6: join
| join kind=inner(FinanceOperationsActivity_CL
| where TimeGenerated >= ago (query_frequency)
| where LogType == "Update" and TableName == "BankAccountTable"
| extend AccountId = tostring(parse_json(tostring(FormattedData.AccountID)).NewData)
| extend AccountNum = parse_json(tostring(FormattedData.AccountNum))
| extend
CurrentAccountNum = tostring(AccountNum.NewData),
OldAccountNum = tostring(AccountNum.OldData)
| where CurrentAccountNum != OldAccountNum
| extend BankUpdated = LogCreatedDateTime)
on $left.UserId == $right.Username
Stage 7: where
| where BankUpdated > AliasUpdated
Stage 8: extend
| extend
FinOpsAppId = 32780,
AccountName = tostring(split(CurrentAlias, "@")[0]),
UPNSuffix = tostring(split(CurrentAlias, "@")[1])
Stage 9: project
| project
AliasUpdated,
AliasChangedBy,
Username,
AccountId,
CurrentAccountNum,
OldAccountNum,
CurrentAlias,
PreviousAlias,
FinOpsAppId,
AccountName,
UPNSuffix
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 |
|---|---|---|
BankUpdated | gt |
|
CurrentAccountNum | ne |
|
CurrentAlias | ne |
|
LogType | eq |
|
TableName | eq |
|
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 |
|---|---|
AccountId | project |
AccountName | project |
AliasChangedBy | project |
AliasUpdated | project |
CurrentAccountNum | project |
CurrentAlias | project |
FinOpsAppId | project |
OldAccountNum | project |
PreviousAlias | project |
UPNSuffix | project |
Username | project |