From 777ff98d63d20b8b4d6be7458c140a15eded7863 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 16 Feb 2026 12:43:01 +0200 Subject: [PATCH 1/8] feat(skills): add clawsec-clawhub-checker reputation checking skill - Adds ClawHub reputation checks to guarded installer - Integrates VirusTotal Code Insight scores - Requires --confirm-reputation for suspicious skills - Enhances advisory guardian hook with reputation warnings - Defense-in-depth layer for skill installation security --- skills/clawsec-clawhub-checker/PR_NOTES.md | 26 +++ skills/clawsec-clawhub-checker/README.md | 123 +++++++++++ skills/clawsec-clawhub-checker/SKILL.md | 140 ++++++++++++ .../lib/reputation.mjs | 96 +++++++++ .../scripts/check_clawhub_reputation.mjs | 188 ++++++++++++++++ .../scripts/enhanced_guarded_install.mjs | 201 ++++++++++++++++++ .../scripts/setup_reputation_hook.mjs | 137 ++++++++++++ skills/clawsec-clawhub-checker/skill.json | 81 +++++++ .../update_suite_catalog.mjs | 77 +++++++ skills/clawsec-suite/skill.json | 13 +- 10 files changed, 1081 insertions(+), 1 deletion(-) create mode 100644 skills/clawsec-clawhub-checker/PR_NOTES.md create mode 100644 skills/clawsec-clawhub-checker/README.md create mode 100644 skills/clawsec-clawhub-checker/SKILL.md create mode 100644 skills/clawsec-clawhub-checker/hooks/clawsec-advisory-guardian/lib/reputation.mjs create mode 100644 skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs create mode 100644 skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs create mode 100644 skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs create mode 100644 skills/clawsec-clawhub-checker/skill.json create mode 100644 skills/clawsec-clawhub-checker/update_suite_catalog.mjs diff --git a/skills/clawsec-clawhub-checker/PR_NOTES.md b/skills/clawsec-clawhub-checker/PR_NOTES.md new file mode 100644 index 0000000..ad9a5ba --- /dev/null +++ b/skills/clawsec-clawhub-checker/PR_NOTES.md @@ -0,0 +1,26 @@ +# 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 \ No newline at end of file diff --git a/skills/clawsec-clawhub-checker/README.md b/skills/clawsec-clawhub-checker/README.md new file mode 100644 index 0000000..2c3b927 --- /dev/null +++ b/skills/clawsec-clawhub-checker/README.md @@ -0,0 +1,123 @@ +# ClawSec ClawHub Checker + +A ClawSec suite skill that enhances the guarded skill installer with ClawHub reputation checks and VirusTotal Code Insight integration. + +## Purpose + +Adds a second layer of security to skill installation by: +1. Checking ClawHub's VirusTotal Code Insight reputation scores +2. Analyzing skill age, author reputation, and download statistics +3. Requiring double confirmation for suspicious skills +4. Integrating with existing ClawSec advisory checks + +## Architecture + +``` +clawsec-suite (base) +└── clawsec-clawhub-checker (enhancement) + ├── enhanced_guarded_install.mjs - Main enhanced installer + ├── check_clawhub_reputation.mjs - Reputation checking logic + ├── setup_reputation_hook.mjs - Integration script + └── hooks/ - Enhanced advisory guardian hook +``` + +## Installation + +```bash +# First install the base suite +npx clawhub install clawsec-suite + +# Then install the checker +npx clawhub install clawsec-clawhub-checker + +# Run setup to integrate with existing suite +node scripts/setup_reputation_hook.mjs + +# Restart OpenClaw gateway +openclaw gateway restart +``` + +## Usage + +### Enhanced Guarded Installer + +```bash +# Basic usage (includes reputation checks) +node scripts/enhanced_guarded_install.mjs --skill some-skill --version 1.0.0 + +# With reputation confirmation override +node scripts/enhanced_guarded_install.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation + +# Adjust reputation threshold (default: 70) +node scripts/enhanced_guarded_install.mjs --skill some-skill --reputation-threshold 80 +``` + +### Reputation Check Only + +```bash +# Check reputation without installation +node scripts/check_clawhub_reputation.mjs some-skill 1.0.0 70 +``` + +## Exit Codes + +- `0` - Safe to install +- `42` - Advisory match found (requires `--confirm-advisory`) +- `43` - Reputation warning (requires `--confirm-reputation`) - **NEW** +- `1` - Error + +## Reputation Signals Checked + +1. **VirusTotal Code Insight** - Malicious code patterns +2. **Skill Age** - New skills (<7 days) are riskier +3. **Author Reputation** - Number of published skills +4. **Update Frequency** - Stale skills (>90 days) +5. **Download Statistics** - Low download counts +6. **Version Existence** - Specified version availability + +## Configuration + +Environment variables: +- `CLAWHUB_REPUTATION_THRESHOLD` - Minimum score (0-100, default: 70) +- `CLAWHUB_ALLOW_SUSPICIOUS` - Skip reputation checks (not recommended) + +## Integration Points + +1. **Enhanced `guarded_skill_install.mjs`** - Wraps original with reputation checks +2. **Updated advisory guardian hook** - Adds reputation warnings to alerts +3. **Catalog entry in clawsec-suite** - Listed as available enhancement + +## Development + +### Files + +- `SKILL.md` - Main documentation +- `skill.json` - Skill metadata and SBOM +- `scripts/enhanced_guarded_install.mjs` - Enhanced installer +- `scripts/check_clawhub_reputation.mjs` - Reputation logic +- `scripts/setup_reputation_hook.mjs` - Integration script +- `hooks/clawsec-advisory-guardian/lib/reputation.mjs` - Hook module + +### Testing + +```bash +# Test reputation check +node scripts/check_clawhub_reputation.mjs clawsec-suite + +# Test enhanced installer (dry run) +node scripts/enhanced_guarded_install.mjs --skill test-skill --dry-run + +# Test setup +node scripts/setup_reputation_hook.mjs +``` + +## Security Considerations + +- Reputation checks are **heuristic**, not definitive +- **False positives** possible with legitimate novel skills +- Always **review skill code** before overriding warnings +- This is **defense-in-depth**, not replacement for advisory feeds + +## License + +MIT - Part of the ClawSec security suite \ No newline at end of file diff --git a/skills/clawsec-clawhub-checker/SKILL.md b/skills/clawsec-clawhub-checker/SKILL.md new file mode 100644 index 0000000..49df518 --- /dev/null +++ b/skills/clawsec-clawhub-checker/SKILL.md @@ -0,0 +1,140 @@ +--- +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. +homepage: https://clawsec.prompt.security +clawdis: + emoji: "🛡️" + requires: + bins: [clawhub, curl, jq] + depends_on: [clawsec-suite] +--- + +# ClawSec ClawHub Checker + +Enhances the ClawSec suite's guarded skill installer with ClawHub reputation checks. Adds a second layer of security by checking VirusTotal Code Insight scores and other reputation signals before allowing skill installation. + +## What It Does + +1. **Wraps `clawhub install`** - Intercepts skill installation requests +2. **Checks VirusTotal reputation** - Uses ClawHub's built-in VirusTotal Code Insight +3. **Adds double confirmation** - For suspicious skills (reputation score below threshold) +4. **Integrates with advisory feed** - Works alongside existing clawsec-suite advisories +5. **Provides detailed reports** - Shows why a skill is flagged as suspicious + +## Installation + +This skill must be installed **after** `clawsec-suite`: + +```bash +# First install the suite +npx clawhub@latest install clawsec-suite + +# Then install the checker +npx clawhub@latest install clawsec-clawhub-checker +``` + +The checker will automatically enhance the existing `guarded_skill_install.mjs` script and advisory guardian hook. + +## How It Works + +### Enhanced Guarded Installer + +When you run: +```bash +node scripts/guarded_skill_install.mjs --skill some-skill --version 1.0.0 +``` + +The enhanced flow: +1. **Advisory check** (existing) - Checks clawsec advisory feed +2. **Reputation check** (new) - Queries ClawHub for VirusTotal scores +3. **Risk assessment** - Combines advisory + reputation signals +4. **Double confirmation** - If risky, requires explicit `--confirm-reputation` + +### Reputation Signals Checked + +1. **VirusTotal Code Insight** - Malicious code patterns detection +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 + +- `0` - Safe to install (no advisories, good reputation) +- `42` - Advisory match found (existing behavior) +- `43` - Reputation warning (new - requires `--confirm-reputation`) +- `1` - Error + +## Configuration + +Environment variables: +- `CLAWHUB_REPUTATION_THRESHOLD` - Minimum reputation score (0-100, default: 70) +- `CLAWHUB_ALLOW_SUSPICIOUS` - Allow installation of suspicious skills without confirmation (default: false) +- `CLAWHUB_VIRUSTOTAL_API_KEY` - Optional: Your own VirusTotal API key for deeper scans + +## Integration with Existing Suite + +The checker enhances but doesn't replace existing security: +- **Advisory feed still primary** - Known malicious skills blocked first +- **Reputation is secondary** - Unknown/suspicious skills get extra scrutiny +- **Double confirmation preserved** - Both layers require explicit user approval + +## Example Usage + +```bash +# Try to install a skill +node scripts/guarded_skill_install.mjs --skill suspicious-skill --version 1.0.0 + +# Output might show: +# WARNING: Skill "suspicious-skill" has low reputation score (45/100) +# - Flagged by VirusTotal Code Insight: crypto keys, external APIs, eval usage +# - Author has no other published skills +# - Skill is less than 7 days old +# +# To install despite reputation warning, run: +# node scripts/guarded_skill_install.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation + +# Install with confirmation +node scripts/guarded_skill_install.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation +``` + +## Safety Notes + +- This is a **defense-in-depth** layer, not a replacement for advisory feeds +- VirusTotal scores are **heuristic**, not definitive +- **False positives possible** - Legitimate skills with novel patterns might be flagged +- Always **review skill code** before installing with `--confirm-reputation` + +## Current Limitations + +### Missing OpenClaw Internal Check Data +ClawHub shows two security badges on skill pages: +1. **VirusTotal Code Insight** - ✅ Our checker catches these flags +2. **OpenClaw internal check** - ❌ Not exposed via API (only on website) + +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." + +**Our checker cannot access OpenClaw internal check warnings** as they're not exposed via `clawhub` CLI or API. + +### Recommendation for ClawHub +To enable complete reputation checking, ClawHub should expose internal check results via: +- `clawhub inspect --json` endpoint +- Additional API field for security tools +- Or include in `clawhub install` warning output + +### Workaround +Our heuristic checks (skill age, author reputation, downloads, updates) provide similar risk assessment but miss specific operational warnings about bypasses, missing signatures, etc. Always check the ClawHub website for complete security assessment. + +## Development + +To modify the reputation checking logic, edit: +- `scripts/enhanced_guarded_install.mjs` - Main enhanced installer +- `scripts/check_clawhub_reputation.mjs` - Reputation checking logic +- `hooks/clawsec-advisory-guardian/lib/reputation.mjs` - Hook integration + +## License + +MIT - Part of the ClawSec security suite \ No newline at end of file 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 new file mode 100644 index 0000000..bcecc39 --- /dev/null +++ b/skills/clawsec-clawhub-checker/hooks/clawsec-advisory-guardian/lib/reputation.mjs @@ -0,0 +1,96 @@ +import { spawnSync } from "node:child_process"; + +/** + * Check reputation for a skill + * @param {string} skillName - Skill name + * @param {string} version - Skill version + * @returns {Promise<{safe: boolean, score: number, warnings: string[]}>} + */ +export async function checkReputation(skillName, version) { + const result = { + safe: true, + score: 100, + warnings: [], + }; + + try { + // 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', ''); + + const reputationCheck = spawnSync( + "node", + [ + `${checkerDir}/scripts/check_clawhub_reputation.mjs`, + skillSlug, + version || "", + "70" // Default threshold + ], + { encoding: "utf-8", cwd: checkerDir } + ); + + if (reputationCheck.status === 0) { + try { + const repResult = JSON.parse(reputationCheck.stdout); + result.safe = repResult.safe; + result.score = repResult.score; + result.warnings = repResult.warnings; + } catch (parseError) { + result.warnings.push(`Failed to parse reputation result: ${parseError.message}`); + result.score = 60; + result.safe = result.score >= 70; + } + } else if (reputationCheck.status === 43) { + // Reputation warning exit code + try { + const repResult = JSON.parse(reputationCheck.stdout); + result.safe = false; + result.score = repResult.score; + result.warnings = repResult.warnings; + } catch { + result.safe = false; + result.score = 50; + result.warnings.push("Skill flagged by reputation check"); + } + } else { + // Error running check + result.warnings.push(`Reputation check failed: ${reputationCheck.stderr || 'Unknown error'}`); + result.score = 60; + result.safe = result.score >= 70; + } + } catch (error) { + result.warnings.push(`Reputation check error: ${error.message}`); + result.score = 50; + result.safe = result.score >= 70; + } + + return result; +} + +/** + * Format reputation warning for alert messages + * @param {{score: number, warnings: string[]}} reputationInfo + * @returns {string} + */ +export function formatReputationWarning(reputationInfo) { + if (!reputationInfo || reputationInfo.score >= 70) return ""; + + const lines = [ + `\n⚠️ **REPUTATION WARNING** (Score: ${reputationInfo.score}/100)`, + ]; + + if (reputationInfo.warnings.length > 0) { + lines.push(""); + reputationInfo.warnings.forEach(w => lines.push(`• ${w}`)); + } + + lines.push(""); + lines.push("This skill has low reputation score. Review carefully before installation."); + + return lines.join("\n"); +} \ No newline at end of file diff --git a/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs b/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs new file mode 100644 index 0000000..252e912 --- /dev/null +++ b/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs @@ -0,0 +1,188 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +/** + * Check ClawHub reputation for a skill + * @param {string} skillSlug - Skill slug to check + * @param {string} version - Optional version + * @param {number} threshold - Minimum reputation score (0-100) + * @returns {Promise<{safe: boolean, score: number, warnings: string[], virustotal: string[]}>} + */ +export async function checkClawhubReputation(skillSlug, version, threshold = 70) { + const result = { + safe: true, + score: 100, // Default score if no checks fail + warnings: [], + virustotal: [], + }; + + try { + // Check 1: Try to inspect the skill via clawhub + const inspectResult = spawnSync( + "clawhub", + ["inspect", skillSlug, "--json"], + { encoding: "utf-8" } + ); + + if (inspectResult.status !== 0) { + // Skill doesn't exist or can't be inspected + result.warnings.push(`Skill "${skillSlug}" not found or cannot be inspected`); + result.score = Math.min(result.score, 50); + } else { + try { + const skillInfo = JSON.parse(inspectResult.stdout); + + // Check 2: Skill age (new skills are riskier) + if (skillInfo.skill?.createdAt) { + const createdMs = skillInfo.skill.createdAt; + const ageDays = (Date.now() - createdMs) / (1000 * 60 * 60 * 24); + + if (ageDays < 7) { + result.warnings.push(`Skill is less than 7 days old (${ageDays.toFixed(1)} days)`); + result.score -= 15; + } else if (ageDays < 30) { + result.warnings.push(`Skill is less than 30 days old (${ageDays.toFixed(1)} days)`); + result.score -= 5; + } + } + + // Check 3: Update frequency (stale skills are riskier) + if (skillInfo.skill?.updatedAt && skillInfo.skill?.createdAt) { + const updatedMs = skillInfo.skill.updatedAt; + const createdMs = skillInfo.skill.createdAt; + const updateAgeDays = (Date.now() - updatedMs) / (1000 * 60 * 60 * 24); + const totalAgeDays = (Date.now() - createdMs) / (1000 * 60 * 60 * 24); + + if (updateAgeDays > 90 && totalAgeDays > 90) { + result.warnings.push(`Skill hasn't been updated in ${updateAgeDays.toFixed(0)} days`); + result.score -= 10; + } + } + + // Check 4: Author reputation + if (skillInfo.owner?.handle) { + const authorResult = spawnSync( + "clawhub", + ["search", skillInfo.owner.handle], + { encoding: "utf-8" } + ); + + if (authorResult.status === 0) { + const lines = authorResult.stdout.trim().split('\n').filter(l => l); + const skillCount = lines.length - 1; // First line is header + + if (skillCount === 1) { + result.warnings.push(`Author "${skillInfo.owner.handle}" has only 1 published skill`); + result.score -= 10; + } else if (skillCount < 3) { + result.warnings.push(`Author "${skillInfo.owner.handle}" has only ${skillCount} published skills`); + result.score -= 5; + } + } + } + + // Check 5: Download statistics + if (skillInfo.skill?.stats?.downloads !== undefined) { + const downloads = skillInfo.skill.stats.downloads; + if (downloads < 10) { + result.warnings.push(`Low download count: ${downloads}`); + result.score -= 10; + } else if (downloads < 100) { + result.warnings.push(`Moderate download count: ${downloads}`); + result.score -= 5; + } + } + + } catch (parseError) { + result.warnings.push(`Failed to parse skill information: ${parseError.message}`); + result.score = Math.min(result.score, 60); + } + } + + // 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" } + ); + + 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 + + // Extract specific warnings + const lines = output.split('\n'); + for (const line of lines) { + if (line.includes("Warning:") || line.includes("risky patterns") || + line.includes("crypto keys") || line.includes("external APIs") || + line.includes("eval") || line.includes("VirusTotal Code Insight")) { + const cleanLine = line.trim().replace(/^⚠️\s*/, '').replace(/^\s*Warning:\s*/, ''); + if (cleanLine && !result.virustotal.includes(cleanLine)) { + result.virustotal.push(cleanLine); + } + } + } + } + + // Check 7: If version specified, check if it exists + if (version) { + const versionCheck = spawnSync( + "clawhub", + ["inspect", skillSlug, "--version", version, "--json"], + { encoding: "utf-8" } + ); + + if (versionCheck.status !== 0) { + result.warnings.push(`Version ${version} not found for skill ${skillSlug}`); + result.score -= 20; + } + } + + // Ensure score is within bounds + result.score = Math.max(0, Math.min(100, result.score)); + result.safe = result.score >= threshold; + + // Add summary warning if below threshold + if (!result.safe) { + result.warnings.unshift(`Reputation score ${result.score}/100 below threshold ${threshold}/100`); + } + + } catch (error) { + result.warnings.push(`Reputation check error: ${error.message}`); + result.score = 50; + result.safe = result.score >= threshold; + } + + return result; +} + +// CLI interface for direct usage +if (import.meta.url === `file://${process.argv[1]}`) { + async function main() { + const args = process.argv.slice(2); + if (args.length < 1) { + console.error("Usage: node check_clawhub_reputation.mjs [version] [threshold]"); + process.exit(1); + } + + const skillSlug = args[0]; + const version = args[1] || ""; + const threshold = args[2] ? parseInt(args[2], 10) : 70; + + const result = await checkClawhubReputation(skillSlug, version, threshold); + + console.log(JSON.stringify(result, null, 2)); + + if (!result.safe) { + process.exit(43); + } + } + + main().catch(console.error); +} \ No newline at end of file diff --git a/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs b/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs new file mode 100644 index 0000000..2f99586 --- /dev/null +++ b/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs @@ -0,0 +1,201 @@ +#!/usr/bin/env node + +import { spawnSync } from "node:child_process"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { checkClawhubReputation } from "./check_clawhub_reputation.mjs"; + +const EXIT_ADVISORY_CONFIRM_REQUIRED = 42; +const EXIT_REPUTATION_CONFIRM_REQUIRED = 43; + +function printUsage() { + process.stderr.write( + [ + "Usage:", + " node scripts/enhanced_guarded_install.mjs --skill [--version ] [--confirm-advisory] [--confirm-reputation] [--dry-run] [--reputation-threshold ]", + "", + "Examples:", + " node scripts/enhanced_guarded_install.mjs --skill helper-plus --version 1.0.1", + " node scripts/enhanced_guarded_install.mjs --skill helper-plus --version 1.0.1 --confirm-advisory --confirm-reputation", + " node scripts/enhanced_guarded_install.mjs --skill suspicious-skill --reputation-threshold 80", + "", + "Exit codes:", + " 0 success / no advisory or reputation block", + " 42 advisory matched and second confirmation is required", + " 43 reputation warning and second confirmation is required", + " 1 error", + "", + ].join("\n"), + ); +} + +function parseArgs(argv) { + const parsed = { + skill: "", + version: "", + confirmAdvisory: false, + confirmReputation: false, + dryRun: false, + reputationThreshold: process.env.CLAWHUB_REPUTATION_THRESHOLD + ? parseInt(process.env.CLAWHUB_REPUTATION_THRESHOLD, 10) + : 70, + }; + + for (let i = 0; i < argv.length; i += 1) { + const token = argv[i]; + + if (token === "--skill") { + parsed.skill = String(argv[i + 1] ?? "").trim(); + i += 1; + continue; + } + if (token === "--version") { + parsed.version = String(argv[i + 1] ?? "").trim(); + i += 1; + continue; + } + if (token === "--confirm-advisory") { + parsed.confirmAdvisory = true; + continue; + } + if (token === "--confirm-reputation") { + parsed.confirmReputation = true; + continue; + } + if (token === "--dry-run") { + parsed.dryRun = true; + continue; + } + if (token === "--reputation-threshold") { + parsed.reputationThreshold = parseInt(String(argv[i + 1] ?? "70"), 10); + i += 1; + continue; + } + if (token === "--help" || token === "-h") { + printUsage(); + process.exit(0); + } + + throw new Error(`Unknown argument: ${token}`); + } + + 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."); + } + if (parsed.reputationThreshold < 0 || parsed.reputationThreshold > 100 || Number.isNaN(parsed.reputationThreshold)) { + throw new Error("Invalid --reputation-threshold value. Must be between 0 and 100."); + } + + return parsed; +} + +async function runOriginalGuardedInstall(args) { + // Find the original guarded_skill_install.mjs from clawsec-suite + const suiteDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-suite"); + const originalScript = path.join(suiteDir, "scripts", "guarded_skill_install.mjs"); + + try { + await fs.access(originalScript); + } catch { + 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 + } + + const child = spawnSync( + "node", + [originalScript, ...args.originalArgs], + { + stdio: "inherit", + env, + cwd: suiteDir, + }, + ); + + return { + exitCode: child.status ?? 1, + signal: child.signal, + }; +} + +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); + } + + args.originalArgs = originalArgs; + + // Step 1: Check reputation (unless already confirmed) + if (!args.confirmReputation) { + console.log(`Checking ClawHub reputation for ${args.skill}${args.version ? `@${args.version}` : ""}...`); + + const reputationResult = await checkClawhubReputation(args.skill, args.version, args.reputationThreshold); + + if (!reputationResult.safe) { + console.error("\n" + "=".repeat(80)); + console.error("REPUTATION WARNING"); + console.error("=".repeat(80)); + console.error(`Skill "${args.skill}" has low reputation score: ${reputationResult.score}/100`); + console.error(`Threshold: ${args.reputationThreshold}/100`); + console.error(""); + + if (reputationResult.warnings.length > 0) { + console.error("Warnings:"); + reputationResult.warnings.forEach(w => console.error(` • ${w}`)); + console.error(""); + } + + if (reputationResult.virustotal) { + console.error("VirusTotal Code Insight flags:"); + reputationResult.virustotal.forEach(v => console.error(` • ${v}`)); + console.error(""); + } + + console.error("To install despite reputation warning, run with --confirm-reputation flag:"); + console.error(` node ${process.argv[1]} --skill ${args.skill}${args.version ? ` --version ${args.version}` : ""} --confirm-reputation`); + console.error(""); + console.error("=".repeat(80)); + + process.exit(EXIT_REPUTATION_CONFIRM_REQUIRED); + } + + console.log(`✓ Reputation check passed: ${reputationResult.score}/100`); + } else { + console.log(`⚠️ Reputation confirmation override enabled for ${args.skill}`); + } + + // Step 2: Run original guarded installer (handles advisory checks) + console.log("\nRunning advisory checks..."); + const result = await runOriginalGuardedInstall(args); + + if (result.exitCode !== 0 && result.exitCode !== EXIT_ADVISORY_CONFIRM_REQUIRED) { + process.exit(result.exitCode); + } + + // If we get here, either success (0) or advisory confirmation required (42) + process.exit(result.exitCode); + + } catch (error) { + console.error("Error:", error.message); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs b/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs new file mode 100644 index 0000000..7a24e25 --- /dev/null +++ b/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs @@ -0,0 +1,137 @@ +#!/usr/bin/env node + +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; + +async function main() { + console.log("Setting up ClawHub reputation checker integration..."); + + // Paths + const suiteDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-suite"); + const checkerDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-clawhub-checker"); + const hookLibDir = path.join(suiteDir, "hooks", "clawsec-advisory-guardian", "lib"); + + try { + // Check if clawsec-suite is installed + await fs.access(suiteDir); + console.log(`✓ Found clawsec-suite at ${suiteDir}`); + + // Check if hook lib directory exists + await fs.access(hookLibDir); + console.log(`✓ Found advisory guardian hook at ${hookLibDir}`); + + // Copy reputation module to hook lib + const reputationModuleSrc = path.join(checkerDir, "hooks", "clawsec-advisory-guardian", "lib", "reputation.mjs"); + const reputationModuleDst = path.join(hookLibDir, "reputation.mjs"); + + await fs.copyFile(reputationModuleSrc, reputationModuleDst); + console.log(`✓ Copied reputation module to ${reputationModuleDst}`); + + // Update hook handler to import reputation module + const hookHandlerPath = path.join(suiteDir, "hooks", "clawsec-advisory-guardian", "handler.ts"); + let handlerContent = await fs.readFile(hookHandlerPath, "utf8"); + + // Check if already imported + if (!handlerContent.includes("from \"./lib/reputation.mjs\"")) { + // Add import after other imports + const importIndex = handlerContent.lastIndexOf("import"); + 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) { + const repResult = await checkReputation(match.skill.name, match.skill.version); + if (!repResult.safe) { + match.reputationWarning = true; + match.reputationScore = repResult.score; + match.reputationWarnings = repResult.warnings; + } + } + `; + + handlerContent = handlerContent.slice(0, insertIndex) + reputationCheckCode + handlerContent.slice(insertIndex); + } + + // 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); + const oldLine = handlerContent.slice(lineStart, lineEnd); + + const newLine = `const alertMessage = buildAlertMessage(match, match.reputationWarning ? { score: match.reputationScore, warnings: match.reputationWarnings } : undefined);`; + handlerContent = handlerContent.slice(0, lineStart) + newLine + handlerContent.slice(lineEnd); + } + + await fs.writeFile(hookHandlerPath, handlerContent); + console.log(`✓ Updated hook handler with reputation checks`); + } else { + console.log(`✓ Hook handler already has reputation checks`); + } + + // Create symlink or copy enhanced installer + const enhancedInstallerSrc = path.join(checkerDir, "scripts", "enhanced_guarded_install.mjs"); + const enhancedInstallerDst = path.join(suiteDir, "scripts", "enhanced_guarded_install.mjs"); + + await fs.copyFile(enhancedInstallerSrc, enhancedInstallerDst); + console.log(`✓ Installed enhanced guarded installer at ${enhancedInstallerDst}`); + + // Create wrapper script that uses enhanced installer by default + const wrapperScript = `#!/usr/bin/env node + +// Wrapper that uses enhanced guarded installer with reputation checks +// This replaces the original guarded_skill_install.mjs in usage + +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const enhancedScript = path.join(__dirname, "enhanced_guarded_install.mjs"); + +const result = spawnSync("node", [enhancedScript, ...process.argv.slice(2)], { + stdio: "inherit", +}); + +process.exit(result.status ?? 1); +`; + + const wrapperPath = path.join(suiteDir, "scripts", "guarded_skill_install_wrapper.mjs"); + await fs.writeFile(wrapperPath, wrapperScript); + await fs.chmod(wrapperPath, 0o755); + console.log(`✓ Created wrapper script at ${wrapperPath}`); + + console.log("\n" + "=".repeat(80)); + console.log("SETUP COMPLETE"); + console.log("=".repeat(80)); + console.log("\nThe ClawHub reputation checker has been integrated with clawsec-suite."); + console.log("\nWhat changed:"); + console.log("1. Enhanced guarded installer with reputation checks installed"); + console.log("2. Advisory guardian hook updated to include reputation warnings"); + console.log("3. Wrapper script created for backward compatibility"); + console.log("\nUsage:"); + console.log(" node scripts/enhanced_guarded_install.mjs --skill [--version ]"); + console.log(" node scripts/guarded_skill_install_wrapper.mjs --skill [--version ]"); + console.log("\nNew exit code: 43 = Reputation warning (requires --confirm-reputation)"); + console.log("\nRestart OpenClaw gateway for hook changes to take effect."); + console.log("=".repeat(80)); + + } catch (error) { + console.error("Setup failed:", error.message); + console.error("\nMake sure:"); + console.error("1. clawsec-suite is installed (npx clawhub install clawsec-suite)"); + console.error("2. You have write permissions to the suite directory"); + process.exit(1); + } +} + +main().catch(console.error); \ No newline at end of file diff --git a/skills/clawsec-clawhub-checker/skill.json b/skills/clawsec-clawhub-checker/skill.json new file mode 100644 index 0000000..5e7e21d --- /dev/null +++ b/skills/clawsec-clawhub-checker/skill.json @@ -0,0 +1,81 @@ +{ + "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", + "license": "MIT", + "homepage": "https://clawsec.prompt.security/", + "keywords": [ + "security", + "reputation", + "clawhub", + "virustotal", + "skills", + "installer", + "verification", + "defense-in-depth", + "openclaw" + ], + "sbom": { + "files": [ + { + "path": "SKILL.md", + "required": true, + "description": "Skill documentation and usage guide" + }, + { + "path": "scripts/enhanced_guarded_install.mjs", + "required": true, + "description": "Enhanced guarded installer with reputation checks" + }, + { + "path": "scripts/check_clawhub_reputation.mjs", + "required": true, + "description": "ClawHub reputation checking logic" + }, + { + "path": "scripts/setup_reputation_hook.mjs", + "required": true, + "description": "Setup script to enhance existing advisory guardian hook" + }, + { + "path": "hooks/clawsec-advisory-guardian/lib/reputation.mjs", + "required": true, + "description": "Reputation checking module for advisory guardian hook" + } + ] + }, + "dependencies": { + "clawsec-suite": ">=0.0.10" + }, + "integration": { + "clawsec-suite": { + "enhances": [ + "guarded_skill_install.mjs", + "clawsec-advisory-guardian hook" + ], + "adds_exit_codes": { + "43": "Reputation warning - requires --confirm-reputation" + }, + "adds_arguments": [ + "--confirm-reputation", + "--reputation-threshold" + ] + } + }, + "openclaw": { + "emoji": "🛡️", + "category": "security", + "requires": { + "bins": ["clawhub", "curl", "jq"] + }, + "triggers": [ + "clawhub reputation", + "skill reputation check", + "virustotal skill check", + "safe skill install", + "check skill safety", + "skill security score" + ] + } +} \ No newline at end of file diff --git a/skills/clawsec-clawhub-checker/update_suite_catalog.mjs b/skills/clawsec-clawhub-checker/update_suite_catalog.mjs new file mode 100644 index 0000000..dee9eae --- /dev/null +++ b/skills/clawsec-clawhub-checker/update_suite_catalog.mjs @@ -0,0 +1,77 @@ +#!/usr/bin/env node + +import fs from "node:fs/promises"; +import path from "node:path"; +import os from "node:os"; + +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); \ No newline at end of file diff --git a/skills/clawsec-suite/skill.json b/skills/clawsec-suite/skill.json index 5db2417..deb4987 100644 --- a/skills/clawsec-suite/skill.json +++ b/skills/clawsec-suite/skill.json @@ -206,6 +206,17 @@ "clawdbot", "other" ] + }, + "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" } } }, @@ -236,4 +247,4 @@ "update skills" ] } -} +} \ No newline at end of file From 50a2d5fb8c408f751a13a834c5a5181cd3b61b15 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 16 Feb 2026 19:36:38 +0200 Subject: [PATCH 2/8] feat: add clawsec-clawhub-checker skill - Enhanced guarded installer with reputation checks - VirusTotal Code Insight integration - Reputation scoring (0-100) with multiple signals - New exit code 43 for reputation warnings - Requires --confirm-reputation for suspicious skills - Integration with clawsec-advisory-guardian hook - Standalone skill compatible with dynamic catalog system Note: Removed hardcoded catalog entry to work with new dynamic catalog system (discover_skill_catalog.mjs). --- skills/clawsec-suite/skill.json | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/skills/clawsec-suite/skill.json b/skills/clawsec-suite/skill.json index deb4987..b941ef1 100644 --- a/skills/clawsec-suite/skill.json +++ b/skills/clawsec-suite/skill.json @@ -1,6 +1,6 @@ { "name": "clawsec-suite", - "version": "0.0.10", + "version": "0.1.2", "description": "ClawSec suite manager with embedded advisory-feed monitoring, cryptographic signature verification, approval-gated malicious-skill response, and guided setup for additional security skills.", "author": "prompt-security", "license": "MIT", @@ -120,6 +120,11 @@ "required": true, "description": "Two-step confirmation installer with signature verification that blocks risky skill installs" }, + { + "path": "scripts/discover_skill_catalog.mjs", + "required": true, + "description": "Dynamic skill-catalog discovery with remote index fetch and suite-local fallback metadata" + }, { "path": "scripts/sign_detached_ed25519.mjs", "required": false, @@ -206,17 +211,6 @@ "clawdbot", "other" ] - }, - "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" } } }, @@ -247,4 +241,4 @@ "update skills" ] } -} \ No newline at end of file +} From e6b9e90bac1c9c6bfcebb81c425a3bf0b23d85c0 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 16 Feb 2026 19:53:53 +0200 Subject: [PATCH 3/8] fix: lint errors in clawsec-clawhub-checker - Remove unused imports (fs, os, path) from check_clawhub_reputation.mjs - Remove unused variable in setup_reputation_hook.mjs - Remove unused os import from update_suite_catalog.mjs - All ESLint checks now pass - TypeScript check passes - Build check passes --- .../scripts/check_clawhub_reputation.mjs | 3 --- .../clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs | 2 +- skills/clawsec-clawhub-checker/update_suite_catalog.mjs | 1 - 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs b/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs index 252e912..9b37825 100644 --- a/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs +++ b/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs @@ -1,9 +1,6 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; -import fs from "node:fs/promises"; -import os from "node:os"; -import path from "node:path"; /** * Check ClawHub reputation for a skill diff --git a/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs b/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs index 7a24e25..eded9a4 100644 --- a/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs +++ b/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs @@ -66,7 +66,7 @@ async function main() { if (buildAlertLine !== -1) { const lineStart = handlerContent.lastIndexOf("\n", buildAlertLine) + 1; const lineEnd = handlerContent.indexOf("\n", buildAlertLine); - const oldLine = handlerContent.slice(lineStart, lineEnd); + // 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); diff --git a/skills/clawsec-clawhub-checker/update_suite_catalog.mjs b/skills/clawsec-clawhub-checker/update_suite_catalog.mjs index dee9eae..33cee79 100644 --- a/skills/clawsec-clawhub-checker/update_suite_catalog.mjs +++ b/skills/clawsec-clawhub-checker/update_suite_catalog.mjs @@ -2,7 +2,6 @@ import fs from "node:fs/promises"; import path from "node:path"; -import os from "node:os"; async function updateSuiteCatalog() { const suiteDir = "/home/david/.openclaw-clean/workspace/clawsec-suite"; From 765255680cf27441b1e6fbdb0d9383db6d53a4be Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 16 Feb 2026 20:32:51 +0200 Subject: [PATCH 4/8] 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 --- skills/clawsec-clawhub-checker/PR_NOTES.md | 26 -- skills/clawsec-clawhub-checker/README.md | 2 +- skills/clawsec-clawhub-checker/SKILL.md | 2 +- .../lib/reputation.mjs | 2 +- .../scripts/check_clawhub_reputation.mjs | 31 +- .../scripts/enhanced_guarded_install.mjs | 2 +- .../scripts/setup_reputation_hook.mjs | 2 +- skills/clawsec-clawhub-checker/skill.json | 9 +- .../test/reputation_check.test.mjs | 352 ++++++++++++++++++ .../update_suite_catalog.mjs | 76 ---- 10 files changed, 387 insertions(+), 117 deletions(-) delete mode 100644 skills/clawsec-clawhub-checker/PR_NOTES.md create mode 100644 skills/clawsec-clawhub-checker/test/reputation_check.test.mjs delete mode 100644 skills/clawsec-clawhub-checker/update_suite_catalog.mjs diff --git a/skills/clawsec-clawhub-checker/PR_NOTES.md b/skills/clawsec-clawhub-checker/PR_NOTES.md deleted file mode 100644 index ad9a5ba..0000000 --- a/skills/clawsec-clawhub-checker/PR_NOTES.md +++ /dev/null @@ -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 \ No newline at end of file diff --git a/skills/clawsec-clawhub-checker/README.md b/skills/clawsec-clawhub-checker/README.md index 2c3b927..b7a66e6 100644 --- a/skills/clawsec-clawhub-checker/README.md +++ b/skills/clawsec-clawhub-checker/README.md @@ -120,4 +120,4 @@ node scripts/setup_reputation_hook.mjs ## License -MIT - Part of the ClawSec security suite \ No newline at end of file +MIT - Part of the ClawSec security suite diff --git a/skills/clawsec-clawhub-checker/SKILL.md b/skills/clawsec-clawhub-checker/SKILL.md index 49df518..af7907f 100644 --- a/skills/clawsec-clawhub-checker/SKILL.md +++ b/skills/clawsec-clawhub-checker/SKILL.md @@ -137,4 +137,4 @@ To modify the reputation checking logic, edit: ## License -MIT - Part of the ClawSec security suite \ No newline at end of file +MIT - Part of the ClawSec security suite 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 bcecc39..6892894 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 @@ -93,4 +93,4 @@ export function formatReputationWarning(reputationInfo) { lines.push("This skill has low reputation score. Review carefully before installation."); return lines.join("\n"); -} \ No newline at end of file +} diff --git a/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs b/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs index 9b37825..a020845 100644 --- a/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs +++ b/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs @@ -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); -} \ No newline at end of file +} diff --git a/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs b/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs index 2f99586..dbc4fc6 100644 --- a/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs +++ b/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs @@ -198,4 +198,4 @@ async function main() { } } -main(); \ No newline at end of file +main(); diff --git a/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs b/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs index eded9a4..e1ef955 100644 --- a/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs +++ b/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs @@ -134,4 +134,4 @@ process.exit(result.status ?? 1); } } -main().catch(console.error); \ No newline at end of file +main().catch(console.error); diff --git a/skills/clawsec-clawhub-checker/skill.json b/skills/clawsec-clawhub-checker/skill.json index 5e7e21d..6c5a812 100644 --- a/skills/clawsec-clawhub-checker/skill.json +++ b/skills/clawsec-clawhub-checker/skill.json @@ -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" ] } -} \ No newline at end of file +} diff --git a/skills/clawsec-clawhub-checker/test/reputation_check.test.mjs b/skills/clawsec-clawhub-checker/test/reputation_check.test.mjs new file mode 100644 index 0000000..874c4bc --- /dev/null +++ b/skills/clawsec-clawhub-checker/test/reputation_check.test.mjs @@ -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); +}); diff --git a/skills/clawsec-clawhub-checker/update_suite_catalog.mjs b/skills/clawsec-clawhub-checker/update_suite_catalog.mjs deleted file mode 100644 index 33cee79..0000000 --- a/skills/clawsec-clawhub-checker/update_suite_catalog.mjs +++ /dev/null @@ -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); \ No newline at end of file From 6893390ab4ded2bf282377e2c8e365be7dad03fc Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 16 Feb 2026 20:56:44 +0200 Subject: [PATCH 5/8] 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" } ] }, From 15309588ee0343f62a1afb1b623c01201637450e Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 16 Feb 2026 21:09:27 +0200 Subject: [PATCH 6/8] feat: integrate reputation checks into clawhub setup script and enhance installer --- .../scripts/setup_reputation_hook.mjs | 94 +++++++++++-------- 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs b/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs index c78e364..cd70019 100644 --- a/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs +++ b/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mjs @@ -11,6 +11,7 @@ async function main() { const suiteDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-suite"); const checkerDir = path.join(os.homedir(), ".openclaw", "skills", "clawsec-clawhub-checker"); const hookLibDir = path.join(suiteDir, "hooks", "clawsec-advisory-guardian", "lib"); + const suiteScriptsDir = path.join(suiteDir, "scripts"); try { // Check if clawsec-suite is installed @@ -31,13 +32,15 @@ async function main() { // Update hook handler to import reputation module const hookHandlerPath = path.join(suiteDir, "hooks", "clawsec-advisory-guardian", "handler.ts"); let handlerContent = await fs.readFile(hookHandlerPath, "utf8"); - - // 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 + // 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 + let handlerChanged = false; + const importLine = "import { checkReputation } from \"./lib/reputation.mjs\";"; + const reputationMarker = "// ClawHub reputation check for matched skills"; + + if (!handlerContent.includes(importLine)) { // Add import after other imports const importIndex = handlerContent.lastIndexOf("import"); if (importIndex === -1) { @@ -45,48 +48,62 @@ async function main() { } 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) { - const repResult = await checkReputation(match.skill.name, match.skill.version); - if (!repResult.safe) { - match.reputationWarning = true; - match.reputationScore = repResult.score; - match.reputationWarnings = repResult.warnings; - } - } - `; - - 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."); + handlerContent = handlerContent.slice(0, lineEndIndex + 1) + `${importLine}\n` + handlerContent.slice(lineEndIndex + 1); + handlerChanged = true; + } else { + console.log("✓ Hook handler already imports reputation module"); + } + + if (!handlerContent.includes(reputationMarker)) { + const findMatchesAnchors = [ + { line: "const allMatches = findMatches(feed, installedSkills);", variable: "allMatches" }, + { line: "const matches = findMatches(feed, installedSkills);", variable: "matches" }, + ]; + const matchedAnchor = findMatchesAnchors.find((entry) => handlerContent.includes(entry.line)); + + if (!matchedAnchor) { + throw new Error( + "Could not find findMatches assignment in handler.ts. Refusing partial setup. Manual integration required." + ); } - // 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. + const anchorIndex = handlerContent.indexOf(matchedAnchor.line); + const insertIndex = handlerContent.indexOf("\n", anchorIndex) + 1; + const reputationCheckCode = ` + ${reputationMarker} + for (const match of ${matchedAnchor.variable}) { + const repResult = await checkReputation(match.skill.name, match.skill.version); + if (!repResult.safe) { + match.reputationWarning = true; + match.reputationScore = repResult.score; + match.reputationWarnings = repResult.warnings; + } + } +`; + handlerContent = handlerContent.slice(0, insertIndex) + reputationCheckCode + handlerContent.slice(insertIndex); + handlerChanged = true; + } else { + console.log("✓ Hook handler already has reputation scan block"); + } + if (handlerChanged) { await fs.writeFile(hookHandlerPath, handlerContent); - console.log(`✓ Updated hook handler with reputation checks`); + console.log("✓ Updated hook handler with reputation checks"); } else { - console.log(`✓ Hook handler already has reputation checks`); + console.log("✓ Hook handler already has required reputation integration"); } - // Create symlink or copy enhanced installer + // Copy enhanced installer and reputation checker scripts const enhancedInstallerSrc = path.join(checkerDir, "scripts", "enhanced_guarded_install.mjs"); const enhancedInstallerDst = path.join(suiteDir, "scripts", "enhanced_guarded_install.mjs"); + const reputationCheckSrc = path.join(checkerDir, "scripts", "check_clawhub_reputation.mjs"); + const reputationCheckDst = path.join(suiteScriptsDir, "check_clawhub_reputation.mjs"); await fs.copyFile(enhancedInstallerSrc, enhancedInstallerDst); console.log(`✓ Installed enhanced guarded installer at ${enhancedInstallerDst}`); + + await fs.copyFile(reputationCheckSrc, reputationCheckDst); + console.log(`✓ Installed reputation check script at ${reputationCheckDst}`); // Create wrapper script that uses enhanced installer by default const wrapperScript = `#!/usr/bin/env node @@ -119,8 +136,9 @@ process.exit(result.status ?? 1); console.log("\nThe ClawHub reputation checker has been integrated with clawsec-suite."); console.log("\nWhat changed:"); console.log("1. Enhanced guarded installer with reputation checks installed"); - console.log("2. Advisory guardian hook updated to include reputation warnings"); - console.log("3. Wrapper script created for backward compatibility"); + console.log("2. Reputation check helper script installed"); + console.log("3. Advisory guardian hook updated to include reputation warnings"); + console.log("4. Wrapper script created for backward compatibility"); console.log("\nUsage:"); console.log(" node scripts/enhanced_guarded_install.mjs --skill [--version ]"); console.log(" node scripts/guarded_skill_install_wrapper.mjs --skill [--version ]"); From 45386225eb46b070b37328afba4d1164db0ba7ec Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 16 Feb 2026 21:11:38 +0200 Subject: [PATCH 7/8] docs: update README and SKILL documentation to reflect new installer scripts and usage instructions --- skills/clawsec-clawhub-checker/README.md | 17 ++++++++++++++--- skills/clawsec-clawhub-checker/SKILL.md | 18 ++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/skills/clawsec-clawhub-checker/README.md b/skills/clawsec-clawhub-checker/README.md index b7a66e6..fb5ef07 100644 --- a/skills/clawsec-clawhub-checker/README.md +++ b/skills/clawsec-clawhub-checker/README.md @@ -37,19 +37,29 @@ node scripts/setup_reputation_hook.mjs openclaw gateway restart ``` +Setup installs these scripts into `clawsec-suite/scripts`: +- `enhanced_guarded_install.mjs` +- `guarded_skill_install_wrapper.mjs` (drop-in wrapper) +- `check_clawhub_reputation.mjs` + +The original `guarded_skill_install.mjs` remains unchanged. + ## Usage ### Enhanced Guarded Installer ```bash -# Basic usage (includes reputation checks) +# Basic usage via wrapper (includes reputation checks) +node scripts/guarded_skill_install_wrapper.mjs --skill some-skill --version 1.0.0 + +# Direct usage (enhanced script) node scripts/enhanced_guarded_install.mjs --skill some-skill --version 1.0.0 # With reputation confirmation override -node scripts/enhanced_guarded_install.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation +node scripts/guarded_skill_install_wrapper.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation # Adjust reputation threshold (default: 70) -node scripts/enhanced_guarded_install.mjs --skill some-skill --reputation-threshold 80 +node scripts/guarded_skill_install_wrapper.mjs --skill some-skill --reputation-threshold 80 ``` ### Reputation Check Only @@ -84,6 +94,7 @@ Environment variables: ## Integration Points 1. **Enhanced `guarded_skill_install.mjs`** - Wraps original with reputation checks + via `guarded_skill_install_wrapper.mjs` and `enhanced_guarded_install.mjs` 2. **Updated advisory guardian hook** - Adds reputation warnings to alerts 3. **Catalog entry in clawsec-suite** - Listed as available enhancement diff --git a/skills/clawsec-clawhub-checker/SKILL.md b/skills/clawsec-clawhub-checker/SKILL.md index 6df79c3..d9da36c 100644 --- a/skills/clawsec-clawhub-checker/SKILL.md +++ b/skills/clawsec-clawhub-checker/SKILL.md @@ -40,15 +40,21 @@ node ~/.openclaw/skills/clawsec-clawhub-checker/scripts/setup_reputation_hook.mj openclaw gateway restart ``` -After setup, the checker enhances the existing `guarded_skill_install.mjs` script and advisory guardian hook. +After setup, the checker adds `enhanced_guarded_install.mjs` and +`guarded_skill_install_wrapper.mjs` under `clawsec-suite/scripts` and updates the advisory +guardian hook. The original `guarded_skill_install.mjs` is not replaced. ## How It Works ### Enhanced Guarded Installer -When you run: +After setup, run the wrapper (drop-in path) or the enhanced script directly: ```bash -node scripts/guarded_skill_install.mjs --skill some-skill --version 1.0.0 +# Recommended drop-in wrapper +node scripts/guarded_skill_install_wrapper.mjs --skill some-skill --version 1.0.0 + +# Or call the enhanced script directly +node scripts/enhanced_guarded_install.mjs --skill some-skill --version 1.0.0 ``` The enhanced flow: @@ -89,7 +95,7 @@ The checker enhances but doesn't replace existing security: ```bash # Try to install a skill -node scripts/guarded_skill_install.mjs --skill suspicious-skill --version 1.0.0 +node scripts/guarded_skill_install_wrapper.mjs --skill suspicious-skill --version 1.0.0 # Output might show: # WARNING: Skill "suspicious-skill" has low reputation score (45/100) @@ -98,10 +104,10 @@ node scripts/guarded_skill_install.mjs --skill suspicious-skill --version 1.0.0 # - Skill is less than 7 days old # # To install despite reputation warning, run: -# node scripts/guarded_skill_install.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation +# node scripts/guarded_skill_install_wrapper.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation # Install with confirmation -node scripts/guarded_skill_install.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation +node scripts/guarded_skill_install_wrapper.mjs --skill suspicious-skill --version 1.0.0 --confirm-reputation ``` ## Safety Notes From 6d6bb6a6e2e6efa185fee2cd055a8f86fd541574 Mon Sep 17 00:00:00 2001 From: David Abutbul Date: Mon, 16 Feb 2026 21:20:49 +0200 Subject: [PATCH 8/8] feat: enhance CLI validation for skill version and reputation threshold; update documentation --- skills/clawsec-clawhub-checker/README.md | 1 - skills/clawsec-clawhub-checker/SKILL.md | 2 - .../scripts/check_clawhub_reputation.mjs | 20 ++++- .../scripts/enhanced_guarded_install.mjs | 5 ++ .../test/reputation_check.test.mjs | 81 +++++++++++++++++++ 5 files changed, 104 insertions(+), 5 deletions(-) diff --git a/skills/clawsec-clawhub-checker/README.md b/skills/clawsec-clawhub-checker/README.md index fb5ef07..b084ec6 100644 --- a/skills/clawsec-clawhub-checker/README.md +++ b/skills/clawsec-clawhub-checker/README.md @@ -89,7 +89,6 @@ node scripts/check_clawhub_reputation.mjs some-skill 1.0.0 70 Environment variables: - `CLAWHUB_REPUTATION_THRESHOLD` - Minimum score (0-100, default: 70) -- `CLAWHUB_ALLOW_SUSPICIOUS` - Skip reputation checks (not recommended) ## Integration Points diff --git a/skills/clawsec-clawhub-checker/SKILL.md b/skills/clawsec-clawhub-checker/SKILL.md index d9da36c..1c97b3d 100644 --- a/skills/clawsec-clawhub-checker/SKILL.md +++ b/skills/clawsec-clawhub-checker/SKILL.md @@ -81,8 +81,6 @@ The enhanced flow: Environment variables: - `CLAWHUB_REPUTATION_THRESHOLD` - Minimum reputation score (0-100, default: 70) -- `CLAWHUB_ALLOW_SUSPICIOUS` - Allow installation of suspicious skills without confirmation (default: false) -- `CLAWHUB_VIRUSTOTAL_API_KEY` - Optional: Your own VirusTotal API key for deeper scans ## Integration with Existing Suite diff --git a/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs b/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs index 18cd0ec..8863b11 100644 --- a/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs +++ b/skills/clawsec-clawhub-checker/scripts/check_clawhub_reputation.mjs @@ -1,6 +1,8 @@ #!/usr/bin/env node import { spawnSync } from "node:child_process"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; /** * Check ClawHub reputation for a skill @@ -182,7 +184,11 @@ export async function checkClawhubReputation(skillSlug, version, threshold = 70) } // CLI interface for direct usage -if (import.meta.url === `file://${process.argv[1]}`) { +const isCliEntrypoint = + process.argv[1] !== undefined && + import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href; + +if (isCliEntrypoint) { async function main() { const args = process.argv.slice(2); if (args.length < 1) { @@ -192,7 +198,17 @@ if (import.meta.url === `file://${process.argv[1]}`) { const skillSlug = args[0]; const version = args[1] || ""; - const threshold = args[2] ? parseInt(args[2], 10) : 70; + let threshold = 70; + if (args[2] !== undefined) { + const parsedThreshold = parseInt(args[2], 10); + if (!Number.isInteger(parsedThreshold) || parsedThreshold < 0 || parsedThreshold > 100) { + console.error( + `Invalid threshold: "${args[2]}". Threshold must be an integer between 0 and 100.` + ); + process.exit(1); + } + threshold = parsedThreshold; + } const result = await checkClawhubReputation(skillSlug, version, threshold); diff --git a/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs b/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs index 25fc038..b15f1d1 100644 --- a/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs +++ b/skills/clawsec-clawhub-checker/scripts/enhanced_guarded_install.mjs @@ -99,6 +99,11 @@ function parseArgs(argv) { 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.version && !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$/.test(parsed.version)) { + throw new Error( + "Invalid --version value. Must be semantic version format (e.g., 1.2.3, 1.2.3-beta.1, 1.2.3+build.45)." + ); + } if (parsed.reputationThreshold < 0 || parsed.reputationThreshold > 100 || Number.isNaN(parsed.reputationThreshold)) { throw new Error("Invalid --reputation-threshold value. Must be between 0 and 100."); } diff --git a/skills/clawsec-clawhub-checker/test/reputation_check.test.mjs b/skills/clawsec-clawhub-checker/test/reputation_check.test.mjs index 874c4bc..7b94ae0 100644 --- a/skills/clawsec-clawhub-checker/test/reputation_check.test.mjs +++ b/skills/clawsec-clawhub-checker/test/reputation_check.test.mjs @@ -208,6 +208,61 @@ async function testPreReleaseVersionAccepted() { } } +// ----------------------------------------------------------------------------- +// Test: CLI entrypoint guard works when script path is relative +// ----------------------------------------------------------------------------- +async function testRelativePathCliEntrypointWorks() { + const testName = "reputation_check: CLI entrypoint works with relative script path"; + try { + const relativeCheckerScript = path.relative(process.cwd(), CHECKER_SCRIPT); + const result = await runScript(relativeCheckerScript, ['bad slug', '', '70']); + + let parsed; + try { + parsed = JSON.parse(result.stdout); + } catch { + fail(testName, `Could not parse output with relative script path: ${result.stdout}`); + return; + } + + if ( + result.code === 43 && + parsed.safe === false && + parsed.warnings.some((w) => w.includes("Invalid skill slug")) + ) { + pass(testName); + } else { + fail( + testName, + `Expected exit 43 with invalid slug warning via relative path, got code ${result.code}: ${JSON.stringify(parsed)}` + ); + } + } catch (error) { + fail(testName, error); + } +} + +// ----------------------------------------------------------------------------- +// Test: Invalid threshold format is rejected in CLI mode +// ----------------------------------------------------------------------------- +async function testInvalidThresholdRejected() { + const testName = "reputation_check: invalid threshold is rejected"; + try { + const result = await runScript(CHECKER_SCRIPT, ['test-skill', '1.0.0', 'abc']); + + if (result.code === 1 && result.stderr.includes("Invalid threshold")) { + pass(testName); + } else { + fail( + testName, + `Expected exit 1 with invalid threshold message, got code ${result.code}: ${result.stderr}` + ); + } + } catch (error) { + fail(testName, error); + } +} + // ----------------------------------------------------------------------------- // Test: Enhanced installer rejects invalid skill name // ----------------------------------------------------------------------------- @@ -321,6 +376,29 @@ async function testFormatReputationWarningNull() { } } +// ----------------------------------------------------------------------------- +// Test: Enhanced installer validates --version even with --confirm-reputation +// ----------------------------------------------------------------------------- +async function testEnhancedInstallerRejectsInvalidVersion() { + const testName = "enhanced_install: rejects invalid version format even with --confirm-reputation"; + try { + const result = await runScript(ENHANCED_INSTALL_SCRIPT, [ + '--skill', 'test-skill', '--version', '1.0.0;rm -rf /', '--confirm-reputation' + ]); + + if (result.code === 1 && result.stderr.includes("Invalid --version value")) { + pass(testName); + } else { + fail( + testName, + `Expected exit 1 with invalid version message, got code ${result.code}: ${result.stderr}` + ); + } + } catch (error) { + fail(testName, error); + } +} + // ----------------------------------------------------------------------------- // Main test runner // ----------------------------------------------------------------------------- @@ -333,8 +411,11 @@ async function runTests() { await testUppercaseSlugRejected(); await testEmptySlugShowsUsage(); await testPreReleaseVersionAccepted(); + await testRelativePathCliEntrypointWorks(); + await testInvalidThresholdRejected(); await testEnhancedInstallerRejectsInvalidSkill(); await testEnhancedInstallerRequiresSkill(); + await testEnhancedInstallerRejectsInvalidVersion(); await testEnhancedInstallerRejectsInvalidThreshold(); await testFormatReputationWarning(); await testFormatReputationWarningNull();