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.
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.
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.
ID: finserv-audit-trail.transaction-logging.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-logging for comprehensive action coverage, and finserv-audit-trail.transaction-logging.append-only-enforcement for immutability enforcement details.
Remediation: Create a dedicated, append-only transaction log table in your database (e.g., db/schema/transaction_logs.sql or prisma/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
}