@@ -0,0 +1,136 @@
|
||||
import { fs, path, os } from 'zx';
|
||||
import chalk from 'chalk';
|
||||
import { PentestError, logError } from '../error-handling.js';
|
||||
|
||||
// Pure function: Save deliverables permanently to user directory
|
||||
export async function savePermanentDeliverables(sourceDir, webUrl, repoPath, session, timingBreakdown, costBreakdown) {
|
||||
try {
|
||||
// Simple universal approach - try Documents, fallback to home
|
||||
const homeDir = os.homedir();
|
||||
const documentsDir = path.join(homeDir, 'Documents');
|
||||
|
||||
// Use Documents if it exists, otherwise use home directory
|
||||
const baseDir = await fs.pathExists(documentsDir) ? documentsDir : homeDir;
|
||||
const permanentBaseDir = path.join(baseDir, 'pentest-deliverables');
|
||||
|
||||
// Generate directory name from repo path and web URL
|
||||
const repoName = path.basename(repoPath);
|
||||
const webDomain = new URL(webUrl).hostname.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace(/T/, '-').split('.')[0];
|
||||
const dirName = `${webDomain}_${repoName}_${timestamp}`;
|
||||
const permanentDir = path.join(permanentBaseDir, dirName);
|
||||
|
||||
// Ensure base directory exists
|
||||
await fs.ensureDir(permanentBaseDir);
|
||||
|
||||
// Create the specific pentest directory
|
||||
await fs.ensureDir(permanentDir);
|
||||
|
||||
// Copy deliverables folder if it exists
|
||||
const deliverablesSource = path.join(sourceDir, 'deliverables');
|
||||
const deliverablesDest = path.join(permanentDir, 'deliverables');
|
||||
|
||||
if (await fs.pathExists(deliverablesSource)) {
|
||||
await fs.copy(deliverablesSource, deliverablesDest, { overwrite: true });
|
||||
}
|
||||
|
||||
// Save metadata with session information
|
||||
const metadata = {
|
||||
session: {
|
||||
id: session.id,
|
||||
webUrl,
|
||||
repoPath,
|
||||
configFile: session.configFile,
|
||||
status: session.status,
|
||||
completedAgents: session.completedAgents,
|
||||
createdAt: session.createdAt,
|
||||
completedAt: new Date().toISOString()
|
||||
},
|
||||
timing: timingBreakdown,
|
||||
cost: costBreakdown,
|
||||
sourceDirectory: sourceDir,
|
||||
savedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
await fs.writeJSON(path.join(permanentDir, 'metadata.json'), metadata, { spaces: 2 });
|
||||
|
||||
// Copy prompts directory for reproducibility
|
||||
const promptsSource = path.join(import.meta.dirname, '..', '..', 'prompts');
|
||||
const promptsDest = path.join(permanentDir, 'prompts');
|
||||
|
||||
if (await fs.pathExists(promptsSource)) {
|
||||
await fs.copy(promptsSource, promptsDest, { overwrite: true });
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✅ Deliverables saved to permanent location: ${permanentDir}`));
|
||||
return permanentDir;
|
||||
} catch (error) {
|
||||
// Non-fatal error - log but don't throw
|
||||
console.log(chalk.yellow(`⚠️ Failed to save permanent deliverables: ${error.message}`));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Pure function: Save run metadata for debugging and reproducibility
|
||||
export async function saveRunMetadata(sourceDir, webUrl, repoPath) {
|
||||
console.log(chalk.blue('💾 Saving run metadata...'));
|
||||
|
||||
try {
|
||||
// Read package.json to get version info with error handling
|
||||
const packagePath = path.join(import.meta.dirname, '..', '..', 'package.json');
|
||||
let packageJson;
|
||||
try {
|
||||
packageJson = await fs.readJSON(packagePath);
|
||||
} catch (packageError) {
|
||||
throw new PentestError(
|
||||
`Cannot read package.json: ${packageError.message}`,
|
||||
'filesystem',
|
||||
false,
|
||||
{ packagePath, originalError: packageError.message }
|
||||
);
|
||||
}
|
||||
|
||||
const metadata = {
|
||||
timestamp: new Date().toISOString(),
|
||||
targets: { webUrl, repoPath },
|
||||
environment: {
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
cwd: process.cwd()
|
||||
},
|
||||
dependencies: {
|
||||
claudeCodeVersion: packageJson.dependencies?.['@anthropic-ai/claude-code'] || 'unknown',
|
||||
zxVersion: packageJson.dependencies?.['zx'] || 'unknown',
|
||||
chalkVersion: packageJson.dependencies?.['chalk'] || 'unknown'
|
||||
},
|
||||
execution: {
|
||||
args: process.argv,
|
||||
env: {
|
||||
PLAYWRIGHT_HEADLESS: process.env.PLAYWRIGHT_HEADLESS || 'true',
|
||||
NODE_ENV: process.env.NODE_ENV
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const metadataPath = path.join(sourceDir, 'run-metadata.json');
|
||||
await fs.writeJSON(metadataPath, metadata, { spaces: 2 });
|
||||
|
||||
console.log(chalk.green(`✅ Run metadata saved to: ${metadataPath}`));
|
||||
return metadata;
|
||||
} catch (error) {
|
||||
if (error instanceof PentestError) {
|
||||
await logError(error, 'Saving run metadata', sourceDir);
|
||||
throw error; // Re-throw PentestError to be handled by caller
|
||||
}
|
||||
|
||||
const metadataError = new PentestError(
|
||||
`Run metadata saving failed: ${error.message}`,
|
||||
'filesystem',
|
||||
false,
|
||||
{ sourceDir, originalError: error.message }
|
||||
);
|
||||
await logError(metadataError, 'Saving run metadata', sourceDir);
|
||||
throw metadataError;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { $, fs, path } from 'zx';
|
||||
import chalk from 'chalk';
|
||||
import { PentestError, logError } from '../error-handling.js';
|
||||
|
||||
// Pure function: Setup MCP with multiple isolated Playwright instances
|
||||
export async function setupMCP(sourceDir) {
|
||||
console.log(chalk.blue('🎭 Setting up 5 isolated Playwright MCP instances...'));
|
||||
|
||||
// Set headless mode for all instances
|
||||
process.env.PLAYWRIGHT_HEADLESS = 'true';
|
||||
|
||||
try {
|
||||
// Clean slate - remove any existing instances
|
||||
const instancesToRemove = ['playwright', ...Array.from({length: 5}, (_, i) => `playwright-agent${i + 1}`)];
|
||||
|
||||
for (const instance of instancesToRemove) {
|
||||
try {
|
||||
await $`claude mcp remove ${instance} --scope user 2>/dev/null`;
|
||||
} catch {
|
||||
// Silent ignore - instance might not exist
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure screenshot directories exist
|
||||
await fs.ensureDir(path.join(sourceDir, 'screenshots'));
|
||||
|
||||
// Create 5 isolated instances sequentially to avoid config conflicts
|
||||
for (let i = 1; i <= 5; i++) {
|
||||
const instanceName = `playwright-agent${i}`;
|
||||
const screenshotDir = path.join(sourceDir, 'screenshots', instanceName);
|
||||
const userDataDir = `/tmp/${instanceName}`;
|
||||
|
||||
// Ensure both directories exist
|
||||
await fs.ensureDir(screenshotDir);
|
||||
await fs.ensureDir(userDataDir);
|
||||
|
||||
try {
|
||||
await $`claude mcp add ${instanceName} --scope user -- npx @playwright/mcp@latest --isolated --user-data-dir ${userDataDir} --output-dir ${screenshotDir}`;
|
||||
console.log(chalk.green(` ✅ ${instanceName} configured`));
|
||||
} catch (error) {
|
||||
if (error.message?.includes('already exists')) {
|
||||
console.log(chalk.gray(` ⏭️ ${instanceName} already exists`));
|
||||
} else {
|
||||
console.log(chalk.yellow(` ⚠️ ${instanceName} failed: ${error.message}, continuing...`));
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(chalk.green('✅ All 5 Playwright MCP instances ready for parallel execution'));
|
||||
|
||||
} catch (error) {
|
||||
// All MCP setup failures are fatal
|
||||
const mcpError = new PentestError(
|
||||
`Critical MCP setup failure: ${error.message}. Browser automation required for pentesting.`,
|
||||
'tool',
|
||||
false,
|
||||
{ sourceDir, originalError: error.message }
|
||||
);
|
||||
await logError(mcpError, 'MCP setup failure', sourceDir);
|
||||
throw mcpError;
|
||||
}
|
||||
}
|
||||
|
||||
// Pure function: Cleanup MCP instances
|
||||
export async function cleanupMCP() {
|
||||
console.log(chalk.blue('🧹 Cleaning up Playwright MCP instances...'));
|
||||
|
||||
try {
|
||||
// Remove all instances (including legacy 'playwright' if it exists)
|
||||
const instancesToRemove = ['playwright', ...Array.from({length: 5}, (_, i) => `playwright-agent${i + 1}`)];
|
||||
|
||||
for (const instance of instancesToRemove) {
|
||||
try {
|
||||
await $`claude mcp remove ${instance} --scope user 2>/dev/null`;
|
||||
console.log(chalk.gray(` 🗑️ Removed ${instance}`));
|
||||
} catch {
|
||||
// Silent ignore - instance might not exist
|
||||
}
|
||||
}
|
||||
console.log(chalk.green('✅ Playwright MCP cleanup complete'));
|
||||
|
||||
} catch (error) {
|
||||
// Non-fatal - log warning but don't throw
|
||||
console.log(chalk.yellow(`⚠️ MCP cleanup warning: ${error.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Pure function: Setup local repository for testing
|
||||
export async function setupLocalRepo(repoPath) {
|
||||
try {
|
||||
const sourceDir = path.resolve(repoPath);
|
||||
|
||||
// Setup MCP in the local repository - critical for browser automation
|
||||
await setupMCP(sourceDir);
|
||||
|
||||
// Initialize git repository if not already initialized and create checkpoint
|
||||
try {
|
||||
// Check if it's already a git repository
|
||||
const isGitRepo = await fs.pathExists(path.join(sourceDir, '.git'));
|
||||
|
||||
if (!isGitRepo) {
|
||||
await $`cd ${sourceDir} && git init`;
|
||||
console.log(chalk.blue('✅ Git repository initialized'));
|
||||
}
|
||||
|
||||
// Configure git for pentest agent
|
||||
await $`cd ${sourceDir} && git config user.name "Pentest Agent"`;
|
||||
await $`cd ${sourceDir} && git config user.email "agent@localhost"`;
|
||||
|
||||
// Create initial checkpoint
|
||||
await $`cd ${sourceDir} && git add -A && git commit -m "Initial checkpoint: Local repository setup" --allow-empty`;
|
||||
console.log(chalk.green('✅ Initial checkpoint created'));
|
||||
} catch (gitError) {
|
||||
console.log(chalk.yellow(`⚠️ Git setup warning: ${gitError.message}`));
|
||||
// Non-fatal - continue without Git setup
|
||||
}
|
||||
|
||||
// Copy TOTP generation script to local repository for agent accessibility
|
||||
try {
|
||||
const totpScriptSource = path.join(import.meta.dirname, '..', '..', 'login_resources', 'generate-totp-standalone.mjs');
|
||||
const totpScriptDest = path.join(sourceDir, 'generate-totp.mjs');
|
||||
|
||||
if (await fs.pathExists(totpScriptSource)) {
|
||||
await fs.copy(totpScriptSource, totpScriptDest);
|
||||
await fs.chmod(totpScriptDest, '755'); // Make executable
|
||||
console.log(chalk.green('✅ TOTP generation script (standalone) copied to target repository'));
|
||||
} else {
|
||||
console.log(chalk.yellow('⚠️ TOTP script not found, authentication may fail if TOTP is required'));
|
||||
}
|
||||
} catch (totpError) {
|
||||
console.log(chalk.yellow(`⚠️ Failed to copy TOTP script: ${totpError.message}`));
|
||||
// Non-fatal - continue without TOTP script
|
||||
}
|
||||
|
||||
return sourceDir;
|
||||
} catch (error) {
|
||||
if (error instanceof PentestError) {
|
||||
throw error;
|
||||
}
|
||||
throw new PentestError(
|
||||
`Local repository setup failed: ${error.message}`,
|
||||
'filesystem',
|
||||
false,
|
||||
{ repoPath, originalError: error.message }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user