mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
f9a7565d6f
* 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>
598 lines
18 KiB
JavaScript
Executable File
598 lines
18 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Dependency scanner tests for clawsec-scanner.
|
|
*
|
|
* Tests cover:
|
|
* - Utility functions (normalizeSeverity, safeJsonParse, commandExists)
|
|
* - Report generation and formatting
|
|
* - Argument parsing
|
|
* - Integration with temp directory setup
|
|
*
|
|
* Run: node skills/clawsec-scanner/test/dependency_scanner.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, "..", "lib");
|
|
|
|
// Dynamic import to ensure we test the actual modules
|
|
const { normalizeSeverity, safeJsonParse, getTimestamp, generateUuid, commandExists } =
|
|
await import(`${LIB_PATH}/utils.mjs`);
|
|
const { generateReport, formatReportJson, formatReportText } = await import(
|
|
`${LIB_PATH}/report.mjs`
|
|
);
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: normalizeSeverity - critical variations
|
|
// -----------------------------------------------------------------------------
|
|
async function testNormalizeSeverity_Critical() {
|
|
const testName = "normalizeSeverity: recognizes critical";
|
|
try {
|
|
const test1 = normalizeSeverity("critical");
|
|
const test2 = normalizeSeverity("CRITICAL");
|
|
const test3 = normalizeSeverity(" Critical ");
|
|
|
|
if (test1 === "critical" && test2 === "critical" && test3 === "critical") {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Expected 'critical', got ${test1}, ${test2}, ${test3}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: normalizeSeverity - high variations
|
|
// -----------------------------------------------------------------------------
|
|
async function testNormalizeSeverity_High() {
|
|
const testName = "normalizeSeverity: recognizes high";
|
|
try {
|
|
const test1 = normalizeSeverity("high");
|
|
const test2 = normalizeSeverity("HIGH");
|
|
|
|
if (test1 === "high" && test2 === "high") {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Expected 'high', got ${test1}, ${test2}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: normalizeSeverity - medium variations (moderate, medium)
|
|
// -----------------------------------------------------------------------------
|
|
async function testNormalizeSeverity_Medium() {
|
|
const testName = "normalizeSeverity: recognizes medium/moderate";
|
|
try {
|
|
const test1 = normalizeSeverity("medium");
|
|
const test2 = normalizeSeverity("moderate");
|
|
const test3 = normalizeSeverity("MODERATE");
|
|
|
|
if (test1 === "medium" && test2 === "medium" && test3 === "medium") {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Expected 'medium', got ${test1}, ${test2}, ${test3}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: normalizeSeverity - low variations
|
|
// -----------------------------------------------------------------------------
|
|
async function testNormalizeSeverity_Low() {
|
|
const testName = "normalizeSeverity: recognizes low";
|
|
try {
|
|
const test1 = normalizeSeverity("low");
|
|
const test2 = normalizeSeverity("LOW");
|
|
|
|
if (test1 === "low" && test2 === "low") {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Expected 'low', got ${test1}, ${test2}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: normalizeSeverity - defaults to info for unknown
|
|
// -----------------------------------------------------------------------------
|
|
async function testNormalizeSeverity_Unknown() {
|
|
const testName = "normalizeSeverity: defaults to info for unknown";
|
|
try {
|
|
const test1 = normalizeSeverity("unknown");
|
|
const test2 = normalizeSeverity("");
|
|
const test3 = normalizeSeverity("garbage");
|
|
|
|
if (test1 === "info" && test2 === "info" && test3 === "info") {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Expected 'info', got ${test1}, ${test2}, ${test3}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: safeJsonParse - valid JSON
|
|
// -----------------------------------------------------------------------------
|
|
async function testSafeJsonParse_Valid() {
|
|
const testName = "safeJsonParse: parses valid JSON";
|
|
try {
|
|
const json = '{"foo": "bar", "num": 42}';
|
|
const result = safeJsonParse(json);
|
|
|
|
if (
|
|
result &&
|
|
typeof result === "object" &&
|
|
result.foo === "bar" &&
|
|
result.num === 42
|
|
) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Unexpected result: ${JSON.stringify(result)}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: safeJsonParse - invalid JSON returns fallback
|
|
// -----------------------------------------------------------------------------
|
|
async function testSafeJsonParse_Invalid() {
|
|
const testName = "safeJsonParse: returns fallback for invalid JSON";
|
|
try {
|
|
const invalid = "{not valid json}";
|
|
const fallback = { error: true };
|
|
const result = safeJsonParse(invalid, { fallback });
|
|
|
|
if (result && result.error === true) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Expected fallback object, got ${JSON.stringify(result)}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: safeJsonParse - empty string returns fallback
|
|
// -----------------------------------------------------------------------------
|
|
async function testSafeJsonParse_Empty() {
|
|
const testName = "safeJsonParse: returns fallback for empty string";
|
|
try {
|
|
const result = safeJsonParse("", { fallback: null });
|
|
|
|
if (result === null) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Expected null, got ${JSON.stringify(result)}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: getTimestamp - returns ISO 8601 format
|
|
// -----------------------------------------------------------------------------
|
|
async function testGetTimestamp() {
|
|
const testName = "getTimestamp: returns ISO 8601 format";
|
|
try {
|
|
const timestamp = getTimestamp();
|
|
const iso8601Pattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
|
|
if (iso8601Pattern.test(timestamp)) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Expected ISO 8601 format, got ${timestamp}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: generateUuid - returns valid UUID v4 format
|
|
// -----------------------------------------------------------------------------
|
|
async function testGenerateUuid() {
|
|
const testName = "generateUuid: returns valid UUID v4 format";
|
|
try {
|
|
const uuid = generateUuid();
|
|
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
|
|
if (uuidPattern.test(uuid)) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Expected UUID v4 format, got ${uuid}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: generateUuid - generates unique IDs
|
|
// -----------------------------------------------------------------------------
|
|
async function testGenerateUuid_Unique() {
|
|
const testName = "generateUuid: generates unique IDs";
|
|
try {
|
|
const uuid1 = generateUuid();
|
|
const uuid2 = generateUuid();
|
|
const uuid3 = generateUuid();
|
|
|
|
if (uuid1 !== uuid2 && uuid2 !== uuid3 && uuid1 !== uuid3) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Expected unique UUIDs, got ${uuid1}, ${uuid2}, ${uuid3}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: generateReport - empty vulnerabilities
|
|
// -----------------------------------------------------------------------------
|
|
async function testGenerateReport_Empty() {
|
|
const testName = "generateReport: handles empty vulnerabilities";
|
|
try {
|
|
const report = generateReport([], "/test/path");
|
|
|
|
if (
|
|
report &&
|
|
report.vulnerabilities.length === 0 &&
|
|
report.summary.critical === 0 &&
|
|
report.summary.high === 0 &&
|
|
report.summary.medium === 0 &&
|
|
report.summary.low === 0 &&
|
|
report.summary.info === 0 &&
|
|
report.target === "/test/path"
|
|
) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Unexpected report structure: ${JSON.stringify(report)}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: generateReport - counts vulnerabilities by severity
|
|
// -----------------------------------------------------------------------------
|
|
async function testGenerateReport_Counts() {
|
|
const testName = "generateReport: counts vulnerabilities by severity";
|
|
try {
|
|
const vulnerabilities = [
|
|
{
|
|
id: "TEST-001",
|
|
source: "test",
|
|
severity: "critical",
|
|
package: "test-pkg",
|
|
version: "1.0.0",
|
|
fixed_version: "1.1.0",
|
|
title: "Test Critical",
|
|
description: "Test",
|
|
references: [],
|
|
discovered_at: "2026-01-01T00:00:00.000Z",
|
|
},
|
|
{
|
|
id: "TEST-002",
|
|
source: "test",
|
|
severity: "high",
|
|
package: "test-pkg",
|
|
version: "1.0.0",
|
|
fixed_version: "1.1.0",
|
|
title: "Test High",
|
|
description: "Test",
|
|
references: [],
|
|
discovered_at: "2026-01-01T00:00:00.000Z",
|
|
},
|
|
{
|
|
id: "TEST-003",
|
|
source: "test",
|
|
severity: "high",
|
|
package: "test-pkg-2",
|
|
version: "2.0.0",
|
|
fixed_version: "2.1.0",
|
|
title: "Test High 2",
|
|
description: "Test",
|
|
references: [],
|
|
discovered_at: "2026-01-01T00:00:00.000Z",
|
|
},
|
|
{
|
|
id: "TEST-004",
|
|
source: "test",
|
|
severity: "medium",
|
|
package: "test-pkg-3",
|
|
version: "3.0.0",
|
|
fixed_version: "3.1.0",
|
|
title: "Test Medium",
|
|
description: "Test",
|
|
references: [],
|
|
discovered_at: "2026-01-01T00:00:00.000Z",
|
|
},
|
|
];
|
|
|
|
const report = generateReport(vulnerabilities, ".");
|
|
|
|
if (
|
|
report.summary.critical === 1 &&
|
|
report.summary.high === 2 &&
|
|
report.summary.medium === 1 &&
|
|
report.summary.low === 0 &&
|
|
report.summary.info === 0 &&
|
|
report.vulnerabilities.length === 4
|
|
) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Unexpected counts: ${JSON.stringify(report.summary)}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: formatReportJson - produces valid JSON
|
|
// -----------------------------------------------------------------------------
|
|
async function testFormatReportJson() {
|
|
const testName = "formatReportJson: produces valid JSON";
|
|
try {
|
|
const report = generateReport([], "/test/path");
|
|
const jsonString = formatReportJson(report);
|
|
const parsed = JSON.parse(jsonString);
|
|
|
|
if (parsed && parsed.target === "/test/path" && Array.isArray(parsed.vulnerabilities)) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Invalid JSON structure: ${jsonString}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: formatReportText - produces text output
|
|
// -----------------------------------------------------------------------------
|
|
async function testFormatReportText() {
|
|
const testName = "formatReportText: produces text output";
|
|
try {
|
|
const report = generateReport([], "/test/path");
|
|
const text = formatReportText(report);
|
|
|
|
if (
|
|
text.includes("VULNERABILITY SCAN REPORT") &&
|
|
text.includes("Target: /test/path") &&
|
|
text.includes("No vulnerabilities detected")
|
|
) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Missing expected text output sections");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: formatReportText - includes vulnerability details
|
|
// -----------------------------------------------------------------------------
|
|
async function testFormatReportText_WithVulnerabilities() {
|
|
const testName = "formatReportText: includes vulnerability details";
|
|
try {
|
|
const vulnerabilities = [
|
|
{
|
|
id: "CVE-2026-1234",
|
|
source: "npm-audit",
|
|
severity: "high",
|
|
package: "test-package",
|
|
version: "1.0.0",
|
|
fixed_version: "1.1.0",
|
|
title: "Test Vulnerability",
|
|
description: "This is a test vulnerability description",
|
|
references: ["https://example.com/cve-2026-1234"],
|
|
discovered_at: "2026-01-01T00:00:00.000Z",
|
|
},
|
|
];
|
|
|
|
const report = generateReport(vulnerabilities, ".");
|
|
const text = formatReportText(report);
|
|
|
|
if (
|
|
text.includes("CVE-2026-1234") &&
|
|
text.includes("test-package") &&
|
|
text.includes("1.0.0") &&
|
|
text.includes("1.1.0") &&
|
|
text.includes("Test Vulnerability") &&
|
|
text.includes("HIGH")
|
|
) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Missing expected vulnerability details in text output");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: commandExists - detects existing command
|
|
// -----------------------------------------------------------------------------
|
|
async function testCommandExists_Found() {
|
|
const testName = "commandExists: detects existing command (node)";
|
|
try {
|
|
// 'node' should always exist in the test environment
|
|
const result = await commandExists("node");
|
|
|
|
if (result === true) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected true for 'node' command");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: commandExists - returns false for non-existent command
|
|
// -----------------------------------------------------------------------------
|
|
async function testCommandExists_NotFound() {
|
|
const testName = "commandExists: returns false for non-existent command";
|
|
try {
|
|
// Use a command that definitely doesn't exist
|
|
const result = await commandExists("definitely-not-a-real-command-12345");
|
|
|
|
if (result === false) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, "Expected false for non-existent command");
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: Report structure - has required fields
|
|
// -----------------------------------------------------------------------------
|
|
async function testReportStructure() {
|
|
const testName = "Report structure: has all required fields";
|
|
try {
|
|
const report = generateReport([], ".");
|
|
|
|
const hasAllFields =
|
|
"scan_id" in report &&
|
|
"timestamp" in report &&
|
|
"target" in report &&
|
|
"vulnerabilities" in report &&
|
|
"summary" in report &&
|
|
"critical" in report.summary &&
|
|
"high" in report.summary &&
|
|
"medium" in report.summary &&
|
|
"low" in report.summary &&
|
|
"info" in report.summary;
|
|
|
|
if (hasAllFields) {
|
|
pass(testName);
|
|
} else {
|
|
fail(testName, `Missing required fields in report: ${JSON.stringify(report)}`);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Test: Temp directory creation
|
|
// -----------------------------------------------------------------------------
|
|
async function testTempDirCreation() {
|
|
const testName = "createTempDir: creates and cleans up temp directory";
|
|
try {
|
|
const { path: tmpPath, cleanup } = await createTempDir();
|
|
|
|
// Verify directory exists
|
|
const stat = await fs.stat(tmpPath);
|
|
if (!stat.isDirectory()) {
|
|
fail(testName, "Created path is not a directory");
|
|
return;
|
|
}
|
|
|
|
// Create a test file
|
|
const testFilePath = path.join(tmpPath, "test.txt");
|
|
await fs.writeFile(testFilePath, "test content");
|
|
|
|
// Verify file exists
|
|
const fileExists = await fs
|
|
.access(testFilePath)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (!fileExists) {
|
|
fail(testName, "Test file was not created");
|
|
return;
|
|
}
|
|
|
|
// Cleanup
|
|
await cleanup();
|
|
|
|
// Verify cleanup
|
|
const dirExists = await fs
|
|
.access(tmpPath)
|
|
.then(() => true)
|
|
.catch(() => false);
|
|
|
|
if (dirExists) {
|
|
fail(testName, "Temp directory was not cleaned up");
|
|
} else {
|
|
pass(testName);
|
|
}
|
|
} catch (error) {
|
|
fail(testName, error);
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Main test runner
|
|
// -----------------------------------------------------------------------------
|
|
async function main() {
|
|
console.log("Running dependency scanner tests...\n");
|
|
|
|
// Utility function tests
|
|
await testNormalizeSeverity_Critical();
|
|
await testNormalizeSeverity_High();
|
|
await testNormalizeSeverity_Medium();
|
|
await testNormalizeSeverity_Low();
|
|
await testNormalizeSeverity_Unknown();
|
|
|
|
await testSafeJsonParse_Valid();
|
|
await testSafeJsonParse_Invalid();
|
|
await testSafeJsonParse_Empty();
|
|
|
|
await testGetTimestamp();
|
|
await testGenerateUuid();
|
|
await testGenerateUuid_Unique();
|
|
|
|
await testCommandExists_Found();
|
|
await testCommandExists_NotFound();
|
|
|
|
// Report generation tests
|
|
await testGenerateReport_Empty();
|
|
await testGenerateReport_Counts();
|
|
await testReportStructure();
|
|
|
|
// Report formatting tests
|
|
await testFormatReportJson();
|
|
await testFormatReportText();
|
|
await testFormatReportText_WithVulnerabilities();
|
|
|
|
// Infrastructure tests
|
|
await testTempDirCreation();
|
|
|
|
// Final report
|
|
report();
|
|
exitWithResults();
|
|
}
|
|
|
|
// Run if executed directly
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
main();
|
|
}
|