refactor: remove PR_NOTES.md and update documentation in README.md and SKILL.md

feat: add input validation for skill slug and version in check_clawhub_reputation.mjs
fix: enhance argument parsing in enhanced_guarded_install.mjs
test: add reputation check tests for input validation and output formatting
chore: delete unused update_suite_catalog.mjs script
This commit is contained in:
David Abutbul
2026-02-16 20:32:51 +02:00
parent 269ff94b84
commit 765255680c
10 changed files with 387 additions and 117 deletions
@@ -1,26 +0,0 @@
# PR Notes for ClawSec ClawHub Checker
## Important Limitation Notice
This skill currently catches **VirusTotal Code Insight flags** but cannot access **OpenClaw internal check results** because:
1. **VirusTotal flags** are exposed via `clawhub install` command output (we parse stderr)
2. **OpenClaw internal checks** are only shown on the ClawHub website, not exposed via API
## Example from `clawsec-suite` page:
-**VirusTotal**: "Benign"
- ⚠️ **OpenClaw internal check**: "The package is internally consistent with a feed-monitoring / advisory-guardian purpose, but a few operational details and optional bypasses deserve attention before installing."
## Recommendation for ClawHub
Expose internal check results via:
- `clawhub inspect --json` endpoint
- Additional API field for security tools
- Or at minimum, include in `clawhub install` warning output
## Current Workaround
Our heuristic checks (skill age, author reputation, downloads, updates) provide similar risk assessment but miss specific operational warnings about bypasses, missing signatures, etc.
## PR Should Include
1. This skill as defense-in-depth layer
2. Feature request to ClawHub for exposing internal check data
3. Documentation about the limitation
+1 -1
View File
@@ -120,4 +120,4 @@ node scripts/setup_reputation_hook.mjs
## License
MIT - Part of the ClawSec security suite
MIT - Part of the ClawSec security suite
+1 -1
View File
@@ -137,4 +137,4 @@ To modify the reputation checking logic, edit:
## License
MIT - Part of the ClawSec security suite
MIT - Part of the ClawSec security suite
@@ -93,4 +93,4 @@ export function formatReputationWarning(reputationInfo) {
lines.push("This skill has low reputation score. Review carefully before installation.");
return lines.join("\n");
}
}
@@ -17,6 +17,20 @@ export async function checkClawhubReputation(skillSlug, version, threshold = 70)
virustotal: [],
};
// Input validation — reject anything that isn't a safe slug or semver
if (!/^[a-z0-9][a-z0-9-]*$/.test(skillSlug)) {
result.warnings.push(`Invalid skill slug: ${skillSlug}`);
result.score = 0;
result.safe = false;
return result;
}
if (version && !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?$/.test(version)) {
result.warnings.push(`Invalid version format: ${version}`);
result.score = 0;
result.safe = false;
return result;
}
try {
// Check 1: Try to inspect the skill via clawhub
const inspectResult = spawnSync(
@@ -101,14 +115,15 @@ export async function checkClawhubReputation(skillSlug, version, threshold = 70)
}
// Check 6: Try installation to see if clawhub flags it as suspicious
// Use --force to bypass interactive prompt in non-interactive mode
const installCheck = spawnSync(
"bash",
["-c", `echo "n" | clawhub install ${skillSlug}${version ? ` --version ${version}` : ''} 2>&1`],
{ encoding: "utf-8" }
);
// Use input:"n\n" to decline the interactive prompt (avoids shell interpolation)
const installArgs = ["install", skillSlug];
if (version) installArgs.push("--version", version);
const installCheck = spawnSync("clawhub", installArgs, {
input: "n\n",
encoding: "utf-8",
});
const output = installCheck.stdout || installCheck.stderr || "";
const output = (installCheck.stdout || "") + (installCheck.stderr || "");
if (output.includes("suspicious") || output.includes("VirusTotal") || output.includes("flagged")) {
result.virustotal.push("Flagged by ClawHub's VirusTotal Code Insight");
result.score -= 40; // More severe penalty for VirusTotal flag
@@ -182,4 +197,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
}
main().catch(console.error);
}
}
@@ -198,4 +198,4 @@ async function main() {
}
}
main();
main();
@@ -134,4 +134,4 @@ process.exit(result.status ?? 1);
}
}
main().catch(console.error);
main().catch(console.error);
+7 -2
View File
@@ -2,7 +2,7 @@
"name": "clawsec-clawhub-checker",
"version": "0.0.1",
"description": "ClawHub reputation checker for ClawSec suite. Enhances guarded skill installer with VirusTotal Code Insight reputation scores and additional safety checks.",
"author": "david",
"author": "abutbul",
"license": "MIT",
"homepage": "https://clawsec.prompt.security/",
"keywords": [
@@ -42,6 +42,11 @@
"path": "hooks/clawsec-advisory-guardian/lib/reputation.mjs",
"required": true,
"description": "Reputation checking module for advisory guardian hook"
},
{
"path": "README.md",
"required": false,
"description": "Additional documentation and development guide"
}
]
},
@@ -78,4 +83,4 @@
"skill security score"
]
}
}
}
@@ -0,0 +1,352 @@
#!/usr/bin/env node
/**
* Reputation check tests for clawsec-clawhub-checker.
*
* Tests cover:
* - Input validation (command injection prevention)
* - Reputation scoring with mocked clawhub output
* - formatReputationWarning output formatting
* - Enhanced installer argument parsing
*
* Run: node skills/clawsec-clawhub-checker/test/reputation_check.test.mjs
*/
import { fileURLToPath } from "node:url";
import path from "node:path";
import { spawn } from "node:child_process";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CHECKER_SCRIPT = path.resolve(__dirname, "..", "scripts", "check_clawhub_reputation.mjs");
const ENHANCED_INSTALL_SCRIPT = path.resolve(__dirname, "..", "scripts", "enhanced_guarded_install.mjs");
let passCount = 0;
let failCount = 0;
function pass(name) {
passCount++;
console.log(`\u2713 ${name}`);
}
function fail(name, error) {
failCount++;
console.error(`\u2717 ${name}`);
console.error(` ${String(error)}`);
}
function runScript(scriptPath, args, env) {
return new Promise((resolve) => {
const proc = spawn("node", [scriptPath, ...args], {
env: { ...process.env, ...env },
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
proc.stdout.on("data", (data) => {
stdout += data.toString();
});
proc.stderr.on("data", (data) => {
stderr += data.toString();
});
proc.on("close", (code) => {
resolve({ code, stdout, stderr });
});
});
}
// -----------------------------------------------------------------------------
// Test: Invalid skill slug is rejected (command injection prevention)
// -----------------------------------------------------------------------------
async function testInvalidSlugRejected() {
const testName = "reputation_check: invalid slug with shell metacharacters is rejected";
try {
const result = await runScript(CHECKER_SCRIPT, ['test; rm -rf /', '', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
if (parsed.score === 0 && parsed.safe === false && parsed.warnings.some(w => w.includes("Invalid skill slug"))) {
pass(testName);
} else {
fail(testName, `Expected score 0 with invalid slug warning, got: ${JSON.stringify(parsed)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Invalid version format is rejected (command injection prevention)
// -----------------------------------------------------------------------------
async function testInvalidVersionRejected() {
const testName = "reputation_check: invalid version with shell metacharacters is rejected";
try {
const result = await runScript(CHECKER_SCRIPT, ['test-skill', '1.0.0; curl evil.com', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
if (parsed.score === 0 && parsed.safe === false && parsed.warnings.some(w => w.includes("Invalid version format"))) {
pass(testName);
} else {
fail(testName, `Expected score 0 with invalid version warning, got: ${JSON.stringify(parsed)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Valid slug and version pass input validation
// -----------------------------------------------------------------------------
async function testValidInputsAccepted() {
const testName = "reputation_check: valid slug and semver pass input validation";
try {
// clawhub is not installed, so the check will fail at the inspect step,
// but it should NOT fail at input validation
const result = await runScript(CHECKER_SCRIPT, ['my-test-skill', '1.0.0', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
// Should not contain input validation errors
const hasInputError = parsed.warnings.some(
w => w.includes("Invalid skill slug") || w.includes("Invalid version format")
);
if (!hasInputError) {
pass(testName);
} else {
fail(testName, `Valid inputs were rejected: ${JSON.stringify(parsed.warnings)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Slug with uppercase or special chars is rejected
// -----------------------------------------------------------------------------
async function testUppercaseSlugRejected() {
const testName = "reputation_check: uppercase slug is rejected";
try {
const result = await runScript(CHECKER_SCRIPT, ['Test-Skill', '1.0.0', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
if (parsed.score === 0 && parsed.safe === false) {
pass(testName);
} else {
fail(testName, `Expected uppercase slug to be rejected, got: ${JSON.stringify(parsed)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Empty slug shows usage error
// -----------------------------------------------------------------------------
async function testEmptySlugShowsUsage() {
const testName = "reputation_check: empty slug shows usage error";
try {
const result = await runScript(CHECKER_SCRIPT, []);
if (result.code === 1 && result.stderr.includes("Usage:")) {
pass(testName);
} else {
fail(testName, `Expected exit 1 with usage message, got code ${result.code}: ${result.stderr}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Version with pre-release tag is accepted
// -----------------------------------------------------------------------------
async function testPreReleaseVersionAccepted() {
const testName = "reputation_check: pre-release version format is accepted";
try {
const result = await runScript(CHECKER_SCRIPT, ['test-skill', '1.0.0-beta.1', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output: ${result.stdout}`);
return;
}
const hasVersionError = parsed.warnings.some(w => w.includes("Invalid version format"));
if (!hasVersionError) {
pass(testName);
} else {
fail(testName, `Pre-release version was rejected: ${JSON.stringify(parsed.warnings)}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Enhanced installer rejects invalid skill name
// -----------------------------------------------------------------------------
async function testEnhancedInstallerRejectsInvalidSkill() {
const testName = "enhanced_install: rejects skill name with invalid characters";
try {
const result = await runScript(ENHANCED_INSTALL_SCRIPT, ['--skill', 'bad skill!']);
if (result.code === 1 && result.stderr.includes("Invalid --skill value")) {
pass(testName);
} else {
fail(testName, `Expected exit 1 with invalid skill error, got code ${result.code}: ${result.stderr}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Enhanced installer requires --skill argument
// -----------------------------------------------------------------------------
async function testEnhancedInstallerRequiresSkill() {
const testName = "enhanced_install: requires --skill argument";
try {
const result = await runScript(ENHANCED_INSTALL_SCRIPT, []);
if (result.code === 1 && result.stderr.includes("Missing required argument")) {
pass(testName);
} else {
fail(testName, `Expected exit 1 with missing argument error, got code ${result.code}: ${result.stderr}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Enhanced installer rejects invalid threshold
// -----------------------------------------------------------------------------
async function testEnhancedInstallerRejectsInvalidThreshold() {
const testName = "enhanced_install: rejects invalid reputation threshold";
try {
const result = await runScript(ENHANCED_INSTALL_SCRIPT, [
'--skill', 'test-skill', '--reputation-threshold', '150'
]);
if (result.code === 1 && result.stderr.includes("Invalid --reputation-threshold")) {
pass(testName);
} else {
fail(testName, `Expected exit 1 with invalid threshold error, got code ${result.code}: ${result.stderr}`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: formatReputationWarning
// -----------------------------------------------------------------------------
async function testFormatReputationWarning() {
const testName = "reputation: formatReputationWarning formats correctly";
try {
const { formatReputationWarning } = await import(
path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib", "reputation.mjs")
);
// Safe reputation — should return empty
const safeResult = formatReputationWarning({ score: 80, warnings: [] });
if (safeResult !== "") {
fail(testName, `Expected empty string for safe score, got: "${safeResult}"`);
return;
}
// Unsafe reputation — should contain warning
const unsafeResult = formatReputationWarning({ score: 45, warnings: ["Low downloads", "New author"] });
if (
unsafeResult.includes("REPUTATION WARNING") &&
unsafeResult.includes("45/100") &&
unsafeResult.includes("Low downloads") &&
unsafeResult.includes("New author")
) {
pass(testName);
} else {
fail(testName, `Unexpected format: "${unsafeResult}"`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: formatReputationWarning handles null/undefined
// -----------------------------------------------------------------------------
async function testFormatReputationWarningNull() {
const testName = "reputation: formatReputationWarning handles null input";
try {
const { formatReputationWarning } = await import(
path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib", "reputation.mjs")
);
const nullResult = formatReputationWarning(null);
const undefinedResult = formatReputationWarning(undefined);
if (nullResult === "" && undefinedResult === "") {
pass(testName);
} else {
fail(testName, `Expected empty for null/undefined, got: "${nullResult}", "${undefinedResult}"`);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Main test runner
// -----------------------------------------------------------------------------
async function runTests() {
console.log("=== ClawSec ClawHub Checker Tests ===\n");
await testInvalidSlugRejected();
await testInvalidVersionRejected();
await testValidInputsAccepted();
await testUppercaseSlugRejected();
await testEmptySlugShowsUsage();
await testPreReleaseVersionAccepted();
await testEnhancedInstallerRejectsInvalidSkill();
await testEnhancedInstallerRequiresSkill();
await testEnhancedInstallerRejectsInvalidThreshold();
await testFormatReputationWarning();
await testFormatReputationWarningNull();
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
if (failCount > 0) {
process.exit(1);
}
}
runTests().catch((error) => {
console.error("Test runner failed:", error);
process.exit(1);
});
@@ -1,76 +0,0 @@
#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
async function updateSuiteCatalog() {
const suiteDir = "/home/david/.openclaw-clean/workspace/clawsec-suite";
const skillJsonPath = path.join(suiteDir, "skill.json");
try {
const skillJson = JSON.parse(await fs.readFile(skillJsonPath, "utf8"));
// Add clawsec-clawhub-checker to catalog
if (!skillJson.catalog) {
skillJson.catalog = {
description: "Available protections in the ClawSec suite",
base_url: "https://clawsec.prompt.security/releases/download",
skills: {}
};
}
skillJson.catalog.skills["clawsec-clawhub-checker"] = {
description: "ClawHub reputation checker - enhances guarded installer with VirusTotal scores",
default_install: false,
compatible: ["openclaw", "moltbot", "clawdbot", "other"],
note: "Requires clawsec-suite as base"
};
// Also update embedded_components if it exists
if (skillJson.embedded_components) {
skillJson.embedded_components["clawsec-clawhub-checker"] = {
source_skill: "clawsec-clawhub-checker",
source_version: "0.1.0",
capabilities: [
"ClawHub reputation checking",
"VirusTotal Code Insight integration",
"Skill age and author reputation analysis",
"Enhanced double confirmation for suspicious skills"
],
standalone_available: false,
depends_on: ["clawsec-suite"]
};
}
await fs.writeFile(skillJsonPath, JSON.stringify(skillJson, null, 2));
console.log(`✓ Updated ${skillJsonPath} with clawsec-clawhub-checker catalog entry`);
// Also update the local copy for PR
const localSuiteDir = "/tmp/clawsec-repo/skills/clawsec-suite";
const localSkillJsonPath = path.join(localSuiteDir, "skill.json");
try {
const localSkillJson = JSON.parse(await fs.readFile(localSkillJsonPath, "utf8"));
if (localSkillJson.catalog) {
localSkillJson.catalog.skills["clawsec-clawhub-checker"] = {
description: "ClawHub reputation checker - enhances guarded installer with VirusTotal scores",
default_install: false,
compatible: ["openclaw", "moltbot", "clawdbot", "other"],
note: "Requires clawsec-suite as base"
};
await fs.writeFile(localSkillJsonPath, JSON.stringify(localSkillJson, null, 2));
console.log(`✓ Updated local repo ${localSkillJsonPath} for PR`);
}
} catch (localError) {
console.log(`Note: Could not update local repo: ${localError.message}`);
}
} catch (error) {
console.error("Failed to update suite catalog:", error.message);
process.exit(1);
}
}
updateSuiteCatalog().catch(console.error);