security operations

Analyzing Azure Activity Logs for Threats

Queries Azure Monitor activity logs and sign-in logs via azure-monitor-query to detect suspicious administrative operations, impossible travel, privilege escalation, and resource modifications. Builds KQL queries for threat hunting in Azure environments. Use when investigating suspicious Azure tenant activity or building cloud SIEM detections.

activity-logsazureazure-monitorcloud-securitykqlthreat-hunting
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When investigating security incidents that require analyzing azure activity logs for threats
  • When building detection rules or threat hunting queries for this domain
  • When SOC analysts need structured procedures for this analysis type
  • When validating security monitoring coverage for related attack techniques

Prerequisites

  • Familiarity with security operations concepts and tools
  • Access to a test or lab environment for safe execution
  • Python 3.8+ with required dependencies installed
  • Appropriate authorization for any testing activities

Instructions

Use azure-monitor-query to execute KQL queries against Azure Log Analytics workspaces, detecting suspicious admin operations and sign-in anomalies.

from azure.identity import DefaultAzureCredential
from azure.monitor.query import LogsQueryClient
from datetime import timedelta
 
credential = DefaultAzureCredential()
client = LogsQueryClient(credential)
 
response = client.query_workspace(
    workspace_id="WORKSPACE_ID",
    query="AzureActivity | where OperationNameValue has 'MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE' | take 10",
    timespan=timedelta(hours=24),
)

Key detection queries:

  1. Role assignment changes (privilege escalation)
  2. Resource group and subscription modifications
  3. Key vault secret access from new IPs
  4. Network security group rule changes
  5. Conditional access policy modifications

Examples

# Detect new Global Admin role assignments
query = '''
AuditLogs
| where OperationName == "Add member to role"
| where TargetResources[0].modifiedProperties[0].newValue has "Global Administrator"
'''
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md1.6 KB

API Reference: Analyzing Azure Activity Logs for Threats

azure-monitor-query

from azure.identity import DefaultAzureCredential
from azure.monitor.query import LogsQueryClient, LogsQueryStatus
from datetime import timedelta
 
credential = DefaultAzureCredential()
client = LogsQueryClient(credential)
 
response = client.query_workspace(
    workspace_id="WORKSPACE_ID",
    query="AzureActivity | take 10",
    timespan=timedelta(hours=24),
)
if response.status == LogsQueryStatus.SUCCESS:
    for table in response.tables:
        columns = [col.name for col in table.columns]
        for row in table.rows:
            print(dict(zip(columns, row)))

Key Azure Log Tables

Table Content
AzureActivity Control plane operations (ARM)
SigninLogs Azure AD sign-in events
AuditLogs Azure AD audit trail
AzureDiagnostics Resource diagnostics (Key Vault, NSG)
SecurityAlert Defender for Cloud alerts

Threat Detection KQL Patterns

// Privilege escalation
AzureActivity | where OperationNameValue has "ROLEASSIGNMENTS/WRITE"
 
// Impossible travel
SigninLogs | where ResultType == 0
| extend Distance = geo_distance_2points(...)
 
// Mass deletion
AzureActivity | where OperationNameValue endswith "/DELETE"
| summarize count() by Caller, bin(TimeGenerated, 1h)

References

Scripts 1

agent.py6.8 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for analyzing Azure activity logs for threat detection."""

import os
import json
import argparse
from datetime import datetime, timedelta

from azure.identity import DefaultAzureCredential, ClientSecretCredential
from azure.monitor.query import LogsQueryClient, LogsQueryStatus


def get_credential(tenant_id=None, client_id=None, client_secret=None):
    """Get Azure credential."""
    if client_id and client_secret and tenant_id:
        return ClientSecretCredential(tenant_id, client_id, client_secret)
    return DefaultAzureCredential()


def run_kql(credential, workspace_id, query, hours=24):
    """Execute KQL query against Log Analytics workspace."""
    client = LogsQueryClient(credential)
    response = client.query_workspace(
        workspace_id, query, timespan=timedelta(hours=hours)
    )
    rows = []
    if response.status == LogsQueryStatus.SUCCESS:
        for table in response.tables:
            columns = [col.name for col in table.columns]
            for row in table.rows:
                rows.append(dict(zip(columns, row)))
    return rows


def detect_privilege_escalation(credential, workspace_id):
    """Detect role assignment changes indicating privilege escalation."""
    query = """
    AzureActivity
    | where OperationNameValue has_any (
        "MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE",
        "MICROSOFT.AUTHORIZATION/ROLEDEFINITIONS/WRITE"
    )
    | where ActivityStatusValue == "Success"
    | project TimeGenerated, Caller, CallerIpAddress,
              OperationNameValue, ResourceGroup, Properties_d
    | order by TimeGenerated desc
    """
    return run_kql(credential, workspace_id, query)


def detect_nsg_changes(credential, workspace_id):
    """Detect Network Security Group rule modifications."""
    query = """
    AzureActivity
    | where OperationNameValue has_any (
        "MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/WRITE",
        "MICROSOFT.NETWORK/NETWORKSECURITYGROUPS/SECURITYRULES/DELETE"
    )
    | where ActivityStatusValue == "Success"
    | project TimeGenerated, Caller, CallerIpAddress,
              OperationNameValue, ResourceGroup
    | order by TimeGenerated desc
    """
    return run_kql(credential, workspace_id, query)


def detect_keyvault_access(credential, workspace_id):
    """Detect Key Vault secret access from unusual sources."""
    query = """
    AzureDiagnostics
    | where ResourceProvider == "MICROSOFT.KEYVAULT"
    | where OperationName in ("SecretGet", "SecretList", "SecretSet")
    | summarize AccessCount = count(), DistinctIPs = dcount(CallerIPAddress),
                IPList = make_set(CallerIPAddress, 10)
                by identity_claim_upn_s, OperationName, Resource
    | where DistinctIPs > 2 or AccessCount > 50
    | order by AccessCount desc
    """
    return run_kql(credential, workspace_id, query)


def detect_impossible_travel(credential, workspace_id):
    """Detect sign-ins from geographically distant locations in short time."""
    query = """
    SigninLogs
    | where ResultType == 0
    | project TimeGenerated, UserPrincipalName, IPAddress,
              Lat = toreal(LocationDetails.geoCoordinates.latitude),
              Lon = toreal(LocationDetails.geoCoordinates.longitude)
    | sort by UserPrincipalName asc, TimeGenerated asc
    | extend PrevLat = prev(Lat), PrevLon = prev(Lon),
             PrevTime = prev(TimeGenerated), PrevUser = prev(UserPrincipalName)
    | where UserPrincipalName == PrevUser
    | extend TimeDiffMin = datetime_diff('minute', TimeGenerated, PrevTime)
    | where TimeDiffMin < 60 and TimeDiffMin > 0
    | extend DistKm = geo_distance_2points(Lon, Lat, PrevLon, PrevLat) / 1000
    | where DistKm > 500
    | project TimeGenerated, UserPrincipalName, IPAddress, DistKm, TimeDiffMin
    """
    return run_kql(credential, workspace_id, query)


def detect_resource_deletion(credential, workspace_id):
    """Detect mass resource deletion events."""
    query = """
    AzureActivity
    | where OperationNameValue endswith "/DELETE"
    | where ActivityStatusValue == "Success"
    | summarize DeleteCount = count(), Resources = make_set(Resource, 20)
                by Caller, bin(TimeGenerated, 1h)
    | where DeleteCount > 10
    | order by DeleteCount desc
    """
    return run_kql(credential, workspace_id, query)


def detect_conditional_access_changes(credential, workspace_id):
    """Detect modifications to Conditional Access policies."""
    query = """
    AuditLogs
    | where OperationName has_any (
        "Update conditional access policy",
        "Delete conditional access policy"
    )
    | project TimeGenerated, InitiatedBy, OperationName,
              TargetResources, Result
    | order by TimeGenerated desc
    """
    return run_kql(credential, workspace_id, query)


def main():
    parser = argparse.ArgumentParser(description="Azure Activity Log Threat Detection Agent")
    parser.add_argument("--workspace-id", default=os.getenv("AZURE_WORKSPACE_ID"))
    parser.add_argument("--tenant-id", default=os.getenv("AZURE_TENANT_ID"))
    parser.add_argument("--client-id", default=os.getenv("AZURE_CLIENT_ID"))
    parser.add_argument("--client-secret", default=os.getenv("AZURE_CLIENT_SECRET"))
    parser.add_argument("--output", default="azure_threat_report.json")
    parser.add_argument("--action", choices=[
        "privesc", "nsg", "keyvault", "travel", "deletion", "full_hunt"
    ], default="full_hunt")
    args = parser.parse_args()

    cred = get_credential(args.tenant_id, args.client_id, args.client_secret)
    report = {"generated_at": datetime.utcnow().isoformat(), "findings": {}}

    if args.action in ("privesc", "full_hunt"):
        results = detect_privilege_escalation(cred, args.workspace_id)
        report["findings"]["privilege_escalation"] = results
        print(f"[+] Privilege escalation events: {len(results)}")

    if args.action in ("nsg", "full_hunt"):
        results = detect_nsg_changes(cred, args.workspace_id)
        report["findings"]["nsg_changes"] = results
        print(f"[+] NSG changes: {len(results)}")

    if args.action in ("keyvault", "full_hunt"):
        results = detect_keyvault_access(cred, args.workspace_id)
        report["findings"]["keyvault_anomalies"] = results
        print(f"[+] Key Vault anomalies: {len(results)}")

    if args.action in ("travel", "full_hunt"):
        results = detect_impossible_travel(cred, args.workspace_id)
        report["findings"]["impossible_travel"] = results
        print(f"[+] Impossible travel: {len(results)}")

    if args.action in ("deletion", "full_hunt"):
        results = detect_resource_deletion(cred, args.workspace_id)
        report["findings"]["mass_deletion"] = results
        print(f"[+] Mass deletion events: {len(results)}")

    with open(args.output, "w") as f:
        json.dump(report, f, indent=2, default=str)
    print(f"[+] Report saved to {args.output}")


if __name__ == "__main__":
    main()
Keep exploring