Files
clawsec/skills/clawsec-suite/test/feed_verification.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

594 lines
19 KiB
JavaScript

#!/usr/bin/env node
/**
* Feed verification tests for clawsec-suite.
*
* Tests cover:
* - Signature verification success/failure/tampered cases
* - Checksum manifest verification success/failure/tampered cases
* - Fail-closed behavior when signatures are missing/invalid
* - Temporary compatibility flag behavior
*
* Run: node skills/clawsec-suite/test/feed_verification.test.mjs
*/
import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
pass,
fail,
report,
exitWithResults,
generateEd25519KeyPair,
signPayload,
createTempDir,
} from "./lib/test_harness.mjs";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIB_PATH = path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib");
// Dynamic import to ensure we test the actual module
const { verifySignedPayload, loadLocalFeed, isValidFeedPayload } = await import(
`${LIB_PATH}/feed.mjs`
);
let tempDirCleanup;
function createValidFeed() {
return JSON.stringify(
{
version: "1.0.0",
updated: "2026-02-08T12:00:00Z",
advisories: [
{
id: "TEST-001",
severity: "high",
affected: ["test-skill@1.0.0"],
},
],
},
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,
);
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - valid signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_ValidSignature() {
const testName = "verifySignedPayload: valid signature passes";
try {
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const payload = "test payload content";
const signature = signPayload(payload, privateKeyPem);
const result = verifySignedPayload(payload, signature, publicKeyPem);
if (result === true) {
pass(testName);
} else {
fail(testName, "Expected true, got false");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - invalid signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_InvalidSignature() {
const testName = "verifySignedPayload: invalid signature fails";
try {
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const payload = "test payload content";
const signature = signPayload(payload, privateKeyPem);
// Tamper with payload
const tamperedPayload = "TAMPERED payload content";
const result = verifySignedPayload(tamperedPayload, signature, publicKeyPem);
if (result === false) {
pass(testName);
} else {
fail(testName, "Expected false for tampered payload, got true");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - wrong key
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_WrongKey() {
const testName = "verifySignedPayload: wrong key fails";
try {
const keyPair1 = generateEd25519KeyPair();
const keyPair2 = generateEd25519KeyPair();
const payload = "test payload content";
const signature = signPayload(payload, keyPair1.privateKeyPem);
// Verify with different public key
const result = verifySignedPayload(payload, signature, keyPair2.publicKeyPem);
if (result === false) {
pass(testName);
} else {
fail(testName, "Expected false for wrong key, got true");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - malformed signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_MalformedSignature() {
const testName = "verifySignedPayload: malformed signature fails";
try {
const { publicKeyPem } = generateEd25519KeyPair();
const payload = "test payload content";
const result = verifySignedPayload(payload, "not-valid-base64!!!", publicKeyPem);
if (result === false) {
pass(testName);
} else {
fail(testName, "Expected false for malformed signature, got true");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - empty signature
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_EmptySignature() {
const testName = "verifySignedPayload: empty signature fails";
try {
const { publicKeyPem } = generateEd25519KeyPair();
const payload = "test payload content";
const result = verifySignedPayload(payload, "", publicKeyPem);
if (result === false) {
pass(testName);
} else {
fail(testName, "Expected false for empty signature, got true");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: verifySignedPayload - JSON-wrapped signature format
// -----------------------------------------------------------------------------
async function testVerifySignedPayload_JsonWrappedSignature() {
const testName = "verifySignedPayload: JSON-wrapped signature passes";
try {
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const payload = "test payload content";
const signatureBase64 = signPayload(payload, privateKeyPem);
const jsonWrapped = JSON.stringify({ signature: signatureBase64 });
const result = verifySignedPayload(payload, jsonWrapped, publicKeyPem);
if (result === true) {
pass(testName);
} else {
fail(testName, "Expected true for JSON-wrapped signature, got false");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - valid signed feed
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_ValidSignedFeed() {
const testName = "loadLocalFeed: valid signed feed loads successfully";
try {
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();
const feedSignature = signPayload(feedContent, privateKeyPem);
// Create checksum manifest
const checksumManifest = createChecksumManifest({
"feed.json": feedContent,
"feed.json.sig": feedSignature + "\n",
"feed-signing-public.pem": publicKeyPem,
});
const checksumSignature = signPayload(checksumManifest, privateKeyPem);
// Write files
const feedPath = path.join(globalThis.__testTempDir, "feed.json");
const sigPath = path.join(globalThis.__testTempDir, "feed.json.sig");
const checksumPath = path.join(globalThis.__testTempDir, "checksums.json");
const checksumSigPath = path.join(globalThis.__testTempDir, "checksums.json.sig");
const keyPath = path.join(globalThis.__testTempDir, "feed-signing-public.pem");
await fs.writeFile(feedPath, feedContent);
await fs.writeFile(sigPath, feedSignature + "\n");
await fs.writeFile(checksumPath, checksumManifest);
await fs.writeFile(checksumSigPath, checksumSignature + "\n");
await fs.writeFile(keyPath, publicKeyPem);
const feed = await loadLocalFeed(feedPath, {
signaturePath: sigPath,
checksumsPath: checksumPath,
checksumsSignaturePath: checksumSigPath,
publicKeyPem,
verifyChecksumManifest: true,
checksumPublicKeyEntry: "feed-signing-public.pem",
});
if (feed && feed.version === "1.0.0" && feed.advisories.length === 1) {
pass(testName);
} else {
fail(testName, "Feed did not load with expected content");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - supports advisories/* checksum keys
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_AdvisoriesPrefixedChecksumKeys() {
const testName = "loadLocalFeed: advisories/* checksum keys are accepted";
try {
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();
const feedSignature = signPayload(feedContent, privateKeyPem);
const advisoriesDir = path.join(globalThis.__testTempDir, "advisories");
await fs.mkdir(advisoriesDir, { recursive: true });
const checksumManifest = createChecksumManifest({
"advisories/feed.json": feedContent,
"advisories/feed.json.sig": feedSignature + "\n",
"advisories/feed-signing-public.pem": publicKeyPem,
});
const checksumSignature = signPayload(checksumManifest, privateKeyPem);
const feedPath = path.join(advisoriesDir, "feed.json");
const sigPath = path.join(advisoriesDir, "feed.json.sig");
const checksumPath = path.join(advisoriesDir, "checksums.json");
const checksumSigPath = path.join(advisoriesDir, "checksums.json.sig");
const keyPath = path.join(advisoriesDir, "feed-signing-public.pem");
await fs.writeFile(feedPath, feedContent);
await fs.writeFile(sigPath, feedSignature + "\n");
await fs.writeFile(checksumPath, checksumManifest);
await fs.writeFile(checksumSigPath, checksumSignature + "\n");
await fs.writeFile(keyPath, publicKeyPem);
const feed = await loadLocalFeed(feedPath, {
signaturePath: sigPath,
checksumsPath: checksumPath,
checksumsSignaturePath: checksumSigPath,
publicKeyPem,
verifyChecksumManifest: true,
checksumPublicKeyEntry: path.basename(keyPath),
});
if (feed && feed.version === "1.0.0" && feed.advisories.length === 1) {
pass(testName);
} else {
fail(testName, "Feed did not load with advisories/* checksum keys");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - tampered feed fails (fail-closed)
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_TamperedFeedFails() {
const testName = "loadLocalFeed: tampered feed fails (fail-closed)";
try {
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();
const feedSignature = signPayload(feedContent, privateKeyPem);
// Tamper with feed after signing
const tamperedFeed = feedContent.replace("TEST-001", "TAMPERED-001");
const feedPath = path.join(globalThis.__testTempDir, "tampered-feed.json");
const sigPath = path.join(globalThis.__testTempDir, "tampered-feed.json.sig");
await fs.writeFile(feedPath, tamperedFeed);
await fs.writeFile(sigPath, feedSignature + "\n");
let didFail = false;
try {
await loadLocalFeed(feedPath, {
signaturePath: sigPath,
publicKeyPem,
verifyChecksumManifest: false,
});
} catch {
didFail = true;
}
if (didFail) {
pass(testName);
} else {
fail(testName, "Expected failure for tampered feed, but it loaded");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - missing signature fails (fail-closed)
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_MissingSignatureFails() {
const testName = "loadLocalFeed: missing signature fails (fail-closed)";
try {
const { publicKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();
const feedPath = path.join(globalThis.__testTempDir, "nosig-feed.json");
const sigPath = path.join(globalThis.__testTempDir, "nosig-feed.json.sig");
await fs.writeFile(feedPath, feedContent);
// Don't write signature file
let didFail = false;
try {
await loadLocalFeed(feedPath, {
signaturePath: sigPath,
publicKeyPem,
verifyChecksumManifest: false,
});
} catch {
didFail = true;
}
if (didFail) {
pass(testName);
} else {
fail(testName, "Expected failure for missing signature, but it loaded");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - allowUnsigned bypasses verification
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_AllowUnsignedBypasses() {
const testName = "loadLocalFeed: allowUnsigned=true bypasses verification";
try {
const feedContent = createValidFeed();
const feedPath = path.join(globalThis.__testTempDir, "unsigned-feed.json");
await fs.writeFile(feedPath, feedContent);
const feed = await loadLocalFeed(feedPath, {
allowUnsigned: true,
verifyChecksumManifest: false,
});
if (feed && feed.version === "1.0.0") {
pass(testName);
} else {
fail(testName, "Feed did not load with allowUnsigned=true");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: loadLocalFeed - checksum mismatch fails
// -----------------------------------------------------------------------------
async function testLoadLocalFeed_ChecksumMismatchFails() {
const testName = "loadLocalFeed: checksum mismatch fails";
try {
const { publicKeyPem, privateKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();
const feedSignature = signPayload(feedContent, privateKeyPem);
// Create checksum manifest with WRONG hash
const badChecksumManifest = JSON.stringify(
{
schema_version: "1.0",
algorithm: "sha256",
files: {
"feed.json": "0".repeat(64), // Wrong hash
"feed.json.sig":
crypto.createHash("sha256").update(feedSignature + "\n").digest("hex"),
},
},
null,
2,
);
const checksumSignature = signPayload(badChecksumManifest, privateKeyPem);
const feedPath = path.join(globalThis.__testTempDir, "badcs-feed.json");
const sigPath = path.join(globalThis.__testTempDir, "badcs-feed.json.sig");
const checksumPath = path.join(globalThis.__testTempDir, "badcs-checksums.json");
const checksumSigPath = path.join(globalThis.__testTempDir, "badcs-checksums.json.sig");
await fs.writeFile(feedPath, feedContent);
await fs.writeFile(sigPath, feedSignature + "\n");
await fs.writeFile(checksumPath, badChecksumManifest);
await fs.writeFile(checksumSigPath, checksumSignature + "\n");
let didFail = false;
try {
await loadLocalFeed(feedPath, {
signaturePath: sigPath,
checksumsPath: checksumPath,
checksumsSignaturePath: checksumSigPath,
publicKeyPem,
verifyChecksumManifest: true,
});
} catch {
didFail = true;
}
if (didFail) {
pass(testName);
} else {
fail(testName, "Expected failure for checksum mismatch, but it loaded");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - valid feed
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_Valid() {
const testName = "isValidFeedPayload: valid feed passes";
try {
const feed = {
version: "1.0.0",
advisories: [
{
id: "TEST-001",
severity: "high",
affected: ["test-skill@1.0.0"],
},
],
};
if (isValidFeedPayload(feed)) {
pass(testName);
} else {
fail(testName, "Expected valid feed to pass validation");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - missing version fails
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_MissingVersion() {
const testName = "isValidFeedPayload: missing version fails";
try {
const feed = {
advisories: [
{
id: "TEST-001",
severity: "high",
affected: [],
},
],
};
if (!isValidFeedPayload(feed)) {
pass(testName);
} else {
fail(testName, "Expected feed without version to fail validation");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: isValidFeedPayload - advisory missing id fails
// -----------------------------------------------------------------------------
async function testIsValidFeedPayload_AdvisoryMissingId() {
const testName = "isValidFeedPayload: advisory missing id fails";
try {
const feed = {
version: "1.0.0",
advisories: [
{
severity: "high",
affected: [],
},
],
};
if (!isValidFeedPayload(feed)) {
pass(testName);
} else {
fail(testName, "Expected advisory without id to fail validation");
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Main test runner
// -----------------------------------------------------------------------------
async function runTests() {
console.log("=== ClawSec Feed Verification Tests ===\n");
const tempDir = await createTempDir();
tempDirCleanup = tempDir.cleanup;
// Store temp dir path in module scope for tests to access
globalThis.__testTempDir = tempDir.path;
try {
// Signature verification tests
await testVerifySignedPayload_ValidSignature();
await testVerifySignedPayload_InvalidSignature();
await testVerifySignedPayload_WrongKey();
await testVerifySignedPayload_MalformedSignature();
await testVerifySignedPayload_EmptySignature();
await testVerifySignedPayload_JsonWrappedSignature();
// Local feed loading tests
await testLoadLocalFeed_ValidSignedFeed();
await testLoadLocalFeed_AdvisoriesPrefixedChecksumKeys();
await testLoadLocalFeed_TamperedFeedFails();
await testLoadLocalFeed_MissingSignatureFails();
await testLoadLocalFeed_AllowUnsignedBypasses();
await testLoadLocalFeed_ChecksumMismatchFails();
// Feed payload validation tests
await testIsValidFeedPayload_Valid();
await testIsValidFeedPayload_MissingVersion();
await testIsValidFeedPayload_AdvisoryMissingId();
} finally {
await tempDirCleanup();
}
report();
exitWithResults();
}
runTests().catch((error) => {
console.error("Test runner failed:", error);
process.exit(1);
});