/** * Append-only local audit log for privileged RPC actions. * * Path: PEARDOCK_AUDIT_LOG (default: ./peardock-audit.log) * Set PEARDOCK_AUDIT=0 to disable. */ import fs from 'fs' import path from 'path' import logger from '../utils/logger.js' const ENABLED = process.env.PEARDOCK_AUDIT !== '0' const LOG_PATH = process.env.PEARDOCK_AUDIT_LOG || path.join(process.cwd(), 'peardock-audit.log') /** Methods that should always be audited when successful/attempted. */ const AUDIT_METHODS = new Set([ 'removeContainer', 'killContainer', 'pruneContainers', 'pruneImages', 'pruneNetworks', 'pruneVolumes', 'pruneBuilder', 'systemPrune', 'recreateContainer', 'upsertSchedule', 'deleteSchedule', 'removeImage', 'removeStack', 'deployStack', 'syncStackFromGit', 'deployContainer', 'createTunnel', 'closeTunnel', 'createContainer', 'buildImage', 'pushImage', 'loadImageFinish', 'importImage', 'registryLogin', 'registryLogout', 'vaultStoreCredential', 'vaultDeleteCredential', 'vaultUseCredential', 'vaultTestCredential', 'invitePeer', 'deleteInvite', 'revokePeer', 'unrevokePeer', 'clearRevokedPeers', 'setPeerRole', 'archiveContainerPut', 'removeNetwork', 'removeVolume', 'updateContainer', 'duplicateContainer', 'commitContainer', 'swarmInit', 'swarmLeave', 'createService', 'removeService', 'createSecret', 'removeSecret', 'installPlugin', 'removePlugin', ]) /** * @param {object} entry * @param {string} entry.method * @param {string} [entry.peerId] * @param {string} [entry.role] * @param {boolean} [entry.ok] * @param {string} [entry.error] * @param {object} [entry.args] */ export function audit(entry) { if (!ENABLED) return if (!entry?.method) return if (!AUDIT_METHODS.has(entry.method) && entry.force !== true) return const line = JSON.stringify({ ts: new Date().toISOString(), method: entry.method, peerId: entry.peerId ? String(entry.peerId).slice(0, 16) : null, role: entry.role || null, ok: entry.ok !== false, error: entry.error || null, // Never log secrets; only allowlisted arg keys args: sanitizeArgs(entry.args), }) try { fs.appendFileSync(LOG_PATH, line + '\n', { encoding: 'utf8' }) } catch (err) { logger.warn('audit log write failed', { error: err.message }) } } /** * @param {unknown} args */ function sanitizeArgs(args) { if (!args || typeof args !== 'object') return null const out = {} const keys = [ 'id', 'name', 'stackName', 'image', 'operation', 'path', 'term', 'containerIds', 'authMode', 'role', ] for (const k of keys) { if (args[k] != null) { if (k === 'containerIds' && Array.isArray(args[k])) { out[k] = args[k].slice(0, 20).map((x) => String(x).slice(0, 12)) } else { out[k] = String(args[k]).slice(0, 128) } } } return Object.keys(out).length ? out : null } export function shouldAudit(method) { return ENABLED && AUDIT_METHODS.has(method) } export { AUDIT_METHODS, LOG_PATH }