Files
peardock/server/core/audit.js
T
Raven Scott e9fb26153a
Release rolling / release (push) Successful in 7m49s
Add Container Flattener (export|import squash with job tray push)
Squash a container into a single-layer image from Manage → Flatten, with
repo/tag, pause, config re-apply, vault credentials, and optional registry push.
2026-07-18 07:31:58 -04:00

146 lines
3.3 KiB
JavaScript

/**
* 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',
'updateAlertsConfig',
'exportAlertsConfig',
'upsertAlertChannel',
'deleteAlertChannel',
'upsertAlertRule',
'deleteAlertRule',
'testAlertChannel',
'fireManualAlert',
'removeImage',
'removeStack',
'deployStack',
'syncStackFromGit',
'deployContainer',
'createTunnel',
'closeTunnel',
'createContainer',
'buildImage',
'pushImage',
'loadImageFinish',
'importImage',
'registryLogin',
'registryLogout',
'registryDeleteTag',
'registryDeleteTags',
'registryDeleteRepository',
'vaultStoreCredential',
'vaultDeleteCredential',
'vaultUseCredential',
'vaultTestCredential',
'invitePeer',
'deleteInvite',
'revokePeer',
'unrevokePeer',
'clearRevokedPeers',
'setPeerRole',
'archiveContainerPut',
'removeNetwork',
'removeVolume',
'updateContainer',
'duplicateContainer',
'commitContainer',
'flattenContainer',
'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 }