UNPKG

@syntropysoft/praetorian

Version:

Praetorian CLI – A universal multi-environment configuration validator for DevSecOps teams. Validate, compare, and secure YAML/ENV files with ease.

402 lines 15.5 kB
"use strict"; /** * @file src/application/validation/ValidationEngine.ts * @description Pure functional validation engine for Praetorian */ Object.defineProperty(exports, "__esModule", { value: true }); exports.validateCompliance = exports.validateVulnerabilities = exports.validatePermissions = exports.validateSecretDetection = exports.validateAgainstSchema = exports.validatePattern = exports.validateFormat = exports.calculateObjectDepth = exports.getNestedValue = exports.hasProperty = exports.createValidationWarning = exports.createValidationError = exports.createFailedRuleResult = exports.createPassedRuleResult = exports.createEmptyValidationOutput = exports.validateSchemaRule = exports.validateSecurityRule = exports.validateFormatRule = exports.validateStructureRule = exports.applyRuleByType = exports.validateRule = exports.validate = void 0; /** * Pure functional validation engine * @param input - Validation input * @returns Validation output */ const validate = (input) => { // Guard clause: invalid input if (!input || !input.rules || input.rules.length === 0) { return (0, exports.createEmptyValidationOutput)(); } const startTime = Date.now(); // Apply all rules to the data const ruleResults = input.rules .filter(rule => rule.enabled) .map(rule => (0, exports.validateRule)(rule, input.data, input.context)); // Collect all results const allErrors = ruleResults.flatMap(result => result.errors); const allWarnings = ruleResults.flatMap(result => result.warnings); const passedRules = ruleResults.filter(result => result.passed).length; const failedRules = ruleResults.filter(result => !result.passed).length; return { valid: allErrors.length === 0, errors: allErrors, warnings: allWarnings, metadata: { rulesApplied: input.rules.filter(rule => rule.enabled).length, rulesPassed: passedRules, rulesFailed: failedRules, duration: Date.now() - startTime, }, }; }; exports.validate = validate; /** * Validates a single rule against data * @param rule - Rule to apply * @param data - Data to validate * @param context - Validation context * @returns Rule validation result */ const validateRule = (rule, data, context) => { // Guard clause: disabled rule if (!rule.enabled) { return (0, exports.createPassedRuleResult)(rule.id); } // Guard clause: no data if (data === null || data === undefined) { return (0, exports.createFailedRuleResult)(rule.id, [ (0, exports.createValidationError)(rule.id, 'NO_DATA', 'Data cannot be null or undefined', rule.severity), ]); } // Apply rule based on type const result = (0, exports.applyRuleByType)(rule, data, context); return { ruleId: rule.id, passed: result.errors.length === 0, errors: result.errors, warnings: result.warnings, }; }; exports.validateRule = validateRule; /** * Applies a rule based on its type * @param rule - Rule to apply * @param data - Data to validate * @param context - Validation context * @returns Rule application result */ const applyRuleByType = (rule, data, context) => { switch (rule.type) { case 'structure': return (0, exports.validateStructureRule)(rule, data); case 'format': return (0, exports.validateFormatRule)(rule, data); case 'security': return (0, exports.validateSecurityRule)(rule, data, context); case 'schema': return (0, exports.validateSchemaRule)(rule, data); default: return { errors: [(0, exports.createValidationError)(rule.id || 'unknown', 'UNKNOWN_RULE_TYPE', `Rule type '${rule.type}' is not supported`, 'error')], warnings: [], }; } }; exports.applyRuleByType = applyRuleByType; /** * Validates structure rules * @param rule - Structure rule * @param data - Data to validate * @returns Validation result */ const validateStructureRule = (rule, // StructureRule data) => { const errors = []; const warnings = []; // Guard clause: not an object if (typeof data !== 'object' || Array.isArray(data)) { errors.push((0, exports.createValidationError)(rule.id, 'INVALID_DATA_STRUCTURE', 'Structure validation requires an object', rule.severity)); return { errors, warnings }; } // Check required properties if (rule.requiredProperties && Array.isArray(rule.requiredProperties)) { for (const prop of rule.requiredProperties) { if (!(0, exports.hasProperty)(data, prop)) { if (rule.severity === 'warning') { warnings.push((0, exports.createValidationWarning)(rule.id, 'MISSING_REQUIRED_PROPERTY', `Required property '${prop}' is missing`)); } else { errors.push((0, exports.createValidationError)(rule.id, 'MISSING_REQUIRED_PROPERTY', `Required property '${prop}' is missing`, rule.severity)); } } } } // Check forbidden properties if (rule.forbiddenProperties && Array.isArray(rule.forbiddenProperties)) { for (const prop of rule.forbiddenProperties) { if ((0, exports.hasProperty)(data, prop)) { if (rule.severity === 'warning') { warnings.push((0, exports.createValidationWarning)(rule.id, 'FORBIDDEN_PROPERTY', `Property '${prop}' is not allowed`)); } else { errors.push((0, exports.createValidationError)(rule.id, 'FORBIDDEN_PROPERTY', `Property '${prop}' is not allowed`, rule.severity)); } } } } // Check max depth if (rule.maxDepth && typeof rule.maxDepth === 'number') { const depth = (0, exports.calculateObjectDepth)(data); if (depth > rule.maxDepth) { if (rule.severity === 'warning') { warnings.push((0, exports.createValidationWarning)(rule.id, 'EXCESSIVE_NESTING', `Object depth ${depth} exceeds maximum allowed depth ${rule.maxDepth}`)); } else { errors.push((0, exports.createValidationError)(rule.id, 'EXCESSIVE_NESTING', `Object depth ${depth} exceeds maximum allowed depth ${rule.maxDepth}`, rule.severity)); } } } return { errors, warnings }; }; exports.validateStructureRule = validateStructureRule; /** * Validates format rules * @param rule - Format rule * @param data - Data to validate * @returns Validation result */ const validateFormatRule = (rule, // FormatRule data) => { const errors = []; const warnings = []; // Get value to validate const value = rule.propertyPath ? (0, exports.getNestedValue)(data, rule.propertyPath) : data; // Guard clause: required field missing if (rule.required && (value === undefined || value === null)) { errors.push((0, exports.createValidationError)(rule.id, 'REQUIRED_FIELD_MISSING', `Required field '${rule.propertyPath || 'root'}' is missing`, rule.severity)); return { errors, warnings }; } // Skip validation if value is not present and not required if (value === undefined || value === null) { return { errors, warnings }; } // Validate format if (rule.format && !(0, exports.validateFormat)(value, rule.format)) { if (rule.severity === 'warning') { warnings.push((0, exports.createValidationWarning)(rule.id, 'INVALID_FORMAT', `Value '${value}' does not match required format '${rule.format}'`)); } else { errors.push((0, exports.createValidationError)(rule.id, 'INVALID_FORMAT', `Value '${value}' does not match required format '${rule.format}'`, rule.severity)); } } // Validate pattern if (rule.pattern && !(0, exports.validatePattern)(value, rule.pattern)) { if (rule.severity === 'warning') { warnings.push((0, exports.createValidationWarning)(rule.id, 'PATTERN_MISMATCH', `Value '${value}' does not match required pattern '${rule.pattern}'`)); } else { errors.push((0, exports.createValidationError)(rule.id, 'PATTERN_MISMATCH', `Value '${value}' does not match required pattern '${rule.pattern}'`, rule.severity)); } } return { errors, warnings }; }; exports.validateFormatRule = validateFormatRule; /** * Validates security rules * @param rule - Security rule * @param data - Data to validate * @param context - Validation context * @returns Validation result */ const validateSecurityRule = (rule, // SecurityRule data, context) => { const errors = []; const warnings = []; // Guard clause: no security type if (!rule.securityType) { errors.push((0, exports.createValidationError)(rule.id, 'INVALID_SECURITY_RULE', 'Security rule must have a securityType', rule.severity)); return { errors, warnings }; } // Apply security validation based on type switch (rule.securityType) { case 'secret': return (0, exports.validateSecretDetection)(rule, data); case 'permission': return (0, exports.validatePermissions)(rule, context); case 'vulnerability': return (0, exports.validateVulnerabilities)(rule, data); case 'compliance': return (0, exports.validateCompliance)(rule, data); default: errors.push((0, exports.createValidationError)(rule.id, 'UNKNOWN_SECURITY_TYPE', `Security type '${rule.securityType}' is not supported`, rule.severity)); } return { errors, warnings }; }; exports.validateSecurityRule = validateSecurityRule; /** * Validates schema rules * @param rule - Schema rule * @param data - Data to validate * @returns Validation result */ const validateSchemaRule = (rule, // SchemaRule data) => { const errors = []; const warnings = []; // Guard clause: schema validation disabled if (!rule.validateSchema) { return { errors, warnings }; } // Guard clause: no schema if (!rule.schema) { errors.push((0, exports.createValidationError)(rule.id, 'NO_SCHEMA_DEFINED', 'Schema validation enabled but no schema provided', rule.severity)); return { errors, warnings }; } // Basic schema validation (simplified for now) const schemaErrors = (0, exports.validateAgainstSchema)(data, rule.schema); errors.push(...schemaErrors.map(error => (0, exports.createValidationError)(rule.id, 'SCHEMA_VALIDATION_FAILED', error, rule.severity))); return { errors, warnings }; }; exports.validateSchemaRule = validateSchemaRule; // Helper functions /** * Creates an empty validation output */ const createEmptyValidationOutput = () => ({ valid: true, errors: [], warnings: [], metadata: { rulesApplied: 0, rulesPassed: 0, rulesFailed: 0, duration: 0, }, }); exports.createEmptyValidationOutput = createEmptyValidationOutput; /** * Creates a passed rule result */ const createPassedRuleResult = (ruleId) => ({ ruleId, passed: true, errors: [], warnings: [], }); exports.createPassedRuleResult = createPassedRuleResult; /** * Creates a failed rule result */ const createFailedRuleResult = (ruleId, errors) => ({ ruleId, passed: false, errors, warnings: [], }); exports.createFailedRuleResult = createFailedRuleResult; /** * Creates a validation error or warning based on severity */ const createValidationError = (ruleId, code, message, severity) => ({ code, message, severity: severity, path: ruleId, }); exports.createValidationError = createValidationError; /** * Creates a validation warning */ const createValidationWarning = (ruleId, code, message) => ({ code, message, severity: 'warning', path: ruleId, }); exports.createValidationWarning = createValidationWarning; /** * Checks if an object has a property (supports dot notation) */ const hasProperty = (obj, path) => { if (!obj || typeof obj !== 'object') return false; return (0, exports.getNestedValue)(obj, path) !== undefined; }; exports.hasProperty = hasProperty; /** * Gets a nested value from an object (supports dot notation) */ const getNestedValue = (obj, path) => { if (!obj || typeof obj !== 'object') return undefined; return path.split('.').reduce((current, key) => current?.[key], obj); }; exports.getNestedValue = getNestedValue; /** * Calculates the depth of an object */ const calculateObjectDepth = (obj, currentDepth = 0) => { if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) { return currentDepth; } const depths = Object.values(obj).map(value => (0, exports.calculateObjectDepth)(value, currentDepth + 1)); return Math.max(currentDepth, ...depths); }; exports.calculateObjectDepth = calculateObjectDepth; /** * Validates a value against a format */ const validateFormat = (value, format) => { const stringValue = String(value); switch (format) { case 'email': return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(stringValue); case 'uri': try { new URL(stringValue); return true; } catch { return false; } case 'uuid': return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(stringValue); case 'semver': return /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/.test(stringValue); case 'string': return typeof value === 'string'; default: return true; // Unknown format, assume valid } }; exports.validateFormat = validateFormat; /** * Validates a value against a regex pattern */ const validatePattern = (value, pattern) => { try { const regex = new RegExp(pattern); return regex.test(String(value)); } catch { return false; // Invalid regex pattern } }; exports.validatePattern = validatePattern; /** * Validates data against a JSON schema (simplified) */ const validateAgainstSchema = (data, schema) => { const errors = []; // Simplified schema validation if (schema.type && typeof data !== schema.type) { errors.push(`Expected type '${schema.type}' but got '${typeof data}'`); } if (schema.required && Array.isArray(schema.required)) { for (const field of schema.required) { if (!(0, exports.hasProperty)(data, field)) { errors.push(`Required field '${field}' is missing`); } } } return errors; }; exports.validateAgainstSchema = validateAgainstSchema; // Placeholder functions for security validation const validateSecretDetection = (rule, data) => ({ errors: [], warnings: [] }); exports.validateSecretDetection = validateSecretDetection; const validatePermissions = (rule, context) => ({ errors: [], warnings: [] }); exports.validatePermissions = validatePermissions; const validateVulnerabilities = (rule, data) => ({ errors: [], warnings: [] }); exports.validateVulnerabilities = validateVulnerabilities; const validateCompliance = (rule, data) => ({ errors: [], warnings: [] }); exports.validateCompliance = validateCompliance; //# sourceMappingURL=ValidationEngine.js.map