Azure Serverless Script Execution


Description

Detects when serverless resources execute PowerShell or Python scripts through Azure Automation runbook jobs or Azure Function Apps. Adversaries may abuse access to serverless resources to execute commands with inherited permissions from managed identities, RunAs accounts, or hybrid worker groups.

Query · python

from panther_azureactivity_helpers import (
    azure_activity_alert_context,
    azure_activity_success,
    extract_resource_name_from_id,
)

# AZT302.1/2/3 - Automation Account Runbook Job Execution
RUNBOOK_JOB_WRITE = "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/WRITE"

# AZT302.4 - Function Application Execution
FUNCTION_APP_ACTION = "MICROSOFT.WEB/SITES/HOSTRUNTIME/HOST/ACTION"

AZT302_OPERATIONS = [
    RUNBOOK_JOB_WRITE,
    FUNCTION_APP_ACTION,
]


def rule(event):
    operation = event.get("operationName", "").upper()
    return all([operation in AZT302_OPERATIONS, azure_activity_success(event)])


def title(event):
    resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
    operation = event.get("operationName", "").upper()

    if operation == RUNBOOK_JOB_WRITE:
        technique = "Automation Runbook Job"
        resource_name = extract_resource_name_from_id(
            resource_id, "automationAccounts", default="<UNKNOWN_ACCOUNT>"
        )
    elif operation == FUNCTION_APP_ACTION:
        technique = "Function App"
        resource_name = extract_resource_name_from_id(resource_id, "sites", default="<UNKNOWN_APP>")
    else:
        technique = "Serverless"
        resource_name = "<UNKNOWN_RESOURCE>"

    return f"Azure {technique} Execution detected on [{resource_name}]"


def alert_context(event):
    context = azure_activity_alert_context(event)
    operation = event.get("operationName", "").upper()
    resource_id = event.get("resourceId", "")

    if operation == RUNBOOK_JOB_WRITE:
        context["resource_type"] = "Automation Runbook Job"

        automation_account = extract_resource_name_from_id(
            resource_id, "automationAccounts", default=""
        )
        if automation_account:
            context["automation_account"] = automation_account

        runbook_name = extract_resource_name_from_id(resource_id, "runbooks", default="")
        if runbook_name:
            context["runbook_name"] = runbook_name

        job_id = extract_resource_name_from_id(resource_id, "jobs", default="")
        if job_id:
            context["job_id"] = job_id

    elif operation == FUNCTION_APP_ACTION:
        context["resource_type"] = "Function App"

        function_app_name = extract_resource_name_from_id(resource_id, "sites", default="")
        if function_app_name:
            context["function_app_name"] = function_app_name

    return context

Analyst notes

  1. Query Azure Monitor Activity logs for all automation and function app execution operations by the callerIpAddress in the 24 hours before and after the alert to identify patterns of serverless execution
  2. Find all runbook or function app creation/modification activities by the same callerIpAddress in the 6 hours before the alert to determine if this is part of a setup-then-execute attack pattern
  3. Check if the callerIpAddress has executed runbook jobs or function apps in the past 90 days to establish if this is typical administrative activity or anomalous behavior
Raw source Azure Serverless Script Execution · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
Filename: azure_serverless_execution.py
RuleID: "Azure.MonitorActivity.Automation.ServerlessExecution"
DisplayName: "Azure Serverless Script Execution"
Enabled: true
LogTypes:
  - Azure.MonitorActivity
Severity: Info
Description: >
  Detects when serverless resources execute PowerShell or Python scripts through Azure Automation
  runbook jobs or Azure Function Apps. Adversaries may abuse access to serverless resources to
  execute commands with inherited permissions from managed identities, RunAs accounts, or hybrid
  worker groups.
Reports:
  MITRE ATT&CK:
    - TA0002:T1059 # Execution: Command and Scripting Interpreter
    - TA0002:T1651 # Execution: Cloud Administration Command
Tags:
  - AZT404
  - AZT404.3
  - AZT302
  - AZT302.1
  - AZT302.2
  - AZT302.3
  - AZT302.4
  - Execution
  - Command and Scripting Interpreter
  - Cloud Administration Command
Runbook: |
  1. Query Azure Monitor Activity logs for all automation and function app execution operations by the callerIpAddress in the 24 hours before and after the alert to identify patterns of serverless execution
  2. Find all runbook or function app creation/modification activities by the same callerIpAddress in the 6 hours before the alert to determine if this is part of a setup-then-execute attack pattern
  3. Check if the callerIpAddress has executed runbook jobs or function apps in the past 90 days to establish if this is typical administrative activity or anomalous behavior
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/Execution/AZT302/AZT302/
SummaryAttributes:
  - resourceId
  - callerIpAddress
  - correlationId
Tests:
  - Name: Automation Runbook Job Executed
    ExpectedResult: true
    Log:
      {
        "time": "2025-12-22T10:30:00.0000000Z",
        "resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/automation-rg/providers/Microsoft.Automation/automationAccounts/MyAutomationAccount/jobs/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/WRITE",
        "operationVersion": "2021-06-22",
        "category": "Administrative",
        "resultType": "Success",
        "resultSignature": "200",
        "callerIpAddress": "1.1.1.1",
        "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
        "level": "Informational",
        "location": "eastus",
        "tenantId": "87654321-4321-4321-4321-111111111111"
      }
  - Name: Runbook Job Case Insensitive
    ExpectedResult: true
    Log:
      {
        "time": "2025-12-22T11:15:00.0000000Z",
        "resourceId": "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/prod-automation/providers/Microsoft.Automation/automationAccounts/ProdAccount/jobs/b2c3d4e5-f6a7-8901-bcde-f23456789012",
        "operationName": "microsoft.automation/automationaccounts/jobs/write",
        "category": "Administrative",
        "resultType": "Succeeded",
        "callerIpAddress": "2.2.2.2",
        "correlationId": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
        "level": "Information",
        "location": "westeurope",
        "tenantId": "22222222-2222-2222-2222-222222222222"
      }
  - Name: Function App Execution
    ExpectedResult: true
    Log:
      {
        "time": "2025-12-22T12:00:00.0000000Z",
        "resourceId": "/subscriptions/22222222-2222-2222-2222-222222222222/resourceGroups/function-rg/providers/Microsoft.Web/sites/myFunctionApp",
        "operationName": "MICROSOFT.WEB/SITES/HOSTRUNTIME/HOST/ACTION",
        "category": "Administrative",
        "resultType": "Success",
        "callerIpAddress": "3.3.3.3",
        "correlationId": "c3d4e5f6-a7b8-9012-cdef-222222222222",
        "level": "Informational",
        "location": "centralus",
        "tenantId": "22222222-2222-2222-2222-222222222222"
      }
  - Name: Function App Case Insensitive
    ExpectedResult: true
    Log:
      {
        "time": "2025-12-22T13:00:00.0000000Z",
        "resourceId": "/subscriptions/33333333-3333-3333-3333-333333333333/resourceGroups/apps-rg/providers/Microsoft.Web/sites/prodFunctionApp",
        "operationName": "microsoft.web/sites/hostruntime/host/action",
        "category": "Administrative",
        "resultType": "Succeeded",
        "callerIpAddress": "4.4.4.4",
        "correlationId": "d4e5f6a7-b8c9-0123-def0-333333333333",
        "level": "Information",
        "location": "westus",
        "tenantId": "33333333-3333-3333-3333-333333333333"
      }
  - Name: Failed Runbook Job Execution
    ExpectedResult: false
    Log:
      {
        "time": "2025-12-22T14:00:00.0000000Z",
        "resourceId": "/subscriptions/44444444-4444-4444-4444-444444444444/resourceGroups/automation-rg/providers/Microsoft.Automation/automationAccounts/TestAccount/jobs/e5f6a7b8-c9d0-1234-ef01-444444444444",
        "operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/WRITE",
        "category": "Administrative",
        "resultType": "Failed",
        "resultSignature": "403",
        "callerIpAddress": "5.5.5.5",
        "correlationId": "e5f6a7b8-c9d0-1234-ef01-444444444444",
        "level": "Error",
        "location": "eastus2",
        "tenantId": "44444444-4444-4444-4444-444444444444"
      }
  - Name: Failed Function App Execution
    ExpectedResult: false
    Log:
      {
        "time": "2025-12-22T15:00:00.0000000Z",
        "resourceId": "/subscriptions/55555555-5555-5555-5555-555555555555/resourceGroups/apps-rg/providers/Microsoft.Web/sites/testFunctionApp",
        "operationName": "MICROSOFT.WEB/SITES/HOSTRUNTIME/HOST/ACTION",
        "category": "Administrative",
        "resultType": "Failed",
        "resultSignature": "403",
        "callerIpAddress": "6.6.6.6",
        "correlationId": "f6a7b8c9-d0e1-2345-f012-555555555555",
        "level": "Error",
        "location": "northeurope",
        "tenantId": "55555555-5555-5555-5555-555555555555"
      }
  - Name: Different Operation
    ExpectedResult: false
    Log:
      {
        "time": "2025-12-22T17:00:00.0000000Z",
        "resourceId": "/subscriptions/77777777-7777-7777-7777-777777777777/resourceGroups/apps-rg/providers/Microsoft.Web/sites/myApp",
        "operationName": "MICROSOFT.WEB/SITES/READ",
        "category": "Administrative",
        "resultType": "Success",
        "callerIpAddress": "8.8.8.8",
        "correlationId": "b8c9d0e1-f2a3-4567-1234-777777777777",
        "tenantId": "77777777-7777-7777-7777-777777777777"
      }

# ------ paired body: azure_serverless_execution.py ------

from panther_azureactivity_helpers import (
    azure_activity_alert_context,
    azure_activity_success,
    extract_resource_name_from_id,
)

# AZT302.1/2/3 - Automation Account Runbook Job Execution
RUNBOOK_JOB_WRITE = "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/JOBS/WRITE"

# AZT302.4 - Function Application Execution
FUNCTION_APP_ACTION = "MICROSOFT.WEB/SITES/HOSTRUNTIME/HOST/ACTION"

AZT302_OPERATIONS = [
    RUNBOOK_JOB_WRITE,
    FUNCTION_APP_ACTION,
]


def rule(event):
    operation = event.get("operationName", "").upper()
    return all([operation in AZT302_OPERATIONS, azure_activity_success(event)])


def title(event):
    resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
    operation = event.get("operationName", "").upper()

    if operation == RUNBOOK_JOB_WRITE:
        technique = "Automation Runbook Job"
        resource_name = extract_resource_name_from_id(
            resource_id, "automationAccounts", default="<UNKNOWN_ACCOUNT>"
        )
    elif operation == FUNCTION_APP_ACTION:
        technique = "Function App"
        resource_name = extract_resource_name_from_id(resource_id, "sites", default="<UNKNOWN_APP>")
    else:
        technique = "Serverless"
        resource_name = "<UNKNOWN_RESOURCE>"

    return f"Azure {technique} Execution detected on [{resource_name}]"


def alert_context(event):
    context = azure_activity_alert_context(event)
    operation = event.get("operationName", "").upper()
    resource_id = event.get("resourceId", "")

    if operation == RUNBOOK_JOB_WRITE:
        context["resource_type"] = "Automation Runbook Job"

        automation_account = extract_resource_name_from_id(
            resource_id, "automationAccounts", default=""
        )
        if automation_account:
            context["automation_account"] = automation_account

        runbook_name = extract_resource_name_from_id(resource_id, "runbooks", default="")
        if runbook_name:
            context["runbook_name"] = runbook_name

        job_id = extract_resource_name_from_id(resource_id, "jobs", default="")
        if job_id:
            context["job_id"] = job_id

    elif operation == FUNCTION_APP_ACTION:
        context["resource_type"] = "Function App"

        function_app_name = extract_resource_name_from_id(resource_id, "sites", default="")
        if function_app_name:
            context["function_app_name"] = function_app_name

    return context

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.