Assessor_2.0/server/gpo/policy-evaluation-rule-cata...

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);
}