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.


v0.59 — R2 production change log (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 — I5 Defender → ticket via 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: X5 pipeline integration tests + X8 synthetic monitoring (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; J4 detection alert rule pack (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 — F7 Terragrunt live-infra reference (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 — E7 TicketPlatform 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 — D5 Policy Waiver Engine 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 — F11 Module Versioning + 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 — B6 Client Self-Service Bootstrap 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 — F12 Brownfield Import Library 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 — L4 Automated Restore Drill 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 — L2 Cross-Region Replication Module 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 — L1 Azure Backup Policy Module 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 — S2 Azure Policy Compliance Dashboard 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 — 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 — V2 + 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 — E0 Lightweight Compliance Snapshot 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 — X7 sandbox cleanup + 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 — U1 budget-alert + U2 tag-policy (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 — N5 private-endpoint-policy + N6 nsg-baseline (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 — M1 encryption-policy + M2 cmk + M3 tls-policy + M6 data-residency-policy (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 — J2 policy-diagnostics + J6 audit-log-archive (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 — J1 log-analytics (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 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 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 — H5 SP inventory + H7 break-glass (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 — F8 GitOps 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 — B5 pim-azure-resources (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 — B4 client-state-backend (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 — B3 subscription-baseline (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 — B2 client-bootstrap (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 — C3 aks-deploy (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 — C2 container-build-sign (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 — H1 aad-baseline + H2 conditional-access + H3 pim-templates (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 — F3 aks-secure (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 — F5 key-vault (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 — F4 acr (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 — D4 + X4 Kyverno bundle (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 — F2 network-hub (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 — F0 contracts + 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 — B1 snowops-onboarder (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 — A1 + A5 + C4 + 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 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 — D3 + 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.