Files
sencho/backend/src/__tests__/securityPosture.test.ts
T
Anso 083442d5ea fix: differentiate security action links and add suppression editing (#1500)
Security page UX fixes:

- Stop the CVSS x EPSS scatter chart from painting a full-plot "white
  rectangle" cursor on click (cursor disabled), and prevent click-drag
  selection on charts.
- Differentiate the overview action links: "fixable" links (masthead primary
  action, review-queue blocker, and the Fixable signal tile) now open the
  Images tab pre-filtered to fixable findings; the Stale and Failed signal
  tiles link to the History tab where those scans are listed; Secrets and
  Misconfigs tiles link to their tabs. The Images tab accepts an initialFilter
  and exposes a Fixable option in the severity dropdown.
- Fix the "Secrets / misconfigs" option wrapping and misaligning in the
  severity dropdown (single-line option labels, wider trigger).
- Add Edit for CVE suppressions and misconfig acknowledgements (reason, scope
  pattern, expiry), reusing the existing dialog and the existing PUT endpoints;
  the CVE/rule identity stays fixed.
2026-06-28 06:17:47 -04:00

172 lines
7.1 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { deriveSecurityPosture, derivePostureReasons, type SecurityPostureFacts } from '../services/securityPosture';
function facts(o: Partial<SecurityPostureFacts> = {}): SecurityPostureFacts {
return {
scannerAvailable: true,
hasCompletedScan: true,
fixableCriticalHigh: 0,
secrets: 0,
dangerousCompose: 0,
knownExploited: 0,
publiclyExposed: 0,
exposedBlocker: 0,
exposedReview: 0,
rawCritical: 0,
rawHigh: 0,
staleScans: 0,
failedScans: 0,
needsReview: 0,
...o,
};
}
describe('deriveSecurityPosture', () => {
it('is Unknown when the scanner is unavailable', () => {
expect(deriveSecurityPosture(facts({ scannerAvailable: false, rawCritical: 9 }))).toBe('Unknown');
});
it('is Unknown when no scan has completed', () => {
expect(deriveSecurityPosture(facts({ hasCompletedScan: false, rawCritical: 9 }))).toBe('Unknown');
});
it('is Action needed when a Critical/High is fixable', () => {
expect(deriveSecurityPosture(facts({ fixableCriticalHigh: 1, rawCritical: 5, rawHigh: 5 }))).toBe('Action needed');
});
it('is Action needed for a detected secret', () => {
expect(deriveSecurityPosture(facts({ secrets: 1 }))).toBe('Action needed');
});
it('is Action needed for a dangerous Compose misconfiguration', () => {
expect(deriveSecurityPosture(facts({ dangerousCompose: 1 }))).toBe('Action needed');
});
it('is Action needed when a finding is known-exploited even if unfixable', () => {
expect(deriveSecurityPosture(facts({ knownExploited: 1, fixableCriticalHigh: 0, rawCritical: 1 }))).toBe('Action needed');
});
it('is Action needed when exposedBlocker > 0 (KEV, fixable, or elevated EPSS on a public interface)', () => {
expect(deriveSecurityPosture(facts({ exposedBlocker: 1 }))).toBe('Action needed');
});
it('is Monitoring when publiclyExposed > 0 but exposedBlocker is 0 (review-only exposure)', () => {
expect(deriveSecurityPosture(facts({ publiclyExposed: 3, exposedReview: 3, rawCritical: 2 }))).toBe('Monitoring');
});
it('is Monitoring when Critical/High exist but nothing is actionable', () => {
expect(deriveSecurityPosture(facts({ rawCritical: 3, rawHigh: 7 }))).toBe('Monitoring');
});
it('is Monitoring when only review/info reasons exist', () => {
expect(deriveSecurityPosture(facts({ exposedReview: 1, needsReview: 2, staleScans: 1 }))).toBe('Monitoring');
});
it('is Secure when a scan completed and nothing is actionable or severe', () => {
expect(deriveSecurityPosture(facts())).toBe('Secure');
});
});
describe('derivePostureReasons', () => {
it('returns an empty reason list and null primary action for a clean node', () => {
const { reasons, primaryAction } = derivePostureReasons(facts());
expect(reasons).toEqual([]);
expect(primaryAction).toBeNull();
});
it('returns a blocker reason for fixable findings', () => {
const { reasons } = derivePostureReasons(facts({ fixableCriticalHigh: 4 }));
expect(reasons).toContainEqual(expect.objectContaining({ kind: 'fixable_cve', count: 4, severity: 'blocker' }));
});
it('returns a blocker reason for known-exploited findings', () => {
const { reasons } = derivePostureReasons(facts({ knownExploited: 2 }));
expect(reasons).toContainEqual(expect.objectContaining({ kind: 'known_exploited', count: 2, severity: 'blocker' }));
});
it('returns a blocker reason for secrets', () => {
const { reasons } = derivePostureReasons(facts({ secrets: 3 }));
expect(reasons).toContainEqual(expect.objectContaining({ kind: 'secret', count: 3, severity: 'blocker' }));
});
it('returns a blocker reason for dangerous Compose misconfigs', () => {
const { reasons } = derivePostureReasons(facts({ dangerousCompose: 5 }));
expect(reasons).toContainEqual(expect.objectContaining({ kind: 'dangerous_compose', count: 5, severity: 'blocker' }));
});
it('returns a blocker reason for exposedBlocker', () => {
const { reasons } = derivePostureReasons(facts({ exposedBlocker: 1 }));
expect(reasons).toContainEqual(expect.objectContaining({ kind: 'public_exposure', count: 1, severity: 'blocker' }));
});
it('returns a review reason for exposedReview', () => {
const { reasons } = derivePostureReasons(facts({ exposedReview: 2 }));
expect(reasons).toContainEqual(expect.objectContaining({ kind: 'public_exposure', count: 2, severity: 'review' }));
});
it('returns a review reason for needsReview', () => {
const { reasons } = derivePostureReasons(facts({ needsReview: 7 }));
expect(reasons).toContainEqual(expect.objectContaining({ kind: 'needs_review', count: 7, severity: 'review' }));
});
it('returns info reasons for stale and failed scans', () => {
const { reasons } = derivePostureReasons(facts({ staleScans: 3, failedScans: 1 }));
expect(reasons).toContainEqual(expect.objectContaining({ kind: 'stale_scan', count: 3, severity: 'info' }));
expect(reasons).toContainEqual(expect.objectContaining({ kind: 'failed_scan', count: 1, severity: 'info' }));
});
it('returns ALL reasons regardless of posture state', () => {
// Even with no scanner (Unknown posture), the facts produce reasons.
const { reasons } = derivePostureReasons(facts({
scannerAvailable: false,
fixableCriticalHigh: 4,
staleScans: 1,
}));
expect(reasons).toHaveLength(2);
expect(reasons[0].kind).toBe('fixable_cve');
expect(reasons[1].kind).toBe('stale_scan');
});
it('sets primaryAction to the first blocker (fixable_cve priority)', () => {
const { primaryAction } = derivePostureReasons(facts({
fixableCriticalHigh: 3,
knownExploited: 1,
secrets: 2,
}));
expect(primaryAction).toEqual({ label: 'Update affected images', targetTab: 'images', kind: 'fixable_cve' });
});
it('falls through to the next blocker when the first is absent', () => {
const { primaryAction } = derivePostureReasons(facts({ secrets: 1 }));
expect(primaryAction).toEqual({ label: 'Review detected secrets', targetTab: 'secrets', kind: 'secret' });
});
it('returns null primaryAction when no blockers exist', () => {
const { primaryAction } = derivePostureReasons(facts({
exposedReview: 1, needsReview: 2, staleScans: 1, failedScans: 0,
}));
expect(primaryAction).toBeNull();
});
it('each blocker reason has a targetTab matching a valid Security tab', () => {
const validTabs = ['images', 'secrets', 'compose', 'history', 'suppressions', 'scanner'];
const { reasons } = derivePostureReasons(facts({
fixableCriticalHigh: 1, secrets: 1, dangerousCompose: 1,
knownExploited: 1, exposedBlocker: 1,
}));
for (const r of reasons) {
expect(validTabs).toContain(r.targetTab);
}
});
// Invariant: Action needed posture always has at least one blocker reason.
it('Action needed posture always has at least one blocker reason', () => {
const f = facts({ fixableCriticalHigh: 1 });
const posture = deriveSecurityPosture(f);
const { reasons } = derivePostureReasons(f);
if (posture === 'Action needed') {
expect(reasons.some((r) => r.severity === 'blocker')).toBe(true);
}
});
});