import { createHash, timingSafeEqual } from 'node:crypto'; import { lstat, readFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { RELEASED_POLICY_EVALUATION_RULE_CATALOG, policyEvaluationRuleCatalogValidationErrors, type PolicyEvaluationRuleCatalog } from '../../src/domain/policy-evaluation-rules'; export const MAXIMUM_POLICY_RULE_CATALOG_BYTES = 5 * 1024 * 1024; export interface PolicyEvaluationRuleCatalogLoadOptions { path?: string; expectedSha256?: string; maximumBytes?: number; } function isObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } function hasValidationShape(value: unknown): value is PolicyEvaluationRuleCatalog { if (!isObject(value) || !isObject(value.catalog) || !Array.isArray(value.rules)) return false; return value.rules.every(rule => isObject(rule) && Array.isArray(rule.contexts) && rule.contexts.every(context => isObject(context) && Array.isArray(context.roles) && Array.isArray(context.scopes)) && Array.isArray(rule.dependencies) && rule.dependencies.every(isObject) && (rule.comparator === undefined || isObject(rule.comparator)) && (rule.riskRule === undefined || isObject(rule.riskRule))); } function normalizedSha256(value: string | undefined): string | undefined { const normalized = value?.trim().toLowerCase(); if (!normalized) return undefined; if (!/^[a-f0-9]{64}$/.test(normalized)) { throw new Error('ASSESSOR_POLICY_RULE_CATALOG_SHA256 muss ein SHA-256-Hexwert sein.'); } return normalized; } export async function loadPolicyEvaluationRuleCatalog( options: PolicyEvaluationRuleCatalogLoadOptions = {} ): Promise { const configuredPath = options.path?.trim(); const expectedSha256 = normalizedSha256(options.expectedSha256); if (!configuredPath && !expectedSha256) return structuredClone(RELEASED_POLICY_EVALUATION_RULE_CATALOG); if (!configuredPath || !expectedSha256) { throw new Error('Policy-Regelkatalogpfad und SHA-256 muessen gemeinsam konfiguriert werden.'); } const maximumBytes = options.maximumBytes ?? MAXIMUM_POLICY_RULE_CATALOG_BYTES; if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 1 || maximumBytes > MAXIMUM_POLICY_RULE_CATALOG_BYTES) { throw new Error('Die Groessengrenze des Policy-Regelkatalogs ist ungueltig.'); } const absolutePath = resolve(configuredPath); const information = await lstat(absolutePath); if (information.isSymbolicLink() || !information.isFile()) { throw new Error('Der Policy-Regelkatalog muss eine regulaere Datei sein.'); } if (information.size < 1 || information.size > maximumBytes) { throw new Error('Der Policy-Regelkatalog verletzt die konfigurierte Groessengrenze.'); } const bytes = await readFile(absolutePath); if (bytes.byteLength !== information.size || bytes.byteLength > maximumBytes) { throw new Error('Der Policy-Regelkatalog wurde waehrend des Lesens veraendert oder ist zu gross.'); } const actualSha256 = createHash('sha256').update(bytes).digest('hex'); if (!timingSafeEqual(Buffer.from(actualSha256, 'hex'), Buffer.from(expectedSha256, 'hex'))) { throw new Error('Der Policy-Regelkatalog stimmt nicht mit dem konfigurierten SHA-256 ueberein.'); } let parsed: unknown; try { const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes).replace(/^\uFEFF/, ''); parsed = JSON.parse(text); } catch { throw new Error('Der Policy-Regelkatalog ist kein gueltiges UTF-8-JSON-Dokument.'); } if (!hasValidationShape(parsed)) { throw new Error('Der Policy-Regelkatalog besitzt keine gueltige Struktur.'); } const errors = policyEvaluationRuleCatalogValidationErrors(parsed); if (errors.length) throw new Error(`Der Policy-Regelkatalog wurde abgelehnt: ${errors.join(' ')}`); return structuredClone(parsed); }