Query Details

HUNT 13 VM Script Extension History

Query

// Hunt     : Hunt - VM Script Extension and Run Command Execution History (30 Days)
// Tactics  : Execution, Persistence
// MITRE    : T1059, T1059.009
// Purpose  : Enumerate all Custom Script Extension deployments and Run Command executions across all VMs for the last 30 days. Use to identify unusual actors, one-off script injections, or scripts deployed outside known IaC patterns. Cross-reference with Rule-15 alerts to investigate full scope.
//==========================================================================================

let ScriptExtTypes = dynamic([
    "CustomScriptExtension", "CustomScriptForLinux",
    "RunCommandHandler", "RunCommand",
    "Microsoft.Compute.CustomScriptExtension",
    "Microsoft.Azure.Extensions.CustomScript"
]);
AzureActivity
| where TimeGenerated > ago(30d)
| where OperationNameValue =~ "MICROSOFT.COMPUTE/VIRTUALMACHINES/EXTENSIONS/WRITE"
    or  OperationNameValue =~ "MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION"
| where ActivityStatusValue =~ "Success"
| where Properties has_any (ScriptExtTypes)
    or  OperationNameValue has "RUNCOMMAND"
| extend PropsParsed   = todynamic(Properties)
| extend ExtensionType = coalesce(
    tostring(PropsParsed["type"]),
    tostring(PropsParsed.type),
    iff(OperationNameValue has "RUNCOMMAND", "RunCommand", "Unknown"))
| extend VMName        = tostring(split(ResourceId, "/")[8])
| project
    TimeGenerated,
    Caller,
    CallerIpAddress,
    VMName,
    ExtensionType,
    ResourceId,
    ResourceGroup,
    SubscriptionId
| summarize
    ExecCount     = count(),
    DistinctVMs   = dcount(VMName),
    VMNames       = make_set(VMName, 20),
    ExtensionTypes = make_set(ExtensionType, 5),
    SourceIPs     = make_set(CallerIpAddress, 5),
    FirstSeen     = min(TimeGenerated),
    LastSeen      = max(TimeGenerated)
    by Caller, ResourceGroup, SubscriptionId
| order by ExecCount desc

Explanation

This KQL query is designed to track and analyze the deployment of custom scripts and run command executions on virtual machines (VMs) over the past 30 days. Here's a simple breakdown of what the query does:

  1. Purpose: The query aims to identify all instances where custom scripts or run commands have been executed on VMs. This helps in spotting unusual activities, such as unauthorized script injections or scripts that don't align with known Infrastructure as Code (IaC) patterns.

  2. Data Source: It pulls data from the AzureActivity log, focusing on activities related to VM extensions and run commands.

  3. Filtering Criteria:

    • It looks at activities from the last 30 days.
    • It filters for operations specifically related to VM extensions and run commands that were successful.
    • It checks if the operation involves any of the specified script extension types or run commands.
  4. Data Extraction:

    • It extracts and processes relevant properties from the activity logs, such as the type of extension, VM name, and other identifiers.
  5. Projection: The query selects specific fields to focus on, including the time of the activity, the caller's identity and IP address, the VM name, extension type, and various identifiers like resource group and subscription ID.

  6. Aggregation:

    • It summarizes the data by counting the number of executions (ExecCount).
    • It counts distinct VMs affected and lists their names.
    • It identifies the types of extensions used and the source IPs involved.
    • It notes the first and last time each caller executed a script or command.
  7. Ordering: The results are sorted by the number of executions in descending order, highlighting the most active callers or scripts.

Overall, this query helps security analysts monitor and investigate script execution activities on VMs, potentially uncovering security threats or policy violations.

Details

David Alonso profile picture

David Alonso

Released: March 12, 2026

Tables

AzureActivity

Keywords

AzureActivityVirtualMachinesExtensionsRunCommandPropertiesResourceIdResourceGroupSubscriptionIdCallerCallerIpAddressTimeGenerated

Operators

letdynamicago=~orhas_anyhasextendtodynamiccoalescetostringiffsplitprojectsummarizecountdcountmake_setminmaxbyorder bydesc

Actions