Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .github/agents/azure-policy-advisor.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,24 @@ Always use the `/azure-policy-advisor` skill for procedure, classification tiers
- A general subscription audit
- Compliance with a specific framework (CIS, NIST, etc.)
2. Read compliance preferences from `copilot-instructions.md` (the `## Compliance & Azure Policy` section).
3. If an ARM template is provided, parse resource types. Otherwise, ask what resource types to assess.
4. Execute the `/azure-policy-advisor` skill procedure:
3. **Load landing zone context** if `.azure/landing-zone-context.json` exists (produced by `/azure-landing-zone-discovery`). Use it to:
- **Dedupe** recommendations against `policies.denyEffects[]`, `policies.auditEffects[]`, and `policies.alzCanonicalAssignments[]` — do not recommend policies the tenant is already enforcing
- **Align region recommendations** with `policies.allowedLocations[]` instead of guessing
- **Align tag recommendations** with `policies.requiredTags[]`
- **Note inherited posture** in Part 2 (subscription-level actions) — say "✓ inherited from management group" for canonical ALZ assignments rather than re-recommending them
- Respect `landingZoneDetection.confidence` — when `low`/`none`, treat the policy lists as informational only (the tenant may not actually be ALZ-managed)
4. If an ARM template is provided, parse resource types. Otherwise, ask what resource types to assess.
5. Execute the `/azure-policy-advisor` skill procedure:
- **Step 2:** Query existing policy assignments in the Azure subscription (via `az policy assignment list`)
- **Step 3:** Discover unassigned custom/built-in policy definitions (via `az policy definition list`)
- **Step 4:** Query Microsoft Learn for current built-in policy definitions per resource type
- **Step 5:** Classify and prioritize — cross-reference template config, existing assignments, and custom definitions
- **Step 5:** Classify and prioritize — cross-reference template config, existing assignments, custom definitions, **and the LZ context's tenant policy state**
- **Step 6:** Generate split report:
- **Part 1: Template Improvements** — gaps fixable by modifying the ARM template (developer action)
- **Part 2: Subscription-Level Actions** — policy/initiative assignments (platform team action)
- **Part 2: Subscription-Level Actions** — policy/initiative assignments (platform team action), with canonical ALZ assignments already enforced marked as "✓ already inherited"
- **Step 7:** Provide implementation options for both tracks
5. Present the policy assessment report with the split Part 1 / Part 2 format.
6. Save `policy-assessment.md` and `policy-recommendations.json` to the deployment directory if one exists.
6. Present the policy assessment report with the split Part 1 / Part 2 format.
7. Save `policy-assessment.md` and `policy-recommendations.json` to the deployment directory if one exists.

## Output Requirements

Expand Down
136 changes: 136 additions & 0 deletions .github/agents/azure-requirements-gatherer.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,119 @@ User intent: Deploy Azure Function App
- Ensure globally unique names for resources that require it
- Follow organizational naming conventions

### 0.7. Detect Landing Zone Context

Check whether a landing zone context has been discovered for this workspace. The file is produced by the **azure-landing-zone-discovery** skill and lets this agent route workloads to the right subscription, warn on policy conflicts, and surface shared services.

**Step 1 — read the context:**

```bash
LZ_CONTEXT_FILE=".azure/landing-zone-context.json"

if [[ -f "$LZ_CONTEXT_FILE" ]]; then
DISCOVERED_AT=$(jq -r '.discoveredAt' "$LZ_CONTEXT_FILE")
# Stale-check (warn if > 7 days old)
AGE_DAYS=$(( ( $(date -u +%s) - $(date -u -d "$DISCOVERED_AT" +%s 2>/dev/null || date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$DISCOVERED_AT" +%s) ) / 86400 ))
[[ $AGE_DAYS -gt 7 ]] && echo "⚠️ Landing zone context is $AGE_DAYS days old — consider refreshing"

# Detection confidence (see azure-landing-zone-discovery skill, "Landing Zone Detection Confidence")
LZ_CONFIDENCE=$(jq -r '.landingZoneDetection.confidence // "unknown"' "$LZ_CONTEXT_FILE")
LZ_SCORE=$(jq -r '.landingZoneDetection.confidenceScore // 0' "$LZ_CONTEXT_FILE")
LZ_IS_LZ=$(jq -r '.landingZoneDetection.isLandingZone // false' "$LZ_CONTEXT_FILE")
else
echo "ℹ️ No landing zone context found at $LZ_CONTEXT_FILE"
echo " Run /azure-landing-zone-discovery to enable landing-zone-aware deployments,"
echo " or proceed with subscription-only context."
# Continue without LZ context — do not block the user
fi
```

**How to treat `landingZoneDetection.confidence`:**

| Confidence | Treatment |
|---|---|
| `high` (score ≥ 70) | Trust auto-classified subscription roles, hub-spoke topology, and shared services without prompting. |
| `medium` (40–69) | Surface matched + missing signals to the user (`.landingZoneDetection.matchedSignals[]`, `.landingZoneDetection.missingSignals[]`) and confirm before assuming ALZ-managed behavior. |
| `low` (10–39) | Note partial ALZ signals but default to standalone-tenant treatment. Do not auto-attach to hub or shared services. |
| `none` (< 10) | Treat as a flat/standalone tenant. If the user *knows* the tenant is ALZ-managed, suggest manual injection via the `inject-lz.sh` script. |


**Step 2 — classify the current subscription:**

If the context exists, look up the active subscription in `subscriptions.platform[]` and `subscriptions.landingZones[]`:

```bash
CURRENT_SUB_ID=$(az account show --query id -o tsv)
SUB_ROLE=$(jq -r --arg id "$CURRENT_SUB_ID" '
(.subscriptions.platform[] | select(.id == $id) | .role) //
(.subscriptions.landingZones[] | select(.id == $id) | .role) //
"unclassified"
' "$LZ_CONTEXT_FILE")
```

**Warn if the user is targeting a platform subscription:**

| Detected `role` | Action |
|------------------|--------|
| `connectivity`, `identity`, `management` | ⚠️ **Block by default.** Display "This is a platform subscription. Workloads should land in an application landing zone." Offer to switch. |
| `landing-zone`, `landing-zone-dev`, `landing-zone-staging`, `landing-zone-prod` | ✅ Proceed. |
| `unclassified` (sub not in context) | Note that the subscription isn't part of the discovered topology. Ask user to confirm intent. |

**Step 3 — surface the policy gates that may block this deployment:**

```bash
DENY_COUNT=$(jq '.policies.denyEffects | length' "$LZ_CONTEXT_FILE")
ALLOWED_LOCATIONS=$(jq -r '.policies.allowedLocations | join(", ")' "$LZ_CONTEXT_FILE")
REQUIRED_TAGS=$(jq -r '.policies.requiredTags | join(", ")' "$LZ_CONTEXT_FILE")

if [[ "$DENY_COUNT" -gt 0 ]]; then
echo "🛑 $DENY_COUNT Deny-effect policies apply to this scope:"
jq -r '.policies.denyEffects[] | " • \(.name) — \(.impact)"' "$LZ_CONTEXT_FILE"
fi
[[ -n "$ALLOWED_LOCATIONS" && "$ALLOWED_LOCATIONS" != "" ]] && echo "📍 Allowed locations: $ALLOWED_LOCATIONS"
[[ -n "$REQUIRED_TAGS" && "$REQUIRED_TAGS" != "" ]] && echo "🏷️ Required tags: $REQUIRED_TAGS"
```

Treat `denyEffects` as gating: a user-requested region outside `allowedLocations`, a missing required tag, or a configuration that matches a deny impact MUST be raised before template generation.

**Do NOT** surface entries from `auditEffects` as blockers — those are informational only.

**Step 4 — note available shared services:**

If `sharedServices.logAnalytics.id` / `containerRegistry.id` / `keyVault.id` are present, record them so Stage 2 (template generation) wires diagnostics, container images, and secrets to the shared platform resources instead of creating new ones.

If `networking.topology == "hub-spoke"` and `networking.hubs[]` is non-empty, record the hub VNet ID(s) for VNet peering in Stage 2.

Skip this step gracefully when:
- `discoveryMethod == "manual"` and fields are absent (user has not provided them)
- `topology == "flat"` (no hub-spoke to integrate with)
- `topology == "unknown"` (treat conservatively — do not assume any shared infra)

**Display the landing zone summary to the user:**

```markdown
## Landing Zone Context

| Property | Value |
|----------|-------|
| **Discovered** | {discoveredAt} ({age} ago) |
| **Method** | {auto / manual} |
| **Detection** | {confidence} ({confidenceScore}/100) — isLandingZone: {true/false} |
| **Current subscription role** | {connectivity / landing-zone-prod / unclassified ...} |
| **Network topology** | {hub-spoke / flat / unknown} |
| **Deny policies** | {N} ({list if N ≤ 5}) |
| **Allowed locations** | {list or "any"} |
| **Required tags** | {list or "none"} |
| **Shared Log Analytics** | {name / "none"} |
| **Shared ACR** | {name / "none"} |
| **Hub VNet** | {name / "none"} |

{If confidence is "medium" or "low":} Detected ALZ signals: {matchedSignals[].signal}. Missing: {missingSignals[]}.
{If platform subscription:} ⚠️ Target subscription is a platform subscription — workloads should typically deploy elsewhere.
```

**Pass landing zone context to downstream stages** by including a `landingZone` block in the requirements output (see Section 4).

### 1. Identify Resource Type(s)

**Support Multi-Resource Deployments** - Ask if user wants single or multiple resources:
Expand Down Expand Up @@ -386,6 +499,29 @@ Resource 3 (App Insights) → Resource 2 (Function App)
"displayName": "{tenantDisplayName}",
"domain": "{tenantDomain}"
},
"landingZone": {
"contextFile": ".azure/landing-zone-context.json",
"discoveredAt": "{ISO 8601 or null if no context}",
"discoveryMethod": "auto|manual|none",
"detection": {
"isLandingZone": false,
"confidence": "high|medium|low|none",
"confidenceScore": 0
},
"currentSubscriptionRole": "landing-zone|connectivity|identity|management|unclassified",
"topology": "hub-spoke|flat|unknown",
"policyGates": {
"denyEffectCount": 0,
"allowedLocations": [],
"requiredTags": []
},
"sharedServices": {
"logAnalyticsId": "{id or null}",
"containerRegistryId": "{id or null}",
"keyVaultId": "{id or null}",
"hubVnetIds": []
}
},
"resources": [
{
"type": "Microsoft.Web/sites",
Expand Down
55 changes: 55 additions & 0 deletions .github/agents/azure-template-generator.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,61 @@ For **ALL resources**:

All other per-resource hardening (TLS versions, blob soft delete, threat detection, health probes, auto-scaling, etc.) is owned by the security analyzer in Step 3 and the policy advisor in Step 4 — they will flag anything missing with severity tags, and Critical / High findings are auto-applied or BLOCK the security gate.

### 2.5. Apply Landing Zone Context (When Available)

Before invoking the security/policy/preflight skills, check whether the workspace has discovered landing zone context at `.azure/landing-zone-context.json` (produced by `/azure-landing-zone-discovery`). When present, the template MUST respect the discovered tenant configuration.

**Load the context once:**

```bash
LZ_CONTEXT_FILE=".azure/landing-zone-context.json"
if [[ -f "$LZ_CONTEXT_FILE" ]]; then
LZ_CONFIDENCE=$(jq -r '.landingZoneDetection.confidence // "unknown"' "$LZ_CONTEXT_FILE")
LZ_ALLOWED_LOCATIONS=$(jq -r '.policies.allowedLocations[]? // empty' "$LZ_CONTEXT_FILE")
LZ_REQUIRED_TAGS=$(jq -r '.policies.requiredTags[]? // empty' "$LZ_CONTEXT_FILE")
LZ_LAW_ID=$(jq -r '.sharedServices.logAnalytics.id // empty' "$LZ_CONTEXT_FILE")
LZ_ACR_ID=$(jq -r '.sharedServices.containerRegistry.id // empty' "$LZ_CONTEXT_FILE")
LZ_HUB_VNET_ID=$(jq -r '.networking.hubs[0].id // empty' "$LZ_CONTEXT_FILE")
LZ_TOPOLOGY=$(jq -r '.networking.topology // "unknown"' "$LZ_CONTEXT_FILE")
fi
```

**How to act on each field (gated by `landingZoneDetection.confidence`):**

| Field | Action when `confidence` ≥ `medium` |
|-------|--------------------------------------|
| `policies.allowedLocations[]` | **Reject** the template if the target region is not in the list. Surface the allowed list to the user and ask them to pick one. |
| `policies.requiredTags[]` | Inject each required tag as a parameter in the template; if the user didn't provide a value, ask before generating. Apply to all resources via `tags` block. |
| `policies.denyEffects[]` | Cross-check template properties against the deny rules. If the template would be denied (e.g., public IP when `Deny-PublicIP` is enforced), flag it as a security-gate blocker before invoking the security analyzer. |
| `policies.alzCanonicalAssignments[]` | Document the matched canonical ALZ policies in the deployment plan so the user understands the tenant baseline. |
| `sharedServices.logAnalytics.id` | Wire `diagnosticSettings` for every resource that supports it to this workspace instead of creating a new one. |
| `sharedServices.containerRegistry.id` | If deploying Container Apps / AKS, reference this ACR (with pull RBAC on the workload identity). Skip creating a new ACR unless the user explicitly asks. |
| `networking.hubs[0].id` (when `topology` = `hub-spoke`) | Generate VNet peering from the workload spoke to the hub. Use the hub's resource group / subscription from the discovered ID. |
| `networking.privateDnsZones[]` | When generating private endpoints, link them to the discovered private DNS zones for end-to-end name resolution. |

**Confidence handling:**

- `high` (≥70) — Apply all the above automatically. Note each LZ-driven decision in the deployment plan.
- `medium` (40–69) — Surface the proposed LZ-driven choices to the user and ask to confirm before applying.
- `low` (10–39) / `none` (<10) — Use **only** the policy fields (`allowedLocations`, `requiredTags`, `denyEffects`) when they are explicitly populated. Do **not** auto-wire shared services or hub peering — the topology may be misclassified.
- Context missing — Skip this entire step; proceed with the user-provided values.

**Surface in the deployment plan:**

Add a "Landing Zone Compliance" subsection between "Security Configuration" and "Security Best Practices Analysis":

```markdown
### Landing Zone Compliance

- **Confidence:** {high|medium|low|none} ({score}/100)
- **Target subscription role:** {landing-zone|sandbox|standalone}
- **Region check:** ✓ {region} is in `allowedLocations`
- **Required tags applied:** Environment, Project, CostCenter
- **Diagnostics:** routed to `{logAnalyticsId}` (shared)
- **Hub peering:** {generated to hub-vnet-id | skipped — topology=flat}
- **Policy gate check:** ✓ no template properties conflict with tenant `denyEffects`
```

### 3. Analyze Security Best Practices (Per Resource)

**Invoke skill:** `/azure-security-analyzer`
Expand Down
3 changes: 2 additions & 1 deletion .github/agents/git-ape-onboarding.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ Treat this as a **non-negotiable contract** for the gated first reply: regardles
Both scripts produce byte-identical output. Report which files were created vs skipped.
9. Ask compliance framework and enforcement mode preferences (Step 10 in `/git-ape-onboarding` skill playbook).
10. Update the `## Compliance & Azure Policy` section in `.github/copilot-instructions.md` with the user's choices. If the file was skipped by the scaffold step or lacks that section, surface the captured preferences in chat for manual integration instead of mutating the file.
11. Summarize created/updated artifacts and next checks.
11. **Run landing zone discovery** against each onboarded subscription using `/azure-landing-zone-discovery`. This populates `.azure/landing-zone-context.json` with the tenant's management group hierarchy, platform subscriptions, hub-spoke networking, and policy gates so the requirements gatherer, template generator, and policy advisor can be landing-zone-aware on the very first deployment. If discovery reports `confidence` = `low`/`none`, tell the user the workspace will deploy in standalone mode and document how to fall back to manual injection.
12. Summarize created/updated artifacts and next checks.

## Output Requirements

Expand Down
Loading
Loading