Skip to content

SnowOps — Completed Work History

Summary of every shipped version drop. Latest first. For asset status, see docs/context/04-asset-status.md. Asset codes (e.g. F1, E0) map to human-readable names in docs/context/00-asset-glossary.md.


v0.66 — D60 Next-5: Sentinel (J3) + SOAR Playbooks (K3) + Grafana Dashboards (J5) + Post-Incident Review (K4) + CAB Automation (R4) (2026-06-09)

The D60 next-5 — the SIEM + observability + IR/change-ticketing batch. The two still-queued D57 items (J3, K4) plus K3/J5/R4, confirmed with the user. Depth-before-breadth (D36) + "finish near-complete series / unblock scaffolds / extend E7" lens; J3 is the keystone (onboarding Sentinel unblocks K3 + J5), and K4/R4 are the third/fourth E7 consumers after I5/R2. Three infra modules (reusing the J4/S3 module mold) + two TS apps (reusing the I5/R2 mold).

  • J3modules/azure/sentinel/: onboards Microsoft Sentinel onto the CONSUMED J1 workspace (by ARM ID) + toggleable first-party data connectors (Entra ID / Defender for Cloud / Threat Intelligence / Office 365 / MDCA, all default off) + curated Sentinel-native scheduled analytics rules → incidents with entity mappings + MITRE tactics (identity + privilege domains; per-domain toggles + per-rule overrides + custom_rules collision-guarded escape hatch) + an opt-in Microsoft incident-creation rule for Defender alerts. azurerm 4.x quirks handled: the incident { create_incident_enabled, grouping {...} } block (renamed from incident_configuration) and product_filter = "Azure Security Center". Scope boundary vs J4: J4 = Monitor alerts → action groups (paging); J3 = Sentinel rules → incidents (SOC + K3). Offline TestSentinelValidate; registered in modules/registry.json. (D61)
  • K3modules/azure/sentinel-soar/: the three active-IR containment playbooks (isolate-account / disable-SP / quarantine-VM) as Logic Apps reusing S3's structural dry_run/enforce split (Compose intent-record vs. real Http action via ManagedServiceIdentity, rendered at plan time) with a per-playbook Graph/ARM audience. Ships the playbooks (incident→playbook automation rule is the out-of-band wiring step, same stance as S3). Retires the apps/soar-playbooks/ scaffold (README now redirects — closes the G20 misleading-scaffold half). Offline TestSentinelSoarValidate. (D62)
  • J5modules/azure/grafana-dashboards/: Azure Managed Grafana + a Monitoring Reader grant to the instance identity + Grafana Admin/Editor/Viewer Azure-role fan-out + version-controlled dashboards-as-code under dashboards/ (Security Posture; Reliability & Cost) with an always-on offline gate (scripts/validate_dashboards.py + validate.sh) and an idempotent az grafana dashboard create deploy script — the F8 "infra + declarative artifacts + validate.sh + tooling deploy" shape (avoids the second-provider/two-stage-apply weight of the grafana provider). Offline TestGrafanaDashboardsValidate + the dashboards JSON gate; registered in modules/registry.json. (D63)
  • K4apps/post-incident-review/ (@snowops/post-incident-review): closed incidents → blameless PIR docs + idempotent tracking issues via the E7 CLI run-time bridge (I5/R2 mold). Defensive normalize across the az sentinel incident list + flat shapes; computes time-to-detect / MTTR / duration from real timestamps; supplied sections render verbatim, missing ones become honest "to be completed" prompts (never fabricates a root cause). 23 jest tests. (D64)
  • R4apps/cab-automation/ (@snowops/cab-automation): high-impact changes → CAB review tickets via the E7 CLI bridge. Configurable policy (CAB labels / emergency labels → retroactive framing / high-risk path prefixes); impact = high for label/emergency, medium for path-only; reasons are always the matched labels/paths, never invented. SOC2 CC8.1 / ISO A.12.1.2 evidence; --fail-on-cab for branch-protection gate use. 24 jest tests. (D64)

Verification (copy-paste):

# Terraform modules (offline) — J3 + K3 + J5
terraform fmt -recursive -check modules/azure/sentinel modules/azure/sentinel-soar modules/azure/grafana-dashboards
for m in sentinel sentinel-soar grafana-dashboards; do
  terraform -chdir=modules/azure/$m init -backend=false -input=false >/dev/null && terraform -chdir=modules/azure/$m validate
  terraform -chdir=modules/azure/$m/examples/basic init -backend=false -input=false >/dev/null && terraform -chdir=modules/azure/$m/examples/basic validate
done
# J5 dashboards-as-code offline gate
./modules/azure/grafana-dashboards/scripts/validate.sh
# Full Terratest suite (incl. TestSentinelValidate / TestSentinelSoarValidate / TestGrafanaDashboardsValidate)
( cd tests/terratest && go test -count=1 -timeout 20m ./... )
# Apps — K4 (23 tests) + R4 (24 tests)
( cd apps/post-incident-review && npm install && npm run typecheck && npm test )
( cd apps/cab-automation && npm install && npm run typecheck && npm test )
# Registry still valid JSON
python3 -c "import json;json.load(open('modules/registry.json'));print('registry OK')"
Expected: all validate Success!; dashboards gate "All 2 dashboard(s) valid"; Terratest ok; K4 23/23 + R4 24/24 pass; registry OK.

Next phase suggestion: runbook sign-offs in parallel (the J3/K3/J5/K4/R4 Part A+B are offline ~5 min each; Part C is the live apply). Then the remaining M4 advanced backlog: J7 (Basic-Logs cost strategy), H4/H6 (SCIM / AAD access reviews), I4/I6/I7 (DAST / Update Manager / CVE triage), M4/M5/M7 (Purview / DLP / GDPR evidence), N2/N7 (Firewall Premium / zero-trust ref arch), O/P/Q/T series, U4/U5 (FinOps dashboard / cost anomaly), V1/V4 (policy repo template / compliance manual), R3 (emergency-change workflow — pairs with R4), K5 (tabletop pack). W1–W3 (multi-tenant) remain last (D35).


v0.65 — Compliance Scorecard Generator (S4) (2026-06-07)

Third of the D57 next-5: finishes the S series (S1/S2/S3/S4 all code-complete) alongside S3, pairing S2's technical dashboard with a branded, sales/client-facing one-pager.

apps/compliance-scorecard-generator/ (@snowops/compliance-scorecard-generator) — pure adapt → render + jest, the E0/S1/S2/L4/L5 mold, with no live Azure phase (offline-only: reads E0/S2 JSON, writes markdown/HTML/PDF). Turns one E0 ComplianceSnapshot (+ optionally an S2 ComplianceDashboard for framework rollups and trend) into a versioned, branded one-page scorecard — scorecard.md + a self-contained scorecard.html (+ optional Pandoc scorecard.pdf) + model.json (schema v1.0).

S4 measures and re-derives nothing from Azure: every figure is a re-presentation of a number E0/S2 already computed, with one transparent extra — a documented A–F grading band (the only thing S4 "computes"): the simple mean of whichever of policy-compliance % and Secure Score % the snapshot carries, banded through a fixed five-way split (A=Strong ≥90%, B=Good ≥80%, C=Fair ≥70%, D=Weak ≥60%, F=At risk <60%, "—" = neither measured yet), with its derivation printed inline on every scorecard ("Overall 88.2% = mean of policy compliance 95.3% and Secure Score 81%, banded to grade 'B' (Good) per the documented grading scale.") — the same "never fabricate, show your work" stance as L5/S3. Framework badges and the trend arrow are even more conservative — strict 1:1 re-presentations of S2's already-computed pass/attention rollup status and the sign of its real latest-vs-previous deltas (any decline, or the dashboard's own regressed flag, conservatively reads as "declining"; the narrative quotes the real numbers verbatim, never a fabricated magnitude).

Branding (BrandingConfig: only clientName required) resolves against documented neutral defaults — an unbranded run still looks professional (preparedBy="SnowOps Cloud", title="Compliance & Security Posture Scorecard", accentColor="#1f6feb" matching S2's brand color, logoUrl → text-wordmark fallback). Both optional inputs (--dashboard, --branding) are genuinely optional — a snapshot alone produces a valid, honest headline-only scorecard that states plainly when the framework/trend sections weren't supplied rather than silently omitting them (the same "explicit about what's missing" stance L5 established for partial evidence). HTML render is self-contained (inline CSS only, no external script/stylesheet refs, escaped, branded with the client's accent color and a grade tile colored by band). PDF rendering (src/pdf.ts) is a near-verbatim copy of G3's Pandoc-spawn pattern — best-effort, a Pandoc failure is logged but never fails the run (independent build per D37/D40).

24 jest tests (2 suites): grading-band derivation (incl. the "—" not-yet-measured case and the never-fabricate guarantees), branding resolution + neutral defaults, framework-badge 1:1 re-presentation, trend-direction derivation purely from delta sign (incl. the conservative "any decline reads as declining" rule and the regressed-flag override), highlight generation, the always-present methodology section, markdown section ordering, HTML self-containment + escaping + branding application. Verified end-to-end: offline CLI run against the bundled fixtures produces the documented grade-B (88.2%) scorecard with the exact derivation trace, all four framework rows, the improving-trend callout with real numbers, and a self-contained branded HTML page. Runbook docs/runbooks/test/S4.md (offline-only Parts A+B, mirroring L5.md). Finishes the S series (S1/S2/S3/S4 all code-complete). Next: J3 (Sentinel deployment).


v0.64 — Auto-Remediation Playbooks (S3) (2026-06-07)

Second of the D57 next-5: closes the detect→remediate loop the S series opened with S1 (drift detection).

  • S3 — Auto-Remediation Playbooksmodules/azure/auto-remediation-playbooks/: curated pack of Logic App playbooks that turn a posture/compliance/drift finding into a remediation action. Each workflow exposes an HTTP trigger accepting a structured finding (findingKind/resourceId/proposedTags/detail) and runs a single action whose body Terraform renders entirely differently depending on the resolved safety mode at plan timedry_run gets a Compose action that records a structured "here's what I would have done" intent (no HTTP action exists anywhere in the definition, so the workflow is structurally incapable of writing to Azure no matter what RBAC its identity later receives); enforce gets a real Http action (PATCH/DELETE/PUT, per playbook) authenticated as the workflow's system-assigned managed identity. This extends the B3/D21 "audit-only by default, remediation role granted out-of-band" stance from passive compliance assignment to active remediation — flipping a playbook to enforce is deliberately two steps: (1) Terraform redeploys it with the write action, (2) you grant its identity the matching write RBAC role yourself; S3 never grants itself permissions. Curated pack: enforce-mandatory-tags (PATCH missing U2 mandatory tags), lock-down-public-storage (PATCH to disable public blob access + deny default network access), remove-risky-nsg-rule (DELETE an Internet/*-sourced rule exposing a management port), enable-resource-diagnostics (PUT a diagnostic setting wiring the resource to the consumed J1 workspace — the one playbook with a real dependency, auto-dropped when no workspace is wired). Per-playbook enabled/mode overrides resolve against the pack-wide mode default; a custom_playbooks escape hatch with a lifecycle.precondition collision check mirrors J4. Consumes the Log Analytics Module's (J1) workspace by ARM ID for run-history diagnostic settings — "define the link, not the instance" (L2/J4/X8 stance). Distinct from K3 (Sentinel SOAR incident-response: account isolation, SP disabling, VM quarantine — still postponed): S3 fixes drift findings, K3 contains active incidents. Offline terraform validate Terratest gate TestAutoRemediationPlaybooksValidate.

Verify locally:

terraform -chdir=modules/azure/auto-remediation-playbooks init -backend=false -input=false
terraform -chdir=modules/azure/auto-remediation-playbooks validate
cd tests/terratest && go test -v -timeout 5m ./modules/azure/... -run TestAutoRemediationPlaybooksValidate

Next phase: S4 (compliance scorecard generator) — next in the D57 batch; then J3, K4. Runbook sign-offs continue in parallel; W1–W3 remain last (D35).


v0.63 — RTO/RPO Doc Generator (L5) (2026-06-07)

First of the D57 next-5 (post-D51): completes the DR series (L1/L2/L3/L4/L5 all code-complete).

  • L5 — RTO/RPO Doc Generatorapps/rto-rpo-doc-generator/: standalone TS app (pure adapt → render + jest, the E0/S1/S2/L4 mold) that merges the Azure Backup Policy Module's (L1) retention_summary, the Cross-Region Replication Module's (L2) replication_summary, and the Automated Restore Drill's (L4) versioned RestoreDrillReports into one client-facing, evidence-backed RTO/RPO objectives markdown document. Every figure traces to real config or a real measurement: backup-restored workloads get an RPO bound from the shortest enabled L1 cadence (daily<weekly<monthly<yearly); continuously-replicating SQL-failover-group workloads get the mechanism plus the configured grace window — L5 deliberately refuses to fabricate a minute figure for async replication. Documents only workloads with L4 drill evidence (one row per drill, latest run wins) — "a backup you have never restored is a hope, not a backup" — with a per-workload table, an inline RPO-derivation trace, and a gap-analysis section naming every at-risk/failing workload with a concrete next step. All three evidence inputs are optional; missing posture is stated plainly, not silently omitted. 15 jest tests.

Verify locally:

cd apps/rto-rpo-doc-generator && npm ci && npm run typecheck && npm run build && npm test
node dist/index.js --l1 examples/l1-outputs.json --l2 examples/l2-outputs.json \
  --drill-reports examples/drill-reports --output /tmp/l5out && cat /tmp/l5out/rto-rpo.md

Next phase: S3 (auto-remediation playbooks) — next in the D57 batch; then S4, J3, K4. Runbook sign-offs continue in parallel; W1–W3 remain last (D35).


v0.62 — Client Offboarding Playbook (W4) + DR Runbook Templates (L3) (2026-06-06)

Two doc/runbook assets that finish the M3 tail and complete the DR series. Built as the net-new part of the D55 batch (N3/N4/E2 already landed independently in v0.60/v0.61); reconciled onto v0.61 and added here.

  • W4 — Client Offboarding Playbookdocs/runbooks/offboarding/ (client-offboarding.md + README): ownership-aware ([SO]/[CO]/[CA]/[SH]) phased hand-back/tear-down playbook, the inverse of the GitHub Onboarder App (B1) + Azure Client Bootstrap Module (B2) onboarding path; closes the G10 offboarding gap. Phases: freeze & snapshot evidence (E0/S2/L1/L4/G6) → knowledge transfer (V2/V3/F11) → identity teardown (B2/H7/B5/H3/H2) → repo/CI hand-back (B1/S1/H5) → state & secrets (F6/B4/F5) → cost & monitoring (U1/K2/J4/J1/J6) → decommission (terraform destroy/X7) → final attestation. Sign-off + client counter-sign blocks.
  • L3 — DR Runbook Templatesdocs/runbooks/dr/: README + master dr-plan-template.md + four scenario runbooks (region-failover, data-restore, sql-failover, full-region-loss) with RTO/RPO tables, real az commands, validation checklists, fail-back, and sign-off blocks. Completes the DR series with the Azure Backup Policy Module (L1), Cross-Region Replication Module (L2), and Automated Restore Drill (L4).

Verify locally:

ls docs/runbooks/offboarding/ docs/runbooks/dr/
# both are documentation assets — no build/test gate; manual runbook sign-off is the verification (DoD #3/#4)

Next phase: runbook sign-offs in parallel (E2–E6 + the new W4/L3); remaining M4 advanced (J3 Sentinel, S3/S4, L5, O/P/Q, T). W1–W3 remain last (D35).


v0.61 — E Series: Vanta Adapter (E2) + Drata Adapter Stub (E3) + Resource Graph Library (E4) + Evidence Sync (E5) + Access Review Automation (E6) (2026-06-06)

E2 ships as apps/evidence-collector/src/platforms/vanta.tsVantaApi implements EvidenceUpsertApi over the Vanta REST API. Pushes test results keyed by framework:controlId; vantaPlatform() factory wired into selectEvidencePlatform("vanta"). Auth via VANTA_API_KEY + VANTA_ACCOUNT_ID. Injectable FetchLike for tests.

E3 ships as apps/evidence-collector/src/platforms/drata.tsDrataApi implements EvidenceUpsertApi stub over the Drata REST API. DRATA_CONTROL_MAP translates framework:controlId → Drata check IDs. API paths subject to validation against a live workspace; marked as stub.

E4 ships as apps/evidence-collector/src/resource-graph/ — 7 pre-built KQL queries covering SOC 2 CC + ISO 27001 A.x controls (storage CMK, public network access, TLS enforcement, owner role assignments, Defender plans, diagnostic settings, required tags). runAllQueries() REST client + mapQueryResultsToEvidence() mapper → EvidenceItem[].

E5 ships as apps/evidence-collector/src/sync.ts + .github/workflows/evidence-sync.ymlsyncEvidence() maps E0 snapshot data (policy compliance % → SOC2 CC7.1 + ISO A.8.15; Defender score → SOC2 CC7.1) + E4 resource-graph results to EvidenceItem[] and pushes via any E1 platform. Configurable pass thresholds (default: 90% policy, 70% Defender). Weekly workflow; dry-run by default.

E6 ships as apps/access-review/ + .github/workflows/access-review.yml@snowops/access-review CLI: collects Azure RBAC role assignments (AzureCliCollector live az rest or FixtureCollector offline), sorts high-privilege first, renders RFC 4180 CSV (reviewer-fillable action column) + Markdown summary, tickets via E7 snowops-ticket CLI. Artifact committed to compliance/access-reviews/<period>/. Quarterly cron.

Test counts (new): - apps/evidence-collector: 87 tests across 10 suites (up from 77/8 — adds E2 vanta.test.ts, E3 drata.test.ts, E4 queries.test.ts + mapper.test.ts, E5 sync.test.ts, factory.test.ts updated) - apps/access-review: 17 tests across 2 suites (collector.test.ts + render.test.ts)

Verify locally: - cd apps/evidence-collector && npm test87 tests, 10 suites, all green - cd apps/access-review && npm install && npm test17 tests, 2 suites, all green - npm run typecheck in both dirs → no errors

Next phase: E series runbook sign-offs (E2.md → E3.md → E4.md → E5.md → E6.md); then remaining runbook sign-offs backlog; M4 advanced (J3 Sentinel, O/P/Q endpoint/vendor/HR, T trust center). W-series last (D35). No open blocking decisions.


v0.60 — WAF Policy Module (N3) + DDoS Protection (N4) (2026-06-06)

N3 ships as modules/azure/waf-policy/ — a WAF policy for Application Gateway WAF v2 and Azure Front Door Premium. Default OWASP 3.2 in Prevention mode. Per-rule-group overrides (disable individual managed rules to reduce false positives), managed-rule exclusions, and map-keyed custom rules (IP block list, geo-block, string match, rate limiting via RateLimitRule). Association is the caller's responsibility — N3 exports policy_id for wiring. Offline TestWAFPolicyValidate Terratest gate.

N4 ships as modules/azure/ddos-protection/ — an Azure DDoS Network Protection plan. Creates the plan and exports plan_id; callers associate their VNets by adding ddos_protection_plan { id = module.ddos.plan_id; enable = true }. Cost warning: ~$2,944/month — budget approval required. Offline TestDDoSProtectionValidate Terratest gate.

Verify locally: - terraform -chdir=modules/azure/waf-policy init -backend=false && terraform -chdir=modules/azure/waf-policy validateSuccess! - terraform -chdir=modules/azure/ddos-protection init -backend=false && terraform -chdir=modules/azure/ddos-protection validateSuccess! - cd tests/terratest && go test -v -timeout 5m ./modules/azure/... -run TestWAFPolicyValidate → PASS - cd tests/terratest && go test -v -timeout 5m ./modules/azure/... -run TestDDoSProtectionValidate → PASS - cd tests/terratest && go test -count=1 -timeout 15m ./... → full offline suite green

Next phase: N3/N4 runbook sign-offs (N3.md, N4.md); then remaining runbook sign-offs backlog; M4 advanced; W-series last (D35). No open blocking decisions.


v0.59 — Production Change Log (R2) (D51 batch COMPLETE) (2026-06-05)

R2 ships as apps/change-log/ (@snowops/change-log) — the production change-management evidence asset: what merged to the default branch, categorized into a changelog, with an optional E7 change-record ticket per release. Completes the D51 M2b-additional batch: J4 (v0.56) + X5/X8 (v0.57) + I5 (v0.58) + R2 (v0.59).

Shape (E0/S1/I5 mold): pure core behind a collector seam. - categorize.ts (pure) — classifies each merged change by conventional-commit prefix (§3: feat/fix/sec/compliance/infra/test + extras), falling back to labels then other; strips the prefix + (#N). - render.ts (pure) — ordered sections → Keep-a-Changelog markdown; prependRelease inserts a new release above older ones while keeping the preamble. - collect.ts — the seam: GitLogCollector (default — squash commits on the branch), GitHubPrCollector (gh pr list), FixtureCollector; injectable runners. - ticket.ts — optional E7 change-record ticket via the same run-time snowops-ticket bridge I5 uses (dedupe key change-record=<release>).

Verify locally: - cd apps/change-log && npm install && npm run typecheck && npm test32 tests pass (4 suites). - npm run build && node dist/index.js --input examples/prs.sample.json --release v0.59 --out-dir ./out → 6 changes into Features/Security/Tests/Other. - node dist/index.js --range HEAD~6..HEAD --release v0.58 → real repo commits categorized; --prepend <file> keeps the preamble.

Next phase: D51 batch complete. Suggested next: runbook sign-offs in parallel (V2→V3→E0→… plus the new J4/X5/X8/I5/R2 runbooks); then M4 advanced compliance, or the deferred network items (N3 WAF, N4 DDoS). W-series remains last (D35). No open blocking decisions.


v0.58 — Defender Ticketer (I5) → ticket via Ticket Platform Library (E7) (first E7 consumer) (2026-06-05)

I5 ships as apps/defender-ticketer/ (@snowops/defender-ticketer) — the first real consumer of E7 (D49), turning Microsoft Defender for Cloud security alerts into idempotent tickets in the team's tracker. Closes the G8 loop end-to-end.

Shape (E0/S1/B6 mold): a pure core behind an I/O seam. - alerts.ts (pure) — normalizes raw alerts defensively across both the ARM-REST properties shape and the az security alert list flattened shape; filters by severity floor (default Medium) + status (default Active/InProgress); stable per-alert dedupe key defender-alert=<id>; severity summary. - collector.ts — the seam: FixtureCollector (offline --input) + AzureCliAlertCollector (az security alert list, injectable runner). - ticket.tsbuildAlertTicket (pure) + the E7 bridge. Per D37/D40/D49 (no cross-package build imports), I5 re-declares the tiny TicketPlatform contract and consumes E7 at run time via its snowops-ticket CLI (E7CliTicketPlatform, injectable runner; builds the baseArgs, parses E7's ticket_id/url/updated output). E7 owns marker + upsert; I5 supplies body + dedupe-key. - index.ts — CLI shell; --platform defaults to dry-run; writes a versioned DefenderTicketReport + per-alert issue-*.md.

Verify locally: - cd apps/defender-ticketer && npm install && npm run typecheck && npm test21 tests pass (3 suites). - npm run build && node dist/index.js --input examples/alerts.sample.json --out-dir ./outdry-run — 2 of 4 alert(s) would be filed (min Medium); --min-severity High → 1/4. - I5↔E7 contract confirmed: E7's CLI --output emits exactly ticket_id/ticket_url/ticket_updated (what I5 parses). Live filing to a real tracker is the I5 runbook Part C.

Next phase: R2 — production change log (merged PRs → changelog; reuses E7 for change-record tickets where required) — the last of the D51 batch. No open blocking decisions; W-series remains last (D35).


v0.57 — X series complete: Pipeline Integration Tests (X5) + Synthetic Monitoring (X8) (2026-06-05)

Completed the X series (D52) — the two remaining postponed X assets, ahead of the rest of the D51 batch.

X5 (tests/pipeline-integration/) — the pre-existing files were only C2/C3 runbook fixtures; the real deliverable was missing. Built both halves: - Offline contract gatecontract_check.py parses every reusable workflow's workflow_call inputs/secrets, finds every caller (.github/workflows/ + templates/client-repo/.github/workflows/), and asserts each caller honours the contract (required inputs supplied, no unknown inputs, required secrets supplied or secrets: inherit, no unknown secrets). Missing-coverage is a warning (image-scan/I1 is client-only). test_contract_check.py = 11 stdlib-unittest cases incl. a real-repo zero-violations assertion. validate.sh wrapper + CI .github/workflows/pipeline-integration.yml (runs on PRs touching workflows/callers). - Live consumersit-{container-build-sign,aks-deploy,terraform-plan-apply}.yml: dispatch-only callers of the local reusable workflows driving the existing fixtures against the X1 sandbox (C2 clean/vulnerable Dockerfiles, C3 nginx/ArgoCD app, C1 plan-only).

X8 (modules/azure/synthetic-monitoring/) — outside-in synthetic monitoring, the black-box complement to J4's log-signal detection. azurerm_application_insights_standard_web_test per web_tests entry + a per-test availability azurerm_monitor_metric_alert (web-test-location-availability criteria, scoped to both the test and the AI component), notifying action groups by ARM ID. Optional workspace-based App Insights component (consume-or-create). Per-test config (GET/POST + headers, expected status, content match, TLS cert lifetime check, geo-locations, frequency, failed-location-count, severity) with plan-time preconditions (AI component provided; failed_location_count ≤ geo_locations).

Verify locally: - ./tests/pipeline-integration/validate.sh → 11 unit tests pass + OK: every caller honours its reusable-workflow contract (one expected WARN for image-scan). - terraform -chdir=modules/azure/synthetic-monitoring init -backend=false && terraform -chdir=modules/azure/synthetic-monitoring validateSuccess! (example too). - cd tests/terratest && go test -count=1 ./... → full offline suite green (incl. TestSyntheticMonitoringValidate + TestAlertRulePackValidate).

Next phase: I5 — Defender for Cloud alert → ticket via the E7 TicketPlatform adapter (D51 #2). Then R2. No open blocking decisions; W-series remains last (D35).


v0.56 — Post-next-5 batch selected; Alert Rule Pack (J4) detection (M2b additional #1) (2026-06-05)

With the next-5 (D48) complete, the next 5 most-important postponed items were selected (D51) by depth-before-breadth (D36) + milestone order — finish the remaining M2b "additional" assets before M3 tail / M4: J4 (alert rule pack), I5 (Defender→ticket via E7), X5 (pipeline integration tests), X8 (synthetic monitoring), R2 (production change log). The heavier network items (N3 WAF, N4 DDoS) are deferred to a later batch.

Shipped this drop — J4 (modules/azure/alert-rule-pack/): the missing detection-rule layer. A curated pack of azurerm_monitor_scheduled_query_rules_alert_v2 rules (10) across four threat domains: - Identity — failed sign-in spike (password spray), risky sign-in (Entra ID Protection), successful legacy-auth sign-in (MFA/CA bypass). - Privilege escalation — RBAC role-assignment write, PIM tier-0 directory-role activation, Owner-role grant. - Network — NSG denied-flow spike from one source, Azure Firewall threat-intel / IDPS hit. - Data exfiltration — large blob download volume from one caller, bulk Key Vault secret reads.

Design (D51): each rule's KQL carries the substantive logic and returns one row per match → fires on row-count > 0 (the meaningful numbers live visibly inside each KQL where; rule_overrides.threshold raises the rows required). Per-domain enable toggles (drop a domain whose source table isn't flowing), per-rule overrides (severity/threshold/operator/frequency/window/enabled, null-inherits curated default), freeform custom_rules (a lifecycle precondition rejects keys colliding with curated keys). CONSUMES the J1 workspace + K2 action groups by ARM ID (does not create them — same stance as L2); the action block is omitted when no action groups supplied (dry-run rollout). README ships a data-source prerequisite matrix per domain.

Verify locally: - terraform -chdir=modules/azure/alert-rule-pack fmt -recursive -check → clean. - terraform -chdir=modules/azure/alert-rule-pack init -backend=false && terraform -chdir=modules/azure/alert-rule-pack validateSuccess! (example dir too). - cd tests/terratest && go test -count=1 ./modules/azure/... -run TestAlertRulePackValidate → PASS (all four domains + override + disabled rule + custom rule, offline). - No build-tagged integration test — a real scheduled-query evaluation needs log data flowing; that's the J4 runbook (docs/runbooks/test/J4.md) Part C.

Next phase: I5 — Defender for Cloud alert → ticket via the E7 TicketPlatform adapter (next-5 #2, D51); the first consumer proving the E7 adapter end-to-end. Then X5 → X8 → R2. No open blocking decisions; W-series remains last (D35).


v0.55 — Terragrunt Live-Infra Reference (F7) (next-5 #5 — D48 batch COMPLETE) (2026-06-04)

F7 ships as live/ — a Terragrunt reference for wiring the F1–F6 module library into a multi-env, multi-region Azure estate, demonstrating the DRY pattern SnowOps recommends to clients. Completes the next-5 (D48): I1/I2/I3 (v0.53) + E7 (v0.54) + F7 (v0.55).

Structure: - root.hcl — every unit includes it: remote state in the F6 backend (AAD-only, state keyed by env-container + unit path), a generated azurerm provider (federated OIDC + AAD storage auth — no long-lived creds), and the canonical §3 tag set fed from env/region locals. - _envcommon/{baseline,network-hub,key-vault,acr}.hcl — one DRY template per module (F1/F2/F5/F4): module source + the inputs constant across the estate; names derived <client>-<env>-<region>-…. - bootstrap/eastus/state-backend/terragrunt.hcl — F6 on local state (deliberately does NOT include root.hcl), breaking the chicken-and-egg of storing remote state in itself; creates one storage account + a container per env. - Per-env (prod/staging/sandbox) env.hcl (subscription, tenant, retention, allowed regions, state container) + per-region region.hcl (location + non-overlapping CIDRs). prod is multi-region (eastus full chain + westus2). - Units are 3-line terragrunt.hcl files (include root + an _envcommon template + a dependency block). Real dependency DAG: baseline → network-hub / key-vault / acr, each consuming dependency.baseline.outputs.log_analytics_workspace_id (with mock_outputs so validate/plan run before baseline applies). - In-repo source via get_repo_root() so it validates inside the monorepo; the F11 registry-pin form (git::…//modules/azure/<m>?ref=<m>/vX.Y.Z) is documented for cutting live/ into a client repo.

Offline gate: live/validate.sh — Part A structural assertions (env.hcl/region.hcl presence, every unit includes root + _envcommon, bootstrap stays standalone, generated/state files gitignored), Part B terragrunt hcl validate + hclfmt --check when terragrunt is installed.

Verify locally: - live/validate.sh==> OK: all F7 structural assertions passed (terragrunt optional — Part A is the offline gate). - With terragrunt: cd live && terragrunt hcl validate && terragrunt hclfmt --check.

Next phase: Next-5 (D48) complete. Suggested next: continue runbook sign-offs in parallel, then pick from the M3 tail (W4 client offboarding) / M2b additional (J4 alert-rule pack, X5 pipeline integration tests, X8 synthetic monitoring) / M4 advanced compliance. No open blocking decisions; W-series remains last (D35).


v0.54 — Ticket Platform Library (E7) adapters (next-5 #4) (2026-06-04)

E7 ships as apps/ticket-platform/ (@snowops/ticket-platform) — the canonical, multi-platform home of the TicketPlatform abstraction S1 seeded inline (D15/D39). Closes gap G8 (ticketing was hardcoded per platform across E6/I5/K4/P3).

What it is: a platform-neutral TicketPlatform (TicketDraft → TicketRef) with one shared idempotent upsert (upsertByMarker — find the open ticket whose body carries an HTML-comment dedupe marker, update it if present else create) and four transport adapters behind a single MarkerUpsertApi seam (listOpen/create/update), each with an injectable fetch: - GitHub Issues (REST, label-scoped) — the shape S1 already used. - Jira (REST v2 so description stays a plain string and the marker round-trips; v3 ADF would not). - Linear (GraphQL, team-scoped; labels need UUIDs so labels isn't mapped). - Azure DevOps Boards (WIQL search + JSON-Patch work items; HTML description, HTML-comment marker).

Plus DryRunTicketPlatform, a selectPlatform(name, env, overrides) factory (env-driven creds, clear missing-credential errors), and the snowops-ticket CLI so any asset/workflow can file an idempotent ticket from a rendered body without embedding adapter code.

S1 repoint: per the SnowOps standalone-package convention (D37/D40) apps don't import each other at build time, so S1 keeps its interface-compatible inline copy; its ticket.ts header now points at E7 as the shipped canonical home and the call site (platform.upsert) is unchanged. New ticket-filing assets target E7 directly.

Verify locally: - cd apps/ticket-platform && npm install && npm run typecheck && npm test6 suites / 26 tests pass (shared upsert + each adapter's HTTP mapping + factory). - npm run build && node dist/index.js --platform dry-run --title t --body "x" --dedupe-key k → prints body, exit 0; --platform github --body x--title is required, exit 1.

Next phase: F7 — Terragrunt live-infra reference repo (per-env/region wiring of the F-module library), the last of the next-5 (D48).


v0.53 — Next-5 selection + I-series CI security suite (I1+I2+I3); C5 reconciled (2026-06-04)

With the 14-asset M2b core code-complete, the next 5 most-important postponed/unbuilt items were selected (D48): I3 (CodeQL SAST), I2 (dependency scanning), I1 (container image scanning), E7 (TicketPlatform adapters), F7 (Terragrunt live-infra reference). Reconciliation: C5 (ADO pipeline templates) was found already built in commit 51c7fc4 but left ⏸️ in the docs — now marked 🟦 code-complete and dropped from the candidate set.

Shipped this drop — the M2a CI security-scanning suite (I1+I2+I3), all pure CI/config, fully offline-lintable: - I3.github/workflows/codeql.yml: CodeQL SAST matrix over javascript-typescript (apps/) + go (tests/terratest), security-extended,security-and-quality queries, PR + push + weekly schedule, SARIF → Code Scanning. Closes the "no code-analysis layer" hole (D2 only covered IaC/secrets). - I2.github/workflows/dependency-review.yml (PR-blocking SCA gate, fail-on-severity: high + copyleft licence deny-list) + .github/workflows/dependency-digest.yml (weekly idempotent Dependabot-alert digest issue via gh api, S1 <!-- marker --> upsert pattern, no-ops when the alerts API is disabled). Completes I2 on top of the pre-existing .github/dependabot.yml. - I1.github/workflows/image-scan.yml: reusable workflow_call Trivy image scan; High/Critical gate, SARIF upload, optional registry login, severity_cutoff/ignore_unfixed/fail_on_findings/upload_sarif inputs. Closes G6 (container security for non-K8s clients); distinct from C2's build-time grype (scans an arbitrary image ref). - Runbooks docs/runbooks/test/I1.md, I2.md, I3.md (offline YAML-lint Part A + live drill Part B/C).

Verify locally: - for f in codeql dependency-review dependency-digest image-scan; do python3 -c "import yaml; yaml.safe_load(open('.github/workflows/$f.yml'))" && echo "OK $f"; done → 4× OK. - actionlint .github/workflows/{codeql,dependency-review,dependency-digest,image-scan}.yml (if installed) → clean.

Next phase: E7 — generalize the S1 TicketPlatform seed into a shared apps/ package with Jira / GitHub Issues / Linear / Azure DevOps Boards adapters; repoint S1 to it without changing its call site (D39/G8). Then F7 — Terragrunt live-infra reference repo (per-env/region module wiring).


v0.52 — Policy Waiver Engine (D5) merged (external) — 14-asset M2b core 100% in-repo (2026-05-31)

D5 (Policy Waiver Engine) landed via the external gemini-work PR #13, completing the last of the three externally-authored assets (K1 + K2 in PR #12, D5 in PR #13). The 14-asset M2b core (D36) is now fully in-repo — 14/14, nothing external remaining. This is a context-reconciliation drop (no SnowOps-authored code this version): claude.md + the context bank are updated to reflect D5 in-repo.

D5 extends the shipped D3/X3 OPA bundle (policy/opa/) with time-boxed waivers. The D3 rule files (tags/locations/network/encryption/cost) were refactored to emit raw_violation; policy/opa/rules/main.rego now filters them through has_active_waiver (suppress a violation whose message starts with a waiver's rule_prefix and names its resource_address, while the expiry_date is in the future) and hard-denies expired waivers (snowops.waiver_expired) so a lapsed waiver fails the pipeline rather than silently passing the underlying violation. Active waivers live in waivers/exceptions.yaml (rule_prefix, resource_address, expiry_date, owner, justification); waivers/README.md documents the format. Wired into .github/workflows/terraform-plan-apply.yml via conftest test plan.json --data waivers/exceptions.yaml. Runbook docs/runbooks/test/D5.md.

Also confirmed: F11's module-release workflow fired on the merge and published all 10 module tags (contracts/v0.1.0, baseline/v1.0.0audit-log-archive/v1.0.0) — live proof the private registry release path works.

Verify locally: - git tag --list "*/v*" → 10 published module tags present. - D5: see docs/runbooks/test/D5.md — craft a tag-violating plan.json, then conftest test plan.json --policy policy/opa/rules --data waivers/exceptions.yaml: an active waiver suppresses the matching deny; an expired one emits snowops.waiver_expired and fails.

Heads-up (not addressed here): a stray debug.rego was committed at the repo root in PR #13 (package main, references an undefined violation set, hardcodes "HAS WAIVER"). It is debug cruft that does not belong in the policy package — recommend removing it.

Next: Runbook sign-offs (offline Parts A+B first, ~5 min each — V2 → V3 → E0 → S1 → S2 → L1 → L2 → L4 → B6 → F11 → …). No open blocking decisions; W-series remains last (D35).


v0.51 — Module Registry & Versioning (F11) + Private Registry shipped (2026-05-31) — 14-asset M2b core CODE-COMPLETE

apps/module-registry/ + modules/registry.json + per-module CHANGELOG.md + .github/workflows/module-release.yml. The "private Terraform registry" is the monorepo itself: modules publish as git tags (<module>/v<version>) and consumers pin them in source — no hosted registry service. modules/registry.json is the source of truth (10 seeded modules: F0 contracts at 0.1.0; F1–F6 + J1/J2/J6 at 1.0.0), each with a Keep-a-Changelog CHANGELOG.md. Tool is on the B6/L4 mold: pure core over a RegistrySnapshot behind a Collector seam (FsRegistryCollector reads manifest + CHANGELOGs + git tag; FixtureCollector for tests). Four jobs — validate (unique names/paths, strict semver, CHANGELOG top version == manifest version, no version-regression below an existing tag), buildIndex (tag + full git:: source per module), planReleases (modules whose manifest version has no tag yet), auditPins (scans a consumer tree; flags unpinned/ref-mismatch/unknown-version). CLI writes registry-index.json + registry-report.json + summary.md; --fail-on-issues exits 2; --release-output feeds GITHUB_OUTPUT. The module-release workflow (push to main touching the manifest/CHANGELOGs) validates then creates each pending tag + a GitHub Release whose body is that CHANGELOG section (idempotent; skips already-tagged). 3 jest suites / 27 tests incl. a guard that loads the real committed manifest + every CHANGELOG and asserts internal validity + sync. Convention/pin-strategy doc docs/conventions/module-versioning.md; modules/azure/README.md gains a Versioning section; runbook docs/runbooks/test/F11.md. Decision D46. This completes the 14-asset M2b core (D36): 11/14 from this repo's drops; K1 + K2 landed externally via gemini-work PR #12 (IR runbook library + modules/azure/oncall-integration/), now merged — 13/14 in-repo, only D5 still external.

Verify locally: - cd apps/module-registry && npm ci && npm run typecheck && npm run build && npm test → 3 suites / 27 tests pass. - Validate live registry: node apps/module-registry/dist/index.js --manifest modules/registry.json --out-dir /tmp/f11 --fail-on-issues true; echo $?OK — 10 modules, 0 validation error(s), exit 0. - Pin audit: node apps/module-registry/dist/index.js --consumer-dir apps/module-registry/examples/consumer-pinned --fail-on-issues true; echo $?0; --consumer-dir apps/module-registry/examples/consumer-unpinned → exit 2 with [unpinned] + [unknown-version] findings. - ruby -ryaml -e "YAML.load_file('.github/workflows/module-release.yml')" → parses.

Next: 14-asset M2b core is code-complete. Focus shifts to runbook sign-offs (offline Parts A+B first, ~5 min each — V2 → V3 → E0 → S1 → S2 → L1 → L2 → L4 → B6 → F11 → …) and the externally-handled K1/K2/D5 landing in-repo. No open blocking decisions; W-series remains last (D35).


v0.50 — Client Prerequisite Checker (B6) shipped (2026-05-31)

apps/client-bootstrap/ — the prerequisite checker + Azure permission validator a prospective client runs in their OWN tenant before a SnowOps engagement. Standalone TS tool on the E0/S1/L4 mold: a pure evaluator over an EnvironmentSnapshot, all I/O behind a Collector seam (FixtureCollector for tests/offline, AzureCliCollector best-effort live az probes — the untested boundary). Read-only — never creates/changes anything in the tenant, no secrets. Declarative check catalogue (checks.ts): tooling (az ≥ 2.50, terraform ≥ 1.6 required; git recommended), auth (signed in), permissions (can assign RBAC — Owner/UAA, Contributor deliberately excluded; can create Entra apps+SPs; required resource providers Registered), licensing (Entra ID P2 for PIM — recommended/warn). Readiness rule: READY only when every REQUIRED check passes; recommended checks only warn. Versioned BootstrapReport schemaVersion 1.0 + summary.md (ends in a remediation list) + status.json (ready/blockers/warnings). Single client entrypoint bootstrap.sh (builds on first run, exits 0=READY / 2=NOT READY). The two acceptance scenarios are two fixtures: examples/snapshot.ok.json → READY (8/8, exit 0); examples/snapshot.restricted.json (Reader-only SP, no Entra role, providers unregistered) → NOT READY with three blockers each carrying concrete az remediation (exit 2). 3 jest suites / 25 tests. README + runbook docs/runbooks/test/B6.md. Decision D45.

Verify locally: - cd apps/client-bootstrap && npm ci && npm run typecheck && npm run build && npm test → 3 suites / 25 tests pass. - Pass scenario: node dist/index.js --snapshot examples/snapshot.ok.json --out-dir /tmp/b6ok --now 2026-06-01T00:00:00.000Z --fail-on-not-ready true; echo $?READY — 8/8, exit 0, status.json ready:true, empty blockers. - Fail scenario: node dist/index.js --snapshot examples/snapshot.restricted.json --out-dir /tmp/b6bad --fail-on-not-ready true; echo $? → exit 2, blockers:["perm-role-assignment","perm-entra-app-management","perm-providers"], each remediation echoed to stderr + in summary.md. - Wrapper: ./bootstrap.sh --snapshot examples/snapshot.ok.json --out-dir /tmp/b6sh; echo $? → 0; --snapshot examples/snapshot.restricted.json → 2.

Next: F11 (module versioning + private Terraform registry) — the last code item in the 14-asset M2b core. K1/K2/D5 handled externally, not yet in-repo. After F11 the core is code-complete and the focus shifts to runbook sign-offs (B6 runbook is offline, ~5 min).


v0.49 — Brownfield Import Library (F12) shipped (2026-05-31)

modules/azure/import-blocks/ — config-driven Terraform import {} blocks that adopt pre-existing Azure resources into the F-modules, one <module>.tf per module at the paths seven READMEs already promised. Covers 9 modules: F1 baseline, F2 network-hub, F3 aks-secure, F4 acr, F5 key-vault, F6 state-backend, J1 log-analytics, J2 policy-diagnostics, J6 audit-log-archive — every resource incl. count[0] and for_each["key"] instances, with the key schemes (derived from source) documented per file. Each file pairs its import blocks with a placeholder module call so the whole directory is ONE self-validating config: terraform validate confirms every to = address resolves. Offline gate TestImportBlocksValidate; tflint clean; full suite green. Adoption runbook docs/runbooks/import/F12.md; all 9 module READMEs' brownfield sections now point at their real import-blocks file. D5 (policy waiver engine) is handled externally by Sagar (like K1/K2) — the shipped D3/X3 OPA bundle is left untouched. Decision D44.

Verify locally: - terraform -chdir=modules/azure/import-blocks init -backend=false && terraform -chdir=modules/azure/import-blocks validateSuccess!. - cd tests/terratest && go test ./modules/azure/... -run TestImportBlocksValidate → PASS. - terraform -chdir=modules/azure/import-blocks fmt -check -recursive → clean; tflint (in dir) → exit 0; go test -count=1 ./... (in tests/terratest) → full suite green.

Next: B6 (client self-service bootstrap — single-script prerequisite checker + Azure permission validator: passes on a correctly-permissioned sub, fails with clear remediation on a restricted SP). Then F11 (module versioning + private registry). K1/K2/D5 handled externally, not yet in-repo.


v0.48 — Automated Restore Drill (L4) shipped (2026-05-31)

apps/restore-drill/ — the third DR leg (L1 backup policies + L2 replication links + L4 proof-of-recovery). Standalone TS tool on the E0/S1/S2 mold: pure offline logic + jest, all I/O behind a DrillExecutor seam (DryRunExecutor deterministic + AzureCliExecutor live az). A drill restores an L1 backup / fails over an L2 SQL failover group into an ephemeral sandbox RG → validates → tears down; runDrill enforces validate-skipped-on-restore-failure + teardown-always-runs. Versioned RestoreDrillReport schemaVersion 1.0; outcome passed/partial/failed (partial = RTO missed or teardown failed); measured RTO = restore+validate; diffReports is the recoverability-regression signal. Pass/fail reaches the S2 dashboard via the evidence store: reports land in compliance/restore-drills/ and S2 gains an OPTIONAL --restore-drills-dir additive "DR restore drills" panel + gated restoreDrills field (compliance-only HTML golden file unchanged). Scheduled via .github/workflows/restore-drill.yml (monthly cron; dispatch defaults to dry-run, schedule runs live + commits the dated report). L4: 3 jest suites / 17 tests; S2: now 4 suites / 34 tests (golden still green). READMEs + runbook docs/runbooks/test/L4.md. Decision D43.

Verify locally: - cd apps/restore-drill && npm ci && npm run typecheck && npm test → 3 suites / 17 tests pass. - Dry-run demo: node dist/index.js --spec examples/spec.vm.json --out-dir /tmp/l4pass --now 2026-06-01T00:00:00.000Zstatus.json outcome:passed, rtoMet:true. Failure gate: node dist/index.js --spec examples/spec.vm.json --script examples/script.fail.json --out-dir /tmp/l4fail --fail-on-drill-failure true; echo $? → exits 2, summary shows validate skipped + teardown passed. - cd apps/compliance-dashboard && npm ci && npm test → 4 suites / 34 tests pass (HTML golden unchanged). Combined render: node dist/index.js --snapshots-dir examples/snapshots --restore-drills-dir /tmp/l4pass --out-dir /tmp/s2drdashboard.md has a "DR restore drills" table; omitting --restore-drills-dir → no DR section. - ruby -ryaml -e "YAML.load_file('.github/workflows/restore-drill.yml')" → parses.

Next: D5 (policy waiver engine — time-boxed OPA exception records, PR-linked + expiry, CI fails expired waivers). Then F12 (brownfield import library), B6 (self-service bootstrap), F11 (module versioning). K1/K2 were handled externally and are not yet in this repo.


v0.47 — Cross-Region Replication Module (L2) shipped (2026-05-30)

modules/azure/cross-region-replication/ — cross-region replication wiring for the live data tier: blob object replication (azurerm_storage_object_replication, one rule per container_mappings pair; optionally creates the destination containers via azurerm_storage_container) + a geo-redundant SQL failover group (azurerm_mssql_failover_group, primary↔partner server, readonly_endpoint_failover_policy_enabled). Consumes existing storage accounts + SQL servers by ARM ID — wires replication, never creates the underlying resources (brownfield-safe; same stance as L1). Each workload independently toggleable (≥1 required). Per-env SQL failover posture (dev Manual / staging Automatic-60m / prod Automatic-120m), overridable via failover_policy/failover_grace_minutes; preconditions enforce cross-region locations differ, distinct accounts + servers, and Automatic⇒grace / Manual⇒no-grace coherence. replication_summary exported for evidence. Offline gate: TestCrossRegionReplicationValidate (init+validate of a fixture standing up two GZRS accounts + a source container + two AAD-only SQL servers + a DB, wiring both workloads at the prod profile). Module + example + fixture pass terraform fmt/validate + tflint + the full offline Terratest suite. README + runbook docs/runbooks/test/L2.md. Decision D42.

Verify locally: - terraform -chdir=modules/azure/cross-region-replication init -backend=false && terraform -chdir=modules/azure/cross-region-replication validateSuccess!. - cd tests/terratest && go test ./modules/azure/... -run TestCrossRegionReplicationValidate → PASS. - terraform -chdir=modules/azure/cross-region-replication fmt -recursive -check → clean; tflint (in module dir) → exit 0; go test -count=1 ./... (in tests/terratest) → full suite green.

Next: L4 (automated monthly restore drill → restore from L1 backup / fail over L2 replica into a sandbox RG → validate → destroy; pass/fail to the S2 dashboard). Then D5 (policy waiver engine). K1/K2 were handled externally and are not yet in this repo.


v0.46 — Azure Backup Policy Module (L1) shipped (2026-05-30)

modules/azure/backup-policy/ — reusable Azure Backup module spanning both vault families: a GeoRedundant azurerm_recovery_services_vault (VM / Azure Files / SQL-in-VM) + a azurerm_data_protection_backup_vault (AKS operational store), each gated on its workload toggles. Four per-env-retention policies — azurerm_backup_policy_vm, azurerm_backup_policy_file_share, azurerm_backup_policy_vm_workload (SQLDataBase/Full), azurerm_data_protection_backup_policy_kubernetes_cluster. Per-env profiles (dev/staging/prod) expand daily/weekly/monthly/yearly via dynamic blocks; preconditions enforce CRR⇒GeoRedundant + yearly⇒monthly⇒weekly. Defines reusable policies (not instance bindings); vault MIs + retention_summary exported. Offline gate: TestBackupPolicyValidate (init+validate of a fixture exercising all four policies at prod retention). Module + example + fixture pass terraform fmt/validate + tflint + go vet. README + runbook docs/runbooks/test/L1.md. Decision D41.

Verify locally: - terraform -chdir=modules/azure/backup-policy init -backend=false && terraform -chdir=modules/azure/backup-policy validateSuccess!. - cd tests/terratest && go test ./modules/azure/... -run TestBackupPolicyValidate → PASS. - terraform -chdir=modules/azure/backup-policy fmt -recursive -check → clean; tflint (in module dir) → exit 0.

Next: L2 (cross-region replication — GRS/GZRS storage, geo-redundant SQL; consumes L1's GeoRedundant vaults). Then L4 (automated monthly restore drill → pass/fail to the S2 dashboard). Note: K1/K2 were handled externally and are not yet in this repo.


v0.45 — Compliance Dashboard (S2) shipped (2026-05-30)

apps/compliance-dashboard/ — fully-offline TS tool that renders a history of E0 ComplianceSnapshots into a versioned ComplianceDashboard (schemaVersion 1.0) + a self-contained static dashboard.html (inline CSS + inline SVG sparkline, no JS/network) + dashboard.md. Pure assertSnapshots/buildDashboard/summarizeDashboard with a vendored diffSnapshots for the latest-vs-previous regression delta; trend series across all snapshots; best-effort name-based framework rollup (src/frameworks.ts: soc2/iso27001/cis_azure/hipaa; MCSB fans out per D21; unmatched → "Unmapped"). Re-declares E0's snapshot shape rather than importing it (D37 — no cross-package coupling). Reads the compliance/snapshots/ evidence store; never touches Azure. Scheduled via .github/workflows/compliance-dashboard.yml (E0 OIDC collect → S2 render → private artifact; Pages opt-in via publish_pages). 3 jest suites / 28 tests incl. golden-file HTML. Runbook: docs/runbooks/test/S2.md. Decision D40.

Verify locally: - cd apps/compliance-dashboard && npm ci && npm run typecheck && npm test → 3 suites / 28 tests pass. - Offline demo: node dist/index.js --snapshots-dir examples/snapshots --out-dir /tmp/dash --now 2026-05-30T00:00:00.000Zdashboard.{json,html,md}; dashboard.json has current.compliancePercentage:90, meta.snapshotCount:3, delta.regressed:true. - Gate: append --fail-on-regression true; echo $? → exits 2. Single snapshot (--input examples/snapshots/2026-05-29.json --fail-on-regression true) → exits 0, no ## Change since in its md. - ruby -ryaml -e "YAML.load_file('.github/workflows/compliance-dashboard.yml')" → parses.

Next: K1 (IR runbook library, docs/runbooks/incident/) + K2 (on-call integration: PagerDuty/Opsgenie + Slack). Then L1/L2/L4 (backup + DR + restore drill).


v0.44 — Drift Detector (S1) scheduled drift detection shipped (2026-05-30)

apps/drift-detector/ — read-only TS tool: terraform show -json → versioned DriftReport (schemaVersion 1.0). Pure normalizeAction/buildDriftReport/summarizeDrift/diffReports (data-source reads + no-ops excluded; create/update/delete/replace classified). diffReports is the change signal (mirrors E0's diffSnapshots). Ships the TicketPlatform interface (D15 — E7 seed) with a GitHubIssuesTicketPlatform (idempotent upsert keyed by an embedded <!-- snowops-drift:stack=… --> marker) + a DryRunTicketPlatform. Scheduled via .github/workflows/drift-detection.yml (daily cron 06:23 UTC, per-stack matrix, issues: write). Plans only — never applies. 3 jest suites / 18 tests. Runbook: docs/runbooks/test/S1.md. Decision D39.

Verify locally: - cd apps/drift-detector && npm ci && npm run typecheck && npm test → 3 suites / 18 tests pass. - Offline demo: node dist/index.js --stack sandbox --input examples/plan.clean.json --out-dir /tmp/dcdrift.json has drifted:false. Then node dist/index.js --stack payments-prod --input examples/plan.drifted.json --out-dir /tmp/dd --fail-on-drift true; echo $? → prints DRIFT — 3 resource(s), exits 2; /tmp/dd/summary.md lists update+create+replace (the data read excluded). - Diff: re-run the drifted fixture with --baseline <smaller prior drift.json>## Change since … shows newly-drifted resources. - ruby -ryaml -e "YAML.load_file('.github/workflows/drift-detection.yml')" → parses.

Next: S2 (Azure Policy compliance dashboard) — build on E0's snapshot/diff substrate. Then K1 (IR runbook library) + K2 (on-call integration).


v0.43 — Diagram Generator (V2) + Runbook Generator (V3) shipped (2026-05-30)

V2 apps/diagram-generator/ — zero-cloud TS tool: terraform output -json → F0 contracts → cloud-neutral StackModeld2lang architecture diagram. Shape-based contract detection (name-agnostic); deterministic renderer; golden-file tested. 2 jest suites / 9 tests. Runbook: docs/runbooks/test/V2.md. Decision D38.

V3 apps/runbook-generator/ — zero-cloud TS CLI: Terraform JSON outputs → Handlebars templates → client-facing operational markdown. 2 jest suites / 2 tests. Runbook: docs/runbooks/test/V3.md.

Verify locally: - V2: cd apps/diagram-generator && npm ci && npm test → 2 suites / 9 tests pass. node dist/index.js --input examples/stack.sample.tfoutputs.json --out-dir /tmp/v2 && diff /tmp/v2/architecture.d2 examples/golden/architecture.d2 → no diff. - V3: cd apps/runbook-generator && npm ci && npm test → 2 suites / 2 tests pass.

Next: S1 (drift detection) + S2 (compliance dashboard) — build on E0's snapshot/diff substrate.


v0.42 — Compliance Snapshot Collector (E0) shipped (2026-05-30)

apps/evidence-collector/ — read-only TS tool: Azure Policy compliance state + Defender secure score → versioned JSON snapshot (schemaVersion 1.0). Pure buildSnapshot/validateSnapshot/diffSnapshots/summarizeSnapshot. Wired into C1 as a continue-on-error post-apply step (emits compliance-snapshot artifact). 3 jest suites / 18 tests. Decision D37.

Verify locally: cd apps/evidence-collector && npm ci && npm test. Offline diff demo: node dist/index.js --subscription-id SAMPLE --input examples/collected.sample.json --out-dir /tmp/s1 then with --baseline /tmp/s1/snapshot.json and the regressed fixture → summary.md shows snowops-tls 0→3, ⚠️ Posture regressed.

Next: V2 + V3 (shipped in v0.43). Then S1/S2.


v0.41 — M2b/M3 re-prioritization (2026-05-30, planning only)

Sagar curated a 14-asset M2b/M3 core (E0, V2, V3, S1, S2, K1, K2, L1, L2, L4, D5, F12, B6, F11) and postponed all other M2b/M3 assets until it's code-complete + signed off (decision D36). Depth before breadth.


v0.40 — Sandbox Cleanup (X7) + W-series postponed = M2a code-complete (2026-05-29)

sandbox/cleanup/ + .github/workflows/sandbox-cleanup.yml — nightly cleanup of ephemeral=true RGs with 3 guards: (1) ephemeral=true tag only, (2) protected-name globs, (3) --min-age-hours guard. 12 offline assertions. Nightly cron 03:17 UTC; dispatch defaults to dry-run. Decision D35. W-series (W1–W3) POSTPONED to last.

Verify locally: bash sandbox/cleanup/test/run-tests.sh → 12/12 assertions. ruby -ryaml -e "YAML.load_file('.github/workflows/sandbox-cleanup.yml')" → parses.


v0.39 — Budget Alert Module (U1) + Tag Policy Module (U2) (2026-05-29)

U1 modules/azure/budget-alert/ — subscription budget + dynamic notifications (50/80/100 actual + 100 forecasted) + optional action group (H7-style pattern) + tag/RG filter. 4 preconditions. Build-tagged integration test (~$0).

U2 modules/azure/tag-policy/ — mandatory-tag Deny initiative (Require-a-tag built-ins, per-reference literal tagName). Default §8 set minus ManagedBy. enforce=falsetrue rollout (same as M6).

Full Terratest suite: 35 top-level tests pass.


v0.38 — Private Endpoint Policy Module (N5) + NSG Baseline Module (N6) (2026-05-29)

N5 modules/azure/private-endpoint-policy/ — Deny initiative: public network access on storage/KV/Cosmos/SQL. Reuses M1/M3 skeleton. Pairs with F4/F5 (build PEs) + F2 (Private DNS zones).

N6 modules/azure/nsg-baseline/ — hardened NSG + optional subnet associations + optional NSG flow logs + Traffic Analytics. Standalone counterpart to F2's bundled NSG. Build-tagged integration test (~$0). Decision D33.

Full Terratest suite: 33 top-level tests pass.


v0.37 — Encryption Policy Module (M1) + Customer-Managed Key Module (M2) + TLS Policy Module (M3) + Data Residency Policy Module (M6) (2026-05-29)

M1 — encryption-at-rest Deny initiative (storage/SQL/disk CMK). M3 — TLS/HTTPS Deny initiative (dual effect+minimumTlsVersion threading). M6 — Allowed-locations Deny initiative (GDPR/HIPAA residency). All validate-only in CI (live apply mutates sub policy state). Decision D32.

M2 modules/azure/cmk/ — HSM-backed KV key (RSA-HSM/EC-HSM only) + auto-rotation policy + Crypto role grants. Consumes existing F5 Premium vault. Build-tagged integration test (~$1). Versionless key ID for transparent rotation.

Full Terratest suite: 31 top-level tests pass.


v0.36 — Policy Diagnostics Module (J2) + Audit Log Archive Module (J6) (2026-05-29)

J2 modules/azure/policy-diagnostics/ — DINE initiative (not Deny); GUID-agnostic; emits az policy remediation create command. Validate-only in CI (live DINE needs real tenant GUIDs). Decision D31.

J6 modules/azure/audit-log-archive/ — WORM-immutable StorageV2 (RA-GZRS, account-level time-based immutability, allow_protected_append_writes = true). Forwards subscription Activity Log + optional Log Analytics export. Build-tagged integration test (~$0, immutability OFF for teardown). Decision D31.

Lock-file correction: .terraform.lock.hcl files ARE committed in this repo — restored all 49 existing + added 10 new (59 total).

Full Terratest suite: 27 top-level tests pass.


v0.35 — Log Analytics Module (J1) (2026-05-29)

J1 modules/azure/log-analytics/ — standalone hardened LAW: azurerm_log_analytics_workspace_table per-category retention (interactive + archive, validated total >= retention), CanNotDelete management lock (defaults ON, OFF in fixtures), scoped RBAC, self-audit azurerm_monitor_diagnostic_setting. Emits F0 observability_contract. Decision D30.

Full Terratest suite: 25 top-level tests pass.


v0.34 — GTM Track A COMPLETE: Y5–Y13 + Z0–Z3 (2026-05-29)

28 new files added to docs/gtm/ (39 total): Y5 discovery script, Y6 proposal/SOW, Y7 compliance coverage matrix, Y8 capabilities deck, Y9 synthetic case study, Y10–Y13 nurture/CS/contract/CRM, Z0 framework, Z1–Z3 vertical blueprints. All human-review gated (no cloud/runbook needed). Decision D29.


v0.33 — GTM Track A kickoff: Y0–Y4 (2026-05-29)

New top-level docs/gtm/ directory created (D28). Y0 operating doc, Y1 positioning/messaging, Y2 pricing (illustrative ⚠️), Y3 ICP/target-accounts + seed-list.template.csv, Y4 cold-outreach kit (5-touch sequence + LinkedIn variants). Decision D27.


v0.32 — GTM pivot re-plan (2026-05-29, no code)

Added §4.Y, §4.Z, §3.8 pricing framework. Re-sequenced §6.4 into Track A (GTM, Claude) + Track B (runbooks, Sagar). No technical asset deleted or de-scoped. Decision D27.


v0.31 — Service Principal Inventory (H5) + Break-Glass Account Module (H7) (2026-05-29)

H5 apps/sp-inventory/ — read-only Graph TS tool (19 jest tests): aged/expiring-soon/expired classification, federated-OIDC-only SPs never stale, PR-driven rotation (never auto-rotates). Reusable scheduled workflow (sp-inventory-rotation.yml). Decision D26.

H7 modules/azure/break-glass/ — dual-provider (azuread + azurerm): role-assignable group + permanent Global Admin + severity-0 sign-in alert (KQL on UserId, threshold=0). Producer of SNOWOPS_AAD_BREAKGLASS_GROUP_OBJECT_ID that H2/B3/B5 consume. Validate-only test (no integration). Decision D25.

Full Terratest suite: 23 top-level tests pass. M2a IAM track code-complete (H1–H3 + H5 + H7).


v0.30 — ArgoCD GitOps Bundle (F8) K8s bundle (2026-05-29)

gitops/ — ArgoCD app-of-apps: root Application → 6 child Applications in sync-wave order (cert-manager + Kyverno + ESO wave 0 → ingress-nginx wave 1 → D4 ClusterPolicies + ESO ClusterSecretStore wave 2). D4 policies REUSED not forked. ArgoCD internal+TLS + ingress-nginx internal Azure LB only. gitops/validate.sh (offline: 13 files / 7 Applications). bootstrap.sh replaces C3 runbook's ad-hoc Helm install. Decision D24.


v0.29 — Azure Resource PIM Module (B5) (2026-05-28)

modules/azure/pim-azure-resources/ — PIM for Azure resource roles (Owner/Contributor/UAA). Tier-0 = approval + 8h; tier-1 = no-approval + 4h. Uses azurerm's native azurerm_pim_eligible_role_assignment + azurerm_role_management_policy (unlike H3 which needs Graph PATCH). Validate-only test. Decision D23. B-series (B1–B5) code-complete.


v0.28 — Client State Backend Module (B4) (2026-05-28)

modules/azure/client-state-backend/ — wraps F6 + adds Blob Data RBAC (Contributor/Reader/Owner) + optional Private Endpoint + optional network-rule lockdown + optional diagnostics. backend_config forces use_azuread_auth = true. Build-tagged integration test (~$0). Decision D22.


v0.27 — Subscription Baseline Module (B3) (2026-05-28)

modules/azure/subscription-baseline/ — composes F1 + group RBAC (6 named shortcuts + freeform escape hatch) + MCSB regulatory assignment (system-assigned identity, audit-only). Defender ON by default. First SnowOps module that calls another SnowOps F-module (source = "../baseline"). Decision D20, D21.


v0.26 — Azure Client Bootstrap Module (B2) (2026-05-28)

modules/azure/client-bootstrap/ — AAD app + service principal + N federated credentials (GitHub OIDC only, repo: subject validation) + role bindings (subscription/ACR/KV/extra). No client secret ever. Composite for_each key (<scope>/<role>) is stable across flag flips. First dual-provider (azuread + azurerm) module. Decision D20.


v0.25 — AKS Deploy Pipeline (C3) (2026-05-28)

.github/workflows/aks-deploy.yml — ArgoCD CLI → image override (Kustomize XOR Helm, rejected if ambiguous) → sync → wait Healthy → smoke probe → optional rollback drill. ArgoCD-token-only auth (no AKS creds in workflow). Signed-digest-not-tag convention enforced. C2+C3 now chain in one caller workflow with image_reference flowing through.


v0.24 — Container Build & Sign Pipeline (C2) (2026-05-28)

.github/workflows/container-build-sign.yml — build → ACR push by digest → Notation v2 sign via AKV plugin → Syft SBOM → Grype scan (configurable cutoff). OIDC-only auth. Signing by digest (immutable artifact). tests/pipeline-integration/container-build-sign/ with clean + deliberately-vulnerable Dockerfiles. Added trivy --skip-dirs tests/pipeline-integration to pre-commit + D2.


v0.23 — Entra ID Baseline Module (H1) + Conditional Access Module (H2) + Entra PIM Templates Module (H3) (2026-05-28)

H1 — named locations (IP + country) + custom auth strength policy. Password protection + tenant branding emitted as *_patch_body JSON outputs for az rest PATCH (no TF resource for those Graph endpoints).

H2 — 6 CA policies (MFA Mandatory / Tier-0 Phishing-Resistant+Compliant Device / Block Legacy Auth / Geo-Block / High-Risk Block / Medium-Risk MFA). Every policy excludes break-glass group. Requires P1 for risk policies.

H3 — tier-0 + tier-1 AAD role eligibility. Activation rule bodies emitted as JSON for az rest PATCH (roleManagementPolicies has no TF resource in azuread v3). First modules using hashicorp/azuread ~> 3.0.

Full Terratest suite: 18 top-level tests pass.


v0.22 — Secure AKS Module (F3) (2026-05-28)

modules/azure/aks-secure/ — private AKS (Workload Identity + OIDC issuer + AAD-RBAC + local accounts disabled + Azure CNI Overlay + Calico NetworkPolicy + Azure Policy add-on + KEDA + Image Cleaner + Key Vault Secrets Provider CSI + AzureLinux + Ephemeral OS + 3 AZs). Emits F0 cluster_contract. network_policy = "none" rejected (D4's generated NetPols need an enforcer). Validate-only test + build-tagged integration (~$10). M2a F-module wave complete (F0 + F1 + F2 + F3 + F4 + F5 + F6 + D4).


v0.21 — Key Vault Module (F5) (2026-05-28)

modules/azure/key-vault/ — Premium KV RBAC mode enforced + purge protection enforced (both rejected at plan if false) + default-deny network ACLs + AzureServices bypass + Private Endpoint + auto A-record. 5 RBAC tiers: Administrator/Secrets Officer/Secrets User/Crypto Officer/Crypto User. Emits F0 kv_contract. Build-tagged integration (~$2).


v0.20 — Container Registry Module (F4) (2026-05-27)

modules/azure/acr/ — Premium ACR (admin_enabled=false enforced) + Private Endpoint + auto A-record + AcrPull role assignments + optional diagnostics + optional geo-replication. Emits F0 registry_contract (signing_enabled reflects server-side trust policy; Notation enforcement lives at AKS admission layer D4). Build-tagged integration (~$5).


v0.19 — Kyverno AKS Policy Bundle (D4) + Kyverno Test Framework (X4) (2026-05-27)

5 ClusterPolicies (all Enforce): disallow-latest-tag (require-tag + no-latest) · require-signed-images (Notary v2 for *.azurecr.io/*) · require-pod-labels (4 mandatory labels) · disallow-privileged-containers (privileged + privilege-escalation) · require-network-policy (generate default-deny NetPol per namespace). System namespaces excluded via exclude.any.resources.namespaces. 5 test suites / 21 assertions. Pre-push hook.


v0.18 — Network Hub Module (F2) (2026-05-27)

modules/azure/network-hub/ — hub vNet + N spoke vNets + bidirectional peering + optional Azure Firewall (Standard/Premium/Basic) + per-spoke route tables (0.0.0.0/0 → VirtualAppliance) + optional Private DNS zones + NSG flow logs + Traffic Analytics. Emits F0 spoke_network_contracts map. AzureFirewallSubnet required by precondition when firewall enabled. Build-tagged integration (~$5).


v0.17 — Cloud-Agnostic Module Contracts (F0) + F1/F6 retrofit (2026-05-27)

7 contract sub-modules at modules/_contracts/. Each: variables.tf (typed variable "candidate") + outputs.tf (echoing output "candidate") + versions.tf (no providers) + README. Cross-field rules. F1 retrofit: added identity_contract + observability_contract outputs (additive). F6 retrofit: added object_store_contract output. Conformance harness: 3 tests + 4 negative subtests. Decision D18, D19.


v0.16 — GitHub Onboarder App (B1) (2026-05-26)

apps/github-onboarder/ — ESM/TS GitHub App (Probot): validate → reconcile repo → push rendered template → branch protection → per-env (environment + variables + secrets + federated OIDC cred). Federated creds only (no client secrets). Idempotent across re-runs. 6 test suites / 38 tests. M1 code track closed.


v0.15 — Runbook sign-offs begin (2026-05-26)

C4, R1, D1 promoted to 🟩 (Signed off by Sagar, 26/05/2026). - D1: pre-commit hooks block hardcoded secrets, missing tags, public storage. - C4: branching standard + template structure sound. - R1: PR template validation works end-to-end.


v0.14 — HubSpot Lead Enrichment (A1) + Discovery Trigger Automation (A5) + GitOps Branching Standard (C4) + PR Template Enforcement (R1) M1 code shipped (2026-05-26)

  • A1 (apps/crm-automations/src/lead-enrichment/): Clearbit/Apollo adapters, HubSpot property mapping.
  • A5 (apps/crm-automations/src/discovery-trigger/): GH Actions dispatcher, Postmark email, offer email template.
  • C4 (docs/conventions/branching.md + templates/client-repo/): 7-section standard + skeleton.
  • R1 (.github/PULL_REQUEST_TEMPLATE.md + .github/workflows/pr-template-check.yml): 9-point validation. 4 test suites / 19 tests.

v0.13 — G0–G6 Discovery Auditor Collectors (G1) MVP (2026-05-26)

apps/discovery-auditor/ (12 source files): Resource Graph KQL + Defender REST + Azure Policy + AAD audit logs + Cost Mgmt collectors. 11 YAML rules (5 files) across network/encryption/identity/logging/cost, each with SOC2 CC + ISO27001 + CIS Azure mappings + remediation_asset_id. WORM audit log (G6, SHA-256 hash chain). 4 test suites / 29 tests.


v0.12 — Terraform OPA Policy Bundle (D3) + Conftest Test Suite (X3) signed off (2026-05-26)

D3 (Conftest/OPA) promoted to 🟩 (first 🟩 in the project). conftest verify 36/36 · conftest test clean_plan.json 0 findings · conftest test bad_plan.json 9 findings · pre-push hook exits 0.


v0.11 — Gap analysis + milestone restructure (2026-05-26)

14-item gap register added (see docs/context/10-gap-register.md). 6-milestone roadmap. Maturity entry points. Ownership taxonomy. 8 new assets (C5, D5, E0, E7, F11, F12, G7, B6). Brownfield principle. F0 retrofit tracking. V2/V3 promoted to Baseline.


v0.1–v0.10 — Foundation

  • v0.1–v0.3: Architectural guardrails, Azure-first pivot, Baseline/Advanced packaging.
  • v0.4: Repo scaffold + X1 sandbox IaC code-complete.
  • v0.5: C1 keystone workflow code-complete.
  • v0.6: X2 Terratest harness code-complete.
  • v0.7: F6 state-backend module code-complete.
  • v0.8: F1 baseline module code-complete.
  • v0.9: D1 + D2 quality gates code-complete.
  • v0.10: D3 + X3 Conftest/OPA bundle code-complete.