202 lines
9.0 KiB
TypeScript
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));
|
|
}
|
|
|