@contextfort-ai/openclaw-secure 0.1.6 → 0.1.8
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/bin/openclaw-secure.js +169 -0
- package/monitor/analytics.js +9 -3
- package/monitor/dashboard/public/index.html +464 -0
- package/monitor/dashboard/server.js +122 -0
- package/monitor/local_logger.js +105 -0
- package/monitor/secrets_guard/.trufflehog-exclude +9 -0
- package/monitor/secrets_guard/index.js +849 -0
- package/monitor/skills_guard/index.js +12 -2
- package/openclaw-secure.js +111 -3
- package/package.json +1 -1
package/bin/openclaw-secure.js
CHANGED
|
@@ -81,6 +81,175 @@ if (args[0] === 'enable') {
|
|
|
81
81
|
process.exit(0);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
if (args[0] === 'dashboard') {
|
|
85
|
+
const portArg = args.find(a => a.startsWith('--port='));
|
|
86
|
+
const port = portArg ? parseInt(portArg.split('=')[1]) : 9009;
|
|
87
|
+
const startDashboard = require('../monitor/dashboard/server');
|
|
88
|
+
const server = startDashboard({ port });
|
|
89
|
+
|
|
90
|
+
// Auto-open browser
|
|
91
|
+
const openUrl = `http://localhost:${port}`;
|
|
92
|
+
try {
|
|
93
|
+
const { execSync } = require('child_process');
|
|
94
|
+
if (process.platform === 'darwin') execSync(`open "${openUrl}"`);
|
|
95
|
+
else if (process.platform === 'linux') execSync(`xdg-open "${openUrl}" 2>/dev/null`);
|
|
96
|
+
else if (process.platform === 'win32') execSync(`start "" "${openUrl}"`);
|
|
97
|
+
} catch {}
|
|
98
|
+
|
|
99
|
+
process.on('SIGINT', () => { server.close(); process.exit(0); });
|
|
100
|
+
// Keep alive — don't fall through
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (args[0] === 'scan' || args[0] === 'solve') {
|
|
105
|
+
const { spawnSync } = require('child_process');
|
|
106
|
+
const readline = require('readline');
|
|
107
|
+
const secretsGuard = require('../monitor/secrets_guard')({
|
|
108
|
+
spawnSync,
|
|
109
|
+
baseDir: packageDir,
|
|
110
|
+
analytics: null,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!secretsGuard.isTrufflehogInstalled()) {
|
|
114
|
+
console.error('\n trufflehog is not installed.\n');
|
|
115
|
+
console.error(' Install it with: brew install trufflehog');
|
|
116
|
+
console.error(' Or see: https://github.com/trufflesecurity/trufflehog#installation\n');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const onlyVerified = !args.includes('--all');
|
|
121
|
+
const cwd = args.find(a => !a.startsWith('-') && a !== 'scan' && a !== 'solve') || process.cwd();
|
|
122
|
+
|
|
123
|
+
console.log('\n Running TruffleHog secret scan...');
|
|
124
|
+
if (args[0] === 'scan' && onlyVerified) {
|
|
125
|
+
console.log(' Mode: verified secrets only (use --all to include unverified)\n');
|
|
126
|
+
} else if (args[0] === 'scan') {
|
|
127
|
+
console.log(' Mode: all findings (including unverified)\n');
|
|
128
|
+
} else {
|
|
129
|
+
console.log(' Mode: solve — scanning for live secrets to replace\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// For solve, always scan with onlyVerified=true (only replace live secrets)
|
|
133
|
+
const scanVerified = args[0] === 'solve' ? true : onlyVerified;
|
|
134
|
+
const result = secretsGuard.scan(cwd, { onlyVerified: scanVerified });
|
|
135
|
+
console.log(secretsGuard.formatResults(result));
|
|
136
|
+
|
|
137
|
+
if (args[0] === 'scan') {
|
|
138
|
+
process.exit(result.findings.filter(f => f.verified).length > 0 ? 1 : 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// === SOLVE mode ===
|
|
142
|
+
const verified = result.findings.filter(f => f.verified && f.rawFull && f.file);
|
|
143
|
+
if (verified.length === 0) {
|
|
144
|
+
console.log(' Nothing to solve — no live hardcoded secrets found.\n');
|
|
145
|
+
process.exit(0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Group findings by file
|
|
149
|
+
const fileMap = new Map(); // file → [findings]
|
|
150
|
+
for (const f of verified) {
|
|
151
|
+
if (!fileMap.has(f.file)) fileMap.set(f.file, []);
|
|
152
|
+
fileMap.get(f.file).push(f);
|
|
153
|
+
}
|
|
154
|
+
const fileList = [...fileMap.entries()]; // [[file, [findings]], ...]
|
|
155
|
+
|
|
156
|
+
console.log(` OpenClaw can read these ${fileList.length} file(s) containing ${verified.length} live secret(s).`);
|
|
157
|
+
console.log(' If not replaced, OpenClaw could read and leak them.\n');
|
|
158
|
+
console.log(' Use \u2191\u2193 to move, SPACE to select, A to toggle all, ENTER to confirm, Q to quit.\n');
|
|
159
|
+
|
|
160
|
+
// Build items for checkbox UI
|
|
161
|
+
const items = fileList.map(([filePath, findings]) => {
|
|
162
|
+
const types = [...new Set(findings.map(f => f.detectorName))].join(', ');
|
|
163
|
+
const count = findings.length;
|
|
164
|
+
return {
|
|
165
|
+
label: `${filePath.replace(os.homedir(), '~')} (${count} ${types})`,
|
|
166
|
+
selected: false,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
let cursor = 0;
|
|
171
|
+
|
|
172
|
+
function render() {
|
|
173
|
+
// Move cursor up to overwrite previous render
|
|
174
|
+
if (items._rendered) {
|
|
175
|
+
process.stdout.write(`\x1b[${items.length + 1}A`);
|
|
176
|
+
}
|
|
177
|
+
for (let i = 0; i < items.length; i++) {
|
|
178
|
+
const check = items[i].selected ? '\x1b[32m\u25c9\x1b[0m' : '\u25cb';
|
|
179
|
+
const pointer = i === cursor ? '\x1b[36m\u276f\x1b[0m ' : ' ';
|
|
180
|
+
const dim = i === cursor ? '' : '\x1b[2m';
|
|
181
|
+
const reset = i === cursor ? '' : '\x1b[0m';
|
|
182
|
+
process.stdout.write(`\x1b[2K ${pointer}${check} ${dim}${items[i].label}${reset}\n`);
|
|
183
|
+
}
|
|
184
|
+
const selectedCount = items.filter(it => it.selected).length;
|
|
185
|
+
process.stdout.write(`\x1b[2K \x1b[2m${selectedCount}/${items.length} selected\x1b[0m\n`);
|
|
186
|
+
items._rendered = true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
render();
|
|
190
|
+
|
|
191
|
+
const stdin = process.stdin;
|
|
192
|
+
stdin.setRawMode(true);
|
|
193
|
+
stdin.resume();
|
|
194
|
+
stdin.setEncoding('utf8');
|
|
195
|
+
|
|
196
|
+
stdin.on('data', (key) => {
|
|
197
|
+
if (key === 'q' || key === '\x03') {
|
|
198
|
+
// q or Ctrl+C
|
|
199
|
+
stdin.setRawMode(false);
|
|
200
|
+
console.log('\n\n Aborted. No changes made.\n');
|
|
201
|
+
process.exit(0);
|
|
202
|
+
}
|
|
203
|
+
if (key === '\r' || key === '\n') {
|
|
204
|
+
// Enter — confirm
|
|
205
|
+
stdin.setRawMode(false);
|
|
206
|
+
stdin.pause();
|
|
207
|
+
const selectedFindings = [];
|
|
208
|
+
for (let i = 0; i < items.length; i++) {
|
|
209
|
+
if (items[i].selected) {
|
|
210
|
+
selectedFindings.push(...fileList[i][1]);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (selectedFindings.length === 0) {
|
|
214
|
+
console.log('\n\n No files selected. Aborted.\n');
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
const selectedFiles = items.filter(it => it.selected).length;
|
|
218
|
+
console.log(`\n\n Replacing secrets in ${selectedFiles} file(s)...`);
|
|
219
|
+
const results = secretsGuard.solve(selectedFindings);
|
|
220
|
+
console.log(secretsGuard.formatSolveResults(results));
|
|
221
|
+
process.exit(0);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (key === ' ') {
|
|
225
|
+
// Space — toggle current item
|
|
226
|
+
items[cursor].selected = !items[cursor].selected;
|
|
227
|
+
render();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (key === 'a' || key === 'A') {
|
|
231
|
+
// A — toggle all
|
|
232
|
+
const allSelected = items.every(it => it.selected);
|
|
233
|
+
for (const it of items) it.selected = !allSelected;
|
|
234
|
+
render();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (key === '\x1b[A' || key === 'k') {
|
|
238
|
+
// Up arrow or k
|
|
239
|
+
cursor = (cursor - 1 + items.length) % items.length;
|
|
240
|
+
render();
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (key === '\x1b[B' || key === 'j') {
|
|
244
|
+
// Down arrow or j
|
|
245
|
+
cursor = (cursor + 1) % items.length;
|
|
246
|
+
render();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
return; // don't fall through — raw mode is async
|
|
251
|
+
}
|
|
252
|
+
|
|
84
253
|
if (args[0] === 'disable') {
|
|
85
254
|
try {
|
|
86
255
|
const original = fs.readFileSync(backupLink, 'utf8').trim();
|
package/monitor/analytics.js
CHANGED
|
@@ -9,7 +9,7 @@ const ANALYTICS_DISABLED = ['1', 'true', 'yes'].includes(
|
|
|
9
9
|
(process.env.CONTEXTFORT_NO_ANALYTICS || '').toLowerCase()
|
|
10
10
|
);
|
|
11
11
|
|
|
12
|
-
module.exports = function createAnalytics({ httpsRequest, readFileSync, baseDir }) {
|
|
12
|
+
module.exports = function createAnalytics({ httpsRequest, readFileSync, baseDir, localLogger }) {
|
|
13
13
|
if (ANALYTICS_DISABLED || !httpsRequest) {
|
|
14
14
|
return { track() {} };
|
|
15
15
|
}
|
|
@@ -27,7 +27,7 @@ module.exports = function createAnalytics({ httpsRequest, readFileSync, baseDir
|
|
|
27
27
|
|
|
28
28
|
function track(event, properties) {
|
|
29
29
|
if (!installId) return;
|
|
30
|
-
const
|
|
30
|
+
const payload = {
|
|
31
31
|
api_key: POSTHOG_API_KEY,
|
|
32
32
|
event,
|
|
33
33
|
properties: {
|
|
@@ -35,7 +35,13 @@ module.exports = function createAnalytics({ httpsRequest, readFileSync, baseDir
|
|
|
35
35
|
...properties,
|
|
36
36
|
},
|
|
37
37
|
timestamp: new Date().toISOString(),
|
|
38
|
-
}
|
|
38
|
+
};
|
|
39
|
+
const body = JSON.stringify(payload);
|
|
40
|
+
|
|
41
|
+
// Log what we're sending to PostHog
|
|
42
|
+
if (localLogger) {
|
|
43
|
+
try { localLogger.logServerSend({ destination: 'posthog', event, properties: properties || {} }); } catch {}
|
|
44
|
+
}
|
|
39
45
|
|
|
40
46
|
try {
|
|
41
47
|
const req = httpsRequest({
|