Skip to content

Remediate TransTrack validation findings (C/H/M) - #229

Merged
NeuroKoder3 merged 41 commits into
mainfrom
cursor/remediate-validation-findings-bbb1
Aug 3, 2026
Merged

Remediate TransTrack validation findings (C/H/M)#229
NeuroKoder3 merged 41 commits into
mainfrom
cursor/remediate-validation-findings-bbb1

Conversation

@NeuroKoder3

Copy link
Copy Markdown
Owner

Summary

Closes the Critical/High findings from the TransTrack Validation Report and the remaining High/Medium items that were still open on this branch (licensing stubs, publisher-key gate, API client silent no-ops, trial reset, UTC date display), with a finding→change map under docs/compliance/VALIDATION_FINDING_REMEDIATION_MAP.md.

Critical

  • C-1 SMART patient-compartment isolation at storage + scopes
  • C-2 Executed validation package (IQ/OQ executed; PQ NOT EXECUTED by vendor), VSR, FMEA, residual risk
  • C-3 Calculator source register / reference tables; PELD fail-closed; LAS→TTLI
  • C-4 Clinical validators at every persistence boundary

High (this turn completes the last open Highs)

  • H-1 / H-13 Renderer PHI list-scope gate, cache purge on logout, path redaction; idle-timeout timer fix
  • H-6 Restored featureGate / tiers; session entitlement + entity write gates
  • H-7 Packaged builds refuse the development publisher key; --for-sale release gate
  • H-14 Remote functions.invoke / unsupported entities fail loudly; Vitest parity suite

Medium / docs

  • M-21 Trial high-water clock (delete/rollback cannot reset)
  • M-24 Clinical dates render in UTC with explicit marker
  • Unsupported AATB product claims scrubbed from UI/splash/compliance view/keywords

Test plan

  • node scripts/run-test-suites.cjs all — 63/63
  • cd server && npx vitest run --config vitest.config.mjs — 328 passed
  • Vitest: IdleTimeoutManager, remoteClient, utils, apiClientParity
  • node tests/license.test.cjs — 26 passed (H-6/H-7/M-21)

Residual risk

See docs/compliance/RESIDUAL_RISK.md (PELD RR-01, IRE unvalidated, KDPI/EPTS approximation, RLS not live-Postgres-proven, PQ site-only). Low/informational items (L-1 typing, L-2 module size, I-2–I-4 IRE disclosure) remain partially open and are documented there.

Open in Web Open in Cursor 

cursoragent and others added 30 commits August 2, 2026 19:18
… boundary

C-1: A patient-level SMART grant could read, search, update, delete and bulk-
export any patient's resources within the organisation. The scope check
returned true for reads (no subject available to compare) and explicitly
returned true for searches of non-Patient types, deferring to a server-side
filter that did not exist.

- Add server/src/fhir/compartment.js: the FHIR R4 CompartmentDefinition/patient
  path map, an in-process membership check, and a parameterised JSONPath SQL
  predicate. Both the check and the predicate are driven by the same map so
  they cannot drift. Unknown and non-compartment types fail closed.
- scopes.js: replace isAllowed's blanket search allowance with resolveAccess,
  which reports the granting access level and refuses patient-level grants on
  types outside the patient compartment.
- middleware/auth.js: pin auth.compartment.patient when the grant is
  patient-level so enforcement cannot be forgotten by a route.
- fhir/storage.js: enforce the compartment on read, search, update, softDelete
  and history, and refuse writes that would place a resource outside it.
- fhir/bulkData.js: constrain $export to the launch patient and use the
  compartment paths rather than a subject/patient best-effort filter.

H-4: authorise every transaction-bundle entry with the same scope check the
individual CRUD routes use, before executing any entry, and bound bundle size.

M-9: native JWTs bypassed FHIR authorisation entirely. Enforce a role matrix
so a viewer can no longer create, update or delete FHIR resources.

Adds 29 regression tests; 16 of them fail against the previous code.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
C-4: electron/functions/validators.cjs was dead code — referenced only by its
own definition and export. Clinical range checking existed solely in the React
form, so IPC, REST, FHIR import, the FHIR webhook and HL7 v2 ingestion could
all persist clinically impossible values (MELD 250, negative LAS).

- Expand the validator into the single clinical-validation authority: MELD-Na,
  MELD 3.0, PELD, KDPI and EPTS ranges; calendar-date sanity (no future or
  impossible birth dates); laboratory plausibility bounds in canonical units;
  and unit-string checking so a umol/L creatinine is rejected rather than
  scored as mg/dL. Every range names its controlled-source id.
- Add validateEntity/assertValidEntity with a per-entity rule table that is
  safe for partial updates.
- Invoke it at entity:create, entity:update, HL7 ingest (insert and
  demographics update), FHIR import, the FHIR webhook, and the server's
  patientService create/update. The server shares the same module by relative
  path, as it already does for the calculators, so the desktop and thin-client
  tiers cannot enforce different rules.

Also registers previously ungrouped suites in the runner and adds an orphan
check so a new test file can no longer sit outside every group (H-8 groundwork).

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
M-13  PGSSL=require set rejectUnauthorized:false, which encrypted the
      connection to the database while accepting any certificate presented.
      Both TLS modes now verify the chain against PGSSL_CA_FILE or the system
      trust store; require differs from verify-full only in hostname
      checking. Skipping verification needs PGSSL_ALLOW_UNVERIFIED, which is
      refused in production.

M-14  With CORS_ALLOWED_ORIGINS empty and NODE_ENV=development, the origin
      option was the boolean `true`, so @fastify/cors reflected any
      requesting origin alongside credentials:true and a hostile page could
      read authenticated responses. The origin is now always matched against
      an explicit allowlist: the configured one, else a fixed localhost list
      in development and test, else nothing.

M-15  .env.example and docker-compose.yml shipped JWT secrets that met the
      32-byte floor while being fully public, plus a fixed Postgres password.
      Neither file now carries a usable secret: compose fails to start
      without POSTGRES_PASSWORD and JWT_SECRET, .env.example holds a
      placeholder too short to parse, and config.js refuses known
      placeholders and filler-padded values in production. Every compose port
      is also published on loopback only.

M-16  No unhandledRejection or uncaughtException handler was registered, so
      either failure mode skipped the shutdown path entirely. Both now log
      and drain the server before exiting non-zero.

L-8   /ready returned the driver's error message (host, port, database, role)
      to an unauthenticated caller, and the unique-violation handler returned
      PostgreSQL's err.detail, which echoes the conflicting values. Both are
      logged server-side and answered generically.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
…tener

H-3   Migration 008 created hl7_dead_letters and hl7_sending_apps with no
      row-level security, and 006 carried a comment claiming issued_licenses
      was "protected by row-level security" when no such DDL was ever
      written. On top of that, POST /hl7/dead-letters/:id/replay selected by
      id alone, so an admin in one organisation could replay another
      organisation's quarantined PHI into their own; the sending-app list and
      delete routes spanned every tenant the same way.

      Migration 010 enables and forces RLS with app_current_org_id() policies
      on all three tables, following 003_rls.sql. hl7_sending_apps gets one
      SELECT-only carve-out because the MLLP listener resolves MSH-3 to an
      org before any tenant context can exist. issued_licenses is written by
      the Stripe webhook, which is keyed by subscription and has no org
      context, so that path declares itself through app.billing_context via
      the new pool.withBillingContext rather than the policy being weakened.
      The route queries carry explicit org predicates as well, so a database
      with misconfigured RLS is not the only thing standing in the way.

M-27  The listener wrote dead letters with a NULL org_id, so raw PHI
      accumulated unowned. A NULL would have been invisible under the new
      policy but still unattributable, so migration 010 instead creates a
      reserved INACTIVE organisation (the all-zero UUID), re-attributes the
      existing NULL rows to it, and makes the column NOT NULL. Nobody is a
      member of that organisation, so its rows are readable by no tenant.
      Sending-app resolution also now tries the facility-qualified key before
      the bare application name.

H-9   MllpFramer.push concatenated indefinitely when no end block arrived — a
      remote memory-exhaustion DoS from an unauthenticated peer. The buffer
      is capped (1 MiB default), the framer releases it and throws on breach,
      and the listener destroys the connection, logging the bound rather than
      the bytes. Connections also get an idle timeout and a concurrency cap,
      and the listener warns when it is reachable off-host without TLS.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
H-12  POST /cds-services/:id wrote the complete CDS Hooks request and
      response into cds_service_invocations. A request carries the patient
      context plus every prefetched FHIR resource the EHR resolved for us, so
      the audit trail had become a second copy of the clinical record with
      none of the minimisation or retention rules that govern the primary
      store. The route now writes a structured summary — hook, context and
      prefetch key names, resource types and counts, card counts and
      indicators, sizes, duration, error — and copies no value or free text
      out of either payload. Full capture is opt-in via
      CDS_CAPTURE_RAW_PAYLOADS; captured rows are flagged and given an
      explicit raw_payload_expires_at so the retention expectation lives on
      the row rather than in a runbook.

      Invocation was also gated by nothing but global authentication, so any
      token in the organisation could pull decision-support cards about any
      patient the service could resolve. A SMART token now needs a FHIR read
      or search scope, and a native JWT needs a role that may read patient
      data.

L-15  The feedback endpoint answered { acknowledged: true } and discarded the
      body, so every EHR sending outcomes believed we were recording them.
      Feedback is persisted to cds_service_feedback (coded override reasons
      only, never the clinician's free text), and a failure to store now
      surfaces as a failure instead of a false acknowledgement.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
…ount

M-10  findUser's unscoped query ended in LIMIT 1 and /auth/login called it
      with no orgId, so an email registered in two organisations
      authenticated into whichever row the planner happened to return —
      possibly a tenant the user has no relationship with. The query now
      looks for a second match and refuses the login with
      `organization_required`; /auth/login accepts an optional orgId to
      disambiguate. The same rule applies to the SMART password flow.

      setLockedUntil and isLockedOut both keyed on email alone, so five
      failures against one tenant locked that address out of every other
      tenant — a cross-tenant denial of service that needed no credentials.
      Both now key on the resolved user id, and the failure window counts
      only attempts recorded against that user's organisation. The account is
      therefore resolved before the lockout check rather than after.

M-27  A session in the reserved quarantine organisation is refused here too,
      so a stray user row could not turn unattributable dead-letter PHI into
      readable PHI.

M-26  The MFA-enrolment token was signed and verified with no audience, so
      only the `purpose` claim separated an enrolment grant from any other
      token this issuer signs. It now carries a dedicated
      "<JWT_AUDIENCE>:mfa-enroll" audience, and the enrolment routes verify
      it.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
…cally

M-11  The consent page carried launch_patient as a hidden field and POST
      /oauth2/authorize trusted whatever came back, so anyone who could reach
      the consent endpoint could name an arbitrary patient and get an
      authorization code whose launch context pointed at them. The launch is
      resolved once at GET /authorize and stored in smart_launch_contexts;
      the form carries only an opaque, hashed, single-use handle bound to the
      client that started the launch. A missing, expired, spent or
      mismatched handle yields no launch context — never a client-supplied
      one.

M-26  makeIdToken signed the SMART/OIDC ID token HS256 with JWT_SECRET, the
      same key that signs our own API access tokens. Every relying party
      would have needed that secret to verify an ID token, and any client
      holding it could mint access tokens for any user in any organisation.
      ID tokens are now signed RS256 (or ES256) with a dedicated key and the
      public half is published at /.well-known/jwks.json, advertised through
      jwks_uri in the SMART configuration. Production refuses to mint a token
      without SMART_ID_TOKEN_KEY_FILE, because an ephemeral key differs per
      replica and per restart.

L-14  verifyAssertion required a jti but kept no record of the ones it had
      accepted, so a captured Backend Services client assertion could be
      replayed until its exp. Accepted (client_id, jti) pairs are recorded in
      smart_client_assertion_jtis, uniqueness is enforced by the primary key
      so concurrent redemptions cannot both win, and the jti is only recorded
      after the signature verifies so the cache cannot be poisoned against a
      legitimate client. Expired rows are reaped on a throttle.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
M-12  Six of the seven calculator routes handed req.body straight to the
      scoring function with no schema at all, so unvalidated client input
      reached the formulas and any extra property travelled through into the
      echoed `inputs` block. Each route now parses an explicit Zod schema
      whose required fields mirror the calculator's own REQUIRED_FIELDS, and
      unknown keys are stripped.

      The one route that did have a schema, /calculators/meld, declared
      fields (bilirubin, inr, creatinine) that the calculator does not take —
      it destructures creatinine_mg_dl, bilirubin_mg_dl and inr — so the
      schema stripped the body down to keys the formula ignored and every
      request scored INSUFFICIENT_DATA. The schema now matches the contract.

      PATCH /patients/:id used z.object({}).passthrough(), which let any
      caller with write access set any allowlisted column by naming it:
      priority_score, meld_score, psychological_clearance, waitlist_status
      and the rest. The writable columns are now enumerated with their types,
      server-owned columns are absent, and a body with nothing writable in it
      is rejected rather than sent to the service layer. POST /patients uses
      the same allowlist instead of its own passthrough.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
H-11: appendAuditRecord in auditChain.cjs is now the only way an audit row is
written. It computes the chain fields inside the insert transaction and throws
when it cannot, so an operation that cannot be evidenced does not proceed.
verifyAuditChain no longer filters rows without record_hash — an unchained row
is reported as a missing_hash failure, which is what makes the direct inserts
that used to bypass the writer visible instead of invisible.

M-6: audit_logs gains a per-org seq column (migration 19, additive) that is part
of the canonical signed payload and the chain ordering, so append order no
longer depends on a clock a local admin can move. Verification detects gaps,
renumbering and non-monotonic timestamps, and treats pre-migration rows as
sequence-exempt explicitly rather than skipping them.

healthCheck reports the cached startup verification result, so a detected break
surfaces as a degraded state rather than only in the logs.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
…lass

H-2: the old check called PRAGMA cipher_version and set encryptionEnabled
unconditionally — that pragma returns an empty result in this build even on a
plaintext database, so it could not fail. verifyDatabaseEncryption now proves
five independent things: the cipher profile in force is sqlcipher, kdf_iter is
the 256000 the compliance claim quotes, a data page reads back with the
configured key, the file carries a SQLCipher salt, and the bytes on disk do not
begin with the plaintext SQLite magic. A packaged build that cannot verify
closes the handle and refuses to start; getEncryptionStatus reports the
evidence rather than a bare boolean.

M-7: applyCipherPragmas is the one definition of the profile, used by the normal
open path, migrateToEncrypted, backup verification and restore, so those paths
cannot drift apart again.

M-4: break-glass now refuses on a packaged build unless the operator confirms
with the account name, writes a high-severity chained audit record before the
credential changes, always forces must_change_password, and only clears MFA when
explicitly asked — as a separately audited event.

Audit rows written by init.cjs itself now go through the chained writer
(auditSystemEvent) so no unchained rows exist (H-11b).

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
… gate PHI exports

H-5: logger.write applies phiRedaction to the message and the metadata before
the entry reaches any sink, so the disk log, the dev console and the optional
remote collector see the same redacted text whether or not the call site
remembered. A re-entrancy flag and a fail-safe branch mean redaction can neither
recurse nor emit the unredacted value when it throws.

L-10: forwarded audit rows carry a stable salted HMAC of the workforce address
by default rather than the address itself. TRANSTRACK_SIEM_WORKFORCE_ID selects
raw or omit; an unrecognised value falls back to pseudonymous, so a typo cannot
start exporting mailboxes.

M-8: _writeKey refuses to write a plaintext key file on a packaged build when
safeStorage is unavailable, matching init.cjs rather than silently degrading.

M-25: a support bundle containing free text requires an explicit confirmation
token and a named operator, is marked PHI-bearing in the payload and the
filename, and cannot be produced when either is missing.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
H-1: entity:list and entity:filter for Patient now require a valid PHI grant.
A list-scope grant (entityId '*') lets a coordinator work a worklist with one
justification instead of one per row, and the read stays audited.

H-11a: logAudit delegates to the chained writer and no longer degrades to a row
without hash-chain fields; a failed audit write fails the originating operation.

M-1: labs, barriers, organOffers, livingDonors, postTransplant and operations
enforce the accessControl permission model instead of only checking that
somebody is logged in. shared.requirePermission/requireAdmin keep the failure
mode uniform. Offer transitions that carry a signature require MATCH_APPROVE;
administrative ones require MATCH_UPDATE.

M-2: function:invoke dispatches only through an allowlist that names the
permission each function needs, so importFHIRData and pushToEHR are no longer
reachable by any authenticated role.

M-3: a packaged build ignores the dev escape hatch entirely, so it cannot be
talked into the relaxed CSP by an environment variable.

M-5: auth:loginHints returns only isPackaged and setupTokenPresent — not the
setup token path, whether an admin exists, or the default admin address.

M-22: update check/download/install require an admin session, and download and
install refuse outright when electron-updater signature verification is not
configured for the build.

L-5: restore and backup paths resolve through fs.realpath and must land inside
the application data directory, the backup directory, or an explicitly
configured export directory. Write targets are additionally limited by extension
so a backup cannot be aimed at the encryption key file.

L-7: every preload listener strips the IpcRendererEvent and returns an
unsubscribe that removes the wrapper actually registered.

I-1: the remaining markers in these files are replaced with the constraint they
were standing in for.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
… on gaps

C-3 / H-10 / L-11.

Verification: tests/calculatorReferenceVectors.test.cjs replaces restatements of
the implementation's own arithmetic with vectors derived from the OPTN policy
text and evaluated longhand — the Rao xB=0 reference donor, each KDRI
coefficient in isolation, and the MELD/MELD-Na/MELD 3.0 equations transcribed
from OPTN Policy 9.1.D.

Defect found and fixed while sourcing: MELD 3.0 applied the adult intercept (6)
and the 1.33 female term to every candidate aged 12 and over. OPTN publishes a
distinct equation for candidates 12-17 with intercept 7.33 and no sex term.
age_years is now required because it selects the equation.

PELD: OPTN replaced PELD with PELD-Cr on 2023-07-13. The code implemented the
superseded pre-2023 equation. The per-term coefficients exist only in Table 9-1
of OPTN Policy 9.1.E, which is an image in the policy PDF; a secondary source
was found but contradicts OPTN's own narrative description. Rather than ship
guessed coefficients for a pediatric liver score, calculatePELD now fails closed
with REFERENCE_DATA_UNAVAILABLE naming the missing table. The superseded
equation survives only as calculatePELDLegacy2016, stamped superseded, for
historical reconciliation. Recorded as residual risk RR-01.

The PELD albumin floor of 1.0 that the report flagged for reconciliation is
confirmed CORRECT against OPTN Policy 9.1.E.

H-10: the KDPI median-KDRI scaling factor and both percentile maps move out of
code into provenanced reference tables. Every result now names the source
revision; an overdue review date flags the result stale, degrades the health
check and fails the build. A missing table produces no score rather than a
substituted one.

C-3 (LAS): the module presented an invented heuristic as the Lung Allocation
Score. Renamed to the TransTrack Lung Triage Index (TTLI), flagged
isPublishedInstrument: false on every result, and removed from ALL_FORMULAS as
'LAS'. The las_score column still stores a real LAS obtained from UNet.

L-11: KDPI rejects a zero donor age instead of extrapolating the Rao age spline.

Adds docs/compliance/CLINICAL_SOURCES.md, the controlled source register that
finding C-3 said did not exist.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
The plain-Node suites stubbed electron.app.getPath() with a directory inside
tests/, so running them wrote SQLite databases, logs and key material into the
repository working tree. services.test.cjs never removed its directory at all,
and a suite that failed part-way left the others behind too.

Adds scripts/test-temp-dir.cjs, which allocates the directory under the OS temp
dir and removes it on process exit — which also covers an uncaught exception and
an explicit process.exit() — plus SIGINT/SIGTERM/SIGHUP for cancelled CI jobs.

phiLeakage.test.cjs pointed userData at tests/ for the same reason and now uses
the helper, which is also what lets its redaction tests execute the real logger.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
scripts/production-audit.mjs carried a hardcoded ALLOWED set of GHSA ids and was
what CI actually ran; scripts/audit-with-exceptions.mjs read
security/vulnerability-exceptions.json and enforced reviewBy expiry, staleness
and severity-increase invalidation. Two sources of truth, and the CI path was
the one without the expiry check.

Removes production-audit.mjs. audit-with-exceptions.mjs gains --scope so the
server workspace (which has its own lockfile) is audited by the same gate
against the same allowlist file; an exception declares its scope and never
silently covers the other workspace.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
release:check reported "RELEASE GATE: PASSED - build is releasable for
first-customer pilot" and exited 0 on a tree with no installer and no signing
credentials, because the installer/signing/notarization gates were 'optional'
unless --for-sale or TRANSTRACK_RELEASE_CHANNEL=public was set.

Those gates are now mandatory by default, so the ordinary invocation blocks.
--allow-unsigned (or TRANSTRACK_ALLOW_UNSIGNED=1) is the explicit developer
escape: it waives them, reports NOT RELEASABLE rather than PASSED, lists the
unmet release requirements, and still exits non-zero (3) so no downstream
automation can read it as a green release gate. --for-sale refuses the waiver.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
FUNCTIONAL_SUITES listed 'apiClientParity.test.cjs', which is not and has never
been in tests/. The runner treats a listed-but-missing suite as a hard error, so
every group aborted before executing anything: 'node scripts/run-test-suites.cjs
core' exited immediately and 'npm test' with it. H-14's parity coverage belongs
with the other renderer tests under Vitest.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
localClient (8.6% lines) and remoteClient (5.3%) carry every PHI read and
write in the two deployment modes, and each wrapper is a one-line delegation
with nothing to catch a renamed IPC channel or endpoint. The new suites walk
every namespace, and pin the properties that are silent when they break: the
browser-dev mock refuses to fabricate a backup/notice/bundle receipt, the
access token stays out of localStorage, a 401 refreshes exactly once, and
clinical config is not written to browser storage in a production build.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
These were excluded from coverage on the grounds that Playwright covered them.
It does not: no e2e spec navigates to any of the five. They are now measured,
and each has a suite covering the rules a coordinator cannot recover from —
MFA cannot be removed without re-authentication, a declined offer must carry
an OPTN reason code, a deferred/declined donor must carry a reason and a
donation must carry the date the Policy 14 schedule is derived from, an HL7
message is not written to the database before it has been parsed and reviewed,
and no post-transplant record can be filed without a selected recipient.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
The compliance directory looked like a validation package and was not one.
The Validation Plan's status was "Template - to be ratified", the IQ, OQ and
PQ protocols were blanks with "_____" execution fields, the Validation Summary
Report was a template, and the only fully worked example was labelled
fictional. In parallel docs/VALIDATION_ARTIFACTS.md described a second, older
v1.0.0 package with empty results tables. No FMEA existed. A reader seeing an
IQ, an OQ and a PQ in a compliance/ directory would reasonably conclude the
system had been qualified. It had not.

A vendor cannot execute a site qualification, so the remediation is to state
precisely what is and is not qualified and make the distinction impossible to
miss:

- VALIDATION_PLAN.md ratified to v2.0, Approved and in force, with an
  effective date, approver role titles and scope bound to 1.3.0. Separates
  vendor release verification from site validation, and designates the server
  tier early access inside the compliance package rather than only in the
  README.
- executed/IQ_TT-IQ-001.md records what could genuinely be evidenced on
  Linux/Node 22: dependency install, native module build, lockfile integrity,
  schema and migration creation, file layout, SBOM tooling. Host-specific
  steps are marked NOT EXECUTED with the reason and the responsible party.
- executed/OQ_TT-OQ-001.md records the automated verification that actually
  ran: 106 test files, 1507 assertions, no failures. Every case cites a test
  file that exists. The interactive portion is marked NOT EXECUTED.
- executed/PQ_TT-PQ-001.md is marked NOT EXECUTED BY THE VENDOR, states why
  (no clinical users, no site data, no site environment), and supplies the
  protocol the deploying organisation runs.
- VALIDATION_SUMMARY_REPORT.md is the cover document and says plainly which
  stages are complete and which are not.
- FMEA.md analyses 30 failure modes drawn from this system's behaviour, with
  severity, occurrence, detection, RPN and required action, cross-referenced
  to RISK_REGISTER.md.
- RESIDUAL_RISK.md carries sixteen formal residual-risk statements, each with
  affected findings, acceptance rationale, compensating controls, accepting
  role and closure criteria.
- docs/VALIDATION_ARTIFACTS.md is withdrawn and now carries only a superseding
  notice. Two packages of different vintage is worse than one honest package.

compliance/README.md is reindexed around the executed package, withdraws the
unsupported AATB claim, and states the server tier's early-access status,
which the review noted was absent from the compliance documentation (M-17).

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
… exist (I-7, M-17)

scripts/check-compliance-docs.mjs enforces requirement-to-matrix consistency
but does not verify that the test files the matrix cites exist on disk, so a
citation naming no file reads identically on the page to a real one. Every
path in the Implementation and Verification columns was checked against the
filesystem.

Four verification citations named no file:

  tests/auth.test.cjs           -> tests/ipc-integration.test.cjs,
                                   tests/compliance.test.cjs
  tests/passwordPolicy.test.cjs -> tests/business-logic.test.cjs,
                                   tests/passwordHistory.test.cjs
  tests/siem.test.cjs           -> tests/siemForwarder.test.cjs,
                                   tests/siemRedaction.test.cjs
  tests/livingDonor.test.cjs    -> tests/livingDonors.test.cjs

Four implementation paths were also stale:

  electron/services/passwordPolicy.cjs    -> electron/ipc/shared.cjs,
                                             electron/services/passwordHistory.cjs
  electron/services/priorityWeighting.cjs -> the module that implements it
  electron/services/livingDonor.cjs       -> electron/services/livingDonors.cjs
  electron/ipc/handlers/livingDonor.cjs   -> electron/ipc/handlers/livingDonors.cjs

TT-R010 (single sign-on) was marked "Not implemented - deferred beyond 1.2.1"
while electron/auth/oidcDesktop.cjs and electron/ipc/handlers/auth.cjs ship
OIDC desktop SSO with mandatory PKCE S256. The row now states what is
implemented (OIDC) and what is not (SAML on the desktop; SAML exists only in
the early-access server tier), and cites tests/oidcDesktop.test.cjs. A
traceability matrix that misstates implementation status is itself a
validation defect.

The gate should check file existence; that change belongs to scripts/, which
this matrix does not own.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
… shipped system (M-17)

Twelve places where the documentation described a different system from the
one that ships. Each is corrected against the source.

HIPAA posture. DUE_DILIGENCE.md claimed a "HIPAA-compliant desktop
application" while README.md correctly said the opposite. HIPAA compliance is
a determination an organisation makes about itself, its workforce, its
policies and its physical environment; it cannot be an attribute of software.
DUE_DILIGENCE now matches the README.

Network dependencies. The same document claimed the system "operates entirely
on-premises with no external network dependencies", contradicted by the
Fastify server tier, the optional remote log sink (SENTRY_DSN /
TRANSTRACK_REMOTE_LOG_URL), the SIEM forwarder and GitHub Releases
auto-update. Every egress path is now enumerated with its default state and
what crosses the boundary, in DUE_DILIGENCE.md, README.md and COMPLIANCE.md.

AATB. The claim of design and validation against AATB standards is withdrawn
from DUE_DILIGENCE.md, COMPLIANCE.md, HIPAA_COMPLIANCE_MATRIX.md and
GITHUB_SETUP.md. No AATB control mapping ever existed behind it, and
TransTrack is a solid-organ tool rather than a tissue-bank system. Removing an
unsupported claim is preferable to constructing a mapping to justify it.
package.json still carries an "AATB" keyword and the application UI still
asserts AATB alignment; neither is owned here and both are reported.

Part 11 signatures. PART_11_CONTROL_MAPPING.md stated that TransTrack "does
not implement electronic signatures" while electron/services/
electronicSignature.cjs had been implementing signRecord(). Sections 11.50,
11.70, 11.100 and 11.200 now describe what exists - an application-level
signature record binding signer identity, declared meaning, a SHA-256 payload
hash and a timestamp, immutable at the trigger level and tamper-evident by
recomputation - and say equally plainly what it is not: no key pair, no
certificate, no non-repudiation against the system operator, and no
re-authentication at the point of signing, so 11.200(a)(1)(i) is not literally
met. Recorded as RR-13.

Installers. README.md listed filenames at version 1.0.0 and did not reflect
that electron-builder.enterprise.json produces TransTrack-Enterprise-
${version}. The table now gives both build configurations as patterns and
notes that signing credentials are not yet procured (RR-10).

Recovery objectives. DISASTER_RECOVERY.md stated RPO = 1 hour while
policies/BUSINESS_CONTINUITY_AND_DR.md stated 24 hours. Two authoritative
objectives for one system is itself a defect. Reconciled to <= 24 hours, which
is what the product delivers unaided - electron/services/disasterRecovery.cjs
schedules automated backups at autoBackupIntervalHours: 24, so an hourly RPO
was never achievable from the application alone. The BCDR policy is now
normative for the objectives and the DR document is procedural and defers to
it.

PHI egress and secure delete. README.md claimed no PHI leaves the local system
unless exported; qualified as a default rather than a structural guarantee. It
also presented multi-pass secure delete as a guarantee while
electron/services/secureDelete.cjs documents that it is ineffective on SSD,
copy-on-write and snapshotted volumes. The README now says the same, and
points at full-disk encryption plus cryptographic erase as the effective
control (RR-08).

Calculators. The list advertised "LAS". That module is the TransTrack Lung
Triage Index, an internal expert-set instrument that is neither the OPTN LAS
nor the Composite Allocation Score, flagged isPublishedInstrument: false
(RR-07). The README and COMPLIANCE.md now say so, record that PELD is
unavailable pending a verifiable OPTN Table 9-1 source (RR-01), and flag the
KDPI and EPTS percentile maps as approximations (RR-03).

Server tier maturity. Its early-access status appeared in the README but not
in the compliance documentation. It is now stated in the README's validation
status section as well.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
…d escalation path (L-13, M-17)

The sole security-disclosure and support contact across the product was a
consumer webmail address. A vulnerability reporter had no role-based endpoint,
no stated response time, and no route past an unresponsive individual.

SECURITY.md now defines:

- security@transtrack.example for vulnerability disclosure and suspected PHI
  incidents, and support@transtrack.example for non-security support, both
  described as group addresses delivered to a role plus a deputy rather than
  to one person;
- response service levels per stage - acknowledgement in 2 business days,
  triage in 5, status updates every 10, and fix or documented mitigation in 7
  / 30 / 90 days by severity, with severity assigned on CVSS v3.1 adjusted
  upward where PHI confidentiality, audit-trail integrity or a clinical
  calculation is affected;
- a four-step escalation path from Information Security Officer through
  Engineering Lead and Quality Assurance Officer to Privacy Officer, with 5
  business days at each step, and a note that a covered entity's own 60-day
  breach-notification deadline is not displaced by any vendor timeline;
- coordinated disclosure terms.

The addresses are placeholders on the reserved .example domain and are not yet
provisioned. That is stated wherever they appear rather than implied, and
provisioning a monitored role address with an on-call rotation behind it is
recorded as a commercial-release prerequisite in RR-15. Until then reporters
are directed to GitHub private vulnerability reporting.

Also in SECURITY.md: the supported-version matrix listed only 1.0.x while the
product shipped 1.2.1 (M-17); it now covers 1.3.x current, 1.2.x maintenance
and the end-of-life lines. The AATB conformance claim is withdrawn (M-17). A
network-egress section enumerates the five optional egress paths with their
defaults and the tests that verify redaction. The "LAS 0-100" range in the
threat table is corrected.

The remaining contact points in the docs tree are updated to the role-based
addresses. LICENSE, TRADEMARK.md, CODE_OF_CONDUCT.md, .github/
SECURITY_ADVISORY_2026-05-08.md, marketing/ and src/components/
ErrorBoundary.jsx still carry the webmail address and are not owned here.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
It was a Docker Compose smoke-test procedure for the server tier. The desktop
procedures a regulated deployment actually depends on - backup, restore, key
rotation, breach notification, DR drills - lived in other documents and were
not reachable from it, so an operator following the runbook would never find
them.

The runbook now:

- indexes every operational procedure against its controlling document, and
  states which document is normative where two cover the same ground;
- gives an operating cadence table naming the evidence each recurring control
  requires, on the principle that an activity without a record does not
  satisfy the control;
- documents the startup health checks (encryption verification, audit-chain
  verification, migration status, licence) and treats each failure as a stop
  condition, with chain-verification failure routed to incident response
  rather than to routine triage;
- carries a disaster recovery drill procedure and log template. The procedure
  deliberately requires the most recent routine backup rather than one made
  for the drill, and requires recovering the key from key backup rather than
  from the production host, because a drill that skips either tests nothing
  about the path that will be used in a real recovery;
- ends with a known-limitations table linking each operational constraint to
  its residual-risk entry, so an operator meets them here rather than during
  an incident.

The drill log is empty and says why. The BCDR policy mandates quarterly
restore drills and none has been executed against this release by the vendor
or by any site, so the recovery time objective is a design target rather than
a demonstrated capability. Recorded as RR-11 and made a precondition of PQ.

The original Docker smoke test is retained as section 7, marked as an
evaluation rather than a production procedure, with a note that a passing
smoke test exercises reachability rather than correctness against
requirements and is not qualification evidence.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
sample-data/ and demo-evidence/ contain synthetic and Epic-sandbox records.
They are appropriately fictional, but nothing in the repository recorded that,
leaving a reviewer to infer it from the data. Inference is not evidence.

docs/TEST_DATA_PROVENANCE.md inventories each location - the 43-entry FHIR
bundle authored for this project, the Epic sandbox round-trip transcript whose
subject is Epic's publicly documented test patient, the fictional pilot-site
example, the in-code test fixtures, and the OPTN coefficient tables - and
states for each where it came from and that it contains no real PHI.

It also explains why Epic sandbox records are not PHI (fabricated by Epic,
published for integration testing, no BAA required) and where that stops: a
transcript captured against a customer's live Epic instance would contain PHI,
and the FHIR base URL is the gate. Contributor rules follow, including that
manual redaction of a production transcript is not an acceptable substitute
for regenerating it against the sandbox, and that a fixture directory absent
from the inventory has no evidenced provenance.

The document is explicit that phiLeakage and loggerRedaction constrain the
running application and cannot prove the absence of real PHI in a committed
fixture; that assurance rests on the inventory and the contributor rules.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
…L-12)

The regulated product repository tracked docs/STRATEGIC_FIT.md, an M&A and
partnership positioning brief naming a prospective acquirer, and
docs/legal/COMMERCIALIZATION_CHECKLIST.md, a commercialisation plan with
indicative pricing, named vendor shortlists and sales outreach templates. Both
are deleted.

The reason is not that the material was wrong or secret. A regulated product
repository is a controlled-document set: everything in it is potentially in
scope for a validation review, an audit or a discovery request, and every file
in it carries an implicit claim to be current and controlled. Commercial
planning documents change on a sales cadence rather than a release cadence,
are owned outside engineering, and are governed by no change-control
procedure. Keeping them here mixes two document sets with different owners and
different audiences, and invites an auditor to read a pricing sheet as though
it were a controlled specification.

docs/legal/README.md indexes the genuinely product-relevant legal documents -
LICENSE, LEGAL_NOTICE.md, TRADEMARK.md, the BAA material, LICENSING.md - and
records the removal and the rule for future additions: a document belongs here
only if a deploying organisation, an auditor or a regulator would need it to
install, operate, validate or lawfully use the software.

Nothing product-relevant was lost. The items that mattered - code-signing
credentials not yet procured, no third-party penetration test performed, no
provisioned vendor domain - are now formal residual risks (RR-09, RR-10,
RR-15) with named owners and closure criteria, which is stronger than a
progress note in a checklist.

References in HECVAT_PREFILL.md and PILOT_DEPLOYMENT_RUNBOOK.md are
redirected. CRITICAL_ACTIONS_REQUIRED.md at the repository root now contains a
dangling link to the deleted checklist and is not owned here; it is reported
for the owning agent to remove.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
There was no CODEOWNERS file and no documented branch protection or mandatory
review, so nothing in the repository established that a change to a security
or clinical control had been seen by anyone qualified to assess it.

.github/CODEOWNERS assigns mandatory reviewers to the paths where a defect is
not recoverable by a later patch: the Electron IPC boundary and preload, the
database schema and migrations, authentication and SSO, the audit chain and
HMAC key handling, encryption and secure delete, the logger and SIEM
forwarder, the clinical calculators and their reference data, the server-tier
FHIR, SMART and auth layers, the controlled documents under docs/compliance/,
the release and signing scripts, and the test suites that verify all of the
above. Team handles are placeholders and the file says so, because an
unresolvable handle matches nobody and the protection rule then passes
silently.

CONTRIBUTING.md documents the required branch-protection configuration for
main - two approvals on security- and clinically-owned paths and one
elsewhere, stale-approval dismissal, code-owner review, conversation
resolution, signed commits, linear history, administrators included, no force
push - and lists the ten required status checks by their workflow job names.
This is recorded in the repository because a protection rule that exists only
in a GitHub setting is invisible to a validation reviewer and is lost on a
fork or migration. Deviations are change-control exceptions under the change
management SOP.

The document states honestly that whether these settings are currently applied
cannot be evidenced from within the repository, and gives the gh api command
to confirm the live rule.

The compliance section is also expanded: no PHI in fixtures with a pointer to
the provenance document, no second audit write path, no unauthorised route
without an explicit authorisation check, no calculator constant change without
a controlled source, and a requirement to fix documentation in the same pull
request that makes it inaccurate - which is the failure mode finding M-17
recorded.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
Adds component tests for src/lib/AuthContext.jsx and
src/components/session/IdleTimeoutManager.jsx, both previously at 0%.

Testing the idle manager with a controlled clock surfaced a real defect:
handleActivity depends on showWarning and the listener effect depends on
handleActivity, so setting showWarning re-runs the effect, which calls
resetTimers() and clears the warning and re-arms the 15-minute logoff. The
dialog therefore mounts for a single commit and the automatic logoff never
fires. Measured with real timers against a 400ms/250ms policy: logout is still
uncalled after nearly three idle periods. The fix is in the component, which
this change does not own, so the required behaviour is pinned with it.fails and
documented in the test file.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
Secret scanning: adds scripts/scan-secrets.mjs, a dependency-free scanner with
14 rules, an allowlist for the eight historical exposures in git history, and a
--self-test mode that fails if any rule has stopped matching its own sample. The
.gitignore has referenced gitleaks-report.json for months with no workflow to
produce it; a committed scanner cannot silently no-op the way a missing action
can. Wired into security.yml on push and PR (working tree) and weekly (full
history), with a commit status that treats anything other than success as
failure.

Removes the escapes that made the other gates decorative:
  - Snyk no longer runs with continue-on-error, and the status job no longer
    maps a skipped scan to success. With no SNYK_TOKEN the job runs the
    committed audit gate at the same high+ threshold over both workspaces, so a
    green snyk status always means a scan ran.
  - The server audit runs scripts/audit-with-exceptions.mjs --scope=server
    instead of npm audit ... || true.
  - The lockfile job fails when either lockfile is missing instead of emitting
    a warning, and verifies the server clean install too.
  - The load-test job runs the suite runner's performance group.

Dependabot: open-pull-requests-limit was 0 in all three ecosystems, which
disables it including security updates. Re-enabled with limits of 5/5/3, minor
and patch grouped into one PR per ecosystem, security updates in their own
group, majors and the native/Electron stack still excluded from automation.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
cursoragent and others added 11 commits August 2, 2026 22:17
…anels

Adds component tests for the two post-login blocking screens
(ForceMfaEnrollment, ForcePasswordChange), the CSV roster import in
pages/Patients.jsx, and the lab and readiness-barrier panels with their status
badges. All were at 0% coverage except Patients.jsx, which was at 23% with the
whole import path unmeasured.

The import path is the highest-risk code among these: it writes patient records
in bulk from a user-chosen file and decides row by row what to skip. The tests
pin that every rejected row is reported with its file row number, that a
non-numeric MELD is never coerced into a record, that a partial import is
reported as partial, and that a stale success banner cannot survive into the
next attempt.

Recorded while writing these: the delete confirmation dialog in
ReadinessBarrierList is unreachable (nothing calls setDeleteConfirm), and the
three password fields in ForcePasswordChange have labels with no htmlFor, so
they are not programmatically associated for a screen reader. Both are noted in
the test files.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
The gate was inline in ci.yml with hardMin = 19 for lines and branches; the 60%
target only emitted a ::warning::, which cannot fail a build. Five IPC-bound PHI
pages were also excluded from measurement, so 19% was measured against a
flattering denominator.

  - Floors now live in scripts/coverage-floor.json and are read by both
    vite.config.js (as Vitest thresholds, so a local --coverage run enforces
    what CI enforces) and scripts/coverage-gate.mjs. Enforced: lines 57,
    statements 57, functions 59, branches 43, plus per-file floors on 22 files
    covering every PHI-handling screen and the whole data-access layer.
  - The gate script adds a ratchet: coverage more than 8 points above a floor
    fails with an instruction to raise it, so the floors cannot drift behind
    reality and can only move up.
  - ci.yml now runs the runner's "all" group (every Node suite, including the
    ones previously reachable only through bespoke npm scripts) instead of
    "npm test", whose pretest/posttest hooks rebuild the native module for
    Electron and leave the wrong ABI in place for the steps that follow.
  - Replaces the deleted scripts/production-audit.mjs reference with the single
    exception-checked audit gate, drops the "|| true" from the server audit and
    continue-on-error from the packaged-native verification (M-18, M-19).
  - Adds a ci-required aggregate job so adding a job to this workflow blocks a
    merge without also editing the branch protection rule.

Measured: lines 19.35% to 58.63%, branches 20.31% to 44.54%, with the five PHI
pages back in the denominator.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
…repping for them

The encryption check in this suite was already converted to a runtime test. The
three checks left behind read electron/ipc/shared.cjs and asserted that the
strings 'minLength: 12', 'SESSION_DURATION_MS' and 'MAX_LOGIN_ATTEMPTS' appear
in it, which a commented-out constant or one that is defined and never consulted
also satisfies.

They now call the exported validatePasswordStrength across five rejection cases
plus an empty credential, and read the exported SESSION_DURATION_MS,
IDLE_TIMEOUT_MS, MAX_LOGIN_ATTEMPTS and LOCKOUT_DURATION_MS, bounding each
against the policy the compliance matrix claims.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
The snyk job carried "if: github.event_name != 'schedule'", which skipped it on
the weekly run — the one run whose purpose is to catch an advisory published
against code that has not changed. With the status job no longer treating a
skipped scan as a passing one, leaving the condition in place would also have
reported a failure every Monday.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
retries and workers were configured but forbidOnly was not, so a `test.only`
left in a spec would reduce the Playwright job to a single test and still exit 0.
That is the same failure shape as the soft assertions in this finding, one level
up: a green check that covers almost nothing.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
…evel (H-4, M-28)

The compartment suite covers requireSmartScope and the storage guards in
isolation, but nothing exercised POST /fhir itself, so the properties that only
exist at the route level were unverified: that every entry is authorised before
any entry executes, that a mixed allowed/denied bundle commits nothing, and
that the entry cap and method/type validation hold.

16 tests through a real Fastify instance. 9 of them fail against the
pre-remediation route.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
- Mount BulkPhiAccessGate for list/filter PHI grant prompts (H-1).
- Purge PHI caches and session storage on logout (H-13).
- Redact patient-id path segments in NavigationTracker (H-13).
- Fix IdleTimeoutManager so warning/auto-logoff timers actually fire.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
- Restore featureGate/tiers and wire session entitlement + write gates (H-6).
- Refuse packaged builds that still embed the DEV publisher key (H-7).
- Persist trial high-water clock so delete/rollback cannot reset trial (M-21).
- Fail loudly for remote functions.invoke and unsupported entities (H-14).
- Render clinical dates in UTC with an explicit marker (M-24).
- Scrub unsupported AATB product claims; add finding remediation map.

Co-authored-by: NeuroKoder3 <NeuroKoder3@users.noreply.github.com>
Unblocks Desktop Build and Security Scanning CI that failed on eqeqeq.

Co-authored-by: Cursor <cursoragent@cursor.com>
Restricted sessions reject a second auth:login; complete password/MFA setup on the existing session instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
Ruleset expects context 'build' but the job displays as Desktop Build & Tests; mirror the audit/snyk status reporter.

Co-authored-by: Cursor <cursoragent@cursor.com>
@NeuroKoder3
NeuroKoder3 marked this pull request as ready for review August 3, 2026 00:44
@strix-security

strix-security Bot commented Aug 3, 2026

Copy link
Copy Markdown

Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here.

@NeuroKoder3
NeuroKoder3 merged commit 0a92b8c into main Aug 3, 2026
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants