mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
fix(governance): enforce frontend dependency audits
This commit is contained in:
@@ -122,6 +122,14 @@ jobs:
|
||||
working-directory: frontend-modern
|
||||
run: npm ci
|
||||
|
||||
- name: Audit complete frontend dependency graph
|
||||
working-directory: frontend-modern
|
||||
run: npm audit
|
||||
|
||||
- name: Audit production frontend dependencies
|
||||
working-directory: frontend-modern
|
||||
run: npm audit --omit=dev
|
||||
|
||||
# Whole-tree, not staged-only: the pre-commit formatter only ever sees
|
||||
# staged files, so drift in untouched files is invisible to it. This is
|
||||
# the backstop that keeps `make format` a no-op on a clean tree.
|
||||
|
||||
@@ -51,6 +51,7 @@ TLS floor in the dynamic config.
|
||||
16. `internal/cloudcp/docker/labels.go`
|
||||
17. `internal/cloudcp/tenant_runtime_rollout.go`
|
||||
13. `.github/workflows/build-release-candidate.yml`
|
||||
14. `.github/workflows/build-and-test.yml`
|
||||
14. `.github/workflows/create-release.yml`
|
||||
14. `.github/workflows/deploy-demo-server.yml`
|
||||
15. `.github/workflows/helm-pages.yml`
|
||||
@@ -2377,6 +2378,19 @@ are part of the same governed bootstrap input even when the package manifest
|
||||
range already permits the newer version; the lockfile must identify the
|
||||
resolved package version and integrity that the release build will actually
|
||||
consume.
|
||||
Frontend dependency-security changes use their own proof route rather than
|
||||
borrowing the local dev-runtime orchestration tests. The canonical
|
||||
`.github/workflows/build-and-test.yml` frontend job must run both the complete
|
||||
`npm audit` and the production-only `npm audit --omit=dev` after a clean
|
||||
install. `frontend-modern/src/security/__tests__/dependencySecurity.test.ts`
|
||||
pins the known safe floors for advisories remediated by commit `6ba85a185`,
|
||||
including DOMPurify `GHSA-55q2-fjhq-7xh7`, brace-expansion
|
||||
`GHSA-mh99-v99m-4gvg` and `GHSA-rgw5-rvv9-x895`, and nanoid
|
||||
`GHSA-2v37-7h3g-55p8`, while
|
||||
`scripts/installtests/build_release_assets_test.go` prevents either CI audit
|
||||
gate from being removed silently. A later advisory must advance these floors
|
||||
and its sanitizer or dependency-specific regression proof together; audit
|
||||
suppression is not a valid closure.
|
||||
Security-driven Go module graph bumps follow the same rule: `go.mod` and
|
||||
`go.sum` must move together when a reachable vulnerability is remediated, and
|
||||
the slice must carry direct vulnerability or dependency-floor proof so the
|
||||
|
||||
@@ -4115,6 +4115,7 @@
|
||||
".github/scripts/check-demo-reachability.sh",
|
||||
".github/scripts/setup-demo-ssh.sh",
|
||||
".github/workflows/backfill-release-assets.yml",
|
||||
".github/workflows/build-and-test.yml",
|
||||
".github/workflows/build-release-candidate.yml",
|
||||
".github/workflows/create-release.yml",
|
||||
".github/workflows/deploy-demo-server.yml",
|
||||
@@ -4515,13 +4516,27 @@
|
||||
"scripts/installtests/provider_msp_deploy_test.go"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "frontend-dependency-security",
|
||||
"label": "frontend dependency security proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
".github/workflows/build-and-test.yml",
|
||||
"frontend-modern/package-lock.json",
|
||||
"frontend-modern/package.json"
|
||||
],
|
||||
"allow_same_subsystem_tests": false,
|
||||
"test_prefixes": [],
|
||||
"exact_files": [
|
||||
"frontend-modern/src/security/__tests__/dependencySecurity.test.ts",
|
||||
"scripts/installtests/build_release_assets_test.go"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dev-runtime-orchestration",
|
||||
"label": "dev runtime orchestration proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
"frontend-modern/package-lock.json",
|
||||
"frontend-modern/package.json",
|
||||
"frontend-modern/vite.config.ts",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
interface PackageManifest {
|
||||
dependencies: Record<string, string>;
|
||||
}
|
||||
|
||||
interface PackageLock {
|
||||
packages: Record<string, { version?: string }>;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'),
|
||||
) as PackageManifest;
|
||||
const lock = JSON.parse(
|
||||
readFileSync(new URL('../../../package-lock.json', import.meta.url), 'utf8'),
|
||||
) as PackageLock;
|
||||
|
||||
const parseVersion = (version: string): [number, number, number] => {
|
||||
const [major = 0, minor = 0, patch = 0] = version
|
||||
.split('-', 1)[0]
|
||||
.split('.')
|
||||
.map((part) => Number.parseInt(part, 10));
|
||||
return [major, minor, patch];
|
||||
};
|
||||
|
||||
const atLeast = (version: string, floor: [number, number, number]): boolean => {
|
||||
const current = parseVersion(version);
|
||||
for (let index = 0; index < current.length; index += 1) {
|
||||
if (current[index] !== floor[index]) return current[index] > floor[index];
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const lockedVersions = (packageName: string): string[] =>
|
||||
Object.entries(lock.packages)
|
||||
.filter(
|
||||
([path]) =>
|
||||
path === `node_modules/${packageName}` || path.endsWith(`/node_modules/${packageName}`),
|
||||
)
|
||||
.map(([, entry]) => entry.version)
|
||||
.filter((version): version is string => Boolean(version));
|
||||
|
||||
const braceExpansionIsPatched = (version: string): boolean => {
|
||||
const [major] = parseVersion(version);
|
||||
if (major === 1) return atLeast(version, [1, 1, 18]);
|
||||
if (major === 2) return atLeast(version, [2, 1, 4]);
|
||||
if (major === 3) return atLeast(version, [3, 0, 6]);
|
||||
return major >= 5 && atLeast(version, [5, 0, 9]);
|
||||
};
|
||||
|
||||
const nanoidIsPatched = (version: string): boolean => {
|
||||
const [major] = parseVersion(version);
|
||||
if (major === 3) return atLeast(version, [3, 3, 17]);
|
||||
return major >= 5 && atLeast(version, [5, 1, 6]);
|
||||
};
|
||||
|
||||
describe('frontend dependency security floors', () => {
|
||||
it('keeps DOMPurify above the hook-detachment XSS floor', () => {
|
||||
expect(manifest.dependencies.dompurify).toBe('^3.4.13');
|
||||
const versions = lockedVersions('dompurify');
|
||||
expect(versions).not.toHaveLength(0);
|
||||
for (const version of versions) {
|
||||
expect(atLeast(version, [3, 4, 13]), `dompurify ${version} is vulnerable`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps every brace-expansion line above both denial-of-service advisory floors', () => {
|
||||
const versions = lockedVersions('brace-expansion');
|
||||
expect(versions).not.toHaveLength(0);
|
||||
for (const version of versions) {
|
||||
expect(braceExpansionIsPatched(version), `brace-expansion ${version} is vulnerable`).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps nanoid custom generators above the zero-size loop floor', () => {
|
||||
const versions = lockedVersions('nanoid');
|
||||
expect(versions).not.toHaveLength(0);
|
||||
for (const version of versions) {
|
||||
expect(nanoidIsPatched(version), `nanoid ${version} is vulnerable`).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -2112,6 +2112,16 @@ func TestReleasePipelinePromotesOneImmutableCandidate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFrontendDependencySecurityAuditsAreRequired(t *testing.T) {
|
||||
workflowPath := repoFile(".github", "workflows", "build-and-test.yml")
|
||||
assertFileContainsAll(t, workflowPath,
|
||||
`- name: Audit complete frontend dependency graph`,
|
||||
`run: npm audit`,
|
||||
`- name: Audit production frontend dependencies`,
|
||||
`run: npm audit --omit=dev`,
|
||||
)
|
||||
}
|
||||
|
||||
func TestReleaseCutGatesCriticalFrontendAndWindowsRuntimeProof(t *testing.T) {
|
||||
content, err := os.ReadFile(repoFile(".github", "workflows", "create-release.yml"))
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user