88 lines
3.8 KiB
TypeScript
88 lines
3.8 KiB
TypeScript
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<string, unknown> {
|
|
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<PolicyEvaluationRuleCatalog> {
|
|
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);
|
|
}
|