feat(audit): implement audit logging for search requests and results
All checks were successful
Publish FaceAI Container / publish (push) Successful in 13m22s

- Added configuration options for audit database path and retention days in backend and processor.
- Integrated audit logging in server and worker processes to track search requests, completions, and failures.
- Created utility functions for reading and parsing audit logs in end-to-end tests.
- Updated Docker Compose files to include audit database configuration.
- Added new tests to verify audit log entries for successful and no-results searches.
This commit is contained in:
MaddoScientisto 2026-05-19 23:29:38 +02:00
commit 32db61c381
14 changed files with 1067 additions and 16 deletions

View file

@ -1,11 +1,17 @@
const fs = require('node:fs/promises');
const fsSync = require('node:fs');
const path = require('node:path');
const { spawn } = require('node:child_process');
const { spawn, spawnSync } = require('node:child_process');
const { DatabaseSync } = require('node:sqlite');
const ROOT_DIR = path.resolve(__dirname, '..', '..');
const WORKSPACE_ROOT = path.resolve(ROOT_DIR, '..');
const LOG_ROOT = path.join(ROOT_DIR, 'logs');
const SEARCH_LOG_ROOT = path.join(LOG_ROOT, 'searches');
const AUDIT_DB_PATH = path.join(LOG_ROOT, 'faceai-audit.sqlite');
const PREFER_CONTAINER_AUDIT_DB = process.env.FACEAI_E2E_AUDIT_READ_FROM_CONTAINER === '1' || process.platform === 'win32';
const AUDIT_DB_PATH_IN_CONTAINER = process.env.FACEAI_E2E_AUDIT_DB_PATH_IN_CONTAINER || '/data/runtime/faceai-audit.sqlite';
const AUDIT_DB_QUERY_CONTAINER = process.env.FACEAI_E2E_AUDIT_QUERY_CONTAINER || 'regalami-faceai-processor';
const FACEAI_BASE_URL = process.env.FACEAI_E2E_BASE_URL || 'http://127.0.0.1:3001';
const SIMULATOR_URL = process.env.FACEAI_E2E_SIMULATOR_URL || 'http://127.0.0.1:8080/Foto2.abl?id_gara=1018547&pageRow=96&pageNumber=1';
const LEGACY_BASE_URL = process.env.FACEAI_E2E_LEGACY_BASE_URL || 'http://127.0.0.1:8080';
@ -260,11 +266,182 @@ function getSearchArtifacts(searchId) {
searchRoot,
backendLogPath: path.join(LOG_ROOT, 'backend.log'),
processorLogPath: path.join(LOG_ROOT, 'processor.log'),
auditDbPath: fsSync.existsSync(AUDIT_DB_PATH) ? AUDIT_DB_PATH : `${AUDIT_DB_QUERY_CONTAINER}:${AUDIT_DB_PATH_IN_CONTAINER}`,
workerLogPath: path.join(searchRoot, 'worker.log'),
matcherLogPath: path.join(searchRoot, 'matcher.log')
};
}
function parseAuditArtifacts(result) {
return {
searchRow: result.searchRow ? {
...result.searchRow,
matches: result.searchRow.matches_json ? JSON.parse(result.searchRow.matches_json) : null
} : null,
events: result.events.map((event) => ({
...event,
payload: event.payload_json ? JSON.parse(event.payload_json) : null
})),
fingerprintMatches: result.fingerprintMatches || []
};
}
function readAuditArtifactsFromContainer(searchId) {
const script = `
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(${JSON.stringify(AUDIT_DB_PATH_IN_CONTAINER)}, { readOnly: true });
try {
const searchId = ${JSON.stringify(String(searchId))};
const searchRow = db.prepare(\`
SELECT
search_id,
requested_at,
completed_at,
redirect_issued_at,
status,
completion_code,
error_code,
error_message,
user_id,
user_display_name,
user_membership_status,
race_id,
race_name,
race_storage,
lang,
request_ip,
request_user_agent,
return_url,
selfie_name,
selfie_sha256,
selfie_size_bytes,
upload_path,
result_id,
match_count,
matches_json
FROM faceai_audit_searches
WHERE search_id = ?
\`).get(searchId);
const events = db.prepare(\`
SELECT
event_type,
happened_at,
status,
user_id,
race_id,
selfie_sha256,
result_id,
match_count,
completion_code,
error_code,
payload_json
FROM faceai_audit_events
WHERE search_id = ?
ORDER BY happened_at ASC, id ASC
\`).all(searchId);
const fingerprintMatches = searchRow?.selfie_sha256
? db.prepare(\`
SELECT search_id, result_id, match_count, status
FROM faceai_audit_searches
WHERE selfie_sha256 = ?
ORDER BY requested_at DESC
\`).all(searchRow.selfie_sha256)
: [];
console.log(JSON.stringify({ searchRow, events, fingerprintMatches }));
} finally {
db.close();
}
`;
const result = spawnSync('docker', ['exec', AUDIT_DB_QUERY_CONTAINER, 'node', '-e', script], {
cwd: ROOT_DIR,
encoding: 'utf8'
});
if (result.status !== 0) {
const details = (result.stderr || result.stdout || '').trim();
throw new Error(`Failed to read audit DB from ${AUDIT_DB_QUERY_CONTAINER}:${AUDIT_DB_PATH_IN_CONTAINER}${details ? `\n${details}` : ''}`);
}
return parseAuditArtifacts(JSON.parse(result.stdout));
}
function readAuditArtifacts(searchId) {
if (PREFER_CONTAINER_AUDIT_DB || !fsSync.existsSync(AUDIT_DB_PATH)) {
return readAuditArtifactsFromContainer(searchId);
}
const db = new DatabaseSync(AUDIT_DB_PATH, { readOnly: true });
try {
const searchRow = db.prepare(`
SELECT
search_id,
requested_at,
completed_at,
redirect_issued_at,
status,
completion_code,
error_code,
error_message,
user_id,
user_display_name,
user_membership_status,
race_id,
race_name,
race_storage,
lang,
request_ip,
request_user_agent,
return_url,
selfie_name,
selfie_sha256,
selfie_size_bytes,
upload_path,
result_id,
match_count,
matches_json
FROM faceai_audit_searches
WHERE search_id = ?
`).get(String(searchId));
const events = db.prepare(`
SELECT
event_type,
happened_at,
status,
user_id,
race_id,
selfie_sha256,
result_id,
match_count,
completion_code,
error_code,
payload_json
FROM faceai_audit_events
WHERE search_id = ?
ORDER BY happened_at ASC, id ASC
`).all(String(searchId));
const fingerprintMatches = searchRow?.selfie_sha256
? db.prepare(`
SELECT search_id, result_id, match_count, status
FROM faceai_audit_searches
WHERE selfie_sha256 = ?
ORDER BY requested_at DESC
`).all(searchRow.selfie_sha256)
: [];
return parseAuditArtifacts({ searchRow, events, fingerprintMatches });
} finally {
db.close();
}
}
async function readUtf8(filePath) {
return fs.readFile(filePath, 'utf8');
}
@ -273,6 +450,7 @@ module.exports = {
ROOT_DIR,
LOG_ROOT,
SEARCH_LOG_ROOT,
AUDIT_DB_PATH,
FACEAI_BASE_URL,
LEGACY_BASE_URL,
LEGACY_HOME_URL,
@ -290,6 +468,7 @@ module.exports = {
ensureLocalAuthenticatedRacePage,
expectLocalRacePageLoaded,
getSearchArtifacts,
readAuditArtifacts,
getSelfiePath,
performLocalLoginRequest,
prepareHostState,