mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-02-12 17:22:50 +00:00
Initial commit
This commit is contained in:
307
src/config-parser.js
Normal file
307
src/config-parser.js
Normal 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
|
||||
|
||||
Reference in New Issue
Block a user