mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-06-09 08:53:57 +02:00
feat: typescript migration (#40)
* chore: initialize TypeScript configuration and build setup - Add tsconfig.json for root and mcp-server with strict type checking - Install typescript and @types/node as devDependencies - Add npm build script for TypeScript compilation - Update main entrypoint to compiled dist/shannon.js - Update Dockerfile to build TypeScript before running - Configure output directory and module resolution for Node.js * refactor: migrate codebase from JavaScript to TypeScript - Convert all 37 JavaScript files to TypeScript (.js -> .ts) - Add type definitions in src/types/ for agents, config, errors, session - Update mcp-server with proper TypeScript types - Move entry point from shannon.mjs to src/shannon.ts - Update tsconfig.json with rootDir: "./src" for cleaner dist output - Update Dockerfile to build TypeScript before runtime - Update package.json paths to use compiled dist/shannon.js No runtime behavior changes - pure type safety migration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: update CLI references from ./shannon.mjs to shannon - Update help text in src/cli/ui.ts - Update usage examples in src/cli/command-handler.ts - Update setup message in src/shannon.ts - Update CLAUDE.md documentation with TypeScript file structure - Replace all ./shannon.mjs references with shannon command 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: remove unnecessary eslint-disable comments ESLint is not configured in this project, making these comments redundant. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,14 +12,51 @@ import { handleToolError, PentestError } from '../error-handling.js';
|
||||
import { AGENTS } from '../session-manager.js';
|
||||
import { runClaudePromptWithRetry } from '../ai/claude-executor.js';
|
||||
import { loadPrompt } from '../prompts/prompt-manager.js';
|
||||
import type { ToolAvailability } from '../tool-checker.js';
|
||||
import type { DistributedConfig } from '../types/config.js';
|
||||
import type { AgentResult } from '../checkpoint-manager.js';
|
||||
|
||||
type ToolName = 'nmap' | 'subfinder' | 'whatweb' | 'schemathesis';
|
||||
type ToolStatus = 'success' | 'skipped' | 'error';
|
||||
|
||||
interface TerminalScanResult {
|
||||
tool: ToolName;
|
||||
output: string;
|
||||
status: ToolStatus;
|
||||
duration: number;
|
||||
success?: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
interface PromptVariables {
|
||||
webUrl: string;
|
||||
repoPath: string;
|
||||
}
|
||||
|
||||
interface Wave1Results {
|
||||
nmap: TerminalScanResult | string | AgentResult;
|
||||
subfinder: TerminalScanResult | string | AgentResult;
|
||||
whatweb: TerminalScanResult | string | AgentResult;
|
||||
naabu?: TerminalScanResult | string | AgentResult;
|
||||
codeAnalysis: AgentResult;
|
||||
}
|
||||
|
||||
interface Wave2Results {
|
||||
schemathesis: TerminalScanResult;
|
||||
}
|
||||
|
||||
interface PreReconResult {
|
||||
duration: number;
|
||||
report: string;
|
||||
}
|
||||
|
||||
// Pure function: Run terminal scanning tools
|
||||
async function runTerminalScan(tool, target, sourceDir = null) {
|
||||
async function runTerminalScan(tool: ToolName, target: string, sourceDir: string | null = null): Promise<TerminalScanResult> {
|
||||
const timer = new Timer(`command-${tool}`);
|
||||
try {
|
||||
let command, result;
|
||||
let result;
|
||||
switch (tool) {
|
||||
case 'nmap':
|
||||
case 'nmap': {
|
||||
console.log(chalk.blue(` 🔍 Running ${tool} scan...`));
|
||||
const nmapHostname = new URL(target).hostname;
|
||||
result = await $({ silent: true, stdio: ['ignore', 'pipe', 'ignore'] })`nmap -sV -sC ${nmapHostname}`;
|
||||
@@ -27,7 +64,8 @@ async function runTerminalScan(tool, target, sourceDir = null) {
|
||||
timingResults.commands[tool] = duration;
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(duration)}`));
|
||||
return { tool: 'nmap', output: result.stdout, status: 'success', duration };
|
||||
case 'subfinder':
|
||||
}
|
||||
case 'subfinder': {
|
||||
console.log(chalk.blue(` 🔍 Running ${tool} scan...`));
|
||||
const hostname = new URL(target).hostname;
|
||||
result = await $({ silent: true, stdio: ['ignore', 'pipe', 'ignore'] })`subfinder -d ${hostname}`;
|
||||
@@ -35,24 +73,26 @@ async function runTerminalScan(tool, target, sourceDir = null) {
|
||||
timingResults.commands[tool] = subfinderDuration;
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(subfinderDuration)}`));
|
||||
return { tool: 'subfinder', output: result.stdout, status: 'success', duration: subfinderDuration };
|
||||
case 'whatweb':
|
||||
}
|
||||
case 'whatweb': {
|
||||
console.log(chalk.blue(` 🔍 Running ${tool} scan...`));
|
||||
command = `whatweb --open-timeout 30 --read-timeout 60 ${target}`;
|
||||
const command = `whatweb --open-timeout 30 --read-timeout 60 ${target}`;
|
||||
console.log(chalk.gray(` Command: ${command}`));
|
||||
result = await $({ silent: true, stdio: ['ignore', 'pipe', 'ignore'] })`whatweb --open-timeout 30 --read-timeout 60 ${target}`;
|
||||
const whatwebDuration = timer.stop();
|
||||
timingResults.commands[tool] = whatwebDuration;
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(whatwebDuration)}`));
|
||||
return { tool: 'whatweb', output: result.stdout, status: 'success', duration: whatwebDuration };
|
||||
case 'schemathesis':
|
||||
}
|
||||
case 'schemathesis': {
|
||||
// Only run if API schemas found
|
||||
const schemasDir = path.join(sourceDir || '.', 'outputs', 'schemas');
|
||||
if (await fs.pathExists(schemasDir)) {
|
||||
const schemaFiles = await fs.readdir(schemasDir);
|
||||
const apiSchemas = schemaFiles.filter(f => f.endsWith('.json') || f.endsWith('.yml') || f.endsWith('.yaml'));
|
||||
const schemaFiles = await fs.readdir(schemasDir) as string[];
|
||||
const apiSchemas = schemaFiles.filter((f: string) => f.endsWith('.json') || f.endsWith('.yml') || f.endsWith('.yaml'));
|
||||
if (apiSchemas.length > 0) {
|
||||
console.log(chalk.blue(` 🔍 Running ${tool} scan...`));
|
||||
let allResults = [];
|
||||
const allResults: string[] = [];
|
||||
|
||||
// Run schemathesis on each schema file
|
||||
for (const schemaFile of apiSchemas) {
|
||||
@@ -61,7 +101,8 @@ async function runTerminalScan(tool, target, sourceDir = null) {
|
||||
result = await $({ silent: true, stdio: ['ignore', 'pipe', 'ignore'] })`schemathesis run ${schemaPath} -u ${target} --max-failures=5`;
|
||||
allResults.push(`Schema: ${schemaFile}\n${result.stdout}`);
|
||||
} catch (schemaError) {
|
||||
allResults.push(`Schema: ${schemaFile}\nError: ${schemaError.stdout || schemaError.message}`);
|
||||
const err = schemaError as { stdout?: string; message?: string };
|
||||
allResults.push(`Schema: ${schemaFile}\nError: ${err.stdout || err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +118,7 @@ async function runTerminalScan(tool, target, sourceDir = null) {
|
||||
console.log(chalk.gray(` ⏭️ ${tool} - schemas directory not found`));
|
||||
return { tool: 'schemathesis', output: 'Schemas directory not found', status: 'skipped', duration: timer.stop() };
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${tool}`);
|
||||
}
|
||||
@@ -84,15 +126,22 @@ async function runTerminalScan(tool, target, sourceDir = null) {
|
||||
const duration = timer.stop();
|
||||
timingResults.commands[tool] = duration;
|
||||
console.log(chalk.red(` ❌ ${tool} failed in ${formatDuration(duration)}`));
|
||||
return handleToolError(tool, error);
|
||||
return handleToolError(tool, error as Error & { code?: string }) as TerminalScanResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Wave 1: Initial footprinting + authentication
|
||||
async function runPreReconWave1(webUrl, sourceDir, variables, config, pipelineTestingMode = false, sessionId = null) {
|
||||
async function runPreReconWave1(
|
||||
webUrl: string,
|
||||
sourceDir: string,
|
||||
variables: PromptVariables,
|
||||
config: DistributedConfig | null,
|
||||
pipelineTestingMode: boolean = false,
|
||||
sessionId: string | null = null
|
||||
): Promise<Wave1Results> {
|
||||
console.log(chalk.blue(' → Launching Wave 1 operations in parallel...'));
|
||||
|
||||
const operations = [];
|
||||
const operations: Promise<TerminalScanResult | AgentResult>[] = [];
|
||||
|
||||
// Skip external commands in pipeline testing mode
|
||||
if (pipelineTestingMode) {
|
||||
@@ -106,7 +155,7 @@ async function runPreReconWave1(webUrl, sourceDir, variables, config, pipelineTe
|
||||
AGENTS['pre-recon'].displayName,
|
||||
'pre-recon', // Agent name for snapshot creation
|
||||
chalk.cyan,
|
||||
{ id: sessionId, webUrl } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
{ id: sessionId!, webUrl, repoPath: sourceDir } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
)
|
||||
);
|
||||
const [codeAnalysis] = await Promise.all(operations);
|
||||
@@ -114,8 +163,7 @@ async function runPreReconWave1(webUrl, sourceDir, variables, config, pipelineTe
|
||||
nmap: 'Skipped (pipeline testing mode)',
|
||||
subfinder: 'Skipped (pipeline testing mode)',
|
||||
whatweb: 'Skipped (pipeline testing mode)',
|
||||
|
||||
codeAnalysis
|
||||
codeAnalysis: codeAnalysis as AgentResult
|
||||
};
|
||||
} else {
|
||||
operations.push(
|
||||
@@ -130,7 +178,7 @@ async function runPreReconWave1(webUrl, sourceDir, variables, config, pipelineTe
|
||||
AGENTS['pre-recon'].displayName,
|
||||
'pre-recon', // Agent name for snapshot creation
|
||||
chalk.cyan,
|
||||
{ id: sessionId, webUrl } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
{ id: sessionId!, webUrl, repoPath: sourceDir } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -138,13 +186,23 @@ async function runPreReconWave1(webUrl, sourceDir, variables, config, pipelineTe
|
||||
// Check if authentication config is provided for login instructions injection
|
||||
console.log(chalk.gray(` → Config check: ${config ? 'present' : 'missing'}, Auth: ${config?.authentication ? 'present' : 'missing'}`));
|
||||
|
||||
const [nmap, subfinder, whatweb, naabu, codeAnalysis] = await Promise.all(operations);
|
||||
const [nmap, subfinder, whatweb, codeAnalysis] = await Promise.all(operations);
|
||||
|
||||
return { nmap, subfinder, whatweb, naabu, codeAnalysis };
|
||||
return {
|
||||
nmap: nmap as TerminalScanResult,
|
||||
subfinder: subfinder as TerminalScanResult,
|
||||
whatweb: whatweb as TerminalScanResult,
|
||||
codeAnalysis: codeAnalysis as AgentResult
|
||||
};
|
||||
}
|
||||
|
||||
// Wave 2: Additional scanning
|
||||
async function runPreReconWave2(webUrl, sourceDir, toolAvailability, pipelineTestingMode = false) {
|
||||
async function runPreReconWave2(
|
||||
webUrl: string,
|
||||
sourceDir: string,
|
||||
toolAvailability: ToolAvailability,
|
||||
pipelineTestingMode: boolean = false
|
||||
): Promise<Wave2Results> {
|
||||
console.log(chalk.blue(' → Running Wave 2 additional scans in parallel...'));
|
||||
|
||||
// Skip external commands in pipeline testing mode
|
||||
@@ -155,7 +213,7 @@ async function runPreReconWave2(webUrl, sourceDir, toolAvailability, pipelineTes
|
||||
};
|
||||
}
|
||||
|
||||
const operations = [];
|
||||
const operations: Promise<TerminalScanResult>[] = [];
|
||||
|
||||
// Parallel additional scans (only run if tools are available)
|
||||
|
||||
@@ -175,21 +233,29 @@ async function runPreReconWave2(webUrl, sourceDir, toolAvailability, pipelineTes
|
||||
const results = await Promise.all(operations);
|
||||
|
||||
// Map results back to named properties
|
||||
const response = {};
|
||||
const response: Wave2Results = {
|
||||
schemathesis: { tool: 'schemathesis', output: 'Tool not available', status: 'skipped', duration: 0 }
|
||||
};
|
||||
let resultIndex = 0;
|
||||
|
||||
if (toolAvailability.schemathesis) {
|
||||
response.schemathesis = results[resultIndex++];
|
||||
response.schemathesis = results[resultIndex++]!;
|
||||
} else {
|
||||
console.log(chalk.gray(' ⏭️ schemathesis - tool not available'));
|
||||
response.schemathesis = { tool: 'schemathesis', output: 'Tool not available', status: 'skipped', duration: 0 };
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Helper type for stitching results
|
||||
interface StitchableResult {
|
||||
status?: string;
|
||||
output?: string;
|
||||
tool?: string;
|
||||
}
|
||||
|
||||
// Pure function: Stitch together pre-recon outputs and save to file
|
||||
async function stitchPreReconOutputs(outputs, sourceDir) {
|
||||
async function stitchPreReconOutputs(outputs: (StitchableResult | string | undefined)[], sourceDir: string): Promise<string> {
|
||||
const [nmap, subfinder, whatweb, naabu, codeAnalysis, ...additionalScans] = outputs;
|
||||
|
||||
// Try to read the code analysis deliverable file
|
||||
@@ -198,7 +264,8 @@ async function stitchPreReconOutputs(outputs, sourceDir) {
|
||||
const codeAnalysisPath = path.join(sourceDir, 'deliverables', 'code_analysis_deliverable.md');
|
||||
codeAnalysisContent = await fs.readFile(codeAnalysisPath, 'utf8');
|
||||
} catch (error) {
|
||||
console.log(chalk.yellow(`⚠️ Could not read code analysis deliverable: ${error.message}`));
|
||||
const err = error as Error;
|
||||
console.log(chalk.yellow(`⚠️ Could not read code analysis deliverable: ${err.message}`));
|
||||
// Fallback message if file doesn't exist
|
||||
codeAnalysisContent = 'Analysis located in deliverables/code_analysis_deliverable.md';
|
||||
}
|
||||
@@ -209,34 +276,52 @@ async function stitchPreReconOutputs(outputs, sourceDir) {
|
||||
if (additionalScans && additionalScans.length > 0) {
|
||||
additionalSection = '\n## Authenticated Scans\n';
|
||||
additionalScans.forEach(scan => {
|
||||
if (scan && scan.tool) {
|
||||
const s = scan as StitchableResult;
|
||||
if (s && s.tool) {
|
||||
additionalSection += `
|
||||
### ${scan.tool.toUpperCase()}
|
||||
Status: ${scan.status}
|
||||
${scan.output}
|
||||
### ${s.tool.toUpperCase()}
|
||||
Status: ${s.status}
|
||||
${s.output}
|
||||
`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const nmapResult = nmap as StitchableResult | string | undefined;
|
||||
const subfinderResult = subfinder as StitchableResult | string | undefined;
|
||||
const whatwebResult = whatweb as StitchableResult | string | undefined;
|
||||
const naabuResult = naabu as StitchableResult | string | undefined;
|
||||
|
||||
const getStatus = (r: StitchableResult | string | undefined): string => {
|
||||
if (!r) return 'Skipped';
|
||||
if (typeof r === 'string') return 'Skipped';
|
||||
return r.status || 'Skipped';
|
||||
};
|
||||
|
||||
const getOutput = (r: StitchableResult | string | undefined): string => {
|
||||
if (!r) return 'No output';
|
||||
if (typeof r === 'string') return r;
|
||||
return r.output || 'No output';
|
||||
};
|
||||
|
||||
const report = `
|
||||
# Pre-Reconnaissance Report
|
||||
|
||||
## Port Discovery (naabu)
|
||||
Status: ${naabu?.status || 'Skipped'}
|
||||
${naabu?.output || naabu || 'No output'}
|
||||
Status: ${getStatus(naabuResult)}
|
||||
${getOutput(naabuResult)}
|
||||
|
||||
## Network Scanning (nmap)
|
||||
Status: ${nmap?.status || 'Skipped'}
|
||||
${nmap?.output || nmap || 'No output'}
|
||||
Status: ${getStatus(nmapResult)}
|
||||
${getOutput(nmapResult)}
|
||||
|
||||
## Subdomain Discovery (subfinder)
|
||||
Status: ${subfinder?.status || 'Skipped'}
|
||||
${subfinder?.output || subfinder || 'No output'}
|
||||
Status: ${getStatus(subfinderResult)}
|
||||
${getOutput(subfinderResult)}
|
||||
|
||||
## Technology Detection (whatweb)
|
||||
Status: ${whatweb?.status || 'Skipped'}
|
||||
${whatweb?.output || whatweb || 'No output'}
|
||||
Status: ${getStatus(whatwebResult)}
|
||||
${getOutput(whatwebResult)}
|
||||
## Code Analysis
|
||||
${codeAnalysisContent}
|
||||
${additionalSection}
|
||||
@@ -252,11 +337,12 @@ Report generated at: ${new Date().toISOString()}
|
||||
// Write to file in the cloned repository
|
||||
await fs.writeFile(deliverablePath, report);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
throw new PentestError(
|
||||
`Failed to write pre-recon report: ${error.message}`,
|
||||
`Failed to write pre-recon report: ${err.message}`,
|
||||
'filesystem',
|
||||
false,
|
||||
{ sourceDir, originalError: error.message }
|
||||
{ sourceDir, originalError: err.message }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -264,7 +350,15 @@ Report generated at: ${new Date().toISOString()}
|
||||
}
|
||||
|
||||
// Main pre-recon phase execution function
|
||||
export async function executePreReconPhase(webUrl, sourceDir, variables, config, toolAvailability, pipelineTestingMode, sessionId = null) {
|
||||
export async function executePreReconPhase(
|
||||
webUrl: string,
|
||||
sourceDir: string,
|
||||
variables: PromptVariables,
|
||||
config: DistributedConfig | null,
|
||||
toolAvailability: ToolAvailability,
|
||||
pipelineTestingMode: boolean,
|
||||
sessionId: string | null = null
|
||||
): Promise<PreReconResult> {
|
||||
console.log(chalk.yellow.bold('\n🔍 PHASE 1: PRE-RECONNAISSANCE'));
|
||||
const timer = new Timer('phase-1-pre-recon');
|
||||
|
||||
@@ -278,13 +372,13 @@ export async function executePreReconPhase(webUrl, sourceDir, variables, config,
|
||||
|
||||
console.log(chalk.blue('📝 Stitching pre-recon outputs...'));
|
||||
// Combine wave 1 and wave 2 results for stitching
|
||||
const allResults = [
|
||||
wave1Results.nmap,
|
||||
wave1Results.subfinder,
|
||||
wave1Results.whatweb,
|
||||
wave1Results.naabu,
|
||||
wave1Results.codeAnalysis,
|
||||
...(wave2Results.schemathesis ? [wave2Results.schemathesis] : [])
|
||||
const allResults: (StitchableResult | string | undefined)[] = [
|
||||
wave1Results.nmap as StitchableResult | string,
|
||||
wave1Results.subfinder as StitchableResult | string,
|
||||
wave1Results.whatweb as StitchableResult | string,
|
||||
wave1Results.naabu as StitchableResult | string | undefined,
|
||||
wave1Results.codeAnalysis as unknown as StitchableResult,
|
||||
...(wave2Results.schemathesis ? [wave2Results.schemathesis as StitchableResult] : [])
|
||||
];
|
||||
const preReconReport = await stitchPreReconOutputs(allResults, sourceDir);
|
||||
const duration = timer.stop();
|
||||
@@ -293,4 +387,4 @@ export async function executePreReconPhase(webUrl, sourceDir, variables, config,
|
||||
console.log(chalk.green(`💾 Saved to ${sourceDir}/deliverables/pre_recon_deliverable.md`));
|
||||
|
||||
return { duration, report: preReconReport };
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,15 @@ import { fs, path } from 'zx';
|
||||
import chalk from 'chalk';
|
||||
import { PentestError } from '../error-handling.js';
|
||||
|
||||
interface DeliverableFile {
|
||||
name: string;
|
||||
path: string;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
// Pure function: Assemble final report from specialist deliverables
|
||||
export async function assembleFinalReport(sourceDir) {
|
||||
const deliverableFiles = [
|
||||
export async function assembleFinalReport(sourceDir: string): Promise<string> {
|
||||
const deliverableFiles: DeliverableFile[] = [
|
||||
{ name: 'Injection', path: 'injection_exploitation_evidence.md', required: false },
|
||||
{ name: 'XSS', path: 'xss_exploitation_evidence.md', required: false },
|
||||
{ name: 'Authentication', path: 'auth_exploitation_evidence.md', required: false },
|
||||
@@ -18,7 +24,7 @@ export async function assembleFinalReport(sourceDir) {
|
||||
{ name: 'Authorization', path: 'authz_exploitation_evidence.md', required: false }
|
||||
];
|
||||
|
||||
const sections = [];
|
||||
const sections: string[] = [];
|
||||
|
||||
for (const file of deliverableFiles) {
|
||||
const filePath = path.join(sourceDir, 'deliverables', file.path);
|
||||
@@ -36,7 +42,8 @@ export async function assembleFinalReport(sourceDir) {
|
||||
if (file.required) {
|
||||
throw error;
|
||||
}
|
||||
console.log(chalk.yellow(`⚠️ Could not read ${file.path}: ${error.message}`));
|
||||
const err = error as Error;
|
||||
console.log(chalk.yellow(`⚠️ Could not read ${file.path}: ${err.message}`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,13 +54,14 @@ export async function assembleFinalReport(sourceDir) {
|
||||
await fs.writeFile(finalReportPath, finalContent);
|
||||
console.log(chalk.green(`✅ Final report assembled at ${finalReportPath}`));
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
throw new PentestError(
|
||||
`Failed to write final report: ${error.message}`,
|
||||
`Failed to write final report: ${err.message}`,
|
||||
'filesystem',
|
||||
false,
|
||||
{ finalReportPath, originalError: error.message }
|
||||
{ finalReportPath, originalError: err.message }
|
||||
);
|
||||
}
|
||||
|
||||
return finalContent;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user