Files
clawsec/skills/clawsec-suite/test/guarded_install.test.mjs
T
davida-ps c9a66d5c99 Extract Shared Test Harness Module from 9 Test Files (#85)
* refactor: extract shared test harness module from 9 test files

Extract duplicated test utilities into a reusable test_harness.mjs module
to eliminate ~200-250 lines of boilerplate code across test files.

Changes:
- Create skills/clawsec-suite/test/lib/test_harness.mjs with:
  - Test reporting: pass(), fail(), report(), exitWithResults()
  - Crypto utilities: generateEd25519KeyPair(), signPayload()
  - Temp directory: createTempDir() with cleanup
  - Environment helpers: withEnv() for isolated env vars
  - Test runner factory: createTestRunner() for isolated counters

- Refactor 9 test files to use shared harness:
  - feed_verification.test.mjs
  - guarded_install.test.mjs
  - skill_catalog_discovery.test.mjs
  - advisory_suppression.test.mjs
  - advisory_application_scope.test.mjs
  - path_resolution.test.mjs
  - fuzz_properties.test.mjs
  - suppression_config.test.mjs
  - render_report_suppression.test.mjs

Benefits:
- Single source of truth for test utilities
- Consistent test reporting across all files
- Easier to add new test files
- Reduced maintenance burden

Verification:
- All 80 tests pass (15+8+3+15+4+6+1+17+11)
- Zero ESLint warnings
- No behavior changes - only code deduplication
- Cross-skill module sharing works (openclaw-audit-watchdog → clawsec-suite)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: update minimatch override to 10.2.4 to resolve ReDoS vulnerabilities

Bump minimatch from 10.2.1 to 10.2.4 in overrides to fix 10 high-severity
ReDoS vulnerabilities (GHSA-7r86-cg39-jmmj, GHSA-23c5-xmqv-rm74).
Also add .venv/ to ESLint ignores to prevent linting Python venv files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-27 09:20:36 +02:00

409 lines
14 KiB
JavaScript

#!/usr/bin/env node
/**
* Guarded skill install tests for clawsec-suite.
*
* Tests cover:
* - Conservative matching when version is omitted
* - Precise version matching when version is provided
* - Exit code 42 for advisory match requiring confirmation
* - High-risk advisory detection
*
* Run: node skills/clawsec-suite/test/guarded_install.test.mjs
*/
import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import { fileURLToPath } from "node:url";
import {
pass,
fail,
report,
exitWithResults,
generateEd25519KeyPair,
signPayload,
} from "./lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SCRIPT_PATH = path.resolve(__dirname, "..", "scripts", "guarded_skill_install.mjs");
let tempDir;
function createFeed(advisories) {
return JSON.stringify(
{
version: "1.0.0",
updated: "2026-02-08T12:00:00Z",
advisories,
},
null,
2,
);
}
function createChecksumManifest(files) {
const checksums = {};
for (const [name, content] of Object.entries(files)) {
checksums[name] = crypto.createHash("sha256").update(content).digest("hex");
}
return JSON.stringify(
{
schema_version: "1.0",
algorithm: "sha256",
files: checksums,
},
null,
2,
);
}
async function setupTestDir() {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawsec-install-test-"));
}
async function cleanupTestDir() {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
}
async function setupSignedFeed(advisories, keyPair) {
const feedContent = createFeed(advisories);
const feedSignature = signPayload(feedContent, keyPair.privateKeyPem);
const checksumManifest = createChecksumManifest({
"feed.json": feedContent,
"feed.json.sig": feedSignature + "\n",
"feed-signing-public.pem": keyPair.publicKeyPem,
});
const checksumSignature = signPayload(checksumManifest, keyPair.privateKeyPem);
const advisoriesDir = path.join(tempDir, "advisories");
await fs.mkdir(advisoriesDir, { recursive: true });
await fs.writeFile(path.join(advisoriesDir, "feed.json"), feedContent);
await fs.writeFile(path.join(advisoriesDir, "feed.json.sig"), feedSignature + "\n");
await fs.writeFile(path.join(advisoriesDir, "checksums.json"), checksumManifest);
await fs.writeFile(path.join(advisoriesDir, "checksums.json.sig"), checksumSignature + "\n");
await fs.writeFile(path.join(advisoriesDir, "feed-signing-public.pem"), keyPair.publicKeyPem);
return advisoriesDir;
}
function runGuardedInstall(args, env) {
return new Promise((resolve) => {
const proc = spawn("node", [SCRIPT_PATH, ...args], {
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data) => {
stdout += data.toString();
});
proc.stderr.on("data", (data) => {
stderr += data.toString();
});
proc.on("close", (code) => {
resolve({ code, stdout, stderr });
});
});
}
// -----------------------------------------------------------------------------
// Test: Conservative matching when version is omitted
// -----------------------------------------------------------------------------
async function testConservativeMatchingWithoutVersion() {
const testName = "guarded_install: conservative matching without version triggers advisory";
try {
const keyPair = generateEd25519KeyPair();
const advisoriesDir = await setupSignedFeed(
[
{
id: "TEST-001",
severity: "high",
affected: ["test-skill@1.0.0", "test-skill@1.0.1"],
},
],
keyPair,
);
const result = await runGuardedInstall(["--skill", "test-skill", "--dry-run"], {
CLAWSEC_LOCAL_FEED: path.join(advisoriesDir, "feed.json"),
CLAWSEC_LOCAL_FEED_SIG: path.join(advisoriesDir, "feed.json.sig"),
CLAWSEC_LOCAL_FEED_CHECKSUMS: path.join(advisoriesDir, "checksums.json"),
CLAWSEC_LOCAL_FEED_CHECKSUMS_SIG: path.join(advisoriesDir, "checksums.json.sig"),
CLAWSEC_FEED_PUBLIC_KEY: path.join(advisoriesDir, "feed-signing-public.pem"),
CLAWSEC_FEED_URL: "file:///nonexistent", // Force local fallback
});
if (result.code === 42 && result.stdout.includes("Conservative")) {
pass(testName);
} else {
fail(testName, `Expected exit 42 with conservative message, got ${result.code}: ${result.stdout}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Precise version matching
// -----------------------------------------------------------------------------
async function testPreciseVersionMatching() {
const testName = "guarded_install: precise version matching only matches exact version";
try {
const keyPair = generateEd25519KeyPair();
const advisoriesDir = await setupSignedFeed(
[
{
id: "TEST-001",
severity: "high",
affected: ["test-skill@1.0.0"],
},
],
keyPair,
);
// Version 2.0.0 should NOT match advisory for 1.0.0
const result = await runGuardedInstall(
["--skill", "test-skill", "--version", "2.0.0", "--dry-run"],
{
CLAWSEC_LOCAL_FEED: path.join(advisoriesDir, "feed.json"),
CLAWSEC_LOCAL_FEED_SIG: path.join(advisoriesDir, "feed.json.sig"),
CLAWSEC_LOCAL_FEED_CHECKSUMS: path.join(advisoriesDir, "checksums.json"),
CLAWSEC_LOCAL_FEED_CHECKSUMS_SIG: path.join(advisoriesDir, "checksums.json.sig"),
CLAWSEC_FEED_PUBLIC_KEY: path.join(advisoriesDir, "feed-signing-public.pem"),
CLAWSEC_FEED_URL: "file:///nonexistent",
},
);
if (result.code === 0 && !result.stdout.includes("Advisory matches")) {
pass(testName);
} else {
fail(testName, `Expected exit 0 without match, got ${result.code}: ${result.stdout}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Version match triggers confirmation requirement
// -----------------------------------------------------------------------------
async function testVersionMatchTriggersConfirmation() {
const testName = "guarded_install: matching version triggers exit 42";
try {
const keyPair = generateEd25519KeyPair();
const advisoriesDir = await setupSignedFeed(
[
{
id: "TEST-001",
severity: "high",
affected: ["test-skill@1.0.0"],
},
],
keyPair,
);
const result = await runGuardedInstall(
["--skill", "test-skill", "--version", "1.0.0", "--dry-run"],
{
CLAWSEC_LOCAL_FEED: path.join(advisoriesDir, "feed.json"),
CLAWSEC_LOCAL_FEED_SIG: path.join(advisoriesDir, "feed.json.sig"),
CLAWSEC_LOCAL_FEED_CHECKSUMS: path.join(advisoriesDir, "checksums.json"),
CLAWSEC_LOCAL_FEED_CHECKSUMS_SIG: path.join(advisoriesDir, "checksums.json.sig"),
CLAWSEC_FEED_PUBLIC_KEY: path.join(advisoriesDir, "feed-signing-public.pem"),
CLAWSEC_FEED_URL: "file:///nonexistent",
},
);
if (result.code === 42 && result.stdout.includes("Advisory matches")) {
pass(testName);
} else {
fail(testName, `Expected exit 42 with advisory match, got ${result.code}: ${result.stdout}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: --confirm-advisory allows proceeding
// -----------------------------------------------------------------------------
async function testConfirmAdvisoryAllowsProceeding() {
const testName = "guarded_install: --confirm-advisory with --dry-run proceeds";
try {
const keyPair = generateEd25519KeyPair();
const advisoriesDir = await setupSignedFeed(
[
{
id: "TEST-001",
severity: "high",
affected: ["test-skill@1.0.0"],
},
],
keyPair,
);
const result = await runGuardedInstall(
["--skill", "test-skill", "--version", "1.0.0", "--confirm-advisory", "--dry-run"],
{
CLAWSEC_LOCAL_FEED: path.join(advisoriesDir, "feed.json"),
CLAWSEC_LOCAL_FEED_SIG: path.join(advisoriesDir, "feed.json.sig"),
CLAWSEC_LOCAL_FEED_CHECKSUMS: path.join(advisoriesDir, "checksums.json"),
CLAWSEC_LOCAL_FEED_CHECKSUMS_SIG: path.join(advisoriesDir, "checksums.json.sig"),
CLAWSEC_FEED_PUBLIC_KEY: path.join(advisoriesDir, "feed-signing-public.pem"),
CLAWSEC_FEED_URL: "file:///nonexistent",
},
);
if (result.code === 0 && result.stdout.includes("Dry run")) {
pass(testName);
} else {
fail(testName, `Expected exit 0 with dry run message, got ${result.code}: ${result.stdout}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: allowUnsigned bypass warning
// -----------------------------------------------------------------------------
async function testAllowUnsignedWarning() {
const testName = "guarded_install: CLAWSEC_ALLOW_UNSIGNED_FEED shows warning";
try {
// Create unsigned feed (no signatures)
const feedContent = createFeed([]);
const feedPath = path.join(tempDir, "unsigned-feed.json");
await fs.writeFile(feedPath, feedContent);
const result = await runGuardedInstall(["--skill", "test-skill", "--dry-run"], {
CLAWSEC_LOCAL_FEED: feedPath,
CLAWSEC_ALLOW_UNSIGNED_FEED: "1",
CLAWSEC_VERIFY_CHECKSUM_MANIFEST: "0",
CLAWSEC_FEED_URL: "file:///nonexistent",
});
if (result.stderr.includes("CLAWSEC_ALLOW_UNSIGNED_FEED")) {
pass(testName);
} else {
fail(testName, `Expected unsigned mode warning, got: ${result.stderr}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Missing signature fails without allowUnsigned
// -----------------------------------------------------------------------------
async function testMissingSignatureFails() {
const testName = "guarded_install: missing signature fails without allowUnsigned";
try {
const feedContent = createFeed([]);
const feedPath = path.join(tempDir, "nosig-feed.json");
await fs.writeFile(feedPath, feedContent);
const result = await runGuardedInstall(["--skill", "test-skill", "--dry-run"], {
CLAWSEC_LOCAL_FEED: feedPath,
CLAWSEC_FEED_URL: "file:///nonexistent",
});
if (result.code === 1) {
pass(testName);
} else {
fail(testName, `Expected exit 1 for missing signature, got ${result.code}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: $HOME path expansion for local feed paths
// -----------------------------------------------------------------------------
async function testHomeExpansionForLocalFeedPaths() {
const testName = "guarded_install: expands $HOME in local feed env paths";
try {
const keyPair = generateEd25519KeyPair();
await setupSignedFeed([], keyPair);
const result = await runGuardedInstall(["--skill", "test-skill", "--dry-run"], {
HOME: tempDir,
CLAWSEC_LOCAL_FEED: "$HOME/advisories/feed.json",
CLAWSEC_LOCAL_FEED_SIG: "$HOME/advisories/feed.json.sig",
CLAWSEC_LOCAL_FEED_CHECKSUMS: "$HOME/advisories/checksums.json",
CLAWSEC_LOCAL_FEED_CHECKSUMS_SIG: "$HOME/advisories/checksums.json.sig",
CLAWSEC_FEED_PUBLIC_KEY: "$HOME/advisories/feed-signing-public.pem",
CLAWSEC_FEED_URL: "file:///nonexistent",
});
if (result.code === 0 && result.stdout.includes("Advisory source: local:")) {
pass(testName);
} else {
fail(testName, `Expected local feed success, got ${result.code}: ${result.stdout} ${result.stderr}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: escaped home token is rejected
// -----------------------------------------------------------------------------
async function testEscapedHomeTokenRejected() {
const testName = "guarded_install: escaped $HOME token is rejected";
try {
const result = await runGuardedInstall(["--skill", "test-skill", "--dry-run"], {
CLAWSEC_LOCAL_FEED: "\\$HOME/advisories/feed.json",
});
if (result.code === 1 && result.stderr.includes("Unexpanded home token")) {
pass(testName);
} else {
fail(testName, `Expected token validation error, got ${result.code}: ${result.stderr || result.stdout}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Main test runner
// -----------------------------------------------------------------------------
async function runTests() {
console.log("=== ClawSec Guarded Install Tests ===\n");
await setupTestDir();
try {
await testConservativeMatchingWithoutVersion();
await testPreciseVersionMatching();
await testVersionMatchTriggersConfirmation();
await testConfirmAdvisoryAllowsProceeding();
await testAllowUnsignedWarning();
await testMissingSignatureFails();
await testHomeExpansionForLocalFeedPaths();
await testEscapedHomeTokenRejected();
} finally {
await cleanupTestDir();
}
report();
exitWithResults();
}
runTests().catch((error) => {
console.error("Test runner failed:", error);
process.exit(1);
});