security: round 3 hardening (CSRF double-submit, TRX MITM, container hardening)
This commit is contained in:
75
apps/api/src/lib/audit-log.ts
Normal file
75
apps/api/src/lib/audit-log.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Audit log — append-only JSON lines в `logs/audit.log`.
|
||||
* Используется для критических custodial операций.
|
||||
* НИКОГДА не логирует mnemonic / privkey / encrypted blob.
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { logger } from './logger';
|
||||
import { getTraceId } from './trace-store';
|
||||
|
||||
const AUDIT_DIR = path.resolve(__dirname, '../../../../logs');
|
||||
const AUDIT_FILE = path.join(AUDIT_DIR, 'audit.log');
|
||||
|
||||
let initialized = false;
|
||||
|
||||
async function ensureFile(): Promise<void> {
|
||||
if (initialized) return;
|
||||
try {
|
||||
await fs.mkdir(AUDIT_DIR, { recursive: true });
|
||||
const handle = await fs.open(AUDIT_FILE, 'a', 0o600);
|
||||
await handle.close();
|
||||
try {
|
||||
await fs.chmod(AUDIT_FILE, 0o600);
|
||||
} catch {
|
||||
// Windows chmod — игнор
|
||||
}
|
||||
initialized = true;
|
||||
} catch (err: any) {
|
||||
logger.error(`Audit log init failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export interface AuditEntry {
|
||||
event: string;
|
||||
userId: string;
|
||||
ip?: string | null;
|
||||
meta?: Record<string, unknown>;
|
||||
result?: 'success' | 'failure';
|
||||
errorCode?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort write. Если запись провалилась — только log, не throws.
|
||||
* Используется для не-критических событий (wallet.create success, etc).
|
||||
*/
|
||||
export async function auditLog(entry: AuditEntry): Promise<void> {
|
||||
await ensureFile();
|
||||
const line = JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
trace_id: getTraceId(),
|
||||
...entry,
|
||||
});
|
||||
try {
|
||||
await fs.appendFile(AUDIT_FILE, line + '\n', { mode: 0o600 });
|
||||
} catch (err: any) {
|
||||
logger.error(`Audit log write failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fail-secure write. Если запись провалилась — throws.
|
||||
* Используется для critical security событий (mnemonic.reveal, wallet.send),
|
||||
* где compliance требует чтобы операция НЕ происходила без audit-trail.
|
||||
*/
|
||||
export async function auditLogStrict(entry: AuditEntry): Promise<void> {
|
||||
await ensureFile();
|
||||
const line = JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
trace_id: getTraceId(),
|
||||
...entry,
|
||||
});
|
||||
// Без try/catch — caller обрабатывает failure
|
||||
await fs.appendFile(AUDIT_FILE, line + '\n', { mode: 0o600 });
|
||||
}
|
||||
@@ -35,14 +35,14 @@ function getCallerInfo(): { file: string; line: number } {
|
||||
const SENSITIVE_PATTERNS: { regex: RegExp; replace: string }[] = [
|
||||
{ regex: /(password|passwd|pwd)\s*[:=]\s*\S+/gi, replace: '$1=***' },
|
||||
{ regex: /(token|secret|api[_-]?key|auth)\s*[:=]\s*\S+/gi, replace: '$1=***' },
|
||||
{ regex: /(role[_-]?id|secret[_-]?id)\s*[:=]\s*\S+/gi, replace: '$1=***' },
|
||||
{ regex: /(mnemonic|seed[_-]?phrase|private[_-]?key|priv[_-]?key)\s*[:=]\s*\S+/gi, replace: '$1=***' },
|
||||
{ regex: /\bhvs\.[A-Za-z0-9]+/g, replace: 'hvs.***' },
|
||||
{ regex: /\bs\.[A-Za-z0-9]{20,}/g, replace: 's.***' },
|
||||
{ regex: /Bearer\s+[A-Za-z0-9._-]+/gi, replace: 'Bearer ***' },
|
||||
{ regex: /Authorization:\s*\S+/gi, replace: 'Authorization: ***' },
|
||||
// BIP39 mnemonic phrase (12+ lowercase английских слов через пробел) — рискованный паттерн,
|
||||
// но лучше пере-санитайзить чем пропустить mnemonic в логи
|
||||
{ regex: /\b([a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/g, replace: '[REDACTED_MNEMONIC]' },
|
||||
// BIP39 mnemonic phrase (12-24 lowercase английских слов через пробел) — case-insensitive
|
||||
{ regex: /\b([a-z]{3,8}\s+){11,23}[a-z]{3,8}\b/gi, replace: '[REDACTED_MNEMONIC]' },
|
||||
// Hex privkey (64 hex chars подряд, optional 0x)
|
||||
{ regex: /\b(0x)?[0-9a-fA-F]{64}\b/g, replace: '[REDACTED_HEX64]' },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user