@apexops/cli 0.10.0-next.5 → 0.11.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/assets/capability-packs/registry.v1.json +1 -62
  2. package/assets/client-projection-metadata/copilot-cli-agent-tools.json +12 -0
  3. package/assets/client-projections/github-copilot-cli/.github/copilot/settings.json +13 -0
  4. package/assets/client-projections/github-copilot-cli/.github/instructions/apex-prompt-authoring.instructions.md +1 -1
  5. package/assets/client-projections/github-copilot-cli/.github/workflows/governance-policy-baseline.yml +101 -0
  6. package/assets/client-projections/github-copilot-cli/tools/schemas/governance-baseline.schema.json +118 -0
  7. package/assets/client-projections/github-copilot-cli/tools/scripts/collect-governance-baseline.ps1 +817 -0
  8. package/assets/config/capability-packs.v1.json +1 -32
  9. package/assets/config/governance-reference.v1.json +26379 -0
  10. package/assets/config/recipe-packs.v1.json +1 -26
  11. package/assets/config/runtime-bundle.v1.json +7 -7
  12. package/assets/config/toolchain.v1.json +35 -33
  13. package/assets/config/workflow.v1.json +103 -72
  14. package/assets/customizations/.github/agents/apex-codegen.agent.md +8 -22
  15. package/assets/customizations/.github/agents/apex-validator.agent.md +14 -20
  16. package/assets/customizations/.github/agents/apex.agent.md +108 -75
  17. package/assets/customizations/.github/copilot/settings.json +13 -0
  18. package/assets/customizations/.github/instructions/apex-prompt-authoring.instructions.md +1 -1
  19. package/assets/customizations/.github/skills/apex-architecture/SKILL.md +62 -18
  20. package/assets/customizations/.github/skills/apex-artifacts/SKILL.md +28 -6
  21. package/assets/customizations/.github/skills/apex-azure-adr/SKILL.md +3 -0
  22. package/assets/customizations/.github/skills/apex-azure-cloud-migrate/SKILL.md +3 -0
  23. package/assets/customizations/.github/skills/apex-azure-compliance/SKILL.md +3 -0
  24. package/assets/customizations/.github/skills/apex-azure-compute/SKILL.md +3 -0
  25. package/assets/customizations/.github/skills/apex-azure-cost-optimization/SKILL.md +3 -0
  26. package/assets/customizations/.github/skills/apex-azure-defaults/SKILL.md +3 -0
  27. package/assets/customizations/.github/skills/apex-azure-defaults/references/governance-effects.md +5 -3
  28. package/assets/customizations/.github/skills/apex-azure-deploy/SKILL.md +3 -0
  29. package/assets/customizations/.github/skills/apex-azure-diagnostics/SKILL.md +3 -0
  30. package/assets/customizations/.github/skills/apex-azure-governance/SKILL.md +19 -3
  31. package/assets/customizations/.github/skills/apex-azure-governance/references/evidence-interpretation.md +38 -3
  32. package/assets/customizations/.github/skills/apex-azure-kusto/SKILL.md +3 -0
  33. package/assets/customizations/.github/skills/apex-azure-prepare/SKILL.md +5 -3
  34. package/assets/customizations/.github/skills/apex-azure-quotas/SKILL.md +6 -2
  35. package/assets/customizations/.github/skills/apex-azure-quotas/references/capacity-decision-rules.md +4 -3
  36. package/assets/customizations/.github/skills/apex-azure-quotas/references/operational-checklist.md +3 -2
  37. package/assets/customizations/.github/skills/apex-azure-rbac/SKILL.md +3 -0
  38. package/assets/customizations/.github/skills/apex-azure-resources/SKILL.md +3 -0
  39. package/assets/customizations/.github/skills/apex-azure-storage/SKILL.md +3 -0
  40. package/assets/customizations/.github/skills/apex-azure-validate/SKILL.md +3 -0
  41. package/assets/customizations/.github/skills/apex-bicep-patterns/SKILL.md +3 -0
  42. package/assets/customizations/.github/skills/apex-codegen/SKILL.md +6 -2
  43. package/assets/customizations/.github/skills/apex-entra-app-registration/SKILL.md +3 -0
  44. package/assets/customizations/.github/skills/apex-mermaid/SKILL.md +3 -0
  45. package/assets/customizations/.github/skills/apex-microsoft-docs/SKILL.md +3 -0
  46. package/assets/customizations/.github/skills/apex-next/SKILL.md +99 -0
  47. package/assets/customizations/.github/skills/apex-operations/SKILL.md +46 -16
  48. package/assets/customizations/.github/skills/apex-planning/SKILL.md +48 -19
  49. package/assets/customizations/.github/skills/apex-requirements/SKILL.md +61 -26
  50. package/assets/customizations/.github/skills/apex-terraform-import/SKILL.md +3 -0
  51. package/assets/customizations/.github/skills/apex-terraform-patterns/SKILL.md +3 -0
  52. package/assets/customizations/.github/skills/apex-terraform-test/SKILL.md +3 -0
  53. package/assets/customizations/.github/skills/apex-unslop/LICENSE.txt +21 -0
  54. package/assets/customizations/.github/skills/apex-unslop/SKILL.md +94 -0
  55. package/assets/customizations/.github/skills/apex-workflow/SKILL.md +15 -5
  56. package/assets/customizations/.github/workflows/governance-policy-baseline.yml +101 -0
  57. package/assets/customizations/README.md +8 -9
  58. package/assets/customizations/manifest.json +167 -209
  59. package/assets/customizations/tools/schemas/governance-baseline.schema.json +118 -0
  60. package/assets/customizations/tools/scripts/collect-governance-baseline.ps1 +817 -0
  61. package/assets/manifest.json +423 -4241
  62. package/assets/schemas/governance-baseline.schema.json +118 -0
  63. package/dist/assets.d.ts +18 -2
  64. package/dist/assets.js +115 -35
  65. package/dist/bootstrap-wizard.d.ts +37 -0
  66. package/dist/bootstrap-wizard.js +259 -0
  67. package/dist/cli.d.ts +17 -2
  68. package/dist/cli.js +180 -63
  69. package/dist/dependency-revision.js +8 -0
  70. package/dist/errors.d.ts +6 -1
  71. package/dist/errors.js +55 -0
  72. package/dist/host-profile.d.ts +47 -0
  73. package/dist/host-profile.js +269 -0
  74. package/dist/mcp-output-schemas.d.ts +43 -0
  75. package/dist/mcp-output-schemas.js +279 -0
  76. package/dist/mcp-tool-effects.d.ts +101 -0
  77. package/dist/mcp-tool-effects.js +66 -0
  78. package/dist/mcp.d.ts +50 -3
  79. package/dist/mcp.js +800 -87
  80. package/dist/runtime-binding.d.ts +37 -0
  81. package/dist/runtime-binding.js +131 -0
  82. package/dist/service.d.ts +643 -38
  83. package/dist/service.js +4937 -662
  84. package/dist/state-transfer.js +21 -1
  85. package/dist/target-scope.d.ts +4 -0
  86. package/dist/target-scope.js +9 -0
  87. package/dist/version.d.ts +3 -1
  88. package/dist/version.js +19 -1
  89. package/dist/workflow-validators.d.ts +20 -3
  90. package/dist/workflow-validators.js +408 -67
  91. package/dist/workspace-root.d.ts +7 -0
  92. package/dist/workspace-root.js +67 -0
  93. package/package.json +11 -12
  94. package/assets/capability-packs/azure-governance-discovery/source/scripts/discover.py +0 -1189
  95. package/assets/capability-packs/azure-governance-discovery/source/scripts/render_governance.py +0 -676
  96. package/assets/client-projections/github-copilot-cli/.github/agents/apex-architect.agent.md +0 -91
  97. package/assets/client-projections/github-copilot-cli/.github/agents/apex-operator.agent.md +0 -75
  98. package/assets/client-projections/github-copilot-cli/.github/agents/apex-planner.agent.md +0 -88
  99. package/assets/client-projections/github-copilot-cli/.github/agents/apex-requirements.agent.md +0 -99
  100. package/assets/client-projections/github-copilot-cli/.github/agents/apex.agent.md +0 -88
  101. package/assets/client-projections/github-copilot-cli/.github/mcp.json +0 -65
  102. package/assets/client-projections/github-copilot-cli/.github/skills/apex-architecture/SKILL.md +0 -36
  103. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/SKILL.md +0 -65
  104. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/references/presentation-conventions.md +0 -52
  105. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/references/reference-only-outlines.md +0 -78
  106. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/architecture-assessment.md +0 -35
  107. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/cost-estimate.md +0 -47
  108. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/deployment-summary.md +0 -35
  109. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/governance-constraints.md +0 -35
  110. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/implementation-plan.md +0 -43
  111. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/operations-runbook.md +0 -35
  112. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/requirements.md +0 -42
  113. package/assets/client-projections/github-copilot-cli/.github/skills/apex-artifacts/templates/resource-inventory.md +0 -29
  114. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-adr/SKILL.md +0 -52
  115. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-adr/references/decision-examples.md +0 -38
  116. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-adr/references/decision-guardrails.md +0 -36
  117. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-adr/references/decision-quality.md +0 -21
  118. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-adr/references/decision-record-fields.md +0 -57
  119. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cloud-migrate/SKILL.md +0 -55
  120. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cloud-migrate/references/lambda-to-functions-assessment.md +0 -29
  121. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cloud-migrate/references/migration-readiness.md +0 -27
  122. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cloud-migrate/references/staged-validation-handoff.md +0 -26
  123. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cloud-migrate/references/workload-mapping.md +0 -27
  124. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compliance/SKILL.md +0 -58
  125. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compliance/references/compliance-finding-criteria.md +0 -63
  126. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compliance/references/operational-checklist.md +0 -24
  127. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compute/SKILL.md +0 -36
  128. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compute/references/compute-selection.md +0 -26
  129. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compute/references/pricing-evidence.md +0 -11
  130. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-compute/references/recommendation-and-scale-rules.md +0 -44
  131. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cost-optimization/SKILL.md +0 -61
  132. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cost-optimization/references/cost-assessment-criteria.md +0 -61
  133. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-cost-optimization/references/operational-checklist.md +0 -24
  134. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/SKILL.md +0 -59
  135. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/avm-binding-guidance.md +0 -29
  136. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/baseline-fallbacks.md +0 -53
  137. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/cost-monitoring.md +0 -47
  138. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/decision-boundaries.md +0 -19
  139. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/governance-effects.md +0 -28
  140. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/naming.md +0 -47
  141. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/network-planning.md +0 -47
  142. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/security-baseline.md +0 -40
  143. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/service-selection.md +0 -51
  144. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-defaults/references/tag-precedence.md +0 -31
  145. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-deploy/SKILL.md +0 -50
  146. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-deploy/references/operational-checklist.md +0 -26
  147. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-deploy/references/preview-recovery-verification.md +0 -48
  148. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-diagnostics/SKILL.md +0 -60
  149. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-diagnostics/references/diagnostic-interpretation.md +0 -42
  150. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-diagnostics/references/operational-checklist.md +0 -24
  151. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-governance/SKILL.md +0 -52
  152. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-governance/references/evidence-interpretation.md +0 -58
  153. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-governance/references/operational-checklist.md +0 -25
  154. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-kusto/SKILL.md +0 -58
  155. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-kusto/references/kql-evidence-interpretation.md +0 -41
  156. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-kusto/references/operational-checklist.md +0 -25
  157. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-prepare/SKILL.md +0 -52
  158. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-prepare/references/functions-recipe-pack.md +0 -27
  159. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-prepare/references/preparation-lineage.md +0 -36
  160. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-quotas/SKILL.md +0 -53
  161. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-quotas/references/capacity-decision-rules.md +0 -48
  162. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-quotas/references/operational-checklist.md +0 -23
  163. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-rbac/SKILL.md +0 -45
  164. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-rbac/references/assignment-intent.md +0 -39
  165. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-rbac/references/least-privilege-selection.md +0 -50
  166. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-resources/SKILL.md +0 -54
  167. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-resources/references/inventory-query-patterns.md +0 -49
  168. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-resources/references/operational-checklist.md +0 -24
  169. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-storage/SKILL.md +0 -35
  170. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-storage/references/security-and-governance.md +0 -12
  171. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-storage/references/service-auth-and-sdk-boundary.md +0 -33
  172. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-storage/references/storage-selection.md +0 -12
  173. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-validate/SKILL.md +0 -49
  174. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-validate/references/operational-checklist.md +0 -24
  175. package/assets/client-projections/github-copilot-cli/.github/skills/apex-azure-validate/references/preflight-evidence.md +0 -43
  176. package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/SKILL.md +0 -41
  177. package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/references/avm-and-codegen-acceptance.md +0 -11
  178. package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/references/codegen-acceptance-checklist.md +0 -46
  179. package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/references/compiler-and-provider-gotchas.md +0 -74
  180. package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/references/module-interfaces-and-parameters.md +0 -56
  181. package/assets/client-projections/github-copilot-cli/.github/skills/apex-bicep-patterns/references/network-and-observability.md +0 -52
  182. package/assets/client-projections/github-copilot-cli/.github/skills/apex-codegen/SKILL.md +0 -32
  183. package/assets/client-projections/github-copilot-cli/.github/skills/apex-entra-app-registration/SKILL.md +0 -53
  184. package/assets/client-projections/github-copilot-cli/.github/skills/apex-entra-app-registration/references/capability-receipts.md +0 -21
  185. package/assets/client-projections/github-copilot-cli/.github/skills/apex-entra-app-registration/references/design-and-diagnostics.md +0 -36
  186. package/assets/client-projections/github-copilot-cli/.github/skills/apex-entra-app-registration/references/oauth-registration-model.md +0 -26
  187. package/assets/client-projections/github-copilot-cli/.github/skills/apex-entra-app-registration/references/permissions-and-credentials.md +0 -27
  188. package/assets/client-projections/github-copilot-cli/.github/skills/apex-mermaid/SKILL.md +0 -44
  189. package/assets/client-projections/github-copilot-cli/.github/skills/apex-mermaid/references/styling.md +0 -33
  190. package/assets/client-projections/github-copilot-cli/.github/skills/apex-mermaid/references/syntax.md +0 -99
  191. package/assets/client-projections/github-copilot-cli/.github/skills/apex-microsoft-docs/SKILL.md +0 -53
  192. package/assets/client-projections/github-copilot-cli/.github/skills/apex-microsoft-docs/references/research-method.md +0 -91
  193. package/assets/client-projections/github-copilot-cli/.github/skills/apex-operations/SKILL.md +0 -34
  194. package/assets/client-projections/github-copilot-cli/.github/skills/apex-planning/SKILL.md +0 -36
  195. package/assets/client-projections/github-copilot-cli/.github/skills/apex-requirements/SKILL.md +0 -46
  196. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-import/SKILL.md +0 -47
  197. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-import/references/adoption-attestation.md +0 -10
  198. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-import/references/import-assessment.md +0 -14
  199. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-import/references/import-mapping.md +0 -27
  200. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-import/references/mapping-and-adoption-attestation.md +0 -12
  201. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/SKILL.md +0 -49
  202. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/references/module-composition-and-refactor.md +0 -20
  203. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/references/module-composition-and-state-boundaries.md +0 -11
  204. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/references/module-locks-and-codegen-acceptance.md +0 -14
  205. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/references/network-and-observability.md +0 -12
  206. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-patterns/references/plan-and-change-assessment.md +0 -24
  207. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-test/SKILL.md +0 -45
  208. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-test/references/evidence-acceptance.md +0 -10
  209. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-test/references/plan-mode-and-mock-design.md +0 -12
  210. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-test/references/plan-mode-test-design.md +0 -23
  211. package/assets/client-projections/github-copilot-cli/.github/skills/apex-terraform-test/references/test-design.md +0 -13
  212. package/assets/client-projections/github-copilot-cli/.github/skills/apex-workflow/SKILL.md +0 -37
  213. package/assets/client-projections/github-copilot-vscode/.github/agents/apex-architect.agent.md +0 -100
  214. package/assets/client-projections/github-copilot-vscode/.github/agents/apex-codegen.agent.md +0 -62
  215. package/assets/client-projections/github-copilot-vscode/.github/agents/apex-operator.agent.md +0 -79
  216. package/assets/client-projections/github-copilot-vscode/.github/agents/apex-planner.agent.md +0 -98
  217. package/assets/client-projections/github-copilot-vscode/.github/agents/apex-requirements.agent.md +0 -108
  218. package/assets/client-projections/github-copilot-vscode/.github/agents/apex-reviewer.agent.md +0 -63
  219. package/assets/client-projections/github-copilot-vscode/.github/agents/apex-validator.agent.md +0 -61
  220. package/assets/client-projections/github-copilot-vscode/.github/agents/apex.agent.md +0 -107
  221. package/assets/client-projections/github-copilot-vscode/.github/copilot-instructions.md +0 -13
  222. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-agent-authoring.instructions.md +0 -14
  223. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-artifact-contracts.instructions.md +0 -12
  224. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-automation.instructions.md +0 -12
  225. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-azure-yaml.instructions.md +0 -12
  226. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-bicep.instructions.md +0 -12
  227. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-code-quality.instructions.md +0 -12
  228. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-context.instructions.md +0 -12
  229. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-documentation.instructions.md +0 -12
  230. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-governance.instructions.md +0 -12
  231. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-instruction-authoring.instructions.md +0 -12
  232. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-javascript.instructions.md +0 -11
  233. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-json.instructions.md +0 -11
  234. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-markdown.instructions.md +0 -13
  235. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-powershell.instructions.md +0 -11
  236. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-prompt-authoring.instructions.md +0 -12
  237. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-python.instructions.md +0 -11
  238. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-safe-file-edits.instructions.md +0 -11
  239. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-safe-shell.instructions.md +0 -11
  240. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-shell.instructions.md +0 -11
  241. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-skill-authoring.instructions.md +0 -13
  242. package/assets/client-projections/github-copilot-vscode/.github/instructions/apex-terraform.instructions.md +0 -12
  243. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-architecture/SKILL.md +0 -36
  244. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/SKILL.md +0 -65
  245. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/references/presentation-conventions.md +0 -52
  246. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/references/reference-only-outlines.md +0 -78
  247. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/architecture-assessment.md +0 -35
  248. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/cost-estimate.md +0 -47
  249. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/deployment-summary.md +0 -35
  250. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/governance-constraints.md +0 -35
  251. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/implementation-plan.md +0 -43
  252. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/operations-runbook.md +0 -35
  253. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/requirements.md +0 -42
  254. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-artifacts/templates/resource-inventory.md +0 -29
  255. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-adr/SKILL.md +0 -52
  256. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-adr/references/decision-examples.md +0 -38
  257. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-adr/references/decision-guardrails.md +0 -36
  258. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-adr/references/decision-quality.md +0 -21
  259. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-adr/references/decision-record-fields.md +0 -57
  260. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cloud-migrate/SKILL.md +0 -55
  261. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cloud-migrate/references/lambda-to-functions-assessment.md +0 -29
  262. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cloud-migrate/references/migration-readiness.md +0 -27
  263. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cloud-migrate/references/staged-validation-handoff.md +0 -26
  264. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cloud-migrate/references/workload-mapping.md +0 -27
  265. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compliance/SKILL.md +0 -58
  266. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compliance/references/compliance-finding-criteria.md +0 -63
  267. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compliance/references/operational-checklist.md +0 -24
  268. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compute/SKILL.md +0 -36
  269. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compute/references/compute-selection.md +0 -26
  270. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compute/references/pricing-evidence.md +0 -11
  271. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-compute/references/recommendation-and-scale-rules.md +0 -44
  272. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cost-optimization/SKILL.md +0 -61
  273. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cost-optimization/references/cost-assessment-criteria.md +0 -61
  274. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-cost-optimization/references/operational-checklist.md +0 -24
  275. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/SKILL.md +0 -59
  276. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/avm-binding-guidance.md +0 -29
  277. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/baseline-fallbacks.md +0 -53
  278. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/cost-monitoring.md +0 -47
  279. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/decision-boundaries.md +0 -19
  280. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/governance-effects.md +0 -28
  281. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/naming.md +0 -47
  282. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/network-planning.md +0 -47
  283. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/security-baseline.md +0 -40
  284. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/service-selection.md +0 -51
  285. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-defaults/references/tag-precedence.md +0 -31
  286. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-deploy/SKILL.md +0 -50
  287. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-deploy/references/operational-checklist.md +0 -26
  288. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-deploy/references/preview-recovery-verification.md +0 -48
  289. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-diagnostics/SKILL.md +0 -60
  290. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-diagnostics/references/diagnostic-interpretation.md +0 -42
  291. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-diagnostics/references/operational-checklist.md +0 -24
  292. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-governance/SKILL.md +0 -52
  293. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-governance/references/evidence-interpretation.md +0 -58
  294. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-governance/references/operational-checklist.md +0 -25
  295. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-kusto/SKILL.md +0 -58
  296. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-kusto/references/kql-evidence-interpretation.md +0 -41
  297. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-kusto/references/operational-checklist.md +0 -25
  298. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-prepare/SKILL.md +0 -52
  299. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-prepare/references/functions-recipe-pack.md +0 -27
  300. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-prepare/references/preparation-lineage.md +0 -36
  301. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-quotas/SKILL.md +0 -53
  302. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-quotas/references/capacity-decision-rules.md +0 -48
  303. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-quotas/references/operational-checklist.md +0 -23
  304. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-rbac/SKILL.md +0 -45
  305. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-rbac/references/assignment-intent.md +0 -39
  306. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-rbac/references/least-privilege-selection.md +0 -50
  307. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-resources/SKILL.md +0 -54
  308. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-resources/references/inventory-query-patterns.md +0 -49
  309. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-resources/references/operational-checklist.md +0 -24
  310. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-storage/SKILL.md +0 -35
  311. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-storage/references/security-and-governance.md +0 -12
  312. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-storage/references/service-auth-and-sdk-boundary.md +0 -33
  313. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-storage/references/storage-selection.md +0 -12
  314. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-validate/SKILL.md +0 -49
  315. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-validate/references/operational-checklist.md +0 -24
  316. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-azure-validate/references/preflight-evidence.md +0 -43
  317. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/SKILL.md +0 -41
  318. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/references/avm-and-codegen-acceptance.md +0 -11
  319. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/references/codegen-acceptance-checklist.md +0 -46
  320. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/references/compiler-and-provider-gotchas.md +0 -74
  321. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/references/module-interfaces-and-parameters.md +0 -56
  322. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-bicep-patterns/references/network-and-observability.md +0 -52
  323. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-codegen/SKILL.md +0 -32
  324. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-entra-app-registration/SKILL.md +0 -53
  325. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-entra-app-registration/references/capability-receipts.md +0 -21
  326. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-entra-app-registration/references/design-and-diagnostics.md +0 -36
  327. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-entra-app-registration/references/oauth-registration-model.md +0 -26
  328. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-entra-app-registration/references/permissions-and-credentials.md +0 -27
  329. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-mermaid/SKILL.md +0 -44
  330. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-mermaid/references/styling.md +0 -33
  331. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-mermaid/references/syntax.md +0 -99
  332. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-microsoft-docs/SKILL.md +0 -53
  333. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-microsoft-docs/references/research-method.md +0 -91
  334. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-operations/SKILL.md +0 -34
  335. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-planning/SKILL.md +0 -36
  336. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-requirements/SKILL.md +0 -46
  337. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-import/SKILL.md +0 -47
  338. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-import/references/adoption-attestation.md +0 -10
  339. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-import/references/import-assessment.md +0 -14
  340. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-import/references/import-mapping.md +0 -27
  341. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-import/references/mapping-and-adoption-attestation.md +0 -12
  342. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/SKILL.md +0 -49
  343. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/references/module-composition-and-refactor.md +0 -20
  344. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/references/module-composition-and-state-boundaries.md +0 -11
  345. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/references/module-locks-and-codegen-acceptance.md +0 -14
  346. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/references/network-and-observability.md +0 -12
  347. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-patterns/references/plan-and-change-assessment.md +0 -24
  348. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-test/SKILL.md +0 -45
  349. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-test/references/evidence-acceptance.md +0 -10
  350. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-test/references/plan-mode-and-mock-design.md +0 -12
  351. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-test/references/plan-mode-test-design.md +0 -23
  352. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-terraform-test/references/test-design.md +0 -13
  353. package/assets/client-projections/github-copilot-vscode/.github/skills/apex-workflow/SKILL.md +0 -37
  354. package/assets/client-projections/github-copilot-vscode/.vscode/mcp.json +0 -29
  355. package/assets/customizations/.github/agents/apex-architect.agent.md +0 -93
  356. package/assets/customizations/.github/agents/apex-operator.agent.md +0 -72
  357. package/assets/customizations/.github/agents/apex-planner.agent.md +0 -91
  358. package/assets/customizations/.github/agents/apex-requirements.agent.md +0 -101
  359. package/assets/customizations/.github/agents/apex-reviewer.agent.md +0 -60
  360. package/assets/customizations/.github/mcp.json +0 -65
  361. package/assets/customizations/.github/skills/apex-azure-prepare/references/functions-recipe-pack.md +0 -27
  362. package/assets/customizations/.vscode/mcp.json +0 -29
  363. package/dist/azure-mcp-environment.d.ts +0 -2
  364. package/dist/azure-mcp-environment.js +0 -7
  365. package/dist/azure-mcp.d.ts +0 -2
  366. package/dist/azure-mcp.js +0 -21
@@ -1,1189 +0,0 @@
1
- #!/usr/bin/env python3
2
- # ruff: noqa: E501
3
- """Deterministic Azure Policy discovery for 04g-Governance.
4
-
5
- Replaces the legacy governance-discovery-subagent. Lists effective policy
6
- assignments at subscription scope (including MG-inherited), pulls definitions,
7
- set-definitions, and exemptions, classifies effects, filters Defender
8
- auto-assignments, and writes a governance-constraints-v1 envelope.
9
-
10
- Stdout line 1 is always a one-line JSON status object. Optional Markdown
11
- preview follows on later lines. Stderr carries warnings and filter notes.
12
- """
13
- from __future__ import annotations
14
-
15
- import argparse
16
- import json
17
- import os
18
- import re
19
- import subprocess
20
- import sys
21
- import time
22
- import urllib.error
23
- import urllib.request
24
- from concurrent.futures import ThreadPoolExecutor
25
- from datetime import datetime, timezone
26
- from pathlib import Path
27
- from typing import Any, Callable
28
-
29
- # --------------------------------------------------------------------------- #
30
- # Constants #
31
- # --------------------------------------------------------------------------- #
32
-
33
- API_ASSIGNMENTS = "2022-06-01"
34
- API_DEFINITIONS = "2021-06-01"
35
- API_EXEMPTIONS = "2022-07-01-preview"
36
- ARM = "https://management.azure.com"
37
-
38
- # L0 envelope: staleness threshold (days). Downstream consumers (Planner,
39
- # CodeGen, Deploy) treat envelopes older than this as STALE and refuse to
40
- # proceed without a fresh `--refresh` discovery.
41
- DEFAULT_TTL_DAYS = 7
42
-
43
- # Only these effects are plan-blocking. Everything else lives in the summary.
44
- BLOCKER_EFFECTS = {"Deny"}
45
- AUTO_REMEDIATE_EFFECTS = {"DeployIfNotExists", "Modify"}
46
- RELEVANT_EFFECTS = BLOCKER_EFFECTS | AUTO_REMEDIATE_EFFECTS
47
-
48
- # Defender-for-Cloud auto-assignment marker. See
49
- # references/effect-classification.md for background.
50
- DEFENDER_ASSIGNED_BY_VALUES = {
51
- "Security Center",
52
- "Microsoft Defender for Cloud",
53
- }
54
-
55
- # Resource-type to Bicep singular-lowercase mapping. Extend as needed; the
56
- # fallback derives from the full ARM type string.
57
- BICEP_TYPE_OVERRIDES = {
58
- "Microsoft.Storage/storageAccounts": "storageAccounts",
59
- "Microsoft.Sql/servers": "sqlServers",
60
- "Microsoft.Sql/servers/databases": "sqlServers/databases",
61
- "Microsoft.KeyVault/vaults": "keyVaults",
62
- "Microsoft.Web/sites": "sites",
63
- "Microsoft.Network/virtualNetworks": "virtualNetworks",
64
- "Microsoft.Compute/virtualMachines": "virtualMachines",
65
- "Microsoft.Resources/subscriptions/resourceGroups": "resourceGroups",
66
- }
67
-
68
-
69
- # --------------------------------------------------------------------------- #
70
- # Azure CLI shim (injectable for tests) #
71
- # --------------------------------------------------------------------------- #
72
-
73
- # Parallelism cap for concurrent ARM calls. ARM throttles aggressively above
74
- # ~16 concurrent; 8 is a safe default that still gives ~4-6x speedup over
75
- # sequential `az rest` subprocess invocations.
76
- _PARALLEL_WORKERS = 8
77
-
78
- # Process-wide ARM token cache. `az account get-access-token` takes ~0.5-1s
79
- # per invocation; caching saves that overhead on every subsequent REST call.
80
- _TOKEN_CACHE: dict[str, Any] = {"token": None, "expires_at": 0.0}
81
-
82
-
83
- def _get_arm_token() -> str:
84
- """Return a cached ARM bearer token, refreshing when near expiry."""
85
- now = time.time()
86
- if _TOKEN_CACHE["token"] and _TOKEN_CACHE["expires_at"] > now + 60:
87
- return _TOKEN_CACHE["token"]
88
- out = subprocess.check_output( # noqa: S603 — trusted Azure CLI
89
- [
90
- "az",
91
- "account",
92
- "get-access-token",
93
- "--resource",
94
- f"{ARM}/",
95
- "-o",
96
- "json",
97
- ],
98
- text=True,
99
- timeout=30,
100
- )
101
- data = json.loads(out)
102
- _TOKEN_CACHE["token"] = data["accessToken"]
103
- # Tokens are valid for ~60 min. Use 50-min safe window to avoid mid-run
104
- # expiry. `expiresOn` format varies across az versions, so we compute
105
- # relative expiry from wall-clock instead of parsing it.
106
- _TOKEN_CACHE["expires_at"] = now + 50 * 60
107
- return data["accessToken"]
108
-
109
-
110
- def _default_az_rest(url: str) -> dict[str, Any]:
111
- """GET `url` with the cached ARM bearer token via stdlib urllib.
112
-
113
- Replaces per-call `az rest` subprocess (saves ~0.5-1s × N calls). Returns
114
- `{}` for 404 (missing policy definition) so callers can filter silently.
115
- Raises on auth failures and other HTTP errors.
116
- """
117
- token = _get_arm_token()
118
- req = urllib.request.Request(
119
- url,
120
- headers={
121
- "Authorization": f"Bearer {token}",
122
- "Content-Type": "application/json",
123
- },
124
- method="GET",
125
- )
126
- try:
127
- with urllib.request.urlopen(req, timeout=60) as resp: # noqa: S310 — ARM https
128
- return json.loads(resp.read().decode("utf-8"))
129
- except urllib.error.HTTPError as e:
130
- if e.code == 404:
131
- return {}
132
- detail = ""
133
- try:
134
- detail = e.read().decode("utf-8", errors="replace")[:500]
135
- except Exception: # pragma: no cover
136
- pass
137
- raise RuntimeError(
138
- f"ARM GET {url} failed: HTTP {e.code} {e.reason}; {detail}"
139
- ) from e
140
-
141
-
142
- def _default_get_subscription() -> str:
143
- out = subprocess.check_output( # noqa: S603
144
- ["az", "account", "show", "--query", "id", "-o", "tsv"],
145
- text=True,
146
- timeout=30,
147
- )
148
- return out.strip()
149
-
150
-
151
- def _default_check_auth() -> None:
152
- """Raise if the ARM token cannot be obtained."""
153
- # Warms the token cache as a side effect so subsequent REST calls are fast.
154
- _get_arm_token()
155
-
156
-
157
- # --------------------------------------------------------------------------- #
158
- # Discovery #
159
- # --------------------------------------------------------------------------- #
160
-
161
-
162
- def _list_all(az_rest: Callable[[str], dict[str, Any]], url: str) -> list[dict[str, Any]]:
163
- """Follow `nextLink` pagination and return the union of `value` arrays."""
164
- items: list[dict[str, Any]] = []
165
- next_url: str | None = url
166
- while next_url:
167
- page = az_rest(next_url)
168
- items.extend(page.get("value", []))
169
- next_url = page.get("nextLink")
170
- return items
171
-
172
-
173
- def _parallel_list(
174
- az_rest: Callable[[str], dict[str, Any]],
175
- urls: dict[str, str],
176
- ) -> dict[str, list[dict[str, Any]]]:
177
- """Run multiple paginated list calls concurrently. Preserves key mapping.
178
-
179
- Uses a thread pool because `az_rest` is a blocking callable (either
180
- urllib GET or subprocess `az rest`). Thread-per-URL is fine at this
181
- concurrency level.
182
- """
183
- if not urls:
184
- return {}
185
- with ThreadPoolExecutor(max_workers=min(_PARALLEL_WORKERS, len(urls))) as pool:
186
- futures = {key: pool.submit(_list_all, az_rest, url) for key, url in urls.items()}
187
- return {key: fut.result() for key, fut in futures.items()}
188
-
189
-
190
- def _parallel_fetch_items(
191
- az_rest: Callable[[str], dict[str, Any]],
192
- urls: list[str],
193
- expected_ids: list[str] | None = None,
194
- ) -> list[dict[str, Any] | None]:
195
- """Fetch individual items concurrently; tolerate both response shapes.
196
-
197
- Azure returns a single object for GET `.../policyDefinitions/{name}`, but
198
- injected test fakes commonly match on a URL substring and return the
199
- wrapped list shape `{"value": [...]}`. This helper accepts both:
200
-
201
- - dict with no `value` key → single-item payload (return as-is, unless empty)
202
- - dict with `value` list → look for the item whose id matches the expected
203
- id (preserved positionally via `expected_ids`); fall back to first
204
- """
205
- if not urls:
206
- return []
207
- if expected_ids is None:
208
- expected_ids = [""] * len(urls)
209
-
210
- def _fetch(url: str, expected_id: str) -> dict[str, Any] | None:
211
- resp = az_rest(url)
212
- if not isinstance(resp, dict) or not resp:
213
- return None
214
- if "value" in resp and isinstance(resp["value"], list):
215
- if expected_id:
216
- for item in resp["value"]:
217
- if (item.get("id") or "").lower() == expected_id.lower():
218
- return item
219
- return resp["value"][0] if resp["value"] else None
220
- if resp.get("id"):
221
- return resp
222
- return None
223
-
224
- with ThreadPoolExecutor(max_workers=min(_PARALLEL_WORKERS, len(urls))) as pool:
225
- return list(pool.map(_fetch, urls, expected_ids))
226
-
227
-
228
- _PARAMETER_EXPRESSION_RE = re.compile(r"^\[parameters\('([^']+)'\)\]$", re.IGNORECASE)
229
-
230
-
231
- def _resolve_parameter_value(value: Any, assignment_params: dict[str, Any]) -> Any:
232
- if isinstance(value, str):
233
- match = _PARAMETER_EXPRESSION_RE.match(value)
234
- if match:
235
- return assignment_params.get(match.group(1))
236
- return value
237
-
238
-
239
- def _effect_of(defn: dict[str, Any], assignment_params: dict[str, Any] | None = None) -> str | None:
240
- rule = (defn.get("properties") or {}).get("policyRule") or {}
241
- then = rule.get("then") or {}
242
- eff = then.get("effect")
243
- assignment_params = assignment_params or {}
244
- eff = _resolve_parameter_value(eff, assignment_params)
245
- if eff is None:
246
- expression = then.get("effect")
247
- if isinstance(expression, str):
248
- match = _PARAMETER_EXPRESSION_RE.match(expression)
249
- if match:
250
- eff = (((defn.get("properties") or {}).get("parameters") or {}).get(match.group(1)) or {}).get(
251
- "defaultValue"
252
- )
253
- if not isinstance(eff, str):
254
- return None
255
- # Canonicalise case (some definitions use lowercase).
256
- mapping = {
257
- "deny": "Deny",
258
- "audit": "Audit",
259
- "auditifnotexists": "AuditIfNotExists",
260
- "deployifnotexists": "DeployIfNotExists",
261
- "modify": "Modify",
262
- "append": "Append",
263
- "disabled": "Disabled",
264
- }
265
- return mapping.get(eff.lower(), eff)
266
-
267
-
268
- def _resource_types(defn: dict[str, Any]) -> list[str]:
269
- rule = (defn.get("properties") or {}).get("policyRule") or {}
270
- types: set[str] = set()
271
- stack = [rule.get("if")]
272
- while stack:
273
- node = stack.pop()
274
- if isinstance(node, dict):
275
- if node.get("field") == "type" and isinstance(node.get("equals"), str):
276
- types.add(node["equals"])
277
- stack.extend(node.values())
278
- elif isinstance(node, list):
279
- stack.extend(node)
280
- return sorted(types)
281
-
282
-
283
- def _required_value(defn: dict[str, Any]) -> Any:
284
- """Extract the value a Deny/Modify policy requires."""
285
- props = defn.get("properties") or {}
286
- then = (props.get("policyRule") or {}).get("then") or {}
287
- details = then.get("details") or {}
288
- if not isinstance(details, dict):
289
- details = {}
290
-
291
- if "value" in details:
292
- return details["value"]
293
-
294
- ops = details.get("operations") or []
295
- if isinstance(ops, list) and len(ops) == 1:
296
- op = ops[0] or {}
297
- if "value" in op:
298
- return op["value"]
299
-
300
- params = props.get("parameters") or {}
301
- for pname, pval in params.items():
302
- dv = (pval or {}).get("defaultValue")
303
- key = pname.lower()
304
- if dv is not None and key in {
305
- "allowedlocations",
306
- "listofallowedlocations",
307
- "allowedskus",
308
- "listofallowedskus",
309
- "tagname",
310
- "tagvalue",
311
- "minimumtlsversion",
312
- "requiredretentiondays",
313
- }:
314
- return dv
315
- return None
316
-
317
-
318
- def _property_paths(defn: dict[str, Any], resource_types: list[str]) -> dict[str, str | None]:
319
- """Best-effort extraction of azurePropertyPath / bicepPropertyPath.
320
-
321
- Walks the policyRule looking for `field: "<Type>/<path>"` patterns, which
322
- is how Azure Policy addresses resource properties. Falls back to the
323
- `Modify` / `DeployIfNotExists` operation field when the top-level rule
324
- does not name a field.
325
- """
326
- rule = (defn.get("properties") or {}).get("policyRule") or {}
327
-
328
- # Tag policies address `tags['<name>']`, often without any `field: type`.
329
- # Check before the resource-type short-circuit below.
330
- category = ((((defn.get("properties") or {}).get("metadata") or {}).get("category")) or "").lower()
331
- if _looks_like_tag_policy(rule) and category == "tags":
332
- return {
333
- "azurePropertyPath": "resourceGroup.tags",
334
- "bicepPropertyPath": "resourceGroups::tags",
335
- "pathSemantics": "tag-policy-non-property",
336
- }
337
-
338
- if not resource_types:
339
- return {"azurePropertyPath": "", "bicepPropertyPath": ""}
340
-
341
- primary_type = resource_types[0]
342
-
343
- candidate_fields: list[str] = []
344
- stack = [rule.get("if"), rule.get("then")]
345
- while stack:
346
- node = stack.pop()
347
- if isinstance(node, dict):
348
- f = node.get("field")
349
- if isinstance(f, str) and "/" in f and f.startswith(primary_type):
350
- candidate_fields.append(f)
351
- stack.extend(node.values())
352
- elif isinstance(node, list):
353
- stack.extend(node)
354
-
355
- # Also check modify operations for their `field` property.
356
- then = rule.get("then") or {}
357
- ops = (then.get("details") or {}).get("operations") or []
358
- if isinstance(ops, list):
359
- for op in ops:
360
- f = (op or {}).get("field")
361
- if isinstance(f, str) and f.startswith(primary_type):
362
- candidate_fields.append(f)
363
-
364
- if not candidate_fields:
365
- return {"azurePropertyPath": "", "bicepPropertyPath": ""}
366
-
367
- # Prefer the field that names the deepest property path (most slashes
368
- # after the type prefix); otherwise take the first candidate.
369
- prefix = primary_type + "/"
370
- scored = [
371
- (f[len(prefix):] if f.startswith(prefix) else f.rsplit("/", 1)[-1], f)
372
- for f in candidate_fields
373
- ]
374
- # Drop empty tails and sort by descending depth.
375
- scored = [(tail, orig) for tail, orig in scored if tail]
376
- if not scored:
377
- return {"azurePropertyPath": "", "bicepPropertyPath": ""}
378
- scored.sort(key=lambda pair: pair[0].count("/"), reverse=True)
379
- path = scored[0][0]
380
- # Azure path: lowerCamel resource type + dot + property path.
381
- provider_type = primary_type.split("/", 1)[-1]
382
- camel_type = provider_type[:1].lower() + provider_type[1:]
383
- azure_path = f"{camel_type}.{path}"
384
- bicep_segment = BICEP_TYPE_OVERRIDES.get(primary_type, camel_type)
385
- bicep_path = f"{bicep_segment}::{path}"
386
- return {"azurePropertyPath": azure_path, "bicepPropertyPath": bicep_path}
387
-
388
-
389
- def _looks_like_tag_policy(rule: dict[str, Any]) -> bool:
390
- operations = (((rule.get("then") or {}).get("details") or {}).get("operations") or [])
391
- if isinstance(operations, list) and operations:
392
- operation_fields = [
393
- (operation or {}).get("field")
394
- for operation in operations
395
- if isinstance((operation or {}).get("field"), str)
396
- ]
397
- if operation_fields and not all(field.lower().startswith("tags[") for field in operation_fields):
398
- return False
399
- stack = [rule.get("if")]
400
- while stack:
401
- node = stack.pop()
402
- if isinstance(node, dict):
403
- f = node.get("field")
404
- if isinstance(f, str) and f.lower().startswith("tags["):
405
- return True
406
- stack.extend(node.values())
407
- elif isinstance(node, list):
408
- stack.extend(node)
409
- return False
410
-
411
-
412
- _TAG_FIELD_RE = re.compile(r"^tags\[(?:'|\")?([^'\"\]]+)(?:'|\")?\]$", re.IGNORECASE)
413
-
414
-
415
- def _extract_policy_rule_tag_keys(defn: dict[str, Any]) -> list[str]:
416
- """Walk policyRule looking for hard-coded `tags['<key>']` field references.
417
-
418
- Many custom deny-style tag policies (e.g. `JV-Enforce Resource Group Tags`)
419
- hard-code the enforced tag list in `policyRule.if.allOf[*].anyOf[*].field`
420
- instead of exposing it as `tagName*` assignment parameters. Without this
421
- extraction the downstream `tag_contract.required_tag_keys` silently falls
422
- back to a sibling Modify policy's keys, producing transcription drift
423
- (e.g. `tech-contact` vs `technical-contact`). See
424
- `tools/scripts/diagnose-governance-tag-drift.md` for the full root-cause.
425
- """
426
- rule = (defn.get("properties") or {}).get("policyRule") or {}
427
- keys: list[str] = []
428
- seen: set[str] = set()
429
- stack: list[Any] = [rule.get("if")]
430
- while stack:
431
- node = stack.pop()
432
- if isinstance(node, dict):
433
- field = node.get("field")
434
- if isinstance(field, str):
435
- m = _TAG_FIELD_RE.match(field.strip())
436
- if m:
437
- key = m.group(1).strip()
438
- if key and key not in seen:
439
- seen.add(key)
440
- keys.append(key)
441
- stack.extend(node.values())
442
- elif isinstance(node, list):
443
- stack.extend(node)
444
- return keys
445
-
446
-
447
- def _is_defender_auto(assignment: dict[str, Any]) -> bool:
448
- metadata = (assignment.get("properties") or {}).get("metadata") or {}
449
- assigned_by = metadata.get("assignedBy")
450
- if isinstance(assigned_by, str) and assigned_by.strip() in DEFENDER_ASSIGNED_BY_VALUES:
451
- return True
452
- created_by = metadata.get("createdBy")
453
- if isinstance(created_by, dict):
454
- name = (created_by.get("displayName") or "").strip()
455
- if name in DEFENDER_ASSIGNED_BY_VALUES:
456
- return True
457
- return False
458
-
459
-
460
- def _build_exemption_map(exemptions: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
461
- """Map lowercase policyAssignmentId → exemption summary."""
462
- out: dict[str, dict[str, Any]] = {}
463
- for ex in exemptions:
464
- props = ex.get("properties") or {}
465
- aid = (props.get("policyAssignmentId") or "").lower()
466
- if not aid:
467
- continue
468
- out[aid] = {
469
- "exemptionCategory": props.get("exemptionCategory"),
470
- "expiresOn": props.get("expiresOn"),
471
- "description": props.get("description"),
472
- "policyDefinitionReferenceIds": props.get("policyDefinitionReferenceIds") or [],
473
- }
474
- return out
475
-
476
-
477
- # --------------------------------------------------------------------------- #
478
- # Envelope enrichment helpers (Fix E) #
479
- # --------------------------------------------------------------------------- #
480
-
481
-
482
- def _extract_tags_required(findings: list[dict[str, Any]]) -> list[dict[str, str]]:
483
- """Extract tag-enforcement findings into a flat tags_required list.
484
-
485
- Looks for actual tag key names in assignment parameters (tagName, tagNames)
486
- and in required_value. Falls back to category-based matching for tag policies
487
- that don't use pathSemantics.
488
- """
489
- seen: set[str] = set()
490
- tags: list[dict[str, str]] = []
491
-
492
- for f in findings:
493
- is_tag = (
494
- f.get("pathSemantics") == "tag-policy-non-property"
495
- or (f.get("category") or "").lower() == "tags"
496
- )
497
- if not is_tag:
498
- continue
499
-
500
- # Try to extract actual tag keys from assignment parameters.
501
- params = f.get("assignment_parameters") or {}
502
- tag_keys: list[str] = []
503
-
504
- # Deny-style tag policies often hard-code keys in policyRule rather
505
- # than exposing them via tagName* parameters. Prefer those — they are
506
- # the authoritative enforced list.
507
- for key in (f.get("extracted_tag_keys") or []):
508
- if isinstance(key, str) and key:
509
- tag_keys.append(key)
510
-
511
- # Common parameter names for tag policies (Modify/Append style).
512
- for pname in ("tagName", "tagname", "tag_name"):
513
- val = params.get(pname)
514
- if isinstance(val, str) and val:
515
- tag_keys.append(val)
516
- elif isinstance(val, list):
517
- tag_keys.extend(str(v) for v in val if v)
518
-
519
- for pname, val in params.items():
520
- if re.fullmatch(r"tagName\d+", pname, re.IGNORECASE) and isinstance(val, str) and val:
521
- tag_keys.append(val)
522
-
523
- # Some policies use tagNames (plural) or listOfTagNames
524
- for pname in ("tagNames", "listOfTagNames", "tagnames"):
525
- val = params.get(pname)
526
- if isinstance(val, list):
527
- tag_keys.extend(str(v) for v in val if v)
528
-
529
- source = f.get("policy_id", "")
530
- policy_name = (f.get("display_name") or "").strip()
531
-
532
- if tag_keys:
533
- for key in tag_keys:
534
- key = key.strip()
535
- if key and key not in seen:
536
- seen.add(key)
537
- tags.append({
538
- "name": key,
539
- "source_policy": source,
540
- "source_assignment": f.get("assignment_display_name", ""),
541
- })
542
- elif policy_name and policy_name not in seen:
543
- # No specific tag key found — record the policy as a tag requirement
544
- # so the agent knows to investigate further.
545
- seen.add(policy_name)
546
- tags.append({
547
- "name": f"[unresolved: {policy_name}]",
548
- "source_policy": source,
549
- "source_assignment": f.get("assignment_display_name", ""),
550
- "unresolved": "true",
551
- })
552
- return tags
553
-
554
-
555
- def _is_location_constraint(defn: dict[str, Any]) -> bool:
556
- props = defn.get("properties") or {}
557
- if any(name.lower() in {"listofallowedlocations", "allowedlocations"} for name in (props.get("parameters") or {})):
558
- return True
559
- display_name = props.get("displayName") or ""
560
- if not re.search(r"\b(?:locations?|regions?)\b", display_name, re.IGNORECASE):
561
- return False
562
- stack = [(props.get("policyRule") or {}).get("if")]
563
- while stack:
564
- node = stack.pop()
565
- if isinstance(node, dict):
566
- field = node.get("field")
567
- if isinstance(field, str) and (field.lower() == "location" or field.lower().endswith("/location")):
568
- return True
569
- stack.extend(node.values())
570
- elif isinstance(node, list):
571
- stack.extend(node)
572
- return False
573
-
574
-
575
- def _location_values(parameters: dict[str, Any], required_value: Any = None) -> list[str]:
576
- locations: set[str] = set()
577
- if isinstance(required_value, list):
578
- locations.update(str(value) for value in required_value if value)
579
- elif isinstance(required_value, str) and required_value:
580
- locations.add(required_value)
581
- for name, value in parameters.items():
582
- if name.lower() in {"listofallowedlocations", "allowedlocations"} and isinstance(value, list):
583
- locations.update(str(item) for item in value if item)
584
- return sorted(locations)
585
-
586
-
587
- def _extract_allowed_locations(
588
- findings: list[dict[str, Any]], location_constraints: list[dict[str, Any]] | None = None
589
- ) -> list[str]:
590
- """Extract allowed-location values from findings with location constraints.
591
-
592
- Checks both required_value (from definition defaults) and assignment_parameters
593
- (which carry the actual configured location list).
594
- """
595
- locations: set[str] = set()
596
- for f in findings:
597
- dn = (f.get("display_name") or "").lower()
598
- cat = (f.get("category") or "").lower()
599
- is_location = "location" in dn or "region" in dn or "location" in cat
600
-
601
- # Check required_value from definition
602
- rv = f.get("required_value")
603
- if is_location:
604
- if isinstance(rv, list):
605
- locations.update(str(v) for v in rv)
606
- elif isinstance(rv, str) and rv:
607
- locations.add(rv)
608
-
609
- # Check assignment parameters for location lists
610
- params = f.get("assignment_parameters") or {}
611
- for pname in (
612
- "listOfAllowedLocations", "allowedLocations",
613
- "listofallowedlocations", "allowedlocations",
614
- ):
615
- val = params.get(pname)
616
- if isinstance(val, list):
617
- locations.update(str(v) for v in val if v)
618
- for constraint in location_constraints or []:
619
- locations.update(str(value) for value in constraint.get("allowed_locations") or [] if value)
620
- return sorted(locations)
621
-
622
-
623
- # --------------------------------------------------------------------------- #
624
- # L0 envelope helpers #
625
- # #
626
- # The canonical implementations live in render_governance.py so the cached #
627
- # baseline path (render_cached_governance.py) and the live discovery path #
628
- # (this module) share one signature algorithm. Re-export under the legacy #
629
- # private names so existing call sites and tests keep working unchanged. #
630
- # --------------------------------------------------------------------------- #
631
-
632
- from render_governance import _build_discovery_metadata # noqa: E402,F401 — shared L0 helper
633
- from render_governance import _completeness_signature # noqa: E402,F401 — shared L0 helper
634
- from render_governance import _extract_management_groups # noqa: E402,F401 — shared L0 helper
635
-
636
-
637
- def _self_check_assignments(
638
- az_rest: Callable[[str], dict[str, Any]],
639
- subscription_id: str,
640
- expected_count: int,
641
- ) -> tuple[bool, int]:
642
- """Re-fetch the first page of policyAssignments and verify count.
643
-
644
- Returns (ok, actual_count). On any network/parse failure, returns
645
- (False, -1) so the caller can downgrade `discovery_status` to
646
- `PARTIAL` without crashing.
647
- """
648
- try:
649
- url = (
650
- f"{ARM}/subscriptions/{subscription_id}/providers/Microsoft.Authorization/"
651
- f"policyAssignments?$filter=atScope()&api-version={API_ASSIGNMENTS}"
652
- )
653
- # We deliberately re-use _default_az_rest here via the caller's
654
- # az_rest, which is the same surface that just produced the original
655
- # list — any drift now is real (filter change, RBAC drop, paging bug).
656
- first = az_rest(url) or {}
657
- items = first.get("value") or []
658
- return len(items) == expected_count, len(items)
659
- except Exception: # noqa: BLE001 — self-check must never crash discovery
660
- return False, -1
661
-
662
-
663
- # --------------------------------------------------------------------------- #
664
- # Core #
665
- # --------------------------------------------------------------------------- #
666
-
667
-
668
- def _classify(effect: str) -> str:
669
- if effect in BLOCKER_EFFECTS:
670
- return "blocker"
671
- if effect in AUTO_REMEDIATE_EFFECTS:
672
- return "auto-remediate"
673
- return "informational"
674
-
675
-
676
- def discover(
677
- subscription_id: str,
678
- *,
679
- project: str,
680
- include_defender_auto: bool = False,
681
- az_rest: Callable[[str], dict[str, Any]] | None = None,
682
- verbose: bool = False,
683
- ) -> dict[str, Any]:
684
- """Run discovery and return the full envelope (not written to disk).
685
-
686
- ``verbose`` gates noisy informational stderr (per-assignment Defender
687
- filter announcements). The count is always recorded in
688
- ``discovery_summary.defender_auto_filtered`` regardless.
689
- """
690
- if az_rest is None:
691
- # Resolve at call time so tests can monkeypatch `_default_az_rest`.
692
- az_rest = _default_az_rest
693
- base = f"{ARM}/subscriptions/{subscription_id}/providers/Microsoft.Authorization"
694
-
695
- # Phase 1 — Parallel list calls for subscription-scope data + exemptions.
696
- # The tenant-wide built-in definition list (~1500 items) is deliberately
697
- # NOT fetched here; we fetch only the tenant definitions an assignment
698
- # actually references (Phase 2). Saves a large, mostly-unused payload.
699
- primary = _parallel_list(
700
- az_rest,
701
- {
702
- "assignments": f"{base}/policyAssignments?$filter=atScope()&api-version={API_ASSIGNMENTS}",
703
- "sub_defs": f"{base}/policyDefinitions?api-version={API_DEFINITIONS}",
704
- "sub_sets": f"{base}/policySetDefinitions?api-version={API_DEFINITIONS}",
705
- "exemptions": f"{base}/policyExemptions?$filter=atScope()&api-version={API_EXEMPTIONS}",
706
- },
707
- )
708
- assignments = primary["assignments"]
709
- defs: dict[str, dict[str, Any]] = {d["id"].lower(): d for d in primary["sub_defs"]}
710
- sets: dict[str, dict[str, Any]] = {s["id"].lower(): s for s in primary["sub_sets"]}
711
- exemptions = primary["exemptions"]
712
-
713
- # Phase 2 — Fetch only the tenant-scoped initiatives and definitions that
714
- # assignments actually reference. Initiatives come first because their
715
- # members may themselves be tenant-scope definitions. We preserve the
716
- # original-case ID for URL construction (ARM is case-insensitive but
717
- # downstream consumers and test routers match by substring).
718
- tenant_set_ids: dict[str, str] = {} # lowercase → original-case
719
- tenant_def_ids: dict[str, str] = {}
720
- for a in assignments:
721
- pid_original = (a.get("properties") or {}).get("policyDefinitionId") or ""
722
- pid = pid_original.lower()
723
- if not pid or not pid.startswith("/providers/microsoft.authorization/"):
724
- continue
725
- if "/policysetdefinitions/" in pid and pid not in sets:
726
- tenant_set_ids.setdefault(pid, pid_original)
727
- elif "/policydefinitions/" in pid and pid not in defs:
728
- tenant_def_ids.setdefault(pid, pid_original)
729
-
730
- if tenant_set_ids:
731
- ordered = sorted(tenant_set_ids.items())
732
- fetched_sets = _parallel_fetch_items(
733
- az_rest,
734
- [f"{ARM}{orig}?api-version={API_DEFINITIONS}" for _, orig in ordered],
735
- expected_ids=[orig for _, orig in ordered],
736
- )
737
- for s in fetched_sets:
738
- if s:
739
- sets.setdefault((s.get("id") or "").lower(), s)
740
- for m in (s.get("properties") or {}).get("policyDefinitions") or []:
741
- mid_original = (m.get("policyDefinitionId") or "")
742
- mid = mid_original.lower()
743
- if (
744
- mid
745
- and mid.startswith("/providers/microsoft.authorization/")
746
- and mid not in defs
747
- ):
748
- tenant_def_ids.setdefault(mid, mid_original)
749
-
750
- if tenant_def_ids:
751
- ordered = sorted(tenant_def_ids.items())
752
- fetched_defs = _parallel_fetch_items(
753
- az_rest,
754
- [f"{ARM}{orig}?api-version={API_DEFINITIONS}" for _, orig in ordered],
755
- expected_ids=[orig for _, orig in ordered],
756
- )
757
- for d in fetched_defs:
758
- if d:
759
- defs.setdefault((d.get("id") or "").lower(), d)
760
-
761
- exemption_map = _build_exemption_map(exemptions)
762
-
763
- # Assignment filtering (Defender auto-assignments).
764
- filtered_defender: list[str] = []
765
- kept_assignments: list[dict[str, Any]] = []
766
- for a in assignments:
767
- if _is_defender_auto(a) and not include_defender_auto:
768
- filtered_defender.append(
769
- (a.get("properties") or {}).get("displayName") or a.get("name") or a.get("id") or "<unknown>"
770
- )
771
- continue
772
- kept_assignments.append(a)
773
-
774
- for name in filtered_defender:
775
- # Per-assignment filter announcements are noise by default — the
776
- # aggregate count lives in discovery_summary.defender_auto_filtered.
777
- # Surface the names only when --verbose is passed (Phase 6 fix).
778
- if verbose:
779
- print(f"filter: skipping Defender auto-assignment: {name}", file=sys.stderr)
780
-
781
- assignment_inventory: list[dict[str, Any]] = []
782
- findings: list[dict[str, Any]] = []
783
- location_constraints: list[dict[str, Any]] = []
784
- audit_count = 0
785
- disabled_count = 0
786
-
787
- for a in kept_assignments:
788
- props = a.get("properties") or {}
789
- display = props.get("displayName") or a.get("name") or a.get("id") or "<unknown>"
790
- scope = props.get("scope") or ""
791
- pid = (props.get("policyDefinitionId") or "").lower()
792
- assignment_id = (a.get("id") or "").lower()
793
- assignment_type = (
794
- "management-group" if "/providers/microsoft.management/managementgroups/" in scope.lower() else "subscription"
795
- )
796
- assignment_params = {
797
- key: (value or {}).get("value")
798
- for key, value in (props.get("parameters") or {}).items()
799
- if (value or {}).get("value") is not None
800
- }
801
- assignment_inventory.append(
802
- {
803
- "displayName": display,
804
- "scope": scope,
805
- "assignmentType": assignment_type,
806
- "policyDefinitionId": pid,
807
- "enforcementMode": props.get("enforcementMode") or "Default",
808
- "parameters": assignment_params,
809
- }
810
- )
811
-
812
- if not pid:
813
- continue
814
-
815
- # Resolve to member definitions (initiative → its members; policy → itself).
816
- members: list[tuple[dict[str, Any], str | None, dict[str, Any]]] = []
817
- if "/policysetdefinitions/" in pid and pid in sets:
818
- initiative_defaults = {
819
- key: (value or {}).get("defaultValue")
820
- for key, value in ((sets[pid].get("properties") or {}).get("parameters") or {}).items()
821
- if (value or {}).get("defaultValue") is not None
822
- }
823
- initiative_params = {**initiative_defaults, **assignment_params}
824
- for m in (sets[pid].get("properties") or {}).get("policyDefinitions") or []:
825
- mid = (m.get("policyDefinitionId") or "").lower()
826
- if mid in defs:
827
- member_params = {
828
- key: _resolve_parameter_value((value or {}).get("value"), initiative_params)
829
- for key, value in (m.get("parameters") or {}).items()
830
- if _resolve_parameter_value((value or {}).get("value"), initiative_params) is not None
831
- }
832
- members.append((defs[mid], m.get("policyDefinitionReferenceId"), member_params))
833
- elif pid in defs:
834
- members.append((defs[pid], None, {}))
835
- # Unknown definition id — nothing to emit; still counted in inventory.
836
-
837
- for defn, member_ref_id, member_params in members:
838
- effective_params = {**assignment_params, **member_params}
839
- eff = _effect_of(defn, effective_params)
840
- if eff is None:
841
- continue
842
- if eff == "Disabled":
843
- disabled_count += 1
844
- continue
845
- if _is_location_constraint(defn):
846
- location_constraints.append(
847
- {
848
- "policy_id": defn.get("id"),
849
- "display_name": (defn.get("properties") or {}).get("displayName")
850
- or defn.get("name")
851
- or defn.get("id"),
852
- "assignment_display_name": display,
853
- "assignment_id": a.get("id"),
854
- "scope": scope,
855
- "effect": eff[:1].lower() + eff[1:],
856
- "enforcement_mode": props.get("enforcementMode") or "Default",
857
- "allowed_locations": _location_values(effective_params, _required_value(defn)),
858
- }
859
- )
860
- if eff in {"Audit", "AuditIfNotExists"}:
861
- audit_count += 1
862
- continue
863
- if eff not in RELEVANT_EFFECTS:
864
- continue
865
-
866
- rtypes = _resource_types(defn)
867
- paths = _property_paths(defn, rtypes)
868
- category = ((defn.get("properties") or {}).get("metadata") or {}).get(
869
- "category"
870
- ) or "Uncategorized"
871
-
872
- exemption = None
873
- if assignment_id in exemption_map:
874
- candidate = exemption_map[assignment_id]
875
- ref_ids = candidate.get("policyDefinitionReferenceIds") or []
876
- if not ref_ids or member_ref_id in ref_ids:
877
- exemption = candidate
878
-
879
- classification = _classify(eff)
880
- if exemption is not None and classification == "blocker":
881
- classification = "informational"
882
-
883
- finding = {
884
- "policy_id": defn.get("id"),
885
- "display_name": (defn.get("properties") or {}).get("displayName")
886
- or defn.get("name")
887
- or defn.get("id"),
888
- "effect": eff[:1].lower() + eff[1:], # lower-camel for schema enum
889
- "scope": scope,
890
- "assignment_display_name": display,
891
- "assignment_id": a.get("id"),
892
- "classification": classification,
893
- "category": category,
894
- "resource_types": rtypes,
895
- "required_value": _required_value(defn),
896
- "azurePropertyPath": paths.get("azurePropertyPath"),
897
- "bicepPropertyPath": paths.get("bicepPropertyPath"),
898
- "exemption": exemption,
899
- "override": None,
900
- }
901
- # Carry assignment-level parameter values (tag keys, location lists).
902
- if effective_params:
903
- finding["assignment_parameters"] = effective_params
904
- if paths.get("pathSemantics"):
905
- finding["pathSemantics"] = paths["pathSemantics"]
906
- # For Tags-category policies, extract enforced tag keys from the
907
- # policyRule body. Deny-style tag policies often hard-code keys
908
- # in `policyRule.if.allOf[*].anyOf[*].field` rather than exposing
909
- # them as `tagName*` assignment parameters; without this we get
910
- # silent drift to sibling Modify-policy keys.
911
- if (
912
- finding.get("pathSemantics") == "tag-policy-non-property"
913
- or (finding.get("category") or "").lower() == "tags"
914
- ):
915
- rule_tag_keys = _extract_policy_rule_tag_keys(defn)
916
- if rule_tag_keys:
917
- finding["extracted_tag_keys"] = rule_tag_keys
918
- findings.append(finding)
919
-
920
- blockers = sum(1 for f in findings if f["classification"] == "blocker")
921
- auto_remediate = sum(1 for f in findings if f["classification"] == "auto-remediate")
922
- exempted = sum(1 for f in findings if f["exemption"] is not None)
923
-
924
- # L0 envelope: signature, scope, and self-check.
925
- management_groups = _extract_management_groups(kept_assignments)
926
- discovery_status = "COMPLETE"
927
- # Re-fetch page 1 of assignments and verify the count matches the
928
- # initial pull. Drift here indicates RBAC change, filter mutation, or
929
- # a paginated REST surface change between the two calls.
930
- self_check_ok, self_check_count = _self_check_assignments(az_rest, subscription_id, len(assignments))
931
- if not self_check_ok:
932
- discovery_status = "PARTIAL"
933
- print(
934
- f"self-check: policyAssignments count drift "
935
- f"(expected={len(assignments)}, observed={self_check_count}); "
936
- f"marking discovery_status=PARTIAL",
937
- file=sys.stderr,
938
- )
939
-
940
- discovered_at = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
941
- discovery_metadata = _build_discovery_metadata(
942
- findings=findings,
943
- subscription_id=subscription_id,
944
- management_groups=management_groups,
945
- page_counts={
946
- # `_parallel_list` collapses pagination, so we record the
947
- # final item counts per REST surface. The self-check above
948
- # validates these against a fresh page-1 fetch.
949
- "policyAssignments": len(assignments),
950
- "policyDefinitions": len(defs),
951
- "policyExemptions": len(exemptions),
952
- },
953
- discovered_at=discovered_at,
954
- discovery_status=discovery_status,
955
- source="live",
956
- )
957
-
958
- envelope = {
959
- "schema_version": "governance-constraints-v1",
960
- "project": project,
961
- "subscription_id": subscription_id,
962
- "discovered_at": discovered_at,
963
- "source": "azure-policy-rest-api",
964
- "discovery_status": discovery_status,
965
- "discovery_metadata": discovery_metadata,
966
- "discovery_summary": {
967
- "assignment_total": len(assignments),
968
- "assignment_kept": len(kept_assignments),
969
- "defender_auto_filtered": len(filtered_defender),
970
- "subscription_scope_count": sum(
971
- 1 for a in kept_assignments if "/providers/microsoft.management/" not in (a.get("properties", {}).get("scope") or "").lower()
972
- ),
973
- "management_group_inherited_count": sum(
974
- 1 for a in kept_assignments if "/providers/microsoft.management/" in (a.get("properties", {}).get("scope") or "").lower()
975
- ),
976
- "blocker_count": blockers,
977
- "auto_remediate_count": auto_remediate,
978
- "informational_count": sum(
979
- 1 for f in findings if f["classification"] == "informational"
980
- ),
981
- "audit_count": audit_count,
982
- "disabled_count": disabled_count,
983
- "exempted_count": exempted,
984
- },
985
- "assignment_inventory": assignment_inventory,
986
- "findings": findings,
987
- # Fix E: canonical aliases so the agent never needs to reshape JSON.
988
- # `policies` is a reference alias of `findings` (not a copy).
989
- "policies": findings,
990
- # Authoritative reference set for the L3 policy-precheck subagent.
991
- # Contains every policy definition ID we resolved during this run
992
- # (subscription-scope + tenant-scope, both initiative members and
993
- # standalone assignments), regardless of effect. The L3 subagent
994
- # uses this to suppress false-positive "live policies missing from
995
- # constraints" noise caused by initiative child policies that the
996
- # findings[] filter (Deny + DeployIfNotExists + Modify only) drops.
997
- # Values are lowercase resource IDs to match `az policy state list`
998
- # output. See iac-common/references/policy-precheck-contract.md
999
- # Phase 3 step 1 for the comparison contract.
1000
- "member_policy_index": sorted(defs.keys()),
1001
- "tags_required": _extract_tags_required(findings),
1002
- "location_constraints": location_constraints,
1003
- "allowed_locations": _extract_allowed_locations(findings, location_constraints),
1004
- }
1005
- return envelope
1006
-
1007
-
1008
- # --------------------------------------------------------------------------- #
1009
- # CLI #
1010
- # --------------------------------------------------------------------------- #
1011
-
1012
-
1013
- def _emit_status(status: dict[str, Any]) -> None:
1014
- """Emit single-line JSON status as the first stdout line."""
1015
- sys.stdout.write(json.dumps(status, separators=(",", ":")) + "\n")
1016
- sys.stdout.flush()
1017
-
1018
-
1019
- def _emit_preview(envelope: dict[str, Any], limit: int = 20) -> None:
1020
- """Human-readable Markdown preview. NEVER re-ingest into LLM context."""
1021
- summary = envelope["discovery_summary"]
1022
- print(
1023
- f"\n{envelope['subscription_id']}: {summary['assignment_kept']} kept "
1024
- f"assignments (of {summary['assignment_total']}; "
1025
- f"{summary['defender_auto_filtered']} Defender auto filtered); "
1026
- f"{summary['blocker_count']} blockers, "
1027
- f"{summary['auto_remediate_count']} auto-remediate, "
1028
- f"{summary['exempted_count']} exempted."
1029
- )
1030
- print()
1031
- print("| Effect | Classification | Category | Policy | Assignment |")
1032
- print("|---|---|---|---|---|")
1033
- for f in envelope["findings"][:limit]:
1034
- print(
1035
- f"| {f['effect']} | {f['classification']} | {f['category']} | "
1036
- f"{f['display_name']} | {f['assignment_display_name']} |"
1037
- )
1038
- if len(envelope["findings"]) > limit:
1039
- print(f"\n… {len(envelope['findings']) - limit} more in {envelope.get('_out_path')}")
1040
-
1041
-
1042
- # Renderer functions extracted to render_governance.py — import and re-export
1043
- # as module-level aliases so existing tests (test_discover.py) that call
1044
- # discover._extract_arch_resources(...) continue to pass unchanged.
1045
- from render_governance import extract_arch_resources as _extract_arch_resources # noqa: F401
1046
- from render_governance import emit_preview_md as _emit_preview_md # noqa: F401
1047
-
1048
-
1049
- def main(argv: list[str] | None = None) -> int:
1050
- parser = argparse.ArgumentParser(
1051
- prog="discover",
1052
- description="Deterministic Azure Policy discovery for 04g-Governance.",
1053
- )
1054
- parser.add_argument("--project", required=True)
1055
- parser.add_argument("--out", required=True)
1056
- parser.add_argument("--subscription", default="default")
1057
- parser.add_argument("--refresh", action="store_true")
1058
- parser.add_argument(
1059
- "--arch",
1060
- default=None,
1061
- help="Path to 02-architecture-assessment.md. Enables policy→resource mapping in preview.md.",
1062
- )
1063
- parser.add_argument(
1064
- "--include-defender-auto",
1065
- action="store_true",
1066
- help="Retain Defender-for-Cloud auto-assignments (filtered by default).",
1067
- )
1068
- parser.add_argument(
1069
- "--verbose",
1070
- action="store_true",
1071
- help="Emit informational stderr (per-assignment Defender filter announcements). "
1072
- "Aggregate counts are always recorded in discovery_summary regardless.",
1073
- )
1074
- args = parser.parse_args(argv)
1075
-
1076
- out_path = Path(args.out)
1077
- existing: dict[str, Any] | None = None
1078
- if out_path.exists():
1079
- try:
1080
- candidate = json.loads(out_path.read_text())
1081
- existing = candidate if isinstance(candidate, dict) else None
1082
- except json.JSONDecodeError:
1083
- existing = None
1084
- preserved_security_exceptions = (
1085
- existing.get("security_exceptions")
1086
- if isinstance(existing, dict) and isinstance(existing.get("security_exceptions"), list)
1087
- else []
1088
- )
1089
-
1090
- # Cache short-circuit — reuse an existing COMPLETE snapshot unless --refresh.
1091
- if out_path.exists() and not args.refresh:
1092
- cached = existing
1093
- if (
1094
- isinstance(cached, dict)
1095
- and cached.get("discovery_status") == "COMPLETE"
1096
- and isinstance(cached.get("findings"), list)
1097
- ):
1098
- summary = cached.get("discovery_summary") or {}
1099
- status = {
1100
- "status": "COMPLETE",
1101
- "cache_hit": True,
1102
- "assignment_total": summary.get("assignment_total", 0),
1103
- "blockers": summary.get("blocker_count", 0),
1104
- "auto_remediate": summary.get("auto_remediate_count", 0),
1105
- "exempted": summary.get("exempted_count", 0),
1106
- "out_path": str(out_path),
1107
- }
1108
- _emit_status(status)
1109
- print(
1110
- f"cache hit: reusing {out_path} ({len(cached.get('findings', []))} findings; "
1111
- f"pass --refresh to re-discover)"
1112
- )
1113
- return 0
1114
-
1115
- # Resolve subscription id.
1116
- try:
1117
- sub_id = _default_get_subscription() if args.subscription == "default" else args.subscription
1118
- except subprocess.CalledProcessError as e:
1119
- _emit_status({"status": "FAILED", "error": "subscription-resolution", "detail": str(e)})
1120
- print("ERROR: could not resolve subscription via `az account show`.", file=sys.stderr)
1121
- return 2
1122
-
1123
- try:
1124
- _default_check_auth()
1125
- except subprocess.CalledProcessError as e:
1126
- _emit_status(
1127
- {
1128
- "status": "FAILED",
1129
- "error": "auth",
1130
- "detail": (e.output or "").strip() if hasattr(e, "output") else str(e),
1131
- }
1132
- )
1133
- print(
1134
- "ERROR: Azure ARM token unavailable. Run `az login --use-device-code`.",
1135
- file=sys.stderr,
1136
- )
1137
- return 2
1138
-
1139
- try:
1140
- envelope = discover(
1141
- sub_id,
1142
- project=args.project,
1143
- include_defender_auto=args.include_defender_auto,
1144
- verbose=args.verbose,
1145
- )
1146
- except subprocess.CalledProcessError as e:
1147
- _emit_status(
1148
- {
1149
- "status": "FAILED",
1150
- "error": "rest",
1151
- "detail": str(e),
1152
- }
1153
- )
1154
- print(f"ERROR: Azure REST call failed: {e}", file=sys.stderr)
1155
- return 2
1156
- except Exception as e: # pragma: no cover — defensive
1157
- _emit_status({"status": "FAILED", "error": "unexpected", "detail": str(e)})
1158
- print(f"ERROR: unexpected failure: {e}", file=sys.stderr)
1159
- return 2
1160
-
1161
- out_path.parent.mkdir(parents=True, exist_ok=True)
1162
- if preserved_security_exceptions:
1163
- envelope["security_exceptions"] = preserved_security_exceptions
1164
- envelope["_out_path"] = str(out_path)
1165
- out_path.write_text(json.dumps(envelope, indent=2, sort_keys=False) + "\n")
1166
-
1167
- # Fix C: emit sibling preview.md for agent annotation
1168
- arch_resources = _extract_arch_resources(args.arch) if args.arch else None
1169
- preview_path = _emit_preview_md(envelope, out_path, arch_resources=arch_resources)
1170
- if preview_path:
1171
- print(f"preview: wrote {preview_path}", file=sys.stderr)
1172
-
1173
- summary = envelope["discovery_summary"]
1174
- status = {
1175
- "status": envelope["discovery_status"],
1176
- "cache_hit": False,
1177
- "assignment_total": summary["assignment_total"],
1178
- "blockers": summary["blocker_count"],
1179
- "auto_remediate": summary["auto_remediate_count"],
1180
- "exempted": summary["exempted_count"],
1181
- "out_path": str(out_path),
1182
- }
1183
- _emit_status(status)
1184
- _emit_preview(envelope)
1185
- return 0 if envelope["discovery_status"] == "COMPLETE" else 1
1186
-
1187
-
1188
- if __name__ == "__main__":
1189
- sys.exit(main())