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();