+34
@@ -25,6 +25,7 @@ import { assembleFinalReport } from './src/phases/reporting.js';
|
|||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import { timingResults, costResults, displayTimingSummary, Timer, formatDuration } from './src/utils/metrics.js';
|
import { timingResults, costResults, displayTimingSummary, Timer, formatDuration } from './src/utils/metrics.js';
|
||||||
|
import { setupLogging } from './src/utils/logger.js';
|
||||||
|
|
||||||
// CLI
|
// CLI
|
||||||
import { handleDeveloperCommand } from './src/cli/command-handler.js';
|
import { handleDeveloperCommand } from './src/cli/command-handler.js';
|
||||||
@@ -44,16 +45,21 @@ import {
|
|||||||
// Configure zx to disable timeouts (let tools run as long as needed)
|
// Configure zx to disable timeouts (let tools run as long as needed)
|
||||||
$.timeout = 0;
|
$.timeout = 0;
|
||||||
|
|
||||||
|
// Global cleanup function for logging
|
||||||
|
let cleanupLogging = null;
|
||||||
|
|
||||||
// Setup graceful cleanup on process signals
|
// Setup graceful cleanup on process signals
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
console.log(chalk.yellow('\n⚠️ Received SIGINT, cleaning up...'));
|
console.log(chalk.yellow('\n⚠️ Received SIGINT, cleaning up...'));
|
||||||
await cleanupMCP();
|
await cleanupMCP();
|
||||||
|
if (cleanupLogging) await cleanupLogging();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
process.on('SIGTERM', async () => {
|
||||||
console.log(chalk.yellow('\n⚠️ Received SIGTERM, cleaning up...'));
|
console.log(chalk.yellow('\n⚠️ Received SIGTERM, cleaning up...'));
|
||||||
await cleanupMCP();
|
await cleanupMCP();
|
||||||
|
if (cleanupLogging) await cleanupLogging();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -377,6 +383,7 @@ if (args[0] && args[0].includes('shannon.mjs')) {
|
|||||||
// Parse flags and arguments
|
// Parse flags and arguments
|
||||||
let configPath = null;
|
let configPath = null;
|
||||||
let pipelineTestingMode = false;
|
let pipelineTestingMode = false;
|
||||||
|
let logFilePath = null;
|
||||||
const nonFlagArgs = [];
|
const nonFlagArgs = [];
|
||||||
let developerCommand = null;
|
let developerCommand = null;
|
||||||
const developerCommands = ['--run-phase', '--run-all', '--rollback-to', '--rerun', '--status', '--list-agents', '--cleanup'];
|
const developerCommands = ['--run-phase', '--run-all', '--rollback-to', '--rerun', '--status', '--list-agents', '--cleanup'];
|
||||||
@@ -390,6 +397,16 @@ for (let i = 0; i < args.length; i++) {
|
|||||||
console.log(chalk.red('❌ --config flag requires a file path'));
|
console.log(chalk.red('❌ --config flag requires a file path'));
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
} else if (args[i] === '--log') {
|
||||||
|
// --log can optionally take a file path, otherwise use default
|
||||||
|
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
||||||
|
logFilePath = args[i + 1];
|
||||||
|
i++; // Skip the next argument
|
||||||
|
} else {
|
||||||
|
// Generate default log filename with timestamp
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
||||||
|
logFilePath = `shannon-${timestamp}.log`;
|
||||||
|
}
|
||||||
} else if (args[i] === '--pipeline-testing') {
|
} else if (args[i] === '--pipeline-testing') {
|
||||||
pipelineTestingMode = true;
|
pipelineTestingMode = true;
|
||||||
} else if (developerCommands.includes(args[i])) {
|
} else if (developerCommands.includes(args[i])) {
|
||||||
@@ -416,10 +433,25 @@ if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup logging if --log flag is present
|
||||||
|
if (logFilePath) {
|
||||||
|
try {
|
||||||
|
cleanupLogging = await setupLogging(logFilePath);
|
||||||
|
const absoluteLogPath = path.isAbsolute(logFilePath)
|
||||||
|
? logFilePath
|
||||||
|
: path.join(process.cwd(), logFilePath);
|
||||||
|
console.log(chalk.green(`📝 Logging enabled: ${absoluteLogPath}`));
|
||||||
|
} catch (error) {
|
||||||
|
console.log(chalk.yellow(`⚠️ Failed to setup logging: ${error.message}`));
|
||||||
|
console.log(chalk.gray('Continuing without logging...'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle developer commands
|
// Handle developer commands
|
||||||
if (developerCommand) {
|
if (developerCommand) {
|
||||||
await handleDeveloperCommand(developerCommand, nonFlagArgs, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
await handleDeveloperCommand(developerCommand, nonFlagArgs, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||||
await cleanupMCP();
|
await cleanupMCP();
|
||||||
|
if (cleanupLogging) await cleanupLogging();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,6 +502,7 @@ try {
|
|||||||
console.log(chalk.green.bold('\n📄 FINAL REPORT AVAILABLE:'));
|
console.log(chalk.green.bold('\n📄 FINAL REPORT AVAILABLE:'));
|
||||||
console.log(chalk.cyan(finalReportPath));
|
console.log(chalk.cyan(finalReportPath));
|
||||||
await cleanupMCP();
|
await cleanupMCP();
|
||||||
|
if (cleanupLogging) await cleanupLogging();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Enhanced error boundary with proper logging
|
// Enhanced error boundary with proper logging
|
||||||
if (error instanceof PentestError) {
|
if (error instanceof PentestError) {
|
||||||
@@ -490,5 +523,6 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await cleanupMCP();
|
await cleanupMCP();
|
||||||
|
if (cleanupLogging) await cleanupLogging();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ export function showHelp() {
|
|||||||
|
|
||||||
console.log(chalk.yellow.bold('OPTIONS:'));
|
console.log(chalk.yellow.bold('OPTIONS:'));
|
||||||
console.log(' --config <file> YAML configuration file for authentication and testing parameters');
|
console.log(' --config <file> YAML configuration file for authentication and testing parameters');
|
||||||
|
console.log(' --log [file] Capture all output to log file (default: shannon-<timestamp>.log)');
|
||||||
console.log(' --pipeline-testing Use minimal prompts for fast pipeline testing (creates minimal deliverables)\n');
|
console.log(' --pipeline-testing Use minimal prompts for fast pipeline testing (creates minimal deliverables)\n');
|
||||||
|
|
||||||
console.log(chalk.yellow.bold('DEVELOPER COMMANDS:'));
|
console.log(chalk.yellow.bold('DEVELOPER COMMANDS:'));
|
||||||
@@ -36,6 +37,7 @@ export function showHelp() {
|
|||||||
console.log(' # Normal mode - create new session');
|
console.log(' # Normal mode - create new session');
|
||||||
console.log(' ./shannon.mjs "https://example.com" "/path/to/local/repo"');
|
console.log(' ./shannon.mjs "https://example.com" "/path/to/local/repo"');
|
||||||
console.log(' ./shannon.mjs "https://example.com" "/path/to/local/repo" --config auth.yaml');
|
console.log(' ./shannon.mjs "https://example.com" "/path/to/local/repo" --config auth.yaml');
|
||||||
|
console.log(' ./shannon.mjs "https://example.com" "/path/to/local/repo" --log pentest.log');
|
||||||
console.log(' ./shannon.mjs "https://example.com" "/path/to/local/repo" --setup-only # Setup only\n');
|
console.log(' ./shannon.mjs "https://example.com" "/path/to/local/repo" --setup-only # Setup only\n');
|
||||||
|
|
||||||
console.log(' # Developer mode - operate on existing session');
|
console.log(' # Developer mode - operate on existing session');
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { fs } from 'zx';
|
||||||
|
import { path } from 'zx';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips ANSI escape codes from a string
|
||||||
|
* @param {string} str - String with ANSI codes
|
||||||
|
* @returns {string} Clean string without ANSI codes
|
||||||
|
*/
|
||||||
|
function stripAnsi(str) {
|
||||||
|
if (typeof str !== 'string') {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove ANSI escape sequences
|
||||||
|
// This regex matches all common ANSI codes including:
|
||||||
|
// - Colors (e.g., \x1b[32m)
|
||||||
|
// - Cursor movement (e.g., \x1b[1;1H)
|
||||||
|
// - Screen clearing (e.g., \x1b[0J)
|
||||||
|
// - 256-color codes (e.g., \x1b[38;2;244;197;66m)
|
||||||
|
return str.replace(
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][0-9];.*?\x07|\x1b\[[\d;]*m/g,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up logging to capture all stdout and stderr to a file
|
||||||
|
* @param {string} logFilePath - Path to the log file
|
||||||
|
* @returns {Promise<Function>} Cleanup function to restore original streams
|
||||||
|
*/
|
||||||
|
export async function setupLogging(logFilePath) {
|
||||||
|
// Resolve to absolute path
|
||||||
|
const absoluteLogPath = path.isAbsolute(logFilePath)
|
||||||
|
? logFilePath
|
||||||
|
: path.join(process.cwd(), logFilePath);
|
||||||
|
|
||||||
|
// Ensure the directory exists
|
||||||
|
await fs.ensureDir(path.dirname(absoluteLogPath));
|
||||||
|
|
||||||
|
// Create write stream for the log file
|
||||||
|
const logStream = fs.createWriteStream(absoluteLogPath, { flags: 'a' });
|
||||||
|
|
||||||
|
// Buffer for lines that might be overwritten (carriage return without newline)
|
||||||
|
let stdoutBuffer = '';
|
||||||
|
let stderrBuffer = '';
|
||||||
|
|
||||||
|
// Store original stdout/stderr write functions
|
||||||
|
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
||||||
|
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
||||||
|
|
||||||
|
// Override stdout
|
||||||
|
process.stdout.write = function(chunk, encoding, callback) {
|
||||||
|
// Write colorized output to terminal
|
||||||
|
originalStdoutWrite(chunk, encoding, callback);
|
||||||
|
|
||||||
|
// Write plain text (without ANSI codes) to log file
|
||||||
|
const cleanChunk = stripAnsi(chunk.toString());
|
||||||
|
|
||||||
|
// Handle carriage returns - only log when we get a newline
|
||||||
|
if (cleanChunk.includes('\r') && !cleanChunk.includes('\n')) {
|
||||||
|
// Buffer this line - it will be overwritten in terminal
|
||||||
|
stdoutBuffer = cleanChunk.replace(/\r/g, '');
|
||||||
|
} else if (cleanChunk.includes('\n')) {
|
||||||
|
// Flush buffer if exists, then write the new line
|
||||||
|
if (stdoutBuffer) {
|
||||||
|
stdoutBuffer = ''; // Clear buffer without writing (it was overwritten)
|
||||||
|
}
|
||||||
|
logStream.write(cleanChunk);
|
||||||
|
} else {
|
||||||
|
// Normal write
|
||||||
|
logStream.write(cleanChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Override stderr
|
||||||
|
process.stderr.write = function(chunk, encoding, callback) {
|
||||||
|
// Write colorized output to terminal
|
||||||
|
originalStderrWrite(chunk, encoding, callback);
|
||||||
|
|
||||||
|
// Write plain text (without ANSI codes) to log file
|
||||||
|
const cleanChunk = stripAnsi(chunk.toString());
|
||||||
|
|
||||||
|
// Handle carriage returns - only log when we get a newline
|
||||||
|
if (cleanChunk.includes('\r') && !cleanChunk.includes('\n')) {
|
||||||
|
// Buffer this line - it will be overwritten in terminal
|
||||||
|
stderrBuffer = cleanChunk.replace(/\r/g, '');
|
||||||
|
} else if (cleanChunk.includes('\n')) {
|
||||||
|
// Flush buffer if exists, then write the new line
|
||||||
|
if (stderrBuffer) {
|
||||||
|
stderrBuffer = ''; // Clear buffer without writing (it was overwritten)
|
||||||
|
}
|
||||||
|
logStream.write(cleanChunk);
|
||||||
|
} else {
|
||||||
|
// Normal write
|
||||||
|
logStream.write(cleanChunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Return cleanup function
|
||||||
|
return async function cleanup() {
|
||||||
|
// Restore original streams
|
||||||
|
process.stdout.write = originalStdoutWrite;
|
||||||
|
process.stderr.write = originalStderrWrite;
|
||||||
|
|
||||||
|
// Flush any remaining buffers
|
||||||
|
if (stdoutBuffer) {
|
||||||
|
logStream.write(stdoutBuffer + '\n');
|
||||||
|
}
|
||||||
|
if (stderrBuffer) {
|
||||||
|
logStream.write(stderrBuffer + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the log stream
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
logStream.end((err) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
else resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user