mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-15 22:41:20 +03:00
feat: enhance clawsec-clawhub-checker with setup script and reputation checks
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user