@goplus/agentguard 1.1.7 → 1.1.9
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 +12 -8
- package/dist/adapters/openclaw-plugin.d.ts +14 -7
- package/dist/adapters/openclaw-plugin.d.ts.map +1 -1
- package/dist/adapters/openclaw-plugin.js +43 -8
- package/dist/adapters/openclaw-plugin.js.map +1 -1
- package/dist/cli.js +469 -51
- package/dist/cli.js.map +1 -1
- package/dist/cloud/client.d.ts +11 -3
- package/dist/cloud/client.d.ts.map +1 -1
- package/dist/cloud/client.js +52 -14
- package/dist/cloud/client.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/feed/cron.d.ts +6 -2
- package/dist/feed/cron.d.ts.map +1 -1
- package/dist/feed/cron.js +32 -15
- package/dist/feed/cron.js.map +1 -1
- package/dist/feed/selfcheck.d.ts +10 -1
- package/dist/feed/selfcheck.d.ts.map +1 -1
- package/dist/feed/selfcheck.js +220 -37
- package/dist/feed/selfcheck.js.map +1 -1
- package/dist/feed/types.d.ts +12 -2
- package/dist/feed/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/installers.js +28 -5
- package/dist/installers.js.map +1 -1
- package/dist/runtime/protect.d.ts +2 -2
- package/dist/runtime/protect.d.ts.map +1 -1
- package/dist/runtime/protect.js +50 -8
- package/dist/runtime/protect.js.map +1 -1
- package/dist/tests/cli-checkup.test.d.ts +2 -0
- package/dist/tests/cli-checkup.test.d.ts.map +1 -0
- package/dist/tests/cli-checkup.test.js +63 -0
- package/dist/tests/cli-checkup.test.js.map +1 -0
- package/dist/tests/cli-subscribe.test.d.ts +2 -0
- package/dist/tests/cli-subscribe.test.d.ts.map +1 -0
- package/dist/tests/cli-subscribe.test.js +123 -0
- package/dist/tests/cli-subscribe.test.js.map +1 -0
- package/dist/tests/cloud-live.test.js +0 -17
- package/dist/tests/cloud-live.test.js.map +1 -1
- package/dist/tests/feed-cloud.test.js +57 -2
- package/dist/tests/feed-cloud.test.js.map +1 -1
- package/dist/tests/feed-cron.test.js +28 -13
- package/dist/tests/feed-cron.test.js.map +1 -1
- package/dist/tests/feed-selfcheck.test.js +65 -3
- package/dist/tests/feed-selfcheck.test.js.map +1 -1
- package/dist/tests/feed-state.test.d.ts +2 -0
- package/dist/tests/feed-state.test.d.ts.map +1 -0
- package/dist/tests/feed-state.test.js +40 -0
- package/dist/tests/feed-state.test.js.map +1 -0
- package/dist/tests/installer.test.js +7 -2
- package/dist/tests/installer.test.js.map +1 -1
- package/dist/tests/integration.test.js +57 -3
- package/dist/tests/integration.test.js.map +1 -1
- package/dist/tests/runtime-cloud.test.js +59 -14
- package/dist/tests/runtime-cloud.test.js.map +1 -1
- package/package.json +5 -1
- package/skills/agentguard/SKILL.md +26 -15
package/dist/cli.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_path_1 = require("node:path");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
5
8
|
const commander_1 = require("commander");
|
|
6
9
|
const client_js_1 = require("./cloud/client.js");
|
|
7
10
|
const config_js_1 = require("./config.js");
|
|
@@ -77,6 +80,15 @@ async function main() {
|
|
|
77
80
|
console.log(`Policy fetch failed; local protection still works offline. ${error instanceof Error ? error.message : ''}`.trim());
|
|
78
81
|
}
|
|
79
82
|
});
|
|
83
|
+
program
|
|
84
|
+
.command('disconnect')
|
|
85
|
+
.description('Disconnect local AgentGuard from AgentGuard Cloud')
|
|
86
|
+
.action(() => {
|
|
87
|
+
const config = (0, config_js_1.disconnectCloud)();
|
|
88
|
+
console.log('Disconnected from AgentGuard Cloud.');
|
|
89
|
+
console.log('Removed local Cloud API key, connection timestamp, pending event spool, and cached Cloud policy.');
|
|
90
|
+
console.log(`Local protection remains active using the built-in policy. Audit log: ${config.auditPath}`);
|
|
91
|
+
});
|
|
80
92
|
program
|
|
81
93
|
.command('status')
|
|
82
94
|
.description('Show local and Cloud connection status')
|
|
@@ -151,7 +163,8 @@ async function main() {
|
|
|
151
163
|
const client = new client_js_1.AgentGuardCloudClient(config);
|
|
152
164
|
try {
|
|
153
165
|
const status = await client.status();
|
|
154
|
-
|
|
166
|
+
const label = status.status || (status.ok ? 'ok' : status.service || 'reachable');
|
|
167
|
+
console.log(`✓ Cloud: ${label}${status.version ? ` (${status.version})` : ''}`);
|
|
155
168
|
}
|
|
156
169
|
catch {
|
|
157
170
|
console.log('! Cloud: unreachable; local protection remains active');
|
|
@@ -202,17 +215,17 @@ async function main() {
|
|
|
202
215
|
if (!result)
|
|
203
216
|
return;
|
|
204
217
|
console.log((0, protect_js_1.formatProtectResult)(result, Boolean(options.json)));
|
|
205
|
-
process.exitCode = (0, protect_js_1.exitCodeForDecision)(result.decision);
|
|
218
|
+
process.exitCode = (0, protect_js_1.exitCodeForDecision)(result.decision, result);
|
|
206
219
|
});
|
|
207
220
|
program
|
|
208
221
|
.command('subscribe')
|
|
209
222
|
.description('Pull new threat-feed advisories from AgentGuard Cloud and run a self-check against locally installed skills')
|
|
210
223
|
.option('--since <iso>', 'Override the persisted last-pulled timestamp')
|
|
211
224
|
.option('--json', 'Emit machine-readable summary instead of human text')
|
|
225
|
+
.option('--quiet', 'Run the full pull, self-check, and match-reporting flow with minimal output')
|
|
212
226
|
.option('--no-report', 'Skip uploading self-check results back to Cloud')
|
|
213
|
-
.option('--
|
|
227
|
+
.option('--cron <expr>', 'Install an OpenClaw cron job with a five-field cron expression, for example "0 * * * *"')
|
|
214
228
|
.option('--cron-name <name>', 'OpenClaw cron job name', 'agentguard-threat-feed')
|
|
215
|
-
.option('--interval-minutes <minutes>', 'OpenClaw cron interval in minutes', '15')
|
|
216
229
|
.option('--force', 'Replace an existing OpenClaw cron job with the same name')
|
|
217
230
|
.option('--cron-run', 'Internal: run from the OpenClaw cron prompt without trying to install cron again')
|
|
218
231
|
.action(async (options) => {
|
|
@@ -220,6 +233,10 @@ async function main() {
|
|
|
220
233
|
const client = new client_js_1.AgentGuardCloudClient(config);
|
|
221
234
|
const state = (0, state_js_1.loadFeedState)();
|
|
222
235
|
const since = options.since ?? state.lastPulledAt;
|
|
236
|
+
const quiet = Boolean(options.quiet);
|
|
237
|
+
const cronExpression = options.cron && !options.cronRun
|
|
238
|
+
? (0, cron_js_1.validateCronExpression)(options.cron)
|
|
239
|
+
: undefined;
|
|
223
240
|
let advisories;
|
|
224
241
|
try {
|
|
225
242
|
advisories = await client.pullAdvisories(since);
|
|
@@ -234,7 +251,7 @@ async function main() {
|
|
|
234
251
|
if (options.json) {
|
|
235
252
|
console.log(JSON.stringify({ supported: false, shouldNotify: false, results: [], cron: { requested: false, installed: false } }));
|
|
236
253
|
}
|
|
237
|
-
else {
|
|
254
|
+
else if (!quiet) {
|
|
238
255
|
console.log('AgentGuard Cloud does not expose /api/v1/feed/advisories yet — nothing to do.');
|
|
239
256
|
}
|
|
240
257
|
return;
|
|
@@ -249,48 +266,58 @@ async function main() {
|
|
|
249
266
|
let cursorOk = true; // stops advancing on the first hard failure
|
|
250
267
|
let latestPublishedAt = state.lastPulledAt;
|
|
251
268
|
let hardFailures = 0;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
result = await (0, selfcheck_js_1.runSelfCheckForAdvisory)(advisory);
|
|
257
|
-
}
|
|
258
|
-
catch (err) {
|
|
259
|
-
// runSelfCheck shouldn't throw, but if it does the advisory has
|
|
260
|
-
// not been evaluated — don't mark it seen and don't advance.
|
|
261
|
-
console.error(`! Self-check threw for ${advisory.id}: ${err.message}`);
|
|
262
|
-
hardFailures += 1;
|
|
263
|
-
cursorOk = false;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
results.push(result);
|
|
267
|
-
if (options.report !== false && client.connected && result.matchedArtifacts.length > 0) {
|
|
268
|
-
// Report is on the critical path — if Cloud doesn't see the
|
|
269
|
-
// match, we must NOT mark the advisory seen, otherwise a
|
|
270
|
-
// transient network blip silently buries a real hit.
|
|
269
|
+
if (quiet) {
|
|
270
|
+
for (const advisory of fresh) {
|
|
271
|
+
let processed = true;
|
|
272
|
+
let result;
|
|
271
273
|
try {
|
|
272
|
-
await
|
|
273
|
-
elapsedMs: result.elapsedMs,
|
|
274
|
-
warnings: result.warnings,
|
|
275
|
-
});
|
|
274
|
+
result = await (0, selfcheck_js_1.runSelfCheckForAdvisory)(advisory);
|
|
276
275
|
}
|
|
277
276
|
catch (err) {
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
// runSelfCheck shouldn't throw, but if it does the advisory has
|
|
278
|
+
// not been evaluated — don't mark it seen and don't advance.
|
|
279
|
+
console.error(`! Self-check threw for ${advisory.id}: ${err.message}`);
|
|
280
280
|
hardFailures += 1;
|
|
281
|
+
cursorOk = false;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
results.push(result);
|
|
285
|
+
if (options.report !== false && client.connected && result.matchedArtifacts.length > 0) {
|
|
286
|
+
// Report is on the critical path — if Cloud doesn't see the
|
|
287
|
+
// match, we must NOT mark the advisory seen, otherwise a
|
|
288
|
+
// transient network blip silently buries a real hit.
|
|
289
|
+
try {
|
|
290
|
+
await client.reportSelfCheck(advisory.id, result.matchedArtifacts, {
|
|
291
|
+
elapsedMs: result.elapsedMs,
|
|
292
|
+
warnings: result.warnings,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
console.error(`! Failed to report self-check for ${advisory.id}: ${err.message}`);
|
|
297
|
+
processed = false;
|
|
298
|
+
hardFailures += 1;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (processed) {
|
|
302
|
+
Object.assign(state, (0, state_js_1.markAdvisorySeen)(state, advisory.id));
|
|
303
|
+
if (cursorOk && (!latestPublishedAt || advisory.publishedAt > latestPublishedAt)) {
|
|
304
|
+
latestPublishedAt = advisory.publishedAt;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// From this point we no longer advance the pull cursor — the
|
|
309
|
+
// failed advisory must be re-pulled on the next run.
|
|
310
|
+
cursorOk = false;
|
|
281
311
|
}
|
|
282
312
|
}
|
|
283
|
-
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
for (const advisory of fresh) {
|
|
284
316
|
Object.assign(state, (0, state_js_1.markAdvisorySeen)(state, advisory.id));
|
|
285
317
|
if (cursorOk && (!latestPublishedAt || advisory.publishedAt > latestPublishedAt)) {
|
|
286
318
|
latestPublishedAt = advisory.publishedAt;
|
|
287
319
|
}
|
|
288
320
|
}
|
|
289
|
-
else {
|
|
290
|
-
// From this point we no longer advance the pull cursor — the
|
|
291
|
-
// failed advisory must be re-pulled on the next run.
|
|
292
|
-
cursorOk = false;
|
|
293
|
-
}
|
|
294
321
|
}
|
|
295
322
|
state.lastPulledAt = latestPublishedAt;
|
|
296
323
|
(0, state_js_1.saveFeedState)(state);
|
|
@@ -299,15 +326,18 @@ async function main() {
|
|
|
299
326
|
supported: true,
|
|
300
327
|
pulled: advisories.length,
|
|
301
328
|
fresh: fresh.length,
|
|
329
|
+
freshAdvisories: fresh,
|
|
302
330
|
results,
|
|
303
331
|
hardFailures,
|
|
332
|
+
quiet,
|
|
304
333
|
});
|
|
305
|
-
if (options.
|
|
334
|
+
if (options.cron && !options.cronRun) {
|
|
306
335
|
summary.cron.requested = true;
|
|
307
336
|
try {
|
|
308
337
|
summary.cron.result = await (0, cron_js_1.installOpenClawThreatFeedCron)({
|
|
309
338
|
name: options.cronName,
|
|
310
|
-
|
|
339
|
+
cronExpression: cronExpression,
|
|
340
|
+
quiet,
|
|
311
341
|
force: Boolean(options.force),
|
|
312
342
|
});
|
|
313
343
|
summary.cron.installed = true;
|
|
@@ -321,8 +351,18 @@ async function main() {
|
|
|
321
351
|
console.log(JSON.stringify(summary, null, 2));
|
|
322
352
|
return;
|
|
323
353
|
}
|
|
354
|
+
if (quiet && fresh.length === 0 && !summary.cron.result) {
|
|
355
|
+
process.exitCode = 0;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
324
358
|
console.log(`Pulled ${advisories.length} advisory record(s); ${fresh.length} new.`);
|
|
325
|
-
if (fresh.length > 0) {
|
|
359
|
+
if (!quiet && fresh.length > 0) {
|
|
360
|
+
console.log('New threat-feed advisories found. Review and handle them manually:');
|
|
361
|
+
for (const advisory of fresh) {
|
|
362
|
+
console.log(` - ${advisory.id} [${advisory.severity}] ${advisory.summary}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
else if (quiet && fresh.length > 0) {
|
|
326
366
|
console.log(`Self-check found ${totalMatches} match(es) across the new advisories.`);
|
|
327
367
|
for (const r of results) {
|
|
328
368
|
if (r.matchedArtifacts.length === 0)
|
|
@@ -335,8 +375,8 @@ async function main() {
|
|
|
335
375
|
}
|
|
336
376
|
if (summary.cron.result) {
|
|
337
377
|
const action = summary.cron.result.created ? 'Installed' : 'OpenClaw cron job already exists';
|
|
338
|
-
console.log(`${action} "${summary.cron.result.name}" (${summary.cron.result.schedule}).`);
|
|
339
|
-
console.log('Notification rule:
|
|
378
|
+
console.log(`${action} "${summary.cron.result.name}" (${summary.cron.result.schedule}, ${summary.cron.result.timezone}).`);
|
|
379
|
+
console.log('Notification rule: non-quiet cron notifies on new advisories; quiet cron notifies on local matches.');
|
|
340
380
|
}
|
|
341
381
|
// Exit codes: 2 = matches found, 1 = at least one advisory failed
|
|
342
382
|
// to evaluate or report (cursor was held back), 0 = clean.
|
|
@@ -344,7 +384,7 @@ async function main() {
|
|
|
344
384
|
console.error(`! ${hardFailures} advisory record(s) failed to process and will be re-pulled next run.`);
|
|
345
385
|
process.exitCode = 1;
|
|
346
386
|
}
|
|
347
|
-
else if (totalMatches > 0) {
|
|
387
|
+
else if (quiet && totalMatches > 0) {
|
|
348
388
|
process.exitCode = 2;
|
|
349
389
|
}
|
|
350
390
|
else {
|
|
@@ -353,22 +393,43 @@ async function main() {
|
|
|
353
393
|
});
|
|
354
394
|
program
|
|
355
395
|
.command('checkup')
|
|
356
|
-
.description('Run a
|
|
396
|
+
.description('Run a local agent health checkup. Use --against-advisory only for targeted threat-feed self-checks.')
|
|
357
397
|
.option('--against-advisory <id>', 'Restrict the check to a single advisory id (fetches it from Cloud if needed)')
|
|
358
398
|
.option('--json', 'Emit machine-readable result')
|
|
359
399
|
.action(async (options) => {
|
|
360
400
|
const config = (0, config_js_1.ensureConfig)();
|
|
361
|
-
const client = new client_js_1.AgentGuardCloudClient(config);
|
|
362
401
|
const advisoryId = options.againstAdvisory;
|
|
363
402
|
if (!advisoryId) {
|
|
364
|
-
|
|
365
|
-
|
|
403
|
+
const report = await runLocalHealthCheckup(config);
|
|
404
|
+
if (options.json) {
|
|
405
|
+
console.log(JSON.stringify(report, null, 2));
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
const htmlPath = await generateCheckupHtml(report).catch((err) => {
|
|
409
|
+
console.error(`! Could not generate visual checkup report: ${err.message}`);
|
|
410
|
+
return null;
|
|
411
|
+
});
|
|
412
|
+
printHealthCheckupSummary(report, htmlPath);
|
|
413
|
+
}
|
|
414
|
+
appendCheckupAudit(config.auditPath, report);
|
|
415
|
+
process.exitCode = 0;
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const client = new client_js_1.AgentGuardCloudClient(config);
|
|
419
|
+
if (!client.connected) {
|
|
420
|
+
const message = 'AgentGuard Cloud is not connected. Run `agentguard connect --key <key>` first.';
|
|
421
|
+
if (options.json) {
|
|
422
|
+
console.log(JSON.stringify({ success: false, error: message }, null, 2));
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
console.error(message);
|
|
426
|
+
}
|
|
427
|
+
process.exitCode = 1;
|
|
366
428
|
return;
|
|
367
429
|
}
|
|
368
430
|
let advisory = null;
|
|
369
431
|
try {
|
|
370
|
-
|
|
371
|
-
advisory = all?.find((a) => a.id === advisoryId) ?? null;
|
|
432
|
+
advisory = await client.getAdvisory(advisoryId);
|
|
372
433
|
}
|
|
373
434
|
catch (err) {
|
|
374
435
|
console.error(`! Could not reach AgentGuard Cloud: ${err.message}`);
|
|
@@ -376,7 +437,7 @@ async function main() {
|
|
|
376
437
|
return;
|
|
377
438
|
}
|
|
378
439
|
if (!advisory) {
|
|
379
|
-
console.error(`No advisory with id "${advisoryId}" found in
|
|
440
|
+
console.error(`No advisory with id "${advisoryId}" found in AgentGuard Cloud.`);
|
|
380
441
|
process.exitCode = 1;
|
|
381
442
|
return;
|
|
382
443
|
}
|
|
@@ -409,9 +470,349 @@ function readStdinIfAvailable() {
|
|
|
409
470
|
return '';
|
|
410
471
|
}
|
|
411
472
|
}
|
|
473
|
+
async function runLocalHealthCheckup(config) {
|
|
474
|
+
const skillRoots = [
|
|
475
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.claude', 'skills'),
|
|
476
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'skills'),
|
|
477
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'workspace', 'skills'),
|
|
478
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.qclaw', 'skills'),
|
|
479
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.qclaw', 'workspace', 'skills'),
|
|
480
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.hermes', 'skills'),
|
|
481
|
+
];
|
|
482
|
+
const skillDirs = discoverSkillDirs(skillRoots);
|
|
483
|
+
const scanner = new index_js_1.SkillScanner({ useExternalScanner: false });
|
|
484
|
+
const codeFindings = [];
|
|
485
|
+
let codeScore = skillDirs.length === 0 ? 70 : 100;
|
|
486
|
+
if (skillDirs.length === 0) {
|
|
487
|
+
codeFindings.push({ severity: 'LOW', text: 'No installed third-party skills were found to audit.' });
|
|
488
|
+
}
|
|
489
|
+
for (const dir of skillDirs) {
|
|
490
|
+
const result = await scanner.quickScan(dir);
|
|
491
|
+
const name = dir.split(/[\\/]/).pop() || dir;
|
|
492
|
+
if (result.risk_level === 'critical')
|
|
493
|
+
codeScore -= 15;
|
|
494
|
+
if (result.risk_level === 'high')
|
|
495
|
+
codeScore -= 8;
|
|
496
|
+
if (result.risk_level === 'medium')
|
|
497
|
+
codeScore -= 3;
|
|
498
|
+
if (result.risk_level !== 'low') {
|
|
499
|
+
codeFindings.push({
|
|
500
|
+
severity: riskLevelToSeverity(result.risk_level),
|
|
501
|
+
text: `${name}: ${result.summary}${result.risk_tags.length ? ` (${result.risk_tags.join(', ')})` : ''}`,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
codeScore = clampScore(codeScore);
|
|
506
|
+
const credential = checkCredentialSafety(skillDirs);
|
|
507
|
+
const network = await checkNetworkExposure(config);
|
|
508
|
+
const runtime = checkRuntimeProtection(config, skillDirs.length);
|
|
509
|
+
const web3 = checkWeb3Safety(skillDirs);
|
|
510
|
+
const dimensions = {
|
|
511
|
+
code_safety: {
|
|
512
|
+
score: codeScore,
|
|
513
|
+
findings: codeFindings,
|
|
514
|
+
details: `${skillDirs.length} installed skill(s) scanned with AgentGuard rules.`,
|
|
515
|
+
},
|
|
516
|
+
credential_safety: credential,
|
|
517
|
+
network_exposure: network,
|
|
518
|
+
runtime_protection: runtime,
|
|
519
|
+
web3_safety: web3,
|
|
520
|
+
};
|
|
521
|
+
const composite = calculateCompositeScore(dimensions);
|
|
522
|
+
const recommendations = Object.values(dimensions)
|
|
523
|
+
.flatMap((d) => d.findings)
|
|
524
|
+
.filter((f) => f.severity !== 'LOW')
|
|
525
|
+
.slice(0, 8);
|
|
526
|
+
return {
|
|
527
|
+
timestamp: new Date().toISOString(),
|
|
528
|
+
composite_score: composite,
|
|
529
|
+
tier: tierForScore(composite),
|
|
530
|
+
dimensions,
|
|
531
|
+
skills_scanned: skillDirs.length,
|
|
532
|
+
protection_level: config.level,
|
|
533
|
+
analysis: buildHealthAnalysis(composite, dimensions),
|
|
534
|
+
recommendations,
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function discoverSkillDirs(roots) {
|
|
538
|
+
const dirs = [];
|
|
539
|
+
for (const root of roots) {
|
|
540
|
+
if (!(0, node_fs_1.existsSync)(root))
|
|
541
|
+
continue;
|
|
542
|
+
let entries;
|
|
543
|
+
try {
|
|
544
|
+
entries = (0, node_fs_1.readdirSync)(root, { withFileTypes: true });
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
for (const entry of entries) {
|
|
550
|
+
if (!entry.isDirectory())
|
|
551
|
+
continue;
|
|
552
|
+
const dir = (0, node_path_1.join)(root, entry.name);
|
|
553
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'SKILL.md')))
|
|
554
|
+
dirs.push(dir);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return dirs;
|
|
558
|
+
}
|
|
559
|
+
function checkCredentialSafety(skillDirs) {
|
|
560
|
+
let score = 100;
|
|
561
|
+
const findings = [];
|
|
562
|
+
for (const [path, severity] of [
|
|
563
|
+
[(0, node_path_1.join)((0, node_os_1.homedir)(), '.ssh'), 'HIGH'],
|
|
564
|
+
[(0, node_path_1.join)((0, node_os_1.homedir)(), '.gnupg'), 'MEDIUM'],
|
|
565
|
+
]) {
|
|
566
|
+
const mode = permissionMode(path);
|
|
567
|
+
if (mode !== null && mode > 0o700) {
|
|
568
|
+
score -= severity === 'HIGH' ? 25 : 15;
|
|
569
|
+
findings.push({ severity, text: `${path} permissions are ${mode.toString(8)}; expected 700 or stricter.` });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const secretPatterns = [
|
|
573
|
+
{ re: /0x[a-fA-F0-9]{64}|-----BEGIN [A-Z ]*PRIVATE KEY-----/, severity: 'CRITICAL', label: 'Plaintext private key pattern' },
|
|
574
|
+
{ re: /\b(seed_phrase|mnemonic)\b/i, severity: 'CRITICAL', label: 'Mnemonic or seed phrase marker' },
|
|
575
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b|\bgh[pousr]_[A-Za-z0-9_]{20,}\b/, severity: 'HIGH', label: 'API key or token pattern' },
|
|
576
|
+
];
|
|
577
|
+
for (const dir of skillDirs) {
|
|
578
|
+
const manifest = (0, node_path_1.join)(dir, 'SKILL.md');
|
|
579
|
+
if (!(0, node_fs_1.existsSync)(manifest))
|
|
580
|
+
continue;
|
|
581
|
+
let body = '';
|
|
582
|
+
try {
|
|
583
|
+
body = (0, node_fs_1.readFileSync)(manifest, 'utf8').slice(0, 256 * 1024);
|
|
584
|
+
}
|
|
585
|
+
catch {
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
for (const pattern of secretPatterns) {
|
|
589
|
+
if (!pattern.re.test(body))
|
|
590
|
+
continue;
|
|
591
|
+
score -= pattern.severity === 'CRITICAL' ? 25 : 15;
|
|
592
|
+
findings.push({ severity: pattern.severity, text: `${pattern.label} found in ${manifest}.` });
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
score: clampScore(score),
|
|
597
|
+
findings,
|
|
598
|
+
details: findings.length ? `${findings.length} credential hygiene issue(s) found.` : 'Credential permissions and scanned manifests look clean.',
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
async function checkNetworkExposure(config) {
|
|
602
|
+
let score = 100;
|
|
603
|
+
const findings = [];
|
|
604
|
+
const listeners = await runCommandText('lsof', ['-i', '-P', '-n']);
|
|
605
|
+
for (const port of ['2375', '3306', '6379', '27017']) {
|
|
606
|
+
const exposed = new RegExp(`(\\*|0\\.0\\.0\\.0):${port}\\b`).test(listeners);
|
|
607
|
+
if (exposed) {
|
|
608
|
+
score -= 25;
|
|
609
|
+
findings.push({ severity: 'HIGH', text: `High-risk service appears exposed on 0.0.0.0:${port}.` });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const cron = await runCommandText('crontab', ['-l']);
|
|
613
|
+
if (/(curl\b.*\|\s*(bash|sh)|wget\b.*\|\s*(bash|sh)|\.ssh)/i.test(cron)) {
|
|
614
|
+
score -= 30;
|
|
615
|
+
findings.push({ severity: 'HIGH', text: 'Suspicious cron entry found that downloads shell code or accesses SSH material.' });
|
|
616
|
+
}
|
|
617
|
+
const sensitiveEnvNames = Object.keys(process.env).filter((name) => /PRIVATE_KEY|MNEMONIC|SECRET|PASSWORD/i.test(name));
|
|
618
|
+
if (sensitiveEnvNames.length > 0) {
|
|
619
|
+
score -= 20;
|
|
620
|
+
findings.push({ severity: 'MEDIUM', text: `Sensitive environment variable names are present: ${sensitiveEnvNames.slice(0, 8).join(', ')}.` });
|
|
621
|
+
}
|
|
622
|
+
for (const path of [(0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'openclaw.json'), (0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'devices', 'paired.json')]) {
|
|
623
|
+
const mode = permissionMode(path);
|
|
624
|
+
if (mode !== null && mode > 0o600) {
|
|
625
|
+
score -= 15;
|
|
626
|
+
findings.push({ severity: 'MEDIUM', text: `${path} permissions are ${mode.toString(8)}; expected 600 or stricter.` });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
score: clampScore(score),
|
|
631
|
+
findings,
|
|
632
|
+
details: findings.length ? `${findings.length} network/system exposure issue(s) found.` : 'No dangerous ports, cron entries, or config permission issues found.',
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function checkRuntimeProtection(config, skillsScanned) {
|
|
636
|
+
let score = 0;
|
|
637
|
+
const findings = [];
|
|
638
|
+
const hookFiles = [
|
|
639
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.claude', 'settings.json'),
|
|
640
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'openclaw.json'),
|
|
641
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.hermes', 'config.yaml'),
|
|
642
|
+
];
|
|
643
|
+
const hasHook = hookFiles.some((path) => {
|
|
644
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
645
|
+
return false;
|
|
646
|
+
try {
|
|
647
|
+
return /agentguard|guard-hook|hermes-hook/i.test((0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
if (hasHook)
|
|
654
|
+
score += 40;
|
|
655
|
+
else
|
|
656
|
+
findings.push({ severity: 'HIGH', text: 'No AgentGuard runtime hook was detected in known agent configuration files.' });
|
|
657
|
+
if ((0, node_fs_1.existsSync)(config.auditPath))
|
|
658
|
+
score += 30;
|
|
659
|
+
else
|
|
660
|
+
findings.push({ severity: 'MEDIUM', text: 'No AgentGuard audit log exists yet, so runtime threat history is unavailable.' });
|
|
661
|
+
if (skillsScanned > 0)
|
|
662
|
+
score += 30;
|
|
663
|
+
else
|
|
664
|
+
findings.push({ severity: 'MEDIUM', text: 'No installed skills were scanned during this checkup.' });
|
|
665
|
+
return {
|
|
666
|
+
score: clampScore(score),
|
|
667
|
+
findings,
|
|
668
|
+
details: findings.length ? `${findings.length} runtime protection gap(s) found.` : 'Runtime hooks, audit logging, and skill scanning are present.',
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function checkWeb3Safety(skillDirs) {
|
|
672
|
+
const web3Detected = ['GOPLUS_API_KEY', 'CHAIN_ID', 'RPC_URL'].some((name) => process.env[name]) ||
|
|
673
|
+
skillDirs.some((dir) => /web3|wallet|chain|defi|token/i.test(dir));
|
|
674
|
+
if (!web3Detected) {
|
|
675
|
+
return { score: null, na: true, findings: [], details: 'No Web3 usage detected.' };
|
|
676
|
+
}
|
|
677
|
+
const findings = [];
|
|
678
|
+
let score = process.env.GOPLUS_API_KEY ? 100 : 70;
|
|
679
|
+
if (!process.env.GOPLUS_API_KEY) {
|
|
680
|
+
findings.push({ severity: 'MEDIUM', text: 'Web3 usage detected but GOPLUS_API_KEY is not configured for transaction checks.' });
|
|
681
|
+
}
|
|
682
|
+
return {
|
|
683
|
+
score,
|
|
684
|
+
findings,
|
|
685
|
+
details: findings.length ? 'Web3 usage detected with missing transaction security configuration.' : 'Web3 safety configuration is present.',
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
function permissionMode(path) {
|
|
689
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
690
|
+
return null;
|
|
691
|
+
try {
|
|
692
|
+
return (0, node_fs_1.statSync)(path).mode & 0o777;
|
|
693
|
+
}
|
|
694
|
+
catch {
|
|
695
|
+
return null;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function runCommandText(command, args) {
|
|
699
|
+
return new Promise((resolvePromise) => {
|
|
700
|
+
try {
|
|
701
|
+
const child = (0, node_child_process_1.execFile)(command, args, { timeout: 2000 }, (error, stdout, stderr) => {
|
|
702
|
+
if (error)
|
|
703
|
+
resolvePromise('');
|
|
704
|
+
else
|
|
705
|
+
resolvePromise(`${stdout || ''}${stderr || ''}`);
|
|
706
|
+
});
|
|
707
|
+
child.on('error', () => resolvePromise(''));
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
resolvePromise('');
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
function calculateCompositeScore(dimensions) {
|
|
715
|
+
const web3Score = dimensions.web3_safety.score;
|
|
716
|
+
if (web3Score === null || dimensions.web3_safety.na) {
|
|
717
|
+
return Math.round((dimensions.code_safety.score ?? 0) * 0.294 +
|
|
718
|
+
(dimensions.credential_safety.score ?? 0) * 0.294 +
|
|
719
|
+
(dimensions.network_exposure.score ?? 0) * 0.235 +
|
|
720
|
+
(dimensions.runtime_protection.score ?? 0) * 0.176);
|
|
721
|
+
}
|
|
722
|
+
return Math.round((dimensions.code_safety.score ?? 0) * 0.25 +
|
|
723
|
+
(dimensions.credential_safety.score ?? 0) * 0.25 +
|
|
724
|
+
(dimensions.network_exposure.score ?? 0) * 0.20 +
|
|
725
|
+
(dimensions.runtime_protection.score ?? 0) * 0.15 +
|
|
726
|
+
web3Score * 0.15);
|
|
727
|
+
}
|
|
728
|
+
async function generateCheckupHtml(report) {
|
|
729
|
+
const tempDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentguard-checkup-'));
|
|
730
|
+
const dataPath = (0, node_path_1.join)(tempDir, 'data.json');
|
|
731
|
+
(0, node_fs_1.writeFileSync)(dataPath, JSON.stringify(report, null, 2), 'utf8');
|
|
732
|
+
const scriptPath = process.env.AGENTGUARD_CHECKUP_REPORT_SCRIPT
|
|
733
|
+
? (0, node_path_1.resolve)(process.env.AGENTGUARD_CHECKUP_REPORT_SCRIPT)
|
|
734
|
+
: (0, node_path_1.resolve)(__dirname, '..', 'skills', 'agentguard', 'scripts', 'checkup-report.js');
|
|
735
|
+
if (!(0, node_fs_1.existsSync)(scriptPath)) {
|
|
736
|
+
throw new Error(`report generator not found at ${scriptPath}`);
|
|
737
|
+
}
|
|
738
|
+
return new Promise((resolvePromise, reject) => {
|
|
739
|
+
(0, node_child_process_1.execFile)('node', [scriptPath, '--file', dataPath], { timeout: 6000 }, (error, stdout, stderr) => {
|
|
740
|
+
if (error)
|
|
741
|
+
reject(new Error(stderr || error.message));
|
|
742
|
+
else
|
|
743
|
+
resolvePromise(stdout.trim().split(/\r?\n/).pop() || '');
|
|
744
|
+
});
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
function printHealthCheckupSummary(report, htmlPath) {
|
|
748
|
+
const totalFindings = Object.values(report.dimensions).reduce((sum, dim) => sum + dim.findings.length, 0);
|
|
749
|
+
console.log('AgentGuard Health Checkup');
|
|
750
|
+
console.log(`Overall Health Score: ${report.composite_score}/100 (Tier ${report.tier})`);
|
|
751
|
+
console.log(`Findings: ${totalFindings}`);
|
|
752
|
+
console.log(`Skills scanned: ${report.skills_scanned}`);
|
|
753
|
+
for (const [name, dim] of Object.entries(report.dimensions)) {
|
|
754
|
+
const score = dim.na || dim.score === null ? 'N/A' : `${dim.score}/100`;
|
|
755
|
+
console.log(`- ${name}: ${score} - ${dim.details}`);
|
|
756
|
+
}
|
|
757
|
+
if (htmlPath) {
|
|
758
|
+
console.log(`Full visual report: ${htmlPath}`);
|
|
759
|
+
}
|
|
760
|
+
else {
|
|
761
|
+
console.log('Full visual report: unavailable (text summary shown above)');
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function appendCheckupAudit(auditPath, report) {
|
|
765
|
+
const totalFindings = Object.values(report.dimensions).reduce((sum, dim) => sum + dim.findings.length, 0);
|
|
766
|
+
try {
|
|
767
|
+
(0, node_fs_1.appendFileSync)(auditPath, `${JSON.stringify({
|
|
768
|
+
timestamp: report.timestamp,
|
|
769
|
+
event: 'checkup',
|
|
770
|
+
composite_score: report.composite_score,
|
|
771
|
+
tier: report.tier,
|
|
772
|
+
checks: 5,
|
|
773
|
+
findings: totalFindings,
|
|
774
|
+
skills_scanned: report.skills_scanned,
|
|
775
|
+
})}\n`, { mode: 0o600 });
|
|
776
|
+
}
|
|
777
|
+
catch {
|
|
778
|
+
// Checkup should still succeed if audit logging is unavailable.
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
function buildHealthAnalysis(score, dimensions) {
|
|
782
|
+
const weak = Object.entries(dimensions)
|
|
783
|
+
.filter(([, dim]) => !dim.na && dim.score !== null && dim.score < 70)
|
|
784
|
+
.map(([name]) => name.replace(/_/g, ' '));
|
|
785
|
+
if (weak.length === 0) {
|
|
786
|
+
return `Overall posture is healthy at ${score}/100. AgentGuard did not find major weaknesses across the local skill, credential, network, and runtime checks.`;
|
|
787
|
+
}
|
|
788
|
+
return `Overall posture is ${score}/100. The areas needing attention are ${weak.join(', ')}; review the findings and fix the highest-severity items first.`;
|
|
789
|
+
}
|
|
790
|
+
function tierForScore(score) {
|
|
791
|
+
if (score >= 90)
|
|
792
|
+
return 'S';
|
|
793
|
+
if (score >= 70)
|
|
794
|
+
return 'A';
|
|
795
|
+
if (score >= 50)
|
|
796
|
+
return 'B';
|
|
797
|
+
return 'F';
|
|
798
|
+
}
|
|
799
|
+
function clampScore(score) {
|
|
800
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
801
|
+
}
|
|
802
|
+
function riskLevelToSeverity(risk) {
|
|
803
|
+
if (risk === 'critical')
|
|
804
|
+
return 'CRITICAL';
|
|
805
|
+
if (risk === 'high')
|
|
806
|
+
return 'HIGH';
|
|
807
|
+
if (risk === 'medium')
|
|
808
|
+
return 'MEDIUM';
|
|
809
|
+
return 'LOW';
|
|
810
|
+
}
|
|
412
811
|
function buildSubscribeSummary(options) {
|
|
413
812
|
const matched = options.results.reduce((acc, r) => acc + r.matchedArtifacts.length, 0);
|
|
414
|
-
const shouldNotify = options.supported
|
|
813
|
+
const shouldNotify = options.supported
|
|
814
|
+
&& options.hardFailures === 0
|
|
815
|
+
&& (options.quiet ? matched > 0 : options.fresh > 0);
|
|
415
816
|
const summary = {
|
|
416
817
|
supported: options.supported,
|
|
417
818
|
pulled: options.pulled,
|
|
@@ -425,14 +826,31 @@ function buildSubscribeSummary(options) {
|
|
|
425
826
|
installed: false,
|
|
426
827
|
},
|
|
427
828
|
};
|
|
428
|
-
if (shouldNotify) {
|
|
829
|
+
if (shouldNotify && options.quiet) {
|
|
429
830
|
summary.notification = {
|
|
430
831
|
title: `AgentGuard detected ${matched} threat-feed match${matched === 1 ? '' : 'es'}`,
|
|
431
832
|
body: formatThreatFeedNotification(options.results),
|
|
432
833
|
};
|
|
433
834
|
}
|
|
835
|
+
else if (shouldNotify) {
|
|
836
|
+
summary.notification = {
|
|
837
|
+
title: `AgentGuard found ${options.fresh} new threat-feed advisor${options.fresh === 1 ? 'y' : 'ies'}`,
|
|
838
|
+
body: formatNewAdvisoryNotification(options.freshAdvisories),
|
|
839
|
+
};
|
|
840
|
+
}
|
|
434
841
|
return summary;
|
|
435
842
|
}
|
|
843
|
+
function formatNewAdvisoryNotification(advisories) {
|
|
844
|
+
const lines = ['AgentGuard found new threat-feed advisories that need manual review:'];
|
|
845
|
+
for (const advisory of advisories.slice(0, 10)) {
|
|
846
|
+
lines.push(`- ${advisory.id} [${advisory.severity}] ${advisory.summary}`);
|
|
847
|
+
}
|
|
848
|
+
if (advisories.length > 10) {
|
|
849
|
+
lines.push(`- ... ${advisories.length - 10} more`);
|
|
850
|
+
}
|
|
851
|
+
lines.push('Run `agentguard subscribe --quiet` to execute the local self-check and report matches automatically.');
|
|
852
|
+
return lines.join('\n');
|
|
853
|
+
}
|
|
436
854
|
function formatThreatFeedNotification(results) {
|
|
437
855
|
const lines = ['AgentGuard threat-feed self-check found local matches:'];
|
|
438
856
|
for (const result of results) {
|