195 lines
8.5 KiB
TypeScript
195 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,
|
|
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;
|
|
});
|
|
}
|