Assessor_2.0/src/domain/processing-compatibility.ts

107 lines
9.6 KiB
TypeScript

import type { ImportFileRecord, ProcessingVersion } from './canonical-model';
import type { ProcessingChangeClass, ToolProcessingContract } from './tool-profile-definition';
export type ProcessingReadinessStatus =
| 'current'
| 'compatible-older'
| 'reimport-recommended'
| 'incompatible'
| 'unknown';
/** Globaler, in der Oberflaeche sichtbarer Stand des ASSESSOR-Verarbeitungsvertrags. */
export const CURRENT_ASSESSOR_PROCESSING_VERSION = '1.4.0';
export interface ProcessingVersionManifest extends ToolProcessingContract {
profileId: string;
profileVersion: string;
current: boolean;
}
/**
* Historische und aktuelle Verarbeitungsvertraege. Diese Liste ist bewusst
* append-only: bereits persistierte Provenienz darf niemals still umgedeutet
* oder geloescht werden.
*/
export const PROCESSING_VERSION_MANIFESTS: readonly ProcessingVersionManifest[] = [
{ profileId: 'generic', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'generic-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: true },
{ profileId: 'defender-for-iot-vulnerabilities', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'defender-for-iot-vulnerabilities-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: false },
{ profileId: 'defender-for-iot-vulnerabilities', profileVersion: '1.1.0', mappingVersion: '1.0.0', normalizationVersion: '1.1.0', presentationVersion: '1.0.0', compatibilityKey: 'defender-for-iot-vulnerabilities-v2', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'resultType', 'deviation'], current: true },
{ profileId: 'purple-knight', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'purple-knight-canonical-v1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: false },
{ profileId: 'purple-knight', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: false },
{ profileId: 'purple-knight', profileVersion: '1.2.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: false },
{ profileId: 'purple-knight', profileVersion: '1.3.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context', 'evidence', 'import-diagnostics'], current: true },
{ profileId: 'pingcastle', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'pingcastle-canonical-v1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: false },
{ profileId: 'pingcastle', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'semantic-breaking', reimportRequired: false, affectedDimensions: ['outcome', 'isIoe'], current: false },
{ profileId: 'pingcastle', profileVersion: '1.2.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'presentation-only', reimportRequired: true, affectedDimensions: ['evidence', 'presentation'], current: false },
{ profileId: 'pingcastle', profileVersion: '1.3.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context', 'evidence', 'presentation'], current: false },
{ profileId: 'pingcastle', profileVersion: '1.4.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: false },
{ profileId: 'pingcastle', profileVersion: '1.5.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context', 'evidence', 'import-diagnostics'], current: true },
{ profileId: 'cis-cat', profileVersion: '1.2.0', mappingVersion: '1.2.0', normalizationVersion: '1.2.0', presentationVersion: '1.2.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['evidence'], current: false },
{ profileId: 'cis-cat', profileVersion: '1.3.0', mappingVersion: '1.3.0', normalizationVersion: '1.3.0', presentationVersion: '1.3.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: ['evidence', 'presentation'], current: false },
{ profileId: 'cis-cat', profileVersion: '1.4.0', mappingVersion: '1.3.0', normalizationVersion: '1.3.0', presentationVersion: '1.3.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: true },
{ profileId: 'maester', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'maester-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: false },
{ profileId: 'maester', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.0.0', presentationVersion: '1.1.0', compatibilityKey: 'maester-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['evidence', 'environment-context', 'presentation'], current: true }
] as const;
const manifestKey = (profileId: string, profileVersion: string) => `${profileId.trim().toLocaleLowerCase('en-US')}@${profileVersion.trim()}`;
const manifestLookup = new Map(PROCESSING_VERSION_MANIFESTS.map(item => [manifestKey(item.profileId, item.profileVersion), item]));
function recordedProfileId(profileId: string | undefined, processedWith: ProcessingVersion): string | undefined {
return processedWith.profileId?.trim() || profileId?.trim();
}
export function processingManifest(profileId: string | undefined, profileVersion: string | undefined): ProcessingVersionManifest | undefined {
if (!profileId || !profileVersion) return undefined;
return manifestLookup.get(manifestKey(profileId, profileVersion));
}
export function currentProcessingManifest(profileId: string | undefined): ProcessingVersionManifest | undefined {
if (!profileId) return undefined;
const normalized = profileId.trim().toLocaleLowerCase('en-US');
return PROCESSING_VERSION_MANIFESTS.find(item => item.current
&& item.profileId.toLocaleLowerCase('en-US') === normalized);
}
export function resolvedCompatibilityKey(profileId: string | undefined, processedWith: ProcessingVersion): string | undefined {
return processedWith.compatibilityKey
?? processingManifest(recordedProfileId(profileId, processedWith), processedWith.profileVersion)?.compatibilityKey;
}
export function processingReadiness(profileId: string | undefined, processedWith: ProcessingVersion): ProcessingReadinessStatus {
const effectiveProfileId = recordedProfileId(profileId, processedWith);
const manifest = processingManifest(effectiveProfileId, processedWith.profileVersion);
const current = currentProcessingManifest(effectiveProfileId);
if (!manifest || !current) return 'unknown';
const recordedKey = processedWith.compatibilityKey ?? manifest.compatibilityKey;
if (recordedKey !== current.compatibilityKey) return 'incompatible';
if (manifest.current) return 'current';
if (manifest.reimportRequired || current.reimportRequired) return 'reimport-recommended';
return 'compatible-older';
}
export interface ProcessingScopeIssue {
toolId: string;
code: 'unknown-processing-contract' | 'incompatible-processing-contracts';
compatibilityKeys: string[];
}
export function processingScopeIssues(files: ImportFileRecord[]): ProcessingScopeIssue[] {
const byTool = new Map<string, ImportFileRecord[]>();
files.forEach(file => {
const toolId = file.detectedTool.id || file.processedWith.profileId;
byTool.set(toolId, [...(byTool.get(toolId) ?? []), file]);
});
return [...byTool.entries()].flatMap(([toolId, entries]): ProcessingScopeIssue[] => {
const keys = entries.map(file => resolvedCompatibilityKey(toolId, file.processedWith));
if (keys.some(key => !key)) return [{ toolId, code: 'unknown-processing-contract', compatibilityKeys: [...new Set(keys.filter((key): key is string => Boolean(key)))] }];
const unique = [...new Set(keys as string[])];
return unique.length > 1 ? [{ toolId, code: 'incompatible-processing-contracts', compatibilityKeys: unique }] : [];
});
}
export function changeClassLabel(value: ProcessingChangeClass | undefined): string {
if (value === 'presentation-only') return 'Presentation only';
if (value === 'semantic-breaking') return 'Semantic change';
return value === 'compatible' ? 'Compatible' : 'Unknown';
}