RULE 15 VM Custom Script Extension
Query
// Rule : Azure - Custom Script Extension or Run Command Executed on Virtual Machine
// Severity: High
// Tactics : Execution, Persistence
// MITRE : T1059, T1059.009
// Freq : PT1H Period: PT2H
//==========================================================================================
let KnownIaCPatterns = dynamic(["terraform", "bicep", "pipeline", "github", "pulumi",
"devops", "arm-deployment", "chef", "puppet", "ansible", "dsc"]);
// Extension type values observed in AzureActivity Properties field
let ScriptExtTypes = dynamic([
"CustomScriptExtension",
"CustomScriptForLinux",
"RunCommandHandler",
"RunCommand",
"Microsoft.Compute.CustomScriptExtension",
"Microsoft.Azure.Extensions.CustomScript"
]);
// Managed / benign extensions that occasionally re-write and produced FPs. If Properties
// indicates these publishers/types, treat as noise even if the operation matches.
let BenignExtensionMarkers = dynamic([
"Microsoft.Azure.Monitor.AzureMonitorLinuxAgent",
"Microsoft.Azure.Monitor.AzureMonitorWindowsAgent",
"Microsoft.EnterpriseCloud.Monitoring.MicrosoftMonitoringAgent",
"Microsoft.EnterpriseCloud.Monitoring.OmsAgentForLinux",
"Microsoft.Azure.Security.Monitoring.AzureSecurityLinuxAgent",
"Microsoft.Azure.Security.AntimalwareSignature",
"Microsoft.GuestConfiguration.ConfigurationForLinux",
"Microsoft.GuestConfiguration.ConfigurationForWindows",
"Microsoft.Azure.NetworkWatcher",
"Microsoft.Azure.Extensions.DependencyAgentLinux",
"Microsoft.Azure.Extensions.DependencyAgentWindows"
]);
AzureActivity
| where TimeGenerated > ago(2h)
| where OperationNameValue =~ "MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE"
or OperationNameValue =~ "MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION"
| where ActivityStatusValue =~ "Success"
| where Properties has_any (ScriptExtTypes)
or OperationNameValue =~ "MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION"
// Tuned: drop Azure-managed extension re-writes (monitor/security/guest-config agents).
| where not(Properties has_any (BenignExtensionMarkers))
| where not(tolower(Caller) has_any (KnownIaCPatterns))
| where isnotempty(CallerIpAddress)
| where CallerIpAddress !startswith "168.63." and CallerIpAddress !startswith "169.254."
| extend PropsParsed = todynamic(Properties)
| extend ExtensionType = coalesce(
tostring(PropsParsed["type"]),
tostring(PropsParsed.type),
iff(OperationNameValue has "RUNCOMMAND", "RunCommand", ""))
| extend VMName = tostring(split(ResourceId, "/")[8])
| extend ExtensionName = iff(array_length(split(ResourceId, "/")) > 10,
tostring(split(ResourceId, "/")[10]), "")
| summarize
ExecutionCount = count(),
AffectedVMs = make_set(VMName, 10),
ExtensionTypes = make_set(ExtensionType, 5),
AffectedResources = make_set(ResourceId, 10),
SourceIPs = make_set(CallerIpAddress, 5),
CallerIP = any(CallerIpAddress),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by Caller, ResourceGroup, SubscriptionId
| extend
AccountName = tostring(split(Caller, "@")[0]),
AccountUPNSuffix = tostring(split(Caller, "@")[1])Explanation
This query is designed to detect potentially suspicious activities involving the execution of custom scripts or run commands on Azure virtual machines. Here's a simplified breakdown of what the query does:
-
Purpose: The query aims to identify high-severity activities related to execution and persistence tactics, as defined by MITRE techniques T1059 and T1059.009.
-
Time Frame: It looks at activities that occurred in the last 2 hours.
-
Targeted Operations: It focuses on operations where extensions are written to virtual machines or run commands are executed successfully.
-
Script Extensions: It checks for specific types of script extensions that are known to be used for executing scripts on VMs.
-
Exclusions:
- It filters out activities involving benign extensions that are managed by Azure and could produce false positives.
- It excludes activities initiated by known infrastructure-as-code (IaC) patterns, which are typically legitimate.
-
IP Address Filtering: It ensures that the caller's IP address is not empty and does not start with specific internal Azure IP ranges.
-
Data Extraction: For each activity, it extracts details such as the type of extension, the virtual machine name, and the extension name.
-
Summarization: The query summarizes the data by counting the number of executions and listing affected VMs, extension types, resources, and source IPs. It also captures the first and last time the activity was seen.
-
Output: The results are grouped by the caller, resource group, and subscription ID, and additional information about the caller's account name and domain is extracted.
Overall, this query helps security teams monitor and investigate potentially unauthorized or suspicious script executions on Azure VMs, while minimizing noise from legitimate operations.
Details

David Alonso
Released: July 27, 2026
Tables
Keywords
Operators