Skip to content

Microsoft Sentinel Cheatsheet

Microsoft Sentinel is a cloud-native Security Information and Event Management (SIEM) and Security Orchestration, Automation, and Response (SOAR) solution that provides intelligent security analytics and threat intelligence across the enterprise. Built on Azure, Sentinel delivers scalable security monitoring, advanced threat detection, and automated incident response capabilities that enable security teams to detect, investigate, and respond to threats at cloud scale.

Platform Overview

Core Architecture

Microsoft Sentinel operates as a fully managed service within the Azure ecosystem, leveraging Azure Monitor Logs as its underlying data platform. The architecture consists of several integrated components that work together to provide comprehensive security operations capabilities.

The data ingestion layer supports a wide variety of data sources through native connectors, REST APIs, and custom integrations. Sentinel can collect data from Microsoft services (Office 365, Azure AD, Windows Security Events), third-party security tools, cloud platforms, network devices, and custom applications.

The analytics engine utilizes machine learning algorithms, behavioral analytics, and rule-based detection to identify potential security threats. Sentinel's detection capabilities include built-in analytics rules, custom KQL (Kusto Query Language) queries, machine learning models for anomaly detection, and threat intelligence integration.

Key Features

powershell
# Core Platform Capabilities
- Cloud-native SIEM and SOAR platform
- Advanced threat detection and analytics
- Security orchestration and automated response
- Threat intelligence integration
- Machine learning and behavioral analytics
- Custom dashboards and workbooks
- Incident management and investigation
- Threat hunting capabilities

Data Connectors and Ingestion

Microsoft Service Connectors

powershell
# Azure Active Directory connector
Connect-AzAccount
Set-AzContext -SubscriptionId "<subscription-id>"

# Enable Azure AD connector via PowerShell
$resourceGroupName = "sentinel-rg"
$workspaceName = "sentinel-workspace"
$dataConnectorId = "AzureActiveDirectory"

New-AzSentinelDataConnector -ResourceGroupName $resourceGroupName -WorkspaceName $workspaceName -Id $dataConnectorId -Kind "AzureActiveDirectory"

# Office 365 connector configuration
$office365Config = @{
    tenantId = "<tenant-id>"
    dataTypes = @{
        exchange = @{ state = "Enabled" }
        sharePoint = @{ state = "Enabled" }
        teams = @{ state = "Enabled" }
    }
}

# Azure Security Center connector
$ascConfig = @{
    subscriptionId = "<subscription-id>"
    dataTypes = @{
        alerts = @{ state = "Enabled" }
        recommendations = @{ state = "Enabled" }
    }
}

Third-Party Connectors

bash
# Syslog connector configuration
# Configure rsyslog on Linux systems
sudo nano /etc/rsyslog.d/95-omsagent.conf

# Add configuration for Sentinel
*.* @@<workspace-id>.ods.opinsights.azure.com:514

# Restart rsyslog service
sudo systemctl restart rsyslog

# CEF (Common Event Format) connector
# Configure log forwarding for CEF events
logger -p local4.warn -t CEF "CEF:0|Microsoft|ATA|1.9.0.0|AbnormalSensitiveGroupMembershipChangeSuspiciousActivity|Abnormal modification of sensitive groups|5|start=2018-12-12T18:52:58.0000000Z app=GroupMembershipChangeEvent suser=krbtgt msg=krbtgt has been added to the following sensitive groups: Domain Admins."

# Custom REST API connector
curl -X POST "https://<workspace-id>.ods.opinsights.azure.com/api/logs?api-version=2016-04-01" \
  -H "Authorization: SharedKey <workspace-id>:<shared-key>" \
  -H "Content-Type: application/json" \
  -H "Log-Type: CustomSecurityLog" \
  -d '{
    "timestamp": "2023-01-01T12:00:00Z",
    "severity": "High",
    "source_ip": "192.168.1.100",
    "event_type": "suspicious_login",
    "user": "admin@company.com"
  }'

Azure Monitor Agent Configuration

powershell
# Install Azure Monitor Agent
$vmName = "security-vm"
$resourceGroupName = "security-rg"
$location = "East US"

# Create data collection rule
$dcrConfig = @{
    location = $location
    properties = @{
        dataSources = @{
            windowsEventLogs = @(
                @{
                    name = "SecurityEvents"
                    streams = @("Microsoft-SecurityEvent")
                    xPathQueries = @(
                        "Security!*[System[(EventID=4624 or EventID=4625 or EventID=4648)]]"
                    )
                }
            )
            syslog = @(
                @{
                    name = "SyslogEvents"
                    streams = @("Microsoft-Syslog")
                    facilityNames = @("auth", "authpriv", "daemon")
                    logLevels = @("Warning", "Error", "Critical")
                }
            )
        }
        destinations = @{
            logAnalytics = @(
                @{
                    workspaceResourceId = "/subscriptions/<subscription-id>/resourceGroups/<rg-name>/providers/Microsoft.OperationalInsights/workspaces/<workspace-name>"
                    name = "SentinelWorkspace"
                }
            )
        }
        dataFlows = @(
            @{
                streams = @("Microsoft-SecurityEvent")
                destinations = @("SentinelWorkspace")
            }
        )
    }
}

KQL (Kusto Query Language) for Security Analytics

Basic KQL Syntax

kql
// Simple table query
SecurityEvent
| take 10

// Time range filtering
SecurityEvent
| where TimeGenerated > ago(1h)

// Field filtering
SecurityEvent
| where EventID == 4624
| where Account contains "admin"

// Multiple conditions
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID in (4624, 4625, 4648)
| where Account !contains "SYSTEM"

// String operations
SecurityEvent
| where Activity contains "Logon"
| where Account startswith "admin"
| where Computer endswith ".contoso.com"

Advanced Analytics Queries

kql
// Failed login analysis
SecurityEvent
| where EventID == 4625
| where TimeGenerated > ago(1h)
| summarize FailedAttempts = count() by Account, Computer, IpAddress
| where FailedAttempts > 5
| sort by FailedAttempts desc

// Privilege escalation detection
SecurityEvent
| where EventID == 4672
| where TimeGenerated > ago(24h)
| where SubjectUserName !in ("SYSTEM", "LOCAL SERVICE", "NETWORK SERVICE")
| summarize ElevationCount = count() by SubjectUserName, Computer
| where ElevationCount > 10
| sort by ElevationCount desc

// Lateral movement detection
SecurityEvent
| where EventID == 4624
| where LogonType in (3, 10)
| where TimeGenerated > ago(1h)
| summarize UniqueComputers = dcount(Computer) by Account
| where UniqueComputers > 5
| sort by UniqueComputers desc

// Anomalous process execution
SecurityEvent
| where EventID == 4688
| where TimeGenerated > ago(1h)
| where Process contains "powershell.exe" or Process contains "cmd.exe"
| where CommandLine contains "Invoke-" or CommandLine contains "Download"
| project TimeGenerated, Computer, Account, Process, CommandLine
| sort by TimeGenerated desc

Threat Hunting Queries

kql
// Suspicious PowerShell activity
SecurityEvent
| where EventID == 4688
| where Process endswith "powershell.exe"
| where CommandLine has_any ("Invoke-Expression", "IEX", "Invoke-WebRequest", "DownloadString", "EncodedCommand")
| extend DecodedCommand = base64_decode_tostring(extract(@"-EncodedCommand\s+([A-Za-z0-9+/=]+)", 1, CommandLine))
| project TimeGenerated, Computer, Account, CommandLine, DecodedCommand
| sort by TimeGenerated desc

// Living off the land techniques
SecurityEvent
| where EventID == 4688
| where Process has_any ("certutil.exe", "bitsadmin.exe", "regsvr32.exe", "rundll32.exe", "mshta.exe")
| where CommandLine has_any ("http", "ftp", "download", "urlcache", "split")
| project TimeGenerated, Computer, Account, Process, CommandLine
| sort by TimeGenerated desc

// Credential dumping detection
SecurityEvent
| where EventID in (4656, 4663)
| where ObjectName has_any ("lsass.exe", "SAM", "SECURITY", "SYSTEM")
| where AccessMask in ("0x1010", "0x1400", "0x100000")
| summarize AccessAttempts = count() by Account, Computer, ObjectName
| where AccessAttempts > 3
| sort by AccessAttempts desc

// DNS tunneling detection
DnsEvents
| where TimeGenerated > ago(1h)
| where QueryType == "TXT" or QueryType == "NULL"
| where Name contains "." and strlen(Name) > 50
| summarize QueryCount = count() by ClientIP, Name
| where QueryCount > 10
| sort by QueryCount desc

Machine Learning Analytics

kql
// User behavior analytics
SigninLogs
| where TimeGenerated > ago(30d)
| where ResultType == "0"
| summarize 
    LoginCount = count(),
    UniqueIPs = dcount(IPAddress),
    UniqueLocations = dcount(Location),
    UniqueDevices = dcount(DeviceDetail.deviceId)
    by UserPrincipalName
| extend RiskScore = (UniqueIPs * 2) + (UniqueLocations * 3) + (UniqueDevices * 1)
| where RiskScore > 20
| sort by RiskScore desc

// Anomalous data transfer detection
CommonSecurityLog
| where TimeGenerated > ago(1h)
| where DeviceVendor == "Palo Alto Networks"
| where Activity == "TRAFFIC"
| summarize TotalBytes = sum(SentBytes + ReceivedBytes) by SourceIP, DestinationIP
| extend BytesInMB = TotalBytes / (1024 * 1024)
| where BytesInMB > 1000
| sort by BytesInMB desc

// Behavioral baseline deviation
SecurityEvent
| where TimeGenerated > ago(30d)
| where EventID == 4624
| where LogonType == 2
| summarize 
    AvgLoginsPerDay = count() / 30,
    StdDev = stdev(todouble(1))
    by Account
| join kind=inner (
    SecurityEvent
    | where TimeGenerated > ago(1d)
    | where EventID == 4624
    | where LogonType == 2
    | summarize TodayLogins = count() by Account
) on Account
| extend Deviation = abs(TodayLogins - AvgLoginsPerDay) / StdDev
| where Deviation > 3
| sort by Deviation desc

Analytics Rules and Detection

Scheduled Analytics Rules

kql
// Create scheduled rule for brute force detection
let threshold = 10;
SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4625
| summarize FailedAttempts = count() by Account, IpAddress, Computer
| where FailedAttempts >= threshold
| extend 
    Severity = case(
        FailedAttempts >= 50, "High",
        FailedAttempts >= 25, "Medium",
        "Low"
    ),
    Tactics = "CredentialAccess",
    Techniques = "T1110"
| project Account, IpAddress, Computer, FailedAttempts, Severity, Tactics, Techniques

// Suspicious file creation rule
SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4663
| where ObjectName endswith ".exe" or ObjectName endswith ".dll" or ObjectName endswith ".bat"
| where ObjectName has_any ("temp", "appdata", "programdata")
| where AccessMask == "0x2"
| extend 
    Severity = "Medium",
    Tactics = "Execution",
    Techniques = "T1059"
| project TimeGenerated, Computer, Account, ObjectName, Severity, Tactics, Techniques

// Privilege escalation rule
SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4672
| where SubjectUserName !in ("SYSTEM", "LOCAL SERVICE", "NETWORK SERVICE")
| where PrivilegeList has_any ("SeDebugPrivilege", "SeTakeOwnershipPrivilege", "SeLoadDriverPrivilege")
| extend 
    Severity = "High",
    Tactics = "PrivilegeEscalation",
    Techniques = "T1134"
| project TimeGenerated, Computer, SubjectUserName, PrivilegeList, Severity, Tactics, Techniques

Near Real-Time (NRT) Rules

kql
// Real-time malware detection
SecurityEvent
| where EventID == 4688
| where Process has_any ("mimikatz", "procdump", "pwdump", "fgdump")
| extend 
    Severity = "High",
    Tactics = "CredentialAccess",
    Techniques = "T1003"
| project TimeGenerated, Computer, Account, Process, CommandLine, Severity, Tactics, Techniques

// Immediate threat response
SecurityEvent
| where EventID == 4625
| where Account == "Administrator"
| where IpAddress !startswith "192.168." and IpAddress !startswith "10." and IpAddress !startswith "172."
| extend 
    Severity = "Critical",
    Tactics = "InitialAccess",
    Techniques = "T1078"
| project TimeGenerated, Computer, Account, IpAddress, Severity, Tactics, Techniques

Machine Learning Rules

kql
// Anomaly detection template
let training_data = 
    SecurityEvent
    | where TimeGenerated between (ago(30d) .. ago(1d))
    | where EventID == 4624
    | summarize LoginCount = count() by Account, bin(TimeGenerated, 1h)
    | summarize AvgLogins = avg(LoginCount), StdDev = stdev(LoginCount) by Account;
SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4624
| summarize CurrentLogins = count() by Account
| join kind=inner training_data on Account
| extend AnomalyScore = abs(CurrentLogins - AvgLogins) / StdDev
| where AnomalyScore > 3
| extend 
    Severity = case(AnomalyScore > 5, "High", "Medium"),
    Tactics = "InitialAccess",
    Techniques = "T1078"
| project Account, CurrentLogins, AvgLogins, AnomalyScore, Severity, Tactics, Techniques

Incident Management and Investigation

Incident Creation and Assignment

powershell
# Create incident via PowerShell
$incidentParams = @{
    ResourceGroupName = "sentinel-rg"
    WorkspaceName = "sentinel-workspace"
    IncidentId = "incident-001"
    Title = "Suspicious Login Activity Detected"
    Description = "Multiple failed login attempts from external IP address"
    Severity = "High"
    Status = "New"
    Owner = @{
        email = "analyst@company.com"
        assignedTo = "Security Analyst"
        objectId = "<user-object-id>"
    }
    Labels = @(
        @{ labelName = "BruteForce"; labelType = "User" }
        @{ labelName = "ExternalIP"; labelType = "User" }
    )
}

New-AzSentinelIncident @incidentParams

# Update incident status
$updateParams = @{
    ResourceGroupName = "sentinel-rg"
    WorkspaceName = "sentinel-workspace"
    IncidentId = "incident-001"
    Status = "Active"
    Classification = "TruePositive"
    ClassificationComment = "Confirmed malicious activity"
    ClassificationReason = "SuspiciousActivity"
}

Update-AzSentinelIncident @updateParams

Investigation Queries

kql
// Timeline reconstruction
let incident_time = datetime(2023-01-01T10:00:00Z);
let suspect_user = "john.doe@company.com";
let time_window = 2h;
union 
    (SecurityEvent | where TimeGenerated between ((incident_time - time_window) .. (incident_time + time_window)) | where Account contains suspect_user),
    (SigninLogs | where TimeGenerated between ((incident_time - time_window) .. (incident_time + time_window)) | where UserPrincipalName == suspect_user),
    (AuditLogs | where TimeGenerated between ((incident_time - time_window) .. (incident_time + time_window)) | where InitiatedBy.user.userPrincipalName == suspect_user)
| sort by TimeGenerated asc
| project TimeGenerated, Type, Activity, Account, Computer, IPAddress, Location

// Entity behavior analysis
let entity_ip = "192.168.1.100";
union 
    (SecurityEvent | where IpAddress == entity_ip),
    (CommonSecurityLog | where SourceIP == entity_ip or DestinationIP == entity_ip),
    (DnsEvents | where ClientIP == entity_ip),
    (NetworkCommunicationEvents | where LocalIP == entity_ip or RemoteIP == entity_ip)
| summarize 
    EventCount = count(),
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    EventTypes = make_set(Type),
    UniqueDestinations = dcount(DestinationIP)
    by SourceIP
| sort by EventCount desc

// Lateral movement tracking
let start_computer = "WORKSTATION-01";
SecurityEvent
| where EventID == 4624
| where LogonType in (3, 10)
| where Computer == start_computer
| extend NextHop = Computer
| join kind=inner (
    SecurityEvent
    | where EventID == 4624
    | where LogonType in (3, 10)
    | where Computer != start_computer
) on Account
| where TimeGenerated1 > TimeGenerated
| project Account, SourceComputer = Computer, DestinationComputer = Computer1, TimeGenerated, TimeGenerated1
| sort by Account, TimeGenerated asc

Evidence Collection

kql
// Collect relevant artifacts
let investigation_timeframe = ago(24h);
let entities = dynamic(["suspicious.user@company.com", "192.168.1.100", "COMPROMISED-PC"]);
union 
    (SecurityEvent 
     | where TimeGenerated > investigation_timeframe
     | where Account has_any (entities) or Computer has_any (entities) or IpAddress has_any (entities)),
    (SigninLogs 
     | where TimeGenerated > investigation_timeframe
     | where UserPrincipalName has_any (entities) or IPAddress has_any (entities)),
    (DeviceEvents 
     | where TimeGenerated > investigation_timeframe
     | where DeviceName has_any (entities) or InitiatingProcessAccountName has_any (entities)),
    (EmailEvents 
     | where TimeGenerated > investigation_timeframe
     | where SenderFromAddress has_any (entities) or RecipientEmailAddress has_any (entities))
| sort by TimeGenerated desc
| take 1000

// File hash analysis
DeviceFileEvents
| where TimeGenerated > ago(7d)
| where SHA256 in ("hash1", "hash2", "hash3")
| summarize 
    FirstSeen = min(TimeGenerated),
    LastSeen = max(TimeGenerated),
    DeviceCount = dcount(DeviceName),
    Devices = make_set(DeviceName)
    by SHA256, FileName
| sort by DeviceCount desc

Automation and Orchestration (SOAR)

Logic Apps Integration

json
{
    "definition": {
        "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
        "contentVersion": "1.0.0.0",
        "parameters": {},
        "triggers": {
            "Microsoft_Sentinel_incident": {
                "type": "ApiConnectionWebhook",
                "inputs": {
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['azuresentinel']['connectionId']"
                        }
                    },
                    "body": {
                        "callback_url": "@{listCallbackUrl()}"
                    },
                    "path": "/incident-creation"
                }
            }
        },
        "actions": {
            "Get_incident": {
                "type": "ApiConnection",
                "inputs": {
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['azuresentinel']['connectionId']"
                        }
                    },
                    "method": "get",
                    "path": "/Incidents/subscriptions/@{encodeURIComponent(triggerBody()?['WorkspaceSubscriptionId'])}/resourceGroups/@{encodeURIComponent(triggerBody()?['WorkspaceResourceGroup'])}/workspaces/@{encodeURIComponent(triggerBody()?['WorkspaceId'])}/alerts/@{encodeURIComponent(triggerBody()?['SystemAlertId'])}"
                }
            },
            "Block_IP_in_firewall": {
                "type": "Http",
                "inputs": {
                    "method": "POST",
                    "uri": "https://firewall-api.company.com/block",
                    "headers": {
                        "Authorization": "Bearer @{parameters('firewall_token')}"
                    },
                    "body": {
                        "ip_address": "@{body('Get_incident')?['properties']?['relatedEntities'][0]?['properties']?['address']}",
                        "reason": "Blocked by Sentinel automation",
                        "duration": "24h"
                    }
                }
            },
            "Send_notification": {
                "type": "ApiConnection",
                "inputs": {
                    "host": {
                        "connection": {
                            "name": "@parameters('$connections')['teams']['connectionId']"
                        }
                    },
                    "method": "post",
                    "path": "/v1.0/teams/@{encodeURIComponent('security-team-id')}/channels/@{encodeURIComponent('incidents')}/messages",
                    "body": {
                        "body": {
                            "content": "🚨 High severity incident detected: @{body('Get_incident')?['properties']?['title']}\n\nIncident ID: @{body('Get_incident')?['properties']?['incidentNumber']}\nSeverity: @{body('Get_incident')?['properties']?['severity']}\nStatus: @{body('Get_incident')?['properties']?['status']}\n\nAutomatic response: IP address blocked in firewall"
                        }
                    }
                }
            }
        }
    }
}

PowerShell Automation

powershell
# Automated incident response script
param(
    [Parameter(Mandatory=$true)]
    [string]$IncidentId,
    
    [Parameter(Mandatory=$true)]
    [string]$WorkspaceName,
    
    [Parameter(Mandatory=$true)]
    [string]$ResourceGroupName
)

# Connect to Azure
Connect-AzAccount -Identity

# Get incident details
$incident = Get-AzSentinelIncident -ResourceGroupName $ResourceGroupName -WorkspaceName $WorkspaceName -IncidentId $IncidentId

# Extract entities
$entities = $incident.RelatedEntities

# Process each entity
foreach ($entity in $entities) {
    switch ($entity.Kind) {
        "Account" {
            # Disable user account
            $userPrincipalName = $entity.Properties.Name
            Write-Output "Disabling user account: $userPrincipalName"
            
            # Connect to Azure AD
            Connect-AzureAD
            $user = Get-AzureADUser -Filter "userPrincipalName eq '$userPrincipalName'"
            if ($user) {
                Set-AzureADUser -ObjectId $user.ObjectId -AccountEnabled $false
                Write-Output "User account disabled successfully"
            }
        }
        
        "Ip" {
            # Block IP address
            $ipAddress = $entity.Properties.Address
            Write-Output "Blocking IP address: $ipAddress"
            
            # Add to Azure Firewall block list
            $firewallPolicy = Get-AzFirewallPolicy -ResourceGroupName $ResourceGroupName -Name "SecurityPolicy"
            $blockRule = New-AzFirewallPolicyNetworkRule -Name "Block-$ipAddress" -Protocol "Any" -SourceAddress $ipAddress -DestinationAddress "*" -DestinationPort "*" -Action "Deny"
            
            # Update firewall policy
            $ruleCollection = $firewallPolicy.RuleCollectionGroups[0].Properties.RuleCollections[0]
            $ruleCollection.Rules += $blockRule
            Set-AzFirewallPolicy -InputObject $firewallPolicy
            Write-Output "IP address blocked successfully"
        }
        
        "Host" {
            # Isolate host
            $hostName = $entity.Properties.HostName
            Write-Output "Isolating host: $hostName"
            
            # Microsoft Defender for Endpoint isolation
            $isolationRequest = @{
                Comment = "Automated isolation due to security incident $IncidentId"
                IsolationType = "Full"
            }
            
            # Call Defender API to isolate machine
            $headers = @{
                'Authorization' = "Bearer $($env:DEFENDER_TOKEN)"
                'Content-Type' = 'application/json'
            }
            
            Invoke-RestMethod -Uri "https://api.securitycenter.microsoft.com/api/machines/$hostName/isolate" -Method Post -Headers $headers -Body ($isolationRequest | ConvertTo-Json)
            Write-Output "Host isolated successfully"
        }
    }
}

# Update incident with automation results
$updateComment = "Automated response completed: Users disabled, IPs blocked, hosts isolated"
Add-AzSentinelIncidentComment -ResourceGroupName $ResourceGroupName -WorkspaceName $WorkspaceName -IncidentId $IncidentId -Message $updateComment

# Update incident status
Update-AzSentinelIncident -ResourceGroupName $ResourceGroupName -WorkspaceName $WorkspaceName -IncidentId $IncidentId -Status "Active" -Classification "TruePositive"

Write-Output "Incident response automation completed successfully"

Custom Connectors

python
# Custom threat intelligence connector
import requests
import json
import hashlib
import hmac
import base64
import datetime
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

class SentinelConnector:
    def __init__(self, workspace_id, shared_key):
        self.workspace_id = workspace_id
        self.shared_key = shared_key
        self.log_type = "ThreatIntelligence"
        
    def build_signature(self, date, content_length, method, content_type, resource):
        x_headers = f"x-ms-date:{date}"
        string_to_hash = f"{method}\n{content_length}\n{content_type}\n{x_headers}\n{resource}"
        bytes_to_hash = bytes(string_to_hash, 'utf-8')
        decoded_key = base64.b64decode(self.shared_key)
        encoded_hash = base64.b64encode(hmac.new(decoded_key, bytes_to_hash, digestmod=hashlib.sha256).digest()).decode()
        authorization = f"SharedKey {self.workspace_id}:{encoded_hash}"
        return authorization

    def send_data(self, body):
        method = 'POST'
        content_type = 'application/json'
        resource = '/api/logs'
        rfc1123date = datetime.datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
        content_length = len(body)
        
        signature = self.build_signature(rfc1123date, content_length, method, content_type, resource)
        
        uri = f"https://{self.workspace_id}.ods.opinsights.azure.com{resource}?api-version=2016-04-01"
        
        headers = {
            'content-type': content_type,
            'Authorization': signature,
            'Log-Type': self.log_type,
            'x-ms-date': rfc1123date
        }
        
        response = requests.post(uri, data=body, headers=headers)
        return response.status_code

# Threat intelligence integration
def collect_threat_intelligence():
    # Collect from multiple sources
    threat_data = []
    
    # VirusTotal integration
    vt_api_key = "your_virustotal_api_key"
    vt_url = "https://www.virustotal.com/vtapi/v2/file/report"
    
    file_hashes = ["hash1", "hash2", "hash3"]
    
    for file_hash in file_hashes:
        params = {'apikey': vt_api_key, 'resource': file_hash}
        response = requests.get(vt_url, params=params)
        
        if response.status_code == 200:
            vt_data = response.json()
            threat_data.append({
                "timestamp": datetime.datetime.utcnow().isoformat(),
                "source": "VirusTotal",
                "indicator_type": "file_hash",
                "indicator_value": file_hash,
                "threat_type": "malware" if vt_data.get('positives', 0) > 0 else "clean",
                "confidence": vt_data.get('positives', 0) / vt_data.get('total', 1) * 100,
                "raw_data": json.dumps(vt_data)
            })
    
    # MISP integration
    misp_url = "https://your-misp-instance.com"
    misp_key = "your_misp_api_key"
    
    misp_headers = {
        'Authorization': misp_key,
        'Accept': 'application/json',
        'Content-Type': 'application/json'
    }
    
    misp_response = requests.get(f"{misp_url}/attributes/restSearch", headers=misp_headers)
    
    if misp_response.status_code == 200:
        misp_data = misp_response.json()
        for attribute in misp_data.get('response', {}).get('Attribute', []):
            threat_data.append({
                "timestamp": datetime.datetime.utcnow().isoformat(),
                "source": "MISP",
                "indicator_type": attribute.get('type'),
                "indicator_value": attribute.get('value'),
                "threat_type": attribute.get('category'),
                "confidence": 85,
                "raw_data": json.dumps(attribute)
            })
    
    return threat_data

# Send to Sentinel
def main():
    workspace_id = "your_workspace_id"
    shared_key = "your_shared_key"
    
    connector = SentinelConnector(workspace_id, shared_key)
    threat_data = collect_threat_intelligence()
    
    if threat_data:
        body = json.dumps(threat_data)
        response_code = connector.send_data(body)
        print(f"Data sent to Sentinel. Response code: {response_code}")
    else:
        print("No threat intelligence data to send")

if __name__ == "__main__":
    main()

Workbooks and Visualization

Security Operations Workbook

kql
// Security overview dashboard queries
// Failed logins by location
SigninLogs
| where TimeGenerated > ago(24h)
| where ResultType != "0"
| summarize FailedLogins = count() by Location
| sort by FailedLogins desc
| take 10

// Top attacked users
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID == 4625
| summarize FailedAttempts = count() by Account
| sort by FailedAttempts desc
| take 10

// Security alerts by severity
SecurityAlert
| where TimeGenerated > ago(7d)
| summarize AlertCount = count() by AlertSeverity
| render piechart

// Incident response metrics
SecurityIncident
| where TimeGenerated > ago(30d)
| extend TimeToClose = iff(Status == "Closed", ClosedTime - CreatedTime, now() - CreatedTime)
| summarize 
    AvgTimeToClose = avg(TimeToClose),
    TotalIncidents = count(),
    OpenIncidents = countif(Status != "Closed")
    by bin(TimeGenerated, 1d)
| render timechart

Threat Hunting Workbook

kql
// Suspicious process execution timeline
SecurityEvent
| where TimeGenerated > ago(7d)
| where EventID == 4688
| where Process has_any ("powershell.exe", "cmd.exe", "wscript.exe", "cscript.exe")
| where CommandLine has_any ("Invoke-", "Download", "http", "ftp", "base64")
| summarize ProcessCount = count() by bin(TimeGenerated, 1h), Process
| render timechart

// Network communication patterns
CommonSecurityLog
| where TimeGenerated > ago(24h)
| where DeviceVendor == "Palo Alto Networks"
| where Activity == "TRAFFIC"
| summarize 
    TotalBytes = sum(SentBytes + ReceivedBytes),
    SessionCount = count()
    by SourceIP, DestinationIP
| where TotalBytes > 1000000000  // 1GB threshold
| sort by TotalBytes desc

// DNS query analysis
DnsEvents
| where TimeGenerated > ago(24h)
| where QueryType in ("TXT", "NULL", "CNAME")
| summarize QueryCount = count() by Name, ClientIP
| where QueryCount > 100
| sort by QueryCount desc

// File creation in suspicious locations
SecurityEvent
| where TimeGenerated > ago(24h)
| where EventID == 4663
| where ObjectName has_any ("\\temp\\", "\\appdata\\", "\\programdata\\")
| where ObjectName endswith ".exe" or ObjectName endswith ".dll" or ObjectName endswith ".bat"
| summarize FileCount = count() by Computer, Account, ObjectName
| sort by FileCount desc

Performance Optimization and Best Practices

Query Optimization

kql
// Optimized query structure
SecurityEvent
| where TimeGenerated > ago(1h)  // Filter time first
| where EventID == 4624          // Filter specific events
| where LogonType == 2           // Additional filters early
| project TimeGenerated, Computer, Account, LogonType  // Project only needed columns
| summarize LoginCount = count() by Computer, Account
| where LoginCount > 10
| sort by LoginCount desc

// Use summarize instead of distinct when possible
// Less efficient
SecurityEvent
| where TimeGenerated > ago(1h)
| distinct Computer

// More efficient
SecurityEvent
| where TimeGenerated > ago(1h)
| summarize by Computer

// Optimize joins
// Use smaller table on the right side of join
let SmallTable = 
    SecurityEvent
    | where TimeGenerated > ago(1h)
    | where EventID == 4624
    | summarize by Account;
SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4625
| join kind=inner SmallTable on Account

Data Retention and Cost Management

kql
// Monitor data ingestion by table
Usage
| where TimeGenerated > ago(30d)
| where IsBillable == true
| summarize TotalVolumeGB = sum(Quantity) / 1000 by DataType
| sort by TotalVolumeGB desc

// Analyze query costs
// Check scan data volume
SecurityEvent
| where TimeGenerated > ago(1h)
| summarize count()
// This query scans all SecurityEvent data for 1 hour

// Optimize with specific filters
SecurityEvent
| where TimeGenerated > ago(1h)
| where EventID == 4624  // Reduces scan volume significantly
| summarize count()

// Set up data retention policies
// Configure via PowerShell
$retentionPolicy = @{
    ResourceGroupName = "sentinel-rg"
    WorkspaceName = "sentinel-workspace"
    TableName = "SecurityEvent"
    RetentionInDays = 90
}

Set-AzOperationalInsightsTable @retentionPolicy

Security and Compliance

kql
// Monitor privileged access to Sentinel
AuditLogs
| where TimeGenerated > ago(24h)
| where Category == "RoleManagement"
| where ActivityDisplayName has_any ("Add member to role", "Remove member from role")
| where TargetResources[0].displayName has "Sentinel"
| project TimeGenerated, InitiatedBy.user.userPrincipalName, ActivityDisplayName, TargetResources[0].displayName

// Track data access patterns
LAQueryLogs
| where TimeGenerated > ago(7d)
| summarize 
    QueryCount = count(),
    DataScannedGB = sum(DataScanned_MB) / 1000,
    UniqueQueries = dcount(QueryText)
    by AADEmail
| sort by DataScannedGB desc

// Compliance reporting
SecurityEvent
| where TimeGenerated > ago(30d)
| where EventID in (4624, 4625, 4648, 4672)
| summarize 
    TotalEvents = count(),
    SuccessfulLogins = countif(EventID == 4624),
    FailedLogins = countif(EventID == 4625),
    PrivilegeUse = countif(EventID == 4672)
    by bin(TimeGenerated, 1d)
| extend ComplianceScore = (SuccessfulLogins * 1.0) / (SuccessfulLogins + FailedLogins) * 100
| project TimeGenerated, TotalEvents, ComplianceScore

Troubleshooting and Maintenance

Common Issues and Solutions

kql
// Check data connector health
Heartbeat
| where TimeGenerated > ago(1h)
| summarize LastHeartbeat = max(TimeGenerated) by Computer
| where LastHeartbeat < ago(15m)
| project Computer, LastHeartbeat, Status = "Missing"

// Verify data ingestion
Usage
| where TimeGenerated > ago(1h)
| summarize IngestedMB = sum(Quantity) by DataType
| where IngestedMB == 0
| project DataType, Status = "No data ingested"

// Analytics rule performance
SecurityAlert
| where TimeGenerated > ago(24h)
| summarize AlertCount = count() by AlertName
| join kind=leftouter (
    SecurityIncident
    | where TimeGenerated > ago(24h)
    | summarize IncidentCount = count() by Title
) on $left.AlertName == $right.Title
| extend FalsePositiveRate = (AlertCount - IncidentCount) * 100.0 / AlertCount
| where FalsePositiveRate > 80
| project AlertName, AlertCount, IncidentCount, FalsePositiveRate

Performance Monitoring

kql
// Query performance analysis
LAQueryLogs
| where TimeGenerated > ago(24h)
| where ResponseCode == 200
| summarize 
    AvgDuration = avg(DurationMs),
    MaxDuration = max(DurationMs),
    QueryCount = count()
    by bin(TimeGenerated, 1h)
| render timechart

// Resource utilization
Perf
| where TimeGenerated > ago(1h)
| where ObjectName == "Processor" and CounterName == "% Processor Time"
| summarize AvgCPU = avg(CounterValue) by Computer
| where AvgCPU > 80

// Storage utilization
Usage
| where TimeGenerated > ago(30d)
| summarize TotalGB = sum(Quantity) / 1000 by Solution
| sort by TotalGB desc

Resources