

























@@ -0,0 +1,114 @@
1+import { execFileSync } from "node:child_process";
2+import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
3+import { tmpdir } from "node:os";
4+import { dirname, join, relative, resolve, sep } from "node:path";
5+import { afterEach, describe, expect, it } from "vitest";
6+import { withAugmentedPluginNpmManifestForPackage } from "../../scripts/lib/plugin-npm-package-manifest.mjs";
7+import { collectPublishablePluginPackages } from "../../scripts/lib/plugin-npm-release.ts";
8+import { isScannable, scanDirectoryWithSummary } from "../security/skill-scanner.js";
9+10+type NpmPackFile = {
11+path?: unknown;
12+};
13+14+type NpmPackResult = {
15+files?: unknown;
16+};
17+18+const tempDirs: string[] = [];
19+20+afterEach(() => {
21+for (const dir of tempDirs.splice(0)) {
22+rmSync(dir, { recursive: true, force: true });
23+}
24+});
25+26+function parseNpmPackFiles(raw: string, packageName: string): string[] {
27+const parsed = JSON.parse(raw) as unknown;
28+if (!Array.isArray(parsed) || parsed.length !== 1) {
29+throw new Error(`${packageName}: npm pack --dry-run did not return one package result.`);
30+}
31+32+const result = parsed[0] as NpmPackResult;
33+if (!Array.isArray(result.files)) {
34+throw new Error(`${packageName}: npm pack --dry-run did not return a files list.`);
35+}
36+37+return result.files
38+.map((entry) => (entry as NpmPackFile).path)
39+.filter((packedPath): packedPath is string => typeof packedPath === "string")
40+.toSorted();
41+}
42+43+function collectNpmPackedFiles(packageDir: string, packageName: string): string[] {
44+return withAugmentedPluginNpmManifestForPackage({ packageDir }, ({ packageDir: cwd }) => {
45+const raw = execFileSync("npm", ["pack", "--dry-run", "--json", "--ignore-scripts"], {
46+ cwd,
47+encoding: "utf8",
48+maxBuffer: 128 * 1024 * 1024,
49+stdio: ["ignore", "pipe", "pipe"],
50+});
51+return parseNpmPackFiles(raw, packageName);
52+});
53+}
54+55+function isScannerWalkedPackedPath(packedPath: string): boolean {
56+return (
57+isScannable(packedPath) &&
58+packedPath.split(/[\\/]/).every((segment) => {
59+return segment.length > 0 && segment !== "node_modules" && !segment.startsWith(".");
60+})
61+);
62+}
63+64+function stageScannerRelevantPackedFiles(
65+packageDir: string,
66+packedFiles: readonly string[],
67+): string {
68+const stageDir = mkdtempSync(join(tmpdir(), "openclaw-plugin-npm-scan-"));
69+tempDirs.push(stageDir);
70+71+for (const packedPath of packedFiles) {
72+if (!isScannerWalkedPackedPath(packedPath)) {
73+continue;
74+}
75+76+const source = resolve(packageDir, packedPath);
77+const target = join(stageDir, ...packedPath.split(/[\\/]/));
78+mkdirSync(dirname(target), { recursive: true });
79+copyFileSync(source, target);
80+}
81+82+return stageDir;
83+}
84+85+describe("publishable plugin npm package install security scan", () => {
86+it("keeps npm-published plugin files clear of env-harvesting hits", async () => {
87+const failures: string[] = [];
88+89+for (const plugin of collectPublishablePluginPackages()) {
90+const packedFiles = collectNpmPackedFiles(plugin.packageDir, plugin.packageName);
91+const stageDir = stageScannerRelevantPackedFiles(plugin.packageDir, packedFiles);
92+const summary = await scanDirectoryWithSummary(stageDir, {
93+excludeTestFiles: true,
94+maxFiles: 10_000,
95+});
96+97+for (const finding of summary.findings) {
98+if (finding.ruleId !== "env-harvesting" || finding.severity !== "critical") {
99+continue;
100+}
101+failures.push(
102+[
103+plugin.packageName,
104+relative(stageDir, finding.file).split(sep).join("/"),
105+`${finding.line}`,
106+finding.evidence,
107+].join(":"),
108+);
109+}
110+}
111+112+expect(failures).toEqual([]);
113+});
114+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。