Files
clawsec/skills/clawsec-scanner/test/reviewer_regressions.test.mjs
T
davida-ps f9a7565d6f Automated Vulnerability Scanner Skill (clawsec-scanner) (#101)
* auto-claude: subtask-1-1 - Create skill.json with SBOM, OpenClaw config, and required binaries

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

* auto-claude: subtask-1-2 - Create SKILL.md with YAML frontmatter and documentation

* auto-claude: subtask-1-3 - Create CHANGELOG.md starting at version 0.1.0

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

* auto-claude: subtask-1-4 - Create directory structure (scripts/, lib/, hooks/, test/)

* auto-claude: subtask-2-1 - Create lib/types.ts with Vulnerability and ScanReport interfaces

- Defined VulnerabilitySource type with 7 possible sources (npm-audit, pip-audit, osv, nvd, github, sast, dast)
- Defined SeverityLevel type with 5 severity levels (critical, high, medium, low, info)
- Created Vulnerability interface with all required fields: id, source, severity, package, version, title, description, references, discovered_at, and optional fixed_version
- Created ScanReport interface with scan_id, timestamp, target, vulnerabilities array, and summary counts
- Added HookEvent and HookContext types for OpenClaw hook integration
- Follows patterns from clawsec-suite advisory-guardian types

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

* auto-claude: subtask-2-2 - Create lib/utils.mjs with subprocess execution and JSON parsing helpers

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

* auto-claude: subtask-2-3 - Create lib/report.mjs for unified vulnerability re

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

* auto-claude: subtask-3-1 - Create scripts/scan_dependencies.mjs for npm audit and pip-audit integration

- Implements npm audit JSON output parsing with non-zero exit handling
- Implements pip-audit JSON output parsing with -f json flag
- Handles missing package-lock.json/requirements.txt gracefully
- Checks for command availability (npm, pip-audit) before running
- Converts audit outputs to unified Vulnerability schema
- Generates ScanReport with UUID scan_id and timestamp
- Supports --target and --format (json|text) CLI flags
- Edge cases: missing files, unavailable commands, malformed JSON
- Verification passes: UUID scan_id matches pattern ^[0-9a-f-]{36}$

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

* auto-claude: subtask-4-1 - Create scripts/query_cve_databases.mjs with OSV pr

Implemented CVE database integration with:
- queryOSV(): Primary CVE source using OSV API (free, no auth)
- queryNVD(): Fallback NVD API with 6s rate limiting (gated by CLAWSEC_NVD_API_KEY)
- queryGitHub(): Placeholder for future GitHub Advisory Database integration
- enrichVulnerability(): Multi-database enrichment pipeline
- Normalization to unified Vulnerability schema with severity, references, fixed versions
- Graceful error handling for network failures and API errors

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

* auto-claude: subtask-5-1 - Create scripts/sast_analyzer.mjs to run Semgrep and Bandit

Implemented static analysis engine following scan_dependencies.mjs pattern:
- Runs Semgrep for JS/TS with --config auto and --json output
- Runs Bandit for Python with -r <path> -f json -c pyproject.toml
- Handles non-zero exit codes gracefully (tools exit 1 on findings)
- Parses JSON output and converts to unified Vulnerability schema
- Supports --target and --format CLI flags
- Gracefully handles missing tools (semgrep, bandit)
- Generates ScanReport with UUID scan_id and severity summary

Verification passed: JSON output with valid vulnerabilities array

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

* auto-claude: subtask-6-1 - Create scripts/dast_runner.mjs with basic security test framework

- Implemented DAST framework with 4 security test cases:
  - DAST-001: Hook handler malicious input test (XSS, command injection, path traversal)
  - DAST-002: Hook handler timeout enforcement (30s default)
  - DAST-003: Hook handler resource limits (memory/CPU)
  - DAST-004: Hook handler event mutation safety
- Supports --target, --format (json|text), --timeout CLI flags
- Returns unified ScanReport with vulnerability schema
- Executes all test cases with configurable timeout
- Tests malicious input patterns: XSS, SQL injection, command injection, path traversal, null bytes, large payloads
- v1 scope: basic test framework for hook security testing (full agent workflow DAST is future work)

Verification:
-  Framework loads and executes 4 test cases
-  Timeout enforcement working (30s default, configurable via --timeout)
-  JSON output with valid scan_id
-  Text format output working
-  Help output displays usage information

* auto-claude: subtask-7-1 - Create scripts/runner.sh as main entry point with CLI flag parsing

- Orchestrates all scanning engines (dependency, SAST, DAST, CVE)
- Supports --target (required), --output, --format flags
- Merges reports from all scanners using jq
- Provides --help documentation
- Follows openclaw-audit-watchdog/scripts/runner.sh pattern
- Includes skip flags for selective scanning
- Verification: --help shows --target flag

* auto-claude: subtask-8-1 - Create hooks/clawsec-scanner-hook/HOOK.md with hook metadata

- Added YAML frontmatter with hook name, description, and OpenClaw events
- Documented hook purpose: periodic vulnerability scanning on agent:bootstrap and command:new
- Described four scanning engines: dependency, SAST, DAST, CVE lookup
- Added safety contract (non-blocking, read-only, configurable interval)
- Documented all environment variables (core config, CVE integration, selective scanning, advanced options)
- Listed required binaries (node, npm, python3, pip-audit, semgrep, bandit, jq, curl)
- Follows clawsec-advisory-guardian/HOOK.md pattern

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

* auto-claude: subtask-8-2 - Create hooks/clawsec-scanner-hook/handler.ts with event.messages mutation

- Implement hook handler following clawsec-advisory-guardian pattern
- Add rate-limited scanning with configurable interval (default 24h)
- Support event types: agent:bootstrap and command:new
- Integrate with runner.sh for vulnerability scanning
- Deduplicate vulnerabilities using state file persistence
- Filter findings by minimum severity (default: medium)
- Push scan results to event.messages array
- Support selective scanning via environment variables
- Handle failures gracefully with partial results

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

* auto-claude: subtask-8-3 - Create scripts/setup_scanner_hook.mjs for hook installation

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

* auto-claude: subtask-9-1 - Create test/dependency_scanner.test.mjs for dependency scanning tests

- Created test harness (test/lib/test_harness.mjs) with test utilities
- Created comprehensive test suite with 20 tests covering:
  - normalizeSeverity function (all severity levels)
  - safeJsonParse function (valid, invalid, empty inputs)
  - getTimestamp and generateUuid functions
  - commandExists function (found and not found cases)
  - generateReport function (empty and with vulnerabilities)
  - formatReportJson and formatReportText functions
  - Report structure validation
  - Temp directory creation and cleanup
- All tests pass successfully (20/20)

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

* auto-claude: subtask-9-2 - Create test/cve_integration.test.mjs for CVE database API tests

Added comprehensive CVE integration tests covering:
- OSV API query and normalization
- NVD API query with rate limiting
- GitHub Advisory Database placeholder
- Multi-source enrichment
- Error handling and network failures
- Vulnerability structure validation
- Multiple ecosystem support (npm, PyPI)

Tests gracefully handle network unavailability and skip API key-dependent tests.
All 20 tests passing.

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

* auto-claude: subtask-9-3 - Create test/sast_engine.test.mjs for static analysis tests

- Added comprehensive test suite for SAST engine functionality
- Tests cover Semgrep and Bandit output parsing
- Validates severity normalization and vulnerability data structures
- Includes edge case handling for malformed JSON and missing fields
- All 16 tests passing

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

* auto-claude: subtask-10-2 - Run ESLint with zero warnings

- Add no-unused-vars rule with argsIgnorePattern to .mjs files in ESLint config
- Prefix unused parameters with underscore in handler.ts, dast_runner.mjs, query_cve_databases.mjs
- Remove unused error binding in handler.ts catch block
- Remove unused result variable in cve_integration.test.mjs
- Remove unused SAMPLE_OSV_VULN and SAMPLE_NVD_CVE constants
- Remove unused safeJsonParse import from query_cve_databases.mjs

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

* fix(clawsec-scanner): resolve baz logical scanner findings

* fix(clawsec-scanner): make scanner state parsing type-safe

* chore(clawsec-scanner): bump version to 0.0.1

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-09 21:16:22 +02:00

249 lines
7.4 KiB
JavaScript

#!/usr/bin/env node
/**
* Regression tests for Baz review findings on PR #101.
*
* These tests enforce:
* - execCommand supports cwd and runs tools in the target directory
* - scan_dependencies chooses pip-audit invocation correctly when requirements.txt is absent
* - runner.sh preserves DAST findings even when dast_runner exits non-zero
*/
import fs from "node:fs/promises";
import path from "node:path";
import { spawn } from "node:child_process";
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 SKILL_ROOT = path.resolve(__dirname, "..");
const SCRIPTS_DIR = path.join(SKILL_ROOT, "scripts");
const { execCommand } = await import(path.join(SKILL_ROOT, "lib", "utils.mjs"));
/**
* @param {string} cmd
* @param {string[]} args
* @param {{cwd?: string, env?: NodeJS.ProcessEnv}} [options]
* @returns {Promise<{code: number, stdout: string, stderr: string}>}
*/
async function runProcess(cmd, args, options = {}) {
return new Promise((resolve) => {
const proc = spawn(cmd, args, {
cwd: options.cwd,
env: options.env,
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (chunk) => {
stdout += String(chunk);
});
proc.stderr.on("data", (chunk) => {
stderr += String(chunk);
});
proc.on("close", (code) => {
resolve({ code: code ?? 1, stdout, stderr });
});
});
}
/**
* @param {string} filePath
* @param {string} content
*/
async function writeExecutable(filePath, content) {
await fs.writeFile(filePath, content, "utf8");
await fs.chmod(filePath, 0o755);
}
async function testExecCommandRespectsCwd() {
const testName = "execCommand: respects cwd option";
const tmp = await createTempDir();
try {
const result = await execCommand("node", ["-e", "process.stdout.write(process.cwd())"], {
cwd: tmp.path,
});
const expectedPath = await fs.realpath(tmp.path);
const actualPath = await fs.realpath(result.stdout.trim());
if (actualPath === expectedPath) {
pass(testName);
} else {
fail(testName, `Expected cwd ${expectedPath}, got ${actualPath}`);
}
} catch (error) {
fail(testName, error);
} finally {
await tmp.cleanup();
}
}
async function testScanDependenciesUsesTargetCwdAndSmartPipArgs() {
const testName = "scan_dependencies: runs npm in target cwd and avoids -r when requirements.txt missing";
const tmp = await createTempDir();
try {
const targetDir = path.join(tmp.path, "target");
const binDir = path.join(tmp.path, "bin");
const npmLogPath = path.join(tmp.path, "npm.log");
const pipLogPath = path.join(tmp.path, "pip.log");
await fs.mkdir(targetDir, { recursive: true });
await fs.mkdir(binDir, { recursive: true });
await fs.writeFile(path.join(targetDir, "package-lock.json"), "{}\n", "utf8");
await fs.writeFile(path.join(targetDir, "pyproject.toml"), "[project]\nname='demo'\nversion='0.1.0'\n", "utf8");
await writeExecutable(
path.join(binDir, "npm"),
`#!/usr/bin/env node
const fs = require("node:fs");
const logPath = process.env.CLAWSEC_TEST_NPM_LOG;
fs.appendFileSync(logPath, JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2) }) + "\\n");
process.stdout.write(JSON.stringify({ vulnerabilities: {} }));
`,
);
await writeExecutable(
path.join(binDir, "pip-audit"),
`#!/usr/bin/env node
const fs = require("node:fs");
const logPath = process.env.CLAWSEC_TEST_PIP_LOG;
fs.appendFileSync(logPath, JSON.stringify({ cwd: process.cwd(), args: process.argv.slice(2) }) + "\\n");
process.stdout.write(JSON.stringify({ dependencies: [] }));
`,
);
const env = {
...process.env,
PATH: `${binDir}:${process.env.PATH}`,
CLAWSEC_TEST_NPM_LOG: npmLogPath,
CLAWSEC_TEST_PIP_LOG: pipLogPath,
};
const result = await runProcess(
"node",
[path.join(SCRIPTS_DIR, "scan_dependencies.mjs"), "--target", targetDir, "--format", "json"],
{ cwd: SKILL_ROOT, env },
);
if (result.code !== 0) {
fail(testName, `scan_dependencies exited ${result.code}: ${result.stderr}`);
return;
}
const npmLog = JSON.parse((await fs.readFile(npmLogPath, "utf8")).trim());
const pipLog = JSON.parse((await fs.readFile(pipLogPath, "utf8")).trim());
const expectedTargetPath = await fs.realpath(targetDir);
const actualNpmCwd = await fs.realpath(npmLog.cwd);
const npmCwdOk = actualNpmCwd === expectedTargetPath;
const pipArgsOk = !pipLog.args.includes("-r");
if (npmCwdOk && pipArgsOk) {
pass(testName);
} else {
fail(
testName,
`npm cwd=${actualNpmCwd}, expected=${expectedTargetPath}; pip args=${JSON.stringify(pipLog.args)}`,
);
}
} catch (error) {
fail(testName, error);
} finally {
await tmp.cleanup();
}
}
async function testRunnerPreservesDastReportOnNonZeroExit() {
const testName = "runner.sh: preserves DAST findings when dast_runner exits 1";
const tmp = await createTempDir();
try {
const targetDir = path.join(tmp.path, "target");
const binDir = path.join(tmp.path, "bin");
await fs.mkdir(targetDir, { recursive: true });
await fs.mkdir(binDir, { recursive: true });
await writeExecutable(
path.join(binDir, "node"),
`#!/usr/bin/env bash
set -euo pipefail
script="\${1:-}"
target="."
while [[ $# -gt 0 ]]; do
if [[ "$1" == "--target" ]]; then
target="\${2:-.}"
break
fi
shift
done
if [[ "$script" == *"scan_dependencies.mjs" ]] || [[ "$script" == *"sast_analyzer.mjs" ]]; then
cat <<JSON
{"scan_id":"test-scan","timestamp":"2026-03-09T00:00:00.000Z","target":"$target","vulnerabilities":[],"summary":{"critical":0,"high":0,"medium":0,"low":0,"info":0}}
JSON
exit 0
fi
if [[ "$script" == *"dast_runner.mjs" ]]; then
cat <<JSON
{"scan_id":"test-scan","timestamp":"2026-03-09T00:00:00.000Z","target":"$target","vulnerabilities":[{"id":"DAST-001","source":"dast","severity":"high","package":"N/A","version":"N/A","title":"DAST finding","description":"Synthetic high severity finding","references":[],"discovered_at":"2026-03-09T00:00:00.000Z"}],"summary":{"critical":0,"high":1,"medium":0,"low":0,"info":0}}
JSON
exit 1
fi
echo "Unexpected node invocation: $*" >&2
exit 2
`,
);
const env = {
...process.env,
PATH: `${binDir}:${process.env.PATH}`,
};
const result = await runProcess(
"bash",
[path.join(SCRIPTS_DIR, "runner.sh"), "--target", targetDir, "--format", "json"],
{ cwd: SKILL_ROOT, env },
);
if (result.code !== 0) {
fail(testName, `runner.sh exited ${result.code}: ${result.stderr}`);
return;
}
const merged = JSON.parse(result.stdout.trim());
const hasDastFinding = Array.isArray(merged.vulnerabilities)
&& merged.vulnerabilities.some((v) => v.id === "DAST-001" && v.source === "dast" && v.severity === "high");
if (hasDastFinding && merged.summary.high >= 1) {
pass(testName);
} else {
fail(testName, `Expected DAST high finding to be preserved. Output: ${result.stdout}`);
}
} catch (error) {
fail(testName, error);
} finally {
await tmp.cleanup();
}
}
async function main() {
await testExecCommandRespectsCwd();
await testScanDependenciesUsesTargetCwdAndSmartPipArgs();
await testRunnerPreservesDastReportOnNonZeroExit();
report();
exitWithResults();
}
await main();