UNPKG

@syntropysoft/praetorian

Version:

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

194 lines 8.58 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.EqualityRule = void 0; class EqualityRule { constructor() { this.id = 'equality-rule'; this.name = 'equality'; this.description = 'Validates that configuration files have consistent keys across environments'; this.category = 'compliance'; this.severity = 'error'; this.enabled = true; this.config = {}; } async execute(files, context) { const startTime = Date.now(); const ignoreKeys = context?.ignoreKeys || []; const requiredKeys = context?.requiredKeys || []; if (files.length < 2) { return { success: true, errors: [], warnings: [{ code: 'INSUFFICIENT_FILES', message: 'Need at least 2 files to compare', severity: 'warning' }], metadata: { duration: Date.now() - startTime, rulesChecked: 1, rulesPassed: 1, rulesFailed: 0, filesCompared: files.length } }; } // Pasada 1: Recolectar todas las claves de todos los archivos (excluyendo ignoradas) const masterKeyDictionary = this.collectAllKeys(files, ignoreKeys); // Pasada 2: Comparar diferencias - qué le falta a cada archivo const missingKeysReport = this.compareDifferences(files, masterKeyDictionary, ignoreKeys); // Pasada 3: Validar claves requeridas const requiredKeysReport = this.validateRequiredKeys(files, requiredKeys); // Pasada 4: Detectar claves vacías (solo información, no afecta success) const emptyKeysReport = this.detectEmptyKeys(files, ignoreKeys); // Combinar todos los errores y warnings const allErrors = [...missingKeysReport.errors, ...requiredKeysReport.errors]; const allWarnings = [...missingKeysReport.warnings, ...requiredKeysReport.warnings]; // Las claves vacías NO afectan el success - solo son información const success = allErrors.length === 0; return { success, errors: allErrors, warnings: allWarnings, info: emptyKeysReport.emptyKeys, // Nueva sección para información metadata: { duration: Date.now() - startTime, rulesChecked: 1, rulesPassed: success ? 1 : 0, rulesFailed: success ? 0 : 1, filesCompared: files.length, totalKeys: masterKeyDictionary.size, ignoredKeys: ignoreKeys.length, requiredKeys: requiredKeys.length, emptyKeys: emptyKeysReport.emptyKeys.length // Metadata para estadísticas } }; } // Pasada 1: Recolectar todas las claves de todos los archivos (excluyendo ignoradas) collectAllKeys(files, ignoreKeys) { return new Set(files.flatMap(file => Array.from(this.extractAllKeys(file.content)) .filter(key => !this.isKeyIgnored(key, ignoreKeys)))); } // Pasada 2: Comparar diferencias - qué le falta a cada archivo compareDifferences(files, masterKeyDictionary, ignoreKeys) { const errors = files.flatMap(file => { const fileKeys = this.extractAllKeys(file.content); // Encontrar claves que faltan en este archivo (excluyendo ignoradas) const missingKeys = Array.from(masterKeyDictionary).filter(masterKey => !fileKeys.has(masterKey) && !this.isKeyIgnored(masterKey, ignoreKeys)); // Crear errores por cada clave faltante return missingKeys.map(missingKey => ({ code: 'MISSING_KEY', message: `Key '${missingKey}' is missing in ${file.path}`, severity: 'error', path: missingKey, context: { file: file.path, missingKey, availableKeys: Array.from(fileKeys) } })); }); return { errors, warnings: [] }; } extractAllKeys(obj, prefix = '') { const keys = new Set(); if (obj && typeof obj === 'object' && !Array.isArray(obj)) { for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; keys.add(fullKey); // Recursively extract nested keys if (value && typeof value === 'object' && !Array.isArray(value)) { const nestedKeys = this.extractAllKeys(value, fullKey); nestedKeys.forEach(nestedKey => keys.add(nestedKey)); } } } return keys; } // Verificar si una clave debe ser ignorada isKeyIgnored(key, ignoreKeys) { return ignoreKeys.some(ignoreKey => { // Soporte para patrones exactos y wildcards if (ignoreKey.includes('*')) { const pattern = ignoreKey.replace(/\*/g, '.*'); return new RegExp(`^${pattern}$`).test(key); } return key === ignoreKey || key.startsWith(ignoreKey + '.'); }); } // Validar claves requeridas validateRequiredKeys(files, requiredKeys) { const errors = requiredKeys.flatMap(requiredKey => files.flatMap(file => { const fileKeys = this.extractAllKeys(file.content); return !fileKeys.has(requiredKey) ? [{ code: 'REQUIRED_KEY_MISSING', message: `Required key '${requiredKey}' is missing in ${file.path}`, severity: 'error', path: requiredKey, context: { file: file.path, requiredKey, availableKeys: Array.from(fileKeys) } }] : []; })); return { errors, warnings: [] }; } // Detectar claves vacías (solo información, no afecta success) detectEmptyKeys(files, ignoreKeys) { const emptyKeys = []; files.forEach(file => { const emptyKeysInFile = this.findEmptyKeysInObject(file.content, '', file.path, ignoreKeys); emptyKeys.push(...emptyKeysInFile); }); return { emptyKeys }; } // Buscar claves vacías recursivamente en un objeto findEmptyKeysInObject(obj, prefix, filePath, ignoreKeys) { const emptyKeys = []; if (obj && typeof obj === 'object' && !Array.isArray(obj)) { for (const [key, value] of Object.entries(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key; // Verificar si la clave debe ser ignorada if (this.isKeyIgnored(fullKey, ignoreKeys)) { continue; } // Verificar si el valor está vacío if (this.isEmptyValue(value)) { emptyKeys.push({ code: 'EMPTY_KEY', message: `Key '${fullKey}' has empty value in ${filePath}`, severity: 'info', path: fullKey, context: { file: filePath, key: fullKey, value: value, valueType: typeof value } }); } // Recursivamente buscar en objetos anidados if (value && typeof value === 'object' && !Array.isArray(value)) { const nestedEmptyKeys = this.findEmptyKeysInObject(value, fullKey, filePath, ignoreKeys); emptyKeys.push(...nestedEmptyKeys); } } } return emptyKeys; } // Verificar si un valor está vacío isEmptyValue(value) { if (value === null || value === undefined) return true; if (typeof value === 'string' && value.trim() === '') return true; if (Array.isArray(value) && value.length === 0) return true; if (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) return true; return false; } } exports.EqualityRule = EqualityRule; //# sourceMappingURL=EqualityRule.js.map