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>
307 lines
8.4 KiB
JavaScript
Executable File
307 lines
8.4 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import {
|
|
execCommand,
|
|
safeJsonParse,
|
|
normalizeSeverity,
|
|
getTimestamp,
|
|
commandExists,
|
|
} from "../lib/utils.mjs";
|
|
import { generateReport, formatReportJson, formatReportText } from "../lib/report.mjs";
|
|
|
|
/**
|
|
* @typedef {import('../lib/types.ts').Vulnerability} Vulnerability
|
|
* @typedef {import('../lib/types.ts').ScanReport} ScanReport
|
|
*/
|
|
|
|
/**
|
|
* Parse CLI arguments.
|
|
*
|
|
* @param {string[]} argv - Command line arguments
|
|
* @returns {{target: string, format: 'json' | 'text'}}
|
|
*/
|
|
function parseArgs(argv) {
|
|
const parsed = {
|
|
target: "",
|
|
format: "json",
|
|
};
|
|
|
|
for (let i = 0; i < argv.length; i += 1) {
|
|
const token = argv[i];
|
|
|
|
if (token === "--target") {
|
|
parsed.target = String(argv[i + 1] ?? "").trim();
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (token === "--format") {
|
|
const formatValue = String(argv[i + 1] ?? "").trim();
|
|
if (formatValue !== "json" && formatValue !== "text") {
|
|
throw new Error("Invalid --format value. Use 'json' or 'text'.");
|
|
}
|
|
parsed.format = formatValue;
|
|
i += 1;
|
|
continue;
|
|
}
|
|
if (token === "--help" || token === "-h") {
|
|
printUsage();
|
|
process.exit(0);
|
|
}
|
|
|
|
throw new Error(`Unknown argument: ${token}`);
|
|
}
|
|
|
|
if (!parsed.target) {
|
|
throw new Error("Missing required argument: --target");
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
/**
|
|
* Print usage information.
|
|
*/
|
|
function printUsage() {
|
|
process.stderr.write(
|
|
[
|
|
"Usage:",
|
|
" node scripts/sast_analyzer.mjs --target <path> [--format json|text]",
|
|
"",
|
|
"Examples:",
|
|
" node scripts/sast_analyzer.mjs --target ./skills/clawsec-suite",
|
|
" node scripts/sast_analyzer.mjs --target ./skills/ --format json",
|
|
"",
|
|
"Flags:",
|
|
" --target Path to scan (required)",
|
|
" --format Output format: json or text (default: json)",
|
|
"",
|
|
].join("\n"),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Check if a file exists.
|
|
*
|
|
* @param {string} filePath - Path to check
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
async function fileExists(filePath) {
|
|
try {
|
|
await fs.access(filePath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Run Semgrep for JavaScript/TypeScript analysis.
|
|
*
|
|
* @param {string} targetPath - Path to scan
|
|
* @returns {Promise<Vulnerability[]>}
|
|
*/
|
|
async function runSemgrep(targetPath) {
|
|
const vulnerabilities = [];
|
|
|
|
// Check if semgrep is available
|
|
const hasSemgrep = await commandExists("semgrep");
|
|
if (!hasSemgrep) {
|
|
process.stderr.write("[semgrep] semgrep command not found, skipping JavaScript/TypeScript SAST\n");
|
|
return vulnerabilities;
|
|
}
|
|
|
|
try {
|
|
// Run Semgrep with security-focused rules
|
|
// NOTE: Semgrep exits non-zero when findings are present
|
|
const { stdout } = await execCommand("semgrep", [
|
|
"scan",
|
|
"--config", "auto",
|
|
"--json",
|
|
targetPath,
|
|
]);
|
|
|
|
const semgrepData = safeJsonParse(stdout, {
|
|
fallback: { results: [] },
|
|
label: "semgrep output",
|
|
});
|
|
|
|
// Semgrep format: { results: [ {check_id, path, extra: {message, severity, ...}, ...} ] }
|
|
if (semgrepData && typeof semgrepData === "object" && "results" in semgrepData) {
|
|
const results = Array.isArray(semgrepData.results) ? semgrepData.results : [];
|
|
|
|
for (const result of results) {
|
|
if (!result || typeof result !== "object") continue;
|
|
|
|
const checkId = String(result.check_id || "semgrep-unknown");
|
|
const filePath = String(result.path || "unknown");
|
|
const extra = result.extra || {};
|
|
|
|
// Extract metadata
|
|
const message = String(extra.message || "Security issue detected");
|
|
const severity = normalizeSeverity(extra.severity || "info");
|
|
const metadata = extra.metadata || {};
|
|
|
|
// Build references from metadata
|
|
const references = [];
|
|
if (metadata.references && Array.isArray(metadata.references)) {
|
|
references.push(...metadata.references.map((r) => String(r)));
|
|
}
|
|
if (metadata.source && typeof metadata.source === "string") {
|
|
references.push(metadata.source);
|
|
}
|
|
|
|
const vuln = {
|
|
id: checkId,
|
|
source: "sast",
|
|
severity,
|
|
package: path.basename(filePath),
|
|
version: `${filePath}:${result.start?.line || 0}`,
|
|
fixed_version: "",
|
|
title: message.slice(0, 150),
|
|
description: message,
|
|
references,
|
|
discovered_at: getTimestamp(),
|
|
};
|
|
|
|
vulnerabilities.push(vuln);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
process.stderr.write(`[semgrep] Warning: ${error.message}\n`);
|
|
}
|
|
// Continue with partial results
|
|
}
|
|
|
|
return vulnerabilities;
|
|
}
|
|
|
|
/**
|
|
* Run Bandit for Python analysis.
|
|
*
|
|
* @param {string} targetPath - Path to scan
|
|
* @returns {Promise<Vulnerability[]>}
|
|
*/
|
|
async function runBandit(targetPath) {
|
|
const vulnerabilities = [];
|
|
|
|
// Check if bandit is available
|
|
const hasBandit = await commandExists("bandit");
|
|
if (!hasBandit) {
|
|
process.stderr.write("[bandit] bandit command not found, skipping Python SAST\n");
|
|
return vulnerabilities;
|
|
}
|
|
|
|
// Check if pyproject.toml exists in the project root
|
|
const pyprojectPath = path.join(process.cwd(), "pyproject.toml");
|
|
const hasPyproject = await fileExists(pyprojectPath);
|
|
|
|
try {
|
|
// Run Bandit with JSON output
|
|
// NOTE: Bandit exits non-zero when findings are present
|
|
const args = ["-r", targetPath, "-f", "json"];
|
|
|
|
// Only add -c flag if pyproject.toml exists
|
|
if (hasPyproject) {
|
|
args.push("-c", pyprojectPath);
|
|
}
|
|
|
|
const { stdout } = await execCommand("bandit", args);
|
|
|
|
const banditData = safeJsonParse(stdout, {
|
|
fallback: { results: [] },
|
|
label: "bandit output",
|
|
});
|
|
|
|
// Bandit format: { results: [ {issue_text, issue_severity, issue_confidence, test_id, filename, line_number, ...} ] }
|
|
if (banditData && typeof banditData === "object" && "results" in banditData) {
|
|
const results = Array.isArray(banditData.results) ? banditData.results : [];
|
|
|
|
for (const result of results) {
|
|
if (!result || typeof result !== "object") continue;
|
|
|
|
const testId = String(result.test_id || "bandit-unknown");
|
|
const filePath = String(result.filename || "unknown");
|
|
const lineNumber = result.line_number || 0;
|
|
const issueText = String(result.issue_text || "Security issue detected");
|
|
const issueSeverity = String(result.issue_severity || "LOW");
|
|
|
|
// Map Bandit severity (HIGH, MEDIUM, LOW) to normalized severity
|
|
const severity = normalizeSeverity(issueSeverity);
|
|
|
|
const vuln = {
|
|
id: testId,
|
|
source: "sast",
|
|
severity,
|
|
package: path.basename(filePath),
|
|
version: `${filePath}:${lineNumber}`,
|
|
fixed_version: "",
|
|
title: issueText.slice(0, 150),
|
|
description: issueText,
|
|
references: [
|
|
`https://bandit.readthedocs.io/en/latest/plugins/${testId.toLowerCase().replace(/_/g, '-')}.html`,
|
|
],
|
|
discovered_at: getTimestamp(),
|
|
};
|
|
|
|
vulnerabilities.push(vuln);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
process.stderr.write(`[bandit] Warning: ${error.message}\n`);
|
|
}
|
|
// Continue with partial results
|
|
}
|
|
|
|
return vulnerabilities;
|
|
}
|
|
|
|
/**
|
|
* Main entry point.
|
|
*/
|
|
async function main() {
|
|
try {
|
|
const args = parseArgs(process.argv.slice(2));
|
|
|
|
// Verify target path exists
|
|
const targetExists = await fileExists(args.target);
|
|
if (!targetExists) {
|
|
throw new Error(`Target path does not exist: ${args.target}`);
|
|
}
|
|
|
|
// Run SAST tools
|
|
const semgrepVulns = await runSemgrep(args.target);
|
|
const banditVulns = await runBandit(args.target);
|
|
|
|
// Combine all vulnerabilities
|
|
const allVulnerabilities = [...semgrepVulns, ...banditVulns];
|
|
|
|
// Generate unified report
|
|
const report = generateReport(allVulnerabilities, args.target);
|
|
|
|
// Output report
|
|
if (args.format === "json") {
|
|
process.stdout.write(formatReportJson(report));
|
|
process.stdout.write("\n");
|
|
} else {
|
|
process.stdout.write(formatReportText(report));
|
|
}
|
|
|
|
// Exit 0 even if vulnerabilities found (advisory only)
|
|
process.exit(0);
|
|
} catch (error) {
|
|
if (error instanceof Error) {
|
|
process.stderr.write(`Error: ${error.message}\n`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Run if executed directly
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
main();
|
|
}
|