fix: prevent incorrect zero inference for empty, null, or boolean numeric facts
This commit is contained in:
parent
3f6259db30
commit
78ea9a616d
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue