openclawsec 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/.github/ISSUE_TEMPLATE/bug-report.md +42 -0
- package/.github/ISSUE_TEMPLATE/feature-request.md +23 -0
- package/.github/workflows/ci.yml +41 -0
- package/CONTRIBUTING.md +28 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/clawshield-web/index.html +344 -0
- package/cli.js +184 -0
- package/package.json +33 -0
- package/src/checks/configHarden.js +210 -0
- package/src/checks/cve.js +115 -0
- package/src/checks/secretsCheck.js +192 -0
- package/src/checks/skillAudit.js +204 -0
- package/src/checks/version.js +114 -0
- package/src/commands/audit.js +59 -0
- package/src/commands/doctor.js +85 -0
- package/src/commands/monitor.js +175 -0
- package/src/commands/scan.js +144 -0
- package/src/utils/output.js +171 -0
package/cli.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { scan } = require('./src/commands/scan');
|
|
4
|
+
const { doctor } = require('./src/commands/doctor');
|
|
5
|
+
const { audit } = require('./src/commands/audit');
|
|
6
|
+
const { runMonitor, checkStatus } = require('./src/commands/monitor');
|
|
7
|
+
const out = require('./src/utils/output');
|
|
8
|
+
|
|
9
|
+
const COMMANDS = {
|
|
10
|
+
scan: {
|
|
11
|
+
description: 'Run full security scan',
|
|
12
|
+
aliases: ['s', 'scan']
|
|
13
|
+
},
|
|
14
|
+
doctor: {
|
|
15
|
+
description: 'Quick health check',
|
|
16
|
+
aliases: ['d', 'doctor', 'diag']
|
|
17
|
+
},
|
|
18
|
+
audit: {
|
|
19
|
+
description: 'Audit installed skills and secrets',
|
|
20
|
+
aliases: ['a', 'audit']
|
|
21
|
+
},
|
|
22
|
+
monitor: {
|
|
23
|
+
description: 'Continuous monitoring mode',
|
|
24
|
+
aliases: ['m', 'monitor', 'watch']
|
|
25
|
+
},
|
|
26
|
+
status: {
|
|
27
|
+
description: 'Check monitoring status',
|
|
28
|
+
aliases: ['st', 'status']
|
|
29
|
+
},
|
|
30
|
+
help: {
|
|
31
|
+
description: 'Show this help message',
|
|
32
|
+
aliases: ['h', 'help', '--help', '-h']
|
|
33
|
+
},
|
|
34
|
+
version: {
|
|
35
|
+
description: 'Show ClawShield version',
|
|
36
|
+
aliases: ['v', 'version', '--version', '-v']
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const VERSION = '1.0.0';
|
|
41
|
+
|
|
42
|
+
function showBanner() {
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(out.COLORS.title(' _____ _ _____ __'));
|
|
45
|
+
console.log(out.COLORS.title(' / ____| | | __ \\ / _|'));
|
|
46
|
+
console.log(out.COLORS.title(' | | | | | |__) | |_ '));
|
|
47
|
+
console.log(out.COLORS.title(' | | | | | ___/| _|'));
|
|
48
|
+
console.log(out.COLORS.title(' | |____| |___| | | | '));
|
|
49
|
+
console.log(out.COLORS.title(' \\_____|______|_| |_| '));
|
|
50
|
+
console.log('');
|
|
51
|
+
console.log(out.COLORS.dim(' OpenClaw Security Monitor v' + VERSION));
|
|
52
|
+
console.log(out.COLORS.dim(' https://github.com/clawshield/openclawsec'));
|
|
53
|
+
console.log('');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function showHelp() {
|
|
57
|
+
showBanner();
|
|
58
|
+
|
|
59
|
+
console.log(out.COLORS.title('USAGE:'));
|
|
60
|
+
console.log(' clawshield <command> [options]');
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
63
|
+
console.log(out.COLORS.title('COMMANDS:'));
|
|
64
|
+
|
|
65
|
+
const longestDesc = Math.max(...Object.values(COMMANDS).map(c => c.description.length));
|
|
66
|
+
|
|
67
|
+
for (const [name, cmd] of Object.entries(COMMANDS)) {
|
|
68
|
+
const paddedName = name.padEnd(12);
|
|
69
|
+
const aliases = cmd.aliases.filter(a => a !== name).join(', ');
|
|
70
|
+
console.log(` ${out.COLORS.cyan(paddedName)} ${out.COLORS.dim(cmd.description)}`);
|
|
71
|
+
if (aliases) {
|
|
72
|
+
console.log(`${out.COLORS.dim(' '.repeat(14))}(alias: ${aliases})`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log(out.COLORS.title('EXAMPLES:'));
|
|
78
|
+
console.log(` ${out.COLORS.cyan('clawshield scan')} ${out.COLORS.dim('Run full security scan')}`);
|
|
79
|
+
console.log(` ${out.COLORS.cyan('clawshield doctor')} ${out.COLORS.dim('Quick health check')}`);
|
|
80
|
+
console.log(` ${out.COLORS.cyan('clawshield audit')} ${out.COLORS.dim('Audit skills and secrets')}`);
|
|
81
|
+
console.log(` ${out.COLORS.cyan('clawshield monitor')} ${out.COLORS.dim('Continuous monitoring')}`);
|
|
82
|
+
console.log(` ${out.COLORS.cyan('clawshield monitor --60')} ${out.COLORS.dim('Monitor every 60 minutes')}`);
|
|
83
|
+
console.log(` ${out.COLORS.cyan('clawshield status')} ${out.COLORS.dim('Check monitoring status')}`);
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(out.COLORS.title('QUICK START:'));
|
|
86
|
+
console.log(' 1. Run: clawshield scan # Full security scan');
|
|
87
|
+
console.log(' 2. Run: clawshield doctor # Quick check');
|
|
88
|
+
console.log(' 3. Run: clawshield monitor # Continuous monitoring');
|
|
89
|
+
console.log('');
|
|
90
|
+
console.log(out.COLORS.dim('For more info, visit: https://github.com/clawshield/openclawsec'));
|
|
91
|
+
console.log('');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function showVersion() {
|
|
95
|
+
console.log('ClawShield v' + VERSION);
|
|
96
|
+
console.log('OpenClaw Security Monitor');
|
|
97
|
+
console.log('');
|
|
98
|
+
console.log('License: MIT');
|
|
99
|
+
console.log('Repository: https://github.com/clawshield/openclawsec');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function findCommand(input) {
|
|
103
|
+
const cmd = input.toLowerCase();
|
|
104
|
+
|
|
105
|
+
for (const [name, config] of Object.entries(COMMANDS)) {
|
|
106
|
+
if (name === cmd || config.aliases.includes(cmd)) {
|
|
107
|
+
return name;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function main() {
|
|
115
|
+
const args = process.argv.slice(2);
|
|
116
|
+
|
|
117
|
+
if (args.length === 0) {
|
|
118
|
+
showHelp();
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const inputCmd = args[0];
|
|
123
|
+
const command = findCommand(inputCmd);
|
|
124
|
+
|
|
125
|
+
if (!command) {
|
|
126
|
+
console.log(out.COLORS.error(`Unknown command: ${inputCmd}`));
|
|
127
|
+
console.log('');
|
|
128
|
+
console.log(`Run '${out.COLORS.cyan('clawshield help')}' for available commands`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (command === 'help') {
|
|
133
|
+
showHelp();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (command === 'version') {
|
|
138
|
+
showVersion();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const remainingArgs = args.slice(1);
|
|
143
|
+
|
|
144
|
+
switch (command) {
|
|
145
|
+
case 'scan':
|
|
146
|
+
await scan();
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
case 'doctor':
|
|
150
|
+
await doctor();
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case 'audit':
|
|
154
|
+
audit();
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case 'monitor':
|
|
158
|
+
let interval = 60 * 60 * 1000;
|
|
159
|
+
|
|
160
|
+
const intervalArg = remainingArgs.find(arg => arg.startsWith('--'));
|
|
161
|
+
if (intervalArg) {
|
|
162
|
+
const minutes = parseInt(intervalArg.replace('--', ''));
|
|
163
|
+
if (!isNaN(minutes) && minutes > 0) {
|
|
164
|
+
interval = minutes * 60 * 1000;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await runMonitor(interval);
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
case 'status':
|
|
172
|
+
checkStatus();
|
|
173
|
+
break;
|
|
174
|
+
|
|
175
|
+
default:
|
|
176
|
+
console.log(out.COLORS.error('Command not implemented: ' + command));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
main().catch(error => {
|
|
182
|
+
console.error(out.COLORS.error('Fatal error: ' + error.message));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclawsec",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenClaw Security Monitoring & Hardening Tool",
|
|
5
|
+
"main": "cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"openclawsec": "./cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node cli.js",
|
|
11
|
+
"scan": "node cli.js scan",
|
|
12
|
+
"doctor": "node cli.js doctor",
|
|
13
|
+
"audit": "node cli.js audit",
|
|
14
|
+
"monitor": "node cli.js monitor"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["openclaw", "security", "monitoring", "cli", "cve", "hardening"],
|
|
17
|
+
"author": "rastuak",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/rastuak/clawshield.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/rastuak/clawshield/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/rastuak/clawshield",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"chalk": "^4.1.2",
|
|
29
|
+
"ora": "^5.4.1",
|
|
30
|
+
"node-fetch": "^2.7.0",
|
|
31
|
+
"tablemark": "^3.1.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const out = require('../utils/output');
|
|
2
|
+
|
|
3
|
+
function getOpenClawConfigPath() {
|
|
4
|
+
const home = process.env.USERPROFILE || process.env.HOME || process.env.HOMEPATH;
|
|
5
|
+
|
|
6
|
+
const possiblePaths = [
|
|
7
|
+
`${home}\\.openclaw\\openclaw.json`,
|
|
8
|
+
`${home}\\.config\\openclaw\\openclaw.json`,
|
|
9
|
+
`${home}\\openclaw\\openclaw.json`,
|
|
10
|
+
'.openclaw/openclaw.json',
|
|
11
|
+
'~/.openclaw/openclaw.json'
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
return possiblePaths[0];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getConfigFromFile(path) {
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const pathLib = require('path');
|
|
20
|
+
|
|
21
|
+
const configPath = path || getOpenClawConfigPath();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(configPath)) {
|
|
25
|
+
const content = fs.readFileSync(configPath, 'utf8');
|
|
26
|
+
return JSON.parse(content);
|
|
27
|
+
}
|
|
28
|
+
} catch (error) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function checkGatewayExposure(config) {
|
|
36
|
+
out.heading('Gateway Port Exposure Check');
|
|
37
|
+
|
|
38
|
+
const issues = [];
|
|
39
|
+
|
|
40
|
+
if (!config) {
|
|
41
|
+
out.warning('Could not read openclaw.json configuration');
|
|
42
|
+
out.text('Checking default gateway port (18789) accessibility...');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const port = config?.gateway?.port || 18789;
|
|
46
|
+
const host = config?.gateway?.host || '127.0.0.1';
|
|
47
|
+
|
|
48
|
+
out.keyValue('Gateway Port', port.toString());
|
|
49
|
+
out.keyValue('Gateway Host', host);
|
|
50
|
+
|
|
51
|
+
if (host === '0.0.0.0' || host === '::') {
|
|
52
|
+
issues.push({
|
|
53
|
+
severity: 'critical',
|
|
54
|
+
message: 'Gateway is bound to 0.0.0.0 - accessible from all network interfaces',
|
|
55
|
+
recommendation: 'Bind to 127.0.0.1 or localhost only for security'
|
|
56
|
+
});
|
|
57
|
+
out.critical('Gateway is exposed to all network interfaces!');
|
|
58
|
+
} else if (host === '127.0.0.1' || host === 'localhost') {
|
|
59
|
+
out.passed('Gateway is bound to localhost only - not exposed externally');
|
|
60
|
+
} else {
|
|
61
|
+
issues.push({
|
|
62
|
+
severity: 'warning',
|
|
63
|
+
message: `Gateway is bound to ${host} - verify this is intentional`,
|
|
64
|
+
recommendation: 'Ensure this is an expected configuration'
|
|
65
|
+
});
|
|
66
|
+
out.warning(`Gateway bound to non-default host: ${host}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (config?.gateway?.token === undefined || config?.gateway?.token === null || config?.gateway?.token === '') {
|
|
70
|
+
issues.push({
|
|
71
|
+
severity: 'critical',
|
|
72
|
+
message: 'No gateway token configured - authentication is disabled!',
|
|
73
|
+
recommendation: 'Set a strong gateway token in config'
|
|
74
|
+
});
|
|
75
|
+
out.critical('No gateway token configured! Anyone can access your gateway.');
|
|
76
|
+
} else if (config?.gateway?.token) {
|
|
77
|
+
if (config.gateway.token.length < 32) {
|
|
78
|
+
issues.push({
|
|
79
|
+
severity: 'warning',
|
|
80
|
+
message: 'Gateway token is shorter than recommended (32+ chars)',
|
|
81
|
+
recommendation: 'Use a longer, random token for better security'
|
|
82
|
+
});
|
|
83
|
+
out.warning('Gateway token is shorter than recommended');
|
|
84
|
+
} else {
|
|
85
|
+
out.passed('Gateway token is configured');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
port,
|
|
91
|
+
host,
|
|
92
|
+
issues,
|
|
93
|
+
exposed: host === '0.0.0.0' || host === '::'
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function checkDMPolicy(config) {
|
|
98
|
+
out.heading('Direct Message Policy Check');
|
|
99
|
+
|
|
100
|
+
const dmPolicy = config?.channels?.dmScope || config?.dmScope || 'main';
|
|
101
|
+
|
|
102
|
+
out.keyValue('DM Policy', dmPolicy);
|
|
103
|
+
|
|
104
|
+
if (dmPolicy === 'all') {
|
|
105
|
+
out.critical('DM policy set to "all" - messages from any user trigger responses');
|
|
106
|
+
out.text('This allows anyone to send messages to your agent!');
|
|
107
|
+
out.text('Recommendation: Set to "pairing" or "allowlist"');
|
|
108
|
+
return { severity: 'critical', policy: dmPolicy, issues: ['DM policy too permissive'] };
|
|
109
|
+
} else if (dmPolicy === 'main') {
|
|
110
|
+
out.passed('DM policy is set to "main" - only existing conversations active');
|
|
111
|
+
return { severity: 'passed', policy: dmPolicy, issues: [] };
|
|
112
|
+
} else if (dmPolicy === 'pairing') {
|
|
113
|
+
out.passed('DM policy is set to "pairing" - users must be approved first');
|
|
114
|
+
return { severity: 'passed', policy: dmPolicy, issues: [] };
|
|
115
|
+
} else if (dmPolicy === 'allowlist') {
|
|
116
|
+
out.passed('DM policy is set to "allowlist" - only approved users can interact');
|
|
117
|
+
return { severity: 'passed', policy: dmPolicy, issues: [] };
|
|
118
|
+
} else {
|
|
119
|
+
out.warning(`Unknown DM policy: ${dmPolicy}`);
|
|
120
|
+
return { severity: 'warning', policy: dmPolicy, issues: [`Unknown DM policy: ${dmPolicy}`] };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function checkToolsPermissions(config) {
|
|
125
|
+
out.heading('Tools & Permissions Check');
|
|
126
|
+
|
|
127
|
+
const toolsProfile = config?.tools?.profile || 'full';
|
|
128
|
+
|
|
129
|
+
out.keyValue('Tools Profile', toolsProfile);
|
|
130
|
+
|
|
131
|
+
const issues = [];
|
|
132
|
+
|
|
133
|
+
if (toolsProfile === 'full') {
|
|
134
|
+
issues.push({
|
|
135
|
+
severity: 'warning',
|
|
136
|
+
message: 'Tools profile is set to "full" - agent has maximum permissions',
|
|
137
|
+
recommendation: 'Consider using "messaging" profile for general use'
|
|
138
|
+
});
|
|
139
|
+
out.warning('Tools profile is "full" - maximum permissions granted');
|
|
140
|
+
} else if (toolsProfile === 'messaging') {
|
|
141
|
+
out.passed('Tools profile is "messaging" - safe default permissions');
|
|
142
|
+
} else if (toolsProfile === 'minimal') {
|
|
143
|
+
out.passed('Tools profile is "minimal" - restricted permissions');
|
|
144
|
+
} else {
|
|
145
|
+
out.warning(`Unknown tools profile: ${toolsProfile}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const dangerousTools = [];
|
|
149
|
+
if (config?.tools?.allowed?.includes('exec') || toolsProfile === 'full') {
|
|
150
|
+
dangerousTools.push('exec (shell commands)');
|
|
151
|
+
}
|
|
152
|
+
if (config?.tools?.allowed?.includes('browser') || toolsProfile === 'full') {
|
|
153
|
+
dangerousTools.push('browser (web browsing)');
|
|
154
|
+
}
|
|
155
|
+
if (config?.tools?.allowed?.includes('read') || toolsProfile === 'full') {
|
|
156
|
+
dangerousTools.push('read (file system access)');
|
|
157
|
+
}
|
|
158
|
+
if (config?.tools?.allowed?.includes('write') || toolsProfile === 'full') {
|
|
159
|
+
dangerousTools.push('write (file system modification)');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (dangerousTools.length > 0 && toolsProfile !== 'full') {
|
|
163
|
+
out.text('Dangerous tools enabled:');
|
|
164
|
+
dangerousTools.forEach(tool => {
|
|
165
|
+
out.text(` - ${tool}`);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return { profile: toolsProfile, issues, dangerousTools };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function checkSecrets(config) {
|
|
173
|
+
out.heading('Secrets & API Keys Check');
|
|
174
|
+
|
|
175
|
+
const secrets = config?.secrets || config?.apiKeys || {};
|
|
176
|
+
|
|
177
|
+
const hasOpenAI = !!secrets?.OPENAI_API_KEY;
|
|
178
|
+
const hasAnthropic = !!secrets?.ANTHROPIC_API_KEY || !!secrets?.CLAUDE_API_KEY;
|
|
179
|
+
const hasGemini = !!secrets?.GEMINI_API_KEY;
|
|
180
|
+
|
|
181
|
+
if (hasOpenAI || hasAnthropic || hasGemini) {
|
|
182
|
+
out.warning('API keys are stored in openclaw.json config file');
|
|
183
|
+
out.text('Consider using environment variables or a secrets manager instead');
|
|
184
|
+
out.text('API keys in config files can be accidentally committed to git');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const issues = [];
|
|
188
|
+
if (Object.keys(secrets).length > 0) {
|
|
189
|
+
issues.push({
|
|
190
|
+
severity: 'warning',
|
|
191
|
+
message: `${Object.keys(secrets).length} API key(s) found in config`,
|
|
192
|
+
recommendation: 'Use environment variables or secrets manager'
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return { secrets, issues, hasOpenAI, hasAnthropic, hasGemini };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function check(config) {
|
|
200
|
+
const results = {
|
|
201
|
+
gateway: checkGatewayExposure(config),
|
|
202
|
+
dmPolicy: checkDMPolicy(config),
|
|
203
|
+
tools: checkToolsPermissions(config),
|
|
204
|
+
secrets: checkSecrets(config)
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
return results;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { check, getOpenClawConfigPath, getConfigFromFile, checkGatewayExposure, checkDMPolicy, checkToolsPermissions, checkSecrets };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
const fetch = require('node-fetch');
|
|
2
|
+
const out = require('../utils/output');
|
|
3
|
+
|
|
4
|
+
const NVD_API_URL = 'https://services.nvd.nist.gov/rest/json/cves/2.0';
|
|
5
|
+
|
|
6
|
+
const KNOWN_CVES = [
|
|
7
|
+
{ id: 'CVE-2026-25253', severity: 'HIGH', cvss: 8.8, description: 'WebSocket token exfiltration - Remote Code Execution' },
|
|
8
|
+
{ id: 'CVE-2026-24763', severity: 'HIGH', cvss: 8.8, description: 'Docker sandbox bypass via PATH manipulation' },
|
|
9
|
+
{ id: 'CVE-2026-33579', severity: 'HIGH', cvss: 8.1, description: 'Privilege escalation via /pair approve' },
|
|
10
|
+
{ id: 'CVE-2026-28446', severity: 'HIGH', cvss: 9.8, description: 'Voice RCE affecting 42,000 instances' },
|
|
11
|
+
{ id: 'CVE-2026-44113', severity: 'HIGH', cvss: 8.3, description: 'TOCTOU race condition in OpenShell filesystem bridge' },
|
|
12
|
+
{ id: 'CVE-2026-25157', severity: 'MEDIUM', cvss: 7.5, description: 'SSH node command injection' },
|
|
13
|
+
{ id: 'CVE-2026-25593', severity: 'MEDIUM', cvss: 6.5, description: 'Authentication bypass on untrusted LANs' },
|
|
14
|
+
{ id: 'CVE-2026-25475', severity: 'MEDIUM', cvss: 5.3, description: 'Gateway configuration exposure' },
|
|
15
|
+
{ id: 'CVE-2026-32302', severity: 'MEDIUM', cvss: 6.1, description: 'WebSocket Origin Validation Bypass' },
|
|
16
|
+
{ id: 'CVE-2026-26327', severity: 'MEDIUM', cvss: 7.1, description: 'Auth bypass via rogue service advertisements' }
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const VULNERABLE_BEFORE = {
|
|
20
|
+
'CVE-2026-25253': '2026.1.29',
|
|
21
|
+
'CVE-2026-24763': '2026.1.30',
|
|
22
|
+
'CVE-2026-33579': '2026.3.28',
|
|
23
|
+
'CVE-2026-28446': '2026.2.12',
|
|
24
|
+
'CVE-2026-44113': '2026.4.22',
|
|
25
|
+
'CVE-2026-25157': '2026.1.25',
|
|
26
|
+
'CVE-2026-25593': '2026.1.30',
|
|
27
|
+
'CVE-2026-25475': '2026.1.30',
|
|
28
|
+
'CVE-2026-32302': '2026.3.11',
|
|
29
|
+
'CVE-2026-26327': '2026.1.30'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function compareVersions(installed, vulnerable) {
|
|
33
|
+
const iParts = installed.split('.').map(Number);
|
|
34
|
+
const vParts = vulnerable.split('.').map(Number);
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < 3; i++) {
|
|
37
|
+
if ((iParts[i] || 0) < (vParts[i] || 0)) return -1;
|
|
38
|
+
if ((iParts[i] || 0) > (vParts[i] || 0)) return 1;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function check(installedVersion) {
|
|
44
|
+
out.heading('CVE Vulnerability Check');
|
|
45
|
+
|
|
46
|
+
if (!installedVersion || installedVersion === 'unknown') {
|
|
47
|
+
out.warning('Cannot check CVEs without knowing the installed version');
|
|
48
|
+
out.text('Run: clawshield doctor to detect your OpenClaw version first');
|
|
49
|
+
return { cves: [], vulnerable: false };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
out.keyValue('Your Version', installedVersion);
|
|
53
|
+
out.text('');
|
|
54
|
+
|
|
55
|
+
const affected = [];
|
|
56
|
+
const fixed = [];
|
|
57
|
+
|
|
58
|
+
for (const cve of KNOWN_CVES) {
|
|
59
|
+
const fixedVersion = VULNERABLE_BEFORE[cve.id];
|
|
60
|
+
if (!fixedVersion) continue;
|
|
61
|
+
|
|
62
|
+
if (compareVersions(installedVersion, fixedVersion) < 0) {
|
|
63
|
+
affected.push(cve);
|
|
64
|
+
} else {
|
|
65
|
+
fixed.push(cve);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (affected.length > 0) {
|
|
70
|
+
out.critical(`FOUND ${affected.length} VULNERABILITIES!`);
|
|
71
|
+
out.text('');
|
|
72
|
+
|
|
73
|
+
affected.forEach(cve => {
|
|
74
|
+
console.log(` ${out.COLORS.critical('●')} ${out.COLORS.error(cve.id)} ${out.COLORS.dim(`CVSS ${cve.cvss}`)}`);
|
|
75
|
+
console.log(` ${cve.description}`);
|
|
76
|
+
console.log(` ${out.COLORS.warning('Fixed in:')} v${VULNERABLE_BEFORE[cve.id]}`);
|
|
77
|
+
console.log('');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
out.text('URGENT: Update OpenClaw immediately!');
|
|
81
|
+
out.text('Run: openclaw upgrade or npm update -g openclaw');
|
|
82
|
+
} else {
|
|
83
|
+
out.passed(`No known vulnerabilities in version ${installedVersion}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (fixed.length > 0 && affected.length === 0) {
|
|
87
|
+
out.text('');
|
|
88
|
+
out.info(`${fixed.length} older vulnerabilities were detected but are now fixed`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
cves: affected,
|
|
93
|
+
fixed,
|
|
94
|
+
vulnerable: affected.length > 0,
|
|
95
|
+
count: affected.length
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function getCVEFromNVD(cveId) {
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch(`${NVD_API_URL}?cveId=${cveId}`, {
|
|
102
|
+
headers: { 'User-Agent': 'ClawShield-Security-Tool' }
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (response.ok) {
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
return data.vulnerabilities?.[0]?.cve;
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
out.error(`Failed to fetch CVE from NVD: ${error.message}`);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { check, getCVEFromNVD, KNOWN_CVES, VULNERABLE_BEFORE };
|