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;
|
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 {
|
function optionalObservationNumber(snapshot: EnvironmentSnapshot | undefined, property: string, label?: string, sectionKey?: string): number | undefined {
|
||||||
const observation = observationFor(snapshot, property, label, sectionKey);
|
const observation = observationFor(snapshot, property, label, sectionKey);
|
||||||
if (!observation) return undefined;
|
if (!observation) return undefined;
|
||||||
const value = observation.normalizedValue ?? observation.originalValue;
|
return finiteFactNumber(observation.normalizedValue ?? observation.originalValue);
|
||||||
const numeric = typeof value === 'number' ? value : Number(value);
|
|
||||||
return Number.isFinite(numeric) ? numeric : undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function numberFact(value: number | undefined): string {
|
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 {
|
function dashboardObservationNumber(snapshot: EnvironmentSnapshot | undefined, property: string): number | undefined {
|
||||||
const observation = snapshot?.observations.find(item => item.property === property && item.state === 'observed');
|
const observation = snapshot?.observations.find(item => item.property === property && item.state === 'observed');
|
||||||
const value = observation?.normalizedValue ?? observation?.originalValue;
|
return finiteFactNumber(observation?.normalizedValue ?? observation?.originalValue);
|
||||||
const number = typeof value === 'number' ? value : Number(value);
|
|
||||||
return Number.isFinite(number) ? number : undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dashboardEnvironmentTableNumber(snapshot: EnvironmentSnapshot | undefined, sectionKey: string, header: RegExp): number | undefined {
|
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()));
|
const column = table.headers.findIndex(value => header.test(value.trim()));
|
||||||
if (column < 0) return [];
|
if (column < 0) return [];
|
||||||
return table.rows.flatMap(row => {
|
return table.rows.flatMap(row => {
|
||||||
const numeric = Number(row[column]);
|
const numeric = finiteFactNumber(row[column]);
|
||||||
return Number.isFinite(numeric) ? [numeric] : [];
|
return numeric === undefined ? [] : [numeric];
|
||||||
});
|
});
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
return values.length ? values.reduce((sum, value) => sum + value, 0) : undefined;
|
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('—');
|
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', () => {
|
it('reads the password policy and PKI counts from their own report tables', () => {
|
||||||
const detail = dashboardEnvironmentDetail(snapshot);
|
const detail = dashboardEnvironmentDetail(snapshot);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue