Manual Test Runbook — I6: Patch Compliance Report¶
Owner: Sagar | Time: ~5 min (mostly offline — I6 is a pure report generator; the only live phase is an optional az export)
Overview¶
I6 turns one Azure Update Manager assessment export (a set of machines, each carrying the OS updates Update Manager found pending) into a versioned, optionally-branded patch-compliance report — markdown + a self-contained HTML page (+ optional Pandoc PDF): overall patch coverage %, the machines out of compliance, the count of pending Critical/Security updates, and SLA aging.
I6 scans nothing and re-derives nothing from Azure: every figure is a
re-presentation of what Update Manager already assessed, with two documented
extras — a compliant rule (zero pending Critical/Security) and a
configurable SLA aging window, each with its basis shown inline (the same
"never fabricate, show your work" stance as S4/L5). Pure adapt → render + jest,
mirroring E0/S2/S4/L4/L5.
Part A — Build, typecheck, unit tests (~3 min, offline)¶
Expect: 2 suites pass — classification normalization (Critical/Security
case-insensitive, everything else → Other), the compliant rule (incl. the
never-assessed honesty case), coverage math (incl. the null-coverage empty
fleet), pending-by-classification tally, SLA aging + breach detection (oldest
publishedAt basis, lastAssessedAt fallback, within-window non-breach,
configurable --sla-days, future-date clamp, oldest-first sort, never-fabricate
null age), highlights + the always-present methodology section, meta/schema
stamping, determinism, and the renderer (section ordering, machine rows, honest
empty/no-breach states, HTML self-containment + escaping + accent coloring).
Part B — Offline CLI demo (~2 min)¶
B1. Generate a report from the bundled fixture (unbranded)¶
node dist/index.js --assessment examples/assessment.json --output /tmp/i6out --now 2026-06-09T12:00:00.000Z
cat /tmp/i6out/patch-compliance.md
Expect: stdout wrote /tmp/i6out/patch-compliance.md, .../patch-compliance.html,
.../model.json (coverage 33.3%, 2 SLA breach(es)). The markdown contains, in
order:
- A header — "Patch Compliance Report", "Prepared by: SnowOps Cloud", "As of:
2026-06-09", subscription
11111111-...(no "Prepared for" line — the run is unbranded). ## Patch coverage: 33.3%with a metric table: 6 total machines, 2 compliant, 3 non-compliant, 1 never assessed, 2 pending Critical, 2 pending Security, 2 pending Other.## Machines— a 6-row table:web-prod-01✅,web-prod-02⚠️ (1 Crit, aged100d (since publish) ⛔),api-stg-01⚠️ (1 Sec,8d, no breach),db-prod-01⚠️ (1 Crit, aged86d (since assessment) ⛔— nopublishedAt, so aged fromlastAssessedAt),cache-prod-01✅ (only Other updates — they don't gate compliance),new-stg-01❓ Never assessed (nolastAssessedAt).## SLA breaches— 2 rows, oldest first:web-prod-02(100 days, aged frompublishedAt),db-prod-01(86 days, aged fromlastAssessedAt).## Highlights— coverage line, out-of-compliance count, a never-assessed callout, and the SLA-breach line namingweb-prod-02as the oldest.## Methodology— always present; states the compliant rule, the coverage math, and the 30-day SLA aging basis.
grep -c http /tmp/i6out/patch-compliance.html # expect 0 — fully self-contained, no network refs
cat /tmp/i6out/model.json | head -5 # versioned model.json (schema v1.0)
Open the HTML — a self-contained styled page (inline CSS only, no external
script/stylesheet refs), a 33.3% coverage tile in red (below the 80% band),
the default SnowOps accent #1f6feb on headings/borders, and a SnowOps Cloud
text wordmark (unbranded run, no logoUrl).
B2. Branded run + custom SLA window¶
node dist/index.js --assessment examples/assessment.json --branding examples/branding.json \
--sla-days 90 --output /tmp/i6branded --now 2026-06-09T12:00:00.000Z
grep -A1 "## SLA breaches" /tmp/i6branded/patch-compliance.md
Expect: the title becomes "June 2026 Patch Compliance Report", a "Prepared for:
Contoso Logistics" line appears, the accent color is #0b5fff. With a 90-day
SLA, only web-prod-02 (100 days) still breaches — db-prod-01 (86 days) drops
below the window. This demonstrates --sla-days driving breach detection.
B3. Honest empty / never-assessed states¶
echo '{"machines":[]}' > /tmp/empty.json
node dist/index.js --assessment /tmp/empty.json --output /tmp/i6empty
grep -A1 "## Machines" /tmp/i6empty/patch-compliance.md
grep "## Patch coverage" /tmp/i6empty/patch-compliance.md
Expect: coverage — (not a fabricated 0/100), and the Machines section plainly
states "No machines were present in the assessment export" — I6 never invents a
fleet it wasn't given. (The fixture's new-stg-01 already exercises the
never-assessed honesty path in B1.)
B4. Optional PDF (best-effort)¶
Expect: patch-compliance.pdf present if Pandoc/TeX Live is installed;
otherwise a PDF render failed (continuing — markdown/HTML are still written)
message on stderr and a successful exit — PDF is opt-in and best-effort (same
stance as G3/S4).
B5. CLI help / required-flag guards¶
node dist/index.js --help
node dist/index.js --output /tmp/x # missing --assessment
node dist/index.js --assessment examples/assessment.json # missing --output
Expect: usage text on --help; clear error: --assessment <file> is required
and error: --output <dir> is required messages (exit 1).
Part C — Live Update Manager export (optional, ~5 min)¶
I6 reads any JSON in the { machines: [...] } shape, so the only "live" step is
producing that JSON from Azure. Update Manager assessment results live under
patchassessmentresources in Resource Graph; a query like the following
exports the per-machine pending-update inventory:
az graph query -q "
patchassessmentresources
| where type =~ 'microsoft.compute/virtualmachines/patchassessmentresults/softwarepatches'
| project id, name, classification=properties.classifications, publishedAt=properties.publishedDateTime
" -o json > /tmp/raw-assessment.json
Reshape raw-assessment.json into the UpdateManagerAssessment contract
(group software-patch rows under their parent machine's pendingUpdates, attach
each machine's lastAssessedAt from its patchassessmentresults parent), then:
node dist/index.js --assessment /tmp/live-assessment.json --output /tmp/i6live --client-name "Acme Corp"
Expect: a report whose coverage %, non-compliant machines, and SLA breaches reconcile with what the Azure Portal's Update Manager blade shows for the same subscription. I6 only re-presents — the numbers should match the portal's assessment, not diverge from it.
Pass criteria¶
- Part A — builds, typechecks; both suites pass
- Part B1 — fixture run reports 33.3% coverage, the 6-machine table with
correct statuses (incl.
cache-prod-01compliant on Other-only updates andnew-stg-01never-assessed), 2 SLA breaches oldest-first with the correct aging basis each; HTML self-contained (grep -c httpreturns 0);model.json(schema v1.0) written - Part B2 — branding applied;
--sla-days 90dropsdb-prod-01from the breach list - Part B3 — empty fleet renders
—coverage and an honest "no machines" state, not a fabricated number - Part B4 — PDF render attempted; failure (if Pandoc absent) does not fail the run
- Part B5 —
--helpand required-flag error messages behave as documented - Part C (optional) — a live Update Manager export reconciles with the portal
Sign-off¶
- Tester: _ | Date: _ | Result: PASS / FAIL / N/A
- Notes: