Affected rows stateful anomaly on database
Id | 2a632013-379d-4993-956f-615063d31e10 |
Rulename | Affected rows stateful anomaly on database |
Description | Goal: To detect anomalous data change/deletion. This query detects SQL queries that changed/deleted a large number of rows, which is significantly higher than normal for this database. The detection is calculated inside recent time window (defined by ‘detectionWindow’ parameter), and the anomaly is calculated based on previous training window (defined by ’trainingWindow’ parameter). The user can set the minimal threshold for anomaly by changing the threshold parameters volThresholdZ and volThresholdQ (higher threshold will detect only more severe anomalies). |
Severity | Medium |
Tactics | Impact |
Techniques | T1485 T1565 T1491 |
Required data connectors | AzureSql |
Kind | Scheduled |
Query frequency | 1h |
Query period | 14d |
Trigger threshold | 0 |
Trigger operator | gt |
Source Uri | https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Azure SQL Database solution for sentinel/Analytic Rules/Detection-VolumeAffectedRowsStatefulAnomalyOnDatabase.yaml |
Version | 1.1.2 |
Arm template | 2a632013-379d-4993-956f-615063d31e10.json |
let volumeThresholdZ = 3.0; // Minimal threshold for the Zscore to trigger anomaly (number of standard deviations above mean). If set higher, only very significant alerts will fire.
let volumeThresholdQ = volumeThresholdZ; // Minimal threshold for the Qscore to trigger anomaly (number of Inter-Percentile Ranges above high percentile). If set higher, only very significant alerts will fire.
let volumeThresholdHardcoded = 500; // Minimal value for the volume metric to trigger anomaly.
let detectionWindow = 1h; // The size of the recent detection window for detecting anomalies.
let trainingWindow = detectionWindow + 14d; // The size of the training window before the detection window for learning the normal state.
let monitoredColumn = 'AffectedRows'; // The name of the column for volumetric anomalies.
let processedData = materialize (
AzureDiagnostics
| where TimeGenerated >= ago(trainingWindow)
| where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM") // Keep only SQL affected rows
| project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
ResponseRows = response_rows_d, Statement = statement_s
| extend QuantityColumn = column_ifexists(monitoredColumn, 0)
| extend WindowType = case( TimeGenerated >= ago(detectionWindow), 'detection',
(ago(trainingWindow) <= TimeGenerated and TimeGenerated < ago(detectionWindow)), 'training', 'other')
| where WindowType in ('detection', 'training'));
let trainingSet =
processedData
| where WindowType == 'training'
| summarize AvgVal = round(avg(QuantityColumn), 2), StdVal = round(stdev(QuantityColumn), 2), N = count(),
P99Val = round(percentile(QuantityColumn, 99), 2), P50Val = round(percentile(QuantityColumn, 50), 2)
by Database;
processedData
| where WindowType == 'detection'
| join kind = inner (trainingSet) on Database
| extend ZScoreVal = iff(N >= 20, round(todouble(QuantityColumn - AvgVal) / todouble(StdVal + 1), 2), 0.00),
QScoreVal = iff(N >= 20, round(todouble(QuantityColumn - P99Val) / todouble(P99Val - P50Val + 1), 2), 0.00)
| extend IsVolumeAnomalyOnVal = iff((ZScoreVal > volumeThresholdZ and QScoreVal > volumeThresholdQ and QuantityColumn > volumeThresholdHardcoded), true, false), AnomalyScore = round((ZScoreVal + QScoreVal)/2, 0)
| project TimeGenerated, Database, PrincipalName, ClientIp, HostName, ApplicationName, ActionName, Statement,
IsSuccess, ResponseRows, AffectedRows, IsVolumeAnomalyOnVal, AnomalyScore,ResourceId
| where IsVolumeAnomalyOnVal == 'true'
| sort by AnomalyScore desc, TimeGenerated desc
| extend Name = tostring(split(PrincipalName,'@',0)[0]), UPNSuffix = tostring(split(PrincipalName,'@',1)[0])
relevantTechniques:
- T1485
- T1565
- T1491
name: Affected rows stateful anomaly on database
requiredDataConnectors:
- dataTypes:
- AzureDiagnostics
connectorId: AzureSql
entityMappings:
- fieldMappings:
- identifier: Name
columnName: Name
- identifier: UPNSuffix
columnName: UPNSuffix
entityType: Account
- fieldMappings:
- identifier: Address
columnName: ClientIp
entityType: IP
- fieldMappings:
- identifier: HostName
columnName: HostName
entityType: Host
- fieldMappings:
- identifier: Name
columnName: ApplicationName
entityType: CloudApplication
- fieldMappings:
- identifier: ResourceId
columnName: ResourceId
entityType: AzureResource
triggerThreshold: 0
id: 2a632013-379d-4993-956f-615063d31e10
tactics:
- Impact
version: 1.1.2
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Azure SQL Database solution for sentinel/Analytic Rules/Detection-VolumeAffectedRowsStatefulAnomalyOnDatabase.yaml
queryPeriod: 14d
kind: Scheduled
tags:
- SQL
queryFrequency: 1h
severity: Medium
status: Available
description: |
'Goal: To detect anomalous data change/deletion. This query detects SQL queries that changed/deleted a large number of rows, which is significantly higher than normal for this database.
The detection is calculated inside recent time window (defined by 'detectionWindow' parameter), and the anomaly is calculated based on previous training window (defined by 'trainingWindow' parameter). The user can set the minimal threshold for anomaly by changing the threshold parameters volThresholdZ and volThresholdQ (higher threshold will detect only more severe anomalies).'
query: |
let volumeThresholdZ = 3.0; // Minimal threshold for the Zscore to trigger anomaly (number of standard deviations above mean). If set higher, only very significant alerts will fire.
let volumeThresholdQ = volumeThresholdZ; // Minimal threshold for the Qscore to trigger anomaly (number of Inter-Percentile Ranges above high percentile). If set higher, only very significant alerts will fire.
let volumeThresholdHardcoded = 500; // Minimal value for the volume metric to trigger anomaly.
let detectionWindow = 1h; // The size of the recent detection window for detecting anomalies.
let trainingWindow = detectionWindow + 14d; // The size of the training window before the detection window for learning the normal state.
let monitoredColumn = 'AffectedRows'; // The name of the column for volumetric anomalies.
let processedData = materialize (
AzureDiagnostics
| where TimeGenerated >= ago(trainingWindow)
| where Category == 'SQLSecurityAuditEvents' and action_id_s has_any ("RCM", "BCM") // Keep only SQL affected rows
| project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,
ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),
IsSuccess = succeeded_s, AffectedRows = affected_rows_d,
ResponseRows = response_rows_d, Statement = statement_s
| extend QuantityColumn = column_ifexists(monitoredColumn, 0)
| extend WindowType = case( TimeGenerated >= ago(detectionWindow), 'detection',
(ago(trainingWindow) <= TimeGenerated and TimeGenerated < ago(detectionWindow)), 'training', 'other')
| where WindowType in ('detection', 'training'));
let trainingSet =
processedData
| where WindowType == 'training'
| summarize AvgVal = round(avg(QuantityColumn), 2), StdVal = round(stdev(QuantityColumn), 2), N = count(),
P99Val = round(percentile(QuantityColumn, 99), 2), P50Val = round(percentile(QuantityColumn, 50), 2)
by Database;
processedData
| where WindowType == 'detection'
| join kind = inner (trainingSet) on Database
| extend ZScoreVal = iff(N >= 20, round(todouble(QuantityColumn - AvgVal) / todouble(StdVal + 1), 2), 0.00),
QScoreVal = iff(N >= 20, round(todouble(QuantityColumn - P99Val) / todouble(P99Val - P50Val + 1), 2), 0.00)
| extend IsVolumeAnomalyOnVal = iff((ZScoreVal > volumeThresholdZ and QScoreVal > volumeThresholdQ and QuantityColumn > volumeThresholdHardcoded), true, false), AnomalyScore = round((ZScoreVal + QScoreVal)/2, 0)
| project TimeGenerated, Database, PrincipalName, ClientIp, HostName, ApplicationName, ActionName, Statement,
IsSuccess, ResponseRows, AffectedRows, IsVolumeAnomalyOnVal, AnomalyScore,ResourceId
| where IsVolumeAnomalyOnVal == 'true'
| sort by AnomalyScore desc, TimeGenerated desc
| extend Name = tostring(split(PrincipalName,'@',0)[0]), UPNSuffix = tostring(split(PrincipalName,'@',1)[0])
triggerOperator: gt
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"workspace": {
"type": "String"
}
},
"resources": [
{
"apiVersion": "2024-01-01-preview",
"id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/2a632013-379d-4993-956f-615063d31e10')]",
"kind": "Scheduled",
"name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/2a632013-379d-4993-956f-615063d31e10')]",
"properties": {
"alertRuleTemplateName": "2a632013-379d-4993-956f-615063d31e10",
"customDetails": null,
"description": "'Goal: To detect anomalous data change/deletion. This query detects SQL queries that changed/deleted a large number of rows, which is significantly higher than normal for this database.\nThe detection is calculated inside recent time window (defined by 'detectionWindow' parameter), and the anomaly is calculated based on previous training window (defined by 'trainingWindow' parameter). The user can set the minimal threshold for anomaly by changing the threshold parameters volThresholdZ and volThresholdQ (higher threshold will detect only more severe anomalies).'\n",
"displayName": "Affected rows stateful anomaly on database",
"enabled": true,
"entityMappings": [
{
"entityType": "Account",
"fieldMappings": [
{
"columnName": "Name",
"identifier": "Name"
},
{
"columnName": "UPNSuffix",
"identifier": "UPNSuffix"
}
]
},
{
"entityType": "IP",
"fieldMappings": [
{
"columnName": "ClientIp",
"identifier": "Address"
}
]
},
{
"entityType": "Host",
"fieldMappings": [
{
"columnName": "HostName",
"identifier": "HostName"
}
]
},
{
"entityType": "CloudApplication",
"fieldMappings": [
{
"columnName": "ApplicationName",
"identifier": "Name"
}
]
},
{
"entityType": "AzureResource",
"fieldMappings": [
{
"columnName": "ResourceId",
"identifier": "ResourceId"
}
]
}
],
"OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Azure SQL Database solution for sentinel/Analytic Rules/Detection-VolumeAffectedRowsStatefulAnomalyOnDatabase.yaml",
"query": "let volumeThresholdZ = 3.0; // Minimal threshold for the Zscore to trigger anomaly (number of standard deviations above mean). If set higher, only very significant alerts will fire.\nlet volumeThresholdQ = volumeThresholdZ; // Minimal threshold for the Qscore to trigger anomaly (number of Inter-Percentile Ranges above high percentile). If set higher, only very significant alerts will fire.\nlet volumeThresholdHardcoded = 500; // Minimal value for the volume metric to trigger anomaly.\nlet detectionWindow = 1h; // The size of the recent detection window for detecting anomalies. \nlet trainingWindow = detectionWindow + 14d; // The size of the training window before the detection window for learning the normal state.\nlet monitoredColumn = 'AffectedRows'; // The name of the column for volumetric anomalies.\nlet processedData = materialize (\n AzureDiagnostics\n | where TimeGenerated >= ago(trainingWindow)\n | where Category == 'SQLSecurityAuditEvents' and action_id_s has_any (\"RCM\", \"BCM\") // Keep only SQL affected rows\n | project TimeGenerated, PrincipalName = server_principal_name_s, ClientIp = client_ip_s, HostName = host_name_s, ResourceId,\n ApplicationName = application_name_s, ActionName = action_name_s, Database = strcat(LogicalServerName_s, '/', database_name_s),\n IsSuccess = succeeded_s, AffectedRows = affected_rows_d,\n ResponseRows = response_rows_d, Statement = statement_s\n | extend QuantityColumn = column_ifexists(monitoredColumn, 0)\n | extend WindowType = case( TimeGenerated >= ago(detectionWindow), 'detection',\n (ago(trainingWindow) <= TimeGenerated and TimeGenerated < ago(detectionWindow)), 'training', 'other')\n | where WindowType in ('detection', 'training'));\nlet trainingSet =\n processedData\n | where WindowType == 'training'\n | summarize AvgVal = round(avg(QuantityColumn), 2), StdVal = round(stdev(QuantityColumn), 2), N = count(),\n P99Val = round(percentile(QuantityColumn, 99), 2), P50Val = round(percentile(QuantityColumn, 50), 2)\n by Database;\nprocessedData\n| where WindowType == 'detection'\n| join kind = inner (trainingSet) on Database\n| extend ZScoreVal = iff(N >= 20, round(todouble(QuantityColumn - AvgVal) / todouble(StdVal + 1), 2), 0.00),\n QScoreVal = iff(N >= 20, round(todouble(QuantityColumn - P99Val) / todouble(P99Val - P50Val + 1), 2), 0.00)\n| extend IsVolumeAnomalyOnVal = iff((ZScoreVal > volumeThresholdZ and QScoreVal > volumeThresholdQ and QuantityColumn > volumeThresholdHardcoded), true, false), AnomalyScore = round((ZScoreVal + QScoreVal)/2, 0)\n| project TimeGenerated, Database, PrincipalName, ClientIp, HostName, ApplicationName, ActionName, Statement,\n IsSuccess, ResponseRows, AffectedRows, IsVolumeAnomalyOnVal, AnomalyScore,ResourceId\n| where IsVolumeAnomalyOnVal == 'true'\n| sort by AnomalyScore desc, TimeGenerated desc\n| extend Name = tostring(split(PrincipalName,'@',0)[0]), UPNSuffix = tostring(split(PrincipalName,'@',1)[0])\n",
"queryFrequency": "PT1H",
"queryPeriod": "P14D",
"severity": "Medium",
"status": "Available",
"subTechniques": [],
"suppressionDuration": "PT1H",
"suppressionEnabled": false,
"tactics": [
"Impact"
],
"tags": [
"SQL"
],
"techniques": [
"T1485",
"T1491",
"T1565"
],
"templateVersion": "1.1.2",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0
},
"type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
}
]
}