clawhub-guard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -0
- package/bin/clawhub-guard.js +3 -0
- package/package.json +33 -0
- package/src/cli.js +132 -0
- package/src/install.js +133 -0
- package/src/scan.js +104 -0
package/README.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# 🛡️ clawhub-guard
|
|
2
|
+
|
|
3
|
+
**Pre-install security scanner for ClawHub skills.**
|
|
4
|
+
Scan before you install. Never trust blindly.
|
|
5
|
+
|
|
6
|
+
[](https://www.npmjs.com/package/clawhub-guard)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why?
|
|
12
|
+
|
|
13
|
+
ClawHub hosts thousands of community skills — but not all are safe. In early 2026, the ClawHavoc campaign distributed 341 malicious skills through the registry, stealing credentials and installing malware.
|
|
14
|
+
|
|
15
|
+
**clawhub-guard** adds a mandatory security checkpoint before every install.
|
|
16
|
+
|
|
17
|
+
## What It Does
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
$ clawhub-guard install summarize
|
|
21
|
+
|
|
22
|
+
📦 Installing summarize for pre-scan...
|
|
23
|
+
✓ Installed summarize@2.1.0
|
|
24
|
+
|
|
25
|
+
🔍 Running security scan on summarize...
|
|
26
|
+
|
|
27
|
+
═══════════════════════════════════════════════════════
|
|
28
|
+
SECURITY SCAN REPORT — clawhub-guard
|
|
29
|
+
═══════════════════════════════════════════════════════
|
|
30
|
+
Target: ~/.openclaw/workspace/skills/summarize
|
|
31
|
+
Score: 94/100
|
|
32
|
+
Threshold: 70/100
|
|
33
|
+
Verdict: ✅ PASS
|
|
34
|
+
Engines: 1/4 available
|
|
35
|
+
Findings: 2
|
|
36
|
+
───────────────────────────────────────────────────────
|
|
37
|
+
⚪ low: 2
|
|
38
|
+
───────────────────────────────────────────────────────
|
|
39
|
+
• [low] tool-shadowing: Redirects from another tool
|
|
40
|
+
• [low] excessive-perms: Too many permissions declared
|
|
41
|
+
═══════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
✅ Safe! summarize is ready to use.
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If the score is too low:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
❌ BLOCKED — Uninstalling malicious-skill for safety.
|
|
50
|
+
💡 Tip: Review the findings above. Use --force to install anyway.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install -g clawhub-guard
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Install a ClawHub skill with automatic pre-scan
|
|
63
|
+
clawhub-guard install <skill-name>
|
|
64
|
+
|
|
65
|
+
# Scan an already-installed skill
|
|
66
|
+
clawhub-guard scan <skill-name>
|
|
67
|
+
|
|
68
|
+
# Scan a local skill directory
|
|
69
|
+
clawhub-guard scan --local ./my-skill/
|
|
70
|
+
|
|
71
|
+
# Custom risk threshold (default: 70)
|
|
72
|
+
clawhub-guard install <skill-name> --threshold 80
|
|
73
|
+
|
|
74
|
+
# Force install (skip security scan)
|
|
75
|
+
clawhub-guard install <skill-name> --force
|
|
76
|
+
|
|
77
|
+
# JSON output for automation
|
|
78
|
+
clawhub-guard scan <skill-name> --json
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Scoring
|
|
82
|
+
|
|
83
|
+
| Score | Verdict | Action |
|
|
84
|
+
|-------|---------|--------|
|
|
85
|
+
| 90–100 | ✅ PASS | Safe to install |
|
|
86
|
+
| 70–89 | ⚠️ WARN | Installed with warnings — review findings |
|
|
87
|
+
| 40–69 | ❌ BLOCK | Automatically uninstalled — review report |
|
|
88
|
+
| 0–39 | ❌ BLOCK | Automatically uninstalled — do not use |
|
|
89
|
+
|
|
90
|
+
## Powered By
|
|
91
|
+
|
|
92
|
+
- **[AgentShield](https://www.npmjs.com/package/@elliotllliu/agent-shield)** — 30 security rules covering credential theft, backdoors, prompt injection, obfuscation, and more.
|
|
93
|
+
|
|
94
|
+
## Recommended Workflow
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
# 1. Always use clawhub-guard instead of raw openclaw skills install
|
|
98
|
+
clawhub-guard install <skill-name>
|
|
99
|
+
|
|
100
|
+
# 2. If blocked, review the scan report
|
|
101
|
+
clawhub-guard scan <skill-name> --json | less
|
|
102
|
+
|
|
103
|
+
# 3. Only force-install if you've manually reviewed the code
|
|
104
|
+
clawhub-guard install <skill-name> --force
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Security
|
|
108
|
+
|
|
109
|
+
This tool is itself a security product. It:
|
|
110
|
+
- Runs **locally only** — no telemetry, no external calls beyond agent-shield
|
|
111
|
+
- Does **not** modify your OpenClaw config
|
|
112
|
+
- Does **not** read your credentials or session data
|
|
113
|
+
- Automatically **uninstalls** skills that fail the security threshold
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT © [taiwanape](https://github.com/taiwanape)
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
> "Trust is earned in milliseconds, lost in microseconds, and clawed back never." — clawhub-guard
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clawhub-guard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Pre-install security scanner for ClawHub skills — scan before you install, never trust blindly.",
|
|
5
|
+
"main": "src/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"clawhub-guard": "./bin/clawhub-guard.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node src/cli.js scan --help"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"clawhub",
|
|
14
|
+
"openclaw",
|
|
15
|
+
"security",
|
|
16
|
+
"skill",
|
|
17
|
+
"scanner",
|
|
18
|
+
"vetter",
|
|
19
|
+
"agent-shield"
|
|
20
|
+
],
|
|
21
|
+
"author": "taiwanape",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/taiwanape/clawhub-guard.git"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@elliotllliu/agent-shield": "^0.16.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// src/cli.js — Main CLI for clawhub-guard
|
|
3
|
+
|
|
4
|
+
const { scanLocal, getSkillInstallPath, isSkillInstalled } = require('./scan.js');
|
|
5
|
+
const { installSkill } = require('./install.js');
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
const command = args[0];
|
|
9
|
+
|
|
10
|
+
function usage() {
|
|
11
|
+
console.log(`
|
|
12
|
+
🛡️ clawhub-guard — Pre-install security scanner for ClawHub skills
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
clawhub-guard install <skill-name> Scan + install a ClawHub skill
|
|
16
|
+
clawhub-guard scan <skill-name> Scan an installed ClawHub skill
|
|
17
|
+
clawhub-guard scan --local <path> Scan a local skill directory
|
|
18
|
+
clawhub-guard --help Show this help
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--threshold <0-100> Minimum score to pass (default: 70)
|
|
22
|
+
--force Skip security scan and install anyway
|
|
23
|
+
--json Output scan result as JSON
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
clawhub-guard install summarize
|
|
27
|
+
clawhub-guard scan --local ./my-skill/
|
|
28
|
+
clawhub-guard install database-query --threshold 80
|
|
29
|
+
clawhub-guard scan skill-vetter --json
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!command || command === '--help' || command === '-h') {
|
|
34
|
+
usage();
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Parse options
|
|
39
|
+
const options = {
|
|
40
|
+
threshold: 70,
|
|
41
|
+
force: false,
|
|
42
|
+
json: false,
|
|
43
|
+
};
|
|
44
|
+
for (let i = 0; i < args.length; i++) {
|
|
45
|
+
if (args[i] === '--threshold' && args[i + 1]) {
|
|
46
|
+
options.threshold = parseInt(args[i + 1], 10);
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
if (args[i] === '--force') options.force = true;
|
|
50
|
+
if (args[i] === '--json') options.json = true;
|
|
51
|
+
if (args[i] === '--local') options.local = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
switch (command) {
|
|
55
|
+
case 'install': {
|
|
56
|
+
const skillName = args[1];
|
|
57
|
+
if (!skillName) {
|
|
58
|
+
console.error('❌ Error: skill name required.\nUsage: clawhub-guard install <skill-name>');
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
const result = installSkill(skillName, options);
|
|
62
|
+
process.exit(result.success ? 0 : 1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
case 'scan': {
|
|
66
|
+
let target = args[1];
|
|
67
|
+
if (!target && !options.local) {
|
|
68
|
+
console.error('❌ Error: skill name or --local <path> required.');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// --local mode: scan a directory
|
|
73
|
+
if (options.local) {
|
|
74
|
+
target = args.find((a, i) => args[i - 1] === '--local') || args[2];
|
|
75
|
+
if (!target) {
|
|
76
|
+
// If --local was passed as the target itself (e.g., scan --local ./path)
|
|
77
|
+
target = args[1];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const fs = require('node:fs');
|
|
82
|
+
const path = require('node:path');
|
|
83
|
+
const os = require('node:os');
|
|
84
|
+
|
|
85
|
+
let scanTarget;
|
|
86
|
+
if (options.local || target.includes('/') || target.includes('\\')) {
|
|
87
|
+
scanTarget = target;
|
|
88
|
+
} else {
|
|
89
|
+
scanTarget = getSkillInstallPath(target);
|
|
90
|
+
if (!isSkillInstalled(target)) {
|
|
91
|
+
// Try fuzzy match
|
|
92
|
+
const workspace = process.env.OPENCLAW_WORKSPACE_DIR ||
|
|
93
|
+
path.join(os.homedir(), '.openclaw', 'workspace');
|
|
94
|
+
const skillsDir = path.join(workspace, 'skills');
|
|
95
|
+
let installedList = [];
|
|
96
|
+
if (fs.existsSync(skillsDir)) {
|
|
97
|
+
installedList = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
98
|
+
.filter(d => d.isDirectory())
|
|
99
|
+
.map(d => d.name);
|
|
100
|
+
// Normalize names (strip hyphens, underscores) for fuzzy matching
|
|
101
|
+
const normalize = s => s.toLowerCase().replace(/[-_]/g, '');
|
|
102
|
+
const normTarget = normalize(target);
|
|
103
|
+
const match = installedList.find(e =>
|
|
104
|
+
normalize(e).includes(normTarget) || normTarget.includes(normalize(e))
|
|
105
|
+
);
|
|
106
|
+
if (match) {
|
|
107
|
+
scanTarget = getSkillInstallPath(match);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!scanTarget) {
|
|
111
|
+
console.error(`❌ Skill '${target}' not installed.`);
|
|
112
|
+
console.error(` Installed skills: ${installedList.join(', ') || 'none'}`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = scanLocal(scanTarget, options);
|
|
119
|
+
if (options.json) {
|
|
120
|
+
console.log(JSON.stringify(result, null, 2));
|
|
121
|
+
} else {
|
|
122
|
+
const { printScanReport } = require('./install.js');
|
|
123
|
+
printScanReport(result);
|
|
124
|
+
}
|
|
125
|
+
process.exit(result.passed ? 0 : 1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
default:
|
|
129
|
+
console.error(`❌ Unknown command: ${command}`);
|
|
130
|
+
usage();
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
package/src/install.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// src/install.js — Install logic with pre-scan guard
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('node:child_process');
|
|
4
|
+
const { scanLocal, isSkillInstalled } = require('./scan.js');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Install a ClawHub skill with pre-scan security check
|
|
8
|
+
* @param {string} skillName - Name of the ClawHub skill
|
|
9
|
+
* @param {object} options
|
|
10
|
+
* @param {number} options.threshold - Minimum security score (default 70)
|
|
11
|
+
* @param {boolean} options.force - Skip scan and install anyway
|
|
12
|
+
* @returns {object} Install result
|
|
13
|
+
*/
|
|
14
|
+
function installSkill(skillName, options = {}) {
|
|
15
|
+
const threshold = options.threshold ?? 70;
|
|
16
|
+
|
|
17
|
+
// Step 0: Force mode — skip scan
|
|
18
|
+
if (options.force) {
|
|
19
|
+
return doInstall(skillName);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Step 1: Can't pre-scan before download, so install first →
|
|
23
|
+
// scan the installed copy, then warn
|
|
24
|
+
console.log(`\n📦 Installing ${skillName} for pre-scan...\n`);
|
|
25
|
+
|
|
26
|
+
let installResult = doInstall(skillName);
|
|
27
|
+
if (!installResult.success) {
|
|
28
|
+
return installResult;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Step 2: Scan the just-installed skill
|
|
32
|
+
console.log(`\n🔍 Running security scan on ${skillName}...\n`);
|
|
33
|
+
|
|
34
|
+
const scanResult = scanLocal(installResult.installPath, { threshold });
|
|
35
|
+
|
|
36
|
+
// Step 3: Print report
|
|
37
|
+
printScanReport(scanResult);
|
|
38
|
+
|
|
39
|
+
// Step 4: If unsafe, offer to uninstall
|
|
40
|
+
if (!scanResult.passed) {
|
|
41
|
+
console.log(`\n⚠️ RISK THRESHOLD NOT MET (${scanResult.score}/100, need ${threshold})`);
|
|
42
|
+
console.log(` Verdict: ${scanResult.verdict}`);
|
|
43
|
+
|
|
44
|
+
if (scanResult.verdict === 'BLOCK') {
|
|
45
|
+
console.log(`\n❌ BLOCKED — Uninstalling ${skillName} for safety.\n`);
|
|
46
|
+
try {
|
|
47
|
+
execSync(`openclaw.cmd skills uninstall ${skillName}`, {
|
|
48
|
+
encoding: 'utf-8',
|
|
49
|
+
stdio: 'pipe'
|
|
50
|
+
});
|
|
51
|
+
} catch {
|
|
52
|
+
// Manual cleanup
|
|
53
|
+
const fs = require('node:fs');
|
|
54
|
+
const path = require('node:path');
|
|
55
|
+
const skillPath = installResult.installPath;
|
|
56
|
+
if (fs.existsSync(skillPath)) {
|
|
57
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
console.log(`💡 Tip: Review the findings above. Use --force to install anyway.\n`);
|
|
61
|
+
return { success: false, blocked: true, scanResult, installResult };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (scanResult.verdict === 'WARN') {
|
|
65
|
+
console.log(`⚠️ Installed with warnings — review the findings above.\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { success: true, blocked: false, scanResult, installResult };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function doInstall(skillName) {
|
|
73
|
+
const path = require('node:path');
|
|
74
|
+
const fs = require('node:fs');
|
|
75
|
+
const os = require('node:os');
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const output = execSync(`openclaw.cmd skills install ${skillName}`, {
|
|
79
|
+
encoding: 'utf-8',
|
|
80
|
+
timeout: 60000,
|
|
81
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
82
|
+
});
|
|
83
|
+
console.log(output);
|
|
84
|
+
|
|
85
|
+
// Determine install path
|
|
86
|
+
const workspace = process.env.OPENCLAW_WORKSPACE_DIR ||
|
|
87
|
+
path.join(os.homedir(), '.openclaw', 'workspace');
|
|
88
|
+
const installPath = path.join(workspace, 'skills', skillName);
|
|
89
|
+
|
|
90
|
+
return { success: true, installPath, output };
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const errMsg = err.stderr || err.stdout || err.message || 'Unknown error';
|
|
93
|
+
console.error(`❌ Install failed: ${errMsg}`);
|
|
94
|
+
return { success: false, error: errMsg };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function printScanReport(result) {
|
|
99
|
+
console.log(`\n${'═'.repeat(55)}`);
|
|
100
|
+
console.log(` SECURITY SCAN REPORT — clawhub-guard`);
|
|
101
|
+
console.log(`${'═'.repeat(55)}`);
|
|
102
|
+
console.log(` Target: ${result.target || 'N/A'}`);
|
|
103
|
+
console.log(` Score: ${result.score}/100`);
|
|
104
|
+
console.log(` Threshold: ${result.threshold}/100`);
|
|
105
|
+
console.log(` Verdict: ${result.passed ? '✅ PASS' : result.verdict === 'BLOCK' ? '❌ BLOCK' : '⚠️ WARN'}`);
|
|
106
|
+
console.log(` Engines: ${result.availableEngines}/${result.totalEngines} available`);
|
|
107
|
+
console.log(` Findings: ${(result.findings || []).length}`);
|
|
108
|
+
console.log(`${'─'.repeat(55)}`);
|
|
109
|
+
|
|
110
|
+
if (result.findings && result.findings.length > 0) {
|
|
111
|
+
const bySeverity = {};
|
|
112
|
+
for (const f of result.findings) {
|
|
113
|
+
bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
|
|
114
|
+
}
|
|
115
|
+
for (const [sev, count] of Object.entries(bySeverity)) {
|
|
116
|
+
const icon = sev === 'critical' ? '🔴' : sev === 'high' ? '🟠' : sev === 'medium' ? '🟡' : '⚪';
|
|
117
|
+
console.log(` ${icon} ${sev}: ${count}`);
|
|
118
|
+
}
|
|
119
|
+
console.log(`${'─'.repeat(55)}`);
|
|
120
|
+
for (const f of (result.findings || []).slice(0, 5)) {
|
|
121
|
+
console.log(` • [${f.severity}] ${f.rule}: ${f.message}`);
|
|
122
|
+
}
|
|
123
|
+
if (result.findings.length > 5) {
|
|
124
|
+
console.log(` • ... and ${result.findings.length - 5} more`);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
console.log(` ✅ No findings — clean!`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(`${'═'.repeat(55)}\n`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { installSkill, printScanReport };
|
package/src/scan.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/scan.js — Security scanning engine using agent-shield
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('node:child_process');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Scan a local directory for security issues using agent-shield
|
|
10
|
+
* @param {string} targetPath - Path to the skill directory
|
|
11
|
+
* @param {object} options
|
|
12
|
+
* @param {number} options.threshold - Minimum score to pass (0-100)
|
|
13
|
+
* @returns {object} Scan result with score, findings, and verdict
|
|
14
|
+
*/
|
|
15
|
+
function scanLocal(targetPath, options = {}) {
|
|
16
|
+
const threshold = options.threshold ?? 70;
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(targetPath)) {
|
|
19
|
+
return {
|
|
20
|
+
success: false,
|
|
21
|
+
error: `Target not found: ${targetPath}`,
|
|
22
|
+
score: 0,
|
|
23
|
+
verdict: 'ERROR',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let stdout;
|
|
28
|
+
try {
|
|
29
|
+
stdout = execSync(
|
|
30
|
+
`npx @elliotllliu/agent-shield scan "${targetPath}" --json`,
|
|
31
|
+
{
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
timeout: 60000,
|
|
34
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
35
|
+
env: { ...process.env },
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
// agent-shield exits non-zero if findings exist — still parse output
|
|
40
|
+
stdout = err.stdout || err.stderr || '';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Try to extract JSON from agent-shield output (it may have noise)
|
|
44
|
+
let result;
|
|
45
|
+
try {
|
|
46
|
+
const jsonStart = stdout.indexOf('{');
|
|
47
|
+
if (jsonStart >= 0) {
|
|
48
|
+
result = JSON.parse(stdout.slice(jsonStart));
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Fallback: parse manually
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!result) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
error: 'Failed to parse agent-shield output',
|
|
58
|
+
rawOutput: stdout.slice(0, 500),
|
|
59
|
+
score: 0,
|
|
60
|
+
verdict: 'ERROR',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Calculate score based on findings
|
|
65
|
+
const allFindings = result.allFindings || [];
|
|
66
|
+
const severityScores = { critical: 40, high: 25, medium: 10, low: 3 };
|
|
67
|
+
let penalty = 0;
|
|
68
|
+
for (const f of allFindings) {
|
|
69
|
+
penalty += severityScores[f.severity] || 5;
|
|
70
|
+
}
|
|
71
|
+
const score = Math.max(0, 100 - penalty);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
success: true,
|
|
75
|
+
score,
|
|
76
|
+
threshold,
|
|
77
|
+
passed: score >= threshold,
|
|
78
|
+
verdict: score >= threshold ? 'PASS' : score < 40 ? 'BLOCK' : 'WARN',
|
|
79
|
+
findings: allFindings,
|
|
80
|
+
engines: result.engines || [],
|
|
81
|
+
totalEngines: result.totalEngines || 0,
|
|
82
|
+
availableEngines: result.availableEngines || 0,
|
|
83
|
+
target: result.target || targetPath,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Determine ClawHub skill install directory
|
|
89
|
+
*/
|
|
90
|
+
function getSkillInstallPath(skillName) {
|
|
91
|
+
const workspace = process.env.OPENCLAW_WORKSPACE_DIR ||
|
|
92
|
+
path.join(os.homedir(), '.openclaw', 'workspace');
|
|
93
|
+
return path.join(workspace, 'skills', skillName);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a skill is already installed locally
|
|
98
|
+
*/
|
|
99
|
+
function isSkillInstalled(skillName) {
|
|
100
|
+
const installPath = getSkillInstallPath(skillName);
|
|
101
|
+
return fs.existsSync(installPath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { scanLocal, getSkillInstallPath, isSkillInstalled };
|