Query Details

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:

  1. Purpose: The query aims to identify high-severity activities related to execution and persistence tactics, as defined by MITRE techniques T1059 and T1059.009.

  2. Time Frame: It looks at activities that occurred in the last 2 hours.

  3. Targeted Operations: It focuses on operations where extensions are written to virtual machines or run commands are executed successfully.

  4. Script Extensions: It checks for specific types of script extensions that are known to be used for executing scripts on VMs.

  5. 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.
  6. IP Address Filtering: It ensures that the caller's IP address is not empty and does not start with specific internal Azure IP ranges.

  7. Data Extraction: For each activity, it extracts details such as the type of extension, the virtual machine name, and the extension name.

  8. 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.

  9. 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 profile picture

David Alonso

Released: July 27, 2026

Tables

AzureActivity

Keywords

AzureActivityVirtualMachinesExtensionsCallerResourceGroupSubscriptionIdTimeGeneratedIpAddress

Operators

letdynamicago=~orhas_anynottolowerisnotempty!startswithextendtodynamiccoalescetostringiffsplitarray_lengthsummarizecountmake_setanyminmax

MITRE Techniques

Actions

GitHub