Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ __pycache__/
*.sln
*.sw?
clawsec-signing-private.pem

# Auto Claude generated files
.auto-claude/
.auto-claude-security.json
.auto-claude-status
.claude_settings.json
.worktrees/
.security-key
logs/security/
2 changes: 1 addition & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,6 @@ export default [
}
},
{
ignores: ['dist/', 'node_modules/', '*.config.js', 'public/']
ignores: ['dist/', 'node_modules/', '*.config.js', 'public/', '.venv/']
}
];
327 changes: 94 additions & 233 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@
"ajv": "6.14.0",
"balanced-match": "4.0.3",
"brace-expansion": "5.0.2",
"minimatch": "10.2.1"
"minimatch": "10.2.4"
}
}
21 changes: 3 additions & 18 deletions skills/clawsec-suite/test/advisory_application_scope.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,12 @@

import path from "node:path";
import { fileURLToPath } from "node:url";
import { pass, fail, report, exitWithResults } from "./lib/test_harness.mjs";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const LIB_PATH = path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib");
const { advisoryAppliesToOpenclaw } = await import(`${LIB_PATH}/advisory_scope.mjs`);

let passCount = 0;
let failCount = 0;

function pass(name) {
passCount += 1;
console.log(`\u2713 ${name}`);
}

function fail(name, error) {
failCount += 1;
console.error(`\u2717 ${name}`);
console.error(` ${String(error)}`);
}

function testFindMatchesFiltersByApplicationScope() {
const testName = "advisoryAppliesToOpenclaw: openclaw + legacy advisories are considered";

Expand Down Expand Up @@ -89,10 +76,8 @@ function runTests() {
testFindMatchesAcceptsApplicationArray();
testInvalidApplicationValueFallsBackCompat();

console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}

runTests();
51 changes: 12 additions & 39 deletions skills/clawsec-suite/test/advisory_suppression.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
*/

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { pass, fail, report, exitWithResults, 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");
Expand All @@ -27,29 +27,6 @@ const { isAdvisorySuppressed, loadAdvisorySuppression } = await import(
);

let tempDir;
let passCount = 0;
let failCount = 0;

function pass(name) {
passCount++;
console.log(`\u2713 ${name}`);
}

function fail(name, error) {
failCount++;
console.error(`\u2717 ${name}`);
console.error(` ${String(error)}`);
}

async function setupTestDir() {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "advisory-suppression-test-"));
}

async function cleanupTestDir() {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
}

function makeMatch(advisoryId, skillName, version = "1.0.0") {
return {
Expand Down Expand Up @@ -190,7 +167,7 @@ async function testMissingAdvisoryId() {
async function testLoadWithAdvisorySentinel() {
const testName = "loadAdvisorySuppression: loads config with advisory sentinel";
try {
const configFile = path.join(tempDir, "advisory-config.json");
const configFile = path.join(tempDir.path, "advisory-config.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["advisory"],
suppressions: [{
Expand All @@ -215,7 +192,7 @@ async function testLoadWithAdvisorySentinel() {
async function testLoadWithMissingSentinel() {
const testName = "loadAdvisorySuppression: missing sentinel returns empty config";
try {
const configFile = path.join(tempDir, "no-sentinel.json");
const configFile = path.join(tempDir.path, "no-sentinel.json");
await fs.writeFile(configFile, JSON.stringify({
suppressions: [{
checkId: "CVE-2026-25593",
Expand All @@ -239,7 +216,7 @@ async function testLoadWithMissingSentinel() {
async function testLoadWithAuditOnlySentinel() {
const testName = "loadAdvisorySuppression: audit-only sentinel returns empty for advisory";
try {
const configFile = path.join(tempDir, "audit-only.json");
const configFile = path.join(tempDir.path, "audit-only.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["audit"],
suppressions: [{
Expand All @@ -264,7 +241,7 @@ async function testLoadWithAuditOnlySentinel() {
async function testLoadWithBothSentinels() {
const testName = "loadAdvisorySuppression: both audit+advisory sentinels activates advisory";
try {
const configFile = path.join(tempDir, "both-sentinel.json");
const configFile = path.join(tempDir.path, "both-sentinel.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["audit", "advisory"],
suppressions: [{
Expand All @@ -289,7 +266,7 @@ async function testLoadWithBothSentinels() {
async function testLoadNonexistentExplicitPath() {
const testName = "loadAdvisorySuppression: explicit nonexistent path throws";
try {
await loadAdvisorySuppression(path.join(tempDir, "does-not-exist.json"));
await loadAdvisorySuppression(path.join(tempDir.path, "does-not-exist.json"));
fail(testName, "Expected error for nonexistent explicit path");
} catch (error) {
if (String(error).includes("not found")) {
Expand Down Expand Up @@ -328,7 +305,7 @@ async function testLoadNoConfigReturnsEmpty() {
async function testEnvPathHomeExpansion() {
const testName = "loadAdvisorySuppression: OPENCLAW_AUDIT_CONFIG expands $HOME";
try {
const configFile = path.join(tempDir, "env-home.json");
const configFile = path.join(tempDir.path, "env-home.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["advisory"],
suppressions: [{
Expand All @@ -341,7 +318,7 @@ async function testEnvPathHomeExpansion() {

const savedConfig = process.env.OPENCLAW_AUDIT_CONFIG;
const savedHome = process.env.HOME;
process.env.HOME = tempDir;
process.env.HOME = tempDir.path;
process.env.OPENCLAW_AUDIT_CONFIG = "$HOME/env-home.json";
try {
const config = await loadAdvisorySuppression();
Expand Down Expand Up @@ -390,7 +367,7 @@ async function testEscapedHomeTokenRejected() {
async function runAllTests() {
console.log("=== Advisory Suppression Tests ===\n");

await setupTestDir();
tempDir = await createTempDir();

try {
// isAdvisorySuppressed tests
Expand All @@ -412,15 +389,11 @@ async function runAllTests() {
await testEnvPathHomeExpansion();
await testEscapedHomeTokenRejected();
} finally {
await cleanupTestDir();
await tempDir.cleanup();
}

console.log("");
console.log(`=== Results: ${passCount} passed, ${failCount} failed ===`);

if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}

runAllTests().catch((err) => {
Expand Down
93 changes: 33 additions & 60 deletions skills/clawsec-suite/test/feed_verification.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@

import crypto from "node:crypto";
import fs from "node:fs/promises";
import os from "node:os";
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");
Expand All @@ -26,33 +34,7 @@ const { verifySignedPayload, loadLocalFeed, isValidFeedPayload } = await import(
`${LIB_PATH}/feed.mjs`
);

let tempDir;
let passCount = 0;
let failCount = 0;

function pass(name) {
passCount++;
console.log(`✓ ${name}`);
}

function fail(name, error) {
failCount++;
console.error(`✗ ${name}`);
console.error(` ${String(error)}`);
}

function generateEd25519KeyPair() {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
return { publicKeyPem, privateKeyPem };
}

function signPayload(data, privateKeyPem) {
const privateKey = crypto.createPrivateKey(privateKeyPem);
const signature = crypto.sign(null, Buffer.from(data, "utf8"), privateKey);
return signature.toString("base64");
}
let tempDirCleanup;

function createValidFeed() {
return JSON.stringify(
Expand Down Expand Up @@ -88,16 +70,6 @@ function createChecksumManifest(files) {
);
}

async function setupTestDir() {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawsec-test-"));
}

async function cleanupTestDir() {
if (tempDir) {
await fs.rm(tempDir, { recursive: true, force: true });
}
}

// -----------------------------------------------------------------------------
// Test: verifySignedPayload - valid signature
// -----------------------------------------------------------------------------
Expand Down Expand Up @@ -252,11 +224,11 @@ async function testLoadLocalFeed_ValidSignedFeed() {
const checksumSignature = signPayload(checksumManifest, privateKeyPem);

// Write files
const feedPath = path.join(tempDir, "feed.json");
const sigPath = path.join(tempDir, "feed.json.sig");
const checksumPath = path.join(tempDir, "checksums.json");
const checksumSigPath = path.join(tempDir, "checksums.json.sig");
const keyPath = path.join(tempDir, "feed-signing-public.pem");
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");
Expand Down Expand Up @@ -293,7 +265,7 @@ async function testLoadLocalFeed_AdvisoriesPrefixedChecksumKeys() {
const feedContent = createValidFeed();
const feedSignature = signPayload(feedContent, privateKeyPem);

const advisoriesDir = path.join(tempDir, "advisories");
const advisoriesDir = path.join(globalThis.__testTempDir, "advisories");
await fs.mkdir(advisoriesDir, { recursive: true });

const checksumManifest = createChecksumManifest({
Expand Down Expand Up @@ -347,8 +319,8 @@ async function testLoadLocalFeed_TamperedFeedFails() {
// Tamper with feed after signing
const tamperedFeed = feedContent.replace("TEST-001", "TAMPERED-001");

const feedPath = path.join(tempDir, "tampered-feed.json");
const sigPath = path.join(tempDir, "tampered-feed.json.sig");
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");
Expand Down Expand Up @@ -383,8 +355,8 @@ async function testLoadLocalFeed_MissingSignatureFails() {
const { publicKeyPem } = generateEd25519KeyPair();
const feedContent = createValidFeed();

const feedPath = path.join(tempDir, "nosig-feed.json");
const sigPath = path.join(tempDir, "nosig-feed.json.sig");
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
Expand Down Expand Up @@ -418,7 +390,7 @@ async function testLoadLocalFeed_AllowUnsignedBypasses() {
try {
const feedContent = createValidFeed();

const feedPath = path.join(tempDir, "unsigned-feed.json");
const feedPath = path.join(globalThis.__testTempDir, "unsigned-feed.json");
await fs.writeFile(feedPath, feedContent);

const feed = await loadLocalFeed(feedPath, {
Expand Down Expand Up @@ -462,10 +434,10 @@ async function testLoadLocalFeed_ChecksumMismatchFails() {
);
const checksumSignature = signPayload(badChecksumManifest, privateKeyPem);

const feedPath = path.join(tempDir, "badcs-feed.json");
const sigPath = path.join(tempDir, "badcs-feed.json.sig");
const checksumPath = path.join(tempDir, "badcs-checksums.json");
const checksumSigPath = path.join(tempDir, "badcs-checksums.json.sig");
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");
Expand Down Expand Up @@ -580,7 +552,11 @@ async function testIsValidFeedPayload_AdvisoryMissingId() {
async function runTests() {
console.log("=== ClawSec Feed Verification Tests ===\n");

await setupTestDir();
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
Expand All @@ -604,14 +580,11 @@ async function runTests() {
await testIsValidFeedPayload_MissingVersion();
await testIsValidFeedPayload_AdvisoryMissingId();
} finally {
await cleanupTestDir();
await tempDirCleanup();
}

console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);

if (failCount > 0) {
process.exit(1);
}
report();
exitWithResults();
}

runTests().catch((error) => {
Expand Down
13 changes: 8 additions & 5 deletions skills/clawsec-suite/test/fuzz_properties.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
* Run: node skills/clawsec-suite/test/fuzz_properties.test.mjs
*/

import { pass, fail, report, exitWithResults } from "./lib/test_harness.mjs";
import { runFuzzProperties } from "./fuzz_properties.js";

console.log("=== ClawSec Fast-Check Fuzz Properties ===\n");

try {
console.log("=== ClawSec Fast-Check Fuzz Properties ===\n");
runFuzzProperties();
console.log("=== Results: all fuzz properties passed ===");
pass("Property-based fuzz tests");
} catch (error) {
console.error("Fuzz property test failed:");
console.error(error);
process.exit(1);
fail("Property-based fuzz tests", error);
}

report();
exitWithResults();
Loading
Loading