ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Azure安全中心策略自动化测试:用Pester构建Policy as Code回归链路

Azure安全中心策略自动化测试:用Pester构建Policy as Code回归链路 在Azure上做安全治理的同学应该都有同感真正难的往往不是策略本身怎么写而是策略上线之后的那一哆嗦。我之前给一个生产订阅加过一条“禁止创建公网IP”的Deny策略逻辑本身没什么问题结果下午就有人来敲我——说内部一个负载均衡器的公网IP也被误伤了创建了新的实例直接失败。根源很简单我当初只是在Portal里点了两下、确认了策略能Avail根本没在真实作用域跑过一轮完整的自动化验证。后面我索性搭了一套Azure安全中心策略自动化测试套件核心组件是PowerShell的Pester框架配合Azure Policy的JSON定义与REST API把“策略能不能上线”这件事变成了一条可以随时回归、可以交给CI/CD执行的工程链路。这篇文章把整套方案从设计思路、目录结构到行为验证、流水线接入再到常见的坑完整记录下来。适合正在做Azure云治理、安全基线、或者打算把IaC基础设施即代码“测试化”的团队参考。顺嘴提一句Azure安全中心现在的官方名称已经叫Microsoft Defender for Cloud但圈子里还是习惯叫“安全中心”下面内容我就统一用“安全中心”指代。1. 整体设计策略测试为什么必须自动化以及三层测试模型1.1 安全策略的本质从“一次性配置”变成“长期代码债”不管你是用安全中心自带的安全建议还是用Azure Policy自定义Deny、Audit、DeployIfNotExists效果最终这些策略都会被固化成JSON定义并且被分配到管理组、订阅或者资源组上。一旦你开始用代码管理策略就意味着你迈入了“策略即代码”Policy as Code的范畴。而只要是代码就一定有回归风险、有环境差异、有需要反复验证的场景。我见过太多团队是这种状态策略在开发环境测过一遍然后复制到生产之后除非出了线上事故基本没有人再去碰它。但事实是Azure的策略评估结果会受到作用域叠加、资源提供者注册情况、参数类型、甚至策略别名的拼写影响。今天写的一条Deny策略可能只挡住了VM下周同一个作用域下面新增了一种存储账号类型策略却压根没覆盖到。这些问题靠人肉检查几乎看不出来。另一个现实问题是时间成本。安全基线里动辄几十上百条策略你不可能每天用Portal一个个建测试资源去试。就算每周试一遍一次全套验证折腾下来也得小半天。而自动化测试套件要解决的正是这几个问题可重复、可追溯、低成本、能提前发现问题。1.2 三层测试模型语法、语义、行为缺一不可在设计套件之前我先想清楚了一个问题到底要测策略的什么拆到最后其实就是三层第一层是语法层。说白了就是保证策略JSON文件是合法JSON必需字段如displayName、policyType、parameters、policyRule齐全结构上没有低级错误。Azure Policy的Schema校验在Portal上会帮你做但当你用代码管理、多人PR时机器校验比人眼靠谱得多。第二层是语义层。这层比语法更“软”一些看的是策略逻辑是否合理。比如policyRule的if条件里引用的别名alias是否存在、参数是否定义了AllowedValues、effect是否落在合法枚举Audit、Deny、Modify等范围内。语义层测试特别适合检测“看着能通过Schema实际上策略执行不了”的情况。第三层是行为层也是最有价值、最容易被忽略的一层。行为层意味着要在隔离的测试订阅里真实分配策略、执行资源创建、然后检查策略评估结果和实际拦截/审计行为是否符合预期。这一层测试能扑捉到最真实的回归——比如你把一条Deny策略从“禁止VM创建公网IP”不小心加了一个等价条件语法和语义都没问题但行为变了只有实际跑一遍才知道。在实际搭建套件时我把这套概念映射成了三个测试目录tests/syntax、tests/semantic、tests/behavior。这三个目录在流水线里承担不同的角色后面会细说。1.3 为什么选Pester而不是pytest或者别的工具网上关于策略测试的讨论大部分都聚焦在pytest配合ARM模板测试上。但我最终选了Pester理由很直接Azure Policy的定义本身就是PowerShell生态和REST API的组合而且安全中心日常运维几乎离不开PowerShell的Az模块。用Pester可以直接在测试脚本里调用Get-AzPolicyAssignment、Get-AzPolicyState这些原生命令不用再包一层解析器或通过Python SDK中转。更关键的是Pester的Describe、Context、It这套结构非常接近自然语言。测试用例写完安全团队里哪怕不太懂代码的同学也能大概读出来在验什么。比如Context 当存储账号允许HTTP访问时 { It 应该被安全中心标记为不合规 { $state Get-AzPolicyState -ResourceId $storageAccount.Id $state.IsCompliant | Should -Be $false } }这种可读性在跨团队协作时非常值钱。另外Pester v5对数据驱动测试的支持也比早期版本好很多后面我会展示怎么用一条测试逻辑批量覆盖几十个策略定义。2. 套件结构设计被测对象、目录约定与数据驱动测试2.1 我们到底在测什么安全中心里的策略生态搭建套件前首先要明确“被测对象”到底包括什么。Azure安全中心里策略相关的东西其实有四类实体第一类是单个策略定义即一条Policy Definition可能来自内建策略或自定义策略。第二类是策略计划Initiative安全中心自带的安全基线本质上就是一组Initiative里面挂着几十上百条策略。第三类是策略分配也就是把策略或计划绑定到某个管理组/订阅/资源组分配时可以指定参数和排除范围。第四类是策略评估结果也就是合规状态这才是用户最终感受到的东西。所以一个完整的测试套件绝不能只测策略JSON文件本身的合法性还要测分配逻辑和评估结果。我在项目里维护了三套内容policies/目录存自定义策略JSONassignments/目录存分配模板带参数和排除项tests/目录里则写测试脚本。这样任何一次改动——不管是策略逻辑还是分配范围——都能触发对应的测试。顺便说个经验安全中心自带的那些内建策略原则上不要直接改但你必须把它们纳入回归范围。因为你的自定义策略很可能跟内建策略存在叠加竞争关系。比如Azure内置了一条“应禁用存储账号的公开访问”你又自建了一条“禁止存储账号任何网络例外”两者评估逻辑一旦冲突合规状态可能反复横跳。测试套件里定期跑一遍基线列表就是为了把这些冲突早期揪出来。2.2 目录结构约定与命名规则我目前的套件目录长这样policy-tests/ ├─ policies/ │ ├─ deny-public-ip-vm.json │ ├─ audit-storage-https-only.json │ └─ initiatives/ │ └─ custom-security-baseline.json ├─ assignments/ │ ├─ dev/assignment.deny-public-ip-vm.json │ └─ prod/assignment.deny-public-ip-vm.json ├─ tests/ │ ├─ syntax/ │ │ └─ syntax.policy.definition.ps1 │ ├─ semantic/ │ │ └─ semantic.policy.rule.ps1 │ └─ behavior/ │ ├─ behavior.deny-public-ip-vm.ps1 │ └─ behavior.audit-storage-https.ps1 ├─ scripts/ │ ├─ connect.ps1 │ ├─ invoke-all-tests.ps1 │ └─ cleanup-test-resources.ps1 └─ azure-pipelines.yml命名规则就两条一条策略对应一个JSON文件一个行为测试对应一条策略的某个关键场景文件名直接带策略名。好处是失败时你从测试报告里一眼就能定位到是哪条策略出了问题而不是面对一堆test1.ps1、test2.ps1。2.3 数据驱动测试一份逻辑覆盖所有策略如果你老老实实给每条策略写一份独立测试代码维护成本会爆炸。所以我在语法和语义层大量使用了Pester v5的数据驱动能力。核心做法是从目录里读取策略文件列表把它作为参数循环注入测试用例。一个典型例子是我遍历所有策略JSON验证必备字段是否存在BeforeAll { $policyFiles Get-ChildItem $PSScriptRoot/../../policies/*.json -ErrorAction SilentlyContinue } Describe 每个策略定义都应包含必要字段 -ForEach $policyFiles { Context 验证 _.BaseName { BeforeAll { $policyJson Get-Content $_.FullName -Raw | ConvertFrom-Json } It displayName 不为空 { $policyJson.displayName | Should -Not -BeNullOrEmpty } It policyType 是 Custom 或 BuiltIn { $policyJson.policyType | Should -BeIn (Custom, BuiltIn) } It policyRule 非空且包含 then 字段 { $policyJson.policyRule.then | Should -Not -BeNullOrEmpty } } }这段代码的精髓在于-ForEach参数。它让Pester自动把列表里的每个文件变成一个独立的测试上下文测试标题里可以用_.BaseName占位符标明当前测的是哪个文件。这样你新增一条策略文件时根本不需要新增测试代码只需要保证它出现在policies/目录里测试逻辑自动把它纳入回归范围。同样道理也适用于语义层把一组别名列表和允许的effect枚举做成数据表用Should -BeIn批量验证。数据驱动测试在策略数量超过30条以后省下的维护成本非常可观。3. 实操记录从零搭建策略自动化测试套件的完整过程3.1 环境准备与权限设计先交代我的环境基线Windows和Linux都实测过统一用PowerShell 7.3以上版本Az模块至少10.xPester必须用5.x。注意Pester 5和4的语法差异不小网上很多老教程还停留在4.x的New-TestDrive那套直接照着写会报错。权限这块我只说结论在测试订阅里给测试用的服务主体分配“Policy Contributor”角色并且确保它可以在这个订阅里创建Resource Group、存储账号、VM等测试资源。思路很简单——测试服务主体本质上是“管理员”但它只能在隔离的测试订阅里活动永远不要拿生产环境当实验场。登录逻辑我单独放在scripts/connect.ps1里支持服务主体和交互登录两种模式param( [string]$ApplicationId, [string]$SubscriptionId, [string]$TenantId, [string]$CertificateThumbprint ) $context Get-AzContext if (-not $context -or -not $context.Account) { if ($CertificateThumbprint) { Connect-AzAccount -ServicePrincipal -ApplicationId $ApplicationId -Tenant $TenantId -CertificateThumbprint $CertificateThumbprint -Subscription $SubscriptionId } else { Connect-AzAccount -Subscription $SubscriptionId } }这里我加了一个条件判断如果当前PowerShell会话已经有有效AzContext就不要重复Connect-AzAccount。否则你在同一个Runner里连续跑多次测试容易遇到上下文互相覆盖、Token过期不刷新之类的玄学问题。3.2 语法层测试的实现细节语法层测试除了检查字段存在性还应该做一件事用Azure Policy的REST API做真正的Schema校验。Az模块虽然能解析JSON但不会替你做完整Schema校验。我的做法是用Test-AzPolicyDefinition这个cmdlet先直接尝试创建策略定义失败就说明语法有问题。Describe 策略定义可被正常创建 -ForEach $policyFiles { It _.BaseName 可以通过 Azure Policy Schema 校验 { $parameter { Name test-$($_.BaseName) Policy $_.FullName Mode Indexed Location (Get-AzLocation | Select-Object -First 1).Location } $result Test-AzPolicyDefinition parameter -ErrorAction SilentlyContinue $result | Should -Be $true } }注意这里的命名不能直接使用策略原有的Name因为策略定义Name在Azure里是全局命名的至少要求在订阅/管理组范围内唯一直接使用生产环境的Name容易冲突。加上test-前缀是一种廉价但有效的碰撞规避方案。真实跑下来你会发现语法层测试抓到的多数问题都是JSON里多了逗号、字段名大小写不对、或者mode写错成了All而不是Indexed。这些问题放在人肉流程里往往要等到分配策略时才会爆出来。3.3 行为层测试在隔离订阅里“搞破坏”行为层测试是整套套件里含金量最高、也最容易被跳过的部分。以前我也没有做这层后来被“策略看似生效但实际评估结果异常”教训过一次才开始重视。给大家看一下我测“禁止VM公网IP”的行为层脚本核心逻辑Describe deny-public-ip-vm 策略行为 { Context 尝试创建一个带公网IP的VM时 { BeforeAll { # 1. 在隔离测试订阅中创建资源组 $rgName rg-policy-behavior-test-$(Get-Random -Minimum 1000 -Maximum 9999) New-AzResourceGroup -Name $rgName -Location eastus2 try { # 2. 分配待测策略作用域限定到这个RG New-AzPolicyAssignment -Name test-deny-public-ip -PolicyDefinition $policyDefId -Scope $rgName.ResourceId # 3. 尝试创建带公网IP的NIC $badNicParams { ... } New-AzNetworkInterface badNicParams -ErrorAction Stop $creationSucceeded $true } catch { $creationSucceeded $false $errorMessage $_.Exception.Message } } It 创建操作应被策略拦截 { $creationSucceeded | Should -Be $false $errorMessage | Should -Match deny-public-ip-vm } AfterAll { # 4. 测试结束必须清理防止资源残留 Remove-AzResourceGroup -Name $rgName -Force -AsJob } } }这里有三个关键工程细节值得单独说。第一策略评估不是毫秒级的。Azure Policy是异步评估引擎New-AzNetworkInterface被Deny拦住是快的但如果你测的是Audit效果你可能需要等一两分钟才能看到Get-AzPolicyState里出现不合规记录。所以行为测试里我一般都会写一个轮询函数每隔10秒查一次状态最多等90秒超时再报失败。function Wait-PolicyState { param($ResourceId, $TimeoutSec 90) $deadline (Get-Date).AddSeconds($TimeoutSec) do { $state Get-AzPolicyState -ResourceId $ResourceId -ErrorAction SilentlyContinue if ($state) { return $state } Start-Sleep -Seconds 10 } while (Get-Date -lt $deadline) return $null }第二行为层测试里永远要写清理逻辑而且是try/finally级别的清理。我刚开始没注意测试几次之后测试订阅里堆了一堆资源安全中心合规简报上全是红色告警反而不利于基线数据对比。后来我统一在AfterAll或finally块里用Remove-AzResourceGroup -Force清理测试资源组因为RG里所有资源会一起删除比逐个删好用。第三行为层测试非常吃订阅配额。比如VM数量、公共IP数量都有默认配额限制连续跑多了可能撞配额。所以CI流水线里我默认行为层测试只跑在两个可能冲突的策略组上而不是每次PR都全量跑。后面流水线部分会展开讲。3.4 语义层的关键窍门验证别名与effect组合语义层测试中最容易被忽略的是“别名验证”。Azure Policy里的field可以引用资源属性的别名比如Microsoft.Storage/storageAccounts/networkAcls.defaultAction。如果别名拼错或者属性路径在某个资源类型上不存在策略会静默降级为不评估不会报错但永远不会命中任何资源。这是典型的语法测试抓不到、语义测试最重要的一环。我在语义层做了这样一件事从所有策略JSON里抽取所有field值过滤出包含Microsoft.开头的别名然后调用Azure提供的别名REST接口或直接用Get-AzPolicyAlias批量校验存在性BeforeAll { $aliases Get-AzPolicyAlias -ResourceTypeMatch Microsoft.Storage -ListAvailable $validAliasSet $aliases.Aliases.AliasName } It 策略引用的别名存在且拼写正确 { $badAliases $aliasUsedInPolicy | Where-Object { $_ -notin $validAliasSet } $badAliases | Should -BeNullOrEmpty }这个测试看起来简陋实际价值很大。因为Azure的别名列表会随着新服务发布而更新如果你的策略用了某个新服务的别名而当前云环境还没有更新别名表那这条策略等于废的。早期发现总比上线后收到“策略被忽略”的工单要好。4. 接入CI/CD让每次策略变更都自动跑测试4.1 触发机制设计PR阶段验证发布阶段全量验证测试套件写好了如果不接流水线那还是靠人肉执行价值大打折扣。我自己的工程实践是把测试分成两级第一级是PR触发级。只要代码合入到主分支之前但凡路径变更涉及policies/或assignments/目录就触发一个快速管道只跑语法和语义测试。这个级别不跑行为测试因为行为测试耗时较久而且要创建真实资源不适合高频执行。它主要负责挡掉低级错误让Reviewer不用自己去克隆代码、切分支、手动跑脚本。第二级是发布触发级。当主分支上有release/policy的Tag打出时触发全量管道跑语法、语义、行为全套测试。行为测试会在隔离订阅里真实模拟一次“违规资源创建”确保策略在目标环境中的表现完全符合预期。4.2 GitHub Actions的流水线参考我在项目里用的是GitHub ActionsWorkflow文件关键部分如下name: policy-tests on: pull_request: paths: - policies/** - assignments/** push: tags: - release/policy-* jobs: quick-tests: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: azure/loginv1 with: creds: ${{ secrets.AZURE_TEST_SUB_CREDENTIALS }} - name: Run syntax and semantic tests shell: pwsh run: | Install-Module Pester -RequiredVersion 5.5.0 -Force Install-Module Az.Policy -Force Invoke-Pester ./tests/syntax, ./tests/semantic -CI - name: Publish test report uses: actions/upload-artifactv4 if: always() with: name: pester-report path: **/TEST-*.xml full-tests: if: startsWith(github.ref, refs/tags/release/policy-) runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: azure/loginv1 with: creds: ${{ secrets.AZURE_TEST_SUB_CREDENTIALS }} - name: Run behavior tests shell: pwsh run: | Invoke-Pester ./tests/behavior -CI这里最值得说的是azure/loginv1这个Step。它会把Azure凭据注入到Runner的PowerShell环境中这样后面的Connect-AzAccount就不需要手动传证书指纹或密码了直接用Connect-AzAccount -Identity或让脚本检测到已有Context即可。别把服务主体密钥直接写进YAMLGitHub Secrets是更安全的方式。另一个细节是测试报告。Pester启用-CI参数后会输出NUnitXML格式的结果GitHub Actions原生的test report摘要和Annotate功能能自动解析这些XML并在PR页面标出失败的测试用例。这个体验比看控制台日志舒服太多值得配一下。4.3 测试失败后如何通知团队触发失败后默认GitHub Actions会在PR页面给出红色状态这对保护主分支已经足够了。但我个人的习惯是再加一层通知当full-tests失败时通过Webhook推送到团队群。实现方式并不复杂用一个PowerShell脚本读取Pester的Result对象如果失败就调用Webhook$result Invoke-Pester -PassThru -CI if ($result.FailedCount -gt 0) { $body { text 策略测试失败共 $($result.FailedCount) 个用例请检查: $env:RUN_URL } | ConvertTo-Json Invoke-RestMethod -Uri $env:TEAMS_WEBHOOK_URL -Method Post -Body $body -ContentType application/json exit 1 }这一步做完团队里的每个人在合并策略之前就能自动获得安全感不用再靠“谁有空谁去手动验证”这种脆弱机制。5. 常见问题与排查技巧实录5.1 策略行为与预期不符的排查三板斧不管测试套件写得多完美总有很多问题是它本身没法完全预防的因为Azure的底层行为也需要你理解。我总结了三步排查法遇到“策略已经分配但就是没生效/没拦截”的情况按顺序查第一查作用域。Azure Policy有四个层级管理组、订阅、资源组、资源。子作用域可以继承父作用域的分配但如果某个子作用域被加入排除范围父策略就不会对它生效。另外Deny效果不能被子作用域“覆盖”成Audit你只能让子作用域变得更严格。很多人被这条规则坑过——他们以为把某个RG从父级Deny中排除就能开绿灯实际不行必须在同一作用域单独分配允许策略并设置优先级。第二查评估延迟。Azure Policy的完整评估周期最高可能需要几十分钟尤其是Audit效果资源刚创建完的那几秒查不到状态是正常的。我的测试套件里特意加了Wait-PolicyState轮询但如果你在做人工排查也请给评估留出至少15分钟再判断“没生效”还是“延迟”。第三查资源提供者。如果策略用的是deployIfNotExists效果DINE策略的实质是触发一次部署任务。如果目标资源提供者没有注册这个部署任务会失败导致策略永远处于NonCompliant但不修复。遇到这种情况不是去改策略而是先注册对应的Resource Provider再重跑评估。5.2 测试套件自身容易踩的坑先分享三个我在Pester使用中踩过的坑。第一个是并行执行冲突。Pester v5自带并行测试能力看起来很美但如果你的测试代码里调用了Az模块多个测试用例并行执行时会共享同一个PowerShell的AzContext经常出现一个用例清了Context、另一个直接401错误。我的解决办法是用-Skip或直接不启用并行必要时用-RunCount 1限制并行度。测试套件的价值在于稳定不是跑得快。第二个坑是BeforeAll里连接AzureAfterAll里又断开。看似干净实际上如果多个测试文件在同一个Runner里先后执行前一个文件断开后后一个文件会傻掉。正确做法是只在流水线的全局启动脚本里连接一次所有测试文件共享Context测试内部不做Connect/Disconnect操作。第三个坑是测试里创建资源后的清理时机。我们前面说过用AfterAll清理但如果AfterAll里也报错清理过程就中断了导致残留资源。稳妥方案是把资源ID写成一个数组在AfterAll里用foreach轮询强制删除并对每个删除操作单独try/catch不要让单点失败阻断整个清理。5.3 常见问题速查表现象可能原因处理方式策略JSON能解析但创建策略定义时报错Schema字段非法如mode写了Indexed但policyRule里有不符合限制的字段用Test-AzPolicyDefinition做前置校验策略分配成功但没有合规结果作用域不存在资源或资源提供者未注册先创建测试资源再等待评估周期Deny策略没有拦住资源创建分配参数里effect被改成Audit检查参数值和策略优先级行为测试中创建操作一直成功策略排除项包含了测试RG检查分配排除范围测试A/B隔离Audit测试一直查不到不合规状态PolicyState接口有延迟加轮询等待60-120秒Pester运行时一直报401AzContext过期或并行上下文冲突全局连接一次不启用测试级并行测试订阅资源残留越滚越多清理逻辑只清理了部分资源用try/finally Remove-AzResourceGroup -Force这张表是我实际维护套件这一年里最常遇到的七类问题基本覆盖了八成以上的“测试爆炸”场景。建议你把这张表也留在项目仓库的README里新同学接手时能少走很多弯路。5.4 一个真实回滚案例策略测试救了我一次最后讲一个真实案例也顺便说明行为层测试的价值到底在哪里。生产环境曾经有一条“禁止暴露公网IP”的Deny策略我拍脑袋觉得策略只作用于VM网卡于是趁着周五下班前上线。结果第二天早上大量UAT环境中的应用因为无法创建新的负载均衡器直接失败——原来这条策略当时把Microsoft.Network/loadBalancers/publicIpAddresses这个字段也覆盖进去了任何新绑定的负载均衡公网IP都被拦掉。那段时间我刚好还没有这套自动化测试套件只能靠人肉一条条翻日志、看参数。事后我复盘如果当时套件已经存在行为层测试只需要在测试订阅里尝试创建一个带公网IP的负载均衡器Deny拦截会直接报错我根本不会把这个问题带进生产。从那以后我把行为层测试的覆盖范围从VM扩展到了LB、存储账号、SQL Server这些常见联网资源并且在策略变更的PR描述里强制要求附上行为测试的通过报告。之后再没出过类似问题。现在每次上线安全策略我都会对自己说一句话宁可让测试套件多跑一轮也别让半夜的告警短信替你跑。这套东西用起来之后价值会随着策略数量增长而指数级放大。后面我还在计划把Pester的测试结果跟安全中心的合规报告做对接做成每周自动巡检的策略成色报表把策略漂移这个隐患往前再压一步。
返回列表