{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "workspace": {
            "type": "String"
        }
    },
    "resources": [
        {
            "id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/suspicious-resource-deployment')]",
            "name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/suspicious-resource-deployment')]",
            "type": "Microsoft.OperationalInsights/workspaces/providers/alertRules",
            "kind": "Scheduled",
            "apiVersion": "2023-12-01-preview",
            "properties": {
                "displayName": "Suspicious Resource Deployment - Azure Activity",
                "description": "Detects suspicious Azure control-plane deployments from AzureActivity. Rarity is keyed on the caller and the operation, never the caller's source IP, which removes roaming-admin and CI/CD false positives. A 14-day success-only baseline records which callers deploy and which operations each caller has performed. The rule then fires on a caller new to the control plane (service principal, or a burst of distinct resources) or on any caller performing a watched high-value operation that is new for them. A weighted RiskIndicators field summarises why each caller surfaced.\n\nTABLES: AzureActivity\nCONNECTORS: Azure Activity (one diagnostic-settings export per subscription in scope)\nLICENSE: Microsoft Sentinel\n\nTUNING:\n1. szOperationNames - the watched control-plane operation set; tune to your environment.\n2. HighValueOps - the escalation / execution / persistence subset that fires on new-for-caller alone.\n3. ExcludedCallers - known IaC / automation identities that deploy by design (UPN or SP object id).\n4. BurstThreshold - distinct resources one caller touches in the window that count as a scripted spree.\n5. DetectionWindow / BaselineWindow - align DetectionWindow to run frequency; BaselineWindow drives the query period.\n6. Keep the rule query period at P14D - the 14-day baseline lookups only resolve if the rule itself looks back that far.\n\nKNOWN FALSE POSITIVES:\n1. IaC pipelines / platform automation - already in the caller baseline, suppressed unless a high-value op is new for them; add to ExcludedCallers if still noisy.\n2. New hires running their first deployment - the new-caller path requires a service principal or a resource burst, so a single interactive write does not fire.\n3. Routine VM / deployment writes by established callers - no longer alert on their own.\n\nINVESTIGATION STEPS:\n1. Read RiskIndicators - NewCaller, HighValueOperation, RoleAssignmentChange, ServicePrincipalCaller, MultiResourceBurst, FailuresPreceding.\n2. Identify the Caller and whether it is a user (UPN) or service principal (IsServicePrincipal / AadUserId).\n3. Review HighValueNewOps and RoleAssignmentOps - confirm the specific escalation or execution operations were expected.\n4. Check ResourceGroups / Resources and DistinctResources for scope and whether the spread looks scripted.\n5. Correlate CallerIps and CorrelationIds against known automation and pipeline runs.\n6. If unexpected, contain the identity (disable / rotate credentials) and review the resources it created or modified.\n\nBLIND SPOTS:\nOnly catches operations in szOperationNames, so ops outside that list are invisible. PIM eligible-role and data-plane actions are not covered. Callers already in the 14-day baseline for a given operation are suppressed for that operation, so an insider abusing an operation they routinely perform will not surface. Slow activity split across separate hourly runs may not aggregate into a burst.\n\nAuthor: Bartosz Wysocki | https://www.itprofessor.cloud\nVersion: 1.0 | 2026-06-17",
                "severity": "Medium",
                "enabled": true,
                "query": "// =====================================================================\n// Suspicious Resource Deployment - Azure Activity\n// =====================================================================\n// Tables  : AzureActivity\n// License : Microsoft Sentinel\n// Tuning  : see rule description for full tuning notes\n// Author  : Bartosz Wysocki | itprofessor.cloud\n// Version : 1.0 | 2026-06-17\n// =====================================================================\nlet DetectionWindow = 1h;\nlet BaselineWindow = 14d;\nlet BurstThreshold = 10;             // distinct resources one caller touches in the window -> scripted spree\nlet ExcludedCallers = dynamic([]);   // known automation, e.g. [\"terraform@contoso.com\", \"<github-actions-sp-object-id>\"]\nlet szOperationNames = dynamic([\n\"Microsoft.Resources/deployments/write\",\n\"Microsoft.Resources/subscriptions/resourceGroups/write\",\n\"Microsoft.Compute/virtualMachines/write\",\n\"Microsoft.Compute/virtualMachines/extensions/write\",\n\"Microsoft.Compute/virtualMachines/runCommand/action\",\n\"Microsoft.Web/sites/write\",\n\"Microsoft.Storage/storageAccounts/write\",\n\"Microsoft.Network/networkSecurityGroups/write\",\n\"Microsoft.Authorization/roleAssignments/write\",\n\"Microsoft.Authorization/roleDefinitions/write\",\n\"Microsoft.KeyVault/vaults/write\",\n\"Microsoft.KeyVault/vaults/accessPolicies/write\",\n\"Microsoft.Automation/automationAccounts/write\",\n\"Microsoft.Automation/automationAccounts/runbooks/write\",\n\"Microsoft.ManagedIdentity/userAssignedIdentities/write\"\n]);\nlet HighValueOps = dynamic([\n\"Microsoft.Authorization/roleAssignments/write\",\n\"Microsoft.Authorization/roleDefinitions/write\",\n\"Microsoft.KeyVault/vaults/accessPolicies/write\",\n\"Microsoft.Compute/virtualMachines/extensions/write\",\n\"Microsoft.Compute/virtualMachines/runCommand/action\",\n\"Microsoft.Automation/automationAccounts/runbooks/write\",\n\"Microsoft.ManagedIdentity/userAssignedIdentities/write\"\n]);\nlet Activity = (windowStart:datetime, windowEnd:datetime) {\n    AzureActivity\n    | where TimeGenerated between (windowStart .. windowEnd)\n    | where OperationNameValue in~ (szOperationNames)\n    | where isnotempty(Caller)\n};\nlet BaselineActivity = materialize(\n    Activity(ago(BaselineWindow), ago(DetectionWindow))\n    | where ActivityStatusValue in~ (\"Success\", \"Succeeded\"));\nlet CallerBaseline = BaselineActivity | distinct Caller | extend SeenCaller = true;\nlet CallerOpBaseline = BaselineActivity | distinct Caller, OperationNameValue | extend SeenOp = true;\nActivity(ago(DetectionWindow), now())\n| where Caller !in~ (ExcludedCallers)\n| extend ResourceType = strcat_array(array_slice(split(OperationNameValue, \"/\"), 0, -2), \"/\")\n| extend IsHighValueOp = OperationNameValue in~ (HighValueOps)\n| extend IsSuccess = ActivityStatusValue in~ (\"Success\", \"Succeeded\")\n| join kind=leftouter CallerOpBaseline on Caller, OperationNameValue\n| extend IsNewOpForCaller = isnull(SeenOp)\n| summarize\n    StartTime = min(TimeGenerated),\n    EndTime = max(TimeGenerated),\n    SuccessOps = countif(IsSuccess),\n    FailedOps = countif(ActivityStatusValue in~ (\"Failed\", \"Failure\")),\n    HighValueNewOpCount = countif(IsHighValueOp and IsNewOpForCaller and IsSuccess),\n    RoleAssignmentOps = countif(OperationNameValue =~ \"Microsoft.Authorization/roleAssignments/write\" and IsSuccess),\n    DistinctResources = dcountif(_ResourceId, IsSuccess),\n    HighValueNewOps = make_set_if(OperationNameValue, IsHighValueOp and IsNewOpForCaller and IsSuccess, 25),\n    Operations = make_set(OperationNameValue, 50),\n    ResourceTypes = make_set(ResourceType, 50),\n    ResourceGroups = make_set(ResourceGroup, 50),\n    Resources = make_set(_ResourceId, 50),\n    CallerIps = make_set(CallerIpAddress, 25),\n    CorrelationIds = make_set(CorrelationId, 25)\n  by Caller\n| where SuccessOps > 0\n| join kind=leftouter CallerBaseline on Caller\n| extend IsNewCaller = isnull(SeenCaller)\n| extend IsServicePrincipal = Caller !has \"@\"\n| where HighValueNewOpCount > 0 or (IsNewCaller and (IsServicePrincipal or DistinctResources >= BurstThreshold))\n| extend Risk_1 = iff(IsNewCaller, \"NewCaller\", \"\")\n| extend Risk_2 = iff(HighValueNewOpCount > 0, \"HighValueOperation\", \"\")\n| extend Risk_3 = iff(RoleAssignmentOps > 0, \"RoleAssignmentChange\", \"\")\n| extend Risk_4 = iff(IsServicePrincipal, \"ServicePrincipalCaller\", \"\")\n| extend Risk_5 = iff(DistinctResources >= BurstThreshold, \"MultiResourceBurst\", \"\")\n| extend Risk_6 = iff(FailedOps > 0, \"FailuresPreceding\", \"\")\n| extend RiskIndicators = trim(@\"\\s\\|\\s*$\", strcat(\n    iff(isnotempty(Risk_1), strcat(Risk_1, \" | \"), \"\"),\n    iff(isnotempty(Risk_2), strcat(Risk_2, \" | \"), \"\"),\n    iff(isnotempty(Risk_3), strcat(Risk_3, \" | \"), \"\"),\n    iff(isnotempty(Risk_4), strcat(Risk_4, \" | \"), \"\"),\n    iff(isnotempty(Risk_5), strcat(Risk_5, \" | \"), \"\"),\n    iff(isnotempty(Risk_6), strcat(Risk_6, \" | \"), \"\")\n))\n| extend Name = iif(Caller has \"@\", tostring(split(Caller, \"@\", 0)[0]), \"\"),\n         UPNSuffix = iif(Caller has \"@\", tostring(split(Caller, \"@\", 1)[0]), \"\"),\n         AadUserId = iif(Caller !has \"@\", Caller, \"\")\n| project\n    StartTime, EndTime, RiskIndicators,\n    Caller, Name, UPNSuffix, AadUserId, IsServicePrincipal,\n    SuccessOps, FailedOps, DistinctResources, RoleAssignmentOps, HighValueNewOpCount,\n    HighValueNewOps, Operations, ResourceTypes, ResourceGroups, Resources,\n    IsNewCaller, CallerIps, CorrelationIds\n| sort by HighValueNewOpCount desc, DistinctResources desc",
                "queryFrequency": "PT1H",
                "queryPeriod": "P14D",
                "triggerOperator": "GreaterThan",
                "triggerThreshold": 0,
                "suppressionDuration": "PT1H",
                "suppressionEnabled": false,
                "startTimeUtc": null,
                "tactics": [
                    "PrivilegeEscalation",
                    "Persistence",
                    "DefenseEvasion",
                    "Execution"
                ],
                "techniques": [
                    "T1098",
                    "T1578",
                    "T1651"
                ],
                "subTechniques": [
                    "T1098.003",
                    "T1578.002"
                ],
                "alertRuleTemplateName": null,
                "incidentConfiguration": {
                    "createIncident": true,
                    "groupingConfiguration": {
                        "enabled": true,
                        "reopenClosedIncident": false,
                        "lookbackDuration": "PT2H",
                        "matchingMethod": "Selected",
                        "groupByEntities": [
                            "Account"
                        ],
                        "groupByAlertDetails": null,
                        "groupByCustomDetails": null
                    }
                },
                "eventGroupingSettings": {
                    "aggregationKind": "SingleAlert"
                },
                "alertDetailsOverride": {
                    "alertDisplayNameFormat": "Suspicious Azure deployment by {{Caller}} ({{RiskIndicators}})",
                    "alertDescriptionFormat": "{{Caller}} surfaced control-plane risk indicators: {{RiskIndicators}}. Distinct resources touched: {{DistinctResources}}."
                },
                "customDetails": {
                    "RiskIndicators": "RiskIndicators",
                    "HighValueNewOps": "HighValueNewOps",
                    "DistinctResources": "DistinctResources",
                    "RoleAssignmentOps": "RoleAssignmentOps",
                    "IsNewCaller": "IsNewCaller",
                    "ResourceGroups": "ResourceGroups",
                    "FailedOps": "FailedOps"
                },
                "entityMappings": [
                    {
                        "entityType": "Account",
                        "fieldMappings": [
                            {
                                "identifier": "Name",
                                "columnName": "Name"
                            },
                            {
                                "identifier": "UPNSuffix",
                                "columnName": "UPNSuffix"
                            },
                            {
                                "identifier": "AadUserId",
                                "columnName": "AadUserId"
                            }
                        ]
                    }
                ],
                "sentinelEntitiesMappings": null,
                "templateVersion": null
            }
        }
    ]
}
