diff --git a/src/features/dashboard/DashboardPanel.tsx b/src/features/dashboard/DashboardPanel.tsx index 68193e7..bcce3d3 100644 --- a/src/features/dashboard/DashboardPanel.tsx +++ b/src/features/dashboard/DashboardPanel.tsx @@ -577,12 +577,23 @@ function observationFact(snapshot: EnvironmentSnapshot | undefined, property: st return observation ? factText(displayValue(observation)) : UNKNOWN_FACT; } +/** + * Parst einen Faktwert, ohne eine Null zu erfinden: leere Zeichenketten, + * ausdrueckliches null und Wahrheitswerte bleiben unbekannt statt zu 0 zu + * werden. `Number('')`, `Number(null)` und `Number(false)` ergeben sonst + * jeweils die endliche 0 und faelschten den em-dash zu einer "0". + */ +function finiteFactNumber(value: unknown): number | undefined { + const numeric = typeof value === 'number' ? value + : typeof value === 'string' && value.trim() !== '' ? Number(value) + : NaN; + return Number.isFinite(numeric) ? numeric : undefined; +} + function optionalObservationNumber(snapshot: EnvironmentSnapshot | undefined, property: string, label?: string, sectionKey?: string): number | undefined { const observation = observationFor(snapshot, property, label, sectionKey); if (!observation) return undefined; - const value = observation.normalizedValue ?? observation.originalValue; - const numeric = typeof value === 'number' ? value : Number(value); - return Number.isFinite(numeric) ? numeric : undefined; + return finiteFactNumber(observation.normalizedValue ?? observation.originalValue); } function numberFact(value: number | undefined): string { @@ -887,9 +898,7 @@ function dashboardFieldValue(record: FindingRecord, pattern: RegExp): string | u function dashboardObservationNumber(snapshot: EnvironmentSnapshot | undefined, property: string): number | undefined { const observation = snapshot?.observations.find(item => item.property === property && item.state === 'observed'); - const value = observation?.normalizedValue ?? observation?.originalValue; - const number = typeof value === 'number' ? value : Number(value); - return Number.isFinite(number) ? number : undefined; + return finiteFactNumber(observation?.normalizedValue ?? observation?.originalValue); } function dashboardEnvironmentTableNumber(snapshot: EnvironmentSnapshot | undefined, sectionKey: string, header: RegExp): number | undefined { @@ -899,8 +908,8 @@ function dashboardEnvironmentTableNumber(snapshot: EnvironmentSnapshot | undefin const column = table.headers.findIndex(value => header.test(value.trim())); if (column < 0) return []; return table.rows.flatMap(row => { - const numeric = Number(row[column]); - return Number.isFinite(numeric) ? [numeric] : []; + const numeric = finiteFactNumber(row[column]); + return numeric === undefined ? [] : [numeric]; }); })) ?? []; return values.length ? values.reduce((sum, value) => sum + value, 0) : undefined; diff --git a/tests/features/dashboard-environment-detail.test.ts b/tests/features/dashboard-environment-detail.test.ts index a14a9c7..925db2f 100644 --- a/tests/features/dashboard-environment-detail.test.ts +++ b/tests/features/dashboard-environment-detail.test.ts @@ -94,6 +94,35 @@ describe('client info environment detail', () => { expect(fact(dashboardEnvironmentDetail(snapshot).inventory, 'Trusts')).toBe('—'); }); + it('does not infer a zero for present numeric facts that are empty, null, or boolean', () => { + const blank = { + version: 1, projectId: 'project-1', scanRunIds: ['run-1'], generatedAt: '2026-08-25T00:00:00.000Z', + coverage: { observed: 4, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 }, + sections: [ + section('sectionUserInformation', 'User information', [{ + name: 'Account analysis', + headers: ['Nb User Accounts', 'Nb Enabled', 'Nb Disabled'], + rows: [['', '', '']] + }]) + ], + observations: [ + observation('benchmark.score', 'Benchmark score', '' as unknown as number), + observation('purple_knight.ad.ioes_found', 'IOEs found', null as unknown as number), + observation('purple_knight.ad.security_posture_score', 'security posture score', false) + ] + } satisfies EnvironmentSnapshot; + + const detail = dashboardEnvironmentDetail(blank); + + // A present observation whose value is an empty string, explicit null, or + // boolean must stay an em dash: Number('')/Number(null)/Number(false) are all 0. + expect(fact(detail.toolContext, 'CIS-CAT score')).toBe('—'); + expect(fact(detail.toolContext, 'Purple Knight IOEs')).toBe('—'); + expect(fact(detail.toolContext, 'Purple Knight posture')).toBe('—'); + // A table column whose cells are all blank must not sum to a fabricated 0. + expect(fact(detail.inventory, 'User accounts')).toBe('—'); + }); + it('reads the password policy and PKI counts from their own report tables', () => { const detail = dashboardEnvironmentDetail(snapshot);