From 6893390ab4ded2bf282377e2c8e365be7dad03fc Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 16 Feb 2026 20:56:44 +0200 Subject: [PATCH] feat: enhance clawsec-clawhub-checker with setup script and reputation checks --- skills/clawsec-clawhub-checker/SKILL.md | 11 ++- .../lib/reputation.mjs | 11 +-- .../scripts/check_clawhub_reputation.mjs | 15 ++-- .../scripts/enhanced_guarded_install.mjs | 71 ++++++++++++------- .../scripts/setup_reputation_hook.mjs | 35 ++++----- skills/clawsec-clawhub-checker/skill.json | 5 ++ 6 files changed, 97 insertions(+), 51 deletions(-) diff --git a/skills/clawsec-clawhub-checker/SKILL.md b/skills/clawsec-clawhub-checker/SKILL.md index af7907f..6df79c3 100644 --- a/skills/clawsec-clawhub-checker/SKILL.md +++ b/skills/clawsec-clawhub-checker/SKILL.md @@ -32,9 +32,15 @@ npx clawhub@latest install clawsec-suite # Then install the checker npx clawhub@latest install clawsec-clawhub-checker + +# Run the setup script to integrate with clawsec-suite +node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs + +# Restart OpenClaw gateway for changes to take effect +openclaw gateway restart ``` -The checker will automatically enhance the existing `guarded_skill_install.mjs` script and advisory guardian hook. +After setup, the checker enhances the existing `guarded_skill_install.mjs` script and advisory guardian hook. ## How It Works @@ -53,11 +59,10 @@ The enhanced flow: ### Reputation Signals Checked -1. **VirusTotal Code Insight** - Malicious code patterns detection +1. **VirusTotal Code Insight** - Malicious code patterns, external dependencies (Docker usage, network calls, eval usage, crypto keys) 2. **Skill age & updates** - New skills vs established ones 3. **Author reputation** - Other skills by same author 4. **Download statistics** - Popularity signals -5. **External dependencies** - Docker, network calls, eval usage ### Exit Codes diff --git a/skills/clawsec-clawhub-checker/hooks/clawsec-advisory-guardian/lib/reputation.mjs b/skills/clawsec-clawhub-checker/hooks/clawsec-advisory-guardian/lib/reputation.mjs index 6892894..6dfc378 100644 --- a/skills/clawsec-clawhub-checker/hooks/clawsec-advisory-guardian/lib/reputation.mjs +++ b/skills/clawsec-clawhub-checker/hooks/clawsec-advisory-guardian/lib/reputation.mjs @@ -1,4 +1,6 @@ import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; /** * Check reputation for a skill @@ -17,11 +19,12 @@ export async function checkReputation(skillName, version) { // Try to get skill slug from directory name or skill.json // For now, use skillName as slug (simplified) const skillSlug = skillName.toLowerCase().replace(/[^a-z0-9-]/g, '-'); - + // Run the reputation check script - const checkScript = new URL(import.meta.url); - const scriptDir = checkScript.pathname.split('/').slice(0, -3).join('/'); // Go up from lib - const checkerDir = scriptDir.replace('/hooks/clawsec-advisory-guardian/lib', ''); + // Current file is at: .../hooks/clawsec-advisory-guardian/lib/reputation.mjs + // We need to go up 3 levels to get to the skill root directory + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + const checkerDir = path.resolve(__dirname, '../../..'); const reputationCheck = spawnSync( "node", diff --git a/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs b/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs index a020845..18cd0ec 100644 --- a/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs +++ b/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs @@ -24,7 +24,10 @@ export async function checkClawhubReputation(skillSlug, version, threshold = 70) result.safe = false; return result; } - if (version && !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?$/.test(version)) { + // Semver validation: supports major.minor.patch with optional pre-release and build metadata + // Examples: 1.0.0, 1.0.0-alpha.1, 1.0.0-beta+20130313144700 + // More restrictive than full semver spec for security (prevents command injection) + if (version && !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$/.test(version)) { result.warnings.push(`Invalid version format: ${version}`); result.score = 0; result.safe = false; @@ -114,12 +117,16 @@ export async function checkClawhubReputation(skillSlug, version, threshold = 70) } } - // Check 6: Try installation to see if clawhub flags it as suspicious - // Use input:"n\n" to decline the interactive prompt (avoids shell interpolation) + // Check 6: Try installation to detect VirusTotal Code Insight warnings + // Note: This approach has potential side effects: + // - May download/cache skill metadata before declining + // - Depends on clawhub's prompting behavior (sending "n\n" to decline) + // - If clawhub inspect provided security flags, we'd use that instead + // This is the only way to programmatically access VirusTotal warnings currently const installArgs = ["install", skillSlug]; if (version) installArgs.push("--version", version); const installCheck = spawnSync("clawhub", installArgs, { - input: "n\n", + input: "n\n", // Automatically decline the installation prompt encoding: "utf-8", }); diff --git a/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs b/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs index dbc4fc6..25fc038 100644 --- a/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs +++ b/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs @@ -31,15 +31,27 @@ function printUsage() { } function parseArgs(argv) { + // Parse and validate CLAWHUB_REPUTATION_THRESHOLD environment variable + let defaultThreshold = 70; + const envThreshold = process.env.CLAWHUB_REPUTATION_THRESHOLD; + + if (envThreshold !== undefined && envThreshold !== "") { + const parsedEnv = parseInt(envThreshold, 10); + if (Number.isNaN(parsedEnv) || parsedEnv < 0 || parsedEnv > 100) { + throw new Error( + `Invalid CLAWHUB_REPUTATION_THRESHOLD environment variable: "${envThreshold}". Must be between 0 and 100.` + ); + } + defaultThreshold = parsedEnv; + } + const parsed = { skill: "", version: "", confirmAdvisory: false, confirmReputation: false, dryRun: false, - reputationThreshold: process.env.CLAWHUB_REPUTATION_THRESHOLD - ? parseInt(process.env.CLAWHUB_REPUTATION_THRESHOLD, 10) - : 70, + reputationThreshold: defaultThreshold, }; for (let i = 0; i < argv.length; i += 1) { @@ -83,8 +95,9 @@ function parseArgs(argv) { if (!parsed.skill) { throw new Error("Missing required argument: --skill"); } - if (!/^[a-z0-9-]+$/.test(parsed.skill)) { - throw new Error("Invalid --skill value. Use lowercase letters, digits, and hyphens only."); + // Must start with alphanumeric, then can contain hyphens (matches check_clawhub_reputation.mjs validation) + if (!/^[a-z0-9][a-z0-9-]*$/.test(parsed.skill)) { + throw new Error("Invalid --skill value. Must start with a letter or digit, followed by lowercase letters, digits, and hyphens."); } if (parsed.reputationThreshold < 0 || parsed.reputationThreshold > 100 || Number.isNaN(parsed.reputationThreshold)) { throw new Error("Invalid --reputation-threshold value. Must be between 0 and 100."); @@ -93,6 +106,28 @@ function parseArgs(argv) { return parsed; } +function buildOriginalArgs(argv) { + // Filter out reputation-specific arguments that the original script doesn't understand + const originalArgs = []; + + for (let i = 0; i < argv.length; i++) { + const token = argv[i]; + + if (token === "--confirm-reputation" || token === "--reputation-threshold") { + // Skip reputation-specific flags + if (token === "--reputation-threshold" && i + 1 < argv.length) { + // Also skip the value associated with --reputation-threshold + i += 1; + } + continue; + } + + originalArgs.push(token); + } + + return originalArgs; +} + async function runOriginalGuardedInstall(args) { // Find the original guarded_skill_install.mjs from clawsec-suite const suiteDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-suite"); @@ -104,17 +139,14 @@ async function runOriginalGuardedInstall(args) { throw new Error(`Original guarded_skill_install.mjs not found at ${originalScript}. Is clawsec-suite installed?`); } - const env = { ...process.env }; - if (args.confirmAdvisory) { - env.CLAWSEC_ALLOW_UNSIGNED_FEED = "1"; // Pass through to original script - } - + // Pass through environment without modification + // The original guarded_skill_install.mjs handles --confirm-advisory properly const child = spawnSync( "node", [originalScript, ...args.originalArgs], { stdio: "inherit", - env, + env: process.env, cwd: suiteDir, }, ); @@ -127,20 +159,11 @@ async function runOriginalGuardedInstall(args) { async function main() { try { - const args = parseArgs(process.argv.slice(2)); - - // Build args for original script (excluding reputation-specific args) - const originalArgs = []; - for (let i = 0; i < process.argv.slice(2).length; i++) { - const token = process.argv.slice(2)[i]; - if (token === "--confirm-reputation" || token === "--reputation-threshold") { - i += token === "--reputation-threshold" ? 1 : 0; - continue; - } - originalArgs.push(token); - } + const cliArgs = process.argv.slice(2); + const args = parseArgs(cliArgs); - args.originalArgs = originalArgs; + // Build args for original script (excluding reputation-specific args) + args.originalArgs = buildOriginalArgs(cliArgs); // Step 1: Check reputation (unless already confirmed) if (!args.confirmReputation) { diff --git a/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs b/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs index e1ef955..c78e364 100644 --- a/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs +++ b/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs @@ -34,18 +34,25 @@ async function main() { // Check if already imported if (!handlerContent.includes("from \"./lib/reputation.mjs\"")) { + // WARNING: This setup script uses string manipulation to modify handler.ts + // This is fragile and may break if the handler structure changes + // Consider using AST-based transformation or manual integration for production use + // Add import after other imports const importIndex = handlerContent.lastIndexOf("import"); + if (importIndex === -1) { + throw new Error("Could not find import statements in handler.ts. Manual integration required."); + } + const lineEndIndex = handlerContent.indexOf("\n", importIndex); - const newImport = `import { checkReputation } from "./lib/reputation.mjs";\n`; handlerContent = handlerContent.slice(0, lineEndIndex + 1) + newImport + handlerContent.slice(lineEndIndex + 1); - + // Find where matches are processed and add reputation check const findMatchesLine = handlerContent.indexOf("const matches = findMatches(feed, installedSkills);"); if (findMatchesLine !== -1) { const insertIndex = handlerContent.indexOf("\n", findMatchesLine) + 1; - + const reputationCheckCode = ` // ClawHub reputation check for matched skills for (const match of matches) { @@ -57,21 +64,17 @@ async function main() { } } `; - + handlerContent = handlerContent.slice(0, insertIndex) + reputationCheckCode + handlerContent.slice(insertIndex); + } else { + console.warn("⚠️ Warning: Could not find 'const matches = findMatches(feed, installedSkills);' in handler.ts"); + console.warn(" Reputation checks will not be added to the hook. Manual integration may be required."); } - - // Update alert message building to include reputation warnings - const buildAlertLine = handlerContent.indexOf("const alertMessage = buildAlertMessage(match);"); - if (buildAlertLine !== -1) { - const lineStart = handlerContent.lastIndexOf("\n", buildAlertLine) + 1; - const lineEnd = handlerContent.indexOf("\n", buildAlertLine); - // oldLine variable removed as it's unused - - const newLine = `const alertMessage = buildAlertMessage(match, match.reputationWarning ? { score: match.reputationScore, warnings: match.reputationWarnings } : undefined);`; - handlerContent = handlerContent.slice(0, lineStart) + newLine + handlerContent.slice(lineEnd); - } - + + // Note: Reputation information is attached to each match object (reputationWarning, reputationScore, reputationWarnings) + // The advisory guardian hook can consume this data if needed. We don't modify buildAlertMessage calls + // since the function signature is buildAlertMessage(matches[], installRoot) and changing it would break compatibility. + await fs.writeFile(hookHandlerPath, handlerContent); console.log(`✓ Updated hook handler with reputation checks`); } else { diff --git a/skills/clawsec-clawhub-checker/skill.json b/skills/clawsec-clawhub-checker/skill.json index 6c5a812..08c0ea7 100644 --- a/skills/clawsec-clawhub-checker/skill.json +++ b/skills/clawsec-clawhub-checker/skill.json @@ -47,6 +47,11 @@ "path": "README.md", "required": false, "description": "Additional documentation and development guide" + }, + { + "path": "test/reputation_check.test.mjs", + "required": false, + "description": "Test suite for reputation checking functionality" } ] },