Fee schedules and interest rate changes are decisions that directly affect customer balances — and they are exactly the kind of discretionary administrative action that regulators and auditors scrutinize most carefully. SOX §404 requires that internal controls over financial reporting extend to the processes that determine what rates and fees are applied; an admin who can change a fee schedule without leaving a before/after record defeats that control. NIST 800-53 AU-2 requires auditing of configuration changes. PCI-DSS 4.0 Req-10.2.1.5 explicitly requires logging of changes to identification and authentication mechanisms as well as to system configurations. CWE-778 (Insufficient Logging) applies directly. Without a config_change_logs record that captures who changed the fee, from what value to what value, and when, there is no way to reconstruct whether a customer was charged the correct rate.
Low because config-change logging is a detective control — its absence enables undetected rate manipulation but requires a second failure (the admin using the ungated path maliciously) to result in direct harm.
Create a config_change_logs table and write a record before every config mutation in src/app/api/admin/config/route.ts or src/services/config-audit.ts:
CREATE TABLE config_change_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
config_key VARCHAR(100) NOT NULL,
old_value JSONB NOT NULL,
new_value JSONB NOT NULL,
changed_by UUID NOT NULL REFERENCES users(id),
changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
reason TEXT
);
async function updateFeeSchedule(
newSchedule: FeeSchedule, adminId: string, reason: string
) {
const current = await db('config').where({ key: 'fee_schedule' }).first();
await db('config_change_logs').insert({
config_key: 'fee_schedule',
old_value: JSON.stringify(current.value),
new_value: JSON.stringify(newSchedule),
changed_by: adminId,
reason
});
await db('config').where({ key: 'fee_schedule' }).update({ value: newSchedule });
}
ID: finserv-audit-trail.retention-compliance.config-change-logging
Severity: low
What to look for: Enumerate all admin endpoints that modify system configuration (fee schedules, interest rates, exchange rates, limits). Count every config-modifying endpoint and verify each one writes a log entry with at least 4 fields: old value, new value, timestamp, and admin user ID. Quote the actual config keys being tracked. An admin endpoint that modifies a fee schedule without logging the previous value does not count as pass.
Pass criteria: At least 90% of configuration-modifying endpoints log changes with at least 4 fields: old value, new value, timestamp, and changed_by user ID. Count all config types — report the ratio even on pass (e.g., "3 of 3 config endpoints logged: fee_schedule, interest_rate, daily_limit").
Fail criteria: Configuration changes are not logged, or logs lack before/after values, or fewer than 90% of config-modifying endpoints include audit entries.
Skip (N/A) when: Never — config changes are regulatory events.
Detail on fail: "Fee schedules changed via /api/admin/config but 0 of 3 config changes are logged." or "Config logs exist but include only new_value — old_value missing in 3 of 3 entries.".
Remediation: Add config change logging (in src/services/config-audit.ts or src/app/api/admin/config/route.ts):
async function updateFeeSchedule(newSchedule, adminUserId) {
const oldSchedule = await db.config.findOne({ key: 'fee_schedule' });
// Log the change
await db.configChangeLogs.create({
configKey: 'fee_schedule',
oldValue: oldSchedule.value,
newValue: newSchedule,
changedBy: adminUserId,
changedAt: new Date(),
reason: req.body.reason || null
});
// Apply change
await db.config.updateOne({ key: 'fee_schedule' }, { value: newSchedule });
}