@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
JavaScript
;
/**
* @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