Initial commit

This commit is contained in:
ajmallesh
2025-10-03 19:35:08 -07:00
commit 9327630c45
69 changed files with 16062 additions and 0 deletions

307
src/config-parser.js Normal file
View File

@@ -0,0 +1,307 @@
import { fs } from 'zx';
import yaml from 'js-yaml';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { PentestError } from './error-handling.js';
// Initialize AJV with formats
const ajv = new Ajv({ allErrors: true, verbose: true });
addFormats(ajv);
// Load JSON Schema
let configSchema;
try {
const schemaPath = new URL('../configs/config-schema.json', import.meta.url);
const schemaContent = await fs.readFile(schemaPath, 'utf8');
configSchema = JSON.parse(schemaContent);
} catch (error) {
throw new PentestError(
`Failed to load configuration schema: ${error.message}`,
'config',
false,
{ schemaPath: '../configs/config-schema.json', originalError: error.message }
);
}
// Compile the schema validator
const validateSchema = ajv.compile(configSchema);
// Security patterns to block
const DANGEROUS_PATTERNS = [
/\.\.\//, // Path traversal
/[<>]/, // HTML/XML injection
/javascript:/i, // JavaScript URLs
/data:/i, // Data URLs
/file:/i // File URLs
];
// Parse and load YAML configuration file with enhanced safety
export const parseConfig = async (configPath) => {
try {
// File existence check
if (!await fs.pathExists(configPath)) {
throw new Error(`Configuration file not found: ${configPath}`);
}
// File size check (prevent extremely large files)
const stats = await fs.stat(configPath);
const maxFileSize = 1024 * 1024; // 1MB
if (stats.size > maxFileSize) {
throw new Error(`Configuration file too large: ${stats.size} bytes (maximum: ${maxFileSize} bytes)`);
}
// Read file content
const configContent = await fs.readFile(configPath, 'utf8');
// Basic content validation
if (!configContent.trim()) {
throw new Error('Configuration file is empty');
}
// Parse YAML with safety options
let config;
try {
config = yaml.load(configContent, {
schema: yaml.FAILSAFE_SCHEMA, // Only basic YAML types, no JS evaluation
json: false, // Don't allow JSON-specific syntax
filename: configPath
});
} catch (yamlError) {
throw new Error(`YAML parsing failed: ${yamlError.message}`);
}
// Additional safety check
if (config === null || config === undefined) {
throw new Error('Configuration file resulted in null/undefined after parsing');
}
// Validate the configuration structure and content
validateConfig(config);
return config;
} catch (error) {
// Enhance error message with context
if (error.message.startsWith('Configuration file not found') ||
error.message.startsWith('YAML parsing failed') ||
error.message.includes('must be') ||
error.message.includes('exceeds maximum')) {
// These are already well-formatted errors, re-throw as-is
throw error;
} else {
// Wrap other errors with context
throw new Error(`Failed to parse configuration file '${configPath}': ${error.message}`);
}
}
};
// Validate overall configuration structure using JSON Schema
const validateConfig = (config) => {
// Basic structure validation
if (!config || typeof config !== 'object') {
throw new Error('Configuration must be a valid object');
}
if (Array.isArray(config)) {
throw new Error('Configuration must be an object, not an array');
}
// JSON Schema validation
const isValid = validateSchema(config);
if (!isValid) {
const errors = validateSchema.errors || [];
const errorMessages = errors.map(err => {
const path = err.instancePath || 'root';
return `${path}: ${err.message}`;
});
throw new Error(`Configuration validation failed:\n - ${errorMessages.join('\n - ')}`);
}
// Additional security validation
performSecurityValidation(config);
// Warn if deprecated fields are used
if (config.login) {
console.warn('⚠️ The "login" section is deprecated. Please use "authentication" instead.');
}
// Ensure at least some configuration is provided
if (!config.rules && !config.authentication) {
console.warn('⚠️ Configuration file contains no rules or authentication. The pentest will run without any scoping restrictions or login capabilities.');
} else if (config.rules && !config.rules.avoid && !config.rules.focus) {
console.warn('⚠️ Configuration file contains no rules. The pentest will run without any scoping restrictions.');
}
};
// Perform additional security validation beyond JSON Schema
const performSecurityValidation = (config) => {
// Validate authentication section for security issues
if (config.authentication) {
const auth = config.authentication;
// Check for dangerous patterns in credentials
if (auth.credentials) {
for (const pattern of DANGEROUS_PATTERNS) {
if (pattern.test(auth.credentials.username)) {
throw new Error('authentication.credentials.username contains potentially dangerous pattern');
}
if (pattern.test(auth.credentials.password)) {
throw new Error('authentication.credentials.password contains potentially dangerous pattern');
}
}
}
// Check login flow for dangerous patterns
if (auth.login_flow) {
auth.login_flow.forEach((step, index) => {
for (const pattern of DANGEROUS_PATTERNS) {
if (pattern.test(step)) {
throw new Error(`authentication.login_flow[${index}] contains potentially dangerous pattern: ${pattern.source}`);
}
}
});
}
}
// Validate rules section for security issues
if (config.rules) {
validateRulesSecurity(config.rules.avoid, 'avoid');
validateRulesSecurity(config.rules.focus, 'focus');
// Check for duplicate and conflicting rules
checkForDuplicates(config.rules.avoid || [], 'avoid');
checkForDuplicates(config.rules.focus || [], 'focus');
checkForConflicts(config.rules.avoid, config.rules.focus);
}
};
// Validate rules for security issues
const validateRulesSecurity = (rules, ruleType) => {
if (!rules) return;
rules.forEach((rule, index) => {
// Security validation
for (const pattern of DANGEROUS_PATTERNS) {
if (pattern.test(rule.url_path)) {
throw new Error(`rules.${ruleType}[${index}].url_path contains potentially dangerous pattern: ${pattern.source}`);
}
if (pattern.test(rule.description)) {
throw new Error(`rules.${ruleType}[${index}].description contains potentially dangerous pattern: ${pattern.source}`);
}
}
// Type-specific validation
validateRuleTypeSpecific(rule, ruleType, index);
});
};
// Validate rule based on its specific type
const validateRuleTypeSpecific = (rule, ruleType, index) => {
switch (rule.type) {
case 'path':
if (!rule.url_path.startsWith('/')) {
throw new Error(`rules.${ruleType}[${index}].url_path for type 'path' must start with '/'`);
}
break;
case 'subdomain':
case 'domain':
// Basic domain validation - no slashes allowed
if (rule.url_path.includes('/')) {
throw new Error(`rules.${ruleType}[${index}].url_path for type '${rule.type}' cannot contain '/' characters`);
}
// Must contain at least one dot for domains
if (rule.type === 'domain' && !rule.url_path.includes('.')) {
throw new Error(`rules.${ruleType}[${index}].url_path for type 'domain' must be a valid domain name`);
}
break;
case 'method':
const allowedMethods = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
if (!allowedMethods.includes(rule.url_path.toUpperCase())) {
throw new Error(`rules.${ruleType}[${index}].url_path for type 'method' must be one of: ${allowedMethods.join(', ')}`);
}
break;
case 'header':
// Header name validation (basic)
if (!rule.url_path.match(/^[a-zA-Z0-9\-_]+$/)) {
throw new Error(`rules.${ruleType}[${index}].url_path for type 'header' must be a valid header name (alphanumeric, hyphens, underscores only)`);
}
break;
case 'parameter':
// Parameter name validation (basic)
if (!rule.url_path.match(/^[a-zA-Z0-9\-_]+$/)) {
throw new Error(`rules.${ruleType}[${index}].url_path for type 'parameter' must be a valid parameter name (alphanumeric, hyphens, underscores only)`);
}
break;
}
};
// Check for duplicate rules
const checkForDuplicates = (rules, ruleType) => {
const seen = new Set();
rules.forEach((rule, index) => {
const key = `${rule.type}:${rule.url_path}`;
if (seen.has(key)) {
throw new Error(`Duplicate rule found in rules.${ruleType}[${index}]: ${rule.type} '${rule.url_path}'`);
}
seen.add(key);
});
};
// Check for conflicting rules between avoid and focus
const checkForConflicts = (avoidRules = [], focusRules = []) => {
const avoidSet = new Set(avoidRules.map(rule => `${rule.type}:${rule.url_path}`));
focusRules.forEach((rule, index) => {
const key = `${rule.type}:${rule.url_path}`;
if (avoidSet.has(key)) {
throw new Error(`Conflicting rule found: rules.focus[${index}] '${rule.url_path}' also exists in rules.avoid`);
}
});
};
// Sanitize and normalize rule values
const sanitizeRule = (rule) => {
return {
description: rule.description.trim(),
type: rule.type.toLowerCase().trim(),
url_path: rule.url_path.trim()
};
};
// Distribute configuration sections to different agents with sanitization
export const distributeConfig = (config) => {
const avoid = config?.rules?.avoid || [];
const focus = config?.rules?.focus || [];
const authentication = config?.authentication || null;
return {
avoid: avoid.map(sanitizeRule),
focus: focus.map(sanitizeRule),
authentication: authentication ? sanitizeAuthentication(authentication) : null
};
};
// Sanitize and normalize authentication values
const sanitizeAuthentication = (auth) => {
return {
login_type: auth.login_type.toLowerCase().trim(),
login_url: auth.login_url.trim(),
credentials: {
username: auth.credentials.username.trim(),
password: auth.credentials.password,
...(auth.credentials.totp_secret && { totp_secret: auth.credentials.totp_secret.trim() })
},
login_flow: auth.login_flow.map(step => step.trim()),
success_condition: {
type: auth.success_condition.type.toLowerCase().trim(),
value: auth.success_condition.value.trim()
}
};
};
// Additional validation functions are already exported above