适用于 PowerShell 5.1 及以上版本
Microsoft Sentinel 是微软云原生的 SIEM(安全信息与事件管理)解决方案,能够收集、检测、调查和响应来自整个企业环境的安全威胁。在大型企业中,安全运营团队每天需要处理成百上千条告警,手动在门户中逐一排查效率极低。通过 PowerShell 自动化与 Sentinel API 的集成,我们可以实现告警的批量查询、自动化响应规则的部署以及威胁情报的快速推送,大幅提升安全运营效率。
本文将介绍如何使用 PowerShell 连接 Microsoft Sentinel REST API,完成查询安全告警、创建自动化分析规则、批量导入威胁指标(TI Indicator)以及导出事件报告等常见操作。这些方法不仅适用于日常安全运维,也能嵌入 CI/CD 流水线,实现安全策略的版本化管理和自动部署。
在开始之前,需要确保已安装 Az PowerShell 模块并完成身份认证。以下示例基于 Azure 资源管理器 REST API,适用于已部署 Microsoft Sentinel 工作区的环境。
连接 Sentinel 并获取工作区信息 第一步是连接 Azure 账户并获取 Sentinel 工作区的基本信息。我们需要订阅 ID、资源组名称和工作区名称三个关键参数,它们构成了所有 Sentinel API 调用的基础路径。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Install-Module -Name Az.Accounts, Az.OperationalInsights, Az.SecurityInsights -Force -Scope CurrentUserConnect-AzAccount $subscriptionId = (Get-AzContext ).Subscription.Id$resourceGroupName = "rg-security-prod" $workspaceName = "law-sentinel-prod" $sentinelBaseUri = "/subscriptions/$subscriptionId /resourceGroups/$resourceGroupName /providers/Microsoft.OperationalInsights/workspaces/$workspaceName /providers/Microsoft.SecurityInsights" Write-Host "Sentinel 基础路径: $sentinelBaseUri " Write-Host "订阅 ID: $subscriptionId "
执行结果示例:
1 2 Sentinel 基础路径: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/rg-security-prod/providers/Microsoft.OperationalInsights/workspaces/law-sentinel-prod/providers/Microsoft.SecurityInsights 订阅 ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
连接成功后,我们得到了 Sentinel API 的基础 URI。后续所有 API 调用都会在这个路径上追加具体的资源端点。如果环境中有多个订阅,可以用 Set-AzContext 切换到包含 Sentinel 工作区的订阅。
查询安全告警 Sentinel 的告警信息可以通过 REST API 直接查询。以下代码展示如何获取最近 24 小时内的活跃告警,并按严重程度进行分类汇总。这在安全运营日报自动化场景中非常实用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 $timeRange = "Last24Hours" $apiVersion = "2024-03-01" $alertUri = "https://management.azure.com" + $sentinelBaseUri + "/alerts?api-version=$apiVersion " $response = Invoke-AzRestMethod -Uri $alertUri -Method Getif ($response .StatusCode -eq 200 ) { $alerts = ($response .Content | ConvertFrom-Json ).value $severityGroups = $alerts | Group-Object -Property { if ($_ .properties.severity) { $_ .properties.severity } else { "Unknown" } } Write-Host "`n===== Sentinel 告警汇总(最近 24 小时)=====" -ForegroundColor Cyan Write-Host ("告警总数: {0}" -f $alerts .Count) Write-Host "" foreach ($group in $severityGroups ) { $severity = $group .Name $count = $group .Count $color = switch ($severity ) { "High" { "Red" } "Medium" { "Yellow" } "Low" { "Green" } "Informational" { "Gray" } default { "White" } } Write-Host (" [{0}] {1} 条" -f $severity .PadRight(14 ), $count ) -ForegroundColor $color } $highAlerts = $alerts | Where-Object { $_ .properties.severity -eq "High" } if ($highAlerts ) { Write-Host "`n===== 高危告警详情 =====" -ForegroundColor Red foreach ($alert in $highAlerts ) { Write-Host (" 名称: {0}" -f $alert .properties.alertDisplayName) Write-Host (" 时间: {0}" -f $alert .properties.startTimeUtc) Write-Host (" 状态: {0}" -f $alert .properties.status) Write-Host "" } } }
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ===== Sentinel 告警汇总(最近 24 小时)===== 告警总数: 47 [High ] 5 条 [Medium ] 18 条 [Low ] 21 条 [Informational ] 3 条 ===== 高危告警详情 ===== 名称: Suspicious PowerShell execution detected 时间: 2025-10-29T14:23:17Z 状态: New 名称: Brute force attack on Azure AD 时间: 2025-10-29T08:45:02Z 状态: New
告警查询是安全运营的日常操作,通过 PowerShell 脚本化后可以定时执行,将结果推送到 Teams 频道或邮件,实现无人值守的安全监控。高危告警的即时通知机制对于缩短平均响应时间(MTTR)至关重要。
创建自动化分析规则 Sentinel 的分析规则(Analytics Rule)是威胁检测的核心引擎。下面演示如何通过 PowerShell 创建一条自定义的分析规则,用于检测异常的远程桌面登录行为。规则的查询逻辑基于 KQL(Kusto Query Language),当匹配到可疑行为时自动触发告警。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 $ruleName = "Anomalous-RDP-Login-Detection" $ruleDisplayName = "异常 RDP 登录检测" $ruleDescription = "检测来自非常见地理位置的 RDP 登录尝试,可能是横向移动攻击的信号" $kqlQuery = @" SecurityEvent | where EventID in (4624, 4625) | where LogonType == 10 | where TimeGenerated > ago(1h) | summarize LoginCount = count(), DistinctIPs = dcount(IpAddress), IPs = make_set(IpAddress, 10) by TargetUserName, Computer | where LoginCount > 5 and DistinctIPs > 3 | project TimeGenerated = now(), TargetUserName, Computer, LoginCount, DistinctIPs, IPs | order by LoginCount desc "@ $ruleBody = @ { properties = @ { displayName = $ruleDisplayName description = $ruleDescription severity = "High" enabled = $true query = $kqlQuery queryFrequency = "PT1H" queryPeriod = "PT1H" triggerOperator = "GreaterThan" triggerThreshold = 0 suppressionDuration = "PT5H" suppressionEnabled = $false tactics = @ ("LateralMovement" ) techniques = @ ("T1021.001" ) alertRuleTemplateName = $null incidentConfiguration = @ { createIncident = $true groupingConfiguration = @ { enabled = $true reopenClosedIncident = $false lookbackDuration = "PT5H" matchingMethod = "Selected" groupByEntities = @ ("Account" ) } } } } | ConvertTo-Json -Depth 10 $ruleApiVersion = "2024-03-01" $ruleUri = "https://management.azure.com" + $sentinelBaseUri + "/alertRules/$ruleName `?api-version=$ruleApiVersion " $result = Invoke-AzRestMethod -Uri $ruleUri -Method Put -Payload $ruleBody if ($result .StatusCode -eq 200 -or $result .StatusCode -eq 201 ) { $createdRule = $result .Content | ConvertFrom-Json Write-Host "分析规则创建成功!" -ForegroundColor Green Write-Host (" 规则名称: {0}" -f $createdRule .properties.displayName) Write-Host (" 严重级别: {0}" -f $createdRule .properties.severity) Write-Host (" 查询频率: {0}" -f $createdRule .properties.queryFrequency) Write-Host (" 战术分类: {0}" -f ($createdRule .properties.tactics -join ", " )) Write-Host (" 创建事件: {0}" -f $createdRule .properties.incidentConfiguration.createIncident) }
执行结果示例:
1 2 3 4 5 6 分析规则创建成功! 规则名称: 异常 RDP 登录检测 严重级别: High 查询频率: PT1H 战术分类: LateralMovement 创建事件: True
通过脚本创建分析规则的最大优势是版本可控。将规则定义存储在 JSON 或 PowerShell 数据文件中,配合 Git 进行版本管理,团队可以追踪每次规则变更的历史,实现安全检测策略的 Infrastructure as Code(基础设施即代码)实践。
批量导入威胁指标 威胁情报(Threat Intelligence)是 Sentinel 主动防御的重要组成部分。安全团队经常需要将外部威胁情报源(如 STIX/TAXII feeds、商业情报平台)的 IoC(失陷指标)批量导入 Sentinel。以下代码展示如何通过 API 批量创建威胁指标。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 $threatIndicators = @ ( @ { displayName = "Malicious C2 Server - apt29-c2.example.com" description = "APT29 已知 C2 通信域名,关联攻击活动 UNC2452" patternType = "domain-name" pattern = "[domain-name:value = 'apt29-c2.example.com']" severity = "High" confidence = 85 source = "Internal-Threat-Intel-Platform" validFrom = (Get-Date ).ToString("yyyy-MM-ddTHH:mm:ssZ" ) validUntil = (Get-Date ).AddDays(90 ).ToString("yyyy-MM-ddTHH:mm:ssZ" ) threatTypes = @ ("malicious-activity" , "command-and-control" ) }, @ { displayName = "Suspicious IP - 198.51.100.42" description = "频繁扫描行为的来源 IP,多次触发 IDS 告警" patternType = "ipv4-addr" pattern = "[ipv4-addr:value = '198.51.100.42']" severity = "Medium" confidence = 70 source = "Honeypot-System" validFrom = (Get-Date ).ToString("yyyy-MM-ddTHH:mm:ssZ" ) validUntil = (Get-Date ).AddDays(30 ).ToString("yyyy-MM-ddTHH:mm:ssZ" ) threatTypes = @ ("malicious-activity" ) }, @ { displayName = "Phishing Domain - secure-login.example.net" description = "钓鱼攻击使用的仿冒域名,目标为财务部门" patternType = "domain-name" pattern = "[domain-name:value = 'secure-login.example.net']" severity = "High" confidence = 92 source = "Phishing-Report-System" validFrom = (Get-Date ).ToString("yyyy-MM-ddTHH:mm:ssZ" ) validUntil = (Get-Date ).AddDays(60 ).ToString("yyyy-MM-ddTHH:mm:ssZ" ) threatTypes = @ ("malicious-activity" , "phishing" ) } ) $tiApiVersion = "2024-03-01" $successCount = 0 $failCount = 0 Write-Host "开始导入威胁指标..." -ForegroundColor Cyanforeach ($indicator in $threatIndicators ) { $indicatorName = $indicator .displayName -replace '\s' , '-' -replace '[^a-zA-Z0-9\-]' , '' $indicatorName = $indicatorName .Substring(0 , [System.Math ]::Min($indicatorName .Length, 50 )) $body = @ { properties = @ { displayName = $indicator .displayName description = $indicator .description patternType = $indicator .patternType pattern = $indicator .pattern severity = $indicator .severity confidence = $indicator .confidence source = $indicator .source validFrom = $indicator .validFrom validUntil = $indicator .validUntil threatTypes = $indicator .threatTypes } kind = "indicator" } | ConvertTo-Json -Depth 5 $uri = "https://management.azure.com" + $sentinelBaseUri + "/threatIntelligence/main/indicators/$indicatorName `?api-version=$tiApiVersion " $result = Invoke-AzRestMethod -Uri $uri -Method Put -Payload $body if ($result .StatusCode -eq 200 -or $result .StatusCode -eq 201 ) { $successCount ++ Write-Host (" [OK] {0}" -f $indicator .displayName) -ForegroundColor Green } else { $failCount ++ $errorDetail = ($result .Content | ConvertFrom-Json ).error.message Write-Host (" [FAIL] {0}: {1}" -f $indicator .displayName, $errorDetail ) -ForegroundColor Red } } Write-Host "`n导入完成: 成功 $successCount 条, 失败 $failCount 条" -ForegroundColor Cyan
执行结果示例:
1 2 3 4 5 6 开始导入威胁指标... [OK] Malicious C2 Server - apt29-c2.example.com [OK] Suspicious IP - 198.51.100.42 [OK] Phishing Domain - secure-login.example.net 导入完成: 成功 3 条, 失败 0 条
批量导入威胁指标时,建议控制单次请求的数量,避免触发 API 速率限制。对于大规模的威胁情报源(数千条 IoC),可以采用分批处理的方式,每批 50-100 条,并在批次之间加入适当的间隔。
导出安全事件报告 安全合规审计通常需要定期的安全事件报告。以下代码展示如何从 Sentinel 提取事件(Incident)数据并生成结构化的报告,便于向管理层汇报或存档备查。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 $incidentsApiVersion = "2024-03-01" $incidentsUri = "https://management.azure.com" + $sentinelBaseUri + "/incidents?api-version=$incidentsApiVersion &`$filter=properties/createdTimeUtc ge 2025-10-01T00:00:00Z and properties/createdTimeUtc le 2025-10-30T23:59:59Z" $incidentsResponse = Invoke-AzRestMethod -Uri $incidentsUri -Method Getif ($incidentsResponse .StatusCode -eq 200 ) { $incidents = ($incidentsResponse .Content | ConvertFrom-Json ).value $reportData = foreach ($incident in $incidents ) { $props = $incident .properties [PSCustomObject ]@ { 事件编号 = $props .incidentNumber 标题 = $props .title 描述 = if ($props .description.Length -gt 80 ) { $props .description.Substring(0 , 80 ) + "..." } else { $props .description } 严重级别 = $props .severity 状态 = $props .status 创建时间 = $props .createdTimeUtc 关闭时间 = if ($props .closedTimeUtc) { $props .closedTimeUtc } else { "未关闭" } 分类 = if ($props .classification) { $props .classification } else { "未分类" } 负责人 = if ($props .owner.assignedTo) { $props .owner.assignedTo } else { "未分配" } } } Write-Host "===== 2025年10月 Sentinel 安全事件报告 =====" -ForegroundColor Cyan Write-Host ("事件总数: {0}" -f $reportData .Count) Write-Host "" $bySeverity = $reportData | Group-Object -Property 严重级别 Write-Host "--- 按严重级别分布 ---" foreach ($group in $bySeverity ) { Write-Host (" {0}: {1} 条 ({2:P1})" -f $group .Name.PadRight(10 ), $group .Count, ($group .Count / $reportData .Count)) } $byStatus = $reportData | Group-Object -Property 状态 Write-Host "`n--- 按状态分布 ---" foreach ($group in $byStatus ) { Write-Host (" {0}: {1} 条" -f $group .Name.PadRight(15 ), $group .Count) } $reportPath = Join-Path $env:TEMP "Sentinel-Incident-Report-2025-10.csv" $reportData | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8 Write-Host ("`n详细报告已导出: {0}" -f $reportPath ) -ForegroundColor Green $openIncidents = $reportData | Where-Object { $_ .状态 -ne "Closed" } | Select-Object -First 5 if ($openIncidents ) { Write-Host "`n--- 待处理事件(前 5 条)---" -ForegroundColor Yellow foreach ($inc in $openIncidents ) { Write-Host (" #{0} [{1}] {2}" -f $inc .事件编号, $inc .严重级别, $inc .标题) } } }
执行结果示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 ===== 2025年10月 Sentinel 安全事件报告 ===== 事件总数: 128 --- 按严重级别分布 --- High : 12 条 (9.4%) Medium : 45 条 (35.2%) Low : 56 条 (43.8%) Informational: 15 条 (11.7%) --- 按状态分布 --- New : 8 条 Active : 23 条 Closed : 97 条 详细报告已导出: /tmp/Sentinel-Incident-Report-2025-10.csv --- 待处理事件(前 5 条)--- #1042 [High] Suspicious PowerShell execution detected #1040 [High] Brute force attack on Azure AD #1039 [Medium] Anomalous sign-in location detected #1037 [Medium] Unusual file download activity #1035 [Low] Multiple failed VPN attempts
导出的 CSV 报告可以直接用于月度安全复盘会议,也可以导入 Excel 进行进一步的数据透视分析。通过 PowerShell 定时任务(Scheduled Task)或 Azure Automation Runbook,可以将此脚本配置为每月自动执行,自动将报告发送到指定邮箱或 SharePoint 文档库。
注意事项
API 版本兼容性 :Microsoft Sentinel REST API 处于持续演进中,不同版本的行为可能存在差异。建议在脚本中显式指定 API 版本(如 2024-03-01),并在升级前查阅官方 changelog 确认破坏性变更。生产环境中应锁定 API 版本,避免因自动升级导致脚本失效。
权限最小化原则 :调用 Sentinel API 的服务主体或用户账户应遵循最小权限原则。建议使用 Azure 自定义角色(Custom Role),仅授予 Microsoft.SecurityInsights/* 相关操作的读取或写入权限,而非贡献者(Contributor)级别的宽泛权限。
API 速率限制 :Azure ARM API 对订阅级别的请求有速率限制(通常每订阅每小时 12000 次写入请求)。在批量操作(如导入数千条威胁指标)时,务必实现重试逻辑和退避策略,监控响应头中的 x-ms-ratelimit-remaining-subscription-writes 值。
KQL 查询性能 :分析规则中的 KQL 查询直接影响 Sentinel 的运行成本和响应速度。避免在查询中使用 join 操作大量数据集,善用 summarize 和 where 子句尽早过滤数据。查询频率(queryFrequency)和查询周期(queryPeriod)的设置也需要根据实际数据量调优,过大的查询周期会显著增加 Log Analytics 的扫描费用。
敏感数据处理 :告警和事件数据中可能包含用户主体名称(UPN)、IP 地址等敏感信息。在导出报告或推送到第三方系统时,注意遵守数据保护法规(如 GDPR、个人信息保护法),必要时对敏感字段进行脱敏处理。存储报告的路径也应设置适当的访问控制。
模块版本管理 :Az.SecurityInsights 模块目前仍处于预览阶段,cmdlet 和参数可能发生变更。建议在脚本中检查模块版本,并在 CI/CD 流水线中固定模块版本号(如 RequiredVersion),确保不同环境中的行为一致。可同时准备 REST API 直接调用的降级方案,以应对模块兼容性问题。