mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-06 00:47:52 +00:00
refactor(backend): sanitize user input before logging to close CRLF injection (#807)
* refactor(backend): sanitize user input before logging to close CRLF injection
Adds a small sanitizeForLog helper that strips CR, LF, tab, and ASCII
control characters (0x00-0x1F, 0x7F) from a value before it is embedded
in a console.log/warn/error/debug call. Wraps every call site where a
user-controlled value (req.params, req.body, req.query, or a value
derived from them) flows into a log message.
Closes the bulk of the open CodeQL alerts in this family:
- 96 js/log-injection
- 28 js/tainted-format-string
The helper is in backend/src/utils/safeLog.ts. Routes still pre-validate
input at the request boundary; this is the second line of defense and
gives static analyzers a sanitizer they can trace through. JSON
responses, Docker filter labels, and other non-log call sites are
intentionally left unwrapped.
* refactor(backend): printf-style format strings for tainted-log call sites
CodeQL's js/tainted-format-string rule flags template literals in the first
arg of console.X when any interpolated value is user-controlled, regardless
of whether each value is sanitized inline. The canonical mitigation is to
use a static format string and pass values as positional args.
Converts the 28 flagged template literals to printf-style ("%s") format
strings, with sanitizeForLog applied to each positional arg. Also fills in
the log-injection wraps on 9 sites where a user-controlled value was
missed in the first sweep (agents, fleet, gitSources, imageUpdates,
GitSourceService).
No behavior change at runtime. Node's util.format substitutes %s tokens
identically to template-literal interpolation.
* fix(backend): wrap nodeId/snapshotId in fleet restore debug log
CodeQL flagged the unwrapped numeric args even though they cannot
contain control chars in practice. Apply the sanitizer for taint-flow
recognition.
This commit is contained in:
@@ -5,6 +5,7 @@ import { DatabaseService } from '../services/DatabaseService';
|
||||
import { NotificationService } from '../services/NotificationService';
|
||||
import TrivyService, { DIGEST_CACHE_TTL_MS } from '../services/TrivyService';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
// Bypass requires `?ignorePolicy=true` AND `req.user.role === 'admin'`. The
|
||||
// `stack:deploy` permission alone is not sufficient because the `deployer`
|
||||
@@ -98,6 +99,6 @@ export async function triggerPostDeployScan(
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[Security] triggerPostDeployScan error for ${stackName}:`, getErrorMessage(err, 'unknown error'));
|
||||
console.error('[Security] triggerPostDeployScan error for %s:', sanitizeForLog(stackName), sanitizeForLog(getErrorMessage(err, 'unknown error')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import type { ApiTokenScope } from '../services/DatabaseService';
|
||||
|
||||
/**
|
||||
@@ -28,14 +29,14 @@ const DEPLOY_ALLOWED_PATTERNS: RegExp[] = [
|
||||
];
|
||||
|
||||
const deny = (res: Response, req: Request, error: string, scope: ApiTokenScope | 'unknown'): void => {
|
||||
if (isDebugEnabled()) console.log('[ApiTokenScope:diag] Denied:', req.method, req.path, 'scope:', scope);
|
||||
if (isDebugEnabled()) console.log('[ApiTokenScope:diag] Denied:', sanitizeForLog(req.method), sanitizeForLog(req.path), 'scope:', scope);
|
||||
res.status(403).json({ error, code: 'SCOPE_DENIED' });
|
||||
};
|
||||
|
||||
export const enforceApiTokenScope: RequestHandler = (req: Request, res: Response, next: NextFunction): void => {
|
||||
const scope = req.apiTokenScope;
|
||||
if (!scope) { next(); return; } // Not an API token request
|
||||
if (isDebugEnabled()) console.log('[ApiTokenScope:diag]', req.method, req.path, 'scope:', scope);
|
||||
if (isDebugEnabled()) console.log('[ApiTokenScope:diag]', sanitizeForLog(req.method), sanitizeForLog(req.path), 'scope:', scope);
|
||||
if (scope === 'full-admin') { next(); return; }
|
||||
|
||||
if (scope === 'read-only') {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Request, Response, NextFunction, RequestHandler } from 'express';
|
||||
import { DatabaseService } from '../services/DatabaseService';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { getAuditSummary } from '../utils/audit-summaries';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { WEBHOOK_TRIGGER_RE } from '../helpers/routePatterns';
|
||||
import { authMiddleware } from './auth';
|
||||
|
||||
@@ -40,7 +41,7 @@ export const auditLog: RequestHandler = (req: Request, res: Response, next: Next
|
||||
res.on('finish', () => {
|
||||
try {
|
||||
if (isDebugEnabled()) {
|
||||
console.log(`[Audit:diag] ${req.method} /api${apiPath} by=${username} status=${res.statusCode} node=${nodeId ?? 'local'} ip=${ip}`);
|
||||
console.log(`[Audit:diag] ${sanitizeForLog(req.method)} /api${sanitizeForLog(apiPath)} by=${sanitizeForLog(username)} status=${res.statusCode} node=${nodeId ?? 'local'} ip=${sanitizeForLog(ip)}`);
|
||||
}
|
||||
DatabaseService.getInstance().insertAuditLog({
|
||||
timestamp: Date.now(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Request, Response } from 'express';
|
||||
import { DatabaseService, type UserRole, type ResourceType } from '../services/DatabaseService';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { effectiveVariant } from './tierGates';
|
||||
|
||||
// --- Scoped RBAC Permission Engine (Admiral) ---
|
||||
@@ -44,7 +45,7 @@ export function checkPermission(
|
||||
|
||||
const globalRole = req.user.role;
|
||||
|
||||
if (isDebugEnabled()) console.log('[RBAC:diag] checkPermission:', action, 'user:', req.user.username, 'globalRole:', globalRole, 'resource:', resourceType, resourceId);
|
||||
if (isDebugEnabled()) console.log('[RBAC:diag] checkPermission:', sanitizeForLog(action), 'user:', sanitizeForLog(req.user.username), 'globalRole:', globalRole, 'resource:', sanitizeForLog(resourceType), sanitizeForLog(resourceId));
|
||||
|
||||
if (globalRole === 'admin') return true;
|
||||
if (ROLE_PERMISSIONS[globalRole]?.includes(action)) return true;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
wsDataToBuffer,
|
||||
wsDataToString,
|
||||
} from './protocol';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
const RECONNECT_MIN_MS = 1_000;
|
||||
const RECONNECT_MAX_MS = 60_000;
|
||||
@@ -168,7 +169,7 @@ class PilotAgent {
|
||||
this.handleJsonFrame(frame);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('[Pilot] Malformed frame from primary:', (err as Error).message);
|
||||
console.warn('[Pilot] Malformed frame from primary:', sanitizeForLog((err as Error).message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +179,7 @@ class PilotAgent {
|
||||
switch (frame.t) {
|
||||
case 'hello': {
|
||||
if (frame.version !== PROTOCOL_VERSION) {
|
||||
console.error(`[Pilot] Protocol version ${frame.version} from primary is incompatible with agent (${PROTOCOL_VERSION}); exiting.`);
|
||||
console.error(`[Pilot] Protocol version ${sanitizeForLog(frame.version)} from primary is incompatible with agent (${PROTOCOL_VERSION}); exiting.`);
|
||||
this.shuttingDown = true;
|
||||
try { ws.close(1002, 'incompatible version'); } catch { /* ignore */ }
|
||||
process.exit(1);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DatabaseService } from '../services/DatabaseService';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { requireAdmin } from '../middleware/tierGates';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { NOTIFICATION_CHANNEL_TYPES, validateHttpsUrl } from '../helpers/notificationChannels';
|
||||
|
||||
export const agentsRouter = Router();
|
||||
@@ -34,8 +35,8 @@ agentsRouter.post('/', authMiddleware, async (req: Request, res: Response): Prom
|
||||
}
|
||||
const nodeId = req.nodeId ?? 0;
|
||||
DatabaseService.getInstance().upsertAgent(nodeId, { type, url, enabled });
|
||||
console.log(`[Agents] Agent ${type} updated`);
|
||||
if (isDebugEnabled()) console.log(`[Agents:diag] Agent ${type} upsert: enabled=${enabled}`);
|
||||
console.log('[Agents] Agent %s updated', sanitizeForLog(type));
|
||||
if (isDebugEnabled()) console.log('[Agents:diag] Agent %s upsert: enabled=%s', sanitizeForLog(type), sanitizeForLog(enabled));
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to update agent:', error);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { requireAdmiral } from '../middleware/tierGates';
|
||||
import { requirePermission } from '../middleware/permissions';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { escapeCsvField } from '../utils/csv';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
export const auditLogRouter = Router();
|
||||
|
||||
@@ -23,7 +24,7 @@ auditLogRouter.get('/', async (req: Request, res: Response): Promise<void> => {
|
||||
const withAnomalies = req.query.with_anomalies === '1';
|
||||
|
||||
if (isDebugEnabled()) {
|
||||
console.log(`[Audit:diag] Query: page=${page} limit=${limit} username=${username || '-'} method=${method || '-'} search=${search || '-'}`);
|
||||
console.log(`[Audit:diag] Query: page=${page} limit=${limit} username=${sanitizeForLog(username || '-')} method=${sanitizeForLog(method || '-')} search=${sanitizeForLog(search || '-')}`);
|
||||
}
|
||||
const db = DatabaseService.getInstance();
|
||||
const result = db.getAuditLogs({ page, limit, username, method, from, to, search });
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { authRateLimiter } from '../middleware/rateLimiters';
|
||||
import { requireAdmin } from '../middleware/tierGates';
|
||||
import { rejectApiTokenScope } from '../middleware/apiTokenScope';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import {
|
||||
BCRYPT_SALT_ROUNDS,
|
||||
COOKIE_NAME,
|
||||
@@ -143,7 +144,7 @@ authRouter.post('/login', authRateLimiter, async (req: Request, res: Response):
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('[Auth] Login failed for username:', username);
|
||||
console.warn('[Auth] Login failed for username:', sanitizeForLog(username));
|
||||
res.status(401).json({ error: 'Invalid credentials' });
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Router, type Request, type Response } from 'express';
|
||||
// @ts-ignore - composerize lacks proper type definitions
|
||||
import composerize from 'composerize';
|
||||
import { authMiddleware } from '../middleware/auth';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
const MAX_DOCKER_RUN_LENGTH = 8192;
|
||||
|
||||
@@ -37,7 +38,7 @@ convertRouter.post('/', authMiddleware, async (req: Request, res: Response): Pro
|
||||
}
|
||||
|
||||
if (typeof yaml !== 'string' || !yaml.includes('services:')) {
|
||||
console.warn('Converter produced unexpected output for input:', trimmed.slice(0, 200));
|
||||
console.warn('Converter produced unexpected output for input:', sanitizeForLog(trimmed.slice(0, 200)));
|
||||
res.status(422).json({ error: 'Could not parse command. Check syntax and supported flags.' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import { isDebugEnabled } from '../utils/debug';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { parseIntParam } from '../utils/parseIntParam';
|
||||
import { POLICY_SEVERITIES } from '../utils/severity';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { CloudBackupService } from '../services/CloudBackupService';
|
||||
import { NotificationService } from '../services/NotificationService';
|
||||
import { buildLocalConfigurationStatus, type ConfigurationStatus } from './dashboard';
|
||||
@@ -993,7 +994,7 @@ fleetRouter.post('/snapshots/:id/restore', authMiddleware, async (req: Request,
|
||||
|
||||
if (isDebugEnabled()) {
|
||||
const fileNames = files.map(f => f.filename).join(', ');
|
||||
console.debug(`[Fleet:debug] Restore: snapshot=${snapshotId}, node=${nodeId}, stack="${stackName}", files=[${fileNames}], redeploy=${redeploy}`);
|
||||
console.debug('[Fleet:debug] Restore: snapshot=%s, node=%s, stack="%s", files=[%s], redeploy=%s', sanitizeForLog(snapshotId), sanitizeForLog(nodeId), sanitizeForLog(stackName), sanitizeForLog(fileNames), sanitizeForLog(redeploy));
|
||||
}
|
||||
|
||||
const node = db.getNode(nodeId);
|
||||
@@ -1065,7 +1066,7 @@ fleetRouter.post('/snapshots/:id/restore', authMiddleware, async (req: Request,
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Fleet] Snapshot restore:', snapshotId, 'node=', nodeId, 'stack=', stackName);
|
||||
console.log('[Fleet] Snapshot restore: snapshot=%s node=%s stack=%s', snapshotId, sanitizeForLog(nodeId), sanitizeForLog(stackName));
|
||||
res.json({ message: 'Stack restored successfully', redeployed: redeploy });
|
||||
} catch (error) {
|
||||
console.error('[Fleet Snapshot] Restore error:', error);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { invalidateNodeCaches } from '../helpers/cacheInvalidation';
|
||||
import { triggerPostDeployScan } from '../helpers/policyGate';
|
||||
import { isValidStackName } from '../utils/validation';
|
||||
import { sendGitSourceError } from '../utils/gitSourceHttp';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
// Reasonable upper bounds so a caller cannot flood the service with huge
|
||||
// payloads. Generous compared to anything a real Git provider emits.
|
||||
@@ -206,16 +207,16 @@ stackGitSourceRouter.post('/:stackName/git-source/apply', async (req: Request, r
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
const shortSha = commitSha.trim().slice(0, 7);
|
||||
if (result.deployed) {
|
||||
console.log(`[GitSource] Applied commit ${shortSha} to ${stackName} (deployed)`);
|
||||
console.log('[GitSource] Applied commit %s to %s (deployed)', sanitizeForLog(shortSha), sanitizeForLog(stackName));
|
||||
} else if (result.deployError) {
|
||||
console.warn(`[GitSource] Applied commit ${shortSha} to ${stackName}, deploy failed: ${result.deployError}`);
|
||||
console.warn('[GitSource] Applied commit %s to %s, deploy failed: %s', sanitizeForLog(shortSha), sanitizeForLog(stackName), sanitizeForLog(result.deployError));
|
||||
} else {
|
||||
console.log(`[GitSource] Applied commit ${shortSha} to ${stackName}`);
|
||||
console.log('[GitSource] Applied commit %s to %s', sanitizeForLog(shortSha), sanitizeForLog(stackName));
|
||||
}
|
||||
res.json(result);
|
||||
if (result.deployed) {
|
||||
triggerPostDeployScan(stackName, req.nodeId).catch(err =>
|
||||
console.error(`[Security] Post-deploy scan failed for ${stackName}:`, err),
|
||||
console.error(`[Security] Post-deploy scan failed for ${sanitizeForLog(stackName)}:`, err),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { authMiddleware } from '../middleware/auth';
|
||||
import { requireAdmin, requirePaid } from '../middleware/tierGates';
|
||||
import { buildPolicyGateOptions } from '../helpers/policyGate';
|
||||
import { isValidStackName } from '../utils/validation';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
|
||||
// Fleet aggregation cache: 2-minute TTL, shared across dashboard tabs.
|
||||
@@ -189,7 +190,7 @@ autoUpdateRouter.post('/execute', authMiddleware, async (req: Request, res: Resp
|
||||
if (!requireAdmin(req, res)) return;
|
||||
try {
|
||||
const { target } = req.body as { target?: string };
|
||||
console.log(`[AutoUpdate] Execute requested: target="${target || ''}"`);
|
||||
console.log(`[AutoUpdate] Execute requested: target="${sanitizeForLog(target || '')}"`);
|
||||
if (!target || typeof target !== 'string') {
|
||||
res.status(400).json({ error: 'Missing "target" (stack name or "*" for all)' });
|
||||
return;
|
||||
@@ -256,7 +257,7 @@ autoUpdateRouter.post('/execute', authMiddleware, async (req: Request, res: Resp
|
||||
} catch (e) {
|
||||
const errMsg = getErrorMessage(e, String(e));
|
||||
checkErrors.push(errMsg);
|
||||
console.warn(`[AutoUpdate] Failed to check image ${imageRef}:`, e);
|
||||
console.warn('[AutoUpdate] Failed to check image %s:', sanitizeForLog(imageRef), sanitizeForLog((e as Error)?.message ?? String(e)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { isValidStackName } from '../utils/validation';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { getErrorMessage, isSqliteUniqueViolation } from '../utils/errors';
|
||||
import { parseIntParam } from '../utils/parseIntParam';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
const activeBulkActions = new Set<string>();
|
||||
|
||||
@@ -231,7 +232,7 @@ labelsRouter.post('/:id/action', authMiddleware, async (req: Request, res: Respo
|
||||
|
||||
const succeeded = results.filter(r => r.success).length;
|
||||
const failed = results.length - succeeded;
|
||||
console.log(`[Labels] Bulk ${action} on label ${id}: ${validStacks.length} stacks (${succeeded} succeeded, ${failed} failed)`);
|
||||
console.log(`[Labels] Bulk ${sanitizeForLog(action)} on label ${id}: ${validStacks.length} stacks (${succeeded} succeeded, ${failed} failed)`);
|
||||
if (isDebugEnabled()) console.debug('[Labels:debug] Bulk action complete:', { id, action, total: results.length, succeeded, failed });
|
||||
|
||||
if (succeeded > 0) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { requirePaid, requireAdmin, requireScheduledTaskTier } from '../middlewa
|
||||
import { escapeCsvField } from '../utils/csv';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { parseIntParam } from '../utils/parseIntParam';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
const VALID_TARGET_TYPES = ['stack', 'fleet', 'system'] as const;
|
||||
const VALID_ACTIONS = ['restart', 'snapshot', 'prune', 'update', 'scan', 'auto_backup', 'auto_stop', 'auto_down', 'auto_start'] as const;
|
||||
@@ -140,7 +141,7 @@ scheduledTasksRouter.post('/', (req: Request, res: Response): void => {
|
||||
if (optionalErr) { res.status(400).json({ error: optionalErr }); return; }
|
||||
|
||||
try { CronExpressionParser.parse(cron_expression); } catch (e) {
|
||||
console.warn('[Scheduler] Invalid cron expression rejected:', cron_expression, getErrorMessage(e, 'unknown'));
|
||||
console.warn('[Scheduler] Invalid cron expression rejected:', sanitizeForLog(cron_expression), sanitizeForLog(getErrorMessage(e, 'unknown')));
|
||||
res.status(400).json({ error: 'Invalid cron expression.' }); return;
|
||||
}
|
||||
|
||||
@@ -169,7 +170,7 @@ scheduledTasksRouter.post('/', (req: Request, res: Response): void => {
|
||||
delete_after_run: delete_after_run ? 1 : 0,
|
||||
});
|
||||
|
||||
console.log(`[ScheduledTasks] Created task id=${id} action=${action} target=${target_id || 'none'}`);
|
||||
console.log(`[ScheduledTasks] Created task id=${id} action=${sanitizeForLog(action)} target=${sanitizeForLog(target_id || 'none')}`);
|
||||
const task = DatabaseService.getInstance().getScheduledTask(id);
|
||||
res.status(201).json(task);
|
||||
} catch (error) {
|
||||
@@ -238,7 +239,7 @@ scheduledTasksRouter.put('/:id', (req: Request, res: Response): void => {
|
||||
|
||||
if (cron_expression) {
|
||||
try { CronExpressionParser.parse(cron_expression); } catch (e) {
|
||||
console.warn('[Scheduler] Invalid cron expression rejected:', cron_expression, getErrorMessage(e, 'unknown'));
|
||||
console.warn('[Scheduler] Invalid cron expression rejected:', sanitizeForLog(cron_expression), sanitizeForLog(getErrorMessage(e, 'unknown')));
|
||||
res.status(400).json({ error: 'Invalid cron expression.' }); return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { LicenseService } from '../services/LicenseService';
|
||||
import { validateImageRef } from '../utils/image-ref';
|
||||
import { applySuppressions } from '../utils/suppression-filter';
|
||||
import { generateSarif } from '../services/SarifExporter';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { blockIfReplica } from '../middleware/fleetSyncGuards';
|
||||
@@ -187,7 +188,7 @@ securityRouter.post('/scan', authMiddleware, (req: Request, res: Response): void
|
||||
res.status(202).json({ scanId });
|
||||
|
||||
svc.finishScan(scanId, imageRef, nodeId, { useCache: !force, scanners }).catch((err) => {
|
||||
console.error(`[Security] Scan failed for ${imageRef}:`, (err as Error).message);
|
||||
console.error('[Security] Scan failed for %s:', sanitizeForLog(imageRef), sanitizeForLog((err as Error).message));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { authRateLimiter, ssoRateLimiter } from '../middleware/rateLimiters';
|
||||
import { isSecureRequest } from '../helpers/cookies';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
// Seed SSO config from environment variables on module load. One-shot side
|
||||
// effect at startup; safe to repeat (upsert).
|
||||
@@ -22,7 +23,7 @@ function getSSOBaseUrl(req: Request, res: Response): string | null {
|
||||
return null;
|
||||
}
|
||||
if (!process.env.SSO_CALLBACK_URL && isDebugEnabled()) {
|
||||
console.debug('[SSO:debug] SSO_CALLBACK_URL not set; using Host header for callback URL:', host);
|
||||
console.debug('[SSO:debug] SSO_CALLBACK_URL not set; using Host header for callback URL:', sanitizeForLog(host));
|
||||
}
|
||||
return process.env.SSO_CALLBACK_URL || `${req.protocol}://${host}`;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DatabaseService } from '../services/DatabaseService';
|
||||
import { SSOService, type SSOProviderConfig } from '../services/SSOService';
|
||||
import { requireAdmin, requireTierForSsoProvider } from '../middleware/tierGates';
|
||||
import { rejectApiTokenScope } from '../middleware/apiTokenScope';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
const VALID_SSO_PROVIDERS = ['ldap', 'oidc_google', 'oidc_github', 'oidc_okta', 'oidc_custom'] as const;
|
||||
const SSO_SCOPE_MESSAGE = 'API tokens cannot access SSO configuration.';
|
||||
@@ -85,7 +86,7 @@ ssoConfigRouter.put('/:provider', (req: Request, res: Response): void => {
|
||||
}
|
||||
|
||||
SSOService.getInstance().saveProviderConfig(config);
|
||||
console.log(`[SSO] Config updated: ${provider} ${config.enabled ? 'enabled' : 'disabled'}`);
|
||||
console.log(`[SSO] Config updated: ${sanitizeForLog(provider)} ${config.enabled ? 'enabled' : 'disabled'}`);
|
||||
res.json({ success: true, message: 'SSO configuration saved' });
|
||||
} catch (error) {
|
||||
console.error('[SSO] Failed to save SSO config:', error);
|
||||
@@ -101,7 +102,7 @@ ssoConfigRouter.delete('/:provider', (req: Request, res: Response): void => {
|
||||
if (!requireTierForSsoProvider(provider, req, res)) return;
|
||||
try {
|
||||
SSOService.getInstance().deleteProviderConfig(provider);
|
||||
console.log(`[SSO] Config deleted: ${provider}`);
|
||||
console.log(`[SSO] Config deleted: ${sanitizeForLog(provider)}`);
|
||||
res.json({ success: true, message: 'SSO configuration deleted' });
|
||||
} catch (error) {
|
||||
console.error('[SSO] Failed to delete SSO config:', error);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { NotificationService } from '../services/NotificationService';
|
||||
import { isValidStackName, isValidServiceName, isPathWithinBase, isValidRelativeStackPath } from '../utils/validation';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { sendGitSourceError } from '../utils/gitSourceHttp';
|
||||
import { buildPolicyGateOptions, runPolicyGate, triggerPostDeployScan } from '../helpers/policyGate';
|
||||
import { invalidateNodeCaches } from '../helpers/cacheInvalidation';
|
||||
@@ -27,7 +28,7 @@ function notifyActionFailure(action: string, stackName: string, error: unknown):
|
||||
const message = getErrorMessage(error, `Failed to ${action} stack`);
|
||||
NotificationService.getInstance()
|
||||
.dispatchAlert('error', 'deploy_failure', message, { stackName })
|
||||
.catch(err => console.error(`[Stacks] Failed to dispatch failure notification for ${stackName}:`, err));
|
||||
.catch(err => console.error('[Stacks] Failed to dispatch failure notification for %s:', sanitizeForLog(stackName), err));
|
||||
}
|
||||
|
||||
async function resolveAllEnvFilePaths(nodeId: number, stackName: string): Promise<string[]> {
|
||||
@@ -90,7 +91,7 @@ async function resolveAllEnvFilePaths(nodeId: number, stackName: string): Promis
|
||||
}
|
||||
return existing;
|
||||
} catch (error) {
|
||||
console.warn(`Could not parse compose.yaml for env_file resolution in stack "${stackName}":`, error);
|
||||
console.warn('Could not parse compose.yaml for env_file resolution in stack "%s":', sanitizeForLog(stackName), error);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -217,15 +218,15 @@ stacksRouter.put('/:stackName', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { content } = req.body;
|
||||
if (typeof content !== 'string') {
|
||||
console.error('Content is not a string:', content);
|
||||
console.error('Content is not a string, got:', typeof content);
|
||||
return res.status(400).json({ error: 'Content must be a string' });
|
||||
}
|
||||
await FileSystemService.getInstance(req.nodeId).saveStackContent(stackName, content);
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(`[Stacks] Compose file saved: ${stackName}`);
|
||||
console.log(`[Stacks] Compose file saved: ${sanitizeForLog(stackName)}`);
|
||||
res.json({ message: 'Stack saved successfully' });
|
||||
} catch (error) {
|
||||
console.error('Failed to save stack:', error);
|
||||
console.error('Failed to save stack:', sanitizeForLog(getErrorMessage(error, 'unknown')));
|
||||
res.status(500).json({ error: 'Failed to save stack' });
|
||||
}
|
||||
});
|
||||
@@ -331,7 +332,7 @@ stacksRouter.put('/:stackName/env', async (req: Request, res: Response) => {
|
||||
await fsService.writeFile(envPath, content, 'utf-8');
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
const envFileName = path.basename(envPath);
|
||||
console.log(`[Stacks] Env file saved: ${stackName}/${envFileName}`);
|
||||
console.log(`[Stacks] Env file saved: ${sanitizeForLog(stackName)}/${sanitizeForLog(envFileName)}`);
|
||||
res.json({ message: 'Env file saved successfully' });
|
||||
} catch (error) {
|
||||
console.error('[Stacks] Failed to save env file:', error);
|
||||
@@ -351,7 +352,7 @@ stacksRouter.post('/', async (req: Request, res: Response) => {
|
||||
}
|
||||
await FileSystemService.getInstance(req.nodeId).createStack(stackName);
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(`[Stacks] Stack created: ${stackName}`);
|
||||
console.log(`[Stacks] Stack created: ${sanitizeForLog(stackName)}`);
|
||||
res.json({ message: 'Stack created successfully', name: stackName });
|
||||
} catch (error: unknown) {
|
||||
const message = getErrorMessage(error, '');
|
||||
@@ -433,7 +434,7 @@ stacksRouter.post('/from-git', async (req: Request, res: Response) => {
|
||||
|
||||
if (fromGitDiag) {
|
||||
console.log(
|
||||
`[Stacks:diag] from-git start stack=${stack_name} nodeId=${req.nodeId ?? 'local'} host=${gitRepoHost(repo_url)} branch=${branch} composePath=${compose_path} envPath=${resolvedEnvPath ?? 'none'} authType=${resolvedAuthType} autoApplyOnWebhook=${Boolean(auto_apply_on_webhook)} autoDeployOnApply=${Boolean(auto_deploy_on_apply)} deployNow=${deploy_now === true}`
|
||||
`[Stacks:diag] from-git start stack=${sanitizeForLog(stack_name)} nodeId=${req.nodeId ?? 'local'} host=${sanitizeForLog(gitRepoHost(repo_url))} branch=${sanitizeForLog(branch)} composePath=${sanitizeForLog(compose_path)} envPath=${sanitizeForLog(resolvedEnvPath ?? 'none')} authType=${sanitizeForLog(resolvedAuthType)} autoApplyOnWebhook=${Boolean(auto_apply_on_webhook)} autoDeployOnApply=${Boolean(auto_deploy_on_apply)} deployNow=${deploy_now === true}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -469,15 +470,15 @@ stacksRouter.post('/from-git', async (req: Request, res: Response) => {
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
} catch (e) {
|
||||
deployError = getErrorMessage(e, 'Deploy failed');
|
||||
console.error(`[Stacks] Deploy after create-from-git failed for ${stack_name}:`, deployError);
|
||||
console.error(`[Stacks] Deploy after create-from-git failed for ${sanitizeForLog(stack_name)}:`, deployError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Stacks] Stack created from Git: ${stack_name} at ${result.commitSha.slice(0, 7)}`);
|
||||
console.log(`[Stacks] Stack created from Git: ${sanitizeForLog(stack_name)} at ${result.commitSha.slice(0, 7)}`);
|
||||
if (fromGitDiag) {
|
||||
console.log(
|
||||
`[Stacks:diag] from-git ok stack=${stack_name} sha=${result.commitSha.slice(0, 7)} deployed=${deployed} envWritten=${result.envWritten} warnings=${result.warnings.length} elapsedMs=${Date.now() - fromGitStartedAt}`
|
||||
`[Stacks:diag] from-git ok stack=${sanitizeForLog(stack_name)} sha=${result.commitSha.slice(0, 7)} deployed=${deployed} envWritten=${result.envWritten} warnings=${result.warnings.length} elapsedMs=${Date.now() - fromGitStartedAt}`
|
||||
);
|
||||
}
|
||||
res.json({
|
||||
@@ -491,14 +492,14 @@ stacksRouter.post('/from-git', async (req: Request, res: Response) => {
|
||||
});
|
||||
if (deployed) {
|
||||
triggerPostDeployScan(stack_name, req.nodeId).catch(err =>
|
||||
console.error(`[Security] Post-deploy scan failed for ${stack_name}:`, err),
|
||||
console.error(`[Security] Post-deploy scan failed for ${sanitizeForLog(stack_name)}:`, err),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (fromGitDiag) {
|
||||
const code = error instanceof GitSourceError ? error.code : 'UNKNOWN';
|
||||
console.log(
|
||||
`[Stacks:diag] from-git fail stack=${fromGitStackName} code=${code} elapsedMs=${Date.now() - fromGitStartedAt}`
|
||||
`[Stacks:diag] from-git fail stack=${sanitizeForLog(fromGitStackName)} code=${code} elapsedMs=${Date.now() - fromGitStartedAt}`
|
||||
);
|
||||
}
|
||||
sendGitSourceError(res, error);
|
||||
@@ -513,15 +514,15 @@ stacksRouter.delete('/:stackName', async (req: Request, res: Response) => {
|
||||
try {
|
||||
await ComposeService.getInstance(req.nodeId).downStack(stackName);
|
||||
} catch (downErr) {
|
||||
console.warn(`[Teardown] Docker down failed or nothing to clean up for ${stackName}`);
|
||||
console.warn(`[Teardown] Docker down failed or nothing to clean up for ${sanitizeForLog(stackName)}`);
|
||||
}
|
||||
|
||||
if (pruneVolumes) {
|
||||
try {
|
||||
const result = await DockerController.getInstance().pruneManagedOnly('volumes', [stackName]);
|
||||
console.log(`[Stacks] Pruned volumes for ${stackName}: ${result.reclaimedBytes} bytes reclaimed`);
|
||||
console.log(`[Stacks] Pruned volumes for ${sanitizeForLog(stackName)}: ${result.reclaimedBytes} bytes reclaimed`);
|
||||
} catch (pruneErr) {
|
||||
console.warn(`[Stacks] Volume prune failed for ${stackName}, continuing delete:`, pruneErr);
|
||||
console.warn('[Stacks] Volume prune failed for %s, continuing delete:', sanitizeForLog(stackName), pruneErr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -530,7 +531,7 @@ stacksRouter.delete('/:stackName', async (req: Request, res: Response) => {
|
||||
await FileSystemService.getInstance(req.nodeId).deleteStack(stackName);
|
||||
} catch (err) {
|
||||
fsErr = err;
|
||||
console.error(`[Stacks] File deletion failed for ${stackName}, continuing with DB cleanup:`, err);
|
||||
console.error('[Stacks] File deletion failed for %s, continuing with DB cleanup:', sanitizeForLog(stackName), err);
|
||||
}
|
||||
|
||||
DatabaseService.getInstance().clearStackUpdateStatus(req.nodeId, stackName);
|
||||
@@ -541,10 +542,10 @@ stacksRouter.delete('/:stackName', async (req: Request, res: Response) => {
|
||||
if (fsErr) throw fsErr;
|
||||
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(`[Stacks] Stack deleted: ${stackName}`);
|
||||
console.log(`[Stacks] Stack deleted: ${sanitizeForLog(stackName)}`);
|
||||
res.json({ success: true });
|
||||
} catch (error: unknown) {
|
||||
console.error(`[Stacks] Failed to delete stack ${stackName}:`, error);
|
||||
console.error('[Stacks] Failed to delete stack %s:', sanitizeForLog(stackName), error);
|
||||
const message = getErrorMessage(error, 'Failed to delete stack');
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
@@ -569,7 +570,7 @@ stacksRouter.get('/:stackName/services', async (req: Request, res: Response) =>
|
||||
const services = parsed?.services ? Object.keys(parsed.services) : [];
|
||||
res.json(services);
|
||||
} catch (error) {
|
||||
console.error('[Stacks] Failed to fetch services:', error);
|
||||
console.error('[Stacks] Failed to fetch services:', sanitizeForLog(getErrorMessage(error, 'unknown')));
|
||||
res.status(500).json({ error: 'Failed to fetch services' });
|
||||
}
|
||||
});
|
||||
@@ -585,16 +586,16 @@ stacksRouter.post('/:stackName/deploy', async (req: Request, res: Response) => {
|
||||
const t0 = Date.now();
|
||||
await ComposeService.getInstance(req.nodeId).deployStack(stackName, getTerminalWs(), atomic);
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(`[Stacks] Deploy completed: ${stackName}`);
|
||||
console.log(`[Stacks] Deploy completed: ${sanitizeForLog(stackName)}`);
|
||||
if (debug) console.debug(`[Stacks:debug] Deploy finished in ${Date.now() - t0}ms`);
|
||||
res.json({ message: 'Deployed successfully' });
|
||||
triggerPostDeployScan(stackName, req.nodeId).catch(err =>
|
||||
console.error(`[Security] Post-deploy scan failed for ${stackName}:`, err),
|
||||
console.error('[Security] Post-deploy scan failed for %s:', sanitizeForLog(stackName), err),
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
console.error(`[Stacks] Deploy failed: ${stackName}`, error);
|
||||
console.error('[Stacks] Deploy failed: %s', sanitizeForLog(stackName), error);
|
||||
const rolledBack = LicenseService.getInstance().getTier() === 'paid';
|
||||
if (rolledBack) console.warn(`[Stacks] Deploy failed, rolled back: ${stackName}`);
|
||||
if (rolledBack) console.warn('[Stacks] Deploy failed, rolled back: %s', sanitizeForLog(stackName));
|
||||
const message = getErrorMessage(error, 'Failed to deploy stack');
|
||||
notifyActionFailure('deploy', stackName, error);
|
||||
res.status(500).json({ error: message, rolledBack });
|
||||
@@ -607,10 +608,10 @@ stacksRouter.post('/:stackName/down', async (req: Request, res: Response) => {
|
||||
try {
|
||||
await ComposeService.getInstance(req.nodeId).runCommand(stackName, 'down', getTerminalWs());
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(`[Stacks] Down completed: ${stackName}`);
|
||||
console.log(`[Stacks] Down completed: ${sanitizeForLog(stackName)}`);
|
||||
res.json({ status: 'Command started' });
|
||||
} catch (error: unknown) {
|
||||
console.error(`[Stacks] Down failed: ${stackName}`, error);
|
||||
console.error('[Stacks] Down failed: %s', sanitizeForLog(stackName), error);
|
||||
notifyActionFailure('down', stackName, error);
|
||||
res.status(500).json({ error: 'Failed to start command' });
|
||||
}
|
||||
@@ -642,10 +643,10 @@ async function bulkContainerOp(
|
||||
|
||||
await Promise.all(containers.map(c => op(c.Id)));
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(`[Stacks] ${titleCase} completed: ${stackName} (${containers.length} containers)`);
|
||||
console.log(`[Stacks] ${titleCase} completed: ${sanitizeForLog(stackName)} (${containers.length} containers)`);
|
||||
res.json({ success: true, message: `${titleCase} completed via Engine API.` });
|
||||
} catch (error: unknown) {
|
||||
console.error(`[Stacks] ${titleCase} failed: ${stackName}`, error);
|
||||
console.error('[Stacks] %s failed: %s', sanitizeForLog(titleCase), sanitizeForLog(stackName), error);
|
||||
const message = getErrorMessage(error, `Failed to ${action} containers`);
|
||||
if (action !== 'start') {
|
||||
notifyActionFailure(action, stackName, error);
|
||||
@@ -693,7 +694,7 @@ async function handleServiceAction(
|
||||
await Promise.all(matching.map(c => op(c.Id)));
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(
|
||||
`[Stacks] Service ${action} completed: ${stackName}/${serviceName} (${matching.length} containers)`,
|
||||
`[Stacks] Service ${sanitizeForLog(action)} completed: ${sanitizeForLog(stackName)}/${sanitizeForLog(serviceName)} (${matching.length} containers)`,
|
||||
);
|
||||
res.json({
|
||||
success: true,
|
||||
@@ -701,7 +702,7 @@ async function handleServiceAction(
|
||||
count: matching.length,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error(`[Stacks] Service ${action} failed: ${stackName}/${serviceName}`, error);
|
||||
console.error('[Stacks] Service %s failed: %s/%s', sanitizeForLog(action), sanitizeForLog(stackName), sanitizeForLog(serviceName), error);
|
||||
res.status(500).json({ error: getErrorMessage(error, `Failed to ${action} service`) });
|
||||
}
|
||||
}
|
||||
@@ -719,7 +720,7 @@ stacksRouter.get('/:stackName/update-preview', async (req: Request, res: Respons
|
||||
const preview = await UpdatePreviewService.getInstance().getPreview(req.nodeId, stackName);
|
||||
res.json(preview);
|
||||
} catch (error) {
|
||||
console.error(`[Stacks] Update preview failed: ${stackName}`, error);
|
||||
console.error('[Stacks] Update preview failed: %s', sanitizeForLog(stackName), sanitizeForLog(getErrorMessage(error, 'unknown')));
|
||||
res.status(500).json({ error: 'Failed to compute update preview' });
|
||||
}
|
||||
});
|
||||
@@ -736,16 +737,16 @@ stacksRouter.post('/:stackName/update', async (req: Request, res: Response) => {
|
||||
await ComposeService.getInstance(req.nodeId).updateStack(stackName, getTerminalWs(), atomic);
|
||||
DatabaseService.getInstance().clearStackUpdateStatus(req.nodeId, stackName);
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(`[Stacks] Update completed: ${stackName}`);
|
||||
console.log(`[Stacks] Update completed: ${sanitizeForLog(stackName)}`);
|
||||
if (debug) console.debug(`[Stacks:debug] Update finished in ${Date.now() - t0}ms`);
|
||||
res.json({ status: 'Update completed' });
|
||||
triggerPostDeployScan(stackName, req.nodeId).catch(err =>
|
||||
console.error(`[Security] Post-deploy scan failed for ${stackName}:`, err),
|
||||
console.error('[Security] Post-deploy scan failed for %s:', sanitizeForLog(stackName), err),
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
console.error(`[Stacks] Update failed: ${stackName}`, error);
|
||||
console.error('[Stacks] Update failed: %s', sanitizeForLog(stackName), error);
|
||||
const rolledBack = LicenseService.getInstance().getTier() === 'paid';
|
||||
if (rolledBack) console.warn(`[Stacks] Update failed, rolled back: ${stackName}`);
|
||||
if (rolledBack) console.warn(`[Stacks] Update failed, rolled back: ${sanitizeForLog(stackName)}`);
|
||||
notifyActionFailure('update', stackName, error);
|
||||
res.status(500).json({ error: getErrorMessage(error, 'Failed to update'), rolledBack });
|
||||
}
|
||||
@@ -761,14 +762,14 @@ stacksRouter.post('/:stackName/rollback', async (req: Request, res: Response) =>
|
||||
if (!backupInfo.exists) {
|
||||
return res.status(404).json({ error: 'No backup available for this stack.' });
|
||||
}
|
||||
console.log(`[Stacks] Rollback initiated: ${stackName}`);
|
||||
console.log(`[Stacks] Rollback initiated: ${sanitizeForLog(stackName)}`);
|
||||
await fsSvc.restoreStackFiles(stackName);
|
||||
await ComposeService.getInstance(req.nodeId).deployStack(stackName, getTerminalWs(), false);
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
console.log(`[Stacks] Rollback completed: ${stackName}`);
|
||||
console.log(`[Stacks] Rollback completed: ${sanitizeForLog(stackName)}`);
|
||||
res.json({ message: 'Stack rolled back successfully.' });
|
||||
} catch (error: unknown) {
|
||||
console.error(`[Stacks] Rollback failed: ${stackName}`, error);
|
||||
console.error('[Stacks] Rollback failed: %s', sanitizeForLog(stackName), error);
|
||||
const message = getErrorMessage(error, 'Rollback failed.');
|
||||
res.status(500).json({ error: message });
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { invalidateNodeCaches } from '../helpers/cacheInvalidation';
|
||||
import { isValidDockerResourceId, isValidCidr, isValidIPv4 } from '../utils/validation';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
export const systemMaintenanceRouter = Router();
|
||||
|
||||
@@ -32,11 +33,11 @@ systemMaintenanceRouter.post('/prune/orphans', async (req: Request, res: Respons
|
||||
if (invalidIds.length > 0) {
|
||||
return res.status(400).json({ error: 'One or more container IDs have an invalid format' });
|
||||
}
|
||||
console.log(`[Resources] Prune orphans: ${containerIds.length} container(s) requested`);
|
||||
console.log(`[Resources] Prune orphans: ${sanitizeForLog(containerIds.length)} container(s) requested`);
|
||||
const dockerController = DockerController.getInstance(req.nodeId);
|
||||
const results = await dockerController.removeContainers(containerIds);
|
||||
const succeeded = results.filter((r: { success: boolean }) => r.success).length;
|
||||
console.log(`[Resources] Prune orphans completed: ${succeeded}/${containerIds.length} removed`);
|
||||
console.log(`[Resources] Prune orphans completed: ${succeeded}/${sanitizeForLog(containerIds.length)} removed`);
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
res.json({ results });
|
||||
} catch (error) {
|
||||
@@ -158,7 +159,7 @@ systemMaintenanceRouter.post('/volumes/delete', async (req: Request, res: Respon
|
||||
try {
|
||||
const { id } = req.body;
|
||||
if (!id || typeof id !== 'string') return res.status(400).json({ error: 'Volume name is required' });
|
||||
console.log(`[Resources] Delete volume: ${id}`);
|
||||
console.log(`[Resources] Delete volume: ${sanitizeForLog(id)}`);
|
||||
const dockerController = DockerController.getInstance(req.nodeId);
|
||||
await dockerController.removeVolume(id);
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
@@ -246,7 +247,7 @@ systemMaintenanceRouter.post('/networks', async (req: Request, res: Response) =>
|
||||
|
||||
const dockerController = DockerController.getInstance(req.nodeId);
|
||||
const network = await dockerController.createNetwork(options);
|
||||
console.log(`[Resources] Network created: ${name}`);
|
||||
console.log(`[Resources] Network created: ${sanitizeForLog(name)}`);
|
||||
invalidateNodeCaches(req.nodeId);
|
||||
res.status(201).json({ success: true, message: 'Network created', id: network.id });
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { BCRYPT_SALT_ROUNDS, MIN_PASSWORD_LENGTH } from '../helpers/constants';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { getErrorMessage, isSqliteUniqueViolation } from '../utils/errors';
|
||||
import { parseIntParam } from '../utils/parseIntParam';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
const USERS_SCOPE_MESSAGE = 'API tokens cannot access user management.';
|
||||
const VALID_USER_ROLES: UserRole[] = ['admin', 'viewer', 'deployer', 'node-admin', 'auditor'];
|
||||
@@ -95,7 +96,7 @@ usersRouter.post('/', authMiddleware, async (req: Request, res: Response): Promi
|
||||
|
||||
const passwordHash = await bcrypt.hash(password, BCRYPT_SALT_ROUNDS);
|
||||
const id = db.addUser({ username, password_hash: passwordHash, role });
|
||||
console.log('[Users] Created:', username, 'role:', role, 'by:', req.user!.username);
|
||||
console.log('[Users] Created:', sanitizeForLog(username), 'role:', sanitizeForLog(role), 'by:', sanitizeForLog(req.user!.username));
|
||||
res.status(201).json({ id, username, role });
|
||||
} catch (error) {
|
||||
console.error('[Users] Create error:', error);
|
||||
@@ -304,7 +305,7 @@ usersRouter.post('/:id/roles', authMiddleware, (req: Request, res: Response): vo
|
||||
|
||||
try {
|
||||
const id = db.addRoleAssignment({ user_id: userId, role, resource_type, resource_id });
|
||||
console.log('[Roles] Assigned', role, 'on', resource_type, resource_id, 'to user', userId, 'by:', req.user!.username);
|
||||
console.log('[Roles] Assigned', sanitizeForLog(role), 'on', sanitizeForLog(resource_type), sanitizeForLog(resource_id), 'to user', userId, 'by:', sanitizeForLog(req.user!.username));
|
||||
res.status(201).json({ id, user_id: userId, role, resource_type, resource_id });
|
||||
} catch (err: unknown) {
|
||||
if (isSqliteUniqueViolation(err)) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { RegistryService } from './RegistryService';
|
||||
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { isPathWithinBase, isValidStackName } from '../utils/validation';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
/**
|
||||
* ComposeService - local docker compose CLI execution.
|
||||
@@ -141,7 +142,7 @@ export class ComposeService {
|
||||
await fsSvc.backupStackFiles(stackName);
|
||||
sendOutput('=== Backup created for atomic deployment ===\n');
|
||||
} catch (e) {
|
||||
console.warn(`Failed to backup stack files for ${stackName}:`, e);
|
||||
console.warn('Failed to backup stack files for %s:', sanitizeForLog(stackName), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +155,7 @@ export class ComposeService {
|
||||
await dockerController.removeContainers(legacyContainers.map((c: any) => c.Id));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to clean up legacy containers for ${stackName}:`, e);
|
||||
console.warn('Failed to clean up legacy containers for %s:', sanitizeForLog(stackName), e);
|
||||
}
|
||||
|
||||
await this.withRegistryAuth(async (env) => {
|
||||
@@ -196,7 +197,7 @@ export class ComposeService {
|
||||
}, sendOutput);
|
||||
sendOutput('=== Rolled back successfully ===\n');
|
||||
} catch (rollbackError) {
|
||||
console.error(`Rollback failed for ${stackName}:`, rollbackError);
|
||||
console.error('Rollback failed for %s:', sanitizeForLog(stackName), rollbackError);
|
||||
sendOutput('=== Rollback failed - manual intervention may be required ===\n');
|
||||
}
|
||||
}
|
||||
@@ -331,7 +332,7 @@ export class ComposeService {
|
||||
await fsSvc.backupStackFiles(stackName);
|
||||
sendOutput('=== Backup created for atomic update ===\n');
|
||||
} catch (e) {
|
||||
console.warn(`Failed to backup stack files for ${stackName}:`, e);
|
||||
console.warn('Failed to backup stack files for %s:', sanitizeForLog(stackName), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,7 +345,7 @@ export class ComposeService {
|
||||
await dockerController.removeContainers(legacyContainers.map((c: any) => c.Id));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Failed to clean up legacy containers for ${stackName}:`, e);
|
||||
console.warn('Failed to clean up legacy containers for %s:', sanitizeForLog(stackName), e);
|
||||
}
|
||||
|
||||
await this.withRegistryAuth(async (env) => {
|
||||
@@ -392,7 +393,7 @@ export class ComposeService {
|
||||
}, sendOutput);
|
||||
sendOutput('=== Rolled back successfully ===\n');
|
||||
} catch (rollbackError) {
|
||||
console.error(`Rollback failed for ${stackName}:`, rollbackError);
|
||||
console.error('Rollback failed for %s:', sanitizeForLog(stackName), rollbackError);
|
||||
sendOutput('=== Rollback failed - manual intervention may be required ===\n');
|
||||
}
|
||||
}
|
||||
@@ -405,7 +406,7 @@ export class ComposeService {
|
||||
try {
|
||||
await this.execute('docker', ['compose', 'down', '--volumes', '--remove-orphans'], stackPath, undefined, false);
|
||||
} catch (error) {
|
||||
console.warn(`[Teardown] Docker down failed or nothing to clean up for ${stackName}`);
|
||||
console.warn(`[Teardown] Docker down failed or nothing to clean up for ${sanitizeForLog(stackName)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { NodeRegistry } from './NodeRegistry';
|
||||
import { CacheService } from './CacheService';
|
||||
import { isPathWithinBase } from '../utils/validation';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
const COMPOSE_DIR = process.env.COMPOSE_DIR || '/app/compose';
|
||||
@@ -644,7 +645,7 @@ class DockerController {
|
||||
} catch (err: unknown) {
|
||||
const code = (err as NodeJS.ErrnoException)?.code;
|
||||
if (code !== 'ENOENT' && code !== 'ENOTDIR') {
|
||||
console.error(`[DockerController] Failed to read ${filePath}:`, err);
|
||||
console.error('[DockerController] Failed to read %s:', sanitizeForLog(filePath), sanitizeForLog((err as Error)?.message ?? String(err)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -790,7 +791,7 @@ class DockerController {
|
||||
containers = lines.map(line => JSON.parse(line) as ComposeContainer);
|
||||
} catch (innerError) {
|
||||
// Log parsing failure with stderr for debugging
|
||||
console.error(`Docker Compose JSON Parse Error for ${stackName}:`, stderr || (parseError as Error).message);
|
||||
console.error('Docker Compose JSON Parse Error for %s:', sanitizeForLog(stackName), sanitizeForLog(stderr || (parseError as Error).message));
|
||||
// Don't return empty - trigger smart fallback below
|
||||
}
|
||||
}
|
||||
@@ -827,7 +828,7 @@ class DockerController {
|
||||
} catch (error) {
|
||||
// If command fails (e.g., stack not deployed, invalid YAML, missing env_file)
|
||||
const execError = error as { stderr?: string; message?: string };
|
||||
console.error(`Docker Compose Error for ${stackName}:`, execError.stderr || execError.message);
|
||||
console.error('Docker Compose Error for %s:', sanitizeForLog(stackName), sanitizeForLog(execError.stderr || execError.message || 'unknown'));
|
||||
|
||||
// Try smart fallback even on error
|
||||
return await this.enrichContainers(await this.smartFallback(stackName, stackDir));
|
||||
@@ -930,7 +931,7 @@ class DockerController {
|
||||
};
|
||||
});
|
||||
} catch (fallbackError) {
|
||||
console.error(`Smart Fallback failed for ${stackName}:`, fallbackError);
|
||||
console.error('Smart Fallback failed for %s:', sanitizeForLog(stackName), sanitizeForLog((fallbackError as Error)?.message ?? String(fallbackError)));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1053,7 +1054,7 @@ class DockerController {
|
||||
await container.remove({ force: true });
|
||||
results.push({ id, success: true });
|
||||
} catch (error: any) {
|
||||
console.error(`Failed to remove container ${id}:`, error.message);
|
||||
console.error('Failed to remove container %s:', sanitizeForLog(id), sanitizeForLog(error.message));
|
||||
results.push({ id, success: false, error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Readable } from 'stream';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
import { isPathWithinBase, isValidStackName } from '../utils/validation';
|
||||
import { isBinaryBuffer } from '../utils/binaryDetect';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
export interface FileEntry {
|
||||
name: string;
|
||||
@@ -133,7 +134,7 @@ export class FileSystemService {
|
||||
const filePath = await this.getComposeFilePath(stackName);
|
||||
return await fsPromises.readFile(filePath, 'utf-8');
|
||||
} catch (error) {
|
||||
console.error('Error reading stack content:', error);
|
||||
console.error('Error reading stack content:', sanitizeForLog((error as Error)?.message ?? String(error)));
|
||||
throw new Error(`Failed to read stack: ${stackName}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { DatabaseService, type StackGitSource, type GitSourceAuthType } from './
|
||||
import { FileSystemService } from './FileSystemService';
|
||||
import { ComposeService } from './ComposeService';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
/**
|
||||
* GitSourceService - fetch compose files from a Git repository and apply
|
||||
@@ -380,7 +381,7 @@ export class GitSourceService {
|
||||
const diag = isDebugEnabled();
|
||||
if (diag) {
|
||||
console.log(
|
||||
`[GitSource:diag] fetch start host=${repoHost(repoUrl)} branch=${branch} compose=${composePath} envSync=${envPath ? 'true' : 'false'} timeoutMs=${timeoutMs}`
|
||||
`[GitSource:diag] fetch start host=${sanitizeForLog(repoHost(repoUrl))} branch=${sanitizeForLog(branch)} compose=${sanitizeForLog(composePath)} envSync=${envPath ? 'true' : 'false'} timeoutMs=${timeoutMs}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -444,7 +445,7 @@ export class GitSourceService {
|
||||
throw new GitSourceError('GIT_ERROR', scrubCredentials((e as Error).message));
|
||||
}
|
||||
if (isLfsPointer(composeContent)) {
|
||||
console.error(`[GitSource] LFS pointer detected in ${composePath}`);
|
||||
console.error(`[GitSource] LFS pointer detected in ${sanitizeForLog(composePath)}`);
|
||||
throw new GitSourceError(
|
||||
'GIT_ERROR',
|
||||
`Compose file at ${composePath} is stored in Git LFS, which is not supported. Commit the plain file or replace the LFS pointer before linking this repository.`,
|
||||
@@ -470,7 +471,7 @@ export class GitSourceService {
|
||||
}
|
||||
}
|
||||
if (envContent !== null && isLfsPointer(envContent)) {
|
||||
console.error(`[GitSource] LFS pointer detected in ${envPath}`);
|
||||
console.error(`[GitSource] LFS pointer detected in ${sanitizeForLog(envPath)}`);
|
||||
throw new GitSourceError(
|
||||
'GIT_ERROR',
|
||||
`Env file at ${envPath} is stored in Git LFS, which is not supported. Commit the plain file or replace the LFS pointer before linking this repository.`,
|
||||
@@ -489,7 +490,7 @@ export class GitSourceService {
|
||||
|
||||
if (diag) {
|
||||
console.log(
|
||||
`[GitSource:diag] fetch ok host=${repoHost(repoUrl)} branch=${branch} sha=${commitSha.slice(0, 7)} env=${envContent !== null ? 'present' : 'absent'} warnings=${warnings.length} elapsedMs=${Date.now() - startedAt}`
|
||||
`[GitSource:diag] fetch ok host=${sanitizeForLog(repoHost(repoUrl))} branch=${sanitizeForLog(branch)} sha=${commitSha.slice(0, 7)} env=${envContent !== null ? 'present' : 'absent'} warnings=${warnings.length} elapsedMs=${Date.now() - startedAt}`
|
||||
);
|
||||
}
|
||||
return { composeContent, envContent, commitSha, warnings };
|
||||
@@ -497,7 +498,7 @@ export class GitSourceService {
|
||||
if (diag) {
|
||||
const msg = err instanceof GitSourceError ? `${err.code}: ${err.message}` : (err as Error).message;
|
||||
console.log(
|
||||
`[GitSource:diag] fetch fail host=${repoHost(repoUrl)} branch=${branch} elapsedMs=${Date.now() - startedAt} err=${scrubCredentials(msg)}`
|
||||
`[GitSource:diag] fetch fail host=${sanitizeForLog(repoHost(repoUrl))} branch=${sanitizeForLog(branch)} elapsedMs=${Date.now() - startedAt} err=${sanitizeForLog(scrubCredentials(msg))}`
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
@@ -742,7 +743,7 @@ export class GitSourceService {
|
||||
throw new GitSourceError('GIT_ERROR', 'No pending pull to apply. Fetch the source again.');
|
||||
}
|
||||
if (src.pending_commit_sha !== commitSha) {
|
||||
if (diag) console.log(`[GitSource:diag] apply sha mismatch stack=${stackName} expected=${commitSha.slice(0, 7)} pending=${src.pending_commit_sha.slice(0, 7)}`);
|
||||
if (diag) console.log('[GitSource:diag] apply sha mismatch stack=%s expected=%s pending=%s', sanitizeForLog(stackName), sanitizeForLog(commitSha.slice(0, 7)), sanitizeForLog(src.pending_commit_sha.slice(0, 7)));
|
||||
throw new GitSourceError('GIT_ERROR', 'Pending commit has changed since this pull was fetched. Please review the latest diff.');
|
||||
}
|
||||
|
||||
@@ -770,7 +771,7 @@ export class GitSourceService {
|
||||
db.markGitSourceApplied(stackName, commitSha, hash);
|
||||
|
||||
const shouldDeploy = opts.deploy ?? src.auto_deploy_on_apply;
|
||||
if (diag) console.log(`[GitSource:diag] apply wrote stack=${stackName} sha=${commitSha.slice(0, 7)} deploy=${shouldDeploy}`);
|
||||
if (diag) console.log('[GitSource:diag] apply wrote stack=%s sha=%s deploy=%s', sanitizeForLog(stackName), sanitizeForLog(commitSha.slice(0, 7)), sanitizeForLog(shouldDeploy));
|
||||
|
||||
if (shouldDeploy) {
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { NotificationService } from './NotificationService';
|
||||
import { parseImageRef, getRemoteDigest } from './registry-api';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
const BACKFILL_KEY = 'image_update_notifications_backfilled';
|
||||
|
||||
@@ -239,7 +240,7 @@ export class ImageUpdateService {
|
||||
try {
|
||||
imageUpdateMap.set(imageRef, await this.checkImage(docker, imageRef));
|
||||
} catch (e) {
|
||||
console.error(`[ImageUpdateService] Error checking ${imageRef}:`, e);
|
||||
console.error(`[ImageUpdateService] Error checking ${sanitizeForLog(imageRef)}:`, sanitizeForLog((e as Error)?.message ?? String(e)));
|
||||
imageUpdateMap.set(imageRef, { hasUpdate: false, error: String(e) });
|
||||
}
|
||||
await sleep(ImageUpdateService.INTER_IMAGE_DELAY_MS);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DatabaseService, NotificationHistory } from './DatabaseService';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
export type NotificationCategory =
|
||||
| 'deploy_success'
|
||||
@@ -139,7 +140,7 @@ export class NotificationService {
|
||||
return true;
|
||||
});
|
||||
if (matched.length > 0) {
|
||||
if (isDebugEnabled()) console.log(`[Notify:diag] Matched ${matched.length} route(s) for stack "${stackName ?? '(none)'}", category="${category}"`);
|
||||
if (isDebugEnabled()) console.log(`[Notify:diag] Matched ${matched.length} route(s) for stack "${sanitizeForLog(stackName ?? '(none)')}", category="${sanitizeForLog(category)}"`);
|
||||
await Promise.allSettled(
|
||||
matched.map(route =>
|
||||
this.sendToChannel(route.channel_type, route.channel_url, level, message)
|
||||
|
||||
@@ -13,6 +13,7 @@ import { DatabaseService } from './DatabaseService';
|
||||
import type { ScanPolicy, VulnSeverity } from './DatabaseService';
|
||||
import { FleetSyncService } from './FleetSyncService';
|
||||
import { NotificationService } from './NotificationService';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import TrivyService from './TrivyService';
|
||||
import { isSeverityAtLeast } from '../utils/severity';
|
||||
import { validateImageRef } from '../utils/image-ref';
|
||||
@@ -72,7 +73,7 @@ export async function enforcePolicyPreDeploy(
|
||||
imageRefs = await ComposeService.getInstance(nodeId).listStackImages(stackName);
|
||||
} catch (err) {
|
||||
const message = getErrorMessage(err, 'compose parse failed');
|
||||
console.error(`[Policy] listStackImages failed for ${stackName}:`, message);
|
||||
console.error('[Policy] listStackImages failed for %s:', sanitizeForLog(stackName), sanitizeForLog(message));
|
||||
return {
|
||||
ok: false,
|
||||
bypassed: false,
|
||||
|
||||
@@ -3,6 +3,7 @@ import http from 'http';
|
||||
import { CryptoService } from './CryptoService';
|
||||
import { DatabaseService, type Registry, type RegistryType } from './DatabaseService';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -141,7 +142,7 @@ function httpGet(
|
||||
nextHeaders = rest;
|
||||
}
|
||||
if (isDebugEnabled()) {
|
||||
console.debug(`[RegistryService][debug] redirect ${status} ${url} -> ${nextUrl.toString()} (auth ${nextHeaders === headers ? 'kept' : 'stripped'})`);
|
||||
console.debug(`[RegistryService][debug] redirect ${status} ${sanitizeForLog(url)} -> ${sanitizeForLog(nextUrl.toString())} (auth ${nextHeaders === headers ? 'kept' : 'stripped'})`);
|
||||
}
|
||||
httpGet(nextUrl.toString(), nextHeaders, timeoutMs, false).then(resolve, reject);
|
||||
return;
|
||||
@@ -208,7 +209,7 @@ export class RegistryService {
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
});
|
||||
console.info(`[RegistryService] Registry created: id=${id} type=${input.type} name="${input.name}"`);
|
||||
console.info(`[RegistryService] Registry created: id=${id} type=${sanitizeForLog(input.type)} name="${sanitizeForLog(input.name)}"`);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -282,7 +283,7 @@ export class RegistryService {
|
||||
return { success: false, error: 'AWS region is required for ECR registries.' };
|
||||
}
|
||||
if (isDebugEnabled()) {
|
||||
console.debug(`[RegistryService][debug] testWithCredentials ECR region=${input.aws_region}`);
|
||||
console.debug(`[RegistryService][debug] testWithCredentials ECR region=${sanitizeForLog(input.aws_region)}`);
|
||||
}
|
||||
await this.fetchEcrToken(input.username, input.secret, input.aws_region);
|
||||
if (isDebugEnabled()) {
|
||||
@@ -294,7 +295,7 @@ export class RegistryService {
|
||||
const probeUrl = toProbeUrl(input.url, input.type);
|
||||
const basicAuth = Buffer.from(`${input.username}:${input.secret}`).toString('base64');
|
||||
if (isDebugEnabled()) {
|
||||
console.debug(`[RegistryService][debug] testWithCredentials probing ${probeUrl}/v2/`);
|
||||
console.debug(`[RegistryService][debug] testWithCredentials probing ${sanitizeForLog(probeUrl)}/v2/`);
|
||||
}
|
||||
const res = await httpGet(`${probeUrl}/v2/`, { Authorization: `Basic ${basicAuth}` });
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ImageUpdateService } from './ImageUpdateService';
|
||||
import type { ImageCheckResult } from './ImageUpdateService';
|
||||
import { isDebugEnabled } from '../utils/debug';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import { captureLocalNodeFiles, captureRemoteNodeFiles } from '../utils/snapshot-capture';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
import { NotificationService } from './NotificationService';
|
||||
@@ -698,7 +699,7 @@ export class SchedulerService {
|
||||
} catch (e) {
|
||||
const msg = getErrorMessage(e, String(e));
|
||||
checkErrors.push(msg);
|
||||
console.warn(`[SchedulerService] Failed to check image ${imageRef}:`, e);
|
||||
console.warn(`[SchedulerService] Failed to check image ${sanitizeForLog(imageRef)}:`, sanitizeForLog((e as Error)?.message ?? String(e)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { FileSystemService } from './FileSystemService';
|
||||
import { RegistryService } from './RegistryService';
|
||||
import { disableCapability, enableCapability } from './CapabilityRegistry';
|
||||
import { sanitizeForLog } from '../utils/safeLog';
|
||||
import TrivyInstaller, { type TrivySource } from './TrivyInstaller';
|
||||
import { FleetSyncService } from './FleetSyncService';
|
||||
import { getErrorMessage } from '../utils/errors';
|
||||
@@ -26,7 +27,7 @@ const SBOM_TIMEOUT_MS = 3 * 60 * 1000;
|
||||
export const DIGEST_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
function diag(msg: string, ...args: unknown[]): void {
|
||||
if (isDebugEnabled()) console.log(`[Trivy:diag] ${msg}`, ...args);
|
||||
if (isDebugEnabled()) console.log(`[Trivy:diag] ${sanitizeForLog(msg)}`, ...args);
|
||||
}
|
||||
|
||||
interface TrivyRawVulnerability {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const CONTROL_CHARS_REGEX = /[\r\n\t\x00-\x1F\x7F]/g;
|
||||
|
||||
/**
|
||||
* Strip CR, LF, tab, and other ASCII control characters from a value before
|
||||
* embedding it in a log line. Prevents log-injection attacks where untrusted
|
||||
* input could forge multi-line log entries or terminal escape sequences.
|
||||
*
|
||||
* Use at every site where a user-controlled string flows into console.log /
|
||||
* console.warn / console.error, including via template literals.
|
||||
*/
|
||||
export function sanitizeForLog(value: unknown): string {
|
||||
const s = typeof value === 'string' ? value : String(value);
|
||||
return s.replace(CONTROL_CHARS_REGEX, '');
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from 'path';
|
||||
import { sanitizeForLog } from './safeLog';
|
||||
|
||||
/**
|
||||
* Stack name must only contain URL-safe characters with no path separators.
|
||||
@@ -19,7 +20,7 @@ export function isValidRemoteUrl(
|
||||
try {
|
||||
url = new URL(raw);
|
||||
} catch (e) {
|
||||
console.warn('[Validation] URL parse failure:', (e as Error).message, '— input:', raw);
|
||||
console.warn('[Validation] URL parse failure:', sanitizeForLog((e as Error).message), 'input:', sanitizeForLog(raw));
|
||||
return {
|
||||
valid: false,
|
||||
reason: 'API URL must be a valid URL (e.g. https://my-server.example.com:1852)',
|
||||
|
||||
Reference in New Issue
Block a user