Assessor_2.0/server/gpo/gpo-container-inventory.ts

96 lines
4.1 KiB
TypeScript

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<ImportResourcePolicy>
): GpoContainerRecognitionResult {
const archive = inventoryZipArchive(bytes, resourcePolicy);
return { version: 1, recognition: recognizeFiles(archive.entries) };
}
export function inventoryGpoContainer(
bytes: Uint8Array,
resourcePolicy?: Partial<ImportResourcePolicy>
): 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
};
}