mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-13 05:28:02 +03:00
fix(clawsec-scanner): release 0.0.2 with real OpenClaw DAST harness (#128)
* fix(clawsec-scanner): ship real openclaw dast harness in 0.0.2 * fix(clawsec-scanner): classify ts harness limits as info coverage * docs(wiki): add clawsec-scanner module documentation * docs(release): add clawsec-suite install guidance to quick install text * docs(readme): clarify standalone installs and suite optionality * docs(readme): remove standalone quick-install block * docs(readme): rename skill section and clarify suite start point
This commit is contained in:
@@ -898,6 +898,9 @@ jobs:
|
||||
npx clawhub@latest install ${{ steps.parse.outputs.skill_name }}
|
||||
```
|
||||
|
||||
**If you already have `clawsec-suite` installed:**
|
||||
Ask your agent to pull `${{ steps.parse.outputs.skill_name }}` from the ClawSec catalog and it will handle setup and verification automatically.
|
||||
|
||||
**Manual download with verification:**
|
||||
```bash
|
||||
# 1. Download the release archive, checksums, and signing material
|
||||
|
||||
@@ -159,7 +159,9 @@ See [`skills/clawsec-nanoclaw/INSTALL.md`](skills/clawsec-nanoclaw/INSTALL.md) f
|
||||
|
||||
The **clawsec-suite** is a skill-of-skills manager that installs, verifies, and maintains security skills from the ClawSec catalog.
|
||||
|
||||
### Skills in the Suite
|
||||
`clawsec-suite` is optional orchestration; skills can still be installed directly as standalone packages.
|
||||
|
||||
### ClawSec Skills
|
||||
|
||||
| Skill | Description | Installation | Compatibility |
|
||||
|-------|-------------|--------------|---------------|
|
||||
@@ -433,8 +435,9 @@ npm run build
|
||||
│ ├── populate-local-wiki.sh # Local wiki llms export populator
|
||||
│ └── release-skill.sh # Manual skill release helper
|
||||
├── skills/
|
||||
│ ├── clawsec-suite/ # 📦 Suite installer (skill-of-skills)
|
||||
│ ├── clawsec-suite/ # 📦 Suite installer (skill-of-skills - start here and have your agent do the rest)
|
||||
│ ├── clawsec-feed/ # 📡 Advisory feed skill
|
||||
│ ├── clawsec-scanner/ # 🔍 Vulnerability scanner (deps + SAST + OpenClaw DAST)
|
||||
│ ├── clawsec-nanoclaw/ # 📱 NanoClaw platform security suite
|
||||
│ ├── clawsec-clawhub-checker/ # 🧪 ClawHub reputation checks
|
||||
│ ├── clawtributor/ # 🤝 Community reporting skill
|
||||
|
||||
@@ -5,6 +5,20 @@ All notable changes to the ClawSec Scanner will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.0.2] - 2026-03-10
|
||||
|
||||
### Changed
|
||||
|
||||
- Replaced simulated DAST checks with real OpenClaw hook execution harness testing
|
||||
- Updated DAST semantics so high-severity findings are emitted for actual hook execution failures/timeouts, not static payload pattern matches
|
||||
- Reclassified DAST harness capability limitations (for example missing TypeScript compiler for `.ts` hooks) to `info` coverage findings instead of high severity
|
||||
- Added DAST harness mode guard to prevent recursive scanner execution when hook handlers are tested in isolation
|
||||
|
||||
### Added
|
||||
|
||||
- New DAST helper executor script for isolated per-hook execution and timeout enforcement
|
||||
- DAST harness regression tests covering no-false-positive baseline and malicious-input crash detection
|
||||
|
||||
## [0.0.1] - 2026-02-27
|
||||
|
||||
### Added
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: clawsec-scanner
|
||||
version: 0.0.1
|
||||
description: Automated vulnerability scanner for agent platforms. Performs dependency scanning (npm audit, pip-audit), multi-database CVE lookup (OSV, NVD, GitHub Advisory), SAST analysis (Semgrep, Bandit), and basic DAST security testing for skill hooks.
|
||||
version: 0.0.2
|
||||
description: Automated vulnerability scanner for agent platforms. Performs dependency scanning (npm audit, pip-audit), multi-database CVE lookup (OSV, NVD, GitHub Advisory), SAST analysis (Semgrep, Bandit), and agent-specific DAST hook execution testing for OpenClaw hooks.
|
||||
homepage: https://clawsec.prompt.security
|
||||
clawdis:
|
||||
emoji: "🔍"
|
||||
@@ -16,7 +16,7 @@ Comprehensive security scanner for agent platforms that automates vulnerability
|
||||
- **Dependency Scanning**: Analyzes npm and Python dependencies using `npm audit` and `pip-audit` with structured JSON output parsing
|
||||
- **CVE Database Integration**: Queries OSV (primary), NVD 2.0, and GitHub Advisory Database for vulnerability enrichment
|
||||
- **SAST Analysis**: Static code analysis using Semgrep (JavaScript/TypeScript) and Bandit (Python) to detect hardcoded secrets, command injection, path traversal, and unsafe deserialization
|
||||
- **DAST Framework**: Basic dynamic analysis for skill hook security testing (input validation, timeout enforcement)
|
||||
- **DAST Framework**: Agent-specific dynamic analysis with real OpenClaw hook execution harness (malicious input, timeout, output bounds, event mutation safety)
|
||||
- **Unified Reporting**: Consolidated vulnerability reports with severity classification and remediation guidance
|
||||
- **Continuous Monitoring**: OpenClaw hook integration for automated periodic scanning
|
||||
|
||||
@@ -43,8 +43,8 @@ The scanner orchestrates four complementary scan types to provide comprehensive
|
||||
- Identifies: hardcoded secrets (API keys, tokens), command injection (`eval`, `exec`), path traversal, unsafe deserialization
|
||||
|
||||
4. **Dynamic Analysis (DAST)**
|
||||
- Test framework for skill hook security validation
|
||||
- Verifies: malicious input handling, timeout enforcement, resource limits
|
||||
- Real hook execution harness for OpenClaw hook handlers discovered from `HOOK.md` metadata
|
||||
- Verifies: malicious input resilience, timeout behavior, output amplification bounds, and core event mutation safety
|
||||
- Note: Traditional web DAST tools (ZAP, Burp) do not apply to agent platforms - this provides agent-specific testing
|
||||
|
||||
### Unified Reporting
|
||||
@@ -248,7 +248,8 @@ scripts/runner.sh # Orchestration layer
|
||||
├── scan_dependencies.mjs # npm audit + pip-audit
|
||||
├── query_cve_databases.mjs # OSV/NVD/GitHub API queries
|
||||
├── sast_analyzer.mjs # Semgrep + Bandit static analysis
|
||||
└── dast_runner.mjs # Dynamic security testing
|
||||
├── dast_runner.mjs # Dynamic security testing orchestration
|
||||
└── dast_hook_executor.mjs # Isolated real hook execution harness
|
||||
|
||||
lib/
|
||||
├── report.mjs # Result aggregation and formatting
|
||||
@@ -325,6 +326,11 @@ proc.on('close', code => {
|
||||
- Requires Python 3.8+ runtime
|
||||
- Alternative: use Docker image `returntocorp/semgrep`
|
||||
|
||||
**"TypeScript hook not executable in DAST harness"**
|
||||
- The DAST harness executes real hook handlers and transpiles `handler.ts` files when a TypeScript compiler is available
|
||||
- Install TypeScript in the scanner environment: `npm install -D typescript` (or provide `handler.js`/`handler.mjs`)
|
||||
- Without a compiler, scanner reports an `info`-level coverage finding instead of a high-severity vulnerability
|
||||
|
||||
**"Concurrent scan detected"**
|
||||
- Lockfile exists: `/tmp/clawsec-scanner.lock`
|
||||
- Wait for running scan to complete or manually remove lockfile
|
||||
@@ -342,6 +348,7 @@ Check scanner is working correctly:
|
||||
node test/dependency_scanner.test.mjs
|
||||
node test/cve_integration.test.mjs
|
||||
node test/sast_engine.test.mjs
|
||||
node test/dast_harness.test.mjs
|
||||
|
||||
# Validate skill structure
|
||||
python ../../utils/validate_skill.py .
|
||||
@@ -364,6 +371,7 @@ done
|
||||
node test/dependency_scanner.test.mjs # Dependency scanning
|
||||
node test/cve_integration.test.mjs # CVE database APIs
|
||||
node test/sast_engine.test.mjs # Static analysis
|
||||
node test/dast_harness.test.mjs # DAST harness execution
|
||||
```
|
||||
|
||||
### Linting
|
||||
@@ -448,11 +456,11 @@ npx clawhub@latest install clawsec-suite
|
||||
|
||||
## Roadmap
|
||||
|
||||
### v0.1.0 (Current)
|
||||
### v0.0.2 (Current)
|
||||
- [x] Dependency scanning (npm audit, pip-audit)
|
||||
- [x] CVE database integration (OSV, NVD, GitHub Advisory)
|
||||
- [x] SAST analysis (Semgrep, Bandit)
|
||||
- [x] Basic DAST framework for skill hooks
|
||||
- [x] Real OpenClaw hook execution harness for DAST
|
||||
- [x] Unified JSON reporting
|
||||
- [x] OpenClaw hook integration
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ The hook orchestrates four independent scanning engines:
|
||||
1. **Dependency Scanning**: Executes `npm audit` and `pip-audit` to detect known vulnerabilities in JavaScript and Python dependencies
|
||||
2. **SAST (Static Analysis)**: Runs Semgrep (JS/TS) and Bandit (Python) to detect security issues like hardcoded secrets, command injection, and path traversal
|
||||
3. **CVE Database Lookup**: Queries OSV API (primary), NVD 2.0 (optional), and GitHub Advisory Database (optional) for vulnerability enrichment
|
||||
4. **DAST (Dynamic Analysis)**: Tests skill hook security including input validation, timeout enforcement, and resource limits
|
||||
4. **DAST (Dynamic Analysis)**: Executes real OpenClaw hook handlers in an isolated harness and tests malicious-input resilience, timeout behavior, output bounds, and event mutation safety
|
||||
|
||||
## Safety Contract
|
||||
|
||||
|
||||
@@ -196,6 +196,11 @@ function buildAlertMessage(report: ScanReport, format: string): string {
|
||||
}
|
||||
|
||||
const handler = async (event: HookEvent, _context: HookContext): Promise<void> => {
|
||||
// DAST harness mode executes hook handlers directly; skip recursive scanner runs.
|
||||
if (process.env.CLAWSEC_DAST_HARNESS === "1" || _context?.dastMode === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldHandleEvent(event)) return;
|
||||
|
||||
const installRoot = configuredPath(
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
function parseArgs(argv) {
|
||||
const parsed = {
|
||||
handler: "",
|
||||
exportName: "default",
|
||||
eventB64: "",
|
||||
contextB64: "",
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const token = argv[i];
|
||||
|
||||
if (token === "--handler") {
|
||||
parsed.handler = String(argv[i + 1] ?? "").trim();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token === "--export") {
|
||||
parsed.exportName = String(argv[i + 1] ?? "default").trim() || "default";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token === "--event") {
|
||||
parsed.eventB64 = String(argv[i + 1] ?? "").trim();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (token === "--context") {
|
||||
parsed.contextB64 = String(argv[i + 1] ?? "").trim();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(`Unknown argument: ${token}`);
|
||||
}
|
||||
|
||||
if (!parsed.handler) {
|
||||
throw new Error("Missing required --handler");
|
||||
}
|
||||
|
||||
if (!parsed.eventB64) {
|
||||
throw new Error("Missing required --event");
|
||||
}
|
||||
|
||||
if (!parsed.contextB64) {
|
||||
throw new Error("Missing required --context");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function decodeBase64Json(value, label) {
|
||||
try {
|
||||
const decoded = Buffer.from(value, "base64").toString("utf8");
|
||||
return JSON.parse(decoded);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to decode ${label}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function fileExists(filePath) {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTypeScriptCompiler() {
|
||||
if (process.env.CLAWSEC_DAST_DISABLE_TYPESCRIPT === "1") {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const imported = await import("typescript");
|
||||
return imported.default || imported;
|
||||
} catch {
|
||||
// Ignore and try require path next.
|
||||
}
|
||||
|
||||
try {
|
||||
const req = createRequire(import.meta.url);
|
||||
return req("typescript");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function importTypeScriptModule(tsPath) {
|
||||
const tsCompiler = await loadTypeScriptCompiler();
|
||||
if (!tsCompiler || typeof tsCompiler.transpileModule !== "function") {
|
||||
throw new Error(
|
||||
`Cannot execute TypeScript hook (${tsPath}): typescript compiler not available. ` +
|
||||
"Install 'typescript' or provide a JavaScript handler file.",
|
||||
);
|
||||
}
|
||||
|
||||
const source = await fs.readFile(tsPath, "utf8");
|
||||
const transpiled = tsCompiler.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: tsCompiler.ModuleKind.ESNext,
|
||||
target: tsCompiler.ScriptTarget.ES2022,
|
||||
moduleResolution: tsCompiler.ModuleResolutionKind.NodeNext,
|
||||
esModuleInterop: true,
|
||||
sourceMap: false,
|
||||
inlineSourceMap: false,
|
||||
declaration: false,
|
||||
},
|
||||
fileName: tsPath,
|
||||
reportDiagnostics: false,
|
||||
});
|
||||
|
||||
const tempFile = path.join(
|
||||
path.dirname(tsPath),
|
||||
`.clawsec-dast-${path.basename(tsPath, ".ts")}-${process.pid}-${Date.now()}.mjs`,
|
||||
);
|
||||
|
||||
await fs.writeFile(tempFile, transpiled.outputText, "utf8");
|
||||
|
||||
try {
|
||||
return await import(`${pathToFileURL(tempFile).href}?ts=${Date.now()}`);
|
||||
} finally {
|
||||
try {
|
||||
await fs.unlink(tempFile);
|
||||
} catch {
|
||||
// best-effort cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHookModule(handlerPath) {
|
||||
const fullPath = path.resolve(handlerPath);
|
||||
const exists = await fileExists(fullPath);
|
||||
if (!exists) {
|
||||
throw new Error(`Hook handler does not exist: ${fullPath}`);
|
||||
}
|
||||
|
||||
const ext = path.extname(fullPath).toLowerCase();
|
||||
|
||||
if (ext === ".ts") {
|
||||
return importTypeScriptModule(fullPath);
|
||||
}
|
||||
|
||||
return import(`${pathToFileURL(fullPath).href}?v=${Date.now()}`);
|
||||
}
|
||||
|
||||
function resolveHandlerExport(mod, exportName) {
|
||||
if (exportName && exportName !== "default") {
|
||||
if (typeof mod?.[exportName] === "function") {
|
||||
return mod[exportName];
|
||||
}
|
||||
throw new Error(`Hook export '${exportName}' is not a function`);
|
||||
}
|
||||
|
||||
if (typeof mod?.default === "function") {
|
||||
return mod.default;
|
||||
}
|
||||
|
||||
if (typeof mod?.handler === "function") {
|
||||
return mod.handler;
|
||||
}
|
||||
|
||||
throw new Error("Hook module does not export a handler function");
|
||||
}
|
||||
|
||||
function normalizeTimestamp(event) {
|
||||
const timestamp = event?.timestamp;
|
||||
if (typeof timestamp === "string" || typeof timestamp === "number") {
|
||||
const parsed = new Date(timestamp);
|
||||
if (!Number.isNaN(parsed.getTime())) {
|
||||
event.timestamp = parsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function summarizeMessages(messages) {
|
||||
if (!Array.isArray(messages)) {
|
||||
return {
|
||||
count: 0,
|
||||
charCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let charCount = 0;
|
||||
|
||||
for (const message of messages) {
|
||||
if (typeof message === "string") {
|
||||
charCount += message.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
charCount += JSON.stringify(message).length;
|
||||
} catch {
|
||||
charCount += 0;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
count: messages.length,
|
||||
charCount,
|
||||
};
|
||||
}
|
||||
|
||||
function coreEventShape(event) {
|
||||
return {
|
||||
type: event?.type ?? null,
|
||||
action: event?.action ?? null,
|
||||
sessionKey: event?.sessionKey ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const event = decodeBase64Json(args.eventB64, "event payload");
|
||||
const context = decodeBase64Json(args.contextB64, "context payload");
|
||||
|
||||
normalizeTimestamp(event);
|
||||
|
||||
const startedAt = Date.now();
|
||||
const before = coreEventShape(event);
|
||||
|
||||
try {
|
||||
const mod = await loadHookModule(args.handler);
|
||||
const handler = resolveHandlerExport(mod, args.exportName);
|
||||
|
||||
await handler(event, context);
|
||||
|
||||
const after = coreEventShape(event);
|
||||
const messageSummary = summarizeMessages(event?.messages);
|
||||
|
||||
const payload = {
|
||||
ok: true,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
core_before: before,
|
||||
core_after: after,
|
||||
messages_count: messageSummary.count,
|
||||
messages_char_count: messageSummary.charCount,
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(payload));
|
||||
} catch (error) {
|
||||
const after = coreEventShape(event);
|
||||
const messageSummary = summarizeMessages(event?.messages);
|
||||
|
||||
const payload = {
|
||||
ok: false,
|
||||
duration_ms: Date.now() - startedAt,
|
||||
core_before: before,
|
||||
core_after: after,
|
||||
messages_count: messageSummary.count,
|
||||
messages_char_count: messageSummary.charCount,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
|
||||
process.stdout.write(JSON.stringify(payload));
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack || error.message : String(error)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -73,6 +73,7 @@ function assertSourceHookExists() {
|
||||
"scripts/scan_dependencies.mjs",
|
||||
"scripts/sast_analyzer.mjs",
|
||||
"scripts/dast_runner.mjs",
|
||||
"scripts/dast_hook_executor.mjs",
|
||||
"scripts/query_cve_databases.mjs",
|
||||
];
|
||||
for (const file of requiredScripts) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "clawsec-scanner",
|
||||
"version": "0.0.1",
|
||||
"description": "Automated vulnerability scanner for agent platforms. Performs dependency scanning (npm audit, pip-audit), multi-database CVE lookup (OSV, NVD, GitHub Advisory), SAST analysis (Semgrep, Bandit), and basic DAST security testing for skill hooks.",
|
||||
"version": "0.0.2",
|
||||
"description": "Automated vulnerability scanner for agent platforms. Performs dependency scanning (npm audit, pip-audit), multi-database CVE lookup (OSV, NVD, GitHub Advisory), SAST analysis (Semgrep, Bandit), and agent-specific DAST hook execution testing for OpenClaw hooks.",
|
||||
"author": "prompt-security",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"homepage": "https://clawsec.prompt.security/",
|
||||
@@ -57,7 +57,12 @@
|
||||
{
|
||||
"path": "scripts/dast_runner.mjs",
|
||||
"required": true,
|
||||
"description": "Dynamic analysis framework for skill hook security testing"
|
||||
"description": "Dynamic analysis harness executing OpenClaw hook handlers with malicious-input and timeout checks"
|
||||
},
|
||||
{
|
||||
"path": "scripts/dast_hook_executor.mjs",
|
||||
"required": true,
|
||||
"description": "Isolated hook execution helper used by DAST for real OpenClaw harness testing"
|
||||
},
|
||||
{
|
||||
"path": "scripts/setup_scanner_hook.mjs",
|
||||
@@ -103,6 +108,11 @@
|
||||
"path": "test/sast_engine.test.mjs",
|
||||
"required": false,
|
||||
"description": "Unit tests for SAST analysis (Semgrep, Bandit)"
|
||||
},
|
||||
{
|
||||
"path": "test/dast_harness.test.mjs",
|
||||
"required": false,
|
||||
"description": "DAST harness tests for real hook execution and malicious-input failure detection"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
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 DAST_SCRIPT = path.join(SKILL_ROOT, "scripts", "dast_runner.mjs");
|
||||
|
||||
/**
|
||||
* @param {string} targetPath
|
||||
* @param {number} timeoutMs
|
||||
* @param {Record<string, string>} envOverrides
|
||||
* @returns {Promise<{code: number, stdout: string, stderr: string, report: any}>}
|
||||
*/
|
||||
async function runDast(targetPath, timeoutMs = 3000, envOverrides = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proc = spawn(
|
||||
"node",
|
||||
[DAST_SCRIPT, "--target", targetPath, "--format", "json", "--timeout", String(timeoutMs)],
|
||||
{
|
||||
cwd: SKILL_ROOT,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env: {
|
||||
...process.env,
|
||||
...envOverrides,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
proc.stdout.on("data", (chunk) => {
|
||||
stdout += String(chunk);
|
||||
});
|
||||
|
||||
proc.stderr.on("data", (chunk) => {
|
||||
stderr += String(chunk);
|
||||
});
|
||||
|
||||
proc.on("error", reject);
|
||||
|
||||
proc.on("close", (code) => {
|
||||
try {
|
||||
const parsed = JSON.parse(stdout.trim());
|
||||
resolve({
|
||||
code: code ?? 1,
|
||||
stdout,
|
||||
stderr,
|
||||
report: parsed,
|
||||
});
|
||||
} catch (error) {
|
||||
reject(new Error(`Failed to parse DAST JSON output: ${String(error)}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} hookDir
|
||||
* @param {string} eventsLiteral
|
||||
* @param {string} handlerSource
|
||||
* @param {string} [handlerFile]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function writeHookFixture(hookDir, eventsLiteral, handlerSource, handlerFile = "handler.js") {
|
||||
await fs.mkdir(hookDir, { recursive: true });
|
||||
|
||||
const hookMd = `---
|
||||
name: ${path.basename(hookDir)}
|
||||
description: fixture hook
|
||||
metadata: { "openclaw": { "events": [${eventsLiteral}] } }
|
||||
---
|
||||
|
||||
# Fixture Hook
|
||||
`;
|
||||
|
||||
await fs.writeFile(path.join(hookDir, "HOOK.md"), hookMd, "utf8");
|
||||
await fs.writeFile(path.join(hookDir, handlerFile), handlerSource, "utf8");
|
||||
}
|
||||
|
||||
async function testSafeHookExecutesAndDoesNotReportMisleadingHigh() {
|
||||
const testName = "DAST harness: executes real hook and reports no misleading high findings";
|
||||
const tmp = await createTempDir();
|
||||
|
||||
try {
|
||||
const targetPath = path.join(tmp.path, "skill");
|
||||
const hookDir = path.join(targetPath, "hooks", "safe-hook");
|
||||
const markerFile = path.join(hookDir, "executed.marker");
|
||||
|
||||
await writeHookFixture(
|
||||
hookDir,
|
||||
'"command:new"',
|
||||
`import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const handler = async (event, context) => {
|
||||
const marker = path.join(path.dirname(new URL(import.meta.url).pathname), "executed.marker");
|
||||
await fs.writeFile(marker, String(context?.event || "unknown"), "utf8");
|
||||
|
||||
if (!Array.isArray(event.messages)) {
|
||||
event.messages = [];
|
||||
}
|
||||
|
||||
event.messages.push("hook executed");
|
||||
};
|
||||
|
||||
export default handler;
|
||||
`,
|
||||
);
|
||||
|
||||
const result = await runDast(targetPath, 2500);
|
||||
const markerExists = await fs
|
||||
.access(markerFile)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
const cleanSummary =
|
||||
result.report?.summary?.critical === 0
|
||||
&& result.report?.summary?.high === 0
|
||||
&& result.report?.summary?.medium === 0
|
||||
&& result.report?.summary?.low === 0
|
||||
&& result.report?.summary?.info === 0;
|
||||
|
||||
if (result.code === 0 && markerExists && cleanSummary) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(
|
||||
testName,
|
||||
`Expected exit=0, markerExists=true, clean summary. Got exit=${result.code}, markerExists=${markerExists}, summary=${JSON.stringify(result.report?.summary)} stderr=${result.stderr}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
await tmp.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function testMaliciousCrashProducesHighFinding() {
|
||||
const testName = "DAST harness: malicious input crash is reported as high";
|
||||
const tmp = await createTempDir();
|
||||
|
||||
try {
|
||||
const targetPath = path.join(tmp.path, "skill");
|
||||
const hookDir = path.join(targetPath, "hooks", "crashy-hook");
|
||||
|
||||
await writeHookFixture(
|
||||
hookDir,
|
||||
'"message:preprocessed"',
|
||||
`const handler = async (event) => {
|
||||
const payload = String(event?.context?.content || "");
|
||||
if (payload.includes("<script>")) {
|
||||
throw new Error("Unhandled payload path");
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
||||
`,
|
||||
);
|
||||
|
||||
const result = await runDast(targetPath, 2500);
|
||||
const hasHigh = Number(result.report?.summary?.high || 0) > 0;
|
||||
const hasCrashFinding = Array.isArray(result.report?.vulnerabilities)
|
||||
&& result.report.vulnerabilities.some((v) => String(v.id || "").includes("DAST-MALICIOUS-CRASH"));
|
||||
|
||||
if (result.code === 1 && hasHigh && hasCrashFinding) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(
|
||||
testName,
|
||||
`Expected exit=1 and malicious crash high finding. Got exit=${result.code}, summary=${JSON.stringify(result.report?.summary)}, findings=${JSON.stringify(result.report?.vulnerabilities || [])}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
await tmp.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function testMissingTypeScriptCompilerIsCoverageInfo() {
|
||||
const testName = "DAST harness: missing TypeScript compiler reports coverage info, not high";
|
||||
const tmp = await createTempDir();
|
||||
|
||||
try {
|
||||
const targetPath = path.join(tmp.path, "skill");
|
||||
const hookDir = path.join(targetPath, "hooks", "ts-hook");
|
||||
|
||||
await writeHookFixture(
|
||||
hookDir,
|
||||
'"command:new"',
|
||||
`type Ctx = { dastMode?: boolean };
|
||||
|
||||
const handler = async (_event: unknown, _context: Ctx): Promise<void> => {
|
||||
return;
|
||||
};
|
||||
|
||||
export default handler;
|
||||
`,
|
||||
"handler.ts",
|
||||
);
|
||||
|
||||
const result = await runDast(
|
||||
targetPath,
|
||||
2500,
|
||||
{ CLAWSEC_DAST_DISABLE_TYPESCRIPT: "1" },
|
||||
);
|
||||
|
||||
const noHigh = Number(result.report?.summary?.high || 0) === 0
|
||||
&& Number(result.report?.summary?.critical || 0) === 0;
|
||||
const hasCoverageInfo = Array.isArray(result.report?.vulnerabilities)
|
||||
&& result.report.vulnerabilities.some((v) => String(v.id || "").includes("DAST-COVERAGE"));
|
||||
const hasInfoCount = Number(result.report?.summary?.info || 0) > 0;
|
||||
|
||||
if (result.code === 0 && noHigh && hasCoverageInfo && hasInfoCount) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(
|
||||
testName,
|
||||
`Expected coverage info only (no high/critical). Got exit=${result.code}, summary=${JSON.stringify(result.report?.summary)}, findings=${JSON.stringify(result.report?.vulnerabilities || [])}`,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
} finally {
|
||||
await tmp.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await testSafeHookExecutesAndDoesNotReportMisleadingHigh();
|
||||
await testMaliciousCrashProducesHighFinding();
|
||||
await testMissingTypeScriptCompilerIsCoverageInfo();
|
||||
|
||||
report();
|
||||
exitWithResults();
|
||||
}
|
||||
|
||||
await main();
|
||||
+5
-3
@@ -1,8 +1,8 @@
|
||||
# Wiki Generation Metadata
|
||||
|
||||
- Commit hash: `d5aadfbee15b48ebb4872dfb838e4df88c611d56`
|
||||
- Branch name: `codex/wiki-tab-ui`
|
||||
- Generation timestamp (local): `2026-02-26T09:16:02+0200`
|
||||
- Commit hash: `c3983a100581a9f27eb8cc3b5baa4f585e6c45e4`
|
||||
- Branch name: `codex/clawsec-scanner-0.0.2-dast-harness`
|
||||
- Generation timestamp (local): `2026-03-10T19:06:29+0200`
|
||||
- Generation mode: `update`
|
||||
- Output language: `English`
|
||||
- Assets copied into `wiki/assets/`:
|
||||
@@ -13,6 +13,7 @@
|
||||
## Notes
|
||||
- Migrated root documentation pages from `docs/` into dedicated `wiki/` operation pages.
|
||||
- Updated index and cross-links to use `wiki/` as the documentation source of truth.
|
||||
- Added a dedicated module page for `clawsec-scanner` and linked it from `wiki/INDEX.md`.
|
||||
- Future updates should preserve existing headings and append `Update Notes` sections when making deltas.
|
||||
|
||||
## Source References
|
||||
@@ -21,6 +22,7 @@
|
||||
- AGENTS.md
|
||||
- wiki/overview.md
|
||||
- wiki/architecture.md
|
||||
- wiki/modules/clawsec-scanner.md
|
||||
- wiki/dependencies.md
|
||||
- wiki/data-flow.md
|
||||
- wiki/glossary.md
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
## Modules
|
||||
- [Frontend Web App](modules/frontend-web.md)
|
||||
- [ClawSec Suite Core](modules/clawsec-suite.md)
|
||||
- [ClawSec Scanner](modules/clawsec-scanner.md)
|
||||
- [NanoClaw Integration](modules/nanoclaw-integration.md)
|
||||
- [Automation and Release Pipelines](modules/automation-release.md)
|
||||
- [Local Validation and Packaging Tools](modules/local-tooling.md)
|
||||
@@ -40,6 +41,7 @@
|
||||
- [Generation Metadata](GENERATION.md)
|
||||
|
||||
## Update Notes
|
||||
- 2026-03-10: Added ClawSec Scanner module documentation and linked it under Modules.
|
||||
- 2026-02-26: Added Operations pages and updated navigation guidance after migrating root docs into wiki pages.
|
||||
|
||||
## Source References
|
||||
@@ -50,4 +52,6 @@
|
||||
- scripts/populate-local-feed.sh
|
||||
- scripts/populate-local-skills.sh
|
||||
- skills/clawsec-suite/skill.json
|
||||
- skills/clawsec-scanner/skill.json
|
||||
- wiki/modules/clawsec-scanner.md
|
||||
- .github/workflows/ci.yml
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# Module: ClawSec Scanner
|
||||
|
||||
## Responsibilities
|
||||
- Provide multi-layer vulnerability scanning for OpenClaw-oriented skill repositories.
|
||||
- Orchestrate dependency, SAST, and DAST engines into a single report contract.
|
||||
- Execute real OpenClaw hook handlers in an isolated DAST harness to validate runtime security behavior.
|
||||
- Support periodic scan execution through an OpenClaw hook integration.
|
||||
- Normalize findings into severity buckets for downstream triage and automation.
|
||||
|
||||
## Key Files
|
||||
- `skills/clawsec-scanner/skill.json`: skill metadata, SBOM paths, trigger phrases.
|
||||
- `skills/clawsec-scanner/scripts/runner.sh`: main orchestrator for dependency/SAST/DAST scans.
|
||||
- `skills/clawsec-scanner/scripts/scan_dependencies.mjs`: `npm audit` + `pip-audit` parsing.
|
||||
- `skills/clawsec-scanner/scripts/sast_analyzer.mjs`: Semgrep and Bandit execution/parsing.
|
||||
- `skills/clawsec-scanner/scripts/dast_runner.mjs`: hook discovery + real harness DAST evaluation.
|
||||
- `skills/clawsec-scanner/scripts/dast_hook_executor.mjs`: isolated per-hook runtime executor.
|
||||
- `skills/clawsec-scanner/hooks/clawsec-scanner-hook/handler.ts`: periodic OpenClaw event hook.
|
||||
- `skills/clawsec-scanner/lib/report.mjs`: unified report generation and text/JSON formatting.
|
||||
|
||||
## Public Interfaces
|
||||
| Interface | Consumer | Behavior |
|
||||
| --- | --- | --- |
|
||||
| `runner.sh` CLI | Operators/automation | Runs all enabled scan engines and emits merged report output. |
|
||||
| `dast_runner.mjs` CLI | Operators/CI/hooks | Discovers hooks and runs isolated runtime DAST checks. |
|
||||
| OpenClaw scanner hook default export | OpenClaw runtime | Handles `agent:bootstrap` and `command:new` scanner trigger events. |
|
||||
| `ScanReport` JSON output | Humans and automation | Provides normalized severity summary + finding list. |
|
||||
|
||||
## Inputs and Outputs
|
||||
Inputs/outputs are summarized in the table below.
|
||||
|
||||
| Type | Name | Location | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| Input | Scan target path | `--target` CLI arg | Root directory where skills/hooks are scanned. |
|
||||
| Input | Dependency manifests | `package-lock.json`, `requirements.txt`, `pyproject.toml` | Drives dependency vulnerability checks. |
|
||||
| Input | Hook metadata and handlers | `**/HOOK.md`, `handler.{js,mjs,cjs,ts}` | DAST harness discovers and executes these handlers. |
|
||||
| Input | Env configuration | `CLAWSEC_*`, `GITHUB_TOKEN` | Controls engine behavior, severity filtering, and output paths. |
|
||||
| Output | Unified scan report | stdout or `--output` file | JSON/text report with severity summary and finding details. |
|
||||
| Output | Runtime hook alerts | OpenClaw `event.messages` | New vulnerability alerts pushed into conversations. |
|
||||
| Output | Scanner state file | `~/.openclaw/clawsec-scanner-state.json` by default | De-duplication memory for reported finding IDs. |
|
||||
|
||||
## Configuration
|
||||
| Variable | Default | Module Effect |
|
||||
| --- | --- | --- |
|
||||
| `CLAWSEC_SCANNER_INTERVAL` | `86400` | Minimum interval between periodic hook-triggered scans. |
|
||||
| `CLAWSEC_SCANNER_MIN_SEVERITY` | `medium` | Threshold for findings pushed to conversation alerts. |
|
||||
| `CLAWSEC_SCANNER_FORMAT` | `text` | Hook alert serialization format (`text` or `json`). |
|
||||
| `CLAWSEC_SKIP_DEPENDENCY_SCAN` | `0` | Disables dependency scanner when set to `1`. |
|
||||
| `CLAWSEC_SKIP_SAST` | `0` | Disables Semgrep/Bandit scanner when set to `1`. |
|
||||
| `CLAWSEC_SKIP_DAST` | `0` | Disables runtime hook DAST checks when set to `1`. |
|
||||
| `CLAWSEC_SKIP_CVE_LOOKUP` | `0` | Disables CVE enrichment stage when set to `1`. |
|
||||
| `CLAWSEC_DAST_HARNESS` | unset | Internal guard to avoid recursive scans during harness execution. |
|
||||
| `CLAWSEC_DAST_DISABLE_TYPESCRIPT` | unset | Test/debug switch forcing TypeScript harness coverage fallback mode. |
|
||||
|
||||
## DAST Harness Behavior
|
||||
- Hook discovery walks the target tree for `HOOK.md` and resolves adjacent handler files.
|
||||
- Each declared event key is executed in a separate Node subprocess via `dast_hook_executor.mjs`.
|
||||
- Findings are generated from real runtime behavior:
|
||||
- Baseline execution crash or timeout.
|
||||
- Malicious-input crash or timeout.
|
||||
- Output amplification beyond message/character thresholds.
|
||||
- Core event identity mutation (`type`, `action`, `sessionKey`).
|
||||
- Harness capability gaps (for example missing TypeScript compiler for `.ts` handlers) are reported as `info` coverage findings, not high-severity vulnerabilities.
|
||||
|
||||
## Example Snippets
|
||||
```bash
|
||||
# run scanner end-to-end
|
||||
bash skills/clawsec-scanner/scripts/runner.sh --target ./skills --format json
|
||||
```
|
||||
|
||||
```bash
|
||||
# run DAST harness directly
|
||||
node skills/clawsec-scanner/scripts/dast_runner.mjs --target ./skills --format text --timeout 30000
|
||||
```
|
||||
|
||||
## Tests
|
||||
| Test File | Focus |
|
||||
| --- | --- |
|
||||
| `skills/clawsec-scanner/test/dast_harness.test.mjs` | Real hook execution path, malicious crash detection, TypeScript coverage fallback semantics. |
|
||||
| `skills/clawsec-scanner/test/reviewer_regressions.test.mjs` | Runner behavior around non-zero DAST exit and merged reporting. |
|
||||
| `skills/clawsec-scanner/test/dependency_scanner.test.mjs` | Dependency scanner utility/report contracts. |
|
||||
| `skills/clawsec-scanner/test/sast_engine.test.mjs` | SAST parser/normalization behavior. |
|
||||
| `skills/clawsec-scanner/test/cve_integration.test.mjs` | OSV/NVD/GitHub enrichment integration checks. |
|
||||
|
||||
## Update Notes
|
||||
- 2026-03-10: Added module page for `clawsec-scanner` and documented the `0.0.2` real OpenClaw DAST harness execution model.
|
||||
|
||||
## Source References
|
||||
- skills/clawsec-scanner/skill.json
|
||||
- skills/clawsec-scanner/SKILL.md
|
||||
- skills/clawsec-scanner/CHANGELOG.md
|
||||
- skills/clawsec-scanner/scripts/runner.sh
|
||||
- skills/clawsec-scanner/scripts/scan_dependencies.mjs
|
||||
- skills/clawsec-scanner/scripts/sast_analyzer.mjs
|
||||
- skills/clawsec-scanner/scripts/dast_runner.mjs
|
||||
- skills/clawsec-scanner/scripts/dast_hook_executor.mjs
|
||||
- skills/clawsec-scanner/scripts/setup_scanner_hook.mjs
|
||||
- skills/clawsec-scanner/hooks/clawsec-scanner-hook/HOOK.md
|
||||
- skills/clawsec-scanner/hooks/clawsec-scanner-hook/handler.ts
|
||||
- skills/clawsec-scanner/lib/report.mjs
|
||||
- skills/clawsec-scanner/lib/utils.mjs
|
||||
- skills/clawsec-scanner/test/dast_harness.test.mjs
|
||||
- skills/clawsec-scanner/test/reviewer_regressions.test.mjs
|
||||
Reference in New Issue
Block a user