feat: enhance clawsec-clawhub-checker with setup script and reputation checks

This commit is contained in:
David Abutbul
2026-02-16 20:56:44 +02:00
parent 765255680c
commit 6893390ab4
6 changed files with 97 additions and 51 deletions
+8 -3
View File
@@ -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
@@ -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",
@@ -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",
});
@@ -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) {
@@ -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 {
@@ -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"
}
]
},