fix(security): upgrade frontend test tooling past new advisories

Use patched Vitest/coverage 4.1.11 and js-yaml 4.3.2. Adapt constructor mocks, callable mock types and explicit call-history cleanup for Vitest 4 without weakening assertions or audit gates.

Change-source: pulse-maintainer
(cherry picked from commit 15000f0a67)
This commit is contained in:
pulse-triage[bot]
2026-09-08 23:29:10 +01:00
parent dc923c0b91
commit 075a5625be
11 changed files with 224 additions and 674 deletions
@@ -3230,6 +3230,18 @@ stages pin the Docker Official Images Linux amd64 manifest
the checked-in toolchain files and release-script guards must reject an older
compiler so local, exact-candidate, provider control-plane, and container builds
cannot silently reintroduce the vulnerable runtime.
The frontend development-tool dependency boundary must retain patched Vitest
and mocker versions at or above `4.1.11`, with the V8 coverage provider matched
to the runner, and a `js-yaml` override at or above `4.3.2`. These floors prevent
reintroducing redirect-mock file reads (GHSA-82fw-gwwq-j7x9) and empty-merge CPU
exhaustion (GHSA-2883-xcg3-v3hh) through the locked development graph. The
`frontend-modern/src/security/__tests__/dependencySecurity.test.ts` proof must
check manifest floors and all installed lockfile copies, including nested
copies; a successful production build alone does not establish this property.
Vitest migration must preserve test assertions while using constructable
constructor mocks and explicit standalone mock-history cleanup. Dependency
updates must retain the required full-graph audit rather than suppressing it.
That same dev-runtime dependency-manifest boundary now also owns the maintained
Docker engine module floor. `go.mod`, `go.sum`, and
`internal/cloudcp/docker/manager.go` must route hosted runtime orchestration
+155 -649
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -65,7 +65,7 @@
"seroval": "^1.4.1",
"seroval-plugins": "^1.4.1",
"@babel/core": "^7.29.6",
"js-yaml": "^4.2.0"
"js-yaml": "^4.3.2"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
@@ -76,7 +76,7 @@
"@types/qrcode": "^1.5.6",
"@typescript-eslint/eslint-plugin": "^8.24.0",
"@typescript-eslint/parser": "^8.24.0",
"@vitest/coverage-v8": "^3.2.6",
"@vitest/coverage-v8": "^4.1.11",
"autoprefixer": "^10.4.0",
"eslint": "^9.20.0",
"eslint-config-prettier": "^10.0.0",
@@ -92,6 +92,6 @@
"vite": "^6.4.3",
"vite-plugin-solid": "^2.8.0",
"vite-plugin-sri-gen": "^1.3.2",
"vitest": "^3.2.6"
"vitest": "^4.1.11"
}
}
@@ -79,8 +79,8 @@ const mockProviders = [gmailProvider, sendgridProvider, outlookProvider];
// --- Tests ---
describe('EmailProviderSelect', () => {
let onChangeMock: ReturnType<typeof vi.fn>;
let onTestMock: ReturnType<typeof vi.fn>;
let onChangeMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
let onTestMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
beforeEach(() => {
onChangeMock = vi.fn();
@@ -168,17 +168,17 @@ interface DefaultProps {
title: string;
resources: Resource[];
columns: string[];
onEdit: ReturnType<typeof vi.fn>;
onSaveEdit: ReturnType<typeof vi.fn>;
onCancelEdit: ReturnType<typeof vi.fn>;
onRemoveOverride: ReturnType<typeof vi.fn>;
onEdit: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
onSaveEdit: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
onCancelEdit: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
onRemoveOverride: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
editingId: () => string | null;
editingThresholds: () => Record<string, number | undefined>;
setEditingThresholds: ReturnType<typeof vi.fn>;
formatMetricValue: ReturnType<typeof vi.fn>;
hasActiveAlert: ReturnType<typeof vi.fn>;
setEditingThresholds: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
formatMetricValue: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
hasActiveAlert: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
editingNote: () => string;
setEditingNote: ReturnType<typeof vi.fn>;
setEditingNote: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
}
function makeProps(
@@ -102,10 +102,10 @@ const mockTemplates: WebhookTemplate[] = [
// --- Tests ---
describe('WebhookConfig', () => {
let onAddMock: ReturnType<typeof vi.fn>;
let onUpdateMock: ReturnType<typeof vi.fn>;
let onDeleteMock: ReturnType<typeof vi.fn>;
let onTestMock: ReturnType<typeof vi.fn>;
let onAddMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
let onUpdateMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
let onDeleteMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
let onTestMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
beforeEach(() => {
onAddMock = vi.fn();
@@ -65,12 +65,12 @@ const currentExportDate = (): string => new Date().toISOString().split('T')[0];
describe('useDiagnosticsPanelState', () => {
let useDiagnosticsPanelState: UseDiagnosticsPanelStateModule['useDiagnosticsPanelState'];
let apiFetchJSONMock: ReturnType<typeof vi.fn>;
let showErrorMock: ReturnType<typeof vi.fn>;
let showSuccessMock: ReturnType<typeof vi.fn>;
let createObjectURLMock: ReturnType<typeof vi.fn>;
let revokeObjectURLMock: ReturnType<typeof vi.fn>;
let anchorClickMock: ReturnType<typeof vi.fn>;
let apiFetchJSONMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
let showErrorMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
let showSuccessMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
let createObjectURLMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
let revokeObjectURLMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
let anchorClickMock: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
let createdAnchor: HTMLAnchorElement | null;
let originalCreateObjectURL: typeof URL.createObjectURL | undefined;
let originalRevokeObjectURL: typeof URL.revokeObjectURL | undefined;
@@ -84,6 +84,8 @@ function getTagDotRing(dot: Element) {
afterEach(() => {
cleanup();
vi.restoreAllMocks();
// Vitest 4 restores spies without clearing standalone vi.fn call history.
vi.clearAllMocks();
// Reset dark mode to default (false) for isolation
darkModeMock.mockReturnValue(false);
});
@@ -5,6 +5,8 @@ import { describe, expect, it } from 'vitest';
interface PackageManifest {
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
overrides: Record<string, string>;
}
interface PackageLock {
@@ -58,6 +60,34 @@ const nanoidIsPatched = (version: string): boolean => {
};
describe('frontend dependency security floors', () => {
it('keeps Vitest and its mocker above the redirect-mock file-read floor', () => {
// GHSA-82fw-gwwq-j7x9: the maintained 4.x fix starts at 4.1.11.
expect(manifest.devDependencies.vitest).toBe('^4.1.11');
expect(manifest.devDependencies['@vitest/coverage-v8']).toBe('^4.1.11');
const runner = lockedVersions('vitest');
expect(runner).toHaveLength(1);
for (const name of ['vitest', '@vitest/mocker', '@vitest/coverage-v8']) {
const versions = lockedVersions(name);
expect(versions).not.toHaveLength(0);
for (const version of versions) {
expect(version).not.toContain('-');
expect(atLeast(version, [4, 1, 11]), `${name} ${version} is vulnerable`).toBe(true);
expect(version, `${name} must match the runner`).toBe(runner[0]);
}
}
});
it('keeps every js-yaml copy above the empty-merge CPU exhaustion floor', () => {
// GHSA-2883-xcg3-v3hh: our override keeps every transitive copy on patched 4.x.
expect(manifest.overrides['js-yaml']).toBe('^4.3.2');
const versions = lockedVersions('js-yaml');
expect(versions).not.toHaveLength(0);
for (const version of versions) {
expect(version).not.toContain('-');
expect(atLeast(version, [4, 3, 2]), `js-yaml ${version} is vulnerable`).toBe(true);
}
});
it('keeps DOMPurify above the hook-detachment XSS floor', () => {
expect(manifest.dependencies.dompurify).toBe('^3.4.13');
const versions = lockedVersions('dompurify');
@@ -26,7 +26,7 @@ interface MockWebSocketInstance {
let mockWsInstance: MockWebSocketInstance | null = null;
const MockWebSocket = vi.fn().mockImplementation((url: string): MockWebSocketInstance => {
const MockWebSocket = vi.fn().mockImplementation(function (url: string): MockWebSocketInstance {
const instance: MockWebSocketInstance = {
url,
readyState: 1,
@@ -25,7 +25,7 @@ import type { ResourceMetadataChangedDetail } from '@/utils/resourceMetadataEven
const win: Window & typeof globalThis = window;
describe('dispatchResourceMetadataChanged', () => {
let listener: ReturnType<typeof vi.fn>;
let listener: ReturnType<typeof vi.fn<(...args: any[]) => any>>;
beforeEach(() => {
listener = vi.fn();