mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-05-18 15:14:49 +02:00
refactor: remove ~275 lines of dead code and enable stricter tsconfig
- Delete unused src/cli/ui.ts, remove zod dependency, drop 4 dead functions (logError, handleToolError, getRetryDelay, displayTimingSummary) - Remove 8 unused types/interfaces and 3 duplicate formatting utils from audit/utils.ts - Narrow export surface: make 7 message-handler functions private, remove unused audit re-exports, unexport AgentDefinition and path constants - Remove unused runClaudePrompt params (sessionMetadata, attemptNumber) and update caller - Enable tsconfig noUnusedLocals, noUnusedParameters, noImplicitReturns, noImplicitOverride, noFallthroughCasesInSwitch
This commit is contained in:
Generated
-1
@@ -21,7 +21,6 @@
|
||||
"figlet": "^1.9.3",
|
||||
"gradient-string": "^3.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"zod": "^4.3.6",
|
||||
"zx": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
"figlet": "^1.9.3",
|
||||
"gradient-string": "^3.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"zod": "^4.3.6",
|
||||
"zx": "^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -16,7 +16,6 @@ import { formatTimestamp } from '../utils/formatting.js';
|
||||
import { AGENT_VALIDATORS, MCP_AGENT_MAPPING } from '../constants.js';
|
||||
import { AuditSession } from '../audit/index.js';
|
||||
import { createShannonHelperServer } from '../../mcp-server/dist/index.js';
|
||||
import type { SessionMetadata } from '../audit/utils.js';
|
||||
import { getPromptNameForAgent } from '../types/agents.js';
|
||||
import type { AgentName } from '../types/index.js';
|
||||
|
||||
@@ -200,9 +199,7 @@ export async function runClaudePrompt(
|
||||
description: string = 'Claude analysis',
|
||||
agentName: string | null = null,
|
||||
colorFn: ChalkInstance = chalk.cyan,
|
||||
sessionMetadata: SessionMetadata | null = null,
|
||||
auditSession: AuditSession | null = null,
|
||||
attemptNumber: number = 1
|
||||
auditSession: AuditSession | null = null
|
||||
): Promise<ClaudePromptResult> {
|
||||
const timer = new Timer(`agent-${description.toLowerCase().replace(/\s+/g, '-')}`);
|
||||
const fullPrompt = context ? `${context}\n\n${prompt}` : prompt;
|
||||
|
||||
@@ -38,7 +38,7 @@ import type {
|
||||
import type { ChalkInstance } from 'chalk';
|
||||
|
||||
// Handles both array and string content formats from SDK
|
||||
export function extractMessageContent(message: AssistantMessage): string {
|
||||
function extractMessageContent(message: AssistantMessage): string {
|
||||
const messageContent = message.message;
|
||||
|
||||
if (Array.isArray(messageContent.content)) {
|
||||
@@ -51,7 +51,7 @@ export function extractMessageContent(message: AssistantMessage): string {
|
||||
}
|
||||
|
||||
// Extracts only text content (no tool_use JSON) to avoid false positives in error detection
|
||||
export function extractTextOnlyContent(message: AssistantMessage): string {
|
||||
function extractTextOnlyContent(message: AssistantMessage): string {
|
||||
const messageContent = message.message;
|
||||
|
||||
if (Array.isArray(messageContent.content)) {
|
||||
@@ -64,7 +64,7 @@ export function extractTextOnlyContent(message: AssistantMessage): string {
|
||||
return String(messageContent.content);
|
||||
}
|
||||
|
||||
export function detectApiError(content: string): ApiErrorDetection {
|
||||
function detectApiError(content: string): ApiErrorDetection {
|
||||
if (!content || typeof content !== 'string') {
|
||||
return { detected: false };
|
||||
}
|
||||
@@ -181,7 +181,7 @@ function handleStructuredError(
|
||||
}
|
||||
}
|
||||
|
||||
export function handleAssistantMessage(
|
||||
function handleAssistantMessage(
|
||||
message: AssistantMessage,
|
||||
turnCount: number
|
||||
): AssistantResult {
|
||||
@@ -219,7 +219,7 @@ export function handleAssistantMessage(
|
||||
}
|
||||
|
||||
// Final message of a query with cost/duration info
|
||||
export function handleResultMessage(message: ResultMessage): ResultData {
|
||||
function handleResultMessage(message: ResultMessage): ResultData {
|
||||
const result: ResultData = {
|
||||
result: message.result || null,
|
||||
cost: message.total_cost_usd || 0,
|
||||
@@ -243,7 +243,7 @@ export function handleResultMessage(message: ResultMessage): ResultData {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function handleToolUseMessage(message: ToolUseMessage): ToolUseData {
|
||||
function handleToolUseMessage(message: ToolUseMessage): ToolUseData {
|
||||
return {
|
||||
toolName: message.name,
|
||||
parameters: message.input || {},
|
||||
@@ -252,7 +252,7 @@ export function handleToolUseMessage(message: ToolUseMessage): ToolUseData {
|
||||
}
|
||||
|
||||
// Truncates long results for display (500 char limit), preserves full content for logging
|
||||
export function handleToolResultMessage(message: ToolResultMessage): ToolResultData {
|
||||
function handleToolResultMessage(message: ToolResultMessage): ToolResultData {
|
||||
const content = message.content;
|
||||
const contentStr =
|
||||
typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
||||
|
||||
@@ -13,22 +13,6 @@ export interface ExecutionContext {
|
||||
agentKey: string;
|
||||
}
|
||||
|
||||
interface ProcessingState {
|
||||
turnCount: number;
|
||||
result: string | null;
|
||||
apiErrorDetected: boolean;
|
||||
totalCost: number;
|
||||
partialCost: number;
|
||||
lastHeartbeat: number;
|
||||
}
|
||||
|
||||
interface ProcessingResult {
|
||||
result: string | null;
|
||||
turnCount: number;
|
||||
apiErrorDetected: boolean;
|
||||
totalCost: number;
|
||||
}
|
||||
|
||||
export interface AssistantResult {
|
||||
content: string;
|
||||
cleanedContent: string;
|
||||
@@ -110,15 +94,6 @@ export interface ApiErrorDetection {
|
||||
shouldThrow?: Error;
|
||||
}
|
||||
|
||||
// Message types from SDK stream
|
||||
type SdkMessage =
|
||||
| AssistantMessage
|
||||
| ResultMessage
|
||||
| ToolUseMessage
|
||||
| ToolResultMessage
|
||||
| SystemInitMessage
|
||||
| UserMessage;
|
||||
|
||||
export interface SystemInitMessage {
|
||||
type: 'system';
|
||||
subtype: 'init';
|
||||
@@ -131,16 +106,3 @@ export interface UserMessage {
|
||||
type: 'user';
|
||||
}
|
||||
|
||||
// Dispatch result types for message processing
|
||||
type MessageDispatchResult =
|
||||
| { action: 'continue' }
|
||||
| { action: 'break'; result: string | null; cost: number }
|
||||
| { action: 'throw'; error: Error };
|
||||
|
||||
interface MessageDispatchContext {
|
||||
turnCount: number;
|
||||
execContext: ExecutionContext;
|
||||
description: string;
|
||||
colorFn: (text: string) => string;
|
||||
useCleanOutput: boolean;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,3 @@
|
||||
*/
|
||||
|
||||
export { AuditSession } from './audit-session.js';
|
||||
export { AgentLogger } from './logger.js';
|
||||
export { WorkflowLogger } from './workflow-logger.js';
|
||||
export { MetricsTracker } from './metrics-tracker.js';
|
||||
export * as AuditUtils from './utils.js';
|
||||
|
||||
+2
-35
@@ -19,8 +19,8 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Get Shannon repository root
|
||||
export const SHANNON_ROOT = path.resolve(__dirname, '..', '..');
|
||||
export const AUDIT_LOGS_DIR = path.join(SHANNON_ROOT, 'audit-logs');
|
||||
const SHANNON_ROOT = path.resolve(__dirname, '..', '..');
|
||||
const AUDIT_LOGS_DIR = path.join(SHANNON_ROOT, 'audit-logs');
|
||||
|
||||
export interface SessionMetadata {
|
||||
id: string;
|
||||
@@ -132,39 +132,6 @@ export async function atomicWrite(filePath: string, data: object | string): Prom
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format duration in milliseconds to human-readable string
|
||||
*/
|
||||
export function formatDuration(ms: number): string {
|
||||
if (ms < 1000) {
|
||||
return `${ms}ms`;
|
||||
}
|
||||
|
||||
const seconds = ms / 1000;
|
||||
if (seconds < 60) {
|
||||
return `${seconds.toFixed(1)}s`;
|
||||
}
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format timestamp to ISO 8601 string
|
||||
*/
|
||||
export function formatTimestamp(timestamp: number = Date.now()): string {
|
||||
return new Date(timestamp).toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentage
|
||||
*/
|
||||
export function calculatePercentage(part: number, total: number): number {
|
||||
if (total === 0) return 0;
|
||||
return (part / total) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse JSON file
|
||||
*/
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { displaySplashScreen } from '../splash-screen.js';
|
||||
|
||||
// Helper function: Display help information
|
||||
function showHelp(): void {
|
||||
console.log(chalk.cyan.bold('AI Penetration Testing Agent'));
|
||||
console.log(chalk.gray('Automated security assessment tool\n'));
|
||||
|
||||
console.log(chalk.yellow.bold('USAGE:'));
|
||||
console.log(' shannon <WEB_URL> <REPO_PATH> [--config config.yaml] [--output /path/to/reports]\n');
|
||||
|
||||
console.log(chalk.yellow.bold('OPTIONS:'));
|
||||
console.log(
|
||||
' --config <file> YAML configuration file for authentication and testing parameters'
|
||||
);
|
||||
console.log(
|
||||
' --output <path> Custom output directory for session folder (default: ./audit-logs/)'
|
||||
);
|
||||
console.log(
|
||||
' --pipeline-testing Use minimal prompts for fast pipeline testing (creates minimal deliverables)'
|
||||
);
|
||||
console.log(
|
||||
' --disable-loader Disable the animated progress loader (useful when logs interfere with spinner)'
|
||||
);
|
||||
console.log(' --help Show this help message\n');
|
||||
|
||||
console.log(chalk.yellow.bold('EXAMPLES:'));
|
||||
console.log(' shannon "https://example.com" "/path/to/local/repo"');
|
||||
console.log(' shannon "https://example.com" "/path/to/local/repo" --config auth.yaml');
|
||||
console.log(' shannon "https://example.com" "/path/to/local/repo" --output /path/to/reports');
|
||||
console.log(' shannon "https://example.com" "/path/to/local/repo" --pipeline-testing\n');
|
||||
|
||||
console.log(chalk.yellow.bold('REQUIREMENTS:'));
|
||||
console.log(' • WEB_URL must start with http:// or https://');
|
||||
console.log(' • REPO_PATH must be an accessible local directory');
|
||||
console.log(' • Only test systems you own or have permission to test\n');
|
||||
|
||||
console.log(chalk.yellow.bold('ENVIRONMENT VARIABLES:'));
|
||||
console.log(' PENTEST_MAX_RETRIES Number of retries for AI agents (default: 3)');
|
||||
}
|
||||
|
||||
// Export the splash screen function for use in main
|
||||
export { displaySplashScreen };
|
||||
@@ -13,7 +13,6 @@ import { PentestError } from './error-handling.js';
|
||||
import type {
|
||||
Config,
|
||||
Rule,
|
||||
Rules,
|
||||
Authentication,
|
||||
DistributedConfig,
|
||||
} from './types/config.js';
|
||||
|
||||
+2
-96
@@ -4,25 +4,15 @@
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { fs, path } from 'zx';
|
||||
import type {
|
||||
PentestErrorType,
|
||||
PentestErrorContext,
|
||||
LogEntry,
|
||||
ToolErrorResult,
|
||||
PromptErrorResult,
|
||||
} from './types/errors.js';
|
||||
|
||||
// Temporal error classification for ApplicationFailure wrapping
|
||||
export interface TemporalErrorClassification {
|
||||
type: string;
|
||||
retryable: boolean;
|
||||
}
|
||||
|
||||
// Custom error class for pentest operations
|
||||
export class PentestError extends Error {
|
||||
name = 'PentestError' as const;
|
||||
override name = 'PentestError' as const;
|
||||
type: PentestErrorType;
|
||||
retryable: boolean;
|
||||
context: PentestErrorContext;
|
||||
@@ -42,76 +32,7 @@ export class PentestError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function logError(
|
||||
error: Error & { type?: PentestErrorType; retryable?: boolean; context?: PentestErrorContext },
|
||||
contextMsg: string,
|
||||
sourceDir: string | null = null
|
||||
): Promise<LogEntry> {
|
||||
const timestamp = new Date().toISOString();
|
||||
const logEntry: LogEntry = {
|
||||
timestamp,
|
||||
context: contextMsg,
|
||||
error: {
|
||||
name: error.name || error.constructor.name,
|
||||
message: error.message,
|
||||
type: error.type || 'unknown',
|
||||
retryable: error.retryable || false,
|
||||
},
|
||||
};
|
||||
// Only add stack if it exists
|
||||
if (error.stack) {
|
||||
logEntry.error.stack = error.stack;
|
||||
}
|
||||
|
||||
// Console logging with color
|
||||
const prefix = error.retryable ? '⚠️' : '❌';
|
||||
const color = error.retryable ? chalk.yellow : chalk.red;
|
||||
console.log(color(`${prefix} ${contextMsg}:`));
|
||||
console.log(color(` ${error.message}`));
|
||||
|
||||
if (error.context && Object.keys(error.context).length > 0) {
|
||||
console.log(chalk.gray(` Context: ${JSON.stringify(error.context)}`));
|
||||
}
|
||||
|
||||
// File logging (if source directory available)
|
||||
if (sourceDir) {
|
||||
try {
|
||||
const logPath = path.join(sourceDir, 'error.log');
|
||||
await fs.appendFile(logPath, JSON.stringify(logEntry) + '\n');
|
||||
} catch (logErr) {
|
||||
const errMsg = logErr instanceof Error ? logErr.message : String(logErr);
|
||||
console.log(chalk.gray(` (Failed to write error log: ${errMsg})`));
|
||||
}
|
||||
}
|
||||
|
||||
return logEntry;
|
||||
}
|
||||
|
||||
// Handle tool execution errors
|
||||
export function handleToolError(
|
||||
toolName: string,
|
||||
error: Error & { code?: string }
|
||||
): ToolErrorResult {
|
||||
const isRetryable =
|
||||
error.code === 'ECONNRESET' ||
|
||||
error.code === 'ETIMEDOUT' ||
|
||||
error.code === 'ENOTFOUND';
|
||||
|
||||
return {
|
||||
tool: toolName,
|
||||
output: `Error: ${error.message}`,
|
||||
status: 'error',
|
||||
duration: 0,
|
||||
success: false,
|
||||
error: new PentestError(
|
||||
`${toolName} execution failed: ${error.message}`,
|
||||
'tool',
|
||||
isRetryable,
|
||||
{ toolName, originalError: error.message, errorCode: error.code }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Handle prompt loading errors
|
||||
export function handlePromptError(
|
||||
promptName: string,
|
||||
@@ -181,21 +102,6 @@ export function isRetryableError(error: Error): boolean {
|
||||
return RETRYABLE_PATTERNS.some((pattern) => message.includes(pattern));
|
||||
}
|
||||
|
||||
// Rate limit errors get longer base delay (30s) vs standard exponential backoff (2s)
|
||||
export function getRetryDelay(error: Error, attempt: number): number {
|
||||
const message = error.message.toLowerCase();
|
||||
|
||||
// Rate limiting gets longer delays
|
||||
if (message.includes('rate limit') || message.includes('429')) {
|
||||
return Math.min(30000 + attempt * 10000, 120000); // 30s, 40s, 50s, max 2min
|
||||
}
|
||||
|
||||
// Exponential backoff with jitter for other retryable errors
|
||||
const baseDelay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
|
||||
const jitter = Math.random() * 1000; // 0-1s random
|
||||
return Math.min(baseDelay + jitter, 30000); // Max 30s
|
||||
}
|
||||
|
||||
/**
|
||||
* Classifies errors for Temporal workflow retry behavior.
|
||||
* Returns error type and whether Temporal should retry.
|
||||
@@ -204,7 +110,7 @@ export function getRetryDelay(error: Error, attempt: number): number {
|
||||
* - Retryable errors: Temporal retries with configured backoff
|
||||
* - Non-retryable errors: Temporal fails immediately
|
||||
*/
|
||||
export function classifyErrorForTemporal(error: unknown): TemporalErrorClassification {
|
||||
export function classifyErrorForTemporal(error: unknown): { type: string; retryable: boolean } {
|
||||
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
|
||||
|
||||
// === BILLING ERRORS (Retryable with long backoff) ===
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { AgentName } from './types/index.js';
|
||||
|
||||
// Agent definition interface
|
||||
export interface AgentDefinition {
|
||||
interface AgentDefinition {
|
||||
name: AgentName;
|
||||
displayName: string;
|
||||
prerequisites: AgentName[];
|
||||
|
||||
@@ -181,9 +181,7 @@ async function runAgentActivity(
|
||||
agentName, // description
|
||||
agentName,
|
||||
chalk.cyan,
|
||||
sessionMetadata,
|
||||
auditSession,
|
||||
attemptNumber
|
||||
auditSession
|
||||
);
|
||||
|
||||
// 6.5. Sanity check: Detect spending cap that slipped through all detection layers
|
||||
|
||||
+1
-3
@@ -29,10 +29,8 @@ export interface Rules {
|
||||
|
||||
export type LoginType = 'form' | 'sso' | 'api' | 'basic';
|
||||
|
||||
export type SuccessConditionType = 'url' | 'cookie' | 'element' | 'redirect';
|
||||
|
||||
export interface SuccessCondition {
|
||||
type: SuccessConditionType;
|
||||
type: 'url' | 'cookie' | 'element' | 'redirect';
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { formatDuration } from './formatting.js';
|
||||
|
||||
// Timing utilities
|
||||
|
||||
export class Timer {
|
||||
@@ -59,52 +56,3 @@ export const costResults: CostResults = {
|
||||
total: 0,
|
||||
};
|
||||
|
||||
// Function to display comprehensive timing summary
|
||||
const displayTimingSummary = (): void => {
|
||||
if (!timingResults.total) {
|
||||
console.log(chalk.yellow('No timing data available'));
|
||||
return;
|
||||
}
|
||||
|
||||
const totalDuration = timingResults.total.stop();
|
||||
|
||||
console.log(chalk.cyan.bold('\n⏱️ TIMING SUMMARY'));
|
||||
console.log(chalk.gray('─'.repeat(60)));
|
||||
|
||||
// Total execution time
|
||||
console.log(chalk.cyan(`📊 Total Execution Time: ${formatDuration(totalDuration)}`));
|
||||
console.log();
|
||||
|
||||
// Agent breakdown
|
||||
if (Object.keys(timingResults.agents).length > 0) {
|
||||
console.log(chalk.magenta.bold('🤖 Agent Breakdown:'));
|
||||
let agentTotal = 0;
|
||||
for (const [agent, duration] of Object.entries(timingResults.agents)) {
|
||||
const percentage = ((duration / totalDuration) * 100).toFixed(1);
|
||||
const displayName = agent.replace(/-/g, ' ');
|
||||
console.log(
|
||||
chalk.magenta(
|
||||
` ${displayName.padEnd(20)} ${formatDuration(duration).padStart(8)} (${percentage}%)`
|
||||
)
|
||||
);
|
||||
agentTotal += duration;
|
||||
}
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` ${'Agents Total'.padEnd(20)} ${formatDuration(agentTotal).padStart(8)} (${((agentTotal / totalDuration) * 100).toFixed(1)}%)`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Cost breakdown
|
||||
if (Object.keys(costResults.agents).length > 0) {
|
||||
console.log(chalk.green.bold('\n💰 Cost Breakdown:'));
|
||||
for (const [agent, cost] of Object.entries(costResults.agents)) {
|
||||
const displayName = agent.replace(/-/g, ' ');
|
||||
console.log(chalk.green(` ${displayName.padEnd(20)} $${cost.toFixed(4).padStart(8)}`));
|
||||
}
|
||||
console.log(chalk.gray(` ${'Total Cost'.padEnd(20)} $${costResults.total.toFixed(4).padStart(8)}`));
|
||||
}
|
||||
|
||||
console.log(chalk.gray('─'.repeat(60)));
|
||||
};
|
||||
|
||||
+5
-5
@@ -33,11 +33,11 @@
|
||||
"exactOptionalPropertyTypes": true,
|
||||
|
||||
// Style Options
|
||||
// "noImplicitReturns": true,
|
||||
// "noImplicitOverride": true,
|
||||
// "noUnusedLocals": true,
|
||||
// "noUnusedParameters": true,
|
||||
// "noFallthroughCasesInSwitch": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitOverride": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
// "noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
// Recommended Options
|
||||
|
||||
Reference in New Issue
Block a user