UNPKG

@flamesshield/rules-engine

Version:

Independent rules engine for security analysis of Firebase

328 lines 13.2 kB
/** * RuleValidator - Phase 1 Implementation * * Validates rules against EnrichedProjectInformation to ensure: * - Rules reference valid paths * - Rules use correct operators * * Following TDD approach with minimal implementation to pass initial tests. */ import get from 'lodash.get'; import { JSONPath } from 'jsonpath-plus'; import { PathDiscovery } from './path-discovery.js'; export class FactPathValidator { validateFactPaths(rules, facts) { return rules.map(rule => { const allConds = rule.conditions?.all || []; for (const cond of allConds) { if (typeof cond.fact === 'string' && cond.fact.includes('.')) { return { ruleId: rule.id, status: 'error', error: `Fact path ${cond.fact} does not exist in project data`, }; } if (typeof cond.fact === 'string' && cond.path) { const factObj = facts[cond.fact]; if (!factObj) { return { ruleId: rule.id, status: 'error', error: `Fact ${cond.fact} does not exist in project data`, }; } try { const found = JSONPath({ path: cond.path, json: factObj, wrap: false }); if (found === undefined || (Array.isArray(found) && found.length === 0)) { return { ruleId: rule.id, status: 'error', error: `Path ${cond.path} does not exist in fact ${cond.fact}`, }; } } catch (e) { return { ruleId: rule.id, status: 'error', error: `Invalid JSONPath ${cond.path} for fact ${cond.fact}`, }; } } } return { ruleId: rule.id, status: 'valid', }; }); } } export class RuleValidator { /** * Validates a rule against the given project information */ validateRule(rule, projectInfo) { const errors = []; const warnings = []; // Basic validation logic - check if rule has required properties if (!rule.id) { errors.push({ type: 'MISSING_REQUIRED_FIELD', message: 'Rule must have an id', rule: rule.id || 'unknown' }); } // Check for null conditions and event if (rule.conditions === null) { errors.push({ type: 'MISSING_REQUIRED_FIELD', message: 'Rule conditions cannot be null', rule: rule.id || 'unknown' }); } if (rule.event === null) { errors.push({ type: 'MISSING_REQUIRED_FIELD', message: 'Rule event cannot be null', rule: rule.id || 'unknown' }); } // --- FactPathValidator integration --- const factPathValidator = new FactPathValidator(); const factPathResults = factPathValidator.validateFactPaths([rule], projectInfo); for (const result of factPathResults) { if (result.status === 'error') { errors.push({ type: 'INVALID_PATH', // Use allowed type message: result.error || 'Invalid fact/path', // Ensure string rule: rule.id || 'unknown', }); } } // --- End integration --- // Validate json-rules-engine format: conditions.all[] or conditions.any[] if (rule.conditions) { const pathErrors = this.validateConditionsObject(rule.conditions, projectInfo, rule.id); errors.push(...pathErrors); const pathWarnings = this.checkDeprecatedPathsInConditions(rule.conditions, projectInfo); warnings.push(...pathWarnings); } // Legacy support: For string-based condition format if (rule.condition && typeof rule.condition === 'string') { const pathErrors = this.validateConditionPaths(rule.condition, projectInfo, rule.id); errors.push(...pathErrors); // Check for deprecated paths const pathWarnings = this.checkDeprecatedPaths(rule.condition, projectInfo); warnings.push(...pathWarnings); } return { isValid: errors.length === 0, errors, warnings }; } /** * Creates validation context from project information */ createValidationContext(projectInfo) { const availablePaths = new Set(); const typeMap = new Map(); const deprecatedPaths = new Set(); // Add paths from project structure this.addPathsFromObject('', projectInfo, availablePaths, typeMap); return { availablePaths, typeMap, deprecatedPaths }; } /** * Recursively adds paths from an object to the available paths set */ addPathsFromObject(prefix, obj, paths, typeMap, depth = 0) { console.log(`[RuleValidator] addPathsFromObject called - prefix: "${prefix}", depth: ${depth}`); // Prevent infinite recursion if (depth > 10 || !obj || typeof obj !== 'object') { console.log(`[RuleValidator] addPathsFromObject early return - depth: ${depth}, obj type: ${typeof obj}`); return; } try { for (const key in obj) { if (obj.hasOwnProperty(key)) { console.log(`[RuleValidator] Processing key: "${key}" at prefix: "${prefix}"`); const fullPath = prefix ? `${prefix}.${key}` : key; const value = obj[key]; console.log(`[RuleValidator] Adding path: "${fullPath}", type: ${typeof value}`); paths.add(fullPath); typeMap.set(fullPath, typeof value); // Recursively add nested paths for objects if (value && typeof value === 'object' && !Array.isArray(value)) { console.log(`[RuleValidator] Recursing into path: "${fullPath}"`); this.addPathsFromObject(fullPath, value, paths, typeMap, depth + 1); } } } console.log(`[RuleValidator] addPathsFromObject completed successfully for prefix: "${prefix}"`); } catch (error) { console.error(`[RuleValidator] ERROR in addPathsFromObject:`, error); console.error(`[RuleValidator] Error details - prefix: "${prefix}", depth: ${depth}`); throw error; // Re-throw to ensure the error is visible } } /** * Validates paths in a condition string */ validateConditionPaths(condition, projectInfo, ruleId) { const errors = []; // Use the same path extraction logic as in PathDiscovery for consistency const paths = PathDiscovery.extractPathsFromCondition(condition); for (const path of paths) { if (get(projectInfo, path) === undefined) { errors.push({ type: 'INVALID_PATH', message: `Path '${path}' does not resolve in project information at runtime`, path, rule: ruleId || 'unknown' }); } } return errors; } /** * Check for deprecated paths in condition string */ checkDeprecatedPaths(condition, projectInfo) { const warnings = []; const deprecatedPaths = this.getDeprecatedPaths(projectInfo); // Use the same path extraction logic for consistency const paths = PathDiscovery.extractPathsFromCondition(condition); for (const path of paths) { // Check if path is deprecated if (deprecatedPaths.has(path)) { warnings.push({ type: 'DEPRECATED_PATH', message: `Path '${path}' is deprecated. Consider using computed.${path} instead`, path: path }); } } return warnings; } /** * Validates json-rules-engine conditions object (with 'all' or 'any' arrays) */ validateConditionsObject(conditions, projectInfo, ruleId) { const errors = []; // Handle conditions.all array if (conditions.all && Array.isArray(conditions.all)) { for (const condition of conditions.all) { const conditionErrors = this.validateSingleCondition(condition, projectInfo, ruleId); errors.push(...conditionErrors); } } // Handle conditions.any array if (conditions.any && Array.isArray(conditions.any)) { for (const condition of conditions.any) { const conditionErrors = this.validateSingleCondition(condition, projectInfo, ruleId); errors.push(...conditionErrors); } } return errors; } /** * Validates a single condition object with fact, operator, value */ validateSingleCondition(condition, projectInfo, ruleId) { const errors = []; if (condition.fact) { // Use lodash.get to check if the fact path exists in enriched project info if (get(projectInfo, condition.fact) === undefined) { errors.push({ type: 'INVALID_PATH', message: `Fact path '${condition.fact}' does not resolve in project information at runtime`, path: condition.fact, rule: ruleId || 'unknown' }); } } return errors; } /** * Check for deprecated paths in conditions object */ checkDeprecatedPathsInConditions(conditions, projectInfo) { const warnings = []; const deprecatedPaths = this.getDeprecatedPaths(projectInfo); // Handle conditions.all array if (conditions.all && Array.isArray(conditions.all)) { for (const condition of conditions.all) { if (condition.fact && deprecatedPaths.has(condition.fact)) { warnings.push({ type: 'DEPRECATED_PATH', message: `Fact path '${condition.fact}' is deprecated. Consider using computed.${condition.fact} instead`, path: condition.fact }); } } } // Handle conditions.any array if (conditions.any && Array.isArray(conditions.any)) { for (const condition of conditions.any) { if (condition.fact && deprecatedPaths.has(condition.fact)) { warnings.push({ type: 'DEPRECATED_PATH', message: `Fact path '${condition.fact}' is deprecated. Consider using computed.${condition.fact} instead`, path: condition.fact }); } } } return warnings; } /** * Discovers all available paths from project structure * Phase 2: Public method for path discovery */ discoverPaths(projectInfo) { const context = this.createValidationContext(projectInfo); return context.availablePaths; } /** * Gets type information for all discovered paths * Phase 2: Public method for type mapping */ getPathTypeMap(projectInfo) { const context = this.createValidationContext(projectInfo); return context.typeMap; } /** * Identifies deprecated direct access paths * Phase 2: Public method for deprecated path detection */ getDeprecatedPaths(projectInfo) { const deprecatedPaths = new Set(); // These are direct access properties that should use computed paths instead const deprecatedDirectPaths = [ 'total_apps', 'total_services', 'unenforced_services', 'enforcement_coverage_ratio', 'apps_without_attestation_providers', 'auth_service_unenforced', 'functions_service_unenforced', 'dataconnect_service_unenforced', 'ailogic_service_unenforced', 'google_identity_ios_service_unenforced', 'maps_js_service_unenforced', 'places_api_service_unenforced' ]; for (const path of deprecatedDirectPaths) { if (projectInfo.hasOwnProperty(path)) { deprecatedPaths.add(path); } } return deprecatedPaths; } } //# sourceMappingURL=rule-validator.js.map