fix: fail closed on release input discovery
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { verifyBuildManifestOutputs } from "../../scripts/lib/build-manifest-outputs.ts";
|
||||
|
||||
import {
|
||||
dependencyInventoryArtifactSchema,
|
||||
fieldWebVitalsArtifactSchema,
|
||||
@@ -58,6 +61,145 @@ const buildManifest = {
|
||||
} as const;
|
||||
|
||||
describe("release artifact contracts", () => {
|
||||
it("verifies confined build outputs and the raw module inventory bytes", async () => {
|
||||
const moduleInventoryBytes = Buffer.from(
|
||||
'{"schemaVersion":1,"chunks":[]}\n',
|
||||
);
|
||||
const files = new Map<string, Buffer>([
|
||||
["/repo/dist/.vite/manifest.json", Buffer.from("{}\n")],
|
||||
["/repo/artifacts/quality/vite-module-inventory.json", moduleInventoryBytes],
|
||||
["/repo/dist/runtime-config.schema.json", Buffer.from("{}\n")],
|
||||
["/repo/dist/assets/home.js", Buffer.from("chunk\n")],
|
||||
]);
|
||||
const manifest = {
|
||||
...buildManifest,
|
||||
moduleInventoryHash: createHash("sha256")
|
||||
.update(moduleInventoryBytes)
|
||||
.digest("hex"),
|
||||
};
|
||||
|
||||
await expect(
|
||||
verifyBuildManifestOutputs(manifest, {
|
||||
repositoryRoot: "/repo",
|
||||
readBytes: async (target) => files.get(target) ?? Promise.reject(Object.assign(new Error("missing"), { code: "ENOENT" })),
|
||||
realpathPath: async (target) => target,
|
||||
assertRegularFile: async () => undefined,
|
||||
assertDirectory: async () => undefined,
|
||||
}),
|
||||
).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["missing", undefined, /moduleInventory:missing/u],
|
||||
[
|
||||
"tampered",
|
||||
Buffer.from('{"schemaVersion":1,"chunks":[{"fileName":"other.js","modules":[]}]}\n'),
|
||||
/moduleInventoryHash/u,
|
||||
],
|
||||
] as const)("rejects a %s module inventory", async (_name, bytes, expected) => {
|
||||
const files = new Map<string, Buffer>([
|
||||
["/repo/dist/.vite/manifest.json", Buffer.from("{}\n")],
|
||||
["/repo/dist/runtime-config.schema.json", Buffer.from("{}\n")],
|
||||
["/repo/dist/assets/home.js", Buffer.from("chunk\n")],
|
||||
...(bytes ? [["/repo/artifacts/quality/vite-module-inventory.json", bytes] as const] : []),
|
||||
]);
|
||||
const mismatches = await verifyBuildManifestOutputs(buildManifest, {
|
||||
repositoryRoot: "/repo",
|
||||
readBytes: async (target) => files.get(target) ?? Promise.reject(Object.assign(new Error("missing"), { code: "ENOENT" })),
|
||||
realpathPath: async (target) => target,
|
||||
assertRegularFile: async () => undefined,
|
||||
assertDirectory: async () => undefined,
|
||||
});
|
||||
expect(mismatches.join("\n")).toMatch(expected);
|
||||
});
|
||||
|
||||
it.each(["../outside", "/absolute", "dist\\escape"])(
|
||||
"rejects unsafe build-manifest output path %s",
|
||||
async (unsafePath) => {
|
||||
const mismatches = await verifyBuildManifestOutputs(
|
||||
{
|
||||
...buildManifest,
|
||||
outputs: { ...buildManifest.outputs, moduleInventory: unsafePath },
|
||||
},
|
||||
{
|
||||
repositoryRoot: "/repo",
|
||||
realpathPath: async (target) => target,
|
||||
},
|
||||
);
|
||||
expect(mismatches).toContain("buildManifest:moduleInventory:path");
|
||||
},
|
||||
);
|
||||
|
||||
it("rejects a realpath escape from a declared build output", async () => {
|
||||
const mismatches = await verifyBuildManifestOutputs(buildManifest, {
|
||||
repositoryRoot: "/repo",
|
||||
realpathPath: async (target) =>
|
||||
target.endsWith("vite-module-inventory.json") ? "/outside/inventory.json" : target,
|
||||
assertRegularFile: async () => undefined,
|
||||
assertDirectory: async () => undefined,
|
||||
readBytes: async () => Buffer.from("inventory\n"),
|
||||
});
|
||||
expect(mismatches).toContain("buildManifest:moduleInventory:path");
|
||||
});
|
||||
|
||||
it("rejects a nested symlink that resolves inside the repository but outside dist", async () => {
|
||||
const moduleInventoryBytes = Buffer.from(
|
||||
'{"schemaVersion":1,"chunks":[]}\n',
|
||||
);
|
||||
const mismatches = await verifyBuildManifestOutputs(
|
||||
{
|
||||
...buildManifest,
|
||||
moduleInventoryHash: createHash("sha256")
|
||||
.update(moduleInventoryBytes)
|
||||
.digest("hex"),
|
||||
},
|
||||
{
|
||||
repositoryRoot: "/repo",
|
||||
realpathPath: async (target) =>
|
||||
target === "/repo/dist/assets/home.js"
|
||||
? "/repo/src/home.js"
|
||||
: target,
|
||||
assertRegularFile: async () => undefined,
|
||||
assertDirectory: async () => undefined,
|
||||
readBytes: async () => moduleInventoryBytes,
|
||||
},
|
||||
);
|
||||
expect(mismatches).toContain("buildManifest:routeChunk:route-home:path");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["viteManifest", "package.json"],
|
||||
["runtimeConfigSchema", "schemas/artifacts/build-manifest.schema.json"],
|
||||
["moduleInventory", "package.json"],
|
||||
] as const)("rejects %s outside its approved output root", async (field, value) => {
|
||||
const mismatches = await verifyBuildManifestOutputs(
|
||||
{
|
||||
...buildManifest,
|
||||
outputs: { ...buildManifest.outputs, [field]: value },
|
||||
},
|
||||
{ repositoryRoot: process.cwd() },
|
||||
);
|
||||
expect(mismatches).toContain(`buildManifest:${field}:path`);
|
||||
});
|
||||
|
||||
it("rejects a parse-invalid module inventory even when its raw hash matches", async () => {
|
||||
const bytes = Buffer.from("{}\n");
|
||||
const mismatches = await verifyBuildManifestOutputs(
|
||||
{
|
||||
...buildManifest,
|
||||
moduleInventoryHash: createHash("sha256").update(bytes).digest("hex"),
|
||||
},
|
||||
{
|
||||
repositoryRoot: "/repo",
|
||||
readBytes: async () => bytes,
|
||||
realpathPath: async (target) => target,
|
||||
assertRegularFile: async () => undefined,
|
||||
assertDirectory: async () => undefined,
|
||||
},
|
||||
);
|
||||
expect(mismatches).toContain("buildManifest:moduleInventory:invalid");
|
||||
});
|
||||
|
||||
it("projects the nested V2 contract-set digest without a legacy scalar", () => {
|
||||
const release = parseReleaseArtifact(releaseV2);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user