mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-08 09:54:26 +00:00
083442d5ea
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.
172 lines
7.1 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|