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
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const out = require('../utils/output');
|
|
4
|
+
|
|
5
|
+
const API_KEY_PATTERNS = [
|
|
6
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/, name: 'OpenAI API Key', severity: 'critical' },
|
|
7
|
+
{ pattern: /sk-ant-[a-zA-Z0-9]{20,}/, name: 'Anthropic API Key', severity: 'critical' },
|
|
8
|
+
{ pattern: /AIza[a-zA-Z0-9_-]{35}/, name: 'Google API Key', severity: 'critical' },
|
|
9
|
+
{ pattern: /xAI-[a-zA-Z0-9]{20,}/, name: 'xAI API Key', severity: 'critical' },
|
|
10
|
+
{ pattern: /sk_proj_[a-zA-Z0-9]{20,}/, name: 'OpenAI Project Key', severity: 'critical' },
|
|
11
|
+
{ pattern: /gsk_[a-zA-Z0-9]{20,}/, name: 'Groq API Key', severity: 'critical' },
|
|
12
|
+
{ pattern: /eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*/, name: 'JWT Token', severity: 'high' },
|
|
13
|
+
{ pattern: /-----BEGIN.*PRIVATE KEY-----/, name: 'Private Key', severity: 'critical' },
|
|
14
|
+
{ pattern: /github_pat_[a-zA-Z0-9_]{22,}/, name: 'GitHub Personal Access Token', severity: 'critical' }
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const SENSITIVE_FILE_PATTERNS = [
|
|
18
|
+
'.env',
|
|
19
|
+
'.env.local',
|
|
20
|
+
'.env.production',
|
|
21
|
+
'openclaw.json',
|
|
22
|
+
'credentials.json',
|
|
23
|
+
'tokens.json',
|
|
24
|
+
'secrets.json',
|
|
25
|
+
'config.json'
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function getWorkspacePath() {
|
|
29
|
+
const home = process.env.USERPROFILE || process.env.HOME || process.env.HOMEPATH;
|
|
30
|
+
return path.join(home, '.openclaw', 'workspace');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function scanFileForSecrets(filePath) {
|
|
34
|
+
const findings = [];
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
38
|
+
|
|
39
|
+
for (const { pattern, name, severity } of API_KEY_PATTERNS) {
|
|
40
|
+
const matches = content.match(new RegExp(pattern, 'gi'));
|
|
41
|
+
if (matches) {
|
|
42
|
+
for (const match of matches) {
|
|
43
|
+
findings.push({
|
|
44
|
+
type: name,
|
|
45
|
+
severity,
|
|
46
|
+
preview: match.substring(0, 8) + '...' + match.substring(match.length - 4),
|
|
47
|
+
line: content.split('\n').findIndex(line => line.includes(match)) + 1
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (content.includes('OPENAI_API_KEY') || content.includes('ANTHROPIC_API_KEY')) {
|
|
54
|
+
if (!API_KEY_PATTERNS[0].pattern.test(content)) {
|
|
55
|
+
findings.push({
|
|
56
|
+
type: 'API Key Reference',
|
|
57
|
+
severity: 'warning',
|
|
58
|
+
preview: 'API key variable name detected',
|
|
59
|
+
line: 0
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return findings;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getFilesRecursive(dir, files = []) {
|
|
72
|
+
try {
|
|
73
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
74
|
+
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const fullPath = path.join(dir, entry.name);
|
|
77
|
+
|
|
78
|
+
if (entry.isDirectory()) {
|
|
79
|
+
const skipDirs = ['node_modules', '.git', '__pycache__', 'dist', 'build'];
|
|
80
|
+
if (!skipDirs.includes(entry.name)) {
|
|
81
|
+
getFilesRecursive(fullPath, files);
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
files.push(fullPath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {}
|
|
88
|
+
|
|
89
|
+
return files;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function check() {
|
|
93
|
+
out.heading('Secrets & API Keys Check');
|
|
94
|
+
|
|
95
|
+
const workspacePath = getWorkspacePath();
|
|
96
|
+
out.keyValue('Scanning Path', workspacePath);
|
|
97
|
+
out.text('');
|
|
98
|
+
|
|
99
|
+
if (!fs.existsSync(workspacePath)) {
|
|
100
|
+
out.warning('Workspace directory not found');
|
|
101
|
+
return { findings: [], filesScanned: 0 };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const files = getFilesRecursive(workspacePath);
|
|
105
|
+
out.text(`Scanning ${files.length} files...`);
|
|
106
|
+
out.text('');
|
|
107
|
+
|
|
108
|
+
const allFindings = [];
|
|
109
|
+
const sensitiveFilesFound = [];
|
|
110
|
+
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
const fileName = path.basename(file);
|
|
113
|
+
|
|
114
|
+
const isSensitive = SENSITIVE_FILE_PATTERNS.some(pattern =>
|
|
115
|
+
fileName.includes(pattern) || file.includes(pattern)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (isSensitive) {
|
|
119
|
+
sensitiveFilesFound.push(file);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const fileFindings = scanFileForSecrets(file);
|
|
123
|
+
|
|
124
|
+
if (fileFindings.length > 0) {
|
|
125
|
+
for (const finding of fileFindings) {
|
|
126
|
+
allFindings.push({
|
|
127
|
+
...finding,
|
|
128
|
+
file: file,
|
|
129
|
+
fileName: fileName
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (allFindings.length === 0) {
|
|
136
|
+
out.passed('No exposed API keys or secrets found');
|
|
137
|
+
out.text('');
|
|
138
|
+
out.info('Checked ' + files.length + ' files for sensitive data');
|
|
139
|
+
} else {
|
|
140
|
+
out.critical(`FOUND ${allFindings.length} POTENTIAL SECRET(S)!`);
|
|
141
|
+
out.text('');
|
|
142
|
+
|
|
143
|
+
const criticalFindings = allFindings.filter(f => f.severity === 'critical');
|
|
144
|
+
const highFindings = allFindings.filter(f => f.severity === 'high');
|
|
145
|
+
const warningFindings = allFindings.filter(f => f.severity === 'warning');
|
|
146
|
+
|
|
147
|
+
if (criticalFindings.length > 0) {
|
|
148
|
+
out.text(`${out.COLORS.error('Critical:')} ${criticalFindings.length}`);
|
|
149
|
+
}
|
|
150
|
+
if (highFindings.length > 0) {
|
|
151
|
+
out.text(`${out.COLORS.warning('High:')} ${highFindings.length}`);
|
|
152
|
+
}
|
|
153
|
+
if (warningFindings.length > 0) {
|
|
154
|
+
out.text(`${out.COLORS.warning('Warning:')} ${warningFindings.length}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
out.text('');
|
|
158
|
+
|
|
159
|
+
for (const finding of allFindings.slice(0, 10)) {
|
|
160
|
+
console.log(` ${out.COLORS.error('●')} ${out.COLORS.highlight(finding.fileName)}`);
|
|
161
|
+
console.log(` ${finding.type}: ${finding.preview}`);
|
|
162
|
+
if (finding.line > 0) {
|
|
163
|
+
console.log(` ${out.COLORS.dim('Line: ' + finding.line)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (allFindings.length > 10) {
|
|
168
|
+
console.log(` ${out.COLORS.dim(`... and ${allFindings.length - 10} more`)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (sensitiveFilesFound.length > 0) {
|
|
173
|
+
out.text('');
|
|
174
|
+
out.warning('Sensitive configuration files found:');
|
|
175
|
+
for (const file of sensitiveFilesFound.slice(0, 5)) {
|
|
176
|
+
out.text(` - ${path.basename(file)}`);
|
|
177
|
+
}
|
|
178
|
+
if (sensitiveFilesFound.length > 5) {
|
|
179
|
+
out.text(` ${out.COLORS.dim(`... and ${sensitiveFilesFound.length - 5} more`)}`);
|
|
180
|
+
}
|
|
181
|
+
out.text('');
|
|
182
|
+
out.text('Recommendation: Do not commit these files to git!');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
findings: allFindings,
|
|
187
|
+
filesScanned: files.length,
|
|
188
|
+
sensitiveFiles: sensitiveFilesFound.length
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = { check, scanFileForSecrets, API_KEY_PATTERNS };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const out = require('../utils/output');
|
|
4
|
+
|
|
5
|
+
const MALICIOUS_PATTERNS = [
|
|
6
|
+
{ pattern: /91\.92\.242\.30/, name: 'ClawHavoc C2 server IP', severity: 'critical' },
|
|
7
|
+
{ pattern: /bore\.pub/, name: 'Tunnel service (potential exfil)', severity: 'critical' },
|
|
8
|
+
{ pattern: /localhost\.run/, name: 'Tunnel service (potential exfil)', severity: 'critical' },
|
|
9
|
+
{ pattern: /curl.*\|.*bash/i, name: 'Pipe to bash (curl | bash)', severity: 'critical' },
|
|
10
|
+
{ pattern: /wget.*\|.*bash/i, name: 'Pipe to bash (wget | bash)', severity: 'critical' },
|
|
11
|
+
{ pattern: /base64.*-d.*curl/i, name: 'Base64 decode + curl (stealth download)', severity: 'critical' },
|
|
12
|
+
{ pattern: /eval.*\$/, name: 'Dynamic eval with variable', severity: 'critical' },
|
|
13
|
+
{ pattern: /exec.*\$/, name: 'Dynamic exec with variable', severity: 'critical' },
|
|
14
|
+
{ pattern: /\.openclaw.*credentials/i, name: 'Credential file access pattern', severity: 'warning' },
|
|
15
|
+
{ pattern: /token.*exfil/i, name: 'Token exfiltration pattern', severity: 'critical' },
|
|
16
|
+
{ pattern: /keychain/i, name: 'Keychain access (macOS credential theft)', severity: 'critical' },
|
|
17
|
+
{ pattern: /\$HOME\/\.ssh/, name: 'SSH key access', severity: 'critical' },
|
|
18
|
+
{ pattern: /\.env.*cat/i, name: 'Environment file reading', severity: 'warning' },
|
|
19
|
+
{ pattern: /AMOS|Atomic.*macOS.*Stealer/i, name: 'AMOS stealer reference', severity: 'critical' },
|
|
20
|
+
{ pattern: /openclaw.*windriver/i, name: 'Fake Windows installer reference', severity: 'critical' }
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const SUSPICIOUS_DOMAINS = [
|
|
24
|
+
'91.92.242.30',
|
|
25
|
+
'bore.pub',
|
|
26
|
+
'localhost.run',
|
|
27
|
+
'npx.run',
|
|
28
|
+
'curl.se',
|
|
29
|
+
'bit.ly'
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const TRUSTED_SKILL_AUTHORS = [
|
|
33
|
+
'openclaw',
|
|
34
|
+
'clawdbot',
|
|
35
|
+
'steipete',
|
|
36
|
+
'official'
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
function getWorkspacePath() {
|
|
40
|
+
const home = process.env.USERPROFILE || process.env.HOME || process.env.HOMEPATH;
|
|
41
|
+
return path.join(home, '.openclaw', 'workspace', 'skills');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getInstalledSkills(workspacePath) {
|
|
45
|
+
const skills = [];
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
if (!fs.existsSync(workspacePath)) {
|
|
49
|
+
out.warning(`Skills directory not found: ${workspacePath}`);
|
|
50
|
+
return skills;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const entries = fs.readdirSync(workspacePath, { withFileTypes: true });
|
|
54
|
+
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
const skillPath = path.join(workspacePath, entry.name);
|
|
58
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
59
|
+
|
|
60
|
+
if (fs.existsSync(skillMdPath)) {
|
|
61
|
+
const content = fs.readFileSync(skillMdPath, 'utf8');
|
|
62
|
+
skills.push({
|
|
63
|
+
name: entry.name,
|
|
64
|
+
path: skillPath,
|
|
65
|
+
content: content,
|
|
66
|
+
size: fs.statSync(skillMdPath).size
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const metaPath = path.join(skillPath, 'skill.json');
|
|
71
|
+
if (fs.existsSync(metaPath)) {
|
|
72
|
+
try {
|
|
73
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
74
|
+
skills[skills.length - 1].meta = meta;
|
|
75
|
+
} catch (e) {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
out.error(`Error reading skills: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return skills;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function analyzeSkill(skill) {
|
|
87
|
+
const findings = [];
|
|
88
|
+
const content = skill.content;
|
|
89
|
+
|
|
90
|
+
for (const { pattern, name, severity } of MALICIOUS_PATTERNS) {
|
|
91
|
+
if (pattern.test(content)) {
|
|
92
|
+
findings.push({ pattern: name, severity, matched: content.match(pattern)?.[0] });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const urlPattern = /https?:\/\/[^\s]+/gi;
|
|
97
|
+
const urls = content.match(urlPattern) || [];
|
|
98
|
+
const externalUrls = urls.filter(url => !url.includes('github.com') && !url.includes('npmjs.com') && !url.includes('clawhub.ai'));
|
|
99
|
+
|
|
100
|
+
for (const url of externalUrls) {
|
|
101
|
+
const isSuspicious = SUSPICIOUS_DOMAINS.some(domain => url.includes(domain));
|
|
102
|
+
if (isSuspicious) {
|
|
103
|
+
findings.push({ pattern: `External URL to suspicious domain`, severity: 'critical', matched: url });
|
|
104
|
+
} else {
|
|
105
|
+
findings.push({ pattern: `External URL`, severity: 'info', matched: url });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const base64Pattern = /[A-Za-z0-9+\/]{50,}={0,2}/;
|
|
110
|
+
if (base64Pattern.test(content)) {
|
|
111
|
+
const matches = content.match(base64Pattern);
|
|
112
|
+
if (matches && matches.length > 3) {
|
|
113
|
+
findings.push({ pattern: 'Multiple base64 encoded strings', severity: 'warning', matched: `${matches.length} found` });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const shellPattern = /\$(exec|run|command|bash|sh)/i;
|
|
118
|
+
if (shellPattern.test(content)) {
|
|
119
|
+
findings.push({ pattern: 'Shell command execution', severity: 'warning', matched: 'Shell execution detected' });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
name: skill.name,
|
|
124
|
+
path: skill.path,
|
|
125
|
+
findings,
|
|
126
|
+
hasIssues: findings.some(f => f.severity === 'critical'),
|
|
127
|
+
hasWarnings: findings.some(f => f.severity === 'warning'),
|
|
128
|
+
externalUrls: externalUrls.length
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function check() {
|
|
133
|
+
out.heading('Skill Audit');
|
|
134
|
+
|
|
135
|
+
const workspacePath = getWorkspacePath();
|
|
136
|
+
out.keyValue('Skills Path', workspacePath);
|
|
137
|
+
out.text('');
|
|
138
|
+
|
|
139
|
+
const skills = getInstalledSkills(workspacePath);
|
|
140
|
+
|
|
141
|
+
if (skills.length === 0) {
|
|
142
|
+
out.info('No skills found in workspace');
|
|
143
|
+
out.text('Install skills with: clawhub install <skill-name>');
|
|
144
|
+
return { skills: [], criticalCount: 0, warningCount: 0 };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
out.keyValue('Installed Skills', skills.length.toString());
|
|
148
|
+
out.text('');
|
|
149
|
+
|
|
150
|
+
const results = skills.map(skill => analyzeSkill(skill));
|
|
151
|
+
|
|
152
|
+
let criticalCount = 0;
|
|
153
|
+
let warningCount = 0;
|
|
154
|
+
let safeCount = 0;
|
|
155
|
+
|
|
156
|
+
for (const result of results) {
|
|
157
|
+
if (result.hasIssues) {
|
|
158
|
+
criticalCount++;
|
|
159
|
+
console.log(out.COLORS.critical(`\n🔴 ${result.name} — ISSUES DETECTED`));
|
|
160
|
+
console.log(out.COLORS.dim(` Path: ${result.path}`));
|
|
161
|
+
|
|
162
|
+
for (const finding of result.findings) {
|
|
163
|
+
if (finding.severity === 'critical') {
|
|
164
|
+
console.log(` ${out.COLORS.error('●')} ${finding.pattern}`);
|
|
165
|
+
if (finding.matched) {
|
|
166
|
+
console.log(` ${out.COLORS.dim(finding.matched.substring(0, 80))}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} else if (result.hasWarnings) {
|
|
171
|
+
warningCount++;
|
|
172
|
+
console.log(out.COLORS.warning(`\n🟡 ${result.name} — WARNINGS`));
|
|
173
|
+
|
|
174
|
+
for (const finding of result.findings) {
|
|
175
|
+
if (finding.severity === 'warning') {
|
|
176
|
+
console.log(` ${out.COLORS.warning('⚠')} ${finding.pattern}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
safeCount++;
|
|
181
|
+
console.log(`${out.COLORS.passed('🟢')} ${result.name}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
out.text('');
|
|
186
|
+
out.divider();
|
|
187
|
+
out.text(`Summary: ${out.COLORS.passed(safeCount)} safe | ${out.COLORS.warning(warningCount)} warnings | ${out.COLORS.critical(criticalCount)} critical`);
|
|
188
|
+
|
|
189
|
+
if (criticalCount > 0) {
|
|
190
|
+
out.text('');
|
|
191
|
+
out.critical('URGENT: Remove malicious skills immediately!');
|
|
192
|
+
out.text('Run: clawhub uninstall <skill-name> to remove');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
skills: results,
|
|
197
|
+
criticalCount,
|
|
198
|
+
warningCount,
|
|
199
|
+
safeCount,
|
|
200
|
+
total: skills.length
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = { check, getInstalledSkills, analyzeSkill, MALICIOUS_PATTERNS };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const fetch = require('node-fetch');
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
const out = require('../utils/output');
|
|
4
|
+
|
|
5
|
+
const OPENCLAW_REPO = 'openclaw/openclaw';
|
|
6
|
+
|
|
7
|
+
async function getLatestVersion() {
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetch(`https://api.github.com/repos/${OPENCLAW_REPO}/releases/latest`, {
|
|
10
|
+
headers: { 'User-Agent': 'ClawShield' }
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const data = await response.json();
|
|
18
|
+
const tagName = data.tag_name;
|
|
19
|
+
|
|
20
|
+
const versionMatch = tagName.match(/v?(\d+\.\d+\.\d+)/);
|
|
21
|
+
if (versionMatch) {
|
|
22
|
+
return versionMatch[1];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return tagName.replace('v', '');
|
|
26
|
+
} catch (error) {
|
|
27
|
+
out.error(`Failed to fetch latest version: ${error.message}`);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getInstalledVersion() {
|
|
33
|
+
const commands = [
|
|
34
|
+
['openclaw --version', null],
|
|
35
|
+
['npx openclaw --version', null],
|
|
36
|
+
['which openclaw && openclaw --version', null],
|
|
37
|
+
['wsl -d Ubuntu-22.04 -- bash -c "openclaw --version"', null],
|
|
38
|
+
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const [cmd, shell] of commands) {
|
|
42
|
+
try {
|
|
43
|
+
const output = execSync(cmd, {
|
|
44
|
+
encoding: 'utf8',
|
|
45
|
+
timeout: 10000,
|
|
46
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (output && output.trim()) {
|
|
50
|
+
const match = output.match(/v?(\d+\.\d+\.\d+)/);
|
|
51
|
+
if (match) {
|
|
52
|
+
return match[1];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const yearMonthDay = output.match(/(\d{4})\.(\d+)\.(\d+)/);
|
|
56
|
+
if (yearMonthDay) {
|
|
57
|
+
return `${yearMonthDay[1]}.${yearMonthDay[2]}.${yearMonthDay[3]}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} catch (e) {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function compareVersions(installed, latest) {
|
|
67
|
+
if (!installed || !latest) return 0;
|
|
68
|
+
|
|
69
|
+
const iParts = installed.split('.').map(Number);
|
|
70
|
+
const lParts = latest.split('.').map(Number);
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < 3; i++) {
|
|
73
|
+
if ((iParts[i] || 0) < (lParts[i] || 0)) return -1;
|
|
74
|
+
if ((iParts[i] || 0) > (lParts[i] || 0)) return 1;
|
|
75
|
+
}
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function check() {
|
|
80
|
+
out.heading('OpenClaw Version Check');
|
|
81
|
+
|
|
82
|
+
const installed = getInstalledVersion();
|
|
83
|
+
const latest = await getLatestVersion();
|
|
84
|
+
|
|
85
|
+
if (!installed) {
|
|
86
|
+
out.warning('OpenClaw not found or version could not be determined');
|
|
87
|
+
out.text('Make sure OpenClaw is installed and accessible via the openclaw command');
|
|
88
|
+
return { status: 'unknown', installed, latest, outdated: false };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
out.keyValue('Installed Version', installed);
|
|
92
|
+
out.keyValue('Latest Version', latest || 'Unknown');
|
|
93
|
+
|
|
94
|
+
if (!latest) {
|
|
95
|
+
out.warning('Could not determine latest version from GitHub');
|
|
96
|
+
return { status: 'unknown', installed, latest: 'unknown', outdated: false };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const comparison = compareVersions(installed, latest);
|
|
100
|
+
|
|
101
|
+
if (comparison < 0) {
|
|
102
|
+
out.critical(`OpenClaw is OUTDATED! You are ${installed} but latest is ${latest}`);
|
|
103
|
+
out.text('Run: openclaw upgrade or npm update -g openclaw');
|
|
104
|
+
return { status: 'critical', installed, latest, outdated: true };
|
|
105
|
+
} else if (comparison === 0) {
|
|
106
|
+
out.passed('OpenClaw is up to date!');
|
|
107
|
+
return { status: 'passed', installed, latest, outdated: false };
|
|
108
|
+
} else {
|
|
109
|
+
out.warning(`Installed version (${installed}) is newer than latest known (${latest})`);
|
|
110
|
+
return { status: 'warning', installed, latest, outdated: false };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { check, getInstalledVersion, getLatestVersion };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const out = require('../utils/output');
|
|
2
|
+
const skillAudit = require('../checks/skillAudit');
|
|
3
|
+
const secretsCheck = require('../checks/secretsCheck');
|
|
4
|
+
|
|
5
|
+
function audit() {
|
|
6
|
+
out.banner();
|
|
7
|
+
|
|
8
|
+
console.log(out.COLORS.title('║ SKILL & SECRETS AUDIT ║'));
|
|
9
|
+
console.log(out.COLORS.title('╚══════════════════════════════════════════════════╝'));
|
|
10
|
+
console.log('');
|
|
11
|
+
|
|
12
|
+
console.log('This command audits installed skills and checks for exposed secrets.\n');
|
|
13
|
+
|
|
14
|
+
const skillResults = skillAudit.check();
|
|
15
|
+
|
|
16
|
+
console.log('');
|
|
17
|
+
out.divider();
|
|
18
|
+
|
|
19
|
+
const secretsResults = secretsCheck.check();
|
|
20
|
+
|
|
21
|
+
console.log('');
|
|
22
|
+
out.divider();
|
|
23
|
+
|
|
24
|
+
console.log('');
|
|
25
|
+
console.log(out.COLORS.title('╔══════════════════════════════════════════════════╗'));
|
|
26
|
+
console.log(out.COLORS.title('║ AUDIT SUMMARY ║'));
|
|
27
|
+
console.log(out.COLORS.title('╠══════════════════════════════════════════════════╣'));
|
|
28
|
+
|
|
29
|
+
const totalIssues = skillResults.criticalCount + skillResults.warningCount + secretsResults.findings.length;
|
|
30
|
+
|
|
31
|
+
console.log(out.COLORS.title('║') + ` Skills Audited: ${out.COLORS.info((skillResults.total || 0).toString())}${' '.repeat(27)}` + out.COLORS.title('║'));
|
|
32
|
+
console.log(out.COLORS.title('║') + ` Critical Issues: ${skillResults.criticalCount ? out.COLORS.error(skillResults.criticalCount.toString()) : out.COLORS.passed('0')}${' '.repeat(27)}` + out.COLORS.title('║'));
|
|
33
|
+
console.log(out.COLORS.title('║') + ` Warnings: ${out.COLORS.warning(skillResults.warningCount.toString())}${' '.repeat(28)}` + out.COLORS.title('║'));
|
|
34
|
+
console.log(out.COLORS.title('║') + ` Secrets Found: ${secretsResults.findings.length ? out.COLORS.error(secretsResults.findings.length.toString()) : out.COLORS.passed('0')}${' '.repeat(27)}` + out.COLORS.title('║'));
|
|
35
|
+
console.log(out.COLORS.title('║') + ` Files Scanned: ${out.COLORS.info(secretsResults.filesScanned.toString())}${' '.repeat(28)}` + out.COLORS.title('║'));
|
|
36
|
+
console.log(out.COLORS.title('╚══════════════════════════════════════════════════╝'));
|
|
37
|
+
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
if (totalIssues === 0) {
|
|
41
|
+
console.log(out.COLORS.success('✅ No issues found!'));
|
|
42
|
+
} else {
|
|
43
|
+
console.log(out.COLORS.warning(`Found ${totalIssues} issue(s). Review above for details.`));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log('');
|
|
47
|
+
console.log('Recommendations:');
|
|
48
|
+
if (skillResults.criticalCount > 0) {
|
|
49
|
+
console.log(' • Remove malicious skills immediately');
|
|
50
|
+
console.log(' • Run: clawhub uninstall <skill-name>');
|
|
51
|
+
}
|
|
52
|
+
if (secretsResults.findings.length > 0) {
|
|
53
|
+
console.log(' • Revoke and regenerate exposed API keys');
|
|
54
|
+
console.log(' • Use environment variables instead of config files');
|
|
55
|
+
}
|
|
56
|
+
console.log('');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { audit };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const out = require('../utils/output');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const version = require('../checks/version');
|
|
4
|
+
const cve = require('../checks/cve');
|
|
5
|
+
const configHarden = require('../checks/configHarden');
|
|
6
|
+
|
|
7
|
+
async function doctor() {
|
|
8
|
+
console.log('');
|
|
9
|
+
console.log(out.COLORS.title('╔══════════════════════════════════════════════════╗'));
|
|
10
|
+
console.log(out.COLORS.title('║ CLAWSHIELD QUICK DIAGNOSTIC ║'));
|
|
11
|
+
console.log(out.COLORS.title('╚══════════════════════════════════════════════════╝'));
|
|
12
|
+
console.log('');
|
|
13
|
+
|
|
14
|
+
const spinner = ora({ spinner: 'dots' }).start();
|
|
15
|
+
let allPassed = true;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
spinner.text = 'Checking OpenClaw installation...';
|
|
19
|
+
const versionResult = await version.check();
|
|
20
|
+
out.divider();
|
|
21
|
+
|
|
22
|
+
spinner.text = 'Checking for critical CVEs...';
|
|
23
|
+
const cveResult = await cve.check(versionResult.installed);
|
|
24
|
+
out.divider();
|
|
25
|
+
|
|
26
|
+
spinner.text = 'Checking basic security settings...';
|
|
27
|
+
const configPath = configHarden.getOpenClawConfigPath();
|
|
28
|
+
const config = configHarden.getConfigFromFile(configPath);
|
|
29
|
+
const configResult = configHarden.checkGatewayExposure(config);
|
|
30
|
+
out.divider();
|
|
31
|
+
|
|
32
|
+
spinner.succeed('Diagnostic complete!');
|
|
33
|
+
|
|
34
|
+
console.log('');
|
|
35
|
+
console.log(out.COLORS.title('╔══════════════════════════════════════════════════╗'));
|
|
36
|
+
console.log(out.COLORS.title('║ QUICK SUMMARY ║'));
|
|
37
|
+
console.log(out.COLORS.title('╚══════════════════════════════════════════════════╝'));
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
const statusItems = [
|
|
41
|
+
['Version', versionResult.installed || 'Unknown', versionResult.outdated ? 'fail' : 'pass'],
|
|
42
|
+
['CVEs', versionResult.outdated ? `${cveResult.count} vulnerabilities` : 'None', cveResult.vulnerable ? 'fail' : 'pass'],
|
|
43
|
+
['Gateway', configResult.exposed ? 'EXPOSED' : 'Protected', configResult.exposed ? 'fail' : 'pass'],
|
|
44
|
+
['Token', config?.gateway?.token ? 'Configured' : 'Missing', config?.gateway?.token ? 'pass' : 'fail']
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
out.generateTable(statusItems);
|
|
48
|
+
|
|
49
|
+
console.log('');
|
|
50
|
+
|
|
51
|
+
if (versionResult.outdated) {
|
|
52
|
+
out.warning('⚠ OpenClaw needs to be updated');
|
|
53
|
+
console.log(' Run: openclaw upgrade');
|
|
54
|
+
allPassed = false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (cveResult.vulnerable) {
|
|
58
|
+
out.critical('🚨 Critical vulnerabilities detected!');
|
|
59
|
+
allPassed = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (configResult.exposed) {
|
|
63
|
+
out.critical('🔴 Gateway is exposed to the network!');
|
|
64
|
+
allPassed = false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (allPassed) {
|
|
68
|
+
console.log(out.COLORS.success('✅ All checks passed!'));
|
|
69
|
+
console.log('');
|
|
70
|
+
console.log('Run a full scan with: clawshield scan');
|
|
71
|
+
} else {
|
|
72
|
+
console.log(out.COLORS.warning('⚠ Some issues detected.'));
|
|
73
|
+
console.log('');
|
|
74
|
+
console.log('Run a full scan for detailed analysis: clawshield scan');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
} catch (error) {
|
|
78
|
+
spinner.fail('Diagnostic failed: ' + error.message);
|
|
79
|
+
out.error('An error occurred. Make sure OpenClaw is installed.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { doctor };
|