Delivery and governance
Risk register and roadmap
Every known weakness, with its severity, its current status and the fix. The list comes from a code-level review of the repository and read-only checks of the live Cloudflare account and GitHub repository on 25 September 2026.
Severity weighs likelihood against impact for a service that holds personal data about named Health NZ staff. Effort is a rough guide: quick is hours to a day, moderate is days, larger is a piece of work in its own right. Most items are ordinary for a young platform built quickly by one person. None requires re-architecture; the fixes fit the existing design.
1critical
4high
16medium
6low
Critical and high
| ID | Risk | Severity, status | Fix |
|---|---|---|---|
| R1 | The launch-page deploy publishes the whole repository. The launch page's Pages project deploys the repository root, so every tracked file is downloadable from hnzradtools.nz and beta.hnzradtools.nz. That includes source, internal plans and two historical referral extracts from Triage's scenario work, which contain patient age, gender and free-text clinical indications. Older Pages deployments keep their own URLs. | Critical Open | Deploy only the launch page's own files from a dedicated folder. Delete the older Pages deployments. Remove the extracts (R5). Assess under Health NZ's privacy process. Quick |
| R2 | The email worker can be called from the internet without authentication. It is meant to be reachable only through service bindings, but its default workers.dev hostname is enabled, and its caller check is in grace mode because the shared secret is not yet set. Production also runs a version from before the check was added, because the promote workflow does not redeploy it (R6). | High Open | Switch off workers.dev for the email worker in both environments, set the shared secret on the worker and its callers, and redeploy production.Quick |
| R3 | Key-person dependency. The repository sits on one personal GitHub account, runtime secrets are set from one workstation, and production API hostnames use that person's workers.dev subdomain. About 1,300 of 1,400 commits are by one person. | High Open | The handover stages on Operations: repository transfer, named administrators, a vault and a secrets register. Moderate |
| R4 | No review or approval gate. The current GitHub plan cannot protect branches. Anyone with write access can push to a production branch, and the promote workflow merges its own pull request without review. One Cloudflare token deploys both preview and production. | High Open | After transfer: branch protection, required reviews, a production environment with approvers, and separate preview and production tokens.Quick, after transfer |
| R5 | Patient-derived text in Triage's workflow. The referral extracts used to build scenarios are in the repository's history. The offline pipeline sent referral text to the Anthropic API directly. Anyone can paste referral text into AI triage, which sends it to Workers AI and Anthropic (it is not stored), and the screen does not warn against including identifiers. | High Open | Remove the extracts and rewrite history before transfer. Route all AI calls through Health NZ's contained AI endpoints. Add a "do not include identifiers" notice to AI triage, and consider requiring sign-in for it. Moderate |
Medium
| ID | Risk | Status | Fix |
|---|---|---|---|
| R6 | Pipeline gaps. The promote workflow merges with GitHub's built-in token, which does not trigger other workflows, so the email worker is never redeployed by a promote. The hub is not yet a promote option. DEPLOYMENT.md lists the wrong promote choices, and some old manual deploy scripts remain. | Open | Add email worker and hub jobs to the promote workflow; update the docs; delete the old scripts. Quick |
| R7 | Token handling in the browser. The hub hands each app its 15-minute token in the redirect URL, and apps keep it in browser storage. Site Profiles' older portal handoff puts a 24-hour token, which includes the email address, in a URL, without a state check. The hub's own sign-in and admin pages hold the raw session value in script (a URL fragment and session storage). | Partly fixed | Hand off with a single-use exchange code instead of the token; retire the older handoff; move hub admin calls onto the cookie with CSRF protection; pair with a Content-Security-Policy (R19). Moderate |
| R8 | APIs on workers.dev. The Service Sizing and Triage APIs are served from the personal workers.dev subdomain, where zone-level firewall and rate-limiting rules cannot apply. Internal Workers also have workers.dev enabled by default. | Open | Custom domains under hnzradtools.nz for both APIs; service bindings for Worker-to-Worker calls; workers.dev off everywhere.Quick to moderate |
| R9 | The member portal is not on the hub yet. It signs members in against the Site Profiles registry and keeps its own session, so deactivating someone in the hub does not end their portal access. Hub invitations link to a portal route that cannot redeem them; they work only because the link falls through to email sign-in. | By design, for now | Move the portal onto the hub with a cross-domain token exchange, then drop its own sign-in. Larger |
| R10 | Shared and legacy credentials. Until the cutover retires them, Site Profiles and Triage password sign-in, and the portal's admin link, issue tokens from the apps' own user tables, so deactivating someone in the hub does not stop them. Site Profiles and Service Sizing share an older token-signing key. One internal bearer value gates three different flows under two names, and two apps compare it without a constant-time check. Triage still accepts a legacy shared editor credential. | Partly fixed | Retire the legacy key and credential once the hub is authoritative (already in the cutover plan); one token per flow; constant-time comparison. Quick |
| R11 | Sign-in code checking. To cope with email security scanners that submit links twice, a code stays acceptable for 60 seconds after use, and guesses in that window are not counted. The code-check endpoint has no per-IP limit. | Open | Count attempts in the replay path and rate-limit the code-check endpoint by IP. Quick |
| R12 | Admin scope is broad. Any app's administrator can see every person's decrypted addresses and the full audit log including IP addresses, and can assign national leadership roles. Announcement authoring and sending are not audited, and any author can edit any announcement. | Open | Limit the directory and log to the admin's own app, restrict leadership roles to portal admins, and audit announcements. Moderate |
| R13 | Preview can email real people. The preview email worker sends real mail, and preview holds real staff records. A test once emailed 48 radiologists. | Partly fixed | Add a recipient allowlist in the preview email worker; point all non-production bindings at it. Announcement sends already rehearse in preview. Quick |
| R14 | Some personal data is stored in plain form. The older staff user tables in Site Profiles and Triage (names and addresses, until they are dropped at the end of the cutover); facility contact names, emails and phone numbers; IP addresses and browser details in portal sessions; an unkeyed hash of addresses in the hub's rate-limit table; addresses in Site Profiles' audit history. | Partly fixed | Encrypt facility contacts with the shared crypto package; use keyed hashes; set retention periods. Moderate |
| R15 | Backup and restore are not documented. D1 Time Travel gives 30-day point-in-time restore, but there is no written procedure and no restore point recorded before production migrations. | Open | Document restore; record a Time Travel bookmark in the promote workflow; add a scheduled export to a separate bucket. Quick |
| R16 | Thin automated testing. CI runs type checks only and does not block deploys. Most apps have no tests. The hub's 12 test suites run against the preview database and are not in CI. | Open | CI for every component, deploys that wait for it, and a test database for the hub suites. Then add tests around access rules and migrations first. Moderate |
| R17 | Dependencies. Several packages have known critical or high advisories (including a pinned Next.js 14.2.5 in a static export, where most server-side issues do not apply). Automated updates are off, and three deployables have no lockfile. | Open | Enable Dependabot; commit lockfiles; use npm ci in deploys; upgrade the flagged packages.Quick to moderate |
| R18 | In-house token and password code, in several copies. The primitives are Web Crypto and sound, but token framing is hand-written, with copies in three apps as well as the shared package, and older password hashing uses 100,000 PBKDF2 iterations (current guidance is 600,000). | Open by decision | Move every app onto the shared @hnz/auth package, or a standard library such as jose, with issuer and audience always checked. Password hashing goes away when password sign-in is retired.Moderate |
| R19 | Browser security headers. Only the hub's pages send a Content-Security-Policy, and it still allows inline script. Service Sizing, Triage and the launch page also lack HSTS and frame protection. The portal renders announcement HTML without a sanitiser (the hub produces it from escaped text, so this is defence in depth). | Partly fixed | One baseline of headers with a strict CSP for every site, set per project or once at the zone; add a sanitiser in the portal. Quick |
| R27 | Derived-tier and grain edge cases. Service Sizing Admin is not produced by any grant, so a later Service Sizing grant change silently demotes an admin to Editor, and the admin screen cannot create one. When two grains merge, the hub's service-area record is not updated. | Open | Treat Service Sizing Admin as a set tier that grant changes do not override; update the hub's service area on a grain merge. Quick |
| R25 | Cloudflare account hygiene. Two-factor sign-in is not enforced for account members, two members are Super Administrators, and the account hosts projects unrelated to these tools. | Open | Enforce two-factor now; reduce Super Administrators to named Health NZ staff; separate the account (see options). Quick |
Low
| ID | Risk | Status | Fix |
|---|---|---|---|
| R20 | Triage migrations are applied by hand. The Triage deploy workflow has no migration step. | Open | Add the same migration step the other apps use. Quick |
| R21 | Two R2 buckets are shared by production and preview. Site Profiles' bucket is unused at runtime; Triage's database exports from both environments land in one bucket. | Open | Separate preview buckets; environment prefix on export keys. Quick |
| R22 | Observability. Logs are on for the hub and email worker only. There is no alerting or uptime check, and the email worker logs recipient addresses. | Open | Enable logs everywhere with redaction; add health checks and alerting. Moderate |
| R23 | Hub housekeeping. No break-glass path or first-owner bootstrap in code; no key-rotation tooling; expired codes, sessions and old audit rows are never cleaned up; no way to add a second address from the admin screen; /health reports which secrets are set; owners and some admins always get 30-day sessions. | Open | A scheduled clean-up, a documented break-glass owner, an "add address" action, a minimal public health check, and shorter sessions for owners. Moderate |
| R24 | Documentation drift. Several per-app guides describe older designs (for example, mail providers, sign-in methods and branch names that have since changed). | Open | Refresh the per-app guides; review this pack at each release. Quick |
| R26 | Triage review pool visibility. Any signed-in Triage user, including reviewers and viewers, can see draft scenarios in the hidden review pool. | Open | Check the tier, not just the presence of a token. Quick |