@flamesshield/rules-engine
Version:
Independent rules engine for security analysis of Firebase
328 lines • 13.2 kB
JavaScript
/**
* 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