Back to Blog
5 min read

Azure Sentinel SOAR: Automating Security Response

Azure Sentinel is Microsoft’s cloud-native SIEM (Security Information and Event Management). Its SOAR (Security Orchestration, Automation, and Response) capabilities enable automated responses to security threats. Let’s explore how to build effective security automation.

What is SOAR?

SOAR combines three capabilities:

  • Orchestration: Connecting security tools
  • Automation: Automatic response to threats
  • Response: Standardized incident handling

In Sentinel, SOAR is implemented through Playbooks (Logic Apps) and Automation Rules.

Setting Up Azure Sentinel

# Create Log Analytics workspace
az monitor log-analytics workspace create \
    --name sentinel-workspace \
    --resource-group security-rg \
    --location eastus \
    --retention-time 90

# Enable Sentinel on workspace
az sentinel onboarding-state create \
    --workspace-name sentinel-workspace \
    --resource-group security-rg

Creating Playbooks

Playbooks are Logic Apps triggered by Sentinel incidents:

{
  "definition": {
    "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
    "triggers": {
      "Microsoft_Sentinel_incident": {
        "type": "ApiConnectionWebhook",
        "inputs": {
          "body": {
            "callback_url": "@{listCallbackUrl()}"
          },
          "host": {
            "connection": {
              "name": "@parameters('$connections')['azuresentinel']['connectionId']"
            }
          },
          "path": "/incident-creation"
        }
      }
    },
    "actions": {
      "Get_incident": {
        "type": "ApiConnection",
        "inputs": {
          "host": {
            "connection": {
              "name": "@parameters('$connections')['azuresentinel']['connectionId']"
            }
          },
          "method": "get",
          "path": "/Incidents/subscriptions/@{triggerBody()?['workspaceSubscriptionId']}/resourceGroups/@{triggerBody()?['workspaceResourceGroup']}/workspaces/@{triggerBody()?['workspaceId']}/incidents/@{triggerBody()?['object']?['properties']?['incidentNumber']}"
        }
      },
      "Enrich_with_TI": {
        "type": "Http",
        "inputs": {
          "method": "GET",
          "uri": "https://api.threatintelligence.com/lookup",
          "queries": {
            "ip": "@{body('Get_incident')?['properties']?['relatedEntities'][0]?['properties']?['address']}"
          },
          "headers": {
            "Authorization": "Bearer @{parameters('TI_API_Key')}"
          }
        },
        "runAfter": {
          "Get_incident": ["Succeeded"]
        }
      },
      "Update_incident_with_enrichment": {
        "type": "ApiConnection",
        "inputs": {
          "host": {
            "connection": {
              "name": "@parameters('$connections')['azuresentinel']['connectionId']"
            }
          },
          "method": "put",
          "path": "/Incidents",
          "body": {
            "incidentArmId": "@{body('Get_incident')?['id']}",
            "tagsToAdd": {
              "TagsToAdd": [
                {
                  "Tag": "@{body('Enrich_with_TI')?['reputation']}"
                }
              ]
            }
          }
        },
        "runAfter": {
          "Enrich_with_TI": ["Succeeded"]
        }
      }
    }
  }
}

Common Playbook Patterns

Block IP Address

{
  "actions": {
    "Get_IP_entities": {
      "type": "ApiConnection",
      "inputs": {
        "host": {
          "connection": {
            "name": "@parameters('$connections')['azuresentinel']['connectionId']"
          }
        },
        "method": "post",
        "path": "/entities/ip"
      }
    },
    "For_each_IP": {
      "type": "Foreach",
      "foreach": "@body('Get_IP_entities')?['IPs']",
      "actions": {
        "Add_to_NSG_deny_rule": {
          "type": "ApiConnection",
          "inputs": {
            "host": {
              "connection": {
                "name": "@parameters('$connections')['arm']['connectionId']"
              }
            },
            "method": "patch",
            "path": "/subscriptions/@{parameters('SubscriptionId')}/resourceGroups/@{parameters('ResourceGroup')}/providers/Microsoft.Network/networkSecurityGroups/@{parameters('NSGName')}/securityRules/DenyMaliciousIPs",
            "body": {
              "properties": {
                "sourceAddressPrefixes": "@union(body('Get_current_NSG_rule')?['properties']?['sourceAddressPrefixes'], array(items('For_each_IP')?['Address']))"
              }
            }
          }
        }
      },
      "runAfter": {
        "Get_IP_entities": ["Succeeded"]
      }
    }
  }
}

Disable Compromised User

# Azure Function for user remediation
import logging
import azure.functions as func
from msgraph.core import GraphClient
from azure.identity import DefaultAzureCredential

def main(req: func.HttpRequest) -> func.HttpResponse:
    # Get user principal name from Sentinel incident
    user_upn = req.params.get('upn')

    if not user_upn:
        return func.HttpResponse("UPN required", status_code=400)

    # Initialize Graph client
    credential = DefaultAzureCredential()
    client = GraphClient(credential=credential)

    # Disable user account
    response = client.patch(
        f'/users/{user_upn}',
        json={'accountEnabled': False}
    )

    if response.status_code == 204:
        # Revoke all sessions
        client.post(f'/users/{user_upn}/revokeSignInSessions')

        # Add to risky users
        logging.info(f"Disabled user: {user_upn}")
        return func.HttpResponse(f"User {user_upn} disabled and sessions revoked")

    return func.HttpResponse(f"Failed to disable user", status_code=500)

Isolate VM

{
  "actions": {
    "Get_VM_entities": {
      "type": "ApiConnection",
      "inputs": {
        "path": "/entities/host"
      }
    },
    "For_each_VM": {
      "type": "Foreach",
      "foreach": "@body('Get_VM_entities')?['Hosts']",
      "actions": {
        "Get_VM_network_interfaces": {
          "type": "ApiConnection",
          "inputs": {
            "method": "get",
            "path": "/subscriptions/@{parameters('SubscriptionId')}/resourceGroups/@{items('For_each_VM')?['AzureID']}/providers/Microsoft.Compute/virtualMachines/@{items('For_each_VM')?['HostName']}"
          }
        },
        "Apply_isolation_NSG": {
          "type": "ApiConnection",
          "inputs": {
            "method": "put",
            "path": "/subscriptions/@{parameters('SubscriptionId')}/resourceGroups/@{parameters('ResourceGroup')}/providers/Microsoft.Network/networkInterfaces/@{body('Get_VM_network_interfaces')?['properties']?['networkInterfaces'][0]?['id']}",
            "body": {
              "properties": {
                "networkSecurityGroup": {
                  "id": "/subscriptions/@{parameters('SubscriptionId')}/resourceGroups/@{parameters('ResourceGroup')}/providers/Microsoft.Network/networkSecurityGroups/IsolationNSG"
                }
              }
            }
          }
        }
      }
    }
  }
}

Automation Rules

Automation rules trigger playbooks automatically:

# Create automation rule
az sentinel automation-rule create \
    --name "Auto-enrich high severity incidents" \
    --resource-group security-rg \
    --workspace-name sentinel-workspace \
    --order 1 \
    --triggering-logic '{
        "isEnabled": true,
        "triggersOn": "Incidents",
        "triggersWhen": "Created",
        "conditions": [
            {
                "conditionType": "Property",
                "conditionProperties": {
                    "propertyName": "IncidentSeverity",
                    "operator": "Equals",
                    "propertyValues": ["High"]
                }
            }
        ]
    }' \
    --actions '[
        {
            "actionType": "RunPlaybook",
            "actionConfiguration": {
                "logicAppResourceId": "/subscriptions/.../logicApps/Enrich-Incident"
            }
        }
    ]'

Integration with Azure Functions

For complex logic, use Azure Functions:

# Function to analyze threat intelligence
import azure.functions as func
from azure.keyvault.secrets import SecretClient
from azure.identity import DefaultAzureCredential
import requests

def main(req: func.HttpRequest) -> func.HttpResponse:
    indicator = req.params.get('indicator')
    indicator_type = req.params.get('type')  # ip, domain, hash

    # Get API keys from Key Vault
    credential = DefaultAzureCredential()
    kv_client = SecretClient(
        vault_url="https://security-vault.vault.azure.net",
        credential=credential
    )

    # Query multiple TI sources
    results = {
        'indicator': indicator,
        'type': indicator_type,
        'sources': []
    }

    # VirusTotal
    vt_key = kv_client.get_secret("virustotal-api-key").value
    vt_response = query_virustotal(indicator, indicator_type, vt_key)
    results['sources'].append({'name': 'VirusTotal', 'data': vt_response})

    # AbuseIPDB (for IPs)
    if indicator_type == 'ip':
        abuse_key = kv_client.get_secret("abuseipdb-api-key").value
        abuse_response = query_abuseipdb(indicator, abuse_key)
        results['sources'].append({'name': 'AbuseIPDB', 'data': abuse_response})

    # Calculate risk score
    results['risk_score'] = calculate_risk_score(results['sources'])
    results['recommendation'] = get_recommendation(results['risk_score'])

    return func.HttpResponse(
        json.dumps(results),
        mimetype="application/json"
    )

def calculate_risk_score(sources):
    scores = []
    for source in sources:
        if source['name'] == 'VirusTotal':
            positives = source['data'].get('positives', 0)
            total = source['data'].get('total', 1)
            scores.append(positives / total * 100)
        elif source['name'] == 'AbuseIPDB':
            scores.append(source['data'].get('abuseConfidenceScore', 0))

    return sum(scores) / len(scores) if scores else 0

def get_recommendation(risk_score):
    if risk_score > 80:
        return "BLOCK_IMMEDIATELY"
    elif risk_score > 50:
        return "INVESTIGATE"
    else:
        return "MONITOR"

Incident Response Workflow

Complete incident response automation:

# Pseudo-code workflow
name: "Malware Detection Response"
trigger: Sentinel Incident (Malware detected)

steps:
  1. Enrich incident:
     - Query file hash in TI databases
     - Get user context from Azure AD
     - Check device compliance status

  2. Assess severity:
     - If TI score > 80 AND user is privileged:
         severity = Critical
     - Else if TI score > 50:
         severity = High

  3. Containment (if Critical):
     - Isolate affected device via Defender ATP
     - Disable user account
     - Block file hash tenant-wide

  4. Investigation:
     - Query all devices for same file hash
     - Check user's recent sign-ins
     - Look for lateral movement

  5. Notification:
     - Create ServiceNow ticket
     - Send Teams notification to SOC
     - Page on-call if after hours

  6. Documentation:
     - Add all enrichment to incident
     - Update incident status
     - Add timeline comments

Resources

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.