一直用Azure安全中心现在叫 Microsoft Defender for Cloud的人应该都有过这种体会安全策略这东西改起来一时爽排查起来火葬场。某天合规团队跑过来说“某项建议怎么又变成不合规了”一查发现是有人调整了策略参数把不该关的监控项关了或者把某个标准设得太宽松。这种问题靠人工一遍遍翻门户检查效率极低而且容易漏。我花了三周时间搭了一套“Azure安全中心策略自动化测试套件”把策略基线校验、结果评估、变更检测、报告输出全部自动化跑在CI/CD里每次策略变更或者定时触发都会自动验证。这篇文章就把这套方案从设计到落地完整拆开讲包括为什么这么设计、每一步怎么实现、运行中踩过哪些坑以及后续还能怎么扩展。适合正在做云安全治理、负责公司Azure环境合规的工程师和DevOps同学参考。1. 为什么我决定给Azure安全中心策略做一套自动化测试先说背景。我们公司在Azure上的订阅比较多每个订阅都启用了Defender for Cloud安全策略基本上是基于CIS基准和公司内部安全基线配置的。一开始策略少手动维护没问题。后来订阅数量上来规则越加越多问题就出现了某些新订阅的初始化流程忘了套用统一策略、某次批量更新参数时不小心改错了影响范围、或者个别策略被所在资源组的作用域覆盖导致整体合规分失真。人工去核对的方式是这样的登录Azure门户进入Defender for Cloud打开“建议”一条条看哪些状态不符合预期。这能发现问题但有几个硬伤。第一门户展示的结果有延迟而且默认只看“资源”不符合项策略本身有没有配置错误完全看不出来。第二不同订阅之间对比太费劲得手动切换。第三没有任何历史记录今天改了某个策略下周才发现影响了别的推荐项中间的过程根本追溯不到。第四策略评估依赖“扫描”默认一天一次你改了策略等评估结果最多要等24小时这在快节奏的变更流程里完全不可接受。所以核心需求就变得很清晰需要一套能快速、重复、可验证地检查安全中心策略状态的工具最好能集成到日常发布流程里。说得直接点就是给安全策略也写单元测试和集成测试策略也有“正确预期”跑了测试就知道当前环境到底符不符合预期。这也是“策略自动化测试套件”这个名字的由来。另一个促使我搞这套的原因是企业合规审计。审计方要的不只是你配置了什么还要你能证明这个配置是经过测试的是可持续验证的。手工检查你根本拿不出证据而自动化测试的执行记录、通过率、每次输出的差异报告都是非常硬的过程证据。哪怕不是为了应付审计自己团队内部做变更管理也需要这样一个机制来背书。所以如果你也在Azure上管理大量资源、被合规要求追着跑、或者经常因为策略调整导致推荐项状态异常这套东西就有参考价值。不一定非要完全照搬我的实现但整体思路和关键环节是通用的。2. 测试套件整体设计与技术选型2.1 先想清楚要测什么动手写代码之前我花了不少时间做设计。测试套件不是简单调几个API就完事得先定义清楚“正确”是什么意思。对于Azure安全中心的策略可以划分成几个层面来测策略定义层策略本身是否存在、是否启用、是否分配在正确的订阅或管理组范围。参数配置层策略参数是否与基线一致。比如“必须启用MFA”这条建议对应的策略它的参数值可能是“针对订阅启用”如果被人改成“不生效”这就是明显的配置漂移。评估结果层推荐项在资源上的评估结果是否与预期一致。比如某个策略在所有相关资源上都应该是“通过”如果出现了警告或失败那就说明存在合规风险。合规性关联层与合规标准如CIS v2.0、NIST、SOC2的关联关系是否保持某些安全策略如果被移出了特定合规策略组合规报告就会失真。基于这四层测试用例的设计就有了框架。每一层都能做成独立的检查项输出布尔结果或者详细数据。2.2 技术栈怎么选我选型的时候对比了几种方案各有各的适用场景。第一种是直接用Azure CLI写Shell脚本搭配az security相关命令。优点是轻量不需要额外编程语言缺点是比较严重的——Azure CLI对策略的展示能力有限数据以文本输出为主不太好做结构化和断言。用它来做个快速抽查可以但要支撑完整的测试套件不够用。第二种是Azure PowerShell有Az.Security模块能拿到更完整的策略定义和评估数据。PowerShell本身在Windows环境下方便但跨平台和后续集成到Linux的CI运行器时需要额外处理。第三种是Python搭配Resource Graph查询。Resource Graph是Azure上非常强大的查询服务可以用Kusto类语法快速筛选订阅下的策略状态。Python的好处是生态好、测试框架成熟pytest、处理JSON方便而且后面接任何报告、告警模块都简单。我最后选择的是Python Azure SDK Resource Graph再加上pytest作为测试框架。这套组合的优势在于所有操作都是API级调用返回结构化JSON容易断言pytest支持参数化、fixture和插件扩展写大量策略检查用例很顺手Python能轻松生成Markdown、HTML或CSV报告也方便后续接钉钉、企微或邮件告警。2.3 核心模块拆分测试套件的整体结构我拆成了四个大模块采集器负责与Azure API交互拉取订阅列表、策略定义、政策参数、评估结果。基线库存放已定义的“预期状态”。所有测试用例的期望值都不是硬编码在测试程序里而是放在一个独立的JSON/YAML文件夹里这样可以版本化管理。执行引擎加载基线库调用采集器拿数据然后逐条比对生成测试结果。报告与通知器把结果整理成人类可读的格式并发送到指定渠道。这样拆的好处是每个部分职责单一。采集器更新API逻辑时不会影响到基线定义基线库里新增一条策略预期不需要改代码。这个结构我后面在维护时受益很大——策略变了只需要改基线数据文件测试逻辑完全不用动。3. 实操步骤从零搭建策略自动化测试套件3.1 环境准备先列一下跑起来需要的依赖Python 3.103.8也行但推荐3.10以上azure-identity处理认证azure-mgmt-resource访问管理组/订阅资源azure-mgmt-policy操作策略定义和分配azure-mgmt-resourcegraph查询资源及策略评估状态pytest执行测试pyyaml解析基线配置requests发HTTP请求比如对接Webhook安装可以直接用 requirements.txtazure-identity1.15.0 azure-mgmt-resource23.0.0 azure-mgmt-policy23.1.0 azure-mgmt-resourcegraph8.1.0 pytest7.4.0 pyyaml6.0 requests2.31.0认证方面我直接用了服务主体Service Principal。因为要跑在CI/CD里不能依赖交互式登录。在Azure AD里创建一个应用注册然后生成client secret把AZURE_CLIENT_ID、AZURE_TENANT_ID、AZURE_CLIENT_SECRET设成环境变量。还要授权这个服务主体在目标订阅或管理组上有“读取”权限比如“Security Reader”或“Reader”角色。如果后面还要做策略分配操作或触发评估扫描才需要更高权限。出于安全考虑我只给读取权限凡是需要写的操作比如重新评估走另一个有权限的短时流程。3.2 基线策略库的建立与维护基线库是整个测试套件的“真理来源”。它描述的是“我们希望每一条策略长什么样”。我用了这样一份YAML结构baseline_version: 2025.04 subscriptions: - subscription_id: sub-azure-01 display_name: 生产环境 control_name: CIS v2.0 policy_expectations: - policy_name: 在 Microsoft Defender for Cloud 中实现安全联系邮箱 policy_type: BuiltIn enabled: true parameters: email: secopsexample.com expected_assessment: Healthy - policy_name: 应启用基于 MFA 的登录访问 enabled: true parameters: enforcementMode: Enabled这里需要注意policy_name不一定等于Azure门户里显示的名称最好同时记录policy_definition_id或name的UUID避免重名导致匹配错误。基线里的expected_assessment就是这个策略下的资源应该处于什么状态。如果是全局性的策略比如“启用高级威胁保护”这类就直接测策略是否启用如果是资源级策略比如“SQL 数据库应启用透明数据加密”那测的是在目标订阅下该策略的评估结果是否符合预期。基线库一定要纳入git仓库管理。每次变更都走Pull Request由另外的人审核。这样策略修改就变成了一次可追溯的代码变更而不是控制台里的随手操作。3.3 自动化测试核心实现先写一个Azure认证客户端。我封装了一个简单的类# azure_helpers.py from azure.identity import ClientSecretCredential from azure.mgmt.resource import ResourceManagementClient from azure.mgmt.policy import PolicyClient from azure.mgmt.resourcegraph import ResourceGraphClient from azure.mgmt.resourcegraph.models import QueryRequest, QueryRequestOptions class AzureSession: def __init__(self, tenant_id, client_id, client_secret): self.credential ClientSecretCredential(tenant_id, client_id, client_secret) self.subscription_id None def set_subscription(self, subscription_id): self.subscription_id subscription_id self.resource_client ResourceManagementClient(self.credential, subscription_id) self.policy_client PolicyClient(self.credential, subscription_id) self.resource_graph_client ResourceGraphClient(self.credential, subscription_id)然后再写一个策略采集器# collector.py from azure.mgmt.resourcegraph.models import QueryRequest class PolicyCollector: def __init__(self, session, subscription_id): self.session session self.subscription_id subscription_id def list_policy_assignments(self): return list(self.session.policy_client.policy_assignments.list(scopef/subscriptions/{self.subscription_id})) def get_policy_definitions(self): return list(self.session.policy_client.policy_definitions.list()) def query_assessment_status(self, policy_name_filterNone): query securityresources | where type microsoft.security/assessments | extend assessmentKey tostring(properties.displayName) | project resourceId, assessmentKey, status properties.status.code if policy_name_filter: query f| where assessmentKey contains policy_name_filter request QueryRequest( subscriptions[self.subscription_id], queryquery, optionsQueryRequestOptions(result_formatobjectArray) ) result self.session.resource_graph_client.resources(request) return result.data这里用Resource Graph来查评估状态非常有效。securityresources类型包含安全中心和Defender相关数据可以直接过滤microsoft.security/assessments也就是某个策略在各个资源上的评估结果。接下来是测试用例。pytest的fixture非常适合做这类数据准备import pytest import yaml pytest.fixture(scopemodule) def baseline(): with open(baseline.yaml, r) as f: return yaml.safe_load(f) pytest.fixture(scopemodule) def collector(session): return PolicyCollector(session, session.subscription_id)单条策略检查用例可以写成这样def test_policy_assignment_enabled(baseline, collector): assignments collector.list_policy_assignments() assignment_names [a.display_name for a in assignments] for expected in baseline[policy_expectations]: assert expected[policy_name] in assignment_names, \ f策略 {expected[policy_name]} 未在订阅上分配为了不让所有用例都挂在一个测试函数里我用了参数化。把基线的策略期望作为参数注入到多个测试函数里这样如果有10条策略基线pytest就会生成10个用例实例哪个断言失败会单独显示定位起来非常方便。pytest.mark.parametrize(policy_expectation,subscription, load_baseline()) def test_policy_expectation(policy_expectation, subscription): ...参数化还能配合pytest的--collect-only查看所有测试点做覆盖率分析。3.4 测试用例的设计技巧设计测试用例时我踩了一些坑也总结了几条经验。第一条一定要有正向测试和反向测试。正向测试是断言策略存在、启用、评估为Healthy。反向测试是故意验证“如果策略被禁用了能不能查出来”。我们不需要所有的反向测试都跑在真实环境里那样会干扰生产。反向测试可以用来验证采集器和断言逻辑本身是否正确比如在测试里用一个假的返回数据模拟策略被禁用断言系统能捕获这种异常。这样能避免一个尴尬情况所有测试都过了但服务主机的权限有问题连数据都没拿到你以为一切正常实际全是空跑。所以我还加了一条“空数据保护”的测试用例def test_data_not_empty(collector): assessments collector.query_assessment_status() assert len(assessments) 0, 采集到的策略评估数据为空可能权限或凭据配置有问题这条用例看起来简单却救过我不少次。第二条测试要保持幂等。每次运行都在同一个环境下执行不应该产生副作用。要注意某些Azure API如果传了强制刷新参数可能触发后台评估扫描这种操作不要放到常规测试套件里。常规测试只做读取触发扫描另写独立的流程否则容易导致被限流。第三条要控制测试粒度。有些策略评估在资源级别比如“SQL Server应该启用高级数据安全”会对应SQL数据库实例如果你的订阅下有几万个资源全量查询返回的数据会很大。解决方案是在Resource Graph里按资源类型先过滤或者用分页获取。我实际跑下来一个中等规模的订阅两百个资源查询评估状态大概需要30秒到1分钟完全可以接受。如果订阅特别多考虑对每个订阅开并行线程但注意API限流。3.5 接入CI/CD管线测试套件跑本地是不够的得集成到CI里才能真正起到“门禁”作用。我用了GitHub Actions因为azure auth action比较顺手。工作流大概是这样的name: run-security-policy-tests on: push: paths: - baselines/*.yaml - src/** schedule: - cron: 0 2 * * * workflow_dispatch: jobs: test: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Azure Login uses: azure/loginv2 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Install dependencies run: pip install -r requirements.txt - name: Run policy tests env: SUBSCRIPTION_ID: ${{ secrets.SUBSCRIPTION_ID }} run: pytest tests/ --junitxmlreport.xml - name: Upload test report uses: actions/upload-artifactv4 with: name: policy-test-report path: report.xml这里有一个细节不是我手动执行pytest就跑完而是在运行前还要做一个“基线文件格式检查”的步骤确保YAML字段没有遗漏。如果基线文件没定义policy_id后面测试数据匹配就会失败这时候应该立刻报错而不是等到执行阶段。接入CI之后每次有人改基线策略push到仓库就会触发全套测试。比如你改了MFA策略的预期参数测试会检查实际环境是否与预期一致。如果当前环境还不符合测试失败合并就会被组织起拦截。这就是把“安全策略当作代码”的落地方式。4. 测试套件运行中的常见问题与排查实录这个部分我整理了实际运行过程中遇到的典型问题都是踩过坑才总结出来的不是文档里能直接找到的标准回答。4.1 API权限不足导致采集失败刚开始测试时我用了团队里已有的一个服务主体它虽然也是“订阅贡献者”但安全相关的data action其实没有授权。调用Resource Graph查询securityresources时返回的是401或“没有访问权限”。但Azure CLI的az security某些命令可能依然能用因为底层API不同导致权限问题不容易一眼发现。解决办法是给服务主体加上安全读取者Security Reader角色作用域要覆盖到具体订阅或管理组。注意如果服务主体是在管理组层面授权的Resource Graph查询时subscriptions列表里必须包含每个订阅否则会漏。我最后把权限收敛成了“订阅读取者”“安全读取者”。这个权限组合够用不会给太大面。4.2 策略评估状态延迟与数据一致性Resource Graph查出来的评估状态和你在Defender for Cloud门户里看到的不一定实时一致。Azure的策略评估不是一个事件触发式的实时计算它是基于规则的扫描虽然有“按需评估”API可以触发但常规情况下是定期扫描默认是24小时一次。这就意味着你改了策略参数后立即跑测试可能查到的还是旧结果。我一开始被这个坑坑惨了基线里把某个参数从“禁用”改成“启用”之后测试一直失败查下来发现策略参数确实已经是启用了但评估状态还没刷新。在CI里这种失败会造成误报。解决办法是在测试用例的设计上划分清楚测“策略配置状态”和测“策略评估结果”是两码事。策略配置状态走Azure Policy的API这个API是实时的评估结果走Resource Graph它依赖扫描。在CI流程里对“评估结果”类测试要容忍一定延迟或者独立跑一个“按需评估”任务评估完成后再继续测试。按需评估API长这样az security assessment create --name assessment-name --status-code Healthy --resource resource-id不过这个命令会直接覆盖评估结果不建议随便用。更合理的做法是等待下一轮扫描或者对“评估结果”类的测试采用“异步校验”方式先标记为pending过几个小时再验证。我的临时方案是CI里加了重试逻辑每5分钟查一次最多等30分钟。如果30分钟后还是旧状态才真正判定为失败。4.3 模拟评估与实际资源配置不一致Security Center里的很多建议都是基于资源配置的评估比如某个VM没有启用漏洞扫描评估结果就是Unhealthy。但这里有一个容易误解的点当你改变了策略参数比如把“漏洞扫描”的阈值调整到更严格在下次扫描前Resource Graph里看到的评估结果还是基于旧参数。你看到的“Unhealthy”不一定是策略本身没生效也可能是评估还没重新计算。我建立了一套概念策略“生效状态”和“评估状态”。生效状态表示这个策略是否在订阅上生效并作用到资源评估状态表示资源是否符合。测试套件需要明确区分这两类断言不能在测试代码里混为一谈。如果混了就会出现“策略明明是启用状态但测试报告显示失败”这种让人摸不着头脑的结果。4.4 权限过大或过小的问题服务主体的权限设计是我踩过最麻烦的坑之一。一开始为了省事把服务主体直接加到订阅的“所有者”角色结果权限太大违背了最小权限原则审计时被人质疑。后来改成“安全读取者”“资源读取者”结果发现Resource Graph查某些安全数据依然报权限不够。最后查询微软文档发现securityresources类型需要额外授权Microsoft.Security/assessments/read这个data action不是传统角色能覆盖的。解决方法是自定义一个角色把所需的数据模型data action加上去。我建了一个自定义角色“Policy Test Service Principal Role”授予的权限包括Microsoft.Security/assessments/read、Microsoft.Security/policies/read、Microsoft.Authorization/policyassignments/read、Microsoft.Authorization/policydefinitions/read。把这些权限收窄之后整个采集过程就不会再出现权限问题也更合规。4.5 报告与告警通知测试跑完只输出JUnit XML不够运维同学更希望看到一个直观的结果。我加了一个报告生成器把pytest结果整理成Markdown表格列出每一条策略的ID、名称、期望状态、实际状态、结果通过/失败/跳过。然后通过Incoming Webhook发给企微群失败结果直接负责人。发送通知的代码简单得像这样import json, requests def send_webhook(url, payload): requests.post(url, jsonpayload, timeout10)关键是要在pytest的pytest_sessionfinish钩子里拿到结果加上一个简单的统计项比如“本次测试共检测12条策略10条通过2条失败”。对于失败的用例要把详细的原因打包成附件链接而不是只发一个“失败”否则人还得登录CI平台自己看。这个细节让我后面维护省了不少沟通成本。5. 一些实践心得与进阶思路5.1 先跑通最小闭环再扩展这套套件一开始我只做了“策略是否启用”的检查连评估结果查询都没接运行速度非常快。后来逐步加上了Resource Graph查询、参数校验、报告、通知。如果你一上来就想做全所有功能代码复杂度会快速上升导致项目迟迟不能上线。我建议先写一个最简单的脚本列出所有策略和预期状态的对比只要能在本地跑通就立刻接入CI。跑通最小闭环后再根据实际使用中暴露出来的问题逐步迭代。我现在回头想这个过程是最合理的因为它可以帮助你快速发现权限问题、数据获取问题和测试设计问题。5.2 基线策略库的版本治理基线库不是写一次就完随着合规标准升级、公司安全要求变化里面的内容会持续改动。我把基线库的版本号放到了文件名里比如baseline-2025.04.yaml同时保留上一版本。这样如果新基线上线后测试大批量失败可以随时对比旧版本看是不是预期本身有问题还是环境确实漂移了。另外基线库和测试代码要分开存不要塞在一个文件里。每个测试用例用基线里的policy_id去匹配而不是用显示名。显示名一旦在产品端被改了微软确实会改一些显示名测试用例就会魔幻地失联而UUID不会。5.3 未来可以做的事目前这套套件已经稳定跑了两三个月我再想还能做几件进阶的事。第一是把“测试结果快照”存到Log Analytics或存储账号中形成历史趋势图。这样可以看到一段时间内策略合规状态的变化如果某天合规率突然下降能快速定位到是哪条策略、哪个资源引起的。我甚至想直接用Resource Graph定期做一次全量的合规汇总形成日报告。第二是做策略变更的“灰度发布”。现在测试套件是直接对生产环境测风险有点大。实际上可以先在测试订阅上修改策略并跑测试确认通过后再在凭据层面应用到生产订阅。这需要把基线库按环境拆成多个文件比如baseline-prod.yaml、baseline-test.yaml每种环境独立执行。我现在的做法还是直接对生产测因为工的订阅里没有太庞杂的资源压力不大但如果你管理的是关键业务系统强烈建议先做灰度环境。第三是结合Azure资源图的数据做更细粒度的“资源级合规监控”。现在测试套件主要查策略状态和聚合评估结果如果你需要知道“哪些VM没有安装Endpoint Protection”可以直接在Resource Graph里查VM资源列表然后逐项和策略评估结果做关联。这是一个不错的方向可以把它做成一个独立的合规巡检器。我在实际使用中发现这套自动化套件最大的价值不是“发现错误”而是“让错误无法悄悄发生”。策略变更被纳入测试门禁之后团队里再也没有发生过“改完参数就忘了恢复”这种事。哪怕后续要排查任何合规问题翻一翻CI记录里的历史测试结果基本能还原出每一次策略调整的“现场证据”。这比一堆口头解释靠谱得多。最后再分享一个小技巧别忘了在测试套件里加一个“自检用例”——定期调用Azure API确认凭据有效、权限没被改、能拿到非空数据。我遇到过两次服务主体密钥过期导致所有测试“全绿通过”的假象因为代码在认证失败时自动跳过了所有用例。自检用例能把这种静默故障提前暴露出来省得等审计报告出来才发现自己一直在裸奔。这个内容后续还可以扩展成Azure政策即代码Policy as Code的完整工作流从基线库生成AzurePolicy定义、用测试套件验证、再通过自动化管道发布到多订阅。不过那又是一个更长的故事了先把眼前的测试套件跑稳就是迈向云安全自动化的第一步。