mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-17 12:11:20 +00:00
* fix: address issue * fix: address review feedback * fix: address PR review feedback * fix: address PR review feedback * fix: address PR review feedback * fix: address PR review feedback * fix: address PR review feedback * Plugins: fix install security CI regressions * Plugins: make manifest traversal linear * Plugins: bound manifest security traversal * Plugins: block denied node_modules package dirs * Plugins: match node_modules case-insensitively * Plugins: block denied package symlink paths * Tests: normalize blocked symlink assertion * Plugins: fail closed on unreadable denied paths * Plugins: block denied node_modules file aliases * Plugins: inspect node_modules symlink targets * Plugins: preserve symlink target package paths * fix: address PR review feedback * chore(changelog): add axios pin and dependency denylist entry --------- Co-authored-by: Devin Robison <drobison@nvidia.com>
210 lines
6.2 KiB
TypeScript
210 lines
6.2 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
blockedInstallDependencyPackageNames,
|
|
findBlockedPackageDirectoryInPath,
|
|
findBlockedPackageFileAliasInPath,
|
|
findBlockedManifestDependencies,
|
|
findBlockedNodeModulesDirectory,
|
|
findBlockedNodeModulesFileAlias,
|
|
} from "./dependency-denylist.js";
|
|
|
|
type RootPackageManifest = {
|
|
dependencies?: Record<string, string>;
|
|
optionalDependencies?: Record<string, string>;
|
|
overrides?: Record<string, string | Record<string, string>>;
|
|
peerDependencies?: Record<string, string>;
|
|
pnpm?: {
|
|
overrides?: Record<string, string>;
|
|
};
|
|
};
|
|
|
|
function readRootManifest(): RootPackageManifest {
|
|
return JSON.parse(
|
|
fs.readFileSync(path.resolve(process.cwd(), "package.json"), "utf8"),
|
|
) as RootPackageManifest;
|
|
}
|
|
|
|
function readRootLockfile(): string {
|
|
return fs.readFileSync(path.resolve(process.cwd(), "pnpm-lock.yaml"), "utf8");
|
|
}
|
|
|
|
describe("dependency denylist guardrails", () => {
|
|
it("finds blocked package names on vendored manifests", () => {
|
|
expect(
|
|
findBlockedManifestDependencies({
|
|
name: "plain-crypto-js",
|
|
}),
|
|
).toEqual([
|
|
{
|
|
dependencyName: "plain-crypto-js",
|
|
field: "name",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("finds blocked packages declared through npm alias specs", () => {
|
|
expect(
|
|
findBlockedManifestDependencies({
|
|
dependencies: {
|
|
"safe-name": "npm:plain-crypto-js@^4.2.1",
|
|
},
|
|
peerDependencies: {
|
|
"@alias/safe": "npm:@scope/ok@^1.0.0",
|
|
},
|
|
}),
|
|
).toEqual([
|
|
{
|
|
dependencyName: "plain-crypto-js",
|
|
declaredAs: "safe-name",
|
|
field: "dependencies",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("finds blocked packages declared through nested override alias specs", () => {
|
|
expect(
|
|
findBlockedManifestDependencies({
|
|
overrides: {
|
|
axios: "1.15.0",
|
|
"@scope/parent": {
|
|
"safe-name": "npm:plain-crypto-js@^4.2.1",
|
|
},
|
|
},
|
|
}),
|
|
).toEqual([
|
|
{
|
|
dependencyName: "plain-crypto-js",
|
|
declaredAs: "@scope/parent > safe-name",
|
|
field: "overrides",
|
|
},
|
|
]);
|
|
});
|
|
|
|
it("pins the axios override to an exact version", () => {
|
|
const manifest = readRootManifest();
|
|
expect(manifest.overrides?.axios).toMatch(/^\d+\.\d+\.\d+$/);
|
|
expect(manifest.pnpm?.overrides?.axios).toMatch(/^\d+\.\d+\.\d+$/);
|
|
});
|
|
|
|
it("finds blocked package directories under node_modules regardless of node_modules casing", () => {
|
|
expect(
|
|
findBlockedNodeModulesDirectory({
|
|
directoryRelativePath: "vendor/Node_Modules/plain-crypto-js",
|
|
}),
|
|
).toEqual({
|
|
dependencyName: "plain-crypto-js",
|
|
directoryRelativePath: "vendor/Node_Modules/plain-crypto-js",
|
|
});
|
|
});
|
|
|
|
it("finds blocked package directories regardless of blocked package segment casing", () => {
|
|
expect(
|
|
findBlockedNodeModulesDirectory({
|
|
directoryRelativePath: "vendor/node_modules/Plain-Crypto-Js",
|
|
}),
|
|
).toEqual({
|
|
dependencyName: "Plain-Crypto-Js",
|
|
directoryRelativePath: "vendor/node_modules/Plain-Crypto-Js",
|
|
});
|
|
});
|
|
|
|
it("finds blocked package file aliases under node_modules regardless of casing", () => {
|
|
expect(
|
|
findBlockedNodeModulesFileAlias({
|
|
fileRelativePath: "vendor/Node_Modules/Plain-Crypto-Js.Js",
|
|
}),
|
|
).toEqual({
|
|
dependencyName: "Plain-Crypto-Js",
|
|
fileRelativePath: "vendor/Node_Modules/Plain-Crypto-Js.Js",
|
|
});
|
|
});
|
|
|
|
it("finds blocked extensionless package file aliases under node_modules", () => {
|
|
expect(
|
|
findBlockedNodeModulesFileAlias({
|
|
fileRelativePath: "vendor/Node_Modules/Plain-Crypto-Js",
|
|
}),
|
|
).toEqual({
|
|
dependencyName: "Plain-Crypto-Js",
|
|
fileRelativePath: "vendor/Node_Modules/Plain-Crypto-Js",
|
|
});
|
|
});
|
|
|
|
it("finds blocked package directories anywhere in a resolved path", () => {
|
|
expect(
|
|
findBlockedPackageDirectoryInPath({
|
|
pathRelativeToRoot: "vendor/Plain-Crypto-Js/dist/index.js",
|
|
}),
|
|
).toEqual({
|
|
dependencyName: "Plain-Crypto-Js",
|
|
directoryRelativePath: "vendor/Plain-Crypto-Js/dist/index.js",
|
|
});
|
|
});
|
|
|
|
it("finds blocked package file aliases anywhere in a resolved path", () => {
|
|
expect(
|
|
findBlockedPackageFileAliasInPath({
|
|
pathRelativeToRoot: "vendor/Plain-Crypto-Js.Js",
|
|
}),
|
|
).toEqual({
|
|
dependencyName: "Plain-Crypto-Js",
|
|
fileRelativePath: "vendor/Plain-Crypto-Js.Js",
|
|
});
|
|
});
|
|
|
|
it("does not treat similarly named non-node_modules segments as package-resolution paths", () => {
|
|
expect(
|
|
findBlockedNodeModulesDirectory({
|
|
directoryRelativePath: "vendor/node_modules_backup/plain-crypto-js",
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("does not treat similarly named non-node_modules file aliases as package-resolution paths", () => {
|
|
expect(
|
|
findBlockedNodeModulesFileAlias({
|
|
fileRelativePath: "vendor/plain-crypto-js.js",
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("does not treat dotted non-loadable file aliases as blocked package paths", () => {
|
|
expect(
|
|
findBlockedNodeModulesFileAlias({
|
|
fileRelativePath: "vendor/node_modules/plain-crypto-js.txt",
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("does not treat similarly named non-package paths as blocked package directories", () => {
|
|
expect(
|
|
findBlockedPackageDirectoryInPath({
|
|
pathRelativeToRoot: "vendor/safe-plain-crypto-js-notes/index.js",
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("does not flag the unscoped name segment from an allowed scoped package path", () => {
|
|
expect(
|
|
findBlockedPackageDirectoryInPath({
|
|
pathRelativeToRoot: "vendor/@scope/plain-crypto-js/dist/index.js",
|
|
}),
|
|
).toBeUndefined();
|
|
});
|
|
|
|
it("keeps blocked packages out of the root manifest", () => {
|
|
const manifest = readRootManifest();
|
|
expect(findBlockedManifestDependencies(manifest)).toEqual([]);
|
|
});
|
|
|
|
it("keeps blocked packages out of the lockfile graph", () => {
|
|
const lockfile = readRootLockfile();
|
|
for (const packageName of blockedInstallDependencyPackageNames) {
|
|
expect(lockfile).not.toContain(`\n ${packageName}@`);
|
|
expect(lockfile).not.toContain(`\n ${packageName}: `);
|
|
}
|
|
});
|
|
});
|