opena2a-cli 0.1.0 → 0.1.2

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.
Files changed (76) hide show
  1. package/README.md +280 -0
  2. package/dist/adapters/registry.js +1 -1
  3. package/dist/adapters/registry.js.map +1 -1
  4. package/dist/commands/init.d.ts.map +1 -1
  5. package/dist/commands/init.js +78 -3
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/protect.d.ts +2 -0
  8. package/dist/commands/protect.d.ts.map +1 -1
  9. package/dist/commands/protect.js +56 -10
  10. package/dist/commands/protect.js.map +1 -1
  11. package/dist/commands/runtime.d.ts +1 -1
  12. package/dist/commands/runtime.js +5 -5
  13. package/dist/commands/runtime.js.map +1 -1
  14. package/dist/commands/self-register.js +6 -6
  15. package/dist/commands/self-register.js.map +1 -1
  16. package/dist/commands/shield.d.ts +36 -0
  17. package/dist/commands/shield.d.ts.map +1 -0
  18. package/dist/commands/shield.js +834 -0
  19. package/dist/commands/shield.js.map +1 -0
  20. package/dist/commands/verify.js +1 -1
  21. package/dist/commands/verify.js.map +1 -1
  22. package/dist/index.js +29 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/shield/detect.d.ts +18 -0
  25. package/dist/shield/detect.d.ts.map +1 -0
  26. package/dist/shield/detect.js +402 -0
  27. package/dist/shield/detect.js.map +1 -0
  28. package/dist/shield/events.d.ts +65 -0
  29. package/dist/shield/events.d.ts.map +1 -0
  30. package/dist/shield/events.js +342 -0
  31. package/dist/shield/events.js.map +1 -0
  32. package/dist/shield/init.d.ts +22 -0
  33. package/dist/shield/init.d.ts.map +1 -0
  34. package/dist/shield/init.js +290 -0
  35. package/dist/shield/init.js.map +1 -0
  36. package/dist/shield/integrity.d.ts +75 -0
  37. package/dist/shield/integrity.d.ts.map +1 -0
  38. package/dist/shield/integrity.js +435 -0
  39. package/dist/shield/integrity.js.map +1 -0
  40. package/dist/shield/llm-backend.d.ts +36 -0
  41. package/dist/shield/llm-backend.d.ts.map +1 -0
  42. package/dist/shield/llm-backend.js +145 -0
  43. package/dist/shield/llm-backend.js.map +1 -0
  44. package/dist/shield/llm.d.ts +116 -0
  45. package/dist/shield/llm.d.ts.map +1 -0
  46. package/dist/shield/llm.js +536 -0
  47. package/dist/shield/llm.js.map +1 -0
  48. package/dist/shield/policy.d.ts +70 -0
  49. package/dist/shield/policy.d.ts.map +1 -0
  50. package/dist/shield/policy.js +399 -0
  51. package/dist/shield/policy.js.map +1 -0
  52. package/dist/shield/session.d.ts +63 -0
  53. package/dist/shield/session.d.ts.map +1 -0
  54. package/dist/shield/session.js +242 -0
  55. package/dist/shield/session.js.map +1 -0
  56. package/dist/shield/signing.d.ts +41 -0
  57. package/dist/shield/signing.d.ts.map +1 -0
  58. package/dist/shield/signing.js +161 -0
  59. package/dist/shield/signing.js.map +1 -0
  60. package/dist/shield/status.d.ts +4 -0
  61. package/dist/shield/status.d.ts.map +1 -0
  62. package/dist/shield/status.js +241 -0
  63. package/dist/shield/status.js.map +1 -0
  64. package/dist/shield/types.d.ts +398 -0
  65. package/dist/shield/types.d.ts.map +1 -0
  66. package/dist/shield/types.js +31 -0
  67. package/dist/shield/types.js.map +1 -0
  68. package/dist/util/drift-liveness.d.ts +37 -0
  69. package/dist/util/drift-liveness.d.ts.map +1 -0
  70. package/dist/util/drift-liveness.js +114 -0
  71. package/dist/util/drift-liveness.js.map +1 -0
  72. package/dist/util/drift-verification.d.ts +60 -0
  73. package/dist/util/drift-verification.d.ts.map +1 -0
  74. package/dist/util/drift-verification.js +457 -0
  75. package/dist/util/drift-verification.js.map +1 -0
  76. package/package.json +4 -2
@@ -0,0 +1,834 @@
1
+ "use strict";
2
+ /**
3
+ * opena2a shield -- Unified security orchestration.
4
+ *
5
+ * Subcommands:
6
+ * - init: Full environment scan, policy generation, shell hooks
7
+ * - status: Product availability, policy mode, integrity state
8
+ * - log: Query the tamper-evident event log
9
+ * - selfcheck: Run integrity checks (alias: check)
10
+ * - policy: Show loaded policy summary
11
+ * - evaluate: Evaluate an action against the policy
12
+ * - recover: Exit lockdown mode
13
+ * - report: Generate a security posture report
14
+ * - session: Show current AI coding assistant session identity
15
+ * - suggest: LLM-powered policy suggestions from observed behavior
16
+ * - explain: LLM-powered anomaly explanations for events
17
+ * - triage: LLM-powered incident classification and response
18
+ */
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.shield = shield;
21
+ const colors_js_1 = require("../util/colors.js");
22
+ // --- Core dispatcher ---
23
+ async function shield(options) {
24
+ switch (options.subcommand) {
25
+ case 'init':
26
+ return handleInit(options);
27
+ case 'status':
28
+ return handleStatus(options);
29
+ case 'log':
30
+ return handleLog(options);
31
+ case 'selfcheck':
32
+ case 'check':
33
+ return handleSelfcheck(options);
34
+ case 'policy':
35
+ return handlePolicy(options);
36
+ case 'evaluate':
37
+ return handleEvaluate(options);
38
+ case 'recover':
39
+ return handleRecover(options);
40
+ case 'report':
41
+ return handleReport(options);
42
+ case 'session':
43
+ return handleSession(options);
44
+ case 'suggest':
45
+ return handleSuggest(options);
46
+ case 'explain':
47
+ return handleExplain(options);
48
+ case 'triage':
49
+ return handleTriage(options);
50
+ default:
51
+ process.stderr.write((0, colors_js_1.red)(`Unknown subcommand: ${options.subcommand}\n`));
52
+ process.stderr.write('Usage: opena2a shield <init|status|log|selfcheck|policy|evaluate|recover|report|session|suggest|explain|triage>\n');
53
+ return 1;
54
+ }
55
+ }
56
+ // --- Subcommand handlers ---
57
+ async function handleInit(options) {
58
+ const { shieldInit } = await import('../shield/init.js');
59
+ const { exitCode } = await shieldInit({
60
+ targetDir: options.dir,
61
+ ci: options.ci,
62
+ format: options.format,
63
+ verbose: options.verbose,
64
+ });
65
+ return exitCode;
66
+ }
67
+ async function handleStatus(options) {
68
+ const { getShieldStatus, formatStatus } = await import('../shield/status.js');
69
+ const format = (options.format === 'json' ? 'json' : 'text');
70
+ const status = getShieldStatus(options.dir);
71
+ const output = formatStatus(status, format);
72
+ process.stdout.write(output + '\n');
73
+ return (status.integrityStatus === 'lockdown' || status.integrityStatus === 'compromised') ? 1 : 0;
74
+ }
75
+ async function handleLog(options) {
76
+ const { readEvents } = await import('../shield/events.js');
77
+ const isJson = options.format === 'json';
78
+ const count = options.count ? parseInt(options.count, 10) : 20;
79
+ const events = readEvents({
80
+ count,
81
+ source: options.source,
82
+ severity: options.severity,
83
+ agent: options.agent,
84
+ since: options.since,
85
+ category: options.category,
86
+ });
87
+ if (isJson) {
88
+ process.stdout.write(JSON.stringify(events, null, 2) + '\n');
89
+ return 0;
90
+ }
91
+ if (events.length === 0) {
92
+ process.stdout.write((0, colors_js_1.dim)('No events found.\n'));
93
+ return 0;
94
+ }
95
+ for (const event of events) {
96
+ const ts = event.timestamp;
97
+ const sev = colorSeverity(event.severity);
98
+ const action = event.action;
99
+ const target = event.target;
100
+ const outcome = event.outcome;
101
+ process.stdout.write(`[${ts}] [${sev}] ${action} -> ${target} (${outcome})\n`);
102
+ }
103
+ return 0;
104
+ }
105
+ async function handleSelfcheck(options) {
106
+ const { runIntegrityChecks } = await import('../shield/integrity.js');
107
+ const isJson = options.format === 'json';
108
+ const shell = process.env.SHELL?.includes('zsh') ? 'zsh'
109
+ : process.env.SHELL?.includes('bash') ? 'bash'
110
+ : undefined;
111
+ const state = runIntegrityChecks({ shell });
112
+ if (isJson) {
113
+ process.stdout.write(JSON.stringify(state, null, 2) + '\n');
114
+ }
115
+ else {
116
+ process.stdout.write((0, colors_js_1.bold)('Shield Integrity Check\n'));
117
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
118
+ for (const check of state.checks) {
119
+ const icon = check.status === 'pass' ? (0, colors_js_1.green)('PASS')
120
+ : check.status === 'warn' ? (0, colors_js_1.yellow)('WARN')
121
+ : (0, colors_js_1.red)('FAIL');
122
+ process.stdout.write(` ${icon} ${check.name.padEnd(22)} ${(0, colors_js_1.dim)(check.detail)}\n`);
123
+ }
124
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
125
+ const statusLabel = state.status === 'healthy' ? (0, colors_js_1.green)(state.status.toUpperCase())
126
+ : state.status === 'degraded' ? (0, colors_js_1.yellow)(state.status.toUpperCase())
127
+ : (0, colors_js_1.red)(state.status.toUpperCase());
128
+ process.stdout.write(` Overall: ${statusLabel}\n`);
129
+ }
130
+ return (state.status === 'compromised' || state.status === 'lockdown') ? 1 : 0;
131
+ }
132
+ async function handlePolicy(options) {
133
+ const { loadPolicy } = await import('../shield/policy.js');
134
+ const isJson = options.format === 'json';
135
+ const policy = loadPolicy(options.dir);
136
+ if (!policy) {
137
+ if (isJson) {
138
+ process.stdout.write(JSON.stringify({ error: 'No policy loaded' }, null, 2) + '\n');
139
+ }
140
+ else {
141
+ process.stderr.write((0, colors_js_1.yellow)('No policy loaded. Run: opena2a shield init\n'));
142
+ }
143
+ return 1;
144
+ }
145
+ if (isJson) {
146
+ process.stdout.write(JSON.stringify(policy, null, 2) + '\n');
147
+ return 0;
148
+ }
149
+ process.stdout.write((0, colors_js_1.bold)('Shield Policy\n'));
150
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(40)) + '\n');
151
+ process.stdout.write(` Mode: ${(0, colors_js_1.cyan)(policy.mode)}\n`);
152
+ process.stdout.write(` Process deny: ${policy.default.processes.deny.length} rules\n`);
153
+ process.stdout.write(` Process allow: ${policy.default.processes.allow.length} rules\n`);
154
+ process.stdout.write(` Cred deny: ${policy.default.credentials.deny.length} rules\n`);
155
+ process.stdout.write(` Network allow: ${policy.default.network.allow.length} rules\n`);
156
+ process.stdout.write(` FS deny: ${policy.default.filesystem.deny.length} rules\n`);
157
+ process.stdout.write(` MCP allow: ${policy.default.mcpServers.allow.length} rules\n`);
158
+ const agentCount = Object.keys(policy.agents).length;
159
+ if (agentCount > 0) {
160
+ process.stdout.write(` Agent overrides: ${agentCount}\n`);
161
+ }
162
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(40)) + '\n');
163
+ return 0;
164
+ }
165
+ async function handleEvaluate(options) {
166
+ const { loadPolicy, evaluatePolicy } = await import('../shield/policy.js');
167
+ const isJson = options.format === 'json';
168
+ const policy = loadPolicy(options.dir);
169
+ if (!policy) {
170
+ if (isJson) {
171
+ process.stdout.write(JSON.stringify({ error: 'No policy loaded' }, null, 2) + '\n');
172
+ }
173
+ else {
174
+ process.stderr.write((0, colors_js_1.yellow)('No policy loaded. Run: opena2a shield init\n'));
175
+ }
176
+ return 1;
177
+ }
178
+ const category = options.category ?? 'processes';
179
+ const agent = options.agent ?? null;
180
+ const target = '';
181
+ const decision = evaluatePolicy(policy, agent, category, target);
182
+ if (isJson) {
183
+ process.stdout.write(JSON.stringify(decision, null, 2) + '\n');
184
+ }
185
+ else {
186
+ const outcomeLabel = decision.outcome === 'allowed' ? (0, colors_js_1.green)('ALLOWED')
187
+ : decision.outcome === 'blocked' ? (0, colors_js_1.red)('BLOCKED')
188
+ : (0, colors_js_1.yellow)('MONITORED');
189
+ process.stdout.write(`${outcomeLabel} rule=${decision.rule}\n`);
190
+ }
191
+ return decision.outcome === 'blocked' ? 1 : 0;
192
+ }
193
+ async function handleRecover(options) {
194
+ const { isLockdown, exitLockdown, getLockdownReason } = await import('../shield/integrity.js');
195
+ const isJson = options.format === 'json';
196
+ if (!isLockdown()) {
197
+ if (isJson) {
198
+ process.stdout.write(JSON.stringify({ status: 'not_in_lockdown' }, null, 2) + '\n');
199
+ }
200
+ else {
201
+ process.stdout.write((0, colors_js_1.green)('System is not in lockdown.\n'));
202
+ }
203
+ return 0;
204
+ }
205
+ const reason = getLockdownReason();
206
+ if (options.verify) {
207
+ const { runIntegrityChecks } = await import('../shield/integrity.js');
208
+ const shell = process.env.SHELL?.includes('zsh') ? 'zsh'
209
+ : process.env.SHELL?.includes('bash') ? 'bash'
210
+ : undefined;
211
+ exitLockdown();
212
+ const state = runIntegrityChecks({ shell });
213
+ if (state.status === 'compromised') {
214
+ const { enterLockdown } = await import('../shield/integrity.js');
215
+ enterLockdown(reason ?? 'Verification failed');
216
+ if (isJson) {
217
+ process.stdout.write(JSON.stringify({ status: 'verification_failed', state }, null, 2) + '\n');
218
+ }
219
+ else {
220
+ process.stderr.write((0, colors_js_1.red)('Verification failed. System remains in lockdown.\n'));
221
+ for (const check of state.checks) {
222
+ if (check.status === 'fail') {
223
+ process.stderr.write(` ${(0, colors_js_1.red)('FAIL')} ${check.name}: ${check.detail}\n`);
224
+ }
225
+ }
226
+ }
227
+ return 1;
228
+ }
229
+ if (isJson) {
230
+ process.stdout.write(JSON.stringify({ status: 'recovered', verified: true }, null, 2) + '\n');
231
+ }
232
+ else {
233
+ process.stdout.write((0, colors_js_1.green)('Lockdown lifted after successful verification.\n'));
234
+ }
235
+ return 0;
236
+ }
237
+ exitLockdown();
238
+ if (isJson) {
239
+ process.stdout.write(JSON.stringify({ status: 'recovered', previousReason: reason }, null, 2) + '\n');
240
+ }
241
+ else {
242
+ process.stdout.write((0, colors_js_1.green)('Lockdown lifted.\n'));
243
+ if (reason) {
244
+ process.stdout.write((0, colors_js_1.dim)(`Previous reason: ${reason}\n`));
245
+ }
246
+ }
247
+ return 0;
248
+ }
249
+ // --- Report ---
250
+ async function handleReport(options) {
251
+ const { readEvents } = await import('../shield/events.js');
252
+ const isJson = options.format === 'json';
253
+ const since = options.since ?? '7d';
254
+ const events = readEvents({ since });
255
+ const total = events.length;
256
+ const bySeverity = {};
257
+ const bySource = {};
258
+ const byAgent = {};
259
+ const byAction = {};
260
+ const byOutcome = {};
261
+ for (const event of events) {
262
+ bySeverity[event.severity] = (bySeverity[event.severity] ?? 0) + 1;
263
+ bySource[event.source] = (bySource[event.source] ?? 0) + 1;
264
+ byOutcome[event.outcome] = (byOutcome[event.outcome] ?? 0) + 1;
265
+ const agentKey = event.agent ?? 'unknown';
266
+ byAgent[agentKey] = (byAgent[agentKey] ?? 0) + 1;
267
+ byAction[event.action] = (byAction[event.action] ?? 0) + 1;
268
+ }
269
+ const topN = (record, n) => Object.entries(record)
270
+ .sort((a, b) => b[1] - a[1])
271
+ .slice(0, n)
272
+ .map(([name, count]) => ({ name, count }));
273
+ const topAgents = topN(byAgent, 10);
274
+ const topActions = topN(byAction, 10);
275
+ if (isJson) {
276
+ const data = {
277
+ periodSince: since,
278
+ totalEvents: total,
279
+ bySeverity,
280
+ bySource,
281
+ byOutcome,
282
+ topAgents,
283
+ topActions,
284
+ };
285
+ if (options.analyze) {
286
+ const narrative = await buildNarrative(events, since, bySeverity, byOutcome, byAgent, topActions);
287
+ if (narrative) {
288
+ data.narrative = narrative;
289
+ }
290
+ }
291
+ process.stdout.write(JSON.stringify(data, null, 2) + '\n');
292
+ return 0;
293
+ }
294
+ process.stdout.write((0, colors_js_1.bold)('Shield Security Report') + '\n');
295
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
296
+ process.stdout.write(` Period: since ${(0, colors_js_1.cyan)(since)}\n`);
297
+ process.stdout.write(` Total events: ${(0, colors_js_1.bold)(String(total))}\n`);
298
+ process.stdout.write('\n');
299
+ process.stdout.write((0, colors_js_1.bold)(' Severity Breakdown') + '\n');
300
+ const severityOrder = ['critical', 'high', 'medium', 'low', 'info'];
301
+ for (const sev of severityOrder) {
302
+ const count = bySeverity[sev];
303
+ if (count === undefined || count === 0)
304
+ continue;
305
+ process.stdout.write(` ${colorSeverity(sev).padEnd(20)} ${String(count)}\n`);
306
+ }
307
+ if (Object.keys(bySeverity).length === 0) {
308
+ process.stdout.write((0, colors_js_1.dim)(' (none)\n'));
309
+ }
310
+ process.stdout.write('\n');
311
+ process.stdout.write((0, colors_js_1.bold)(' Events by Source') + '\n');
312
+ for (const { name, count } of topN(bySource, 10)) {
313
+ process.stdout.write(` ${name.padEnd(20)} ${String(count)}\n`);
314
+ }
315
+ if (Object.keys(bySource).length === 0) {
316
+ process.stdout.write((0, colors_js_1.dim)(' (none)\n'));
317
+ }
318
+ process.stdout.write('\n');
319
+ process.stdout.write((0, colors_js_1.bold)(' Top Agents') + '\n');
320
+ for (const { name, count } of topAgents) {
321
+ process.stdout.write(` ${name.padEnd(20)} ${String(count)} events\n`);
322
+ }
323
+ if (topAgents.length === 0) {
324
+ process.stdout.write((0, colors_js_1.dim)(' (none)\n'));
325
+ }
326
+ process.stdout.write('\n');
327
+ process.stdout.write((0, colors_js_1.bold)(' Top Actions') + '\n');
328
+ for (const { name, count } of topActions) {
329
+ process.stdout.write(` ${name.padEnd(30)} ${String(count)}\n`);
330
+ }
331
+ if (topActions.length === 0) {
332
+ process.stdout.write((0, colors_js_1.dim)(' (none)\n'));
333
+ }
334
+ if (options.analyze) {
335
+ process.stdout.write('\n');
336
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
337
+ process.stdout.write((0, colors_js_1.bold)(' AI Analysis') + '\n');
338
+ const narrative = await buildNarrative(events, since, bySeverity, byOutcome, byAgent, topActions);
339
+ if (narrative) {
340
+ process.stdout.write('\n');
341
+ process.stdout.write(` ${(0, colors_js_1.bold)('Summary')}\n`);
342
+ process.stdout.write(` ${narrative.summary}\n`);
343
+ if (narrative.highlights.length > 0) {
344
+ process.stdout.write('\n');
345
+ process.stdout.write(` ${(0, colors_js_1.green)('Highlights')}\n`);
346
+ for (const h of narrative.highlights) {
347
+ process.stdout.write(` - ${h}\n`);
348
+ }
349
+ }
350
+ if (narrative.concerns.length > 0) {
351
+ process.stdout.write('\n');
352
+ process.stdout.write(` ${(0, colors_js_1.yellow)('Concerns')}\n`);
353
+ for (const c of narrative.concerns) {
354
+ process.stdout.write(` - ${c}\n`);
355
+ }
356
+ }
357
+ if (narrative.recommendations.length > 0) {
358
+ process.stdout.write('\n');
359
+ process.stdout.write(` ${(0, colors_js_1.cyan)('Recommendations')}\n`);
360
+ for (const r of narrative.recommendations) {
361
+ process.stdout.write(` - ${r}\n`);
362
+ }
363
+ }
364
+ }
365
+ else {
366
+ process.stdout.write((0, colors_js_1.dim)(' LLM analysis unavailable (no API key or backend configured).\n'));
367
+ }
368
+ }
369
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
370
+ return 0;
371
+ }
372
+ /**
373
+ * Build a WeeklyReport from aggregated event data and call generateNarrative().
374
+ * Returns the narrative or null if LLM is unavailable.
375
+ */
376
+ async function buildNarrative(events, since, bySeverity, byOutcome, byAgent, topActions) {
377
+ const { generateNarrative } = await import('../shield/llm.js');
378
+ const { hostname } = await import('node:os');
379
+ const now = new Date();
380
+ const sinceMatch = since.match(/^(\d+)([dwm])$/);
381
+ let periodStart;
382
+ if (sinceMatch) {
383
+ const amount = parseInt(sinceMatch[1], 10);
384
+ const unit = sinceMatch[2];
385
+ const msPerDay = 24 * 60 * 60 * 1000;
386
+ const daysAgo = unit === 'd' ? amount : unit === 'w' ? amount * 7 : amount * 30;
387
+ periodStart = new Date(now.getTime() - daysAgo * msPerDay);
388
+ }
389
+ else {
390
+ const parsed = new Date(since);
391
+ periodStart = isNaN(parsed.getTime()) ? new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) : parsed;
392
+ }
393
+ const agentSummaries = {};
394
+ const sessionIds = new Set();
395
+ for (const event of events) {
396
+ const agentKey = event.agent ?? 'unknown';
397
+ if (!agentSummaries[agentKey]) {
398
+ agentSummaries[agentKey] = {
399
+ sessions: 0,
400
+ actions: 0,
401
+ firstSeen: event.timestamp,
402
+ lastSeen: event.timestamp,
403
+ topActions: [],
404
+ };
405
+ }
406
+ const summary = agentSummaries[agentKey];
407
+ summary.actions += 1;
408
+ if (event.timestamp < summary.firstSeen)
409
+ summary.firstSeen = event.timestamp;
410
+ if (event.timestamp > summary.lastSeen)
411
+ summary.lastSeen = event.timestamp;
412
+ if (event.sessionId)
413
+ sessionIds.add(event.sessionId);
414
+ }
415
+ const agentActionCounts = {};
416
+ for (const event of events) {
417
+ const agentKey = event.agent ?? 'unknown';
418
+ if (!agentActionCounts[agentKey])
419
+ agentActionCounts[agentKey] = {};
420
+ agentActionCounts[agentKey][event.action] = (agentActionCounts[agentKey][event.action] ?? 0) + 1;
421
+ }
422
+ for (const [agent, actions] of Object.entries(agentActionCounts)) {
423
+ if (agentSummaries[agent]) {
424
+ agentSummaries[agent].topActions = Object.entries(actions)
425
+ .sort((a, b) => b[1] - a[1])
426
+ .slice(0, 5)
427
+ .map(([action, count]) => ({ action, count }));
428
+ }
429
+ }
430
+ const agentSessions = {};
431
+ for (const event of events) {
432
+ const agentKey = event.agent ?? 'unknown';
433
+ if (!agentSessions[agentKey])
434
+ agentSessions[agentKey] = new Set();
435
+ if (event.sessionId)
436
+ agentSessions[agentKey].add(event.sessionId);
437
+ }
438
+ for (const [agent, sessions] of Object.entries(agentSessions)) {
439
+ if (agentSummaries[agent]) {
440
+ agentSummaries[agent].sessions = sessions.size || 1;
441
+ }
442
+ }
443
+ const violations = [];
444
+ const violationMap = {};
445
+ for (const event of events) {
446
+ if (event.outcome === 'blocked' || event.severity === 'high' || event.severity === 'critical') {
447
+ const key = `${event.action}:${event.target}:${event.agent ?? 'unknown'}`;
448
+ if (!violationMap[key]) {
449
+ violationMap[key] = { count: 0, event };
450
+ }
451
+ violationMap[key].count += 1;
452
+ }
453
+ }
454
+ for (const [, { count, event }] of Object.entries(violationMap)) {
455
+ violations.push({
456
+ action: event.action,
457
+ target: event.target,
458
+ agent: event.agent ?? 'unknown',
459
+ count,
460
+ severity: event.severity,
461
+ recommendation: event.outcome === 'blocked' ? 'Already blocked by policy' : 'Review and consider blocking',
462
+ });
463
+ }
464
+ violations.sort((a, b) => b.count - a.count);
465
+ let credAccessAttempts = 0;
466
+ const credProviders = {};
467
+ const credNames = new Set();
468
+ for (const event of events) {
469
+ if (event.source === 'secretless' || event.category.includes('credential')) {
470
+ credAccessAttempts += 1;
471
+ credNames.add(event.target);
472
+ credProviders[event.source] = (credProviders[event.source] ?? 0) + 1;
473
+ }
474
+ }
475
+ let packagesInstalled = 0;
476
+ let advisoriesFound = 0;
477
+ let blockedInstalls = 0;
478
+ for (const event of events) {
479
+ if (event.source === 'registry' || event.category.includes('supply-chain')) {
480
+ packagesInstalled += 1;
481
+ if (event.severity === 'high' || event.severity === 'critical')
482
+ advisoriesFound += 1;
483
+ if (event.outcome === 'blocked')
484
+ blockedInstalls += 1;
485
+ }
486
+ }
487
+ const criticalCount = bySeverity['critical'] ?? 0;
488
+ const highCount = bySeverity['high'] ?? 0;
489
+ const mediumCount = bySeverity['medium'] ?? 0;
490
+ const blockedCount = byOutcome['blocked'] ?? 0;
491
+ let score = 100;
492
+ score -= criticalCount * 15;
493
+ score -= highCount * 8;
494
+ score -= mediumCount * 3;
495
+ score += blockedCount * 2;
496
+ score = Math.max(0, Math.min(100, score));
497
+ const grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : score >= 60 ? 'D' : 'F';
498
+ const report = {
499
+ version: 1,
500
+ generatedAt: now.toISOString(),
501
+ periodStart: periodStart.toISOString(),
502
+ periodEnd: now.toISOString(),
503
+ hostname: hostname(),
504
+ agentActivity: {
505
+ totalSessions: sessionIds.size || (events.length > 0 ? 1 : 0),
506
+ totalActions: events.length,
507
+ byAgent: agentSummaries,
508
+ },
509
+ policyEvaluation: {
510
+ monitored: byOutcome['monitored'] ?? 0,
511
+ wouldBlock: 0,
512
+ blocked: blockedCount,
513
+ topViolations: violations.slice(0, 5),
514
+ },
515
+ credentialExposure: {
516
+ accessAttempts: credAccessAttempts,
517
+ uniqueCredentials: credNames.size,
518
+ byProvider: credProviders,
519
+ recommendations: [],
520
+ },
521
+ supplyChain: {
522
+ packagesInstalled,
523
+ advisoriesFound,
524
+ blockedInstalls,
525
+ lowTrustPackages: [],
526
+ },
527
+ configIntegrity: {
528
+ filesMonitored: 0,
529
+ tamperedFiles: [],
530
+ signatureStatus: 'unsigned',
531
+ },
532
+ runtimeProtection: {
533
+ arpActive: false,
534
+ processesSpawned: 0,
535
+ networkConnections: 0,
536
+ anomalies: 0,
537
+ },
538
+ posture: {
539
+ score,
540
+ grade,
541
+ factors: [
542
+ { name: 'severity', score: Math.max(0, 100 - criticalCount * 15 - highCount * 8), weight: 0.4, detail: `${criticalCount} critical, ${highCount} high` },
543
+ { name: 'enforcement', score: blockedCount > 0 ? 80 : 50, weight: 0.3, detail: `${blockedCount} blocked` },
544
+ { name: 'coverage', score: Object.keys(byAgent).length > 0 ? 70 : 30, weight: 0.3, detail: `${Object.keys(byAgent).length} agents monitored` },
545
+ ],
546
+ trend: null,
547
+ comparative: null,
548
+ },
549
+ };
550
+ return generateNarrative(report);
551
+ }
552
+ // --- Session ---
553
+ async function handleSession(options) {
554
+ const { identifySession, collectSignals } = await import('../shield/session.js');
555
+ const isJson = options.format === 'json';
556
+ const session = identifySession();
557
+ if (!session) {
558
+ if (isJson) {
559
+ process.stdout.write(JSON.stringify({ detected: false }, null, 2) + '\n');
560
+ }
561
+ else {
562
+ process.stdout.write((0, colors_js_1.dim)('No AI coding assistant session detected.\n'));
563
+ }
564
+ return 0;
565
+ }
566
+ const { isSessionExpired } = await import('../shield/session.js');
567
+ const expired = isSessionExpired(session);
568
+ if (isJson) {
569
+ process.stdout.write(JSON.stringify({ detected: true, expired, ...session }, null, 2) + '\n');
570
+ return 0;
571
+ }
572
+ process.stdout.write((0, colors_js_1.bold)('Shield Session\n'));
573
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(40)) + '\n');
574
+ process.stdout.write(` Agent: ${(0, colors_js_1.cyan)(session.agent)}\n`);
575
+ process.stdout.write(` Confidence: ${session.confidence.toFixed(2)}\n`);
576
+ process.stdout.write(` Session ID: ${(0, colors_js_1.dim)(session.sessionId)}\n`);
577
+ process.stdout.write(` Signals: ${session.signals.length} detected\n`);
578
+ process.stdout.write(` Expired: ${expired ? (0, colors_js_1.yellow)('yes') : (0, colors_js_1.green)('no')}\n`);
579
+ process.stdout.write(` Started: ${(0, colors_js_1.dim)(session.startedAt)}\n`);
580
+ process.stdout.write(` Last seen: ${(0, colors_js_1.dim)(session.lastSeenAt)}\n`);
581
+ if (options.verbose) {
582
+ const signals = collectSignals();
583
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(40)) + '\n');
584
+ process.stdout.write((0, colors_js_1.bold)(' Raw signals:\n'));
585
+ for (const sig of signals) {
586
+ process.stdout.write(` ${(0, colors_js_1.dim)(sig.type.padEnd(8))} ${sig.name.padEnd(24)} ${sig.value} ${(0, colors_js_1.dim)(`(${sig.confidence.toFixed(2)})`)}\n`);
587
+ }
588
+ }
589
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(40)) + '\n');
590
+ return 0;
591
+ }
592
+ // --- LLM intelligence handlers ---
593
+ async function handleSuggest(options) {
594
+ const { checkLlmAvailable, suggestPolicy } = await import('../shield/llm.js');
595
+ const { readEvents } = await import('../shield/events.js');
596
+ const isJson = options.format === 'json';
597
+ const { backend } = await checkLlmAvailable();
598
+ if (backend === 'none') {
599
+ process.stderr.write((0, colors_js_1.yellow)('LLM intelligence is not available.\n'));
600
+ process.stderr.write('Enable it with: opena2a config llm on\n');
601
+ return 1;
602
+ }
603
+ const events = readEvents({ count: 100, agent: options.agent });
604
+ if (events.length === 0) {
605
+ process.stdout.write((0, colors_js_1.dim)('No events found. Run shield init and use your tools to generate events.\n'));
606
+ return 0;
607
+ }
608
+ const agentName = options.agent ?? events[0].agent ?? 'unknown';
609
+ const sessionIds = new Set(events.map(e => e.sessionId).filter(Boolean));
610
+ const processCounts = {};
611
+ const credentialCounts = {};
612
+ const filePathCounts = {};
613
+ const networkHostCounts = {};
614
+ for (const event of events) {
615
+ if (event.category === 'process' || event.category === 'processes') {
616
+ processCounts[event.target] = (processCounts[event.target] ?? 0) + 1;
617
+ }
618
+ else if (event.category === 'credential' || event.category === 'credentials') {
619
+ credentialCounts[event.target] = (credentialCounts[event.target] ?? 0) + 1;
620
+ }
621
+ else if (event.category === 'filesystem') {
622
+ filePathCounts[event.target] = (filePathCounts[event.target] ?? 0) + 1;
623
+ }
624
+ else if (event.category === 'network') {
625
+ networkHostCounts[event.target] = (networkHostCounts[event.target] ?? 0) + 1;
626
+ }
627
+ }
628
+ const toSorted = (counts) => Object.entries(counts)
629
+ .map(([name, count]) => ({ name, count }))
630
+ .sort((a, b) => b.count - a.count);
631
+ const toSortedPaths = (counts) => Object.entries(counts)
632
+ .map(([path, count]) => ({ path, count }))
633
+ .sort((a, b) => b.count - a.count);
634
+ const toSortedHosts = (counts) => Object.entries(counts)
635
+ .map(([host, count]) => ({ host, count }))
636
+ .sort((a, b) => b.count - a.count);
637
+ const suggestion = await suggestPolicy(agentName, {
638
+ totalActions: events.length,
639
+ totalSessions: sessionIds.size || 1,
640
+ topProcesses: toSorted(processCounts),
641
+ topCredentials: toSorted(credentialCounts),
642
+ topFilePaths: toSortedPaths(filePathCounts),
643
+ topNetworkHosts: toSortedHosts(networkHostCounts),
644
+ });
645
+ if (!suggestion) {
646
+ process.stdout.write((0, colors_js_1.dim)('LLM analysis unavailable. The backend may be unreachable.\n'));
647
+ return 0;
648
+ }
649
+ if (isJson) {
650
+ process.stdout.write(JSON.stringify(suggestion, null, 2) + '\n');
651
+ return 0;
652
+ }
653
+ process.stdout.write((0, colors_js_1.bold)('Policy Suggestion') + (0, colors_js_1.dim)(` (confidence: ${Math.round(suggestion.confidence * 100)}%)`) + '\n');
654
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
655
+ process.stdout.write((0, colors_js_1.dim)(`Based on ${suggestion.basedOnActions} actions across ${suggestion.basedOnSessions} sessions for agent "${suggestion.agent}"\n\n`));
656
+ if (suggestion.rules.processes) {
657
+ if (suggestion.rules.processes.deny?.length) {
658
+ process.stdout.write((0, colors_js_1.red)(' Deny processes:\n'));
659
+ for (const proc of suggestion.rules.processes.deny) {
660
+ process.stdout.write(` - ${proc}\n`);
661
+ }
662
+ }
663
+ if (suggestion.rules.processes.allow?.length) {
664
+ process.stdout.write((0, colors_js_1.green)(' Allow processes:\n'));
665
+ for (const proc of suggestion.rules.processes.allow) {
666
+ process.stdout.write(` + ${proc}\n`);
667
+ }
668
+ }
669
+ }
670
+ if (suggestion.rules.credentials?.deny?.length) {
671
+ process.stdout.write((0, colors_js_1.red)(' Deny credentials:\n'));
672
+ for (const cred of suggestion.rules.credentials.deny) {
673
+ process.stdout.write(` - ${cred}\n`);
674
+ }
675
+ }
676
+ if (suggestion.rules.filesystem?.deny?.length) {
677
+ process.stdout.write((0, colors_js_1.red)(' Deny filesystem:\n'));
678
+ for (const p of suggestion.rules.filesystem.deny) {
679
+ process.stdout.write(` - ${p}\n`);
680
+ }
681
+ }
682
+ if (suggestion.rules.network?.deny?.length) {
683
+ process.stdout.write((0, colors_js_1.red)(' Deny network:\n'));
684
+ for (const host of suggestion.rules.network.deny) {
685
+ process.stdout.write(` - ${host}\n`);
686
+ }
687
+ }
688
+ process.stdout.write('\n' + (0, colors_js_1.dim)('Reasoning: ') + suggestion.reasoning + '\n');
689
+ return 0;
690
+ }
691
+ async function handleExplain(options) {
692
+ const { checkLlmAvailable, explainAnomaly } = await import('../shield/llm.js');
693
+ const { readEvents } = await import('../shield/events.js');
694
+ const isJson = options.format === 'json';
695
+ const { backend } = await checkLlmAvailable();
696
+ if (backend === 'none') {
697
+ process.stderr.write((0, colors_js_1.yellow)('LLM intelligence is not available.\n'));
698
+ process.stderr.write('Enable it with: opena2a config llm on\n');
699
+ return 1;
700
+ }
701
+ const count = options.count ? parseInt(options.count, 10) : 1;
702
+ const events = readEvents({
703
+ count,
704
+ severity: options.severity,
705
+ agent: options.agent,
706
+ });
707
+ if (events.length === 0) {
708
+ process.stdout.write((0, colors_js_1.dim)('No events found matching the filters.\n'));
709
+ return 0;
710
+ }
711
+ const agentName = options.agent ?? events[0].agent ?? 'unknown';
712
+ const allAgentEvents = readEvents({ count: 100, agent: agentName });
713
+ const actionCounts = {};
714
+ for (const e of allAgentEvents) {
715
+ const key = `${e.action} -> ${e.target}`;
716
+ actionCounts[key] = (actionCounts[key] ?? 0) + 1;
717
+ }
718
+ const normalActions = Object.entries(actionCounts)
719
+ .sort((a, b) => b[1] - a[1])
720
+ .slice(0, 10)
721
+ .map(([action]) => action);
722
+ const seenActions = new Set();
723
+ const results = [];
724
+ for (const event of events) {
725
+ const actionKey = `${event.action}:${event.target}`;
726
+ const isFirstOccurrence = !seenActions.has(actionKey);
727
+ seenActions.add(actionKey);
728
+ const explanation = await explainAnomaly(event, {
729
+ agentName,
730
+ normalActions,
731
+ isFirstOccurrence,
732
+ });
733
+ results.push({ event, explanation });
734
+ }
735
+ if (isJson) {
736
+ process.stdout.write(JSON.stringify(results.map(r => ({
737
+ event: r.event,
738
+ explanation: r.explanation,
739
+ })), null, 2) + '\n');
740
+ return 0;
741
+ }
742
+ for (const { event, explanation } of results) {
743
+ const sev = colorSeverity(event.severity);
744
+ process.stdout.write(`[${event.timestamp}] [${sev}] ${event.action} -> ${event.target}\n`);
745
+ if (!explanation) {
746
+ process.stdout.write((0, colors_js_1.dim)(' Analysis unavailable.\n\n'));
747
+ continue;
748
+ }
749
+ const explSev = colorSeverity(explanation.severity);
750
+ process.stdout.write(` Severity: ${explSev}\n`);
751
+ process.stdout.write(` ${explanation.explanation}\n`);
752
+ if (explanation.riskFactors.length > 0) {
753
+ process.stdout.write((0, colors_js_1.dim)(' Risk factors:\n'));
754
+ for (const factor of explanation.riskFactors) {
755
+ process.stdout.write((0, colors_js_1.dim)(` - ${factor}\n`));
756
+ }
757
+ }
758
+ const actionColor = explanation.suggestedAction === 'block' ? colors_js_1.red
759
+ : explanation.suggestedAction === 'investigate' ? colors_js_1.yellow
760
+ : colors_js_1.dim;
761
+ process.stdout.write(` Recommended: ${actionColor(explanation.suggestedAction)}\n\n`);
762
+ }
763
+ return 0;
764
+ }
765
+ async function handleTriage(options) {
766
+ const { checkLlmAvailable, triageIncident } = await import('../shield/llm.js');
767
+ const { readEvents } = await import('../shield/events.js');
768
+ const { loadPolicy } = await import('../shield/policy.js');
769
+ const isJson = options.format === 'json';
770
+ const { backend } = await checkLlmAvailable();
771
+ if (backend === 'none') {
772
+ process.stderr.write((0, colors_js_1.yellow)('LLM intelligence is not available.\n'));
773
+ process.stderr.write('Enable it with: opena2a config llm on\n');
774
+ return 1;
775
+ }
776
+ const severity = options.severity ?? 'high';
777
+ const count = options.count ? parseInt(options.count, 10) : 10;
778
+ const events = readEvents({
779
+ severity,
780
+ count,
781
+ agent: options.agent,
782
+ });
783
+ if (events.length === 0) {
784
+ process.stdout.write((0, colors_js_1.dim)(`No ${severity}+ severity events found.\n`));
785
+ return 0;
786
+ }
787
+ const agentName = options.agent ?? events[0].agent ?? 'unknown';
788
+ const policy = loadPolicy(options.dir);
789
+ const policyMode = policy?.mode ?? 'monitor';
790
+ const baselineEvents = readEvents({ count: 100, agent: agentName });
791
+ const recentBaseline = [...new Set(baselineEvents.map(e => `${e.action} -> ${e.target}`))].slice(0, 10);
792
+ const triage = await triageIncident(events, {
793
+ policyMode,
794
+ agentName,
795
+ recentBaseline,
796
+ });
797
+ if (!triage) {
798
+ process.stdout.write((0, colors_js_1.dim)('LLM analysis unavailable. The backend may be unreachable.\n'));
799
+ return 0;
800
+ }
801
+ if (isJson) {
802
+ process.stdout.write(JSON.stringify(triage, null, 2) + '\n');
803
+ return 0;
804
+ }
805
+ const classColor = triage.classification === 'confirmed-threat' ? colors_js_1.red
806
+ : triage.classification === 'suspicious' ? colors_js_1.yellow
807
+ : colors_js_1.dim;
808
+ process.stdout.write((0, colors_js_1.bold)('Incident Triage') + '\n');
809
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
810
+ process.stdout.write(` Classification: ${classColor(triage.classification)}\n`);
811
+ process.stdout.write(` Severity: ${colorSeverity(triage.severity)}\n`);
812
+ process.stdout.write(` Events: ${triage.eventIds.length}\n`);
813
+ process.stdout.write(` Explanation: ${triage.explanation}\n`);
814
+ if (triage.responseSteps.length > 0) {
815
+ process.stdout.write('\n' + (0, colors_js_1.bold)(' Recommended actions:\n'));
816
+ for (let i = 0; i < triage.responseSteps.length; i++) {
817
+ process.stdout.write(` ${i + 1}. ${triage.responseSteps[i]}\n`);
818
+ }
819
+ }
820
+ process.stdout.write((0, colors_js_1.gray)('-'.repeat(50)) + '\n');
821
+ return 0;
822
+ }
823
+ // --- Formatting helpers ---
824
+ function colorSeverity(severity) {
825
+ switch (severity) {
826
+ case 'info': return (0, colors_js_1.dim)('INFO');
827
+ case 'low': return (0, colors_js_1.gray)('LOW');
828
+ case 'medium': return (0, colors_js_1.yellow)('MEDIUM');
829
+ case 'high': return (0, colors_js_1.red)('HIGH');
830
+ case 'critical': return (0, colors_js_1.bold)((0, colors_js_1.red)('CRITICAL'));
831
+ default: return (0, colors_js_1.dim)(String(severity));
832
+ }
833
+ }
834
+ //# sourceMappingURL=shield.js.map