import { createHash } from 'node:crypto'; import type { GpoArtifactInventory } from '../../src/domain/gpo-ingestion'; import { gpoArtifactInventoryValidationErrors } from '../../src/domain/gpo-ingestion'; import { classifyGpoArtifactPath, isGpoSysvolPath } from '../../src/gpo/artifact-classifier'; import type { ImportResourcePolicy } from '../../src/importers/resource-policy'; import { inventoryZipArchive, type SafeZipInventory } from './zip-inventory'; export type GpoContainerRecognition = | { status: 'gpo-backup-candidate'; evidenceCodes: string[] } | { status: 'unrecognized-zip'; evidenceCodes: string[] }; export interface GpoContainerInventoryResult { version: 1; recognition: GpoContainerRecognition; archive: SafeZipInventory; inventory: GpoArtifactInventory; } export interface GpoContainerRecognitionResult { version: 1; recognition: GpoContainerRecognition; } function artifactId(containerHash: string, path: string, contentHash: string): string { return `gpo-entry-${createHash('sha256').update(`${containerHash}\0${path}\0${contentHash}`).digest('hex').slice(0, 32)}`; } function recognizeFiles(files: SafeZipInventory['entries']): GpoContainerRecognition { const metadataRoots = files .filter(entry => entry.kind === 'file' && classifyGpoArtifactPath(entry.relativePath).role === 'backup-metadata') .map(entry => entry.relativePath.includes('/') ? entry.relativePath.slice(0, entry.relativePath.lastIndexOf('/')) : ''); const hasMetadata = metadataRoots.length > 0; const hasSysvolStructure = files.some(entry => entry.kind === 'file' && isGpoSysvolPath(entry.relativePath)); const recognized = metadataRoots.some(root => files.some(entry => entry.kind === 'file' && isGpoSysvolPath(entry.relativePath) && (!root || entry.relativePath.startsWith(root + '/')) )); const evidenceCodes = [ ...(hasMetadata ? ['gpo-backup-metadata-path-observed'] : []), ...(hasSysvolStructure ? ['gpo-domain-sysvol-structure-observed'] : []), ...(recognized ? [] : ['gpo-container-structure-unconfirmed']) ].sort(); return recognized ? { status: 'gpo-backup-candidate', evidenceCodes } : { status: 'unrecognized-zip', evidenceCodes }; } export function recognizeGpoContainer( bytes: Uint8Array, resourcePolicy?: Partial ): GpoContainerRecognitionResult { const archive = inventoryZipArchive(bytes, resourcePolicy); return { version: 1, recognition: recognizeFiles(archive.entries) }; } export function inventoryGpoContainer( bytes: Uint8Array, resourcePolicy?: Partial ): GpoContainerInventoryResult { const archive = inventoryZipArchive(bytes, resourcePolicy); const files = archive.entries.filter(entry => entry.kind === 'file'); const classified = files.map(entry => ({ entry, classification: classifyGpoArtifactPath(entry.relativePath) })); const recognition = recognizeFiles(archive.entries); const evidenceCodes = recognition.evidenceCodes; const diagnosticCodes = new Set(evidenceCodes); const entries = classified.map(({ entry, classification }) => { const disposition = classification.role === 'unknown' ? 'unsupported' as const : 'inventory-only' as const; if (classification.role === 'unknown') diagnosticCodes.add('gpo-artifact-unsupported'); else diagnosticCodes.add('gpo-artifact-parser-pending'); if (classification.role === 'script') diagnosticCodes.add('gpo-script-inventory-only'); return { version: 1 as const, artifactId: artifactId(archive.containerSha256, entry.relativePath, entry.sha256 ?? ''), relativePath: entry.relativePath, role: classification.role, scope: classification.scope, disposition, secretIndicatorStatus: 'not-inspected' as const }; }); const inventory: GpoArtifactInventory = { version: 1, status: 'complete', entries, diagnosticCodes: [...diagnosticCodes].sort() }; const validationErrors = gpoArtifactInventoryValidationErrors(inventory); if (validationErrors.length) throw new Error(`Intern ungültiges GPO-Artefaktinventar: ${validationErrors.join(' ')}`); return { version: 1, recognition, archive, inventory }; }