Assessor_2.0/server/gpo/security-template-parser.ts

202 lines
9.0 KiB
TypeScript

import { createHash } from 'node:crypto';
import type { GpoPolicyObservation, GpoPolicyValue } from '../../src/domain/gpo-policy';
import { gpoPolicyObservationValidationErrors } from '../../src/domain/gpo-policy';
import type { GpoPolicyScope } from '../../src/domain/gpo-ingestion';
import type { ImportResourcePolicy } from '../../src/importers/resource-policy';
import { resolveImportResourcePolicy, validateDecodedTextResources } from '../../src/importers/resource-policy';
import { ImportError } from '../../src/shared/errors';
import type { VerifiedZipFile } from './zip-inventory';
export const SECURITY_TEMPLATE_PARSER = { id: 'gpo-security-template-inf', version: '1.1.0' } as const;
export const SECURITY_TEMPLATE_REDUCER = { id: 'gpo-security-template-reducer', version: '1.0.0' } as const;
const supportedSections = new Set(['system access', 'kerberos policy', 'privilege rights', 'registry values']);
const metadataSections = new Set(['unicode', 'version']);
const maximumFileBytes = 4 * 1024 * 1024;
const maximumLines = 50_000;
const maximumFieldCharacters = 4_095;
export interface SecurityTemplateInstruction {
version: 1;
section: string;
key: string;
rawValue: string;
line: number;
}
export interface SecurityTemplateDocument {
version: 1;
parser: typeof SECURITY_TEMPLATE_PARSER;
sourcePath: string;
sourceSha256: string;
scope: GpoPolicyScope;
status: 'complete' | 'partial' | 'invalid';
instructions: SecurityTemplateInstruction[];
diagnosticCodes: string[];
}
function parserError(code: string, message: string, details: Record<string, string | number> = {}): never {
throw new ImportError(message, [{ severity: 'error', code, message, stage: 'format-decoding', details }]);
}
function decodeSecurityTemplate(bytes: Uint8Array): string {
if (bytes.byteLength > maximumFileBytes) {
parserError('gpo-security-template-size-limit', 'Ein Security Template überschreitet das Größenlimit.', { sizeBytes: bytes.byteLength });
}
let encoding = 'utf-8';
let content = bytes;
if (bytes[0] === 0xff && bytes[1] === 0xfe) { encoding = 'utf-16le'; content = bytes.subarray(2); }
else if (bytes[0] === 0xfe && bytes[1] === 0xff) { encoding = 'utf-16be'; content = bytes.subarray(2); }
else if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) content = bytes.subarray(3);
try {
return new TextDecoder(encoding, { fatal: true }).decode(content);
} catch {
parserError('gpo-security-template-encoding-rejected', 'Das Security Template ist nicht eindeutig als UTF-8 oder BOM-markiertes UTF-16 dekodierbar.');
}
}
export function parseSecurityTemplateInf(
file: VerifiedZipFile,
scope: GpoPolicyScope,
resourceOverrides?: Partial<ImportResourcePolicy>
): SecurityTemplateDocument {
const text = decodeSecurityTemplate(file.bytes);
const policy = resolveImportResourcePolicy({
...resourceOverrides,
maximumDecodedCharacters: Math.min(resourceOverrides?.maximumDecodedCharacters ?? maximumFileBytes, maximumFileBytes)
});
validateDecodedTextResources(text, 'inf', policy);
const lines = text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n');
if (lines.length > maximumLines) parserError('gpo-security-template-line-limit', 'Das Security Template überschreitet das Zeilenlimit.', { lines: lines.length });
const diagnostics = new Set<string>();
const instructions: SecurityTemplateInstruction[] = [];
let section: string | undefined;
let invalid = false;
for (let index = 0; index < lines.length; index += 1) {
const rawLine = lines[index];
const line = index + 1;
if (rawLine.length > maximumFieldCharacters) {
diagnostics.add('gpo-security-template-field-limit');
invalid = true;
continue;
}
const trimmed = rawLine.trim();
if (!trimmed || trimmed.startsWith(';')) continue;
const sectionMatch = /^\[([^\[\]]+)\]$/.exec(trimmed);
if (sectionMatch) {
section = sectionMatch[1].trim().toLocaleLowerCase('en-US');
if (!section || section.length > 256) { diagnostics.add('gpo-security-template-section-invalid'); invalid = true; }
else if (!supportedSections.has(section) && !metadataSections.has(section)) diagnostics.add('gpo-security-template-section-unsupported');
continue;
}
// A damaged section boundary makes the attribution of following entries unsafe.
if (trimmed.startsWith('[')) {
diagnostics.add('gpo-security-template-section-invalid');
invalid = true;
section = undefined;
continue;
}
if (!section) { diagnostics.add('gpo-security-template-entry-outside-section'); invalid = true; continue; }
// Other INF sections have their own grammars, e.g. comma-separated service entries.
if (metadataSections.has(section) || !supportedSections.has(section)) continue;
const separator = rawLine.indexOf('=');
if (separator < 1) { diagnostics.add('gpo-security-template-entry-invalid'); continue; }
const key = rawLine.slice(0, separator).trim();
const rawValue = rawLine.slice(separator + 1).trim();
if (!key || key.length > 1_024 || rawValue.length > maximumFieldCharacters) {
diagnostics.add('gpo-security-template-entry-invalid');
continue;
}
if (section === 'registry values' && !/^\s*\d+\s*,/.test(rawValue)) {
diagnostics.add('gpo-security-template-registry-value-invalid');
continue;
}
instructions.push({ version: 1, section, key, rawValue, line });
}
return {
version: 1,
parser: SECURITY_TEMPLATE_PARSER,
sourcePath: file.entry.relativePath,
sourceSha256: file.entry.sha256,
scope,
status: invalid ? 'invalid' : diagnostics.size ? 'partial' : 'complete',
instructions: invalid ? [] : instructions,
diagnosticCodes: [...diagnostics].sort()
};
}
function policyValue(instruction: SecurityTemplateInstruction): GpoPolicyValue | undefined {
if (instruction.section === 'privilege rights') {
return { kind: 'principal-set', principals: instruction.rawValue.split(',').map(value => value.trim()).filter(Boolean) };
}
if (instruction.section === 'registry values') {
const separator = instruction.rawValue.indexOf(',');
if (separator < 1) return undefined;
const typeCode = Number(instruction.rawValue.slice(0, separator).trim());
if (!Number.isSafeInteger(typeCode) || typeCode < 0) return undefined;
return { kind: 'registry', typeCode, rawData: instruction.rawValue.slice(separator + 1).trim() };
}
return { kind: 'scalar', raw: instruction.rawValue };
}
function canonicalValue(value: GpoPolicyValue): string {
if (value.kind === 'principal-set') return JSON.stringify({ ...value, principals: [...value.principals].sort() });
return JSON.stringify(value);
}
export function reduceSecurityTemplate(
document: SecurityTemplateDocument,
context: { backupInstanceId: string; sourceArtifactId: string; artifactId: string }
): GpoPolicyObservation[] {
if (document.status === 'invalid' || !['computer', 'user'].includes(document.scope)) return [];
const groups = new Map<string, SecurityTemplateInstruction[]>();
for (const instruction of document.instructions) {
const key = `${instruction.section}\0${instruction.key.toLocaleLowerCase('en-US')}`;
groups.set(key, [...(groups.get(key) ?? []), instruction]);
}
return [...groups.values()].map(instructions => {
const values = instructions.map(policyValue);
if (values.some(value => value === undefined)) return undefined;
const typedValues = values as GpoPolicyValue[];
const distinctValues = [...new Map(typedValues.map(value => [canonicalValue(value), value])).values()];
const first = instructions[0];
const conflicted = distinctValues.length > 1;
const id = `gpo-policy-${createHash('sha256').update([
context.backupInstanceId,
context.artifactId,
document.sourceSha256,
first.section,
first.key.toLocaleLowerCase('en-US'),
SECURITY_TEMPLATE_REDUCER.version
].join('\0')).digest('hex').slice(0, 32)}`;
const observation: GpoPolicyObservation = {
version: 1,
id,
backupInstanceId: context.backupInstanceId,
sourceArtifactId: context.sourceArtifactId,
artifactId: context.artifactId,
sourcePath: document.sourcePath,
sourceSha256: document.sourceSha256,
scope: document.scope as 'computer' | 'user',
deliveryMechanism: 'security-template',
parser: document.parser,
reducer: SECURITY_TEMPLATE_REDUCER,
identity: {
status: 'unresolved',
namespace: 'security-template',
section: first.section,
key: first.key
},
state: conflicted ? 'conflicted' : 'configured',
values: distinctValues,
sourceLines: instructions.map(instruction => instruction.line),
effectiveState: 'unknown',
diagnosticCodes: conflicted ? ['gpo-security-template-value-conflict'] : []
};
const errors = gpoPolicyObservationValidationErrors(observation);
if (errors.length) throw new Error(`Intern ungültige Security-Template-Beobachtung: ${errors.join(' ')}`);
return observation;
}).filter((observation): observation is GpoPolicyObservation => Boolean(observation));
}