fix: preserve contracts after Michael rebase

This commit is contained in:
Binh 2026-09-21 10:05:09 +02:00
parent 6600198312
commit dbe244b32f
6 changed files with 30 additions and 21 deletions

View File

@ -1,4 +1,4 @@
import { memo, useEffect, useRef } from 'react'; import { memo } from 'react';
import type { FindingRecord } from '../../domain/finding'; import type { FindingRecord } from '../../domain/finding';
import { normalizeSeverity } from '../../profiles/purple-knight/presentation'; import { normalizeSeverity } from '../../profiles/purple-knight/presentation';
import { toolIdForRecord } from '../../importers/status-normalization'; import { toolIdForRecord } from '../../importers/status-normalization';
@ -27,15 +27,8 @@ function FindingCardComponent({ record, title, bookmarked, selected, toolId, sea
const outcomeMark = findingOutcomeMark(record, toolId); const outcomeMark = findingOutcomeMark(record, toolId);
const resolvedToolId = toolIdForRecord(record, toolId); const resolvedToolId = toolIdForRecord(record, toolId);
const diagnostics = policyDiagnosticBadges(record, resolvedToolId); const diagnostics = policyDiagnosticBadges(record, resolvedToolId);
const cardRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!selected) return;
cardRef.current?.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' });
}, [selected]);
return ( return (
<button <button
ref={cardRef}
aria-current={selected ? 'true' : undefined} aria-current={selected ? 'true' : undefined}
className={`record-card severity-${severity}${bookmarked ? ' is-workspace' : ''}${selected ? ' active' : ''}`} className={`record-card severity-${severity}${bookmarked ? ' is-workspace' : ''}${selected ? ' active' : ''}`}
type="button" type="button"

View File

@ -133,12 +133,8 @@ export function DatasetViewer() {
left: Math.max(0, Math.min(requestedClusterPos.left, maxClusterLeft)), left: Math.max(0, Math.min(requestedClusterPos.left, maxClusterLeft)),
top: Math.max(minClusterTop, Math.min(requestedClusterPos.top, maxClusterTop)) top: Math.max(minClusterTop, Math.min(requestedClusterPos.top, maxClusterTop))
}; };
const recordTitles = useMemo(() => { const recordTitleCache = useMemo(() => {
if (!activeSheet) return new Map<string, string>(); return new WeakMap<FindingRecord, string>();
return new Map<string, string>(activeSheet.analysis.records.map(record => [
record.id,
recordTitleFor(record, titleColumn, activeSheet.toolId ?? dataset?.profileId)
]));
}, [activeSheet, dataset?.profileId, titleColumn]); }, [activeSheet, dataset?.profileId, titleColumn]);
const resultColorsByRecord = useMemo(() => new Map(resultWindows.map(item => [item.recordId, item.color])), [resultWindows]); const resultColorsByRecord = useMemo(() => new Map(resultWindows.map(item => [item.recordId, item.color])), [resultWindows]);
@ -236,7 +232,13 @@ export function DatasetViewer() {
if (!dataset || !activeSheet) return <EmptyWorkspace />; if (!dataset || !activeSheet) return <EmptyWorkspace />;
const recordTitle = (record: FindingRecord) => recordTitles.get(record.id) ?? record.title; const recordTitle = (record: FindingRecord) => {
const cached = recordTitleCache.get(record);
if (cached !== undefined) return cached;
const title = recordTitleFor(record, titleColumn, activeSheet.toolId ?? dataset.profileId);
recordTitleCache.set(record, title);
return title;
};
const selected = records.find(record => record.id === selectedRecordId) ?? records[0]; const selected = records.find(record => record.id === selectedRecordId) ?? records[0];
const toolId = activeSheet.toolId ?? dataset.profileId; const toolId = activeSheet.toolId ?? dataset.profileId;
const linksFor = (record: FindingRecord) => recordResultSheets(record, dataset.sheets, activeSheet.id, toolId); const linksFor = (record: FindingRecord) => recordResultSheets(record, dataset.sheets, activeSheet.id, toolId);

View File

@ -197,7 +197,7 @@ export function ResultRow({ item, expanded, onToggle, onUpdated }: {
item: UnifiedResultProjection; item: UnifiedResultProjection;
expanded: boolean; expanded: boolean;
onToggle(): void; onToggle(): void;
onUpdated(): Promise<void>; onUpdated?(): Promise<void>;
}) { }) {
const environment = item.sourceKind === 'environment-observation'; const environment = item.sourceKind === 'environment-observation';
const policyObservation = item.sourceKind === 'policy-observation'; const policyObservation = item.sourceKind === 'policy-observation';
@ -213,7 +213,7 @@ export function ResultRow({ item, expanded, onToggle, onUpdated }: {
setEnrichmentError(null); setEnrichmentError(null);
try { try {
await assessorApi.enrichCve(cveId); await assessorApi.enrichCve(cveId);
await onUpdated(); await onUpdated?.();
} catch (reason) { } catch (reason) {
setEnrichmentError(reason instanceof Error ? reason.message : 'CVE enrichment failed.'); setEnrichmentError(reason instanceof Error ? reason.message : 'CVE enrichment failed.');
} finally { } finally {

View File

@ -4,6 +4,22 @@ import { commonSeverityMappings, commonStatusMappings } from '../common-normaliz
export const defenderForIotDefinition: ToolProfileDefinition = { export const defenderForIotDefinition: ToolProfileDefinition = {
schemaVersion: 1, schemaVersion: 1,
id: 'defender-for-iot-vulnerabilities', id: 'defender-for-iot-vulnerabilities',
securityScoring: {
version: '1.0.0',
native: { kind: 'unavailable', reason: 'Affected-device counts describe vulnerability reach, not a native security score.' },
targets: [], resultPopulation: 'findings-only'
},
priorityBasis: {
version: '1.0.0',
basis: { kind: 'severity', properties: ['vulnerability.severity'] },
excludedProperties: [
{ property: 'affected-device-count', reason: 'Vulnerability reach is not a criticality.' }
]
},
timeslotComparison: {
version: '1.0.0', kind: 'unavailable',
reason: 'The aggregated CVE export does not establish complete negative evidence for vulnerability remediation.'
},
name: 'Microsoft Defender for IoT', name: 'Microsoft Defender for IoT',
version: '1.2.0', version: '1.2.0',
processing: { processing: {

View File

@ -107,6 +107,7 @@ export function createImportProfile(definition: ToolProfileDefinition): ImportPr
} }
if (status === 'no-baseline-rule') return actualStatus === 'no baseline rule'; if (status === 'no-baseline-rule') return actualStatus === 'no baseline rule';
if (status === 'not-comparable') return actualStatus === 'not comparable'; if (status === 'not-comparable') return actualStatus === 'not comparable';
if (status === 'deviation') return isDeviationRecord(record, definition.id);
const filter = definition.presentation.statusFilters.find(item => item.value === status); const filter = definition.presentation.statusFilters.find(item => item.value === status);
const statusHeaders = definition.presentation.statusHeaders; const statusHeaders = definition.presentation.statusHeaders;
const preferredStatus = statusHeaders?.map(pattern => record.fields.find(field => field.value.trim() && matches(pattern, field.label.trim()))) const preferredStatus = statusHeaders?.map(pattern => record.fields.find(field => field.value.trim() && matches(pattern, field.label.trim())))
@ -114,9 +115,6 @@ export function createImportProfile(definition: ToolProfileDefinition): ImportPr
const values = statusHeaders?.length const values = statusHeaders?.length
? preferredStatus ? [plainTextValue(preferredStatus.value).trim().toLowerCase()] : [] ? preferredStatus ? [plainTextValue(preferredStatus.value).trim().toLowerCase()] : []
: normalizedFieldValues(record); : normalizedFieldValues(record);
if (status === 'deviation' && !filter) {
return values.some(value => UNIVERSAL_DEVIATION_PATTERNS.some(pattern => matches(pattern, value)));
}
if (!filter) return true; if (!filter) return true;
return values.some(value => filter.patterns.some(pattern => matches(pattern, value))); return values.some(value => filter.patterns.some(pattern => matches(pattern, value)));
}; };

View File

@ -87,7 +87,7 @@ describe('Datahub client processing readiness', () => {
// A display tool label cannot turn a generic processor into a native one. // A display tool label cannot turn a generic processor into a native one.
expect(summarizeScanProcessingReadiness([{ ...companion, detectedTool: { id: 'purple-knight', name: 'PurpleKnight' } }]).state).toBe('review'); expect(summarizeScanProcessingReadiness([{ ...companion, detectedTool: { id: 'purple-knight', name: 'PurpleKnight' } }]).state).toBe('review');
expect(summarizeScanProcessingReadiness([currentPurpleKnight('run'), companion, expect(summarizeScanProcessingReadiness([currentPurpleKnight('run'), companion,
file('run', 'pingcastle', '1.5.0', 'pingcastle-canonical-v1.1')]).state).toBe('review'); file('run', 'pingcastle', '1.6.0', 'pingcastle-canonical-v1.1')]).state).toBe('review');
}); });
it('marks GPO failure and recovery red, including clients with healthy scanner reports', () => { it('marks GPO failure and recovery red, including clients with healthy scanner reports', () => {