refactor: remove ~70 low-value comments across 13 files
- Remove empty section markers (// === ... ===, // --- ... ---) that duplicate JSDoc or function names - Remove "what" comments that restate the next line of code (e.g. // Save to disk, // Check for retryable patterns) - Remove file-level descriptions that restate the filename (e.g. // Pure functions for formatting console output) - Fix "Added by client" comment referencing implementation history → "Used for audit correlation" - Preserve all WHY comments: error classification groups, billing/session limit explanations, ESM interop, exactOptionalPropertyTypes, mutex reasoning
This commit is contained in:
@@ -4,8 +4,6 @@
|
|||||||
// it under the terms of the GNU Affero General Public License version 3
|
// it under the terms of the GNU Affero General Public License version 3
|
||||||
// as published by the Free Software Foundation.
|
// as published by the Free Software Foundation.
|
||||||
|
|
||||||
// Pure functions for processing SDK message types
|
|
||||||
|
|
||||||
import { PentestError } from '../services/error-handling.js';
|
import { PentestError } from '../services/error-handling.js';
|
||||||
import { ErrorCode } from '../types/errors.js';
|
import { ErrorCode } from '../types/errors.js';
|
||||||
import { matchesBillingTextPattern } from '../utils/billing-detection.js';
|
import { matchesBillingTextPattern } from '../utils/billing-detection.js';
|
||||||
@@ -263,14 +261,12 @@ function handleToolResultMessage(message: ToolResultMessage): ToolResultData {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output helper for console logging
|
|
||||||
function outputLines(lines: string[]): void {
|
function outputLines(lines: string[]): void {
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
console.log(line);
|
console.log(line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Message dispatch result types
|
|
||||||
export type MessageDispatchAction =
|
export type MessageDispatchAction =
|
||||||
| { type: 'continue'; apiErrorDetected?: boolean | undefined; model?: string | undefined }
|
| { type: 'continue'; apiErrorDetected?: boolean | undefined; model?: string | undefined }
|
||||||
| { type: 'complete'; result: string | null; cost: number }
|
| { type: 'complete'; result: string | null; cost: number }
|
||||||
|
|||||||
@@ -4,14 +4,10 @@
|
|||||||
// it under the terms of the GNU Affero General Public License version 3
|
// it under the terms of the GNU Affero General Public License version 3
|
||||||
// as published by the Free Software Foundation.
|
// as published by the Free Software Foundation.
|
||||||
|
|
||||||
// Pure functions for formatting console output
|
|
||||||
|
|
||||||
import { extractAgentType, formatDuration } from '../utils/formatting.js';
|
import { extractAgentType, formatDuration } from '../utils/formatting.js';
|
||||||
import { AGENTS } from '../session-manager.js';
|
import { AGENTS } from '../session-manager.js';
|
||||||
import type { ExecutionContext, ResultData } from './types.js';
|
import type { ExecutionContext, ResultData } from './types.js';
|
||||||
|
|
||||||
// --- Types for tool call filtering ---
|
|
||||||
|
|
||||||
interface ToolCallInput {
|
interface ToolCallInput {
|
||||||
url?: string;
|
url?: string;
|
||||||
element?: string;
|
element?: string;
|
||||||
@@ -32,8 +28,6 @@ interface ToolCall {
|
|||||||
input?: ToolCallInput;
|
input?: ToolCallInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Agent prefix logic ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get agent prefix for parallel execution
|
* Get agent prefix for parallel execution
|
||||||
*/
|
*/
|
||||||
@@ -70,8 +64,6 @@ export function getAgentPrefix(description: string): string {
|
|||||||
return '[Agent]';
|
return '[Agent]';
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Tool call filtering ---
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract domain from URL for display
|
* Extract domain from URL for display
|
||||||
*/
|
*/
|
||||||
@@ -273,8 +265,6 @@ export function filterJsonToolCalls(content: string | null | undefined): string
|
|||||||
return processedLines.join('\n');
|
return processedLines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Console output formatting ---
|
|
||||||
|
|
||||||
export function detectExecutionContext(description: string): ExecutionContext {
|
export function detectExecutionContext(description: string): ExecutionContext {
|
||||||
const isParallelExecution =
|
const isParallelExecution =
|
||||||
description.includes('vuln agent') || description.includes('exploit agent');
|
description.includes('vuln agent') || description.includes('exploit agent');
|
||||||
|
|||||||
@@ -112,24 +112,19 @@ export class AuditSession {
|
|||||||
await AgentLogger.savePrompt(this.sessionMetadata, agentName, promptContent);
|
await AgentLogger.savePrompt(this.sessionMetadata, agentName, promptContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track current agent name for workflow logging
|
|
||||||
this.currentAgentName = agentName;
|
this.currentAgentName = agentName;
|
||||||
|
|
||||||
// Create and initialize logger for this attempt
|
|
||||||
this.currentLogger = new AgentLogger(this.sessionMetadata, agentName, attemptNumber);
|
this.currentLogger = new AgentLogger(this.sessionMetadata, agentName, attemptNumber);
|
||||||
await this.currentLogger.initialize();
|
await this.currentLogger.initialize();
|
||||||
|
|
||||||
// Start metrics tracking
|
|
||||||
this.metricsTracker.startAgent(agentName, attemptNumber);
|
this.metricsTracker.startAgent(agentName, attemptNumber);
|
||||||
|
|
||||||
// Log start event
|
|
||||||
await this.currentLogger.logEvent('agent_start', {
|
await this.currentLogger.logEvent('agent_start', {
|
||||||
agentName,
|
agentName,
|
||||||
attemptNumber,
|
attemptNumber,
|
||||||
timestamp: formatTimestamp(),
|
timestamp: formatTimestamp(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Log to unified workflow log
|
|
||||||
await this.workflowLogger.logAgent(agentName, 'start', { attemptNumber });
|
await this.workflowLogger.logAgent(agentName, 'start', { attemptNumber });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +172,6 @@ export class AuditSession {
|
|||||||
* End agent execution (mutex-protected)
|
* End agent execution (mutex-protected)
|
||||||
*/
|
*/
|
||||||
async endAgent(agentName: string, result: AgentEndResult): Promise<void> {
|
async endAgent(agentName: string, result: AgentEndResult): Promise<void> {
|
||||||
// Log end event
|
|
||||||
if (this.currentLogger) {
|
if (this.currentLogger) {
|
||||||
await this.currentLogger.logEvent('agent_end', {
|
await this.currentLogger.logEvent('agent_end', {
|
||||||
agentName,
|
agentName,
|
||||||
@@ -187,15 +181,12 @@ export class AuditSession {
|
|||||||
timestamp: formatTimestamp(),
|
timestamp: formatTimestamp(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close logger
|
|
||||||
await this.currentLogger.close();
|
await this.currentLogger.close();
|
||||||
this.currentLogger = null;
|
this.currentLogger = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset current agent name
|
|
||||||
this.currentAgentName = null;
|
this.currentAgentName = null;
|
||||||
|
|
||||||
// Log to unified workflow log
|
|
||||||
const agentLogDetails: AgentLogDetails = {
|
const agentLogDetails: AgentLogDetails = {
|
||||||
attemptNumber: result.attemptNumber,
|
attemptNumber: result.attemptNumber,
|
||||||
duration_ms: result.duration_ms,
|
duration_ms: result.duration_ms,
|
||||||
@@ -211,7 +202,6 @@ export class AuditSession {
|
|||||||
// Reload inside mutex to prevent lost updates during parallel exploitation phase
|
// Reload inside mutex to prevent lost updates during parallel exploitation phase
|
||||||
await this.metricsTracker.reload();
|
await this.metricsTracker.reload();
|
||||||
|
|
||||||
// Update metrics
|
|
||||||
await this.metricsTracker.endAgent(agentName, result);
|
await this.metricsTracker.endAgent(agentName, result);
|
||||||
} finally {
|
} finally {
|
||||||
unlock();
|
unlock();
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export class AgentLogger {
|
|||||||
this.attemptNumber = attemptNumber;
|
this.attemptNumber = attemptNumber;
|
||||||
this.timestamp = Date.now();
|
this.timestamp = Date.now();
|
||||||
|
|
||||||
// Generate log file path and create stream
|
|
||||||
const logPath = generateLogPath(sessionMetadata, agentName, this.timestamp, attemptNumber);
|
const logPath = generateLogPath(sessionMetadata, agentName, this.timestamp, attemptNumber);
|
||||||
this.logStream = new LogStream(logPath);
|
this.logStream = new LogStream(logPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,7 +227,6 @@ export class MetricsTracker {
|
|||||||
// Recalculate aggregations
|
// Recalculate aggregations
|
||||||
this.recalculateAggregations();
|
this.recalculateAggregations();
|
||||||
|
|
||||||
// Save to disk
|
|
||||||
await this.save();
|
await this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export class WorkflowLogger {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the stream (LogStream.open() handles directory creation)
|
|
||||||
await this.logStream.open();
|
await this.logStream.open();
|
||||||
|
|
||||||
// Write header only if file is new (empty)
|
// Write header only if file is new (empty)
|
||||||
|
|||||||
@@ -22,11 +22,9 @@ import type {
|
|||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const addFormats: FormatsPlugin = require('ajv-formats');
|
const addFormats: FormatsPlugin = require('ajv-formats');
|
||||||
|
|
||||||
// Initialize AJV with formats
|
|
||||||
const ajv = new Ajv({ allErrors: true, verbose: true });
|
const ajv = new Ajv({ allErrors: true, verbose: true });
|
||||||
addFormats(ajv);
|
addFormats(ajv);
|
||||||
|
|
||||||
// Load JSON Schema
|
|
||||||
let configSchema: object;
|
let configSchema: object;
|
||||||
let validateSchema: ValidateFunction;
|
let validateSchema: ValidateFunction;
|
||||||
|
|
||||||
@@ -45,7 +43,6 @@ try {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Security patterns to block
|
|
||||||
const DANGEROUS_PATTERNS: RegExp[] = [
|
const DANGEROUS_PATTERNS: RegExp[] = [
|
||||||
/\.\.\//, // Path traversal
|
/\.\.\//, // Path traversal
|
||||||
/[<>]/, // HTML/XML injection
|
/[<>]/, // HTML/XML injection
|
||||||
@@ -179,10 +176,8 @@ function formatAjvErrors(errors: ErrorObject[]): string[] {
|
|||||||
return errors.map(formatAjvError);
|
return errors.map(formatAjvError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse and load YAML configuration file with enhanced safety
|
|
||||||
export const parseConfig = async (configPath: string): Promise<Config> => {
|
export const parseConfig = async (configPath: string): Promise<Config> => {
|
||||||
try {
|
try {
|
||||||
// File existence check
|
|
||||||
if (!(await fs.pathExists(configPath))) {
|
if (!(await fs.pathExists(configPath))) {
|
||||||
throw new PentestError(
|
throw new PentestError(
|
||||||
`Configuration file not found: ${configPath}`,
|
`Configuration file not found: ${configPath}`,
|
||||||
@@ -193,7 +188,6 @@ export const parseConfig = async (configPath: string): Promise<Config> => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// File size check (prevent extremely large files)
|
|
||||||
const stats = await fs.stat(configPath);
|
const stats = await fs.stat(configPath);
|
||||||
const maxFileSize = 1024 * 1024; // 1MB
|
const maxFileSize = 1024 * 1024; // 1MB
|
||||||
if (stats.size > maxFileSize) {
|
if (stats.size > maxFileSize) {
|
||||||
@@ -206,10 +200,8 @@ export const parseConfig = async (configPath: string): Promise<Config> => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read file content
|
|
||||||
const configContent = await fs.readFile(configPath, 'utf8');
|
const configContent = await fs.readFile(configPath, 'utf8');
|
||||||
|
|
||||||
// Basic content validation
|
|
||||||
if (!configContent.trim()) {
|
if (!configContent.trim()) {
|
||||||
throw new PentestError(
|
throw new PentestError(
|
||||||
'Configuration file is empty',
|
'Configuration file is empty',
|
||||||
@@ -220,7 +212,6 @@ export const parseConfig = async (configPath: string): Promise<Config> => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse YAML with safety options
|
|
||||||
let config: unknown;
|
let config: unknown;
|
||||||
try {
|
try {
|
||||||
config = yaml.load(configContent, {
|
config = yaml.load(configContent, {
|
||||||
@@ -239,7 +230,6 @@ export const parseConfig = async (configPath: string): Promise<Config> => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional safety check
|
|
||||||
if (config === null || config === undefined) {
|
if (config === null || config === undefined) {
|
||||||
throw new PentestError(
|
throw new PentestError(
|
||||||
'Configuration file resulted in null/undefined after parsing',
|
'Configuration file resulted in null/undefined after parsing',
|
||||||
@@ -250,7 +240,6 @@ export const parseConfig = async (configPath: string): Promise<Config> => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the configuration structure and content
|
|
||||||
validateConfig(config as Config);
|
validateConfig(config as Config);
|
||||||
|
|
||||||
return config as Config;
|
return config as Config;
|
||||||
@@ -259,7 +248,6 @@ export const parseConfig = async (configPath: string): Promise<Config> => {
|
|||||||
if (error instanceof PentestError) {
|
if (error instanceof PentestError) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
// Wrap other errors with context
|
|
||||||
const errMsg = error instanceof Error ? error.message : String(error);
|
const errMsg = error instanceof Error ? error.message : String(error);
|
||||||
throw new PentestError(
|
throw new PentestError(
|
||||||
`Failed to parse configuration file '${configPath}': ${errMsg}`,
|
`Failed to parse configuration file '${configPath}': ${errMsg}`,
|
||||||
@@ -271,9 +259,7 @@ export const parseConfig = async (configPath: string): Promise<Config> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate overall configuration structure using JSON Schema
|
|
||||||
const validateConfig = (config: Config): void => {
|
const validateConfig = (config: Config): void => {
|
||||||
// Basic structure validation
|
|
||||||
if (!config || typeof config !== 'object') {
|
if (!config || typeof config !== 'object') {
|
||||||
throw new PentestError(
|
throw new PentestError(
|
||||||
'Configuration must be a valid object',
|
'Configuration must be a valid object',
|
||||||
@@ -294,7 +280,6 @@ const validateConfig = (config: Config): void => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSON Schema validation
|
|
||||||
const isValid = validateSchema(config);
|
const isValid = validateSchema(config);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
const errors = validateSchema.errors || [];
|
const errors = validateSchema.errors || [];
|
||||||
@@ -308,10 +293,8 @@ const validateConfig = (config: Config): void => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional security validation
|
|
||||||
performSecurityValidation(config);
|
performSecurityValidation(config);
|
||||||
|
|
||||||
// Ensure at least some configuration is provided
|
|
||||||
if (!config.rules && !config.authentication) {
|
if (!config.rules && !config.authentication) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'⚠️ Configuration file contains no rules or authentication. The pentest will run without any scoping restrictions or login capabilities.'
|
'⚠️ Configuration file contains no rules or authentication. The pentest will run without any scoping restrictions or login capabilities.'
|
||||||
@@ -323,9 +306,7 @@ const validateConfig = (config: Config): void => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Perform additional security validation beyond JSON Schema
|
|
||||||
const performSecurityValidation = (config: Config): void => {
|
const performSecurityValidation = (config: Config): void => {
|
||||||
// Validate authentication section for security issues
|
|
||||||
if (config.authentication) {
|
if (config.authentication) {
|
||||||
const auth = config.authentication;
|
const auth = config.authentication;
|
||||||
|
|
||||||
@@ -344,7 +325,6 @@ const performSecurityValidation = (config: Config): void => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for dangerous patterns in credentials
|
|
||||||
if (auth.credentials) {
|
if (auth.credentials) {
|
||||||
for (const pattern of DANGEROUS_PATTERNS) {
|
for (const pattern of DANGEROUS_PATTERNS) {
|
||||||
if (pattern.test(auth.credentials.username)) {
|
if (pattern.test(auth.credentials.username)) {
|
||||||
@@ -368,7 +348,6 @@ const performSecurityValidation = (config: Config): void => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check login flow for dangerous patterns
|
|
||||||
if (auth.login_flow) {
|
if (auth.login_flow) {
|
||||||
auth.login_flow.forEach((step, index) => {
|
auth.login_flow.forEach((step, index) => {
|
||||||
for (const pattern of DANGEROUS_PATTERNS) {
|
for (const pattern of DANGEROUS_PATTERNS) {
|
||||||
@@ -386,24 +365,20 @@ const performSecurityValidation = (config: Config): void => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate rules section for security issues
|
|
||||||
if (config.rules) {
|
if (config.rules) {
|
||||||
validateRulesSecurity(config.rules.avoid, 'avoid');
|
validateRulesSecurity(config.rules.avoid, 'avoid');
|
||||||
validateRulesSecurity(config.rules.focus, 'focus');
|
validateRulesSecurity(config.rules.focus, 'focus');
|
||||||
|
|
||||||
// Check for duplicate and conflicting rules
|
|
||||||
checkForDuplicates(config.rules.avoid || [], 'avoid');
|
checkForDuplicates(config.rules.avoid || [], 'avoid');
|
||||||
checkForDuplicates(config.rules.focus || [], 'focus');
|
checkForDuplicates(config.rules.focus || [], 'focus');
|
||||||
checkForConflicts(config.rules.avoid, config.rules.focus);
|
checkForConflicts(config.rules.avoid, config.rules.focus);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate rules for security issues
|
|
||||||
const validateRulesSecurity = (rules: Rule[] | undefined, ruleType: string): void => {
|
const validateRulesSecurity = (rules: Rule[] | undefined, ruleType: string): void => {
|
||||||
if (!rules) return;
|
if (!rules) return;
|
||||||
|
|
||||||
rules.forEach((rule, index) => {
|
rules.forEach((rule, index) => {
|
||||||
// Security validation
|
|
||||||
for (const pattern of DANGEROUS_PATTERNS) {
|
for (const pattern of DANGEROUS_PATTERNS) {
|
||||||
if (pattern.test(rule.url_path)) {
|
if (pattern.test(rule.url_path)) {
|
||||||
throw new PentestError(
|
throw new PentestError(
|
||||||
@@ -425,12 +400,10 @@ const validateRulesSecurity = (rules: Rule[] | undefined, ruleType: string): voi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type-specific validation
|
|
||||||
validateRuleTypeSpecific(rule, ruleType, index);
|
validateRuleTypeSpecific(rule, ruleType, index);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate rule based on its specific type
|
|
||||||
const validateRuleTypeSpecific = (rule: Rule, ruleType: string, index: number): void => {
|
const validateRuleTypeSpecific = (rule: Rule, ruleType: string, index: number): void => {
|
||||||
const field = `rules.${ruleType}[${index}].url_path`;
|
const field = `rules.${ruleType}[${index}].url_path`;
|
||||||
|
|
||||||
@@ -486,7 +459,6 @@ const validateRuleTypeSpecific = (rule: Rule, ruleType: string, index: number):
|
|||||||
}
|
}
|
||||||
|
|
||||||
case 'header':
|
case 'header':
|
||||||
// Header name validation (basic)
|
|
||||||
if (!rule.url_path.match(/^[a-zA-Z0-9\-_]+$/)) {
|
if (!rule.url_path.match(/^[a-zA-Z0-9\-_]+$/)) {
|
||||||
throw new PentestError(
|
throw new PentestError(
|
||||||
`${field} for type 'header' must be a valid header name (alphanumeric, hyphens, underscores only)`,
|
`${field} for type 'header' must be a valid header name (alphanumeric, hyphens, underscores only)`,
|
||||||
@@ -499,7 +471,6 @@ const validateRuleTypeSpecific = (rule: Rule, ruleType: string, index: number):
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'parameter':
|
case 'parameter':
|
||||||
// Parameter name validation (basic)
|
|
||||||
if (!rule.url_path.match(/^[a-zA-Z0-9\-_]+$/)) {
|
if (!rule.url_path.match(/^[a-zA-Z0-9\-_]+$/)) {
|
||||||
throw new PentestError(
|
throw new PentestError(
|
||||||
`${field} for type 'parameter' must be a valid parameter name (alphanumeric, hyphens, underscores only)`,
|
`${field} for type 'parameter' must be a valid parameter name (alphanumeric, hyphens, underscores only)`,
|
||||||
@@ -513,7 +484,6 @@ const validateRuleTypeSpecific = (rule: Rule, ruleType: string, index: number):
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for duplicate rules
|
|
||||||
const checkForDuplicates = (rules: Rule[], ruleType: string): void => {
|
const checkForDuplicates = (rules: Rule[], ruleType: string): void => {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
rules.forEach((rule, index) => {
|
rules.forEach((rule, index) => {
|
||||||
@@ -531,7 +501,6 @@ const checkForDuplicates = (rules: Rule[], ruleType: string): void => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for conflicting rules between avoid and focus
|
|
||||||
const checkForConflicts = (avoidRules: Rule[] = [], focusRules: Rule[] = []): void => {
|
const checkForConflicts = (avoidRules: Rule[] = [], focusRules: Rule[] = []): void => {
|
||||||
const avoidSet = new Set(avoidRules.map((rule) => `${rule.type}:${rule.url_path}`));
|
const avoidSet = new Set(avoidRules.map((rule) => `${rule.type}:${rule.url_path}`));
|
||||||
|
|
||||||
@@ -549,7 +518,6 @@ const checkForConflicts = (avoidRules: Rule[] = [], focusRules: Rule[] = []): vo
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sanitize and normalize rule values
|
|
||||||
const sanitizeRule = (rule: Rule): Rule => {
|
const sanitizeRule = (rule: Rule): Rule => {
|
||||||
return {
|
return {
|
||||||
description: rule.description.trim(),
|
description: rule.description.trim(),
|
||||||
@@ -558,7 +526,6 @@ const sanitizeRule = (rule: Rule): Rule => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Distribute configuration sections to different agents with sanitization
|
|
||||||
export const distributeConfig = (config: Config | null): DistributedConfig => {
|
export const distributeConfig = (config: Config | null): DistributedConfig => {
|
||||||
const avoid = config?.rules?.avoid || [];
|
const avoid = config?.rules?.avoid || [];
|
||||||
const focus = config?.rules?.focus || [];
|
const focus = config?.rules?.focus || [];
|
||||||
@@ -571,7 +538,6 @@ export const distributeConfig = (config: Config | null): DistributedConfig => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sanitize and normalize authentication values
|
|
||||||
const sanitizeAuthentication = (auth: Authentication): Authentication => {
|
const sanitizeAuthentication = (auth: Authentication): Authentication => {
|
||||||
return {
|
return {
|
||||||
login_type: auth.login_type.toLowerCase().trim() as Authentication['login_type'],
|
login_type: auth.login_type.toLowerCase().trim() as Authentication['login_type'],
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
matchesBillingTextPattern,
|
matchesBillingTextPattern,
|
||||||
} from '../utils/billing-detection.js';
|
} from '../utils/billing-detection.js';
|
||||||
|
|
||||||
// Custom error class for pentest operations
|
|
||||||
export class PentestError extends Error {
|
export class PentestError extends Error {
|
||||||
override name = 'PentestError' as const;
|
override name = 'PentestError' as const;
|
||||||
type: PentestErrorType;
|
type: PentestErrorType;
|
||||||
@@ -43,8 +42,6 @@ export class PentestError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle tool execution errors
|
|
||||||
// Handle prompt loading errors
|
|
||||||
export function handlePromptError(
|
export function handlePromptError(
|
||||||
promptName: string,
|
promptName: string,
|
||||||
error: Error
|
error: Error
|
||||||
@@ -60,7 +57,6 @@ export function handlePromptError(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patterns that indicate retryable errors
|
|
||||||
const RETRYABLE_PATTERNS = [
|
const RETRYABLE_PATTERNS = [
|
||||||
// Network and connection errors
|
// Network and connection errors
|
||||||
'network',
|
'network',
|
||||||
@@ -104,12 +100,10 @@ const NON_RETRYABLE_PATTERNS = [
|
|||||||
export function isRetryableError(error: Error): boolean {
|
export function isRetryableError(error: Error): boolean {
|
||||||
const message = error.message.toLowerCase();
|
const message = error.message.toLowerCase();
|
||||||
|
|
||||||
// Check for explicit non-retryable patterns first
|
|
||||||
if (NON_RETRYABLE_PATTERNS.some((pattern) => message.includes(pattern))) {
|
if (NON_RETRYABLE_PATTERNS.some((pattern) => message.includes(pattern))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for retryable patterns
|
|
||||||
return RETRYABLE_PATTERNS.some((pattern) => message.includes(pattern));
|
return RETRYABLE_PATTERNS.some((pattern) => message.includes(pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,8 +194,6 @@ async function runAgentActivity(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Individual Agent Activity Exports ===
|
|
||||||
|
|
||||||
export async function runPreReconAgent(input: ActivityInput): Promise<AgentMetrics> {
|
export async function runPreReconAgent(input: ActivityInput): Promise<AgentMetrics> {
|
||||||
return runAgentActivity('pre-recon', input);
|
return runAgentActivity('pre-recon', input);
|
||||||
}
|
}
|
||||||
@@ -248,8 +246,6 @@ export async function runReportAgent(input: ActivityInput): Promise<AgentMetrics
|
|||||||
return runAgentActivity('report', input);
|
return runAgentActivity('report', input);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Report Assembly Activities ===
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assemble the final report by concatenating exploitation evidence files.
|
* Assemble the final report by concatenating exploitation evidence files.
|
||||||
*/
|
*/
|
||||||
@@ -282,8 +278,6 @@ export async function injectReportMetadataActivity(input: ActivityInput): Promis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Exploitation Queue Check ===
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if exploitation should run for a given vulnerability type.
|
* Check if exploitation should run for a given vulnerability type.
|
||||||
*
|
*
|
||||||
@@ -304,8 +298,6 @@ export async function checkExploitationQueue(
|
|||||||
return checker.checkQueue(vulnType, repoPath, logger);
|
return checker.checkQueue(vulnType, repoPath, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Resume Activities ===
|
|
||||||
|
|
||||||
interface SessionJson {
|
interface SessionJson {
|
||||||
session: {
|
session: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -511,8 +503,6 @@ export async function recordResumeAttempt(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Phase Transition Activities ===
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log phase transition to the unified workflow log.
|
* Log phase transition to the unified workflow log.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -229,7 +229,6 @@ async function startPipeline(): Promise<void> {
|
|||||||
const workspaceExists = await fileExists(sessionPath);
|
const workspaceExists = await fileExists(sessionPath);
|
||||||
|
|
||||||
if (workspaceExists) {
|
if (workspaceExists) {
|
||||||
// === Resume Mode: existing workspace ===
|
|
||||||
isResume = true;
|
isResume = true;
|
||||||
console.log('=== RESUME MODE ===');
|
console.log('=== RESUME MODE ===');
|
||||||
console.log(`Workspace: ${resumeFromWorkspace}\n`);
|
console.log(`Workspace: ${resumeFromWorkspace}\n`);
|
||||||
@@ -255,7 +254,6 @@ async function startPipeline(): Promise<void> {
|
|||||||
workflowId = `${resumeFromWorkspace}_resume_${Date.now()}`;
|
workflowId = `${resumeFromWorkspace}_resume_${Date.now()}`;
|
||||||
sessionId = resumeFromWorkspace;
|
sessionId = resumeFromWorkspace;
|
||||||
} else {
|
} else {
|
||||||
// === New Named Workspace ===
|
|
||||||
if (!isValidWorkspaceName(resumeFromWorkspace)) {
|
if (!isValidWorkspaceName(resumeFromWorkspace)) {
|
||||||
console.error(`ERROR: Invalid workspace name: "${resumeFromWorkspace}"`);
|
console.error(`ERROR: Invalid workspace name: "${resumeFromWorkspace}"`);
|
||||||
console.error(' Must be 1-128 characters, alphanumeric/hyphens/underscores, starting with alphanumeric');
|
console.error(' Must be 1-128 characters, alphanumeric/hyphens/underscores, starting with alphanumeric');
|
||||||
@@ -269,7 +267,6 @@ async function startPipeline(): Promise<void> {
|
|||||||
sessionId = resumeFromWorkspace;
|
sessionId = resumeFromWorkspace;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// === New Auto-Named Workflow ===
|
|
||||||
const hostname = sanitizeHostname(webUrl);
|
const hostname = sanitizeHostname(webUrl);
|
||||||
workflowId = customWorkflowId || `${hostname}_shannon-${Date.now()}`;
|
workflowId = customWorkflowId || `${hostname}_shannon-${Date.now()}`;
|
||||||
sessionId = workflowId;
|
sessionId = workflowId;
|
||||||
@@ -278,8 +275,8 @@ async function startPipeline(): Promise<void> {
|
|||||||
const input: PipelineInput = {
|
const input: PipelineInput = {
|
||||||
webUrl,
|
webUrl,
|
||||||
repoPath,
|
repoPath,
|
||||||
workflowId, // Add for audit correlation
|
workflowId,
|
||||||
sessionId, // Workspace directory name
|
sessionId,
|
||||||
...(configPath && { configPath }),
|
...(configPath && { configPath }),
|
||||||
...(outputPath && { outputPath }),
|
...(outputPath && { outputPath }),
|
||||||
...(pipelineTestingMode && { pipelineTestingMode }),
|
...(pipelineTestingMode && { pipelineTestingMode }),
|
||||||
@@ -287,7 +284,6 @@ async function startPipeline(): Promise<void> {
|
|||||||
...(terminatedWorkflows.length > 0 && { terminatedWorkflows }),
|
...(terminatedWorkflows.length > 0 && { terminatedWorkflows }),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine output directory for display (use sessionId for persistent directory)
|
|
||||||
// Use displayOutputPath (host path) if provided, otherwise fall back to outputPath or default
|
// Use displayOutputPath (host path) if provided, otherwise fall back to outputPath or default
|
||||||
const effectiveDisplayPath = displayOutputPath || outputPath || './audit-logs';
|
const effectiveDisplayPath = displayOutputPath || outputPath || './audit-logs';
|
||||||
const outputDir = `${effectiveDisplayPath}/${sessionId}`;
|
const outputDir = `${effectiveDisplayPath}/${sessionId}`;
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ import { defineQuery } from '@temporalio/workflow';
|
|||||||
export type { AgentMetrics } from '../types/metrics.js';
|
export type { AgentMetrics } from '../types/metrics.js';
|
||||||
import type { AgentMetrics } from '../types/metrics.js';
|
import type { AgentMetrics } from '../types/metrics.js';
|
||||||
|
|
||||||
// === Types ===
|
|
||||||
|
|
||||||
export interface PipelineInput {
|
export interface PipelineInput {
|
||||||
webUrl: string;
|
webUrl: string;
|
||||||
repoPath: string;
|
repoPath: string;
|
||||||
configPath?: string;
|
configPath?: string;
|
||||||
outputPath?: string;
|
outputPath?: string;
|
||||||
pipelineTestingMode?: boolean;
|
pipelineTestingMode?: boolean;
|
||||||
workflowId?: string; // Added by client, used for audit correlation
|
workflowId?: string; // Used for audit correlation
|
||||||
sessionId?: string; // Workspace directory name (distinct from workflowId for named workspaces)
|
sessionId?: string; // Workspace directory name (distinct from workflowId for named workspaces)
|
||||||
resumeFromWorkspace?: string; // Workspace name to resume from
|
resumeFromWorkspace?: string; // Workspace name to resume from
|
||||||
terminatedWorkflows?: string[]; // Workflows terminated during resume
|
terminatedWorkflows?: string[]; // Workflows terminated during resume
|
||||||
@@ -62,6 +60,4 @@ export interface VulnExploitPipelineResult {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Queries ===
|
|
||||||
|
|
||||||
export const getProgress = defineQuery<PipelineProgress>('getProgress');
|
export const getProgress = defineQuery<PipelineProgress>('getProgress');
|
||||||
|
|||||||
@@ -105,11 +105,9 @@ export async function pentestPipelineWorkflow(
|
|||||||
): Promise<PipelineState> {
|
): Promise<PipelineState> {
|
||||||
const { workflowId } = workflowInfo();
|
const { workflowId } = workflowInfo();
|
||||||
|
|
||||||
// Select activity proxy based on testing mode
|
|
||||||
// Pipeline testing uses fast retry intervals (10s) for quick iteration
|
// Pipeline testing uses fast retry intervals (10s) for quick iteration
|
||||||
const a = input.pipelineTestingMode ? testActs : acts;
|
const a = input.pipelineTestingMode ? testActs : acts;
|
||||||
|
|
||||||
// Workflow state (queryable)
|
|
||||||
const state: PipelineState = {
|
const state: PipelineState = {
|
||||||
status: 'running',
|
status: 'running',
|
||||||
currentPhase: null,
|
currentPhase: null,
|
||||||
@@ -122,7 +120,6 @@ export async function pentestPipelineWorkflow(
|
|||||||
summary: null,
|
summary: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Register query handler for real-time progress inspection
|
|
||||||
setHandler(getProgress, (): PipelineProgress => ({
|
setHandler(getProgress, (): PipelineProgress => ({
|
||||||
...state,
|
...state,
|
||||||
workflowId,
|
workflowId,
|
||||||
@@ -147,18 +144,15 @@ export async function pentestPipelineWorkflow(
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// === RESUME LOGIC ===
|
|
||||||
let resumeState: ResumeState | null = null;
|
let resumeState: ResumeState | null = null;
|
||||||
|
|
||||||
if (input.resumeFromWorkspace) {
|
if (input.resumeFromWorkspace) {
|
||||||
// Load resume state from existing workspace
|
|
||||||
resumeState = await a.loadResumeState(
|
resumeState = await a.loadResumeState(
|
||||||
input.resumeFromWorkspace,
|
input.resumeFromWorkspace,
|
||||||
input.webUrl,
|
input.webUrl,
|
||||||
input.repoPath
|
input.repoPath
|
||||||
);
|
);
|
||||||
|
|
||||||
// Restore git checkpoint and clean up partial deliverables
|
|
||||||
const incompleteAgents = ALL_AGENTS.filter(
|
const incompleteAgents = ALL_AGENTS.filter(
|
||||||
(agentName) => !resumeState!.completedAgents.includes(agentName)
|
(agentName) => !resumeState!.completedAgents.includes(agentName)
|
||||||
) as AgentName[];
|
) as AgentName[];
|
||||||
@@ -169,7 +163,6 @@ export async function pentestPipelineWorkflow(
|
|||||||
incompleteAgents
|
incompleteAgents
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if all agents are already complete
|
|
||||||
if (resumeState.completedAgents.length === ALL_AGENTS.length) {
|
if (resumeState.completedAgents.length === ALL_AGENTS.length) {
|
||||||
log.info(`All ${ALL_AGENTS.length} agents already completed. Nothing to resume.`);
|
log.info(`All ${ALL_AGENTS.length} agents already completed. Nothing to resume.`);
|
||||||
state.status = 'completed';
|
state.status = 'completed';
|
||||||
@@ -178,7 +171,6 @@ export async function pentestPipelineWorkflow(
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Record resume attempt in session.json and write resume header to workflow.log
|
|
||||||
await a.recordResumeAttempt(
|
await a.recordResumeAttempt(
|
||||||
activityInput,
|
activityInput,
|
||||||
input.terminatedWorkflows || [],
|
input.terminatedWorkflows || [],
|
||||||
@@ -190,7 +182,6 @@ export async function pentestPipelineWorkflow(
|
|||||||
log.info('Resume state loaded and workspace restored');
|
log.info('Resume state loaded and workspace restored');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to check if an agent should be skipped
|
|
||||||
const shouldSkip = (agentName: string): boolean => {
|
const shouldSkip = (agentName: string): boolean => {
|
||||||
return resumeState?.completedAgents.includes(agentName) ?? false;
|
return resumeState?.completedAgents.includes(agentName) ?? false;
|
||||||
};
|
};
|
||||||
@@ -413,7 +404,6 @@ export async function pentestPipelineWorkflow(
|
|||||||
state.completedAgents.push('report');
|
state.completedAgents.push('report');
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Complete ===
|
|
||||||
state.status = 'completed';
|
state.status = 'completed';
|
||||||
state.currentPhase = null;
|
state.currentPhase = null;
|
||||||
state.currentAgent = null;
|
state.currentAgent = null;
|
||||||
|
|||||||
@@ -4,8 +4,6 @@
|
|||||||
// it under the terms of the GNU Affero General Public License version 3
|
// it under the terms of the GNU Affero General Public License version 3
|
||||||
// as published by the Free Software Foundation.
|
// as published by the Free Software Foundation.
|
||||||
|
|
||||||
// Timing utilities
|
|
||||||
|
|
||||||
export class Timer {
|
export class Timer {
|
||||||
name: string;
|
name: string;
|
||||||
startTime: number;
|
startTime: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user