[grade=pending] QA sprint: commit 103 at-risk files from multi-agent sprint work
Committed by Hermes autonomous QA sprint 2026-08-13. These files were modified during the Aug 12 sprint but never committed.
This commit is contained in:
@@ -184,10 +184,10 @@ class AuditLogger {
|
||||
});
|
||||
} catch (e) {
|
||||
// Non-fatal — security store is a best-effort mirror
|
||||
console.error('[AuditLogger] Security event emit failed:', e.message);
|
||||
process.stderr.write(`[AuditLogger] Security event emit failed: ${e.message}\n`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[AuditLogger] Failed to write entry:', e.message);
|
||||
process.stderr.write(`[AuditLogger] Failed to write entry: ${e.message}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ class AuditLogger {
|
||||
}
|
||||
return entries.slice(offset, offset + limit);
|
||||
} catch (e) {
|
||||
console.error('[AuditLogger] Failed to read:', e.message);
|
||||
process.stderr.write(`[AuditLogger] Failed to read: ${e.message}\n`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const platformPaths = require('../../platform-paths');
|
||||
const { log } = require('../utils/logging');
|
||||
|
||||
// Encryption settings
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
@@ -65,7 +66,7 @@ function loadOrCreateKey() {
|
||||
// Check for key in environment variable first
|
||||
if (process.env.DASHCADDY_ENCRYPTION_KEY) {
|
||||
encryptionKey = Buffer.from(process.env.DASHCADDY_ENCRYPTION_KEY, 'hex');
|
||||
console.log('[Crypto] Using encryption key from environment variable');
|
||||
log.info('crypto', 'Using encryption key from environment variable');
|
||||
return encryptionKey;
|
||||
}
|
||||
|
||||
@@ -75,16 +76,16 @@ function loadOrCreateKey() {
|
||||
const keyData = fs.readFileSync(KEY_FILE, 'utf8').trim();
|
||||
if (keyData.length >= 64) {
|
||||
encryptionKey = Buffer.from(keyData, 'hex');
|
||||
console.log('[Crypto] Loaded encryption key from file');
|
||||
log.info('crypto', 'Loaded encryption key from file');
|
||||
// First-run bootstrap: if .bak doesn't exist yet, write the current
|
||||
// key to it. This ensures the silent recovery path is available from
|
||||
// the very next restart without requiring an explicit rotateKey().
|
||||
if (!fs.existsSync(KEY_FILE + '.bak')) {
|
||||
try {
|
||||
fs.writeFileSync(KEY_FILE + '.bak', keyData, { mode: 0o600 });
|
||||
console.log(`[Crypto] Seeded ${KEY_FILE}.bak with current key for future fallback`);
|
||||
log.info('crypto', 'Seeded .bak key file for future fallback');
|
||||
} catch (e) {
|
||||
console.warn('[Crypto] Could not seed .bak key file:', e.message);
|
||||
log.warn('crypto', 'Could not seed .bak key file', { error: e.message });
|
||||
}
|
||||
}
|
||||
// Try fallback to .bak key if primary can't decrypt existing credentials.
|
||||
@@ -98,14 +99,14 @@ function loadOrCreateKey() {
|
||||
encryptionKey = tryFallbackToBackupKey(Buffer.from(keyData, 'hex'), Buffer.from(backupData, 'hex'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Crypto] Could not check backup key:', e.message);
|
||||
log.warn('crypto', 'Could not check backup key', { error: e.message });
|
||||
}
|
||||
}
|
||||
return encryptionKey;
|
||||
}
|
||||
// File exists but key is invalid/empty - will generate new one below
|
||||
} catch (error) {
|
||||
console.error('[Crypto] Error loading key file:', error.message);
|
||||
log.error('crypto', error, { operation: 'loadKey' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +116,10 @@ function loadOrCreateKey() {
|
||||
try {
|
||||
// Save key to file with restricted permissions
|
||||
fs.writeFileSync(KEY_FILE, encryptionKey.toString('hex'), { mode: 0o600 });
|
||||
console.log('[Crypto] Generated and saved new encryption key');
|
||||
log.info('crypto', 'Generated and saved new encryption key');
|
||||
} catch (error) {
|
||||
console.warn('[Crypto] Could not save key to file:', error.message);
|
||||
console.warn('[Crypto] Key will be regenerated on restart - credentials will need to be re-entered');
|
||||
log.warn('crypto', 'Could not save key to file', { error: error.message });
|
||||
log.warn('crypto', 'Key will be regenerated on restart - credentials will need to be re-entered');
|
||||
}
|
||||
|
||||
return encryptionKey;
|
||||
@@ -171,12 +172,7 @@ function tryFallbackToBackupKey(primaryKey, backupKey) {
|
||||
|
||||
if (tryDecrypt(primaryKey)) return primaryKey;
|
||||
if (tryDecrypt(backupKey)) {
|
||||
console.warn(
|
||||
'[Crypto] Primary encryption key failed to decrypt credentials; ' +
|
||||
'fell back to .encryption-key.bak. The current primary key was set ' +
|
||||
'without preserving the original. Consider rotating the key explicitly ' +
|
||||
'via the credential-manager API to avoid this warning next restart.'
|
||||
);
|
||||
log.warn('crypto', 'Primary encryption key failed to decrypt credentials; fell back to .encryption-key.bak. Consider rotating the key explicitly via the credential-manager API.');
|
||||
return backupKey;
|
||||
}
|
||||
return primaryKey; // neither works — credential-manager.diagnose() will report 'unreadable'
|
||||
@@ -291,7 +287,7 @@ function decryptFields(obj, fields = null) {
|
||||
try {
|
||||
result[field] = decrypt(result[field]);
|
||||
} catch (error) {
|
||||
console.error(`[Crypto] Failed to decrypt field '${field}':`, error.message);
|
||||
log.error('crypto', error, { field, operation: 'decryptField' });
|
||||
// Leave the field as-is if decryption fails
|
||||
}
|
||||
}
|
||||
@@ -315,7 +311,7 @@ function migrateToEncrypted(credentials, sensitiveFields) {
|
||||
return credentials; // Already encrypted
|
||||
}
|
||||
|
||||
console.log('[Crypto] Migrating plaintext credentials to encrypted format');
|
||||
log.info('crypto', 'Migrating plaintext credentials to encrypted format');
|
||||
return encryptFields(credentials, sensitiveFields);
|
||||
}
|
||||
|
||||
@@ -340,10 +336,10 @@ function readEncryptedFile(filePath, sensitiveFields = ['password', 'token', 'ap
|
||||
}
|
||||
|
||||
// Plain text data - migrate it
|
||||
console.log(`[Crypto] Found plaintext data in ${filePath}, will encrypt on next save`);
|
||||
log.info('crypto', 'Found plaintext data', { filePath });
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.error(`[Crypto] Error reading ${filePath}:`, error.message);
|
||||
log.error('crypto', error, { filePath, operation: 'readFile' });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -357,7 +353,7 @@ function readEncryptedFile(filePath, sensitiveFields = ['password', 'token', 'ap
|
||||
function writeEncryptedFile(filePath, credentials, sensitiveFields = ['password', 'token', 'apiKey', 'secret']) {
|
||||
const encrypted = encryptFields(credentials, sensitiveFields);
|
||||
fs.writeFileSync(filePath, JSON.stringify(encrypted, null, 2), 'utf8');
|
||||
console.log(`[Crypto] Saved encrypted credentials to ${filePath}`);
|
||||
log.info('crypto', 'Saved encrypted credentials', { filePath });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -377,7 +373,7 @@ function rotateKey() {
|
||||
try {
|
||||
fs.writeFileSync(KEY_FILE + '.bak', oldKey.toString('hex'), { mode: 0o600 });
|
||||
} catch (error) {
|
||||
console.warn(`[Crypto] Could not save backup key to ${KEY_FILE}.bak:`, error.message);
|
||||
log.warn('crypto', 'Could not save backup key', { error: error.message });
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -216,14 +216,14 @@ function csrfValidationMiddleware(req, res, next) {
|
||||
|
||||
// Validate both values exist
|
||||
if (!cookieNonce) {
|
||||
console.warn(`[CSRF] Missing CSRF cookie: ${method} ${req.path} from ${req.ip}`);
|
||||
process.stderr.write(`[CSRF] Missing CSRF cookie: ${method} ${req.path} from ${req.ip}\n`);
|
||||
return errorResponse(res, 403, '[DC-100] CSRF token missing', {
|
||||
message: 'CSRF cookie not found. Please refresh the page (Ctrl+Shift+R) and try again.'
|
||||
});
|
||||
}
|
||||
|
||||
if (!headerToken) {
|
||||
console.warn(`[CSRF] Missing CSRF header: ${method} ${req.path} from ${req.ip}`);
|
||||
process.stderr.write(`[CSRF] Missing CSRF header: ${method} ${req.path} from ${req.ip}\n`);
|
||||
return errorResponse(res, 403, '[DC-100] CSRF token missing', {
|
||||
message: 'CSRF token not provided in request headers. Please refresh the page (Ctrl+Shift+R) and try again.'
|
||||
});
|
||||
@@ -247,7 +247,7 @@ function csrfValidationMiddleware(req, res, next) {
|
||||
next();
|
||||
|
||||
} catch (err) {
|
||||
console.warn(`[CSRF] Invalid CSRF token: ${method} ${req.path} from ${req.ip} - ${err.message}`);
|
||||
process.stderr.write(`[CSRF] Invalid CSRF token: ${method} ${req.path} from ${req.ip} - ${err.message}\n`);
|
||||
return errorResponse(res, 403, '[DC-101] CSRF token invalid', {
|
||||
message: 'CSRF token validation failed. Please refresh the page (Ctrl+Shift+R) and try again.'
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ const path = require('path');
|
||||
const https = require('https');
|
||||
const Docker = require('dockerode');
|
||||
const platformPaths = require('../../platform-paths');
|
||||
const { log } = require('../utils/logging');
|
||||
|
||||
const docker = new Docker();
|
||||
|
||||
@@ -19,7 +20,7 @@ class DockerSecurity {
|
||||
constructor() {
|
||||
this.config = this.loadConfig();
|
||||
this.mode = VERIFICATION_MODE;
|
||||
console.log(`[DockerSecurity] Initialized in ${this.mode} mode`);
|
||||
log.info('security', 'Docker security initialized', { mode: this.mode });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -32,7 +33,7 @@ class DockerSecurity {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`[DockerSecurity] Failed to load config: ${error.message}`);
|
||||
log.warn('security', 'Failed to load config', { error: error.message });
|
||||
}
|
||||
|
||||
// Default configuration
|
||||
@@ -51,7 +52,7 @@ class DockerSecurity {
|
||||
try {
|
||||
fs.writeFileSync(SECURITY_CONFIG_FILE, JSON.stringify(this.config, null, 2));
|
||||
} catch (error) {
|
||||
console.error(`[DockerSecurity] Failed to save config: ${error.message}`);
|
||||
log.error('security', error, { operation: 'saveConfig' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +111,7 @@ class DockerSecurity {
|
||||
repository = repository.split(':')[0];
|
||||
}
|
||||
|
||||
console.log(`[DockerSecurity] Fetching manifest for ${registry}/${repository}:${tag}`);
|
||||
log.info('security', 'Fetching manifest', { registry, repository, tag });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const isDockerHub = registry === 'registry-1.docker.io';
|
||||
@@ -216,7 +217,7 @@ class DockerSecurity {
|
||||
if (this.config.updateTrustedOnPull) {
|
||||
this.config.trustedDigests[imageName] = actualDigest;
|
||||
this.saveConfig();
|
||||
console.log(`[DockerSecurity] Added trusted digest for ${imageName}`);
|
||||
log.info('security', 'Added trusted digest', { imageName });
|
||||
}
|
||||
}
|
||||
} else if (actualDigest === trustedDigest) {
|
||||
@@ -250,26 +251,26 @@ class DockerSecurity {
|
||||
* @returns {Promise<object>} Verification result
|
||||
*/
|
||||
async verifyPulledImage(imageName) {
|
||||
console.log(`[DockerSecurity] Verifying image: ${imageName}`);
|
||||
log.info('security', 'Verifying image', { imageName });
|
||||
|
||||
try {
|
||||
const actualDigest = await this.getImageDigest(imageName);
|
||||
const result = await this.verifyImageDigest(imageName, actualDigest);
|
||||
|
||||
if (result.action === 'reject') {
|
||||
console.error(`[DockerSecurity] REJECTED: ${result.reason}`);
|
||||
log.error('security', 'Image REJECTED', { imageName, reason: result.reason });
|
||||
throw new Error(`Image verification failed: ${result.reason}`);
|
||||
} else if (result.action === 'warn') {
|
||||
console.warn(`[DockerSecurity] WARNING: ${result.reason}`);
|
||||
console.warn(`[DockerSecurity] Expected: ${result.trustedDigest}`);
|
||||
console.warn(`[DockerSecurity] Actual: ${result.actualDigest}`);
|
||||
log.warn('security', 'Image WARNING', { imageName, reason: result.reason });
|
||||
log.warn('security', 'Expected digest', { imageName, digest: result.trustedDigest });
|
||||
log.warn('security', 'Actual digest', { imageName, digest: result.actualDigest });
|
||||
} else {
|
||||
console.log(`[DockerSecurity] ACCEPTED: ${result.reason}`);
|
||||
log.info('security', 'Image ACCEPTED', { imageName, reason: result.reason });
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`[DockerSecurity] Verification error: ${error.message}`);
|
||||
log.error('security', error, { imageName, operation: 'verify' });
|
||||
|
||||
if (this.mode === 'strict') {
|
||||
throw error;
|
||||
@@ -294,7 +295,7 @@ class DockerSecurity {
|
||||
setTrustedDigest(imageName, digest) {
|
||||
this.config.trustedDigests[imageName] = digest;
|
||||
this.saveConfig();
|
||||
console.log(`[DockerSecurity] Updated trusted digest for ${imageName}`);
|
||||
log.info('security', 'Updated trusted digest', { imageName });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,7 +305,7 @@ class DockerSecurity {
|
||||
removeTrustedDigest(imageName) {
|
||||
delete this.config.trustedDigests[imageName];
|
||||
this.saveConfig();
|
||||
console.log(`[DockerSecurity] Removed trusted digest for ${imageName}`);
|
||||
log.info('security', 'Removed trusted digest', { imageName });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,7 +326,7 @@ class DockerSecurity {
|
||||
this.mode = mode;
|
||||
this.config.verificationMode = mode;
|
||||
this.saveConfig();
|
||||
console.log(`[DockerSecurity] Verification mode set to: ${mode}`);
|
||||
log.info('security', 'Verification mode set', { mode });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const { log } = require('../utils/logging');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const platformPaths = require('../../platform-paths');
|
||||
@@ -95,7 +96,7 @@ function createTail({ filePath, stateFile, onLine, label = 'tail', pollMs = 1000
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
try { onLine(line); } catch (e) {
|
||||
console.error(`[${label}] onLine threw:`, e.message);
|
||||
log.error('events', e, { worker: label, phase: 'onLine' });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,7 +107,7 @@ function createTail({ filePath, stateFile, onLine, label = 'tail', pollMs = 1000
|
||||
setTimeout(tick, pollMs);
|
||||
});
|
||||
stream.on('error', (e) => {
|
||||
console.error(`[${label}] read error:`, e.message);
|
||||
log.error('events', e, { worker: label, phase: 'read' });
|
||||
setTimeout(tick, pollMs * 5);
|
||||
});
|
||||
});
|
||||
@@ -267,11 +268,11 @@ function startFail2banWorker({ log } = {}) {
|
||||
function startAll({ log } = {}) {
|
||||
const workers = [];
|
||||
try { workers.push(startCaddyWorker({ log })); }
|
||||
catch (e) { console.error('[workers] caddy worker failed to start:', e.message); }
|
||||
catch (e) { log.error('events', e, { worker: 'caddy', phase: 'start' }); }
|
||||
try { workers.push(startSharedBansWorker({ log })); }
|
||||
catch (e) { console.error('[workers] shared_bans worker failed to start:', e.message); }
|
||||
catch (e) { log.error('events', e, { worker: 'shared_bans', phase: 'start' }); }
|
||||
try { workers.push(startFail2banWorker({ log })); }
|
||||
catch (e) { console.error('[workers] fail2ban worker failed to start:', e.message); }
|
||||
catch (e) { log.error('events', e, { worker: 'fail2ban', phase: 'start' }); }
|
||||
return {
|
||||
stop() { workers.forEach(w => { try { w.stop(); } catch {} }); },
|
||||
workers,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
const { execSync, execFileSync } = require('child_process');
|
||||
const os = require('os');
|
||||
const crypto = require('crypto');
|
||||
const { log } = require('../utils/logging');
|
||||
|
||||
const SERVICE_NAME = 'DashCaddy';
|
||||
const ACCOUNT_PREFIX = 'dashcaddy';
|
||||
@@ -44,7 +45,7 @@ class KeychainManager {
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
console.warn('[Keychain] OS keychain not available, will use encrypted file storage');
|
||||
log.warn('keychain', 'OS keychain not available, will use encrypted file storage');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -72,7 +73,7 @@ class KeychainManager {
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`[Keychain] Failed to store ${key}:`, error.message);
|
||||
log.error('keychain', error, { key, operation: 'store' });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -99,7 +100,7 @@ class KeychainManager {
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error(`[Keychain] Failed to retrieve ${key}:`, error.message);
|
||||
log.error('keychain', error, { key, operation: 'retrieve' });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -126,7 +127,7 @@ class KeychainManager {
|
||||
}
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.error(`[Keychain] Failed to delete ${key}:`, error.message);
|
||||
log.error('keychain', error, { key, operation: 'delete' });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ const fs = require('fs');
|
||||
const fsp = require('fs').promises;
|
||||
const path = require('path');
|
||||
const { DOCKER } = require('../utilities/constants');
|
||||
const { log } = require('../utils/logging');
|
||||
|
||||
const docker = new Docker();
|
||||
|
||||
@@ -63,7 +64,7 @@ class LogDigest extends EventEmitter {
|
||||
// Collect logs every hour
|
||||
this.collectInterval = setInterval(() => {
|
||||
this._collectHourlyLogs().catch(e =>
|
||||
console.error('[LogDigest] Hourly collection failed:', e.message)
|
||||
log.error('logdigest', e, { phase: 'hourlyCollect' })
|
||||
);
|
||||
}, DOCKER.DIGEST.COLLECT_INTERVAL);
|
||||
|
||||
@@ -71,7 +72,7 @@ class LogDigest extends EventEmitter {
|
||||
this._scheduleDailyDigest();
|
||||
|
||||
// Run initial collection after 2 minutes
|
||||
setTimeout(() => {
|
||||
this._initialTimeout = setTimeout(() => {
|
||||
if (this.running) {
|
||||
this._collectHourlyLogs().catch(() => {});
|
||||
}
|
||||
@@ -89,6 +90,10 @@ class LogDigest extends EventEmitter {
|
||||
clearTimeout(this.digestTimeout);
|
||||
this.digestTimeout = null;
|
||||
}
|
||||
if (this._initialTimeout) {
|
||||
clearTimeout(this._initialTimeout);
|
||||
this._initialTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,7 +200,7 @@ class LogDigest extends EventEmitter {
|
||||
hourSummary.services[appId] = serviceSummary;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[LogDigest] Container enumeration failed:', e.message);
|
||||
log.error('logdigest', e, { phase: 'enumerateContainers' });
|
||||
}
|
||||
|
||||
// Add to ring buffer
|
||||
@@ -258,7 +263,7 @@ class LogDigest extends EventEmitter {
|
||||
const delay = next.getTime() - now.getTime();
|
||||
this.digestTimeout = setTimeout(() => {
|
||||
this.generateDailyDigest().catch(e =>
|
||||
console.error('[LogDigest] Daily digest generation failed:', e.message)
|
||||
log.error('logdigest', e, { phase: 'dailyDigest' })
|
||||
);
|
||||
// Reschedule for tomorrow
|
||||
if (this.running) this._scheduleDailyDigest();
|
||||
|
||||
Reference in New Issue
Block a user