All financial transactions logged immutably with UTC timestamp, user ID, operation type
Why it matters
Missing or mutable transaction logs are the single largest SOX §404 finding class in financial technology audits. Without an append-only record of every transfer, withdrawal, deposit, and adjustment — each stamped with a UTC timestamp, user ID, and operation type — you cannot reconstruct the sequence of events that produced a balance, respond to a regulatory examination, or prove to an auditor that your ledger is complete. PCI-DSS 4.0 Req-10.2 mandates logging of all individual user access to cardholder data and all financial transactions. NIST 800-53 AU-2 requires that the organization determine which events are auditable; AU-3 specifies the content those records must contain (user identity, time, type of event, and outcome). CWE-778 (Insufficient Logging) is the direct vulnerability class — an adversary who moves funds without triggering a log entry leaves no forensic trail to detect or prosecute the fraud.
Severity rationale
Critical because gaps in transaction logging create undetectable embezzlement vectors and directly trigger SOX §404 material-weakness findings that can halt a public offering or trigger SEC enforcement.
Remediation
Create a dedicated append-only transaction_logs table and instrument every financial endpoint to write a record before returning a success response. Place the DDL in db/migrations/ or prisma/schema.prisma:
CREATE TABLE transaction_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() AT TIME ZONE 'UTC',
user_id UUID NOT NULL REFERENCES users(id),
operation_type VARCHAR(50) NOT NULL,
amount DECIMAL(19,4),
currency CHAR(3),
details JSONB
);
REVOKE UPDATE, DELETE ON transaction_logs FROM app_role;
GRANT INSERT, SELECT ON transaction_logs TO app_role;
In your service layer, log before the side-effectful operation commits so a crash never produces an unlogged transaction.
Detection
-
ID:
transaction-immutable-logging -
Severity:
critical -
What to look for: Enumerate all financial operation endpoints (transfers, withdrawals, deposits, adjustments, cancellations, reversals, fee changes) and count every endpoint that writes to the audit/transaction log table. Examine database schema for a dedicated audit/transaction log table with at least 3 required columns: UTC timestamp, user identifier, and operation type. Quote the exact column names found. Look for INSERT-only queries (no UPDATE, no DELETE) or append-only table design. A table that stores transaction data but allows UPDATE or DELETE does not count as pass — do not pass if any mutation path exists.
-
Pass criteria: A transaction log table exists with at least 3 columns for timestamp (UTC), user ID, and operation type. Count all financial transaction endpoints — at least 90% must contain calls to log transactions immediately upon execution. Report the count even on pass (e.g., "6 of 7 endpoints log to audit table"). Logging is INSERT-only with no modification mechanism found in at least 5 code paths examined.
-
Fail criteria: No audit log table found, or fewer than 90% of financial transaction endpoints contain logging calls, or the audit log is mutable (allows UPDATE/DELETE).
-
Skip (N/A) when: Never — every financial application must log transactions.
-
Detail on fail: Name the transaction types that lack logging (e.g.,
"Transfers logged but withdrawals are not — 3 of 5 endpoints missing logging"), or specify that the audit log table allows modifications ("audit_logs table has UPDATE and DELETE permissions — not append-only"). -
Cross-reference: Check
finserv-audit-trail.retention-compliance.complete-action-loggingfor comprehensive action coverage, andfinserv-audit-trail.transaction-logging.append-only-enforcementfor immutability enforcement details. -
Remediation: Create a dedicated, append-only transaction log table in your database (e.g.,
db/schema/transaction_logs.sqlorprisma/schema.prisma):CREATE TABLE transaction_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), timestamp TIMESTAMP NOT NULL DEFAULT NOW() AT TIME ZONE 'UTC', user_id UUID NOT NULL, operation_type VARCHAR(50) NOT NULL, -- 'transfer', 'withdraw', 'deposit', 'adjustment' amount DECIMAL(19, 2), currency VARCHAR(3), details JSONB, created_at TIMESTAMP NOT NULL DEFAULT NOW() AT TIME ZONE 'UTC' ); -- Append-only: never allow UPDATE or DELETE REVOKE UPDATE, DELETE ON transaction_logs FROM app_role; GRANT INSERT, SELECT ON transaction_logs TO app_role;Then, log every financial operation immediately:
async function executeTransfer(userId, toAccount, amount) { await db.transactionLogs.create({ timestamp: new Date(Date.now()).toISOString(), userId, operationType: 'transfer', amount, details: { toAccount } }); // ... execute transfer }
External references
- cwe · CWE-778 — Insufficient Logging
- nist:rev5 · AU-2 — Event Logging
- nist:rev5 · AU-3 — Content of Audit Records
- sox · Section 404 — Management Assessment of Internal Controls
- pci-dss:4.0 · Req-10.2 — Audit log events for cardholder data environment
Taxons
History
- 2026-04-18·v1.0.0·Initial import from finserv-audit-trail·automated