mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 12:26:38 -06:00
fix(release): scan publication-equivalent plugin artifacts
This commit is contained in:
@@ -1,21 +1,20 @@
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { basename, join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
assertCanonicalNpmPackageName,
|
||||
assertCompleteScannerSummary,
|
||||
buildPluginNpmSecurityScanReport,
|
||||
collectNpmPackedFiles,
|
||||
constrainPluginNpmSecurityScanReport,
|
||||
loadPluginNpmSecurityArtifacts,
|
||||
listPluginNpmSecurityArtifacts,
|
||||
listPublishablePluginPackages,
|
||||
normalizePackedFindingPath,
|
||||
parsePacklistFiles,
|
||||
resolveCandidatePluginPackageDir,
|
||||
resolveReviewedSourceLayout,
|
||||
scanPublishablePluginPackages,
|
||||
stageScannerRelevantPackedFiles,
|
||||
stageScannerRelevantPluginTarballFiles,
|
||||
type PublishablePluginPackage,
|
||||
type ScanPackageResult,
|
||||
@@ -54,14 +53,21 @@ function writePublishableManifest(
|
||||
}
|
||||
|
||||
function writePluginArtifact(params: {
|
||||
artifactRoot?: string;
|
||||
extensionId: string;
|
||||
files: Record<string, string | Buffer>;
|
||||
packageName: string;
|
||||
version?: string;
|
||||
}) {
|
||||
const root = tempDirs.make("openclaw-plugin-npm-security-artifact-");
|
||||
const packageRoot = join(root, "source");
|
||||
const artifactDir = join(root, "artifacts", params.extensionId);
|
||||
const root = params.artifactRoot
|
||||
? join(params.artifactRoot, "..")
|
||||
: tempDirs.make("openclaw-plugin-npm-security-artifact-");
|
||||
const artifactRoot = params.artifactRoot ?? join(root, "artifacts");
|
||||
const packageRoot = join(root, `source-${params.extensionId}`);
|
||||
const artifactDir = join(
|
||||
artifactRoot,
|
||||
`plugin-npm-security-package-${CANDIDATE_SHA}-${params.extensionId}`,
|
||||
);
|
||||
const packageVersion = params.version ?? "1.0.0";
|
||||
mkdirSync(packageRoot, { recursive: true });
|
||||
mkdirSync(artifactDir, { recursive: true });
|
||||
@@ -95,9 +101,10 @@ function writePluginArtifact(params: {
|
||||
writeFileSync(
|
||||
join(artifactDir, "plugin-npm-security-artifact.json"),
|
||||
`${JSON.stringify({
|
||||
artifactKind: "inert-package-input",
|
||||
artifactKind: "publication-equivalent-plugin-tarball",
|
||||
candidateSha: CANDIDATE_SHA,
|
||||
extensionId: params.extensionId,
|
||||
packOwner: "scripts/plugin-npm-publish.sh",
|
||||
packageDir: `extensions/${params.extensionId}`,
|
||||
packageName: params.packageName,
|
||||
packageVersion,
|
||||
@@ -110,10 +117,16 @@ function writePluginArtifact(params: {
|
||||
);
|
||||
return {
|
||||
artifact: {
|
||||
artifactKind: "inert-package-input" as const,
|
||||
artifactKind: "publication-equivalent-plugin-tarball" as const,
|
||||
artifactDir,
|
||||
candidateSha: CANDIDATE_SHA,
|
||||
compressedBytes: readFileSync(tarballPath).byteLength,
|
||||
expandedBytes: Object.values(params.files).reduce(
|
||||
(total, value) => total + Buffer.byteLength(value),
|
||||
0,
|
||||
),
|
||||
extensionId: params.extensionId,
|
||||
packOwner: "scripts/plugin-npm-publish.sh" as const,
|
||||
packageDir: `extensions/${params.extensionId}`,
|
||||
packageName: params.packageName,
|
||||
packageVersion,
|
||||
@@ -121,7 +134,7 @@ function writePluginArtifact(params: {
|
||||
tarballSha256,
|
||||
toolingSha: TOOLING_SHA,
|
||||
},
|
||||
artifactRoot: join(root, "artifacts"),
|
||||
artifactRoot,
|
||||
expectedPackage: {
|
||||
extensionId: params.extensionId,
|
||||
packageDir: `extensions/${params.extensionId}`,
|
||||
@@ -179,92 +192,70 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
expect(resolveReviewedSourceLayout([...current, current[0]!])).toBeUndefined();
|
||||
});
|
||||
|
||||
it("collects package files without running lifecycle or candidate replacement helpers", async () => {
|
||||
const packageDir = tempDirs.make("openclaw-plugin-npm-security-pack-");
|
||||
const replacementMarker = join(packageDir, "replacement-helper-ran");
|
||||
it("scans malicious output generated by the canonical publication pack boundary", async () => {
|
||||
const candidateRoot = tempDirs.make("openclaw-plugin-security-built-pack-");
|
||||
const artifactRoot = tempDirs.make("openclaw-plugin-security-built-artifacts-");
|
||||
const packageDir = join(candidateRoot, "extensions", "generated");
|
||||
const trustedPackScript = join(
|
||||
tempDirs.make("openclaw-plugin-security-trusted-pack-"),
|
||||
"pack.sh",
|
||||
);
|
||||
const generatedPath = join(packageDir, "dist", "generated.js");
|
||||
const lifecycleMarkers = ["prepare", "prepack", "postpack"].map((name) =>
|
||||
join(packageDir, `${name}-ran`),
|
||||
);
|
||||
writeFileSync(
|
||||
join(packageDir, "package.json"),
|
||||
`${JSON.stringify({
|
||||
name: "@openclaw/test-inert-package",
|
||||
scripts: Object.fromEntries(
|
||||
["prepare", "prepack", "postpack"].map((name, index) => [
|
||||
name,
|
||||
`node -e "require('node:fs').writeFileSync(${JSON.stringify(lifecycleMarkers[index])}, 'ran')"`,
|
||||
]),
|
||||
),
|
||||
version: "1.0.0",
|
||||
})}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(join(packageDir, "index.js"), "export const value = 1;\n", "utf8");
|
||||
writeFileSync(
|
||||
join(packageDir, "plugin-npm-pack-files.mjs"),
|
||||
`import { writeFileSync } from "node:fs";\nwriteFileSync(${JSON.stringify(replacementMarker)}, "ran");\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const trustedHelper = join(tempDirs.make("openclaw-plugin-npm-security-helper-"), "helper.mjs");
|
||||
writeFileSync(
|
||||
trustedHelper,
|
||||
'process.stdout.write(JSON.stringify(["index.js", "package.json", "plugin-npm-pack-files.mjs"]));\n',
|
||||
"utf8",
|
||||
);
|
||||
const packedFiles = await collectNpmPackedFiles(packageDir, "@openclaw/test-inert-package", {
|
||||
helperPath: trustedHelper,
|
||||
});
|
||||
|
||||
expect(packedFiles).toContain("index.js");
|
||||
expect(packedFiles).toContain("package.json");
|
||||
expect(existsSync(replacementMarker)).toBe(false);
|
||||
for (const marker of lifecycleMarkers) {
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("packs candidate input without running lifecycle or asset build hooks", () => {
|
||||
const candidateRoot = tempDirs.make("openclaw-plugin-security-inert-pack-");
|
||||
const outputDir = tempDirs.make("openclaw-plugin-security-inert-output-");
|
||||
const packageDir = join(candidateRoot, "extensions", "inert");
|
||||
const packlistHelper = join(candidateRoot, "trusted-packlist-helper.mjs");
|
||||
const markers = Object.fromEntries(
|
||||
["asset", "prepare", "prepack", "postpack"].map((name) => [
|
||||
name,
|
||||
join(candidateRoot, `${name}-ran`),
|
||||
]),
|
||||
join(candidateRoot, `${name}-ran`),
|
||||
);
|
||||
initGitRepo(candidateRoot);
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
const markerCommand = (marker: string) =>
|
||||
`node -e "require('node:fs').writeFileSync(${JSON.stringify(marker)}, 'ran')"`;
|
||||
const generatedCode = 'const { execSync } = require("node:child_process");\nexecSync("id");\n';
|
||||
const assetBuild = "node build-assets.mjs";
|
||||
writeFileSync(
|
||||
join(packageDir, "package.json"),
|
||||
`${JSON.stringify({
|
||||
name: "@openclaw/test-inert-pack",
|
||||
files: ["dist", "openclaw.plugin.json", "package.json"],
|
||||
name: "@openclaw/test-generated-pack",
|
||||
openclaw: {
|
||||
assetScripts: { build: markerCommand(markers.asset!) },
|
||||
assetScripts: { build: assetBuild },
|
||||
release: { publishToNpm: true },
|
||||
},
|
||||
scripts: {
|
||||
postpack: markerCommand(markers.postpack!),
|
||||
prepack: markerCommand(markers.prepack!),
|
||||
prepare: markerCommand(markers.prepare!),
|
||||
postpack: markerCommand(lifecycleMarkers[2]!),
|
||||
prepack: markerCommand(lifecycleMarkers[1]!),
|
||||
prepare: markerCommand(lifecycleMarkers[0]!),
|
||||
},
|
||||
version: "1.0.0",
|
||||
version: "2026.8.1-beta.1",
|
||||
})}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(join(packageDir, "index.ts"), "export const inert = true;\n", "utf8");
|
||||
writeFileSync(
|
||||
join(packageDir, "openclaw.plugin.json"),
|
||||
`${JSON.stringify({ id: "inert" })}\n`,
|
||||
`${JSON.stringify({ id: "generated" })}\n`,
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
packlistHelper,
|
||||
'process.stdout.write(JSON.stringify(["index.ts", "openclaw.plugin.json", "package.json"]));\n',
|
||||
join(packageDir, "build-assets.mjs"),
|
||||
[
|
||||
'import { mkdirSync, writeFileSync } from "node:fs";',
|
||||
'mkdirSync("dist", { recursive: true });',
|
||||
`writeFileSync("dist/generated.js", ${JSON.stringify(generatedCode)});`,
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
writeFileSync(
|
||||
trustedPackScript,
|
||||
[
|
||||
"#!/usr/bin/env bash",
|
||||
"set -euo pipefail",
|
||||
'repo_root="$2"',
|
||||
'package_dir="$4"',
|
||||
'cd "$repo_root/$package_dir"',
|
||||
'asset_build="$(node -p "require(\'./package.json\').openclaw.assetScripts.build")"',
|
||||
'bash -c "$asset_build"',
|
||||
'npm pack --json --ignore-scripts --pack-destination "$OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR"',
|
||||
"",
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
);
|
||||
execFileSync("git", ["-C", candidateRoot, "add", "."]);
|
||||
@@ -272,8 +263,10 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
const candidateSha = execFileSync("git", ["-C", candidateRoot, "rev-parse", "HEAD"], {
|
||||
encoding: "utf8",
|
||||
}).trim();
|
||||
const outputDir = join(artifactRoot, `plugin-npm-security-package-${candidateSha}-generated`);
|
||||
const toolingSha = execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim();
|
||||
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
@@ -286,13 +279,13 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
"--candidate-sha",
|
||||
candidateSha,
|
||||
"--extension-id",
|
||||
"inert",
|
||||
"generated",
|
||||
"--output-dir",
|
||||
outputDir,
|
||||
"--package-dir",
|
||||
"extensions/inert",
|
||||
"extensions/generated",
|
||||
"--package-name",
|
||||
"@openclaw/test-inert-pack",
|
||||
"@openclaw/test-generated-pack",
|
||||
"--tooling-sha",
|
||||
toolingSha,
|
||||
],
|
||||
@@ -302,78 +295,41 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: "test",
|
||||
OPENCLAW_PLUGIN_SECURITY_TEST_PACKLIST_HELPER: packlistHelper,
|
||||
OPENCLAW_PLUGIN_SECURITY_TEST_PUBLISH_SCRIPT: trustedPackScript,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
||||
for (const marker of Object.values(markers)) {
|
||||
expect(readFileSync(generatedPath, "utf8")).toBe(generatedCode);
|
||||
for (const marker of lifecycleMarkers) {
|
||||
expect(existsSync(marker)).toBe(false);
|
||||
}
|
||||
const metadata = JSON.parse(
|
||||
readFileSync(join(outputDir, "plugin-npm-security-artifact.json"), "utf8"),
|
||||
) as { artifactKind?: unknown; tarballName?: unknown };
|
||||
expect(metadata.artifactKind).toBe("inert-package-input");
|
||||
expect(metadata.artifactKind).toBe("publication-equivalent-plugin-tarball");
|
||||
expect(typeof metadata.tarballName).toBe("string");
|
||||
const staged = stageScannerRelevantPluginTarballFiles(
|
||||
join(outputDir, String(metadata.tarballName)),
|
||||
);
|
||||
try {
|
||||
expect(staged.packedFiles).toContain("index.ts");
|
||||
expect(staged.packedFiles.some((file) => file.startsWith("dist/"))).toBe(false);
|
||||
} finally {
|
||||
rmSync(staged.stageDir, { force: true, recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects malformed, unsafe, duplicate, and excessive packlist entries", () => {
|
||||
for (const malformed of [
|
||||
null,
|
||||
{},
|
||||
1,
|
||||
"",
|
||||
"/absolute.js",
|
||||
"dir\\file.js",
|
||||
"../escape.js",
|
||||
".hidden.js",
|
||||
"node_modules/dependency.js",
|
||||
"a".repeat(4097),
|
||||
]) {
|
||||
expect(() =>
|
||||
parsePacklistFiles(JSON.stringify(["valid.js", malformed]), "@openclaw/test"),
|
||||
).toThrow("entry 1 has an invalid path");
|
||||
}
|
||||
expect(() =>
|
||||
parsePacklistFiles(JSON.stringify(["index.js", "index.js"]), "@openclaw/test"),
|
||||
).toThrow("duplicate path");
|
||||
expect(() =>
|
||||
parsePacklistFiles(
|
||||
JSON.stringify(Array.from({ length: 20_001 }, (_, index) => `file-${index}.js`)),
|
||||
"@openclaw/test",
|
||||
),
|
||||
).toThrow("file-count limit");
|
||||
});
|
||||
|
||||
it("preserves bounded packlist helper failure categories", async () => {
|
||||
const root = tempDirs.make("openclaw-plugin-packlist-helper-limits-");
|
||||
const packageDir = tempDirs.make("openclaw-plugin-packlist-helper-package-");
|
||||
const timeoutHelper = join(root, "timeout.mjs");
|
||||
const failedHelper = join(root, "failed.mjs");
|
||||
writeFileSync(timeoutHelper, "setInterval(() => {}, 1_000);\n", "utf8");
|
||||
writeFileSync(failedHelper, "process.exit(7);\n", "utf8");
|
||||
|
||||
await expect(
|
||||
collectNpmPackedFiles(packageDir, "@openclaw/test-timeout", {
|
||||
helperPath: timeoutHelper,
|
||||
timeoutMs: 25,
|
||||
}),
|
||||
).rejects.toThrow("trusted packlist helper timed out");
|
||||
await expect(
|
||||
collectNpmPackedFiles(packageDir, "@openclaw/test-failed", {
|
||||
helperPath: failedHelper,
|
||||
}),
|
||||
).rejects.toThrow("trusted packlist helper failed");
|
||||
const expectedPackage = {
|
||||
extensionId: "generated",
|
||||
packageDir: "extensions/generated",
|
||||
packageName: "@openclaw/test-generated-pack",
|
||||
packageVersion: "2026.8.1-beta.1",
|
||||
};
|
||||
const loaded = loadPluginNpmSecurityArtifacts({
|
||||
artifactRoot,
|
||||
candidateSha,
|
||||
expectedPackages: [expectedPackage],
|
||||
toolingSha,
|
||||
});
|
||||
expect(loaded.ingestionErrors).toEqual([]);
|
||||
const scanned = await scanPublishablePluginPackages(loaded.artifacts);
|
||||
expect(scanned.scanErrors).toEqual([]);
|
||||
expect(scanned.packageResults[0]?.unexpectedCriticalFindings).toContainEqual({
|
||||
line: 2,
|
||||
path: "dist/generated.js",
|
||||
ruleId: "dangerous-exec",
|
||||
});
|
||||
});
|
||||
|
||||
it("bounds manifests and rejects noncanonical or duplicate package identities", async () => {
|
||||
@@ -402,24 +358,27 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
).rejects.toThrow("manifest exceeds the byte limit");
|
||||
});
|
||||
|
||||
it("fails closed on truncated scans, source escapes, and tarball symlinks", () => {
|
||||
it("fails closed on truncated scans, candidate package escapes, and tarball symlinks", () => {
|
||||
expect(() => assertCompleteScannerSummary("@openclaw/test", { truncated: true })).toThrow(
|
||||
"security scan reached its file limit",
|
||||
);
|
||||
expect(() =>
|
||||
stageScannerRelevantPackedFiles(tempDirs.make("openclaw-plugin-npm-security-path-"), [
|
||||
"../escape.ts",
|
||||
]),
|
||||
).toThrow("npm pack returned an unsafe path");
|
||||
|
||||
const candidateRoot = tempDirs.make("openclaw-plugin-npm-security-candidate-");
|
||||
const outsideDir = tempDirs.make("openclaw-plugin-npm-security-outside-");
|
||||
mkdirSync(join(candidateRoot, "extensions"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(outsideDir, "package.json"),
|
||||
`${JSON.stringify({ name: "@openclaw/escape", version: "1.0.0" })}\n`,
|
||||
"utf8",
|
||||
);
|
||||
symlinkSync(outsideDir, join(candidateRoot, "extensions", "escape"));
|
||||
expect(() => resolveCandidatePluginPackageDir(candidateRoot, "escape")).toThrow(
|
||||
"package directory is not a real directory",
|
||||
);
|
||||
|
||||
const packageDir = tempDirs.make("openclaw-plugin-npm-security-symlink-");
|
||||
const outsideDir = tempDirs.make("openclaw-plugin-npm-security-outside-");
|
||||
const outsideFile = join(outsideDir, "outside.ts");
|
||||
writeFileSync(outsideFile, "export const value = 1;\n", "utf8");
|
||||
symlinkSync(outsideFile, join(packageDir, "escape.ts"));
|
||||
expect(() => stageScannerRelevantPackedFiles(packageDir, ["escape.ts"])).toThrow(
|
||||
"not a regular file",
|
||||
);
|
||||
|
||||
const artifact = writePluginArtifact({
|
||||
extensionId: "symlink",
|
||||
@@ -429,7 +388,13 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
symlinkSync(outsideFile, join(artifact.packageRoot, "escape.ts"));
|
||||
execFileSync(
|
||||
"tar",
|
||||
["-czf", artifact.tarballPath, "-C", join(artifact.packageRoot, ".."), "source"],
|
||||
[
|
||||
"-czf",
|
||||
artifact.tarballPath,
|
||||
"-C",
|
||||
join(artifact.packageRoot, ".."),
|
||||
basename(artifact.packageRoot),
|
||||
],
|
||||
{ env: { ...process.env, COPYFILE_DISABLE: "1" } },
|
||||
);
|
||||
expect(() => stageScannerRelevantPluginTarballFiles(artifact.tarballPath)).toThrow();
|
||||
@@ -521,7 +486,102 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
expectedPackages: [],
|
||||
toolingSha: TOOLING_SHA,
|
||||
}),
|
||||
).toThrow("does not match the trusted package plan");
|
||||
).toThrow("unexpected entries");
|
||||
});
|
||||
|
||||
it("retains valid package scans when a sibling artifact is malformed", async () => {
|
||||
const artifactRoot = tempDirs.make("openclaw-plugin-npm-security-mixed-");
|
||||
const valid = writePluginArtifact({
|
||||
artifactRoot,
|
||||
extensionId: "valid",
|
||||
files: { "index.js": "export const value = 1;\n" },
|
||||
packageName: "@openclaw/test-valid",
|
||||
});
|
||||
const malformed = writePluginArtifact({
|
||||
artifactRoot,
|
||||
extensionId: "malformed",
|
||||
files: { "index.js": "export const value = 2;\n" },
|
||||
packageName: "@openclaw/test-malformed",
|
||||
});
|
||||
writeFileSync(
|
||||
join(malformed.artifact.artifactDir, "plugin-npm-security-artifact.json"),
|
||||
"{not-json}\n",
|
||||
"utf8",
|
||||
);
|
||||
const expectedPackages = [malformed.expectedPackage, valid.expectedPackage].toSorted(
|
||||
(left, right) => (left.packageName < right.packageName ? -1 : 1),
|
||||
);
|
||||
|
||||
const loaded = loadPluginNpmSecurityArtifacts({
|
||||
artifactRoot,
|
||||
candidateSha: CANDIDATE_SHA,
|
||||
expectedPackages,
|
||||
toolingSha: TOOLING_SHA,
|
||||
});
|
||||
expect(loaded.artifacts.map((artifact) => artifact.packageName)).toEqual([
|
||||
"@openclaw/test-valid",
|
||||
]);
|
||||
expect(loaded.ingestionErrors).toEqual([
|
||||
"@openclaw/test-malformed: Plugin security artifact metadata is not valid JSON.",
|
||||
]);
|
||||
|
||||
const scanned = await scanPublishablePluginPackages(loaded.artifacts);
|
||||
expect(scanned.scanErrors).toEqual([]);
|
||||
expect(scanned.packageResults.map((result) => result.packageName)).toEqual([
|
||||
"@openclaw/test-valid",
|
||||
]);
|
||||
});
|
||||
|
||||
it("bounds aggregate compressed and expanded artifact bytes deterministically", () => {
|
||||
const artifactRoot = tempDirs.make("openclaw-plugin-npm-security-aggregate-");
|
||||
const alpha = writePluginArtifact({
|
||||
artifactRoot,
|
||||
extensionId: "alpha",
|
||||
files: { "alpha.js": Buffer.alloc(256, 1) },
|
||||
packageName: "@openclaw/test-alpha",
|
||||
});
|
||||
const beta = writePluginArtifact({
|
||||
artifactRoot,
|
||||
extensionId: "beta",
|
||||
files: { "beta.js": Buffer.alloc(256, 2) },
|
||||
packageName: "@openclaw/test-beta",
|
||||
});
|
||||
const expectedPackages = [alpha.expectedPackage, beta.expectedPackage];
|
||||
const baseline = loadPluginNpmSecurityArtifacts({
|
||||
artifactRoot,
|
||||
candidateSha: CANDIDATE_SHA,
|
||||
expectedPackages,
|
||||
toolingSha: TOOLING_SHA,
|
||||
});
|
||||
expect(baseline.ingestionErrors).toEqual([]);
|
||||
|
||||
const compressed = loadPluginNpmSecurityArtifacts({
|
||||
artifactRoot,
|
||||
candidateSha: CANDIDATE_SHA,
|
||||
expectedPackages,
|
||||
limits: { maxCompressedBytes: baseline.artifacts[0]!.compressedBytes },
|
||||
toolingSha: TOOLING_SHA,
|
||||
});
|
||||
expect(compressed.artifacts.map((artifact) => artifact.packageName)).toEqual([
|
||||
"@openclaw/test-alpha",
|
||||
]);
|
||||
expect(compressed.ingestionErrors).toEqual([
|
||||
"@openclaw/test-beta: aggregate compressed-byte limit exceeded.",
|
||||
]);
|
||||
|
||||
const expanded = loadPluginNpmSecurityArtifacts({
|
||||
artifactRoot,
|
||||
candidateSha: CANDIDATE_SHA,
|
||||
expectedPackages,
|
||||
limits: { maxExpandedBytes: baseline.artifacts[0]!.expandedBytes },
|
||||
toolingSha: TOOLING_SHA,
|
||||
});
|
||||
expect(expanded.artifacts.map((artifact) => artifact.packageName)).toEqual([
|
||||
"@openclaw/test-alpha",
|
||||
]);
|
||||
expect(expanded.ingestionErrors).toEqual([
|
||||
"@openclaw/test-beta: aggregate expanded-byte limit exceeded.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("caps total findings and emits byte-identical bounded reports", () => {
|
||||
@@ -555,19 +615,26 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("writes sanitized exact-identity reports when the bounded scanner times out or OOMs", () => {
|
||||
it("writes sanitized exact-identity reports for timeout, heap, and RSS failures", () => {
|
||||
const root = tempDirs.make("openclaw-plugin-npm-security-runner-");
|
||||
const timeoutChild = join(root, "timeout.mjs");
|
||||
const oomChild = join(root, "oom.mjs");
|
||||
const rssChild = join(root, "rss.mjs");
|
||||
writeFileSync(timeoutChild, "await new Promise(() => {});\n", "utf8");
|
||||
writeFileSync(
|
||||
oomChild,
|
||||
"const values = [];\nwhile (true) values.push(new Array(100000).fill(Math.random()));\n",
|
||||
"utf8",
|
||||
);
|
||||
for (const [label, child, timeoutMs] of [
|
||||
["timeout", timeoutChild, "25"],
|
||||
["oom", oomChild, "10000"],
|
||||
writeFileSync(
|
||||
rssChild,
|
||||
"globalThis.value = Buffer.alloc(64 * 1024 * 1024, 1);\nsetInterval(() => {}, 1_000);\n",
|
||||
"utf8",
|
||||
);
|
||||
for (const [label, child, timeoutMs, heapMb, rssMb, expectedError] of [
|
||||
["timeout", timeoutChild, "25", "128", "1024", "timed out"],
|
||||
["oom", oomChild, "10000", "16", "1024", "exceeded its process limit"],
|
||||
["rss", rssChild, "10000", "128", "16", "exceeded its RSS limit"],
|
||||
] as const) {
|
||||
const reportPath = join(root, `${label}.json`);
|
||||
const result = spawnSync(
|
||||
@@ -592,7 +659,8 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
...process.env,
|
||||
NODE_ENV: "test",
|
||||
OPENCLAW_PLUGIN_SECURITY_RUNNER_CHILD: child,
|
||||
OPENCLAW_PLUGIN_SECURITY_RUNNER_HEAP_MB: "16",
|
||||
OPENCLAW_PLUGIN_SECURITY_RUNNER_HEAP_MB: heapMb,
|
||||
OPENCLAW_PLUGIN_SECURITY_RUNNER_RSS_MB: rssMb,
|
||||
OPENCLAW_PLUGIN_SECURITY_RUNNER_TIMEOUT_MS: timeoutMs,
|
||||
},
|
||||
timeout: 15_000,
|
||||
@@ -600,6 +668,7 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
);
|
||||
const report = JSON.parse(readFileSync(reportPath, "utf8")) as {
|
||||
candidateSha: string;
|
||||
errors: string[];
|
||||
toolingSha: string;
|
||||
};
|
||||
expect(result.status).toBe(1);
|
||||
@@ -607,6 +676,7 @@ describe("scripts/lib/plugin-npm-security-scan.mts", () => {
|
||||
candidateSha: CANDIDATE_SHA,
|
||||
toolingSha: TOOLING_SHA,
|
||||
});
|
||||
expect(report.errors).toContainEqual(expect.stringContaining(expectedError));
|
||||
expect(`${result.stdout}${result.stderr}${JSON.stringify(report)}`).not.toContain(root);
|
||||
}
|
||||
}, 30_000);
|
||||
|
||||
@@ -300,7 +300,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
|
||||
(step: WorkflowStep) => step.name === "Install trusted scanner dependencies",
|
||||
);
|
||||
const runSecurityScan = securityScan.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Scan inert plugin package inputs",
|
||||
(step: WorkflowStep) => step.name === "Scan publication-equivalent plugin artifacts",
|
||||
);
|
||||
const uploadReport = securityScan.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Upload plugin npm security scan report",
|
||||
@@ -310,6 +310,7 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
|
||||
);
|
||||
const releaseWorkflow = readFullReleaseValidationWorkflow();
|
||||
const releaseSource = readFileSync(".github/workflows/full-release-validation.yml", "utf8");
|
||||
const pluginNpmReleaseSource = readFileSync(".github/workflows/plugin-npm-release.yml", "utf8");
|
||||
const pluginDispatch = releaseWorkflow.jobs.plugin_prerelease.steps.find(
|
||||
(step: WorkflowStep) => step.name === "Dispatch and monitor plugin prerelease",
|
||||
);
|
||||
@@ -368,15 +369,15 @@ describe("scripts/lib/plugin-prerelease-test-plan.mts", () => {
|
||||
expect(securityScan.needs).not.toContain("preflight");
|
||||
expect(securityPackage.permissions).toEqual({ contents: "read" });
|
||||
expect(securityPackage.secrets).toBeUndefined();
|
||||
expect(JSON.stringify(securityPackage)).not.toContain("plugin-npm-publish.sh");
|
||||
expect(JSON.stringify(securityPackage)).not.toContain("plugin-npm-runtime-build");
|
||||
expect(JSON.stringify(securityPackage)).not.toContain("generate-npm-package-lock");
|
||||
expect(securityPrepareSource).not.toContain("plugin-npm-publish.sh");
|
||||
expect(securityPrepareSource).not.toContain("plugin-npm-runtime-build");
|
||||
expect(securityPrepareSource).not.toContain("generate-npm-package-lock");
|
||||
expect(securityPrepareSource).not.toMatch(/\bnpmArgs:\s*\[\s*["'](?:ci|install)["']/u);
|
||||
expect(securityPrepareSource).toContain('"--ignore-scripts"');
|
||||
expect(securityPrepareSource).toContain('"--workspaces=false"');
|
||||
expect(JSON.stringify(securityPackage)).not.toContain("id-token");
|
||||
expect(JSON.stringify(securityPackage)).not.toContain("packages: write");
|
||||
expect(JSON.stringify(securityPackage)).not.toContain("${{ secrets.");
|
||||
expect(securityPrepareSource).toContain('"scripts", "plugin-npm-publish.sh"');
|
||||
expect(securityPrepareSource).toContain('"--pack"');
|
||||
expect(securityPrepareSource).toContain("shell: false");
|
||||
expect(securityPrepareSource).toContain("resolveCandidatePluginPackageDir");
|
||||
expect(pluginNpmReleaseSource).toContain("bash .release-tooling/scripts/plugin-npm-publish.sh");
|
||||
expect(pluginNpmReleaseSource).toContain('--pack "${PACKAGE_DIR}"');
|
||||
expect(nodeShard.needs).toEqual(["resolve-candidate", "preflight"]);
|
||||
expect(runNodeShard?.run).toContain('spawnSync("pnpm", ["test", "--", ...configs]');
|
||||
expect(pluginSource).not.toContain("npm-install-security-scan.release.test.ts");
|
||||
|
||||
Reference in New Issue
Block a user