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

403 lines
13 KiB
JavaScript

#!/usr/bin/env node
/**
* Advisory suppression tests for clawsec-suite.
*
* Tests cover:
* - isAdvisorySuppressed matching logic (exact checkId + normalized skill name)
* - Partial matches do not suppress (checkId only, skill only)
* - Empty suppressions never suppress
* - loadAdvisorySuppression sentinel gating (enabledFor: ["advisory"])
* - Missing sentinel returns empty config
* - Wrong sentinel (only "audit") returns empty config
*
* Run: node skills/clawsec-suite/test/advisory_suppression.test.mjs
*/
import fs from "node:fs/promises";
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");
const { isAdvisorySuppressed, loadAdvisorySuppression } = await import(
`${LIB_PATH}/suppression.mjs`
);
let tempDir;
function makeMatch(advisoryId, skillName, version = "1.0.0") {
return {
advisory: { id: advisoryId, severity: "high", title: `Advisory ${advisoryId}` },
skill: { name: skillName, dirName: skillName, version },
matchedAffected: [`${skillName}@<=${version}`],
};
}
function makeRules(entries) {
return entries.map(([checkId, skill, reason]) => ({
checkId,
skill,
reason: reason || "Test suppression",
suppressedAt: "2026-02-15",
}));
}
// ---------------------------------------------------------------------------
// isAdvisorySuppressed tests
// ---------------------------------------------------------------------------
async function testExactMatch() {
const testName = "isAdvisorySuppressed: exact match suppresses";
try {
const match = makeMatch("CVE-2026-25593", "clawsec-suite");
const rules = makeRules([["CVE-2026-25593", "clawsec-suite"]]);
if (isAdvisorySuppressed(match, rules) === true) {
pass(testName);
} else {
fail(testName, "Expected suppression but got false");
}
} catch (error) {
fail(testName, error);
}
}
async function testCaseInsensitiveSkillMatch() {
const testName = "isAdvisorySuppressed: case-insensitive skill name match";
try {
const match = makeMatch("CVE-2026-25593", "ClawSec-Suite");
const rules = makeRules([["CVE-2026-25593", "clawsec-suite"]]);
if (isAdvisorySuppressed(match, rules) === true) {
pass(testName);
} else {
fail(testName, "Expected case-insensitive match to suppress");
}
} catch (error) {
fail(testName, error);
}
}
async function testCheckIdMismatch() {
const testName = "isAdvisorySuppressed: checkId mismatch does not suppress";
try {
const match = makeMatch("CVE-2026-99999", "clawsec-suite");
const rules = makeRules([["CVE-2026-25593", "clawsec-suite"]]);
if (isAdvisorySuppressed(match, rules) === false) {
pass(testName);
} else {
fail(testName, "Expected no suppression for mismatched checkId");
}
} catch (error) {
fail(testName, error);
}
}
async function testSkillMismatch() {
const testName = "isAdvisorySuppressed: skill mismatch does not suppress";
try {
const match = makeMatch("CVE-2026-25593", "other-skill");
const rules = makeRules([["CVE-2026-25593", "clawsec-suite"]]);
if (isAdvisorySuppressed(match, rules) === false) {
pass(testName);
} else {
fail(testName, "Expected no suppression for mismatched skill");
}
} catch (error) {
fail(testName, error);
}
}
async function testEmptySuppressions() {
const testName = "isAdvisorySuppressed: empty suppressions never suppress";
try {
const match = makeMatch("CVE-2026-25593", "clawsec-suite");
if (isAdvisorySuppressed(match, []) === false) {
pass(testName);
} else {
fail(testName, "Expected no suppression with empty rules");
}
} catch (error) {
fail(testName, error);
}
}
async function testMultipleRules() {
const testName = "isAdvisorySuppressed: multiple rules match correct one";
try {
const match = makeMatch("CLAW-2026-0001", "openclaw-audit-watchdog");
const rules = makeRules([
["CVE-2026-25593", "clawsec-suite"],
["CLAW-2026-0001", "openclaw-audit-watchdog"],
]);
if (isAdvisorySuppressed(match, rules) === true) {
pass(testName);
} else {
fail(testName, "Expected match against second rule");
}
} catch (error) {
fail(testName, error);
}
}
async function testMissingAdvisoryId() {
const testName = "isAdvisorySuppressed: missing advisory.id does not suppress";
try {
const match = {
advisory: { severity: "high", title: "No ID advisory" },
skill: { name: "clawsec-suite", dirName: "clawsec-suite", version: "1.0.0" },
matchedAffected: [],
};
const rules = makeRules([["CVE-2026-25593", "clawsec-suite"]]);
if (isAdvisorySuppressed(match, rules) === false) {
pass(testName);
} else {
fail(testName, "Expected no suppression when advisory has no id");
}
} catch (error) {
fail(testName, error);
}
}
// ---------------------------------------------------------------------------
// loadAdvisorySuppression tests
// ---------------------------------------------------------------------------
async function testLoadWithAdvisorySentinel() {
const testName = "loadAdvisorySuppression: loads config with advisory sentinel";
try {
const configFile = path.join(tempDir.path, "advisory-config.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["advisory"],
suppressions: [{
checkId: "CVE-2026-25593",
skill: "clawsec-suite",
reason: "First-party tooling",
suppressedAt: "2026-02-15",
}],
}));
const config = await loadAdvisorySuppression(configFile);
if (config.suppressions.length === 1 && config.source === configFile) {
pass(testName);
} else {
fail(testName, `Expected 1 suppression from ${configFile}, got: ${JSON.stringify(config)}`);
}
} catch (error) {
fail(testName, error);
}
}
async function testLoadWithMissingSentinel() {
const testName = "loadAdvisorySuppression: missing sentinel returns empty config";
try {
const configFile = path.join(tempDir.path, "no-sentinel.json");
await fs.writeFile(configFile, JSON.stringify({
suppressions: [{
checkId: "CVE-2026-25593",
skill: "clawsec-suite",
reason: "First-party tooling",
suppressedAt: "2026-02-15",
}],
}));
const config = await loadAdvisorySuppression(configFile);
if (config.suppressions.length === 0) {
pass(testName);
} else {
fail(testName, `Expected empty suppressions without sentinel, got: ${JSON.stringify(config)}`);
}
} catch (error) {
fail(testName, error);
}
}
async function testLoadWithAuditOnlySentinel() {
const testName = "loadAdvisorySuppression: audit-only sentinel returns empty for advisory";
try {
const configFile = path.join(tempDir.path, "audit-only.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["audit"],
suppressions: [{
checkId: "CVE-2026-25593",
skill: "clawsec-suite",
reason: "First-party tooling",
suppressedAt: "2026-02-15",
}],
}));
const config = await loadAdvisorySuppression(configFile);
if (config.suppressions.length === 0) {
pass(testName);
} else {
fail(testName, `Expected empty for audit-only sentinel, got: ${JSON.stringify(config)}`);
}
} catch (error) {
fail(testName, error);
}
}
async function testLoadWithBothSentinels() {
const testName = "loadAdvisorySuppression: both audit+advisory sentinels activates advisory";
try {
const configFile = path.join(tempDir.path, "both-sentinel.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["audit", "advisory"],
suppressions: [{
checkId: "CVE-2026-25593",
skill: "clawsec-suite",
reason: "First-party tooling",
suppressedAt: "2026-02-15",
}],
}));
const config = await loadAdvisorySuppression(configFile);
if (config.suppressions.length === 1) {
pass(testName);
} else {
fail(testName, `Expected 1 suppression with both sentinels, got: ${JSON.stringify(config)}`);
}
} catch (error) {
fail(testName, error);
}
}
async function testLoadNonexistentExplicitPath() {
const testName = "loadAdvisorySuppression: explicit nonexistent path throws";
try {
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")) {
pass(testName);
} else {
fail(testName, `Unexpected error: ${error}`);
}
}
}
async function testLoadNoConfigReturnsEmpty() {
const testName = "loadAdvisorySuppression: no config available returns empty";
try {
// Clear env var to ensure no ambient config
const savedEnv = process.env.OPENCLAW_AUDIT_CONFIG;
delete process.env.OPENCLAW_AUDIT_CONFIG;
try {
// Call without explicit path and with no env var — falls through to default paths
// which likely don't exist in test environment
const config = await loadAdvisorySuppression();
if (config.suppressions.length === 0 && config.source === "none") {
pass(testName);
} else {
fail(testName, `Expected empty config, got: ${JSON.stringify(config)}`);
}
} finally {
if (savedEnv !== undefined) process.env.OPENCLAW_AUDIT_CONFIG = savedEnv;
else delete process.env.OPENCLAW_AUDIT_CONFIG;
}
} catch (error) {
fail(testName, error);
}
}
async function testEnvPathHomeExpansion() {
const testName = "loadAdvisorySuppression: OPENCLAW_AUDIT_CONFIG expands $HOME";
try {
const configFile = path.join(tempDir.path, "env-home.json");
await fs.writeFile(configFile, JSON.stringify({
enabledFor: ["advisory"],
suppressions: [{
checkId: "CVE-2026-25593",
skill: "clawsec-suite",
reason: "Env home expansion",
suppressedAt: "2026-02-15",
}],
}));
const savedConfig = process.env.OPENCLAW_AUDIT_CONFIG;
const savedHome = process.env.HOME;
process.env.HOME = tempDir.path;
process.env.OPENCLAW_AUDIT_CONFIG = "$HOME/env-home.json";
try {
const config = await loadAdvisorySuppression();
if (config.suppressions.length === 1 && config.source === configFile) {
pass(testName);
} else {
fail(testName, `Expected env-expanded config, got: ${JSON.stringify(config)}`);
}
} finally {
if (savedConfig !== undefined) process.env.OPENCLAW_AUDIT_CONFIG = savedConfig;
else delete process.env.OPENCLAW_AUDIT_CONFIG;
if (savedHome !== undefined) process.env.HOME = savedHome;
else delete process.env.HOME;
}
} catch (error) {
fail(testName, error);
}
}
async function testEscapedHomeTokenRejected() {
const testName = "loadAdvisorySuppression: escaped home token is rejected";
try {
const savedEnv = process.env.OPENCLAW_AUDIT_CONFIG;
process.env.OPENCLAW_AUDIT_CONFIG = "\\$HOME/not-real.json";
try {
await loadAdvisorySuppression();
fail(testName, "Expected error for escaped token");
} catch (error) {
if (String(error).includes("Unexpanded home token")) {
pass(testName);
} else {
fail(testName, `Unexpected error: ${error}`);
}
} finally {
if (savedEnv !== undefined) process.env.OPENCLAW_AUDIT_CONFIG = savedEnv;
else delete process.env.OPENCLAW_AUDIT_CONFIG;
}
} catch (error) {
fail(testName, error);
}
}
// ---------------------------------------------------------------------------
// Main test runner
// ---------------------------------------------------------------------------
async function runAllTests() {
console.log("=== Advisory Suppression Tests ===\n");
tempDir = await createTempDir();
try {
// isAdvisorySuppressed tests
await testExactMatch();
await testCaseInsensitiveSkillMatch();
await testCheckIdMismatch();
await testSkillMismatch();
await testEmptySuppressions();
await testMultipleRules();
await testMissingAdvisoryId();
// loadAdvisorySuppression tests
await testLoadWithAdvisorySentinel();
await testLoadWithMissingSentinel();
await testLoadWithAuditOnlySentinel();
await testLoadWithBothSentinels();
await testLoadNonexistentExplicitPath();
await testLoadNoConfigReturnsEmpty();
await testEnvPathHomeExpansion();
await testEscapedHomeTokenRejected();
} finally {
await tempDir.cleanup();
}
report();
exitWithResults();
}
runAllTests().catch((err) => {
console.error("Test runner failed:", err);
process.exit(1);
});