mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-15 22:41:20 +03:00
feat: enhance CLI validation for skill version and reputation threshold; update documentation
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user