mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 06:16:37 +00:00
fix(logging): redact CLI argv in lifecycle start logs
Squash merge PR #1297 into dev.
This commit is contained in:
+2
-1
@@ -6,6 +6,7 @@ import { fail } from './utils/ui';
|
||||
import { handleError, runCleanup } from './errors';
|
||||
|
||||
import { createLogger, runWithRequestId } from './services/logging';
|
||||
import { redactArgv } from './services/logging/log-redaction';
|
||||
// Import target adapter system
|
||||
import { registerTarget, ClaudeAdapter, DroidAdapter, CodexAdapter } from './targets';
|
||||
|
||||
@@ -143,7 +144,7 @@ const cliEntryStartedAt = Date.now();
|
||||
const cliEntryLogger = createLogger('cli:entry');
|
||||
runWithRequestId(() => {
|
||||
cliEntryLogger.stage('intake', 'cli.command.start', 'CLI invocation started', {
|
||||
argv: process.argv.slice(2),
|
||||
argv: redactArgv(process.argv.slice(2)),
|
||||
});
|
||||
return main()
|
||||
.then(() => {
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { createEmptyUnifiedConfig } from '../../../../src/config/unified-config-types';
|
||||
import { saveUnifiedConfig } from '../../../../src/config/unified-config-loader';
|
||||
import {
|
||||
clearRecentLogEntries,
|
||||
getRecentLogEntries,
|
||||
} from '../../../../src/services/logging/log-buffer';
|
||||
import { invalidateLoggingConfigCache } from '../../../../src/services/logging/log-config';
|
||||
|
||||
let originalArgv: string[] = [];
|
||||
let originalCcsHome: string | undefined;
|
||||
let tempHome = '';
|
||||
let baselineSigintListeners: Array<(...args: unknown[]) => void> = [];
|
||||
let baselineSigtermListeners: Array<(...args: unknown[]) => void> = [];
|
||||
let baselineUncaughtExceptionListeners: Array<(...args: unknown[]) => void> = [];
|
||||
let baselineUnhandledRejectionListeners: Array<(...args: unknown[]) => void> = [];
|
||||
|
||||
function removeNewListeners(
|
||||
event: 'SIGINT' | 'SIGTERM' | 'uncaughtException' | 'unhandledRejection',
|
||||
baseline: Array<(...args: unknown[]) => void>
|
||||
): void {
|
||||
for (const listener of process.listeners(event)) {
|
||||
if (!baseline.includes(listener as (...args: unknown[]) => void)) {
|
||||
process.removeListener(event, listener as (...args: unknown[]) => void);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
originalArgv = process.argv.slice();
|
||||
originalCcsHome = process.env.CCS_HOME;
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cli-entry-log-'));
|
||||
process.env.CCS_HOME = tempHome;
|
||||
process.argv = [
|
||||
'bun',
|
||||
'src/ccs.ts',
|
||||
'launch',
|
||||
'--api-key',
|
||||
'secret-key',
|
||||
'--mode',
|
||||
'prod',
|
||||
'--secret',
|
||||
'top-secret',
|
||||
];
|
||||
|
||||
clearRecentLogEntries();
|
||||
invalidateLoggingConfigCache();
|
||||
const config = createEmptyUnifiedConfig();
|
||||
config.logging = {
|
||||
...config.logging,
|
||||
enabled: true,
|
||||
level: 'debug',
|
||||
redact: false,
|
||||
};
|
||||
saveUnifiedConfig(config);
|
||||
invalidateLoggingConfigCache();
|
||||
|
||||
baselineSigintListeners = process.listeners('SIGINT');
|
||||
baselineSigtermListeners = process.listeners('SIGTERM');
|
||||
baselineUncaughtExceptionListeners = process.listeners('uncaughtException');
|
||||
baselineUnhandledRejectionListeners = process.listeners('unhandledRejection');
|
||||
|
||||
mock.module('../../../../src/utils/fetch-proxy-setup', () => ({}));
|
||||
mock.module('../../../../src/utils/error-manager', () => ({
|
||||
ErrorManager: class ErrorManager {
|
||||
static async showProfileNotFound(): Promise<void> {}
|
||||
},
|
||||
}));
|
||||
mock.module('../../../../src/utils/ui', () => ({
|
||||
fail: (message: string) => message,
|
||||
}));
|
||||
mock.module('../../../../src/errors', () => ({
|
||||
handleError: () => {},
|
||||
runCleanup: () => {},
|
||||
}));
|
||||
mock.module('../../../../src/targets', () => ({
|
||||
registerTarget: () => {},
|
||||
ClaudeAdapter: class ClaudeAdapter {},
|
||||
DroidAdapter: class DroidAdapter {},
|
||||
CodexAdapter: class CodexAdapter {},
|
||||
}));
|
||||
mock.module('../../../../src/dispatcher/cli-argument-parser', () => ({
|
||||
bootstrapAndParseEarlyCli: async () => ({
|
||||
exitNow: true,
|
||||
args: [],
|
||||
browserLaunchOverride: undefined,
|
||||
}),
|
||||
}));
|
||||
mock.module('../../../../src/dispatcher/pre-dispatch', () => ({
|
||||
runPreDispatchHandlers: async () => false,
|
||||
}));
|
||||
mock.module('../../../../src/dispatcher/profile-resolver', () => ({
|
||||
resolveProfileAndTarget: async () => {
|
||||
throw new Error('resolveProfileAndTarget should not be called in this test');
|
||||
},
|
||||
}));
|
||||
mock.module('../../../../src/dispatcher/target-executor', () => ({
|
||||
dispatchProfile: async () => {
|
||||
throw new Error('dispatchProfile should not be called in this test');
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
process.argv = originalArgv;
|
||||
if (originalCcsHome === undefined) delete process.env.CCS_HOME;
|
||||
else process.env.CCS_HOME = originalCcsHome;
|
||||
|
||||
removeNewListeners('SIGINT', baselineSigintListeners);
|
||||
removeNewListeners('SIGTERM', baselineSigtermListeners);
|
||||
removeNewListeners('uncaughtException', baselineUncaughtExceptionListeners);
|
||||
removeNewListeners('unhandledRejection', baselineUnhandledRejectionListeners);
|
||||
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
clearRecentLogEntries();
|
||||
invalidateLoggingConfigCache();
|
||||
});
|
||||
|
||||
async function loadCliEntryModule(): Promise<void> {
|
||||
await import(`../../../../src/ccs?test=${Date.now()}-${Math.random()}`);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
}
|
||||
|
||||
describe('CLI entry log redaction', () => {
|
||||
it('redacts sensitive argv values before emitting the lifecycle start log', async () => {
|
||||
await loadCliEntryModule();
|
||||
|
||||
const startEntry = getRecentLogEntries().find(
|
||||
(entry) => entry.source === 'cli:entry' && entry.event === 'cli.command.start'
|
||||
);
|
||||
const completeEntry = getRecentLogEntries().find(
|
||||
(entry) => entry.source === 'cli:entry' && entry.event === 'cli.command.complete'
|
||||
);
|
||||
|
||||
expect(startEntry).toBeDefined();
|
||||
expect(startEntry?.context).toEqual({
|
||||
argv: ['launch', '--api-key', '[redacted]', '--mode', 'prod', '--secret', '[redacted]'],
|
||||
});
|
||||
expect(startEntry?.requestId).toBeTruthy();
|
||||
expect(completeEntry?.requestId).toBe(startEntry?.requestId);
|
||||
|
||||
const serializedStartEntry = JSON.stringify(startEntry);
|
||||
expect(serializedStartEntry).not.toContain('secret-key');
|
||||
expect(serializedStartEntry).not.toContain('top-secret');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user