Expose transaction_aml_check.amlResponsible on /gs/debug - #4393
Conversation
The structured debug endpoint could show that an amlCheck transition happened and which code path caused it, but never who approved it: amlResponsible was blocked by the startup invariant that forbids any overlap with the /gs/db masking list. That left "who released this transaction" unanswerable from the audit trail. Allowlist the column and introduce DebugRestrictedOverlapExceptions, an explicit registry of (table, column) pairs that may overlap. Every unregistered overlap still aborts module load, and a counter-check rejects stale entries so the list cannot rot into a silent gap. /gs/db masking is untouched, and `comment` on the same table stays blocked. Extract the invariant into the pure, exported assertDebugAllowlistInvariants so the guard itself is testable: asserting the real constants only proves today's data is consistent, not that the exception matches per column rather than per table. Covered by synthetic fixtures, including the case where excepting one column must not amnesty a second overlap in the same table.
|
Pre-review completed in 2 rounds before this PR was opened, with two independent reviewers per Round 1 — 1 finding (MEDIUM): the new exception logic was not covered in isolation. The Fix: the invariant was extracted into the pure, exported Round 2 — 0 findings from both reviewers on the fixed diff. One open argument from round 2 was closed separately: whether the newly visible value could serve Local verification: 353/353 in |
Why
transaction_aml_checkis the audit trail behind everyamlChecktransition. On the structured/gs/debugendpoint it could answer when a transition happened and which code path caused it(
AmlSourceType), but never who approved it —amlResponsiblewas unreachable.That column was blocked by the startup invariant forbidding any overlap between
DebugAllowedColumnsandGsRestrictedColumns(the per-role masking list/gs/dbapplies).The invariant is sound as a default, but it made "who released this transaction" unanswerable
from the audit trail, which is exactly what AML forensics needs.
What
transaction_aml_check.amlResponsibleis now allowlisted for/gs/debug.DebugRestrictedOverlapExceptions: an explicit registry of(table, column)pairs that mayoverlap the masking list. The invariant now permits only registered pairs; every other
overlap still aborts module load, so a future addition still cannot slip in silently.
registry cannot rot into a silent gap.
assertDebugAllowlistInvariants(...)and is coveredby synthetic fixtures.
Scope of the exposure
/gs/dbmasking is unchanged.GsRestrictedColumnsis untouched, so callers belowSuperAdminstill see[RESTRICTED]there.commenton the same table stays blocked — free-form internal note text, no exception entry,invariant still rejects it.
/gs/debugrole gate:Debug, plusAdmin/SuperAdminvia the rolehierarchy in
role.guard.ts.executeDebugQueryaudit-logs caller address and query.GROUP BY/ORDER BYresolve only allowlisted columns or selectaliases (
emitDebugGroupIdentemits ordinals to sidestep Postgres name resolution), so thenewly visible value cannot be used as an oracle onto columns that remain blocked.
Why the guard was extracted
Asserting the real constants only proves today's data is consistent. It cannot show that the
exception matches per column rather than per table — with the current data there is no
second overlap left to expose such a bug. If the check were ever loosened to table level, a later
accidental re-add of
commentwould pass silently. The fixtures drive the guard directly,including the case where excepting one column must not amnesty a second overlap in the same table.
Tests
gs.service.spec.ts: 353 passing (8 new fixture tests for the guard, 4 for the exposure itself).The existing "rejects sensitive column" matrix keeps
transaction_aml_check.comment;amlResponsiblewas removed from it deliberately and replaced by dedicated coverage.Review notes
This deliberately widens what a
Debug-role caller can read. The security argument is the rolegate plus the audit log, not obscurity — worth a conscious call from the compliance side rather
than a routine approval.