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"
]);
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"
| 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 and summarize instances where custom scripts or run commands have been executed on Azure virtual machines within the last two hours. It focuses on identifying potentially unauthorized or suspicious activities by filtering out known infrastructure-as-code patterns and internal Azure IP addresses. Here's a breakdown of what the query does:

  1. Define Known Patterns: It establishes a list of known infrastructure-as-code (IaC) patterns, such as "terraform" and "ansible", which are typically used for legitimate automation purposes.

  2. Identify Script Extensions: It specifies a list of script extension types that are commonly used in Azure for executing scripts on virtual machines.

  3. Filter Azure Activity Logs:

    • It looks at activities from the last two hours.
    • It filters for operations related to writing extensions or executing run commands on virtual machines.
    • It ensures that the activity was successful.
    • It checks if the properties contain any of the specified script extension types.
    • It excludes activities initiated by known IaC patterns to focus on potentially unauthorized actions.
    • It filters out internal Azure IP addresses to focus on external sources.
  4. Extract and Summarize Information:

    • It extracts details such as the type of extension, virtual machine name, and extension name from the activity logs.
    • It summarizes the data by counting the number of executions, listing affected virtual machines, extension types, resources, and source IPs.
    • It captures the first and last time the activity was seen.
  5. Extend with Additional Information:

    • It extracts the account name and domain from the caller's email address for further analysis.

The query is designed to help security teams quickly identify and investigate potentially suspicious script executions on Azure virtual machines, which could indicate unauthorized access or persistent threats.

Details

David Alonso profile picture

David Alonso

Released: March 12, 2026

Tables

AzureActivity

Keywords

AzureActivityVirtualMachinesExtensionsCallerResourceGroupSubscriptionIdResourceIdTimeGeneratedCallerIpAddressAccountNameAccountUPNSuffix

Operators

letdynamic=~has_anynottolowerisnotempty!startswithextendtodynamiccoalescetostringiffsplitarray_lengthsummarizecountmake_setanyminmax

Actions