@goplus/agentguard 1.1.8 → 1.1.10
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 +31 -12
- package/dist/cli.js +518 -54
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/feed/cron.d.ts +32 -2
- package/dist/feed/cron.d.ts.map +1 -1
- package/dist/feed/cron.js +398 -30
- 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 +1 -1
- package/dist/feed/types.d.ts.map +1 -1
- package/dist/installers.d.ts +1 -1
- package/dist/installers.d.ts.map +1 -1
- package/dist/installers.js +57 -0
- package/dist/installers.js.map +1 -1
- package/dist/postinstall.js +9 -0
- package/dist/postinstall.js.map +1 -1
- package/dist/runtime/types.d.ts +1 -1
- package/dist/runtime/types.d.ts.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-init.test.d.ts +2 -0
- package/dist/tests/cli-init.test.d.ts.map +1 -0
- package/dist/tests/cli-init.test.js +40 -0
- package/dist/tests/cli-init.test.js.map +1 -0
- package/dist/tests/cli-policy.test.js +47 -0
- package/dist/tests/cli-policy.test.js.map +1 -1
- 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/feed-cron.test.js +266 -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 +13 -0
- package/dist/tests/installer.test.js.map +1 -1
- package/dist/tests/postinstall.test.d.ts +2 -0
- package/dist/tests/postinstall.test.d.ts.map +1 -0
- package/dist/tests/postinstall.test.js +27 -0
- package/dist/tests/postinstall.test.js.map +1 -0
- package/package.json +2 -1
- package/skills/agentguard/SKILL.md +37 -9
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");
|
|
@@ -23,7 +26,7 @@ async function main() {
|
|
|
23
26
|
.command('init')
|
|
24
27
|
.description('Create ~/.agentguard/config.json and local runtime paths')
|
|
25
28
|
.option('--level <level>', 'Protection level: strict | balanced | permissive')
|
|
26
|
-
.option('--agent <agent>', 'Install hook/template for claude-code, codex, or
|
|
29
|
+
.option('--agent <agent>', 'Install hook/template for claude-code, codex, openclaw, hermes, or qclaw')
|
|
27
30
|
.option('--cloud <url>', 'AgentGuard Cloud URL to store in local config')
|
|
28
31
|
.option('--force', 'Overwrite existing hook/template files')
|
|
29
32
|
.action((options) => {
|
|
@@ -43,10 +46,13 @@ async function main() {
|
|
|
43
46
|
console.log(`AgentGuard initialized at ${paths.home}`);
|
|
44
47
|
console.log(`Config: ${paths.configPath}`);
|
|
45
48
|
if (options.agent) {
|
|
46
|
-
if (!['claude-code', 'codex', 'openclaw'].includes(options.agent)) {
|
|
47
|
-
throw new Error('Invalid agent. Use claude-code, codex, or
|
|
49
|
+
if (!['claude-code', 'codex', 'openclaw', 'hermes', 'qclaw'].includes(options.agent)) {
|
|
50
|
+
throw new Error('Invalid agent. Use claude-code, codex, openclaw, hermes, or qclaw.');
|
|
48
51
|
}
|
|
49
|
-
const
|
|
52
|
+
const agent = options.agent;
|
|
53
|
+
config.agentHost = agent;
|
|
54
|
+
(0, config_js_1.saveConfig)(config);
|
|
55
|
+
const result = (0, installers_js_1.installAgentTemplates)(agent, { force: options.force });
|
|
50
56
|
console.log(`Installed ${result.agent} template:`);
|
|
51
57
|
for (const file of result.files)
|
|
52
58
|
console.log(`- ${file}`);
|
|
@@ -96,6 +102,7 @@ async function main() {
|
|
|
96
102
|
console.log(`Protection level: ${config.level}`);
|
|
97
103
|
console.log(`Cloud URL: ${config.cloudUrl || 'not configured'}`);
|
|
98
104
|
console.log(`API key: ${(0, config_js_1.maskApiKey)(config.apiKey)}`);
|
|
105
|
+
console.log(`Agent host: ${config.agentHost || 'not configured'}`);
|
|
99
106
|
console.log(`Policy cache: ${config.policyCachePath}`);
|
|
100
107
|
console.log(`Audit log: ${config.auditPath}`);
|
|
101
108
|
});
|
|
@@ -147,6 +154,41 @@ async function main() {
|
|
|
147
154
|
process.exitCode = 1;
|
|
148
155
|
}
|
|
149
156
|
});
|
|
157
|
+
policy
|
|
158
|
+
.command('show')
|
|
159
|
+
.description('Show the cached effective runtime policy, or the bundled default policy when no cache exists')
|
|
160
|
+
.option('--json', 'Print JSON output')
|
|
161
|
+
.action((options) => {
|
|
162
|
+
const config = (0, config_js_1.ensureConfig)();
|
|
163
|
+
const cachedPolicy = (0, policy_js_1.loadCachedPolicy)(config.policyCachePath);
|
|
164
|
+
const source = cachedPolicy ? 'cache' : 'default';
|
|
165
|
+
const shownPolicy = cachedPolicy ?? (0, policy_js_1.getDefaultEffectiveRuntimePolicy)();
|
|
166
|
+
if (options.json) {
|
|
167
|
+
console.log(JSON.stringify({
|
|
168
|
+
success: true,
|
|
169
|
+
source,
|
|
170
|
+
cachePath: config.policyCachePath,
|
|
171
|
+
policy: shownPolicy,
|
|
172
|
+
}, null, 2));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
console.log(`Policy source: ${source}`);
|
|
176
|
+
console.log(`Policy version: ${shownPolicy.policyVersion}`);
|
|
177
|
+
console.log(`Mode: ${shownPolicy.mode}`);
|
|
178
|
+
console.log(`Updated at: ${shownPolicy.updatedAt}`);
|
|
179
|
+
console.log(`Cache path: ${config.policyCachePath}`);
|
|
180
|
+
console.log('Decisions:');
|
|
181
|
+
for (const [name, decision] of Object.entries(shownPolicy.decisions)) {
|
|
182
|
+
console.log(`- ${name}: ${decision}`);
|
|
183
|
+
}
|
|
184
|
+
console.log(`Protected paths: ${shownPolicy.protectedPaths.length}`);
|
|
185
|
+
console.log(`Blocked command patterns: ${shownPolicy.blockedCommandPatterns.length}`);
|
|
186
|
+
console.log(`Allowed command patterns: ${shownPolicy.allowedCommandPatterns.length}`);
|
|
187
|
+
console.log(`Approval action types: ${shownPolicy.approvalActionTypes.join(', ') || 'none'}`);
|
|
188
|
+
console.log(`Network default outbound: ${shownPolicy.network.defaultOutbound}`);
|
|
189
|
+
console.log(`Blocked domains: ${shownPolicy.network.blockedDomains.length}`);
|
|
190
|
+
console.log(`Approval domains: ${shownPolicy.network.approvalDomains.length}`);
|
|
191
|
+
});
|
|
150
192
|
program
|
|
151
193
|
.command('doctor')
|
|
152
194
|
.description('Check local AgentGuard setup')
|
|
@@ -219,17 +261,23 @@ async function main() {
|
|
|
219
261
|
.description('Pull new threat-feed advisories from AgentGuard Cloud and run a self-check against locally installed skills')
|
|
220
262
|
.option('--since <iso>', 'Override the persisted last-pulled timestamp')
|
|
221
263
|
.option('--json', 'Emit machine-readable summary instead of human text')
|
|
264
|
+
.option('--quiet', 'Run the full pull, self-check, and match-reporting flow with minimal output')
|
|
222
265
|
.option('--no-report', 'Skip uploading self-check results back to Cloud')
|
|
223
|
-
.option('--
|
|
224
|
-
.option('--cron-
|
|
225
|
-
.option('--
|
|
226
|
-
.option('--force', 'Replace an existing
|
|
266
|
+
.option('--cron <expr>', 'Install a cron job with a five-field cron expression, for example "0 * * * *"')
|
|
267
|
+
.option('--cron-target <target>', 'Cron backend: auto, openclaw, qclaw, hermes, or system', 'auto')
|
|
268
|
+
.option('--cron-name <name>', 'Cron job name', 'agentguard-threat-feed')
|
|
269
|
+
.option('--force', 'Replace an existing cron job with the same name')
|
|
227
270
|
.option('--cron-run', 'Internal: run from the OpenClaw cron prompt without trying to install cron again')
|
|
228
271
|
.action(async (options) => {
|
|
229
272
|
const config = (0, config_js_1.ensureConfig)();
|
|
230
273
|
const client = new client_js_1.AgentGuardCloudClient(config);
|
|
231
274
|
const state = (0, state_js_1.loadFeedState)();
|
|
232
275
|
const since = options.since ?? state.lastPulledAt;
|
|
276
|
+
const quiet = Boolean(options.quiet);
|
|
277
|
+
const cronTarget = validateCronTarget(options.cronTarget);
|
|
278
|
+
const cronExpression = options.cron && !options.cronRun
|
|
279
|
+
? (0, cron_js_1.validateCronExpression)(options.cron)
|
|
280
|
+
: undefined;
|
|
233
281
|
let advisories;
|
|
234
282
|
try {
|
|
235
283
|
advisories = await client.pullAdvisories(since);
|
|
@@ -244,7 +292,7 @@ async function main() {
|
|
|
244
292
|
if (options.json) {
|
|
245
293
|
console.log(JSON.stringify({ supported: false, shouldNotify: false, results: [], cron: { requested: false, installed: false } }));
|
|
246
294
|
}
|
|
247
|
-
else {
|
|
295
|
+
else if (!quiet) {
|
|
248
296
|
console.log('AgentGuard Cloud does not expose /api/v1/feed/advisories yet — nothing to do.');
|
|
249
297
|
}
|
|
250
298
|
return;
|
|
@@ -259,48 +307,58 @@ async function main() {
|
|
|
259
307
|
let cursorOk = true; // stops advancing on the first hard failure
|
|
260
308
|
let latestPublishedAt = state.lastPulledAt;
|
|
261
309
|
let hardFailures = 0;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
result = await (0, selfcheck_js_1.runSelfCheckForAdvisory)(advisory);
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
269
|
-
// runSelfCheck shouldn't throw, but if it does the advisory has
|
|
270
|
-
// not been evaluated — don't mark it seen and don't advance.
|
|
271
|
-
console.error(`! Self-check threw for ${advisory.id}: ${err.message}`);
|
|
272
|
-
hardFailures += 1;
|
|
273
|
-
cursorOk = false;
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
results.push(result);
|
|
277
|
-
if (options.report !== false && client.connected && result.matchedArtifacts.length > 0) {
|
|
278
|
-
// Report is on the critical path — if Cloud doesn't see the
|
|
279
|
-
// match, we must NOT mark the advisory seen, otherwise a
|
|
280
|
-
// transient network blip silently buries a real hit.
|
|
310
|
+
if (quiet) {
|
|
311
|
+
for (const advisory of fresh) {
|
|
312
|
+
let processed = true;
|
|
313
|
+
let result;
|
|
281
314
|
try {
|
|
282
|
-
await
|
|
283
|
-
elapsedMs: result.elapsedMs,
|
|
284
|
-
warnings: result.warnings,
|
|
285
|
-
});
|
|
315
|
+
result = await (0, selfcheck_js_1.runSelfCheckForAdvisory)(advisory);
|
|
286
316
|
}
|
|
287
317
|
catch (err) {
|
|
288
|
-
|
|
289
|
-
|
|
318
|
+
// runSelfCheck shouldn't throw, but if it does the advisory has
|
|
319
|
+
// not been evaluated — don't mark it seen and don't advance.
|
|
320
|
+
console.error(`! Self-check threw for ${advisory.id}: ${err.message}`);
|
|
290
321
|
hardFailures += 1;
|
|
322
|
+
cursorOk = false;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
results.push(result);
|
|
326
|
+
if (options.report !== false && client.connected && result.matchedArtifacts.length > 0) {
|
|
327
|
+
// Report is on the critical path — if Cloud doesn't see the
|
|
328
|
+
// match, we must NOT mark the advisory seen, otherwise a
|
|
329
|
+
// transient network blip silently buries a real hit.
|
|
330
|
+
try {
|
|
331
|
+
await client.reportSelfCheck(advisory.id, result.matchedArtifacts, {
|
|
332
|
+
elapsedMs: result.elapsedMs,
|
|
333
|
+
warnings: result.warnings,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
console.error(`! Failed to report self-check for ${advisory.id}: ${err.message}`);
|
|
338
|
+
processed = false;
|
|
339
|
+
hardFailures += 1;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (processed) {
|
|
343
|
+
Object.assign(state, (0, state_js_1.markAdvisorySeen)(state, advisory.id));
|
|
344
|
+
if (cursorOk && (!latestPublishedAt || advisory.publishedAt > latestPublishedAt)) {
|
|
345
|
+
latestPublishedAt = advisory.publishedAt;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// From this point we no longer advance the pull cursor — the
|
|
350
|
+
// failed advisory must be re-pulled on the next run.
|
|
351
|
+
cursorOk = false;
|
|
291
352
|
}
|
|
292
353
|
}
|
|
293
|
-
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
for (const advisory of fresh) {
|
|
294
357
|
Object.assign(state, (0, state_js_1.markAdvisorySeen)(state, advisory.id));
|
|
295
358
|
if (cursorOk && (!latestPublishedAt || advisory.publishedAt > latestPublishedAt)) {
|
|
296
359
|
latestPublishedAt = advisory.publishedAt;
|
|
297
360
|
}
|
|
298
361
|
}
|
|
299
|
-
else {
|
|
300
|
-
// From this point we no longer advance the pull cursor — the
|
|
301
|
-
// failed advisory must be re-pulled on the next run.
|
|
302
|
-
cursorOk = false;
|
|
303
|
-
}
|
|
304
362
|
}
|
|
305
363
|
state.lastPulledAt = latestPublishedAt;
|
|
306
364
|
(0, state_js_1.saveFeedState)(state);
|
|
@@ -309,16 +367,22 @@ async function main() {
|
|
|
309
367
|
supported: true,
|
|
310
368
|
pulled: advisories.length,
|
|
311
369
|
fresh: fresh.length,
|
|
370
|
+
freshAdvisories: fresh,
|
|
312
371
|
results,
|
|
313
372
|
hardFailures,
|
|
373
|
+
quiet,
|
|
314
374
|
});
|
|
315
|
-
if (options.
|
|
375
|
+
if (options.cron && !options.cronRun) {
|
|
316
376
|
summary.cron.requested = true;
|
|
317
377
|
try {
|
|
318
|
-
summary.cron.result = await (0, cron_js_1.
|
|
378
|
+
summary.cron.result = await (0, cron_js_1.installThreatFeedCron)({
|
|
319
379
|
name: options.cronName,
|
|
320
|
-
|
|
380
|
+
cronExpression: cronExpression,
|
|
381
|
+
quiet,
|
|
321
382
|
force: Boolean(options.force),
|
|
383
|
+
backend: cronTarget,
|
|
384
|
+
agentHost: config.agentHost,
|
|
385
|
+
agentGuardHome: (0, config_js_1.getAgentGuardPaths)().home,
|
|
322
386
|
});
|
|
323
387
|
summary.cron.installed = true;
|
|
324
388
|
}
|
|
@@ -331,8 +395,18 @@ async function main() {
|
|
|
331
395
|
console.log(JSON.stringify(summary, null, 2));
|
|
332
396
|
return;
|
|
333
397
|
}
|
|
398
|
+
if (quiet && fresh.length === 0 && !summary.cron.result) {
|
|
399
|
+
process.exitCode = 0;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
334
402
|
console.log(`Pulled ${advisories.length} advisory record(s); ${fresh.length} new.`);
|
|
335
|
-
if (fresh.length > 0) {
|
|
403
|
+
if (!quiet && fresh.length > 0) {
|
|
404
|
+
console.log('New threat-feed advisories found. Review and handle them manually:');
|
|
405
|
+
for (const advisory of fresh) {
|
|
406
|
+
console.log(` - ${advisory.id} [${advisory.severity}] ${advisory.summary}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (quiet && fresh.length > 0) {
|
|
336
410
|
console.log(`Self-check found ${totalMatches} match(es) across the new advisories.`);
|
|
337
411
|
for (const r of results) {
|
|
338
412
|
if (r.matchedArtifacts.length === 0)
|
|
@@ -344,9 +418,15 @@ async function main() {
|
|
|
344
418
|
}
|
|
345
419
|
}
|
|
346
420
|
if (summary.cron.result) {
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
console.log(
|
|
421
|
+
const label = summary.cron.result.backend ?? 'cron';
|
|
422
|
+
const action = summary.cron.result.created ? `Installed ${label} cron job` : `${label} cron job already exists`;
|
|
423
|
+
console.log(`${action} "${summary.cron.result.name}" (${summary.cron.result.schedule}, ${summary.cron.result.timezone}).`);
|
|
424
|
+
if (summary.cron.result.backend === 'system') {
|
|
425
|
+
console.log(`System cron output: ${(0, node_path_1.join)((0, config_js_1.getAgentGuardPaths)().home, 'feed-cron.log')}`);
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
console.log('Notification rule: non-quiet cron notifies on new advisories; quiet cron notifies on local matches.');
|
|
429
|
+
}
|
|
350
430
|
}
|
|
351
431
|
// Exit codes: 2 = matches found, 1 = at least one advisory failed
|
|
352
432
|
// to evaluate or report (cursor was held back), 0 = clean.
|
|
@@ -354,7 +434,7 @@ async function main() {
|
|
|
354
434
|
console.error(`! ${hardFailures} advisory record(s) failed to process and will be re-pulled next run.`);
|
|
355
435
|
process.exitCode = 1;
|
|
356
436
|
}
|
|
357
|
-
else if (totalMatches > 0) {
|
|
437
|
+
else if (quiet && totalMatches > 0) {
|
|
358
438
|
process.exitCode = 2;
|
|
359
439
|
}
|
|
360
440
|
else {
|
|
@@ -363,16 +443,38 @@ async function main() {
|
|
|
363
443
|
});
|
|
364
444
|
program
|
|
365
445
|
.command('checkup')
|
|
366
|
-
.description('Run a
|
|
446
|
+
.description('Run a local agent health checkup. Use --against-advisory only for targeted threat-feed self-checks.')
|
|
367
447
|
.option('--against-advisory <id>', 'Restrict the check to a single advisory id (fetches it from Cloud if needed)')
|
|
368
448
|
.option('--json', 'Emit machine-readable result')
|
|
369
449
|
.action(async (options) => {
|
|
370
450
|
const config = (0, config_js_1.ensureConfig)();
|
|
371
|
-
const client = new client_js_1.AgentGuardCloudClient(config);
|
|
372
451
|
const advisoryId = options.againstAdvisory;
|
|
373
452
|
if (!advisoryId) {
|
|
374
|
-
|
|
375
|
-
|
|
453
|
+
const report = await runLocalHealthCheckup(config);
|
|
454
|
+
if (options.json) {
|
|
455
|
+
console.log(JSON.stringify(report, null, 2));
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
const htmlPath = await generateCheckupHtml(report).catch((err) => {
|
|
459
|
+
console.error(`! Could not generate visual checkup report: ${err.message}`);
|
|
460
|
+
return null;
|
|
461
|
+
});
|
|
462
|
+
printHealthCheckupSummary(report, htmlPath);
|
|
463
|
+
}
|
|
464
|
+
appendCheckupAudit(config.auditPath, report);
|
|
465
|
+
process.exitCode = 0;
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const client = new client_js_1.AgentGuardCloudClient(config);
|
|
469
|
+
if (!client.connected) {
|
|
470
|
+
const message = 'AgentGuard Cloud is not connected. Run `agentguard connect --key <key>` first.';
|
|
471
|
+
if (options.json) {
|
|
472
|
+
console.log(JSON.stringify({ success: false, error: message }, null, 2));
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
console.error(message);
|
|
476
|
+
}
|
|
477
|
+
process.exitCode = 1;
|
|
376
478
|
return;
|
|
377
479
|
}
|
|
378
480
|
let advisory = null;
|
|
@@ -408,6 +510,11 @@ async function main() {
|
|
|
408
510
|
});
|
|
409
511
|
await program.parseAsync(process.argv);
|
|
410
512
|
}
|
|
513
|
+
function validateCronTarget(value) {
|
|
514
|
+
if (value === 'auto' || value === 'openclaw' || value === 'qclaw' || value === 'hermes' || value === 'system')
|
|
515
|
+
return value;
|
|
516
|
+
throw new Error('Invalid cron target. Use auto, openclaw, qclaw, hermes, or system.');
|
|
517
|
+
}
|
|
411
518
|
function readStdinIfAvailable() {
|
|
412
519
|
if (process.stdin.isTTY)
|
|
413
520
|
return '';
|
|
@@ -418,9 +525,349 @@ function readStdinIfAvailable() {
|
|
|
418
525
|
return '';
|
|
419
526
|
}
|
|
420
527
|
}
|
|
528
|
+
async function runLocalHealthCheckup(config) {
|
|
529
|
+
const skillRoots = [
|
|
530
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.claude', 'skills'),
|
|
531
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'skills'),
|
|
532
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'workspace', 'skills'),
|
|
533
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.qclaw', 'skills'),
|
|
534
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.qclaw', 'workspace', 'skills'),
|
|
535
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.hermes', 'skills'),
|
|
536
|
+
];
|
|
537
|
+
const skillDirs = discoverSkillDirs(skillRoots);
|
|
538
|
+
const scanner = new index_js_1.SkillScanner({ useExternalScanner: false });
|
|
539
|
+
const codeFindings = [];
|
|
540
|
+
let codeScore = skillDirs.length === 0 ? 70 : 100;
|
|
541
|
+
if (skillDirs.length === 0) {
|
|
542
|
+
codeFindings.push({ severity: 'LOW', text: 'No installed third-party skills were found to audit.' });
|
|
543
|
+
}
|
|
544
|
+
for (const dir of skillDirs) {
|
|
545
|
+
const result = await scanner.quickScan(dir);
|
|
546
|
+
const name = dir.split(/[\\/]/).pop() || dir;
|
|
547
|
+
if (result.risk_level === 'critical')
|
|
548
|
+
codeScore -= 15;
|
|
549
|
+
if (result.risk_level === 'high')
|
|
550
|
+
codeScore -= 8;
|
|
551
|
+
if (result.risk_level === 'medium')
|
|
552
|
+
codeScore -= 3;
|
|
553
|
+
if (result.risk_level !== 'low') {
|
|
554
|
+
codeFindings.push({
|
|
555
|
+
severity: riskLevelToSeverity(result.risk_level),
|
|
556
|
+
text: `${name}: ${result.summary}${result.risk_tags.length ? ` (${result.risk_tags.join(', ')})` : ''}`,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
codeScore = clampScore(codeScore);
|
|
561
|
+
const credential = checkCredentialSafety(skillDirs);
|
|
562
|
+
const network = await checkNetworkExposure(config);
|
|
563
|
+
const runtime = checkRuntimeProtection(config, skillDirs.length);
|
|
564
|
+
const web3 = checkWeb3Safety(skillDirs);
|
|
565
|
+
const dimensions = {
|
|
566
|
+
code_safety: {
|
|
567
|
+
score: codeScore,
|
|
568
|
+
findings: codeFindings,
|
|
569
|
+
details: `${skillDirs.length} installed skill(s) scanned with AgentGuard rules.`,
|
|
570
|
+
},
|
|
571
|
+
credential_safety: credential,
|
|
572
|
+
network_exposure: network,
|
|
573
|
+
runtime_protection: runtime,
|
|
574
|
+
web3_safety: web3,
|
|
575
|
+
};
|
|
576
|
+
const composite = calculateCompositeScore(dimensions);
|
|
577
|
+
const recommendations = Object.values(dimensions)
|
|
578
|
+
.flatMap((d) => d.findings)
|
|
579
|
+
.filter((f) => f.severity !== 'LOW')
|
|
580
|
+
.slice(0, 8);
|
|
581
|
+
return {
|
|
582
|
+
timestamp: new Date().toISOString(),
|
|
583
|
+
composite_score: composite,
|
|
584
|
+
tier: tierForScore(composite),
|
|
585
|
+
dimensions,
|
|
586
|
+
skills_scanned: skillDirs.length,
|
|
587
|
+
protection_level: config.level,
|
|
588
|
+
analysis: buildHealthAnalysis(composite, dimensions),
|
|
589
|
+
recommendations,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
function discoverSkillDirs(roots) {
|
|
593
|
+
const dirs = [];
|
|
594
|
+
for (const root of roots) {
|
|
595
|
+
if (!(0, node_fs_1.existsSync)(root))
|
|
596
|
+
continue;
|
|
597
|
+
let entries;
|
|
598
|
+
try {
|
|
599
|
+
entries = (0, node_fs_1.readdirSync)(root, { withFileTypes: true });
|
|
600
|
+
}
|
|
601
|
+
catch {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
for (const entry of entries) {
|
|
605
|
+
if (!entry.isDirectory())
|
|
606
|
+
continue;
|
|
607
|
+
const dir = (0, node_path_1.join)(root, entry.name);
|
|
608
|
+
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'SKILL.md')))
|
|
609
|
+
dirs.push(dir);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return dirs;
|
|
613
|
+
}
|
|
614
|
+
function checkCredentialSafety(skillDirs) {
|
|
615
|
+
let score = 100;
|
|
616
|
+
const findings = [];
|
|
617
|
+
for (const [path, severity] of [
|
|
618
|
+
[(0, node_path_1.join)((0, node_os_1.homedir)(), '.ssh'), 'HIGH'],
|
|
619
|
+
[(0, node_path_1.join)((0, node_os_1.homedir)(), '.gnupg'), 'MEDIUM'],
|
|
620
|
+
]) {
|
|
621
|
+
const mode = permissionMode(path);
|
|
622
|
+
if (mode !== null && mode > 0o700) {
|
|
623
|
+
score -= severity === 'HIGH' ? 25 : 15;
|
|
624
|
+
findings.push({ severity, text: `${path} permissions are ${mode.toString(8)}; expected 700 or stricter.` });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const secretPatterns = [
|
|
628
|
+
{ re: /0x[a-fA-F0-9]{64}|-----BEGIN [A-Z ]*PRIVATE KEY-----/, severity: 'CRITICAL', label: 'Plaintext private key pattern' },
|
|
629
|
+
{ re: /\b(seed_phrase|mnemonic)\b/i, severity: 'CRITICAL', label: 'Mnemonic or seed phrase marker' },
|
|
630
|
+
{ re: /\bAKIA[0-9A-Z]{16}\b|\bgh[pousr]_[A-Za-z0-9_]{20,}\b/, severity: 'HIGH', label: 'API key or token pattern' },
|
|
631
|
+
];
|
|
632
|
+
for (const dir of skillDirs) {
|
|
633
|
+
const manifest = (0, node_path_1.join)(dir, 'SKILL.md');
|
|
634
|
+
if (!(0, node_fs_1.existsSync)(manifest))
|
|
635
|
+
continue;
|
|
636
|
+
let body = '';
|
|
637
|
+
try {
|
|
638
|
+
body = (0, node_fs_1.readFileSync)(manifest, 'utf8').slice(0, 256 * 1024);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
for (const pattern of secretPatterns) {
|
|
644
|
+
if (!pattern.re.test(body))
|
|
645
|
+
continue;
|
|
646
|
+
score -= pattern.severity === 'CRITICAL' ? 25 : 15;
|
|
647
|
+
findings.push({ severity: pattern.severity, text: `${pattern.label} found in ${manifest}.` });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return {
|
|
651
|
+
score: clampScore(score),
|
|
652
|
+
findings,
|
|
653
|
+
details: findings.length ? `${findings.length} credential hygiene issue(s) found.` : 'Credential permissions and scanned manifests look clean.',
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
async function checkNetworkExposure(config) {
|
|
657
|
+
let score = 100;
|
|
658
|
+
const findings = [];
|
|
659
|
+
const listeners = await runCommandText('lsof', ['-i', '-P', '-n']);
|
|
660
|
+
for (const port of ['2375', '3306', '6379', '27017']) {
|
|
661
|
+
const exposed = new RegExp(`(\\*|0\\.0\\.0\\.0):${port}\\b`).test(listeners);
|
|
662
|
+
if (exposed) {
|
|
663
|
+
score -= 25;
|
|
664
|
+
findings.push({ severity: 'HIGH', text: `High-risk service appears exposed on 0.0.0.0:${port}.` });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const cron = await runCommandText('crontab', ['-l']);
|
|
668
|
+
if (/(curl\b.*\|\s*(bash|sh)|wget\b.*\|\s*(bash|sh)|\.ssh)/i.test(cron)) {
|
|
669
|
+
score -= 30;
|
|
670
|
+
findings.push({ severity: 'HIGH', text: 'Suspicious cron entry found that downloads shell code or accesses SSH material.' });
|
|
671
|
+
}
|
|
672
|
+
const sensitiveEnvNames = Object.keys(process.env).filter((name) => /PRIVATE_KEY|MNEMONIC|SECRET|PASSWORD/i.test(name));
|
|
673
|
+
if (sensitiveEnvNames.length > 0) {
|
|
674
|
+
score -= 20;
|
|
675
|
+
findings.push({ severity: 'MEDIUM', text: `Sensitive environment variable names are present: ${sensitiveEnvNames.slice(0, 8).join(', ')}.` });
|
|
676
|
+
}
|
|
677
|
+
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')]) {
|
|
678
|
+
const mode = permissionMode(path);
|
|
679
|
+
if (mode !== null && mode > 0o600) {
|
|
680
|
+
score -= 15;
|
|
681
|
+
findings.push({ severity: 'MEDIUM', text: `${path} permissions are ${mode.toString(8)}; expected 600 or stricter.` });
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
score: clampScore(score),
|
|
686
|
+
findings,
|
|
687
|
+
details: findings.length ? `${findings.length} network/system exposure issue(s) found.` : 'No dangerous ports, cron entries, or config permission issues found.',
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function checkRuntimeProtection(config, skillsScanned) {
|
|
691
|
+
let score = 0;
|
|
692
|
+
const findings = [];
|
|
693
|
+
const hookFiles = [
|
|
694
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.claude', 'settings.json'),
|
|
695
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.openclaw', 'openclaw.json'),
|
|
696
|
+
(0, node_path_1.join)((0, node_os_1.homedir)(), '.hermes', 'config.yaml'),
|
|
697
|
+
];
|
|
698
|
+
const hasHook = hookFiles.some((path) => {
|
|
699
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
700
|
+
return false;
|
|
701
|
+
try {
|
|
702
|
+
return /agentguard|guard-hook|hermes-hook/i.test((0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
if (hasHook)
|
|
709
|
+
score += 40;
|
|
710
|
+
else
|
|
711
|
+
findings.push({ severity: 'HIGH', text: 'No AgentGuard runtime hook was detected in known agent configuration files.' });
|
|
712
|
+
if ((0, node_fs_1.existsSync)(config.auditPath))
|
|
713
|
+
score += 30;
|
|
714
|
+
else
|
|
715
|
+
findings.push({ severity: 'MEDIUM', text: 'No AgentGuard audit log exists yet, so runtime threat history is unavailable.' });
|
|
716
|
+
if (skillsScanned > 0)
|
|
717
|
+
score += 30;
|
|
718
|
+
else
|
|
719
|
+
findings.push({ severity: 'MEDIUM', text: 'No installed skills were scanned during this checkup.' });
|
|
720
|
+
return {
|
|
721
|
+
score: clampScore(score),
|
|
722
|
+
findings,
|
|
723
|
+
details: findings.length ? `${findings.length} runtime protection gap(s) found.` : 'Runtime hooks, audit logging, and skill scanning are present.',
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function checkWeb3Safety(skillDirs) {
|
|
727
|
+
const web3Detected = ['GOPLUS_API_KEY', 'CHAIN_ID', 'RPC_URL'].some((name) => process.env[name]) ||
|
|
728
|
+
skillDirs.some((dir) => /web3|wallet|chain|defi|token/i.test(dir));
|
|
729
|
+
if (!web3Detected) {
|
|
730
|
+
return { score: null, na: true, findings: [], details: 'No Web3 usage detected.' };
|
|
731
|
+
}
|
|
732
|
+
const findings = [];
|
|
733
|
+
let score = process.env.GOPLUS_API_KEY ? 100 : 70;
|
|
734
|
+
if (!process.env.GOPLUS_API_KEY) {
|
|
735
|
+
findings.push({ severity: 'MEDIUM', text: 'Web3 usage detected but GOPLUS_API_KEY is not configured for transaction checks.' });
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
score,
|
|
739
|
+
findings,
|
|
740
|
+
details: findings.length ? 'Web3 usage detected with missing transaction security configuration.' : 'Web3 safety configuration is present.',
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function permissionMode(path) {
|
|
744
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
745
|
+
return null;
|
|
746
|
+
try {
|
|
747
|
+
return (0, node_fs_1.statSync)(path).mode & 0o777;
|
|
748
|
+
}
|
|
749
|
+
catch {
|
|
750
|
+
return null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function runCommandText(command, args) {
|
|
754
|
+
return new Promise((resolvePromise) => {
|
|
755
|
+
try {
|
|
756
|
+
const child = (0, node_child_process_1.execFile)(command, args, { timeout: 2000 }, (error, stdout, stderr) => {
|
|
757
|
+
if (error)
|
|
758
|
+
resolvePromise('');
|
|
759
|
+
else
|
|
760
|
+
resolvePromise(`${stdout || ''}${stderr || ''}`);
|
|
761
|
+
});
|
|
762
|
+
child.on('error', () => resolvePromise(''));
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
resolvePromise('');
|
|
766
|
+
}
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
function calculateCompositeScore(dimensions) {
|
|
770
|
+
const web3Score = dimensions.web3_safety.score;
|
|
771
|
+
if (web3Score === null || dimensions.web3_safety.na) {
|
|
772
|
+
return Math.round((dimensions.code_safety.score ?? 0) * 0.294 +
|
|
773
|
+
(dimensions.credential_safety.score ?? 0) * 0.294 +
|
|
774
|
+
(dimensions.network_exposure.score ?? 0) * 0.235 +
|
|
775
|
+
(dimensions.runtime_protection.score ?? 0) * 0.176);
|
|
776
|
+
}
|
|
777
|
+
return Math.round((dimensions.code_safety.score ?? 0) * 0.25 +
|
|
778
|
+
(dimensions.credential_safety.score ?? 0) * 0.25 +
|
|
779
|
+
(dimensions.network_exposure.score ?? 0) * 0.20 +
|
|
780
|
+
(dimensions.runtime_protection.score ?? 0) * 0.15 +
|
|
781
|
+
web3Score * 0.15);
|
|
782
|
+
}
|
|
783
|
+
async function generateCheckupHtml(report) {
|
|
784
|
+
const tempDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentguard-checkup-'));
|
|
785
|
+
const dataPath = (0, node_path_1.join)(tempDir, 'data.json');
|
|
786
|
+
(0, node_fs_1.writeFileSync)(dataPath, JSON.stringify(report, null, 2), 'utf8');
|
|
787
|
+
const scriptPath = process.env.AGENTGUARD_CHECKUP_REPORT_SCRIPT
|
|
788
|
+
? (0, node_path_1.resolve)(process.env.AGENTGUARD_CHECKUP_REPORT_SCRIPT)
|
|
789
|
+
: (0, node_path_1.resolve)(__dirname, '..', 'skills', 'agentguard', 'scripts', 'checkup-report.js');
|
|
790
|
+
if (!(0, node_fs_1.existsSync)(scriptPath)) {
|
|
791
|
+
throw new Error(`report generator not found at ${scriptPath}`);
|
|
792
|
+
}
|
|
793
|
+
return new Promise((resolvePromise, reject) => {
|
|
794
|
+
(0, node_child_process_1.execFile)('node', [scriptPath, '--file', dataPath], { timeout: 6000 }, (error, stdout, stderr) => {
|
|
795
|
+
if (error)
|
|
796
|
+
reject(new Error(stderr || error.message));
|
|
797
|
+
else
|
|
798
|
+
resolvePromise(stdout.trim().split(/\r?\n/).pop() || '');
|
|
799
|
+
});
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
function printHealthCheckupSummary(report, htmlPath) {
|
|
803
|
+
const totalFindings = Object.values(report.dimensions).reduce((sum, dim) => sum + dim.findings.length, 0);
|
|
804
|
+
console.log('AgentGuard Health Checkup');
|
|
805
|
+
console.log(`Overall Health Score: ${report.composite_score}/100 (Tier ${report.tier})`);
|
|
806
|
+
console.log(`Findings: ${totalFindings}`);
|
|
807
|
+
console.log(`Skills scanned: ${report.skills_scanned}`);
|
|
808
|
+
for (const [name, dim] of Object.entries(report.dimensions)) {
|
|
809
|
+
const score = dim.na || dim.score === null ? 'N/A' : `${dim.score}/100`;
|
|
810
|
+
console.log(`- ${name}: ${score} - ${dim.details}`);
|
|
811
|
+
}
|
|
812
|
+
if (htmlPath) {
|
|
813
|
+
console.log(`Full visual report: ${htmlPath}`);
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
console.log('Full visual report: unavailable (text summary shown above)');
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
function appendCheckupAudit(auditPath, report) {
|
|
820
|
+
const totalFindings = Object.values(report.dimensions).reduce((sum, dim) => sum + dim.findings.length, 0);
|
|
821
|
+
try {
|
|
822
|
+
(0, node_fs_1.appendFileSync)(auditPath, `${JSON.stringify({
|
|
823
|
+
timestamp: report.timestamp,
|
|
824
|
+
event: 'checkup',
|
|
825
|
+
composite_score: report.composite_score,
|
|
826
|
+
tier: report.tier,
|
|
827
|
+
checks: 5,
|
|
828
|
+
findings: totalFindings,
|
|
829
|
+
skills_scanned: report.skills_scanned,
|
|
830
|
+
})}\n`, { mode: 0o600 });
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
// Checkup should still succeed if audit logging is unavailable.
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
function buildHealthAnalysis(score, dimensions) {
|
|
837
|
+
const weak = Object.entries(dimensions)
|
|
838
|
+
.filter(([, dim]) => !dim.na && dim.score !== null && dim.score < 70)
|
|
839
|
+
.map(([name]) => name.replace(/_/g, ' '));
|
|
840
|
+
if (weak.length === 0) {
|
|
841
|
+
return `Overall posture is healthy at ${score}/100. AgentGuard did not find major weaknesses across the local skill, credential, network, and runtime checks.`;
|
|
842
|
+
}
|
|
843
|
+
return `Overall posture is ${score}/100. The areas needing attention are ${weak.join(', ')}; review the findings and fix the highest-severity items first.`;
|
|
844
|
+
}
|
|
845
|
+
function tierForScore(score) {
|
|
846
|
+
if (score >= 90)
|
|
847
|
+
return 'S';
|
|
848
|
+
if (score >= 70)
|
|
849
|
+
return 'A';
|
|
850
|
+
if (score >= 50)
|
|
851
|
+
return 'B';
|
|
852
|
+
return 'F';
|
|
853
|
+
}
|
|
854
|
+
function clampScore(score) {
|
|
855
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
856
|
+
}
|
|
857
|
+
function riskLevelToSeverity(risk) {
|
|
858
|
+
if (risk === 'critical')
|
|
859
|
+
return 'CRITICAL';
|
|
860
|
+
if (risk === 'high')
|
|
861
|
+
return 'HIGH';
|
|
862
|
+
if (risk === 'medium')
|
|
863
|
+
return 'MEDIUM';
|
|
864
|
+
return 'LOW';
|
|
865
|
+
}
|
|
421
866
|
function buildSubscribeSummary(options) {
|
|
422
867
|
const matched = options.results.reduce((acc, r) => acc + r.matchedArtifacts.length, 0);
|
|
423
|
-
const shouldNotify = options.supported
|
|
868
|
+
const shouldNotify = options.supported
|
|
869
|
+
&& options.hardFailures === 0
|
|
870
|
+
&& (options.quiet ? matched > 0 : options.fresh > 0);
|
|
424
871
|
const summary = {
|
|
425
872
|
supported: options.supported,
|
|
426
873
|
pulled: options.pulled,
|
|
@@ -434,14 +881,31 @@ function buildSubscribeSummary(options) {
|
|
|
434
881
|
installed: false,
|
|
435
882
|
},
|
|
436
883
|
};
|
|
437
|
-
if (shouldNotify) {
|
|
884
|
+
if (shouldNotify && options.quiet) {
|
|
438
885
|
summary.notification = {
|
|
439
886
|
title: `AgentGuard detected ${matched} threat-feed match${matched === 1 ? '' : 'es'}`,
|
|
440
887
|
body: formatThreatFeedNotification(options.results),
|
|
441
888
|
};
|
|
442
889
|
}
|
|
890
|
+
else if (shouldNotify) {
|
|
891
|
+
summary.notification = {
|
|
892
|
+
title: `AgentGuard found ${options.fresh} new threat-feed advisor${options.fresh === 1 ? 'y' : 'ies'}`,
|
|
893
|
+
body: formatNewAdvisoryNotification(options.freshAdvisories),
|
|
894
|
+
};
|
|
895
|
+
}
|
|
443
896
|
return summary;
|
|
444
897
|
}
|
|
898
|
+
function formatNewAdvisoryNotification(advisories) {
|
|
899
|
+
const lines = ['AgentGuard found new threat-feed advisories that need manual review:'];
|
|
900
|
+
for (const advisory of advisories.slice(0, 10)) {
|
|
901
|
+
lines.push(`- ${advisory.id} [${advisory.severity}] ${advisory.summary}`);
|
|
902
|
+
}
|
|
903
|
+
if (advisories.length > 10) {
|
|
904
|
+
lines.push(`- ... ${advisories.length - 10} more`);
|
|
905
|
+
}
|
|
906
|
+
lines.push('Run `agentguard subscribe --quiet` to execute the local self-check and report matches automatically.');
|
|
907
|
+
return lines.join('\n');
|
|
908
|
+
}
|
|
445
909
|
function formatThreatFeedNotification(results) {
|
|
446
910
|
const lines = ['AgentGuard threat-feed self-check found local matches:'];
|
|
447
911
|
for (const result of results) {
|