import ConfigurationModel from '#models/configuration' import DeviceModel from '#models/device' import FirmwareCatalogService from './firmware_catalog_service.js' import type { Configuration, ParameterCriteria, ParameterValueType } from '../domain/configuration/configuration.js' import type { Device } from '../domain/device/device.js' import type { MigrationStep } from '../domain/firmware/migration_step.js' import { isWithinSchedule } from '../utils/schedule_window.js' import { matchGlob } from '../utils/glob_match.js' import { computeContentHash, parseProvisioningCode } from '../utils/provisioning_code.js' export interface ComplianceDeviceResult { identifier: string deviceId: any compliant: boolean parameterDiffs: ParameterDiff[] firmwareMigrationPath: MigrationStep[] } export interface ComplianceReport { configurationId: string configurationName: string totalDevices: number compliantCount: number nonCompliantCount: number devices: ComplianceDeviceResult[] } /** * Result of a single device configuration evaluation. * Contains all the actions to be executed by the state machine. */ export interface EngineResult { configuration: Configuration | null inventoryPaths: string[] parameterDiffs: ParameterDiff[] firmwareMigrationPath: MigrationStep[] isAlreadyCompliant: boolean skipFirmware: boolean // true if outside schedule window } /** * A single parameter difference (desired vs current). */ export interface ParameterDiff { name: string currentValue: string | number | boolean | null targetValue: string | number | boolean type: ParameterValueType } /** * Result of a dry-run evaluation. */ export interface DryRunResult { configurationId: string deviceIdentifier: string evaluation: EngineResult timestamp: Date } /** * ConfigurationEngine evaluates device compliance against configurations. * * Responsibilities: * - Find matching enabled configurations for a device * - Select the highest priority configuration * - Compute parameter differences (desired vs current) * - Handle policy "once" via ProvisioningCode comparison * - Resolve firmware migration paths * - Check schedule windows for firmware operations */ export default class ConfigurationEngine { private firmwareService: FirmwareCatalogService constructor() { this.firmwareService = new FirmwareCatalogService() } /** * Evaluate device compliance against all matching configurations. * * Steps: * 1. Load all enabled configurations * 2. Filter by criteria (glob matching against DeviceId) * 3. Select highest priority configuration * 4. Check if already compliant via ProvisioningCode (policy "once") * 5. Compute parameter differences * 6. Resolve firmware migration path * 7. Check schedule window for firmware operations */ async evaluate(device: Device): Promise { // 1. Load all enabled configurations const allConfigs = await ConfigurationModel.find({ enabled: true }) // 2. Filter by criteria (glob matching against DeviceId) const matchingConfigs = allConfigs.filter((config) => this.matchesCriteria(device, config)) // 3. Select highest priority configuration let selectedConfig: Configuration | null = null if (matchingConfigs.length > 0) { selectedConfig = matchingConfigs.reduce((prev, curr) => curr.priority > prev.priority ? curr : prev ) } // If no matching configuration, device is already compliant (nothing to do) if (!selectedConfig) { return { configuration: null, inventoryPaths: [], parameterDiffs: [], firmwareMigrationPath: [], isAlreadyCompliant: true, skipFirmware: false, } } // 4. Check if already compliant via ProvisioningCode (policy "once") if (!this.shouldApplyOnce(device, selectedConfig)) { return { configuration: selectedConfig, inventoryPaths: [], parameterDiffs: [], firmwareMigrationPath: [], isAlreadyCompliant: true, skipFirmware: false, } } // 5. Compute parameter differences const parameterDiffs = await this.computeParameterDiffs(device, selectedConfig) // 6. Resolve firmware migration path let firmwareMigrationPath: MigrationStep[] = [] let skipFirmware = false if (selectedConfig.firmwareTarget) { // Check schedule window for firmware operations if (!isWithinSchedule(selectedConfig.schedule)) { skipFirmware = true } else { // Get current device version (from properties or parameter) const currentVersion = this.getDeviceVersion(device, selectedConfig.firmwareTarget.productClass) if (currentVersion && currentVersion !== selectedConfig.firmwareTarget.targetVersion) { firmwareMigrationPath = await this.firmwareService.resolveUpgradePath( selectedConfig.firmwareTarget.productClass, currentVersion, selectedConfig.firmwareTarget.targetVersion ) } } } // Check if compliant (no diffs and no firmware needed) const isCompliant = parameterDiffs.length === 0 && firmwareMigrationPath.length === 0 return { configuration: selectedConfig, inventoryPaths: selectedConfig.inventory.map((inv) => inv.path), parameterDiffs, firmwareMigrationPath, isAlreadyCompliant: isCompliant, skipFirmware, } } /** * Dry-run evaluation for a specific configuration on a device. * Similar to evaluate() but can be called explicitly by the user. */ async dryRun(configId: string, deviceIdentifier: string): Promise { const configuration = await ConfigurationModel.findOne({ identifier: configId }) if (!configuration) { throw new Error(`Configuration ${configId} not found`) } const device = await DeviceModel.findOne({ $or: [{ identifier: deviceIdentifier }, { 'deviceId.id': deviceIdentifier }], }) if (!device) { throw new Error(`Device ${deviceIdentifier} not found`) } // Check if configuration matches device criteria if (!this.matchesCriteria(device, configuration)) { throw new Error(`Configuration ${configId} does not match device ${deviceIdentifier}`) } const evaluation = await this.evaluateSpecific(device, configuration) return { configurationId: configId, deviceIdentifier, evaluation, timestamp: new Date(), } } /** * Evaluate a specific configuration against a device. * Used by dry-run, compliance check, and potentially other flows. */ async evaluateSpecific(device: Device, config: Configuration): Promise { // Check policy "once" if (!this.shouldApplyOnce(device, config)) { return { configuration: config, inventoryPaths: [], parameterDiffs: [], firmwareMigrationPath: [], isAlreadyCompliant: true, skipFirmware: false, } } // Compute parameter differences const parameterDiffs = await this.computeParameterDiffs(device, config) // Resolve firmware migration path let firmwareMigrationPath: MigrationStep[] = [] let skipFirmware = false if (config.firmwareTarget) { if (!isWithinSchedule(config.schedule)) { skipFirmware = true } else { const currentVersion = this.getDeviceVersion(device, config.firmwareTarget.productClass) if (currentVersion && currentVersion !== config.firmwareTarget.targetVersion) { firmwareMigrationPath = await this.firmwareService.resolveUpgradePath( config.firmwareTarget.productClass, currentVersion, config.firmwareTarget.targetVersion ) } } } const isCompliant = parameterDiffs.length === 0 && firmwareMigrationPath.length === 0 return { configuration: config, inventoryPaths: config.inventory.map((inv) => inv.path), parameterDiffs, firmwareMigrationPath, isAlreadyCompliant: isCompliant, skipFirmware, } } /** * Check if a device matches all criteria of a configuration. * Uses glob matching for each criterion. */ private matchesCriteria(device: Device, config: Configuration): boolean { const criteria = config.criteria // manufacturer if (criteria.manufacturer) { const deviceValue = device.deviceId.manufacturer || '' if (!matchGlob(criteria.manufacturer, deviceValue)) { return false } } // oui if (criteria.oui) { const deviceValue = device.deviceId.oui || '' if (!matchGlob(criteria.oui, deviceValue)) { return false } } // productClass if (criteria.productClass) { const deviceValue = device.deviceId.productClass || '' if (!matchGlob(criteria.productClass, deviceValue)) { return false } } // serialNumber if (criteria.serialNumber) { const deviceValue = device.deviceId.serialNumber || '' if (!matchGlob(criteria.serialNumber, deviceValue)) { return false } } // parameterCriteria: all conditions must match a known device parameter value if (Array.isArray(criteria.parameterCriteria) && criteria.parameterCriteria.length > 0) { const hasGetters = typeof (device as any).getParameters === 'function' const allParams = hasGetters ? [...(device as any).getParameters(), ...(device as any).getProperties()] : [...((device as any).parameters ?? []), ...((device as any).properties ?? [])] for (const pc of criteria.parameterCriteria as ParameterCriteria[]) { if (!pc.name) continue const known = allParams.find((p: any) => p.name === pc.name) if (known === undefined || known.value === null || known.value === undefined) { return false } if (String(known.value) !== String(pc.value)) { return false } } } return true } /** * Determine if policy "once" should allow application of the configuration. * * - For policy "maintain": always apply * - For policy "once": apply if device doesn't have the provisioning code * (indicating content has changed or never applied) */ private shouldApplyOnce(device: Device, config: Configuration): boolean { if (config.policy === 'maintain') { return true } if (config.policy !== 'once') { return true } // For "once" policy, check if device has matching provisioning code const deviceProvCode = (device as any).provisioningCode if (!deviceProvCode) { return true // No provisioning code yet, apply it } // Parse the provisioning code const parsed = parseProvisioningCode(deviceProvCode) if (!parsed) { return true // Invalid format, re-apply } // Check if identifiers match and content hasn't changed if (parsed.identifier !== config.identifier) { return true // Different configuration, apply } const currentHash = computeContentHash(config) if (parsed.hash !== currentHash) { return true // Content changed, re-apply } // Same configuration, same content hash => already applied return false } /** * Compute parameter differences between desired and current state. */ private async computeParameterDiffs( device: Device, config: Configuration ): Promise { const diffs: ParameterDiff[] = [] for (const desired of config.desiredParameters) { // Get current value from device const currentValue = this.getDeviceParameterValue(device, desired.name) // Compare values if (currentValue !== desired.value) { diffs.push({ name: desired.name, currentValue, targetValue: desired.value, type: desired.type, }) } } return diffs } /** * Get the current value of a parameter from the device. * Returns null if not found. */ private getDeviceParameterValue(device: Device, paramName: string): string | number | boolean | null { // Try method-based access first (Device class instances) if (typeof (device as any).getParameter === 'function') { const param = (device as any).getParameter(paramName) if (param) { const value = param.value if (value === undefined || value === null) return null if (typeof value === 'boolean') return value if (typeof value === 'number') return value return String(value) } return null } // Fallback: plain DeviceRecord objects (parameters + properties arrays) const allEntries = [...((device as any).parameters ?? []), ...((device as any).properties ?? [])] const entry = allEntries.find((p: any) => p.name === paramName) if (!entry || entry.value === undefined || entry.value === null) return null if (typeof entry.value === 'boolean') return entry.value if (typeof entry.value === 'number') return entry.value return String(entry.value) } /** * Get the device's current firmware version. * Looks in device properties or parameters (e.g., "Device.DeviceInfo.SoftwareVersion") */ private getDeviceVersion(device: Device, productClass: string): string | null { const versionParams = [ 'Device.DeviceInfo.SoftwareVersion', 'InternetGatewayDevice.DeviceInfo.SoftwareVersion', `Device.${productClass}.FirmwareVersion`, ] for (const paramName of versionParams) { const value = this.getDeviceParameterValue(device, paramName) if (value && typeof value === 'string') { return value } } return null } /** * Generate a compliance report for a configuration. * Returns all devices matching the criteria with their compliance status. */ async complianceReport(configId: string): Promise { const configuration = await ConfigurationModel.findOne({ identifier: configId }) if (!configuration) { throw new Error(`Configuration ${configId} not found`) } // Load all devices const collection = DeviceModel.getCollection() const allDevices = (await collection.find({}).toArray()) as unknown as Device[] // Filter by criteria const matchingDevices = allDevices.filter((d) => this.matchesCriteria(d, configuration)) // Evaluate each matching device const devices: ComplianceDeviceResult[] = await Promise.all( matchingDevices.map(async (device) => { const evaluation = await this.evaluateSpecific(device, configuration) return { identifier: (device as any).identifier ?? (device as any).deviceId?.serialNumber ?? '?', deviceId: (device as any).deviceId, compliant: evaluation.isAlreadyCompliant, parameterDiffs: evaluation.parameterDiffs, firmwareMigrationPath: evaluation.firmwareMigrationPath, } }) ) return { configurationId: configId, configurationName: configuration.name, totalDevices: devices.length, compliantCount: devices.filter((d) => d.compliant).length, nonCompliantCount: devices.filter((d) => !d.compliant).length, devices, } } }