Azure Automation Schedule Created or Modified


Description

Detects when an Azure Automation schedule is created or modified. Schedules define when and how often automation runbooks execute. Adversaries may create or modify schedules to establish persistence by executing malicious runbooks at regular intervals or specific times. This technique allows attackers to maintain access and execute commands without requiring direct interaction.

Query · python

from panther_azureactivity_helpers import (
    azure_activity_alert_context,
    azure_activity_success,
    extract_resource_name_from_id,
)

SCHEDULE_OPERATIONS = [
    "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/WRITE",
]


def rule(event):
    return event.get("operationName", "").upper() in SCHEDULE_OPERATIONS and azure_activity_success(
        event
    )


def title(event):
    resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
    caller = event.get("callerIpAddress", "<UNKNOWN_CALLER>")

    schedule_name = extract_resource_name_from_id(
        resource_id, "schedules", default="<UNKNOWN_SCHEDULE_NAME>"
    )

    return f"Azure Automation Schedule Created or Modified: [{schedule_name}] by [{caller}]"


def alert_context(event):
    context = azure_activity_alert_context(event)

    resource_id = event.get("resourceId", "")

    schedule_name = extract_resource_name_from_id(resource_id, "schedules", default="")
    if schedule_name:
        context["schedule_name"] = schedule_name

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

    return context

Analyst notes

  1. Find all automation account operations by the callerIpAddress in the 24 hours before and after this alert to identify if runbooks were created or modified alongside this schedule
  2. Query for runbook job execution events from the automation account in the 6 hours after the schedule creation to determine if the schedule has already triggered
  3. Check if the callerIpAddress is associated with known cloud providers, VPNs, or corporate network ranges used by authorized DevOps personnel
Raw source Azure Automation Schedule Created or Modified · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
AnalysisType: rule
Filename: azure_automation_schedule_created.py
RuleID: "Azure.MonitorActivity.Automation.ScheduleCreated"
DisplayName: "Azure Automation Schedule Created or Modified"
Enabled: true
LogTypes:
  - Azure.MonitorActivity
Severity: Info
Description: >
  Detects when an Azure Automation schedule is created or modified. Schedules define when and how often
  automation runbooks execute. Adversaries may create or modify schedules to establish persistence by
  executing malicious runbooks at regular intervals or specific times. This technique allows attackers
  to maintain access and execute commands without requiring direct interaction.
Reports:
  MITRE ATT&CK:
    - TA0003:T1053.005 # Persistence: Scheduled Task/Job - Scheduled Task
    - TA0004:T1068 # Privilege Escalation: Exploitation for Privilege Escalation
Tags:
  - AZT505
  - Persistence
  - Privilege Escalation
  - Scheduled Task
  - Exploitation for Privilege Escalation
Runbook: |
  1. Find all automation account operations by the callerIpAddress in the 24 hours before and after this alert to identify if runbooks were created or modified alongside this schedule
  2. Query for runbook job execution events from the automation account in the 6 hours after the schedule creation to determine if the schedule has already triggered
  3. Check if the callerIpAddress is associated with known cloud providers, VPNs, or corporate network ranges used by authorized DevOps personnel
Reference: https://microsoft.github.io/Azure-Threat-Research-Matrix/Persistence/AZT505/AZT505-1
SummaryAttributes:
  - resourceId
  - callerIpAddress
  - correlationId
Tests:
  - Name: Schedule Created Successfully
    ExpectedResult: true
    Log:
      {
        "time": "2025-12-23T10:30:00.0000000Z",
        "resourceId": "/subscriptions/12345678-1234-1234-1234-123456789abc/resourceGroups/automation-rg/providers/Microsoft.Automation/automationAccounts/MyAutomationAccount/schedules/DailyMaintenanceSchedule",
        "operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/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: Schedule Modified
    ExpectedResult: true
    Log:
      {
        "time": "2025-12-23T11:15:00.0000000Z",
        "resourceId": "/subscriptions/11111111-1111-1111-1111-111111111111/resourceGroups/prod-automation/providers/Microsoft.Automation/automationAccounts/ProdAccount/schedules/HourlyBackup",
        "operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/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: Persistence Schedule Created
    ExpectedResult: true
    Log:
      {
        "time": "2025-12-23T12:00:00.0000000Z",
        "resourceId": "/subscriptions/33333333-3333-3333-3333-333333333333/resourceGroups/compromised-rg/providers/Microsoft.Automation/automationAccounts/CompromisedAccount/schedules/MaliciousSchedule",
        "operationName": "microsoft.automation/automationaccounts/schedules/write",
        "category": "Administrative",
        "resultType": "Success",
        "callerIpAddress": "3.3.3.3",
        "correlationId": "c3d4e5f6-a7b8-9012-cdef-333333333333",
        "level": "Informational",
        "location": "centralus",
        "tenantId": "33333333-3333-3333-3333-333333333333"
      }
  - Name: Different Operation
    ExpectedResult: false
    Log:
      {
        "time": "2025-12-23T15:00:00.0000000Z",
        "resourceId": "/subscriptions/66666666-6666-6666-6666-666666666666/resourceGroups/automation-rg/providers/Microsoft.Automation/automationAccounts/MyAccount/schedules/OldSchedule",
        "operationName": "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/DELETE",
        "category": "Administrative",
        "resultType": "Success",
        "callerIpAddress": "6.6.6.6",
        "correlationId": "f6a7b8c9-d0e1-2345-f012-678901234567",
        "tenantId": "66666666-6666-6666-6666-666666666666"
      }

# ------ paired body: azure_automation_schedule_created.py ------

from panther_azureactivity_helpers import (
    azure_activity_alert_context,
    azure_activity_success,
    extract_resource_name_from_id,
)

SCHEDULE_OPERATIONS = [
    "MICROSOFT.AUTOMATION/AUTOMATIONACCOUNTS/SCHEDULES/WRITE",
]


def rule(event):
    return event.get("operationName", "").upper() in SCHEDULE_OPERATIONS and azure_activity_success(
        event
    )


def title(event):
    resource_id = event.get("resourceId", "<UNKNOWN_RESOURCE>")
    caller = event.get("callerIpAddress", "<UNKNOWN_CALLER>")

    schedule_name = extract_resource_name_from_id(
        resource_id, "schedules", default="<UNKNOWN_SCHEDULE_NAME>"
    )

    return f"Azure Automation Schedule Created or Modified: [{schedule_name}] by [{caller}]"


def alert_context(event):
    context = azure_activity_alert_context(event)

    resource_id = event.get("resourceId", "")

    schedule_name = extract_resource_name_from_id(resource_id, "schedules", default="")
    if schedule_name:
        context["schedule_name"] = schedule_name

    automation_account_name = extract_resource_name_from_id(
        resource_id, "automationAccounts", default=""
    )
    if automation_account_name:
        context["automation_account_name"] = automation_account_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.