Assessor_2.0/server/gpo/advanced-audit-parser.ts

196 lines
8.5 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 { parseCsvLexically } from '../../src/importers/csv-importer';
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 ADVANCED_AUDIT_PARSER = { id: 'gpo-advanced-audit-csv', version: '1.0.0' } as const;
export const ADVANCED_AUDIT_REDUCER = { id: 'gpo-advanced-audit-reducer', version: '1.0.0' } as const;
const maximumFileBytes = 4 * 1024 * 1024;
const maximumRows = 20_000;
const maximumColumns = 32;
const maximumCellCharacters = 4_096;
const guidPattern = /^\{?([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\}?$/i;
export interface AdvancedAuditInstruction {
version: 1;
subcategoryGuid: string;
settingCode: 0 | 1 | 2 | 3;
line: number;
}
export interface AdvancedAuditDocument {
version: 1;
parser: typeof ADVANCED_AUDIT_PARSER;
sourcePath: string;
sourceSha256: string;
scope: GpoPolicyScope;
status: 'complete' | 'partial' | 'invalid';
instructions: AdvancedAuditInstruction[];
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 decodeAuditCsv(bytes: Uint8Array): string {
if (bytes.byteLength > maximumFileBytes) {
parserError('gpo-advanced-audit-size-limit', 'Eine Advanced-Audit-Datei ü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-advanced-audit-encoding-rejected', 'Die Advanced-Audit-Datei ist nicht eindeutig als UTF-8 oder BOM-markiertes UTF-16 dekodierbar.');
}
}
function canonicalHeader(value: string): string {
return value.trim().toLocaleLowerCase('en-US').replace(/\s+/g, ' ');
}
function canonicalGuid(value: string): string | undefined {
const guid = guidPattern.exec(value.trim())?.[1]?.toLocaleLowerCase('en-US');
return guid && guid !== '00000000-0000-0000-0000-000000000000' ? guid : undefined;
}
export function parseAdvancedAuditCsv(
file: VerifiedZipFile,
scope: GpoPolicyScope,
resourceOverrides?: Partial<ImportResourcePolicy>
): AdvancedAuditDocument {
const text = decodeAuditCsv(file.bytes);
const policy = resolveImportResourcePolicy({
...resourceOverrides,
maximumDecodedCharacters: Math.min(resourceOverrides?.maximumDecodedCharacters ?? maximumFileBytes, maximumFileBytes)
});
validateDecodedTextResources(text, 'csv', policy);
const parsed = parseCsvLexically(text, ',');
const diagnostics = new Set<string>();
let invalid = parsed.unclosedQuote || parsed.rows.length === 0;
if (parsed.unclosedQuote) diagnostics.add('gpo-advanced-audit-csv-unclosed-quote');
if (!parsed.rows.length) diagnostics.add('gpo-advanced-audit-header-missing');
if (parsed.rows.length > maximumRows) parserError('gpo-advanced-audit-row-limit', 'Die Advanced-Audit-Datei überschreitet das Zeilenlimit.', { rows: parsed.rows.length });
const header = parsed.rows[0] ?? [];
if (header.length > maximumColumns) parserError('gpo-advanced-audit-column-limit', 'Die Advanced-Audit-Datei überschreitet das Spaltenlimit.', { columns: header.length });
const headerMap = new Map<string, number>();
header.forEach((value, index) => {
const key = canonicalHeader(value);
if (!key || headerMap.has(key)) { diagnostics.add('gpo-advanced-audit-header-invalid'); invalid = true; }
else headerMap.set(key, index);
});
const guidColumn = headerMap.get('subcategory guid');
const settingColumn = headerMap.get('setting value');
const targetColumn = headerMap.get('policy target');
if (guidColumn === undefined || settingColumn === undefined || targetColumn === undefined) {
diagnostics.add('gpo-advanced-audit-required-column-missing');
invalid = true;
}
const instructions: AdvancedAuditInstruction[] = [];
let partial = scope !== 'computer';
if (scope !== 'computer') diagnostics.add('gpo-advanced-audit-scope-unsupported');
if (!invalid) {
for (let rowIndex = 1; rowIndex < parsed.rows.length; rowIndex += 1) {
const row = parsed.rows[rowIndex];
if (row.length !== header.length || row.length > maximumColumns || row.some(cell => cell.length > maximumCellCharacters || cell.includes('\n') || cell.includes('\r'))) {
diagnostics.add('gpo-advanced-audit-row-invalid');
invalid = true;
continue;
}
const target = row[targetColumn!].trim().toLocaleLowerCase('en-US');
if (target !== 'system') {
diagnostics.add('gpo-advanced-audit-target-unsupported');
partial = true;
continue;
}
const subcategoryGuid = canonicalGuid(row[guidColumn!]);
const setting = row[settingColumn!].trim();
if (!subcategoryGuid || !/^[0-3]$/.test(setting)) {
diagnostics.add('gpo-advanced-audit-instruction-invalid');
invalid = true;
continue;
}
instructions.push({
version: 1,
subcategoryGuid,
settingCode: Number(setting) as 0 | 1 | 2 | 3,
line: parsed.rowStartLines[rowIndex]
});
}
}
return {
version: 1,
parser: ADVANCED_AUDIT_PARSER,
sourcePath: file.entry.relativePath,
sourceSha256: file.entry.sha256,
scope,
status: invalid ? 'invalid' : partial ? 'partial' : 'complete',
instructions: invalid ? [] : instructions,
diagnosticCodes: [...diagnostics].sort()
};
}
function auditValue(code: 0 | 1 | 2 | 3): GpoPolicyValue {
return { kind: 'audit-flags', code, success: Boolean(code & 1), failure: Boolean(code & 2) };
}
export function reduceAdvancedAuditPolicy(
document: AdvancedAuditDocument,
context: { backupInstanceId: string; sourceArtifactId: string; artifactId: string }
): GpoPolicyObservation[] {
if (document.status === 'invalid' || document.scope !== 'computer') return [];
const groups = new Map<string, AdvancedAuditInstruction[]>();
for (const instruction of document.instructions) {
groups.set(instruction.subcategoryGuid, [...(groups.get(instruction.subcategoryGuid) ?? []), instruction]);
}
return [...groups.values()].map(instructions => {
const first = instructions[0];
const distinctCodes = [...new Set(instructions.map(instruction => instruction.settingCode))].sort();
const conflicted = distinctCodes.length > 1;
const id = `gpo-policy-${createHash('sha256').update([
context.backupInstanceId,
context.artifactId,
document.sourceSha256,
first.subcategoryGuid,
ADVANCED_AUDIT_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: 'computer',
deliveryMechanism: 'advanced-audit-policy',
parser: document.parser,
reducer: ADVANCED_AUDIT_REDUCER,
identity: {
status: 'unresolved',
namespace: 'advanced-audit-policy',
section: 'system audit subcategory',
key: first.subcategoryGuid
},
state: conflicted ? 'conflicted' : 'configured',
values: distinctCodes.map(code => auditValue(code as 0 | 1 | 2 | 3)),
sourceLines: instructions.map(instruction => instruction.line),
effectiveState: 'unknown',
diagnosticCodes: conflicted ? ['gpo-advanced-audit-value-conflict'] : []
};
const errors = gpoPolicyObservationValidationErrors(observation);
if (errors.length) throw new Error(`Intern ungültige Advanced-Audit-Beobachtung: ${errors.join(' ')}`);
return observation;
});
}