opena2a-cli 0.3.2 → 0.3.4

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 (69) hide show
  1. package/README.md +22 -21
  2. package/dist/adapters/python.d.ts.map +1 -1
  3. package/dist/adapters/python.js +7 -3
  4. package/dist/adapters/python.js.map +1 -1
  5. package/dist/adapters/registry.d.ts.map +1 -1
  6. package/dist/adapters/registry.js +1 -7
  7. package/dist/adapters/registry.js.map +1 -1
  8. package/dist/commands/guard.d.ts +8 -0
  9. package/dist/commands/guard.d.ts.map +1 -1
  10. package/dist/commands/guard.js +30 -0
  11. package/dist/commands/guard.js.map +1 -1
  12. package/dist/commands/init.d.ts +8 -2
  13. package/dist/commands/init.d.ts.map +1 -1
  14. package/dist/commands/init.js +612 -162
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/onepassword-migration.d.ts.map +1 -1
  17. package/dist/commands/onepassword-migration.js +6 -0
  18. package/dist/commands/onepassword-migration.js.map +1 -1
  19. package/dist/commands/protect.d.ts +4 -0
  20. package/dist/commands/protect.d.ts.map +1 -1
  21. package/dist/commands/protect.js +259 -15
  22. package/dist/commands/protect.js.map +1 -1
  23. package/dist/commands/review.d.ts +2 -2
  24. package/dist/commands/review.d.ts.map +1 -1
  25. package/dist/commands/review.js +7 -7
  26. package/dist/commands/review.js.map +1 -1
  27. package/dist/commands/shield.d.ts +1 -1
  28. package/dist/commands/shield.js +1 -1
  29. package/dist/index.js +10 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/natural/llm-fallback.d.ts.map +1 -1
  32. package/dist/natural/llm-fallback.js +24 -4
  33. package/dist/natural/llm-fallback.js.map +1 -1
  34. package/dist/report/review-html.js +2 -2
  35. package/dist/router.js +1 -1
  36. package/dist/router.js.map +1 -1
  37. package/dist/semantic/command-index.json +1 -1
  38. package/dist/shield/status.d.ts.map +1 -1
  39. package/dist/shield/status.js +16 -16
  40. package/dist/shield/status.js.map +1 -1
  41. package/dist/shield/types.d.ts +3 -3
  42. package/dist/shield/types.d.ts.map +1 -1
  43. package/dist/util/ai-config.d.ts +40 -0
  44. package/dist/util/ai-config.d.ts.map +1 -0
  45. package/dist/util/ai-config.js +389 -0
  46. package/dist/util/ai-config.js.map +1 -0
  47. package/dist/util/credential-patterns.js +6 -6
  48. package/dist/util/credential-patterns.js.map +1 -1
  49. package/dist/util/detect.d.ts +2 -1
  50. package/dist/util/detect.d.ts.map +1 -1
  51. package/dist/util/detect.js +31 -1
  52. package/dist/util/detect.js.map +1 -1
  53. package/dist/util/format.d.ts +1 -0
  54. package/dist/util/format.d.ts.map +1 -1
  55. package/dist/util/format.js +20 -0
  56. package/dist/util/format.js.map +1 -1
  57. package/dist/util/hygiene.d.ts +16 -0
  58. package/dist/util/hygiene.d.ts.map +1 -0
  59. package/dist/util/hygiene.js +119 -0
  60. package/dist/util/hygiene.js.map +1 -0
  61. package/dist/util/scoring.d.ts +34 -0
  62. package/dist/util/scoring.d.ts.map +1 -0
  63. package/dist/util/scoring.js +144 -0
  64. package/dist/util/scoring.js.map +1 -0
  65. package/dist/util/secretless-config.d.ts +39 -0
  66. package/dist/util/secretless-config.d.ts.map +1 -0
  67. package/dist/util/secretless-config.js +265 -0
  68. package/dist/util/secretless-config.js.map +1 -0
  69. package/package.json +1 -1
@@ -2,8 +2,8 @@
2
2
  /**
3
3
  * opena2a init -- Initialize security posture assessment for a project.
4
4
  *
5
- * Detects project type, scans for credentials, checks hygiene,
6
- * calculates trust score, and generates prioritized next steps.
5
+ * Findings-first design: shows what was found, explains why it matters,
6
+ * calculates a unified security score, and generates prioritized actions.
7
7
  */
8
8
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
9
  if (k2 === undefined) k2 = k;
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.calculateSecurityScore = void 0;
42
43
  exports.init = init;
43
44
  const fs = __importStar(require("node:fs"));
44
45
  const path = __importStar(require("node:path"));
@@ -46,9 +47,13 @@ const colors_js_1 = require("../util/colors.js");
46
47
  const detect_js_1 = require("../util/detect.js");
47
48
  const credential_patterns_js_1 = require("../util/credential-patterns.js");
48
49
  const advisories_js_1 = require("../util/advisories.js");
50
+ const format_js_1 = require("../util/format.js");
49
51
  const version_js_1 = require("../util/version.js");
52
+ const spinner_js_1 = require("../util/spinner.js");
50
53
  const events_js_1 = require("../shield/events.js");
51
54
  const status_js_1 = require("../shield/status.js");
55
+ const ai_config_js_1 = require("../util/ai-config.js");
56
+ const scoring_js_1 = require("../util/scoring.js");
52
57
  // --- Core ---
53
58
  async function init(options) {
54
59
  const targetDir = path.resolve(options.targetDir ?? process.cwd());
@@ -56,54 +61,86 @@ async function init(options) {
56
61
  process.stderr.write((0, colors_js_1.red)(`Directory not found: ${targetDir}\n`));
57
62
  return 1;
58
63
  }
64
+ const startTime = Date.now();
65
+ const isTTY = process.stderr.isTTY && options.format !== 'json';
66
+ const spinner = new spinner_js_1.Spinner('Scanning project...');
67
+ if (isTTY)
68
+ spinner.start();
59
69
  // 1. Detect project type
60
70
  const project = (0, detect_js_1.detectProject)(targetDir);
61
- // 2. Quick credential scan
71
+ // 2. Quick credential scan (source files + MCP configs)
72
+ if (isTTY)
73
+ spinner.update('Scanning for credentials...');
62
74
  const credentialMatches = (0, credential_patterns_js_1.quickCredentialScan)(targetDir);
75
+ // Scan MCP config files for credentials (these are skipped by walkFiles)
76
+ const mcpCreds = (0, ai_config_js_1.scanMcpCredentials)(targetDir);
77
+ const seenCredValues = new Set(credentialMatches.map(m => m.value));
78
+ for (const mc of mcpCreds) {
79
+ if (!seenCredValues.has(mc.value)) {
80
+ credentialMatches.push(mc);
81
+ seenCredValues.add(mc.value);
82
+ }
83
+ }
63
84
  const credsBySeverity = {};
64
85
  for (const m of credentialMatches) {
65
86
  credsBySeverity[m.severity] = (credsBySeverity[m.severity] || 0) + 1;
66
87
  }
67
88
  // 3. Security hygiene checks
68
- const checks = runHygieneChecks(targetDir, project, credentialMatches.length);
89
+ if (isTTY)
90
+ spinner.update('Checking environment...');
91
+ const checks = await runHygieneChecks(targetDir, project, credentialMatches.length);
69
92
  // 4. Check advisories (non-blocking)
70
93
  let advisoryCheck = { advisories: [], matchedPackages: [], total: 0, fromCache: false };
71
94
  try {
72
95
  advisoryCheck = await (0, advisories_js_1.checkAdvisories)(targetDir);
73
96
  }
74
97
  catch {
75
- // Advisory check is best-effort, don't fail init
98
+ // Advisory check is best-effort
99
+ }
100
+ // 5. HMA integration (optional dynamic import)
101
+ if (isTTY)
102
+ spinner.update('Scanning shell environment...');
103
+ let hmaAvailable = false;
104
+ const hmaFindings = [];
105
+ try {
106
+ const hma = await import('hackmyagent');
107
+ hmaAvailable = true;
108
+ if (typeof hma.checkShellEnvironment === 'function') {
109
+ const shellEnv = await hma.checkShellEnvironment();
110
+ if (Array.isArray(shellEnv))
111
+ hmaFindings.push(...shellEnv);
112
+ }
113
+ if (typeof hma.checkShellHistory === 'function') {
114
+ const shellHistory = await hma.checkShellHistory();
115
+ if (Array.isArray(shellHistory))
116
+ hmaFindings.push(...shellHistory);
117
+ }
118
+ }
119
+ catch {
120
+ // HMA not installed -- skip silently
76
121
  }
77
- // 5. Calculate trust score
78
- const { score, grade } = calculateTrustScore(credsBySeverity, checks, targetDir);
79
- // 6. Generate next steps
122
+ // 6. Group findings
123
+ const groupedFindings = groupFindings(credentialMatches, checks, hmaFindings);
124
+ // 7. Calculate unified security score
125
+ if (isTTY)
126
+ spinner.update('Assessing security posture...');
127
+ const hmaBySeverity = {};
128
+ for (const f of hmaFindings) {
129
+ hmaBySeverity[f.severity] = (hmaBySeverity[f.severity] || 0) + 1;
130
+ }
131
+ const { score, grade, breakdown } = (0, exports.calculateSecurityScore)(credsBySeverity, checks, hmaBySeverity);
132
+ // 8. Generate actions
133
+ const actions = generateActions(credentialMatches, credsBySeverity, checks, groupedFindings);
134
+ // 9. Generate legacy next steps (backward compat)
80
135
  const nextSteps = generateNextSteps(credentialMatches.length, credsBySeverity, checks, project.type);
81
- // 6.5. Compute posture score from Shield product detection
136
+ // 10. Shield tool status (for backward compat and tip line)
82
137
  const shieldStatus = (0, status_js_1.getShieldStatus)(targetDir);
83
- const activeProducts = shieldStatus.products.filter(p => p.active).length;
84
- const totalProducts = shieldStatus.products.length;
85
- let postureScore = 0;
86
- postureScore += Math.min(activeProducts * 10, 60);
87
- if (shieldStatus.policyLoaded)
88
- postureScore += 10;
89
- if (shieldStatus.shellIntegration)
90
- postureScore += 5;
91
- if (credentialMatches.length === 0)
92
- postureScore += 15;
93
- const sigDir = path.join(targetDir, '.opena2a', 'signatures');
94
- if (fs.existsSync(sigDir))
95
- postureScore += 10;
96
- postureScore = Math.max(0, Math.min(100, postureScore));
97
- const riskLevel = postureScore < 30 ? 'CRITICAL'
98
- : postureScore < 50 ? 'HIGH'
99
- : postureScore < 70 ? 'MEDIUM'
100
- : postureScore < 90 ? 'LOW'
101
- : 'SECURE';
102
- // 6.6. Write shield events for posture and credential findings
103
- // Events are written to the project-local .opena2a/shield/ when available,
104
- // falling back to the global ~/.opena2a/shield/.
138
+ const activeTools = shieldStatus.tools.filter(p => p.active).length;
139
+ const totalTools = shieldStatus.tools.length;
140
+ // 11. Write shield events
105
141
  try {
106
142
  (0, events_js_1.getShieldDir)(targetDir);
143
+ const riskLevel = scoreToRiskLevel(score);
107
144
  (0, events_js_1.writeEvent)({
108
145
  source: 'shield',
109
146
  category: 'shield.posture',
@@ -113,7 +150,7 @@ async function init(options) {
113
150
  action: 'posture-assessment',
114
151
  target: targetDir,
115
152
  outcome: 'monitored',
116
- detail: { score: postureScore, riskLevel, activeProducts, totalProducts, trustScore: score, grade },
153
+ detail: { score, grade, breakdown, activeTools, totalTools },
117
154
  orgId: null,
118
155
  managed: false,
119
156
  agentId: null,
@@ -138,8 +175,13 @@ async function init(options) {
138
175
  catch {
139
176
  // Shield event writing is best-effort
140
177
  }
141
- // 7. Build report
178
+ if (isTTY)
179
+ spinner.stop();
180
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
181
+ // 12. Build report
182
+ const riskLevel = scoreToRiskLevel(score);
142
183
  const report = {
184
+ version: 2,
143
185
  projectName: project.name,
144
186
  projectVersion: project.version,
145
187
  projectType: formatProjectType(project),
@@ -147,41 +189,31 @@ async function init(options) {
147
189
  credentialFindings: credentialMatches.length,
148
190
  credentialsBySeverity: credsBySeverity,
149
191
  hygieneChecks: checks,
150
- trustScore: score,
151
- grade,
192
+ securityScore: score,
193
+ securityGrade: grade,
194
+ scoreBreakdown: breakdown,
195
+ findings: groupedFindings,
196
+ actions,
152
197
  nextSteps,
153
198
  advisories: {
154
199
  count: advisoryCheck.advisories.length,
155
200
  matchedPackages: advisoryCheck.matchedPackages,
156
201
  },
157
- postureScore,
202
+ hmaAvailable,
203
+ // Backward compat aliases
204
+ trustScore: score,
205
+ grade,
206
+ postureScore: score,
158
207
  riskLevel,
159
- activeProducts,
160
- totalProducts,
208
+ activeTools,
209
+ totalTools,
161
210
  };
162
- // 8. Output
211
+ // 13. Output
163
212
  if (options.format === 'json') {
164
213
  process.stdout.write(JSON.stringify(report, null, 2) + '\n');
165
214
  }
166
215
  else {
167
- printReport(report, options.verbose);
168
- // Verbose: show individual credential findings
169
- if (options.verbose && credentialMatches.length > 0) {
170
- process.stdout.write((0, colors_js_1.bold)(' Credential Details') + '\n');
171
- process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(47)) + '\n');
172
- for (const m of credentialMatches) {
173
- const sev = m.severity === 'critical' ? (0, colors_js_1.red)('[CRITICAL]')
174
- : m.severity === 'high' ? (0, colors_js_1.yellow)('[HIGH]')
175
- : (0, colors_js_1.cyan)('[MEDIUM]');
176
- const relPath = path.relative(targetDir, m.filePath);
177
- process.stdout.write(` ${sev} ${(0, colors_js_1.bold)(m.findingId)}: ${m.title}\n`);
178
- process.stdout.write(` ${(0, colors_js_1.dim)(' File:')} ${relPath}:${m.line}\n`);
179
- if (m.explanation) {
180
- process.stdout.write(` ${(0, colors_js_1.dim)(' Why:')} ${m.explanation}\n`);
181
- }
182
- process.stdout.write('\n');
183
- }
184
- }
216
+ printReport(report, elapsed, options.verbose);
185
217
  // Drift detection callout (always shown when drift findings exist)
186
218
  const driftFindings = credentialMatches.filter(m => m.findingId.startsWith('DRIFT'));
187
219
  if (driftFindings.length > 0) {
@@ -208,7 +240,7 @@ async function init(options) {
208
240
  return hasCritical ? 1 : 0;
209
241
  }
210
242
  // --- Hygiene checks ---
211
- function runHygieneChecks(dir, project, credCount) {
243
+ async function runHygieneChecks(dir, project, credCount) {
212
244
  const checks = [];
213
245
  // Credential scan result
214
246
  if (credCount === 0) {
@@ -268,48 +300,287 @@ function runHygieneChecks(dir, project, credCount) {
268
300
  if (project.hasMcp) {
269
301
  checks.push({ label: 'MCP config', status: 'info', detail: 'found' });
270
302
  }
303
+ // LLM server exposure (lightweight probe of common ports)
304
+ const llmCheck = await checkLLMServerExposure();
305
+ if (llmCheck) {
306
+ checks.push(llmCheck);
307
+ }
308
+ // AI-specific configuration scans
309
+ for (const f of (0, ai_config_js_1.scanMcpConfig)(dir)) {
310
+ checks.push({ label: f.label, status: f.status, detail: f.detail });
311
+ }
312
+ const aiCfg = (0, ai_config_js_1.scanAiConfigFiles)(dir);
313
+ if (aiCfg)
314
+ checks.push({ label: aiCfg.label, status: aiCfg.status, detail: aiCfg.detail });
315
+ const skills = (0, ai_config_js_1.scanSkillFiles)(dir);
316
+ if (skills)
317
+ checks.push({ label: skills.label, status: skills.status, detail: skills.detail });
318
+ const soul = (0, ai_config_js_1.scanSoulFile)(dir);
319
+ if (soul)
320
+ checks.push({ label: soul.label, status: soul.status, detail: soul.detail });
271
321
  return checks;
272
322
  }
273
- // --- Trust score ---
274
- function calculateTrustScore(credsBySeverity, checks, dir) {
275
- let score = 100;
276
- // Credential penalties
277
- score -= (credsBySeverity['critical'] || 0) * 25;
278
- score -= (credsBySeverity['high'] || 0) * 15;
279
- score -= (credsBySeverity['medium'] || 0) * 8;
280
- score -= (credsBySeverity['low'] || 0) * 3;
281
- // Hygiene penalties
282
- const gitignoreCheck = checks.find(c => c.label === '.gitignore');
283
- if (gitignoreCheck?.status !== 'pass')
284
- score -= 15;
323
+ // --- LLM server exposure check ---
324
+ const LLM_PROBE_PORTS = [
325
+ { name: 'Ollama', port: 11434, path: '/api/tags' },
326
+ { name: 'LM Studio', port: 1234, path: '/v1/models' },
327
+ ];
328
+ async function checkLLMServerExposure() {
329
+ for (const server of LLM_PROBE_PORTS) {
330
+ const controller = new AbortController();
331
+ const timer = setTimeout(() => controller.abort(), 2000);
332
+ try {
333
+ const resp = await fetch(`http://127.0.0.1:${server.port}${server.path}`, {
334
+ signal: controller.signal,
335
+ });
336
+ clearTimeout(timer);
337
+ if (resp.ok || resp.status < 500) {
338
+ // Check if no auth required
339
+ const noAuth = resp.status !== 401 && resp.status !== 403;
340
+ if (noAuth) {
341
+ return {
342
+ label: 'LLM server exposure',
343
+ status: 'warn',
344
+ detail: `${server.name} on :${server.port} (no auth)`,
345
+ };
346
+ }
347
+ return {
348
+ label: 'LLM server exposure',
349
+ status: 'info',
350
+ detail: `${server.name} on :${server.port}`,
351
+ };
352
+ }
353
+ }
354
+ catch {
355
+ clearTimeout(timer);
356
+ // Server not running on this port, continue
357
+ }
358
+ }
359
+ return null;
360
+ }
361
+ // --- Finding grouping ---
362
+ function groupFindings(creds, checks, hmaFindings) {
363
+ const groups = new Map();
364
+ // Group credential findings by findingId
365
+ for (const cred of creds) {
366
+ const existing = groups.get(cred.findingId);
367
+ if (existing) {
368
+ existing.count++;
369
+ existing.locations.push({
370
+ file: cred.filePath,
371
+ line: cred.line,
372
+ });
373
+ }
374
+ else {
375
+ groups.set(cred.findingId, {
376
+ findingId: cred.findingId,
377
+ title: cred.title,
378
+ severity: cred.severity,
379
+ count: 1,
380
+ explanation: cred.explanation ?? '',
381
+ businessImpact: cred.businessImpact ?? '',
382
+ locations: [{ file: cred.filePath, line: cred.line }],
383
+ });
384
+ }
385
+ }
386
+ // Add hygiene findings as grouped findings
387
+ const llmCheck = checks.find(c => c.label === 'LLM server exposure' && c.status === 'warn');
388
+ if (llmCheck) {
389
+ groups.set('ENV-LLM', {
390
+ findingId: 'ENV-LLM',
391
+ title: llmCheck.detail,
392
+ severity: 'high',
393
+ count: 1,
394
+ explanation: 'Local LLM server is responding without authentication. Adding auth limits access to authorized users.',
395
+ businessImpact: 'Unauthenticated model access. Adding auth ensures only intended users can query.',
396
+ locations: [],
397
+ });
398
+ }
399
+ const envCheck = checks.find(c => c.label === '.env protection' && c.status === 'warn');
400
+ if (envCheck) {
401
+ groups.set('ENV-DOTENV', {
402
+ findingId: 'ENV-DOTENV',
403
+ title: '.env not in .gitignore',
404
+ severity: 'medium',
405
+ count: 1,
406
+ explanation: 'Environment files may be committed to version control.',
407
+ businessImpact: 'Adding .env to .gitignore keeps secrets out of version control.',
408
+ locations: [],
409
+ });
410
+ }
411
+ // Add AI config findings
412
+ const mcpToolsCheck = checks.find(c => c.label === 'MCP high-risk tools' && c.status === 'warn');
413
+ if (mcpToolsCheck) {
414
+ groups.set('MCP-TOOLS', {
415
+ findingId: 'MCP-TOOLS',
416
+ title: mcpToolsCheck.detail,
417
+ severity: 'high',
418
+ count: 1,
419
+ explanation: 'MCP servers with filesystem or shell access can read, modify, or delete files on your system when invoked by an AI assistant.',
420
+ businessImpact: 'Review server permissions to ensure each server has only the access it needs.',
421
+ locations: [],
422
+ });
423
+ }
424
+ const mcpCredCheck = checks.find(c => c.label === 'MCP credentials' && c.status === 'warn');
425
+ if (mcpCredCheck) {
426
+ groups.set('MCP-CRED', {
427
+ findingId: 'MCP-CRED',
428
+ title: mcpCredCheck.detail,
429
+ severity: 'high',
430
+ count: 1,
431
+ explanation: 'API keys hardcoded in MCP config files are readable by anyone with access to the project directory.',
432
+ businessImpact: 'Move credentials to environment variables so they are not stored in plaintext.',
433
+ locations: [],
434
+ });
435
+ }
436
+ const aiConfigCheck = checks.find(c => c.label === 'AI config exposure' && c.status === 'warn');
437
+ if (aiConfigCheck) {
438
+ groups.set('AI-CONFIG', {
439
+ findingId: 'AI-CONFIG',
440
+ title: aiConfigCheck.detail,
441
+ severity: 'medium',
442
+ count: 1,
443
+ explanation: 'AI instruction files (CLAUDE.md, .cursorrules, etc.) reveal tooling choices and system prompts when committed to a public repository.',
444
+ businessImpact: 'Add these files to .git/info/exclude to keep them local without modifying .gitignore.',
445
+ locations: [],
446
+ });
447
+ }
448
+ // Add HMA findings
449
+ for (const f of hmaFindings) {
450
+ const key = `HMA-${f.checkId}`;
451
+ const existing = groups.get(key);
452
+ if (existing) {
453
+ existing.count++;
454
+ }
455
+ else {
456
+ groups.set(key, {
457
+ findingId: key,
458
+ title: f.message,
459
+ severity: f.severity,
460
+ count: 1,
461
+ explanation: '',
462
+ businessImpact: '',
463
+ locations: [],
464
+ });
465
+ }
466
+ }
467
+ // Sort by severity order: critical > high > medium > low
468
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
469
+ return Array.from(groups.values()).sort((a, b) => {
470
+ const sa = severityOrder[a.severity] ?? 4;
471
+ const sb = severityOrder[b.severity] ?? 4;
472
+ if (sa !== sb)
473
+ return sa - sb;
474
+ return b.count - a.count;
475
+ });
476
+ }
477
+ // --- Unified Security Score ---
478
+ /**
479
+ * Re-export from shared module for backward compatibility.
480
+ * Tests (security-score.test.ts) import this from init.ts.
481
+ */
482
+ exports.calculateSecurityScore = scoring_js_1.calculateSecurityScore;
483
+ const scoreToRiskLevel = scoring_js_1.scoreToRiskLevel;
484
+ // --- Actions ---
485
+ function generateActions(creds, credsBySeverity, checks, findings) {
486
+ const actions = [];
487
+ // Credential migration action
488
+ if (creds.length > 0) {
489
+ // Build breakdown string
490
+ const byTitle = new Map();
491
+ for (const c of creds) {
492
+ byTitle.set(c.title, (byTitle.get(c.title) || 0) + 1);
493
+ }
494
+ const breakdownParts = Array.from(byTitle.entries())
495
+ .map(([title, count]) => `${count} ${title.replace(/ \(.*\)/, '')}`)
496
+ .join(', ');
497
+ actions.push({
498
+ description: `Migrate ${creds.length} hardcoded credential${creds.length === 1 ? '' : 's'} to a vault`,
499
+ command: 'opena2a protect',
500
+ why: 'Credentials in source files are readable by anyone with repo access. A vault stores them encrypted, rotates them automatically, and provides an audit trail.',
501
+ approach: 'Moves keys to environment variables backed by an encrypted vault. Keys rotate without code changes, and access is auditable.',
502
+ detail: `Keys found: ${breakdownParts}`,
503
+ });
504
+ }
505
+ // .env protection
285
506
  const envCheck = checks.find(c => c.label === '.env protection');
286
- if (envCheck?.status === 'warn')
287
- score -= 10;
288
- const lockCheck = checks.find(c => c.label === 'Lock file');
289
- if (lockCheck?.status !== 'pass')
290
- score -= 5;
291
- // Bonus for security config
507
+ if (envCheck?.status === 'warn') {
508
+ actions.push({
509
+ description: 'Add .env to .gitignore',
510
+ command: 'opena2a protect',
511
+ why: 'Adding .env to .gitignore prevents secrets from entering version control. Existing tracked .env files also need `git rm --cached .env`.',
512
+ });
513
+ }
514
+ // .gitignore
515
+ const gitignoreCheck = checks.find(c => c.label === '.gitignore');
516
+ if (gitignoreCheck?.status !== 'pass') {
517
+ actions.push({
518
+ description: 'Create .gitignore with .env exclusion',
519
+ command: 'opena2a protect',
520
+ why: 'Without a .gitignore, build artifacts and sensitive files can be committed accidentally. Protect creates one with .env exclusion to prevent secret leaks.',
521
+ });
522
+ }
523
+ // Shell environment findings (from HMA)
524
+ const hmaFindings = findings.filter(f => f.findingId.startsWith('HMA-'));
525
+ if (hmaFindings.length > 0) {
526
+ const totalHma = hmaFindings.reduce((n, f) => n + f.count, 0);
527
+ actions.push({
528
+ description: `Clean ${totalHma} shell environment finding${totalHma === 1 ? '' : 's'}`,
529
+ command: 'opena2a scan secure',
530
+ why: 'Shell config files and history can contain API keys in plaintext. Rotating exposed keys and clearing history entries removes persistent exposure.',
531
+ });
532
+ }
533
+ // LLM server exposure
534
+ const llmCheck = checks.find(c => c.label === 'LLM server exposure' && c.status === 'warn');
535
+ if (llmCheck) {
536
+ actions.push({
537
+ description: 'Secure LLM server',
538
+ command: 'opena2a shield status',
539
+ why: 'A local LLM server without authentication accepts requests from any process on the network. Binding to localhost or adding auth limits access.',
540
+ });
541
+ }
542
+ // MCP high-risk tools
543
+ const mcpToolsFinding = findings.find(f => f.findingId === 'MCP-TOOLS');
544
+ if (mcpToolsFinding) {
545
+ actions.push({
546
+ description: 'Review MCP server permissions',
547
+ command: 'opena2a shield status',
548
+ why: 'MCP servers with filesystem or shell access can read, modify, or delete files when invoked by an AI assistant. Review each server to confirm it has only the access it needs.',
549
+ });
550
+ }
551
+ // MCP credentials
552
+ const mcpCredFinding = findings.find(f => f.findingId === 'MCP-CRED');
553
+ if (mcpCredFinding) {
554
+ actions.push({
555
+ description: 'Move MCP config credentials to environment variables',
556
+ command: 'opena2a protect',
557
+ why: 'API keys hardcoded in MCP config files are stored in plaintext. Environment variables keep credentials out of the project directory and version control.',
558
+ });
559
+ }
560
+ // AI config exposure
561
+ const aiConfigFinding = findings.find(f => f.findingId === 'AI-CONFIG');
562
+ if (aiConfigFinding) {
563
+ actions.push({
564
+ description: 'Exclude AI instruction files from git',
565
+ command: 'opena2a protect',
566
+ why: 'AI instruction files reveal tooling choices and system prompts when committed to a public repository. Adding them to .git/info/exclude keeps them local without modifying .gitignore.',
567
+ });
568
+ }
569
+ // Config signing (only suggest if fewer than 5 actions already -- lower priority)
292
570
  const secConfig = checks.find(c => c.label === 'Security config');
293
- if (secConfig?.status === 'pass')
294
- score += 5;
295
- score = Math.max(0, Math.min(100, score));
296
- let grade;
297
- if (score >= 90)
298
- grade = 'A';
299
- else if (score >= 80)
300
- grade = 'B';
301
- else if (score >= 70)
302
- grade = 'C';
303
- else if (score >= 60)
304
- grade = 'D';
305
- else
306
- grade = 'F';
307
- return { score, grade };
571
+ if (actions.length < 5 && secConfig?.status !== 'pass') {
572
+ actions.push({
573
+ description: 'Sign config files for integrity monitoring',
574
+ command: 'opena2a protect',
575
+ why: 'Signed baselines let you detect unintended config changes before they affect runtime behavior.',
576
+ });
577
+ }
578
+ // Cap at 5 actions
579
+ return actions.slice(0, 5);
308
580
  }
309
- // --- Next steps ---
581
+ // --- Legacy next steps (backward compat) ---
310
582
  function generateNextSteps(credCount, credsBySeverity, checks, projectType) {
311
583
  const steps = [];
312
- // Credentials -> protect
313
584
  if (credCount > 0) {
314
585
  steps.push({
315
586
  severity: 'critical',
@@ -317,34 +588,27 @@ function generateNextSteps(credCount, credsBySeverity, checks, projectType) {
317
588
  command: 'opena2a protect',
318
589
  });
319
590
  }
320
- // .env protection
321
591
  const envCheck = checks.find(c => c.label === '.env protection');
322
592
  if (envCheck?.status === 'warn') {
323
593
  steps.push({
324
594
  severity: 'high',
325
595
  description: 'Add .env to .gitignore',
326
- command: "echo '.env' >> .gitignore",
596
+ command: 'opena2a protect',
327
597
  });
328
598
  }
329
- // No .gitignore
330
599
  const gitignoreCheck = checks.find(c => c.label === '.gitignore');
331
600
  if (gitignoreCheck?.status !== 'pass') {
332
- const gitignoreTemplate = projectType === 'python' ? 'python'
333
- : projectType === 'go' ? 'go'
334
- : 'node';
335
601
  steps.push({
336
602
  severity: 'high',
337
- description: 'Create .gitignore',
338
- command: `npx gitignore ${gitignoreTemplate}`,
603
+ description: 'Create .gitignore with .env exclusion',
604
+ command: 'opena2a protect',
339
605
  });
340
606
  }
341
- // Sign config files
342
607
  steps.push({
343
608
  severity: 'medium',
344
609
  description: 'Sign config files for integrity',
345
- command: 'opena2a guard sign',
610
+ command: 'opena2a protect',
346
611
  });
347
- // Runtime protection
348
612
  steps.push({
349
613
  severity: 'low',
350
614
  description: 'Start runtime protection',
@@ -352,84 +616,270 @@ function generateNextSteps(credCount, credsBySeverity, checks, projectType) {
352
616
  });
353
617
  return steps;
354
618
  }
619
+ // --- Verification & Recommendation helpers ---
620
+ function getVerificationCommand(finding, reportDir) {
621
+ // Credential/drift findings with file locations
622
+ if ((finding.findingId.startsWith('CRED-') || finding.findingId.startsWith('DRIFT-')) &&
623
+ finding.locations.length > 0) {
624
+ const loc = finding.locations[0];
625
+ const rel = path.relative(reportDir, loc.file);
626
+ return `sed -n '${loc.line}p' ${rel}`;
627
+ }
628
+ // HMA findings -- title contains "~/.zshrc:132 contains ..." pattern
629
+ if (finding.findingId.startsWith('HMA-')) {
630
+ const match = finding.title.match(/^(.+?):(\d+)\s+contains\s+/);
631
+ if (match) {
632
+ return `sed -n '${match[2]}p' ${match[1]}`;
633
+ }
634
+ }
635
+ if (finding.findingId === 'ENV-LLM') {
636
+ return 'curl -s http://127.0.0.1:11434/api/tags | head -c 200';
637
+ }
638
+ if (finding.findingId === 'ENV-DOTENV') {
639
+ return "cat .gitignore | grep -c '.env'";
640
+ }
641
+ if (finding.findingId === 'MCP-TOOLS') {
642
+ // Show the first MCP config file found
643
+ for (const f of ['mcp.json', '.mcp.json', '.claude/settings.json', '.cursor/mcp.json']) {
644
+ if (fs.existsSync(path.join(reportDir, f))) {
645
+ return `cat ${f}`;
646
+ }
647
+ }
648
+ return 'cat mcp.json';
649
+ }
650
+ if (finding.findingId === 'MCP-CRED') {
651
+ for (const f of ['mcp.json', '.mcp.json', '.claude/settings.json', '.cursor/mcp.json']) {
652
+ if (fs.existsSync(path.join(reportDir, f))) {
653
+ return `cat ${f}`;
654
+ }
655
+ }
656
+ return 'cat mcp.json';
657
+ }
658
+ if (finding.findingId === 'AI-CONFIG') {
659
+ return 'cat .gitignore';
660
+ }
661
+ if (finding.findingId === 'AI-SKILLS') {
662
+ return 'ls *.skill.md SKILL.md 2>/dev/null';
663
+ }
664
+ if (finding.findingId === 'AI-SOUL') {
665
+ return 'head -20 soul.md';
666
+ }
667
+ return null;
668
+ }
669
+ function getToolRecommendation(findingId) {
670
+ if (findingId.startsWith('CRED-') || findingId.startsWith('DRIFT-')) {
671
+ return { command: 'opena2a protect', label: 'opena2a protect' };
672
+ }
673
+ if (findingId === 'ENV-LLM') {
674
+ return { command: 'opena2a shield status', label: 'opena2a shield status' };
675
+ }
676
+ if (findingId === 'ENV-DOTENV') {
677
+ return { command: 'opena2a protect', label: 'opena2a protect' };
678
+ }
679
+ if (findingId.startsWith('HMA-')) {
680
+ return { command: 'opena2a scan secure', label: 'opena2a scan secure' };
681
+ }
682
+ if (findingId === 'MCP-TOOLS') {
683
+ return { command: 'opena2a shield status', label: 'opena2a shield status' };
684
+ }
685
+ if (findingId === 'MCP-CRED') {
686
+ return { command: 'opena2a protect', label: 'opena2a protect' };
687
+ }
688
+ if (findingId === 'AI-CONFIG') {
689
+ return { command: 'opena2a protect', label: 'opena2a protect' };
690
+ }
691
+ if (findingId === 'AI-SKILLS') {
692
+ return { command: 'opena2a guard sign --skills', label: 'opena2a guard sign --skills' };
693
+ }
694
+ if (findingId === 'AI-SOUL') {
695
+ return { command: 'opena2a guard sign', label: 'opena2a guard sign' };
696
+ }
697
+ return null;
698
+ }
699
+ function getContextualTip(report) {
700
+ const hasAnyCreds = report.findings.some(f => f.findingId.startsWith('CRED-') || f.findingId.startsWith('DRIFT-'));
701
+ if (hasAnyCreds) {
702
+ return {
703
+ text: 'Migrate credentials out of source files',
704
+ command: 'opena2a protect',
705
+ };
706
+ }
707
+ const hasMcpCred = report.findings.some(f => f.findingId === 'MCP-CRED');
708
+ if (hasMcpCred) {
709
+ return {
710
+ text: 'Move credentials out of MCP config files',
711
+ command: 'opena2a protect',
712
+ };
713
+ }
714
+ const hasAiConfig = report.findings.some(f => f.findingId === 'AI-CONFIG');
715
+ if (hasAiConfig) {
716
+ return {
717
+ text: 'Fix all auto-fixable findings',
718
+ command: 'opena2a protect',
719
+ };
720
+ }
721
+ const hasLLM = report.findings.some(f => f.findingId === 'ENV-LLM');
722
+ if (hasLLM) {
723
+ return {
724
+ text: 'Add authentication to your LLM server',
725
+ command: 'opena2a shield status',
726
+ };
727
+ }
728
+ const hasEnv = report.findings.some(f => f.findingId === 'ENV-DOTENV');
729
+ if (hasEnv) {
730
+ return {
731
+ text: 'Fix all auto-fixable findings',
732
+ command: 'opena2a protect',
733
+ };
734
+ }
735
+ if (report.securityScore >= 90) {
736
+ return {
737
+ text: 'Strong baseline. Run a full scan for deeper coverage',
738
+ command: 'opena2a scan secure',
739
+ };
740
+ }
741
+ if (report.securityScore >= 70) {
742
+ return {
743
+ text: 'Good posture. Lock it in with config file integrity signing',
744
+ command: 'opena2a guard sign',
745
+ };
746
+ }
747
+ return {
748
+ text: 'See all available security tools',
749
+ command: 'opena2a shield status',
750
+ };
751
+ }
355
752
  // --- Output ---
356
753
  function formatProjectType(project) {
357
- const parts = [];
358
- switch (project.type) {
359
- case 'node':
360
- parts.push('Node.js');
361
- break;
362
- case 'go':
363
- parts.push('Go');
364
- break;
365
- case 'python':
366
- parts.push('Python');
367
- break;
368
- default: parts.push('Unknown');
369
- }
370
- if (project.hasMcp)
371
- parts.push('+ MCP server');
754
+ const primary = {
755
+ node: 'Node.js',
756
+ go: 'Go',
757
+ python: 'Python',
758
+ rust: 'Rust',
759
+ java: 'Java',
760
+ ruby: 'Ruby',
761
+ docker: 'Docker',
762
+ generic: 'Project',
763
+ };
764
+ const parts = [primary[project.type]];
765
+ for (const hint of project.frameworkHints) {
766
+ parts.push(`+ ${hint}`);
767
+ }
372
768
  return parts.join(' ');
373
769
  }
374
- function printReport(report, _verbose) {
770
+ function printReport(report, elapsed, verbose) {
375
771
  const VERSION = (0, version_js_1.getVersion)();
376
772
  process.stdout.write('\n');
377
- process.stdout.write((0, colors_js_1.bold)(' OpenA2A Security Initialization') + (0, colors_js_1.dim)(` v${VERSION}`) + '\n\n');
773
+ process.stdout.write((0, colors_js_1.bold)(' OpenA2A Security Assessment') + (0, colors_js_1.dim)(` v${VERSION}`) + (0, colors_js_1.dim)(` ${elapsed}s`) + '\n\n');
378
774
  // Project info
379
775
  const projectDisplay = report.projectName
380
776
  ? `${report.projectName}${report.projectVersion ? ' v' + report.projectVersion : ''}`
381
777
  : path.basename(report.directory);
382
778
  process.stdout.write(` ${(0, colors_js_1.dim)('Project')} ${projectDisplay}\n`);
383
- process.stdout.write(` ${(0, colors_js_1.dim)('Type')} ${report.projectType}\n`);
779
+ process.stdout.write(` ${(0, colors_js_1.dim)('Stack')} ${report.projectType}\n`);
384
780
  process.stdout.write(` ${(0, colors_js_1.dim)('Directory')} ${report.directory}\n`);
385
781
  process.stdout.write('\n');
386
- // Security posture
387
- process.stdout.write((0, colors_js_1.bold)(' Security Posture') + '\n');
388
- process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(47)) + '\n');
389
- for (const check of report.hygieneChecks) {
390
- const statusDisplay = check.status === 'pass' ? (0, colors_js_1.green)(check.detail)
391
- : check.status === 'fail' ? (0, colors_js_1.red)(check.detail)
392
- : check.status === 'warn' ? (0, colors_js_1.yellow)(check.detail)
393
- : (0, colors_js_1.dim)(check.detail);
394
- process.stdout.write(` ${(0, colors_js_1.dim)(check.label.padEnd(20))} ${statusDisplay}\n`);
395
- }
396
- process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(47)) + '\n');
397
- // Trust score
398
- const scoreColor = report.trustScore >= 80 ? colors_js_1.green
399
- : report.trustScore >= 60 ? colors_js_1.yellow
400
- : colors_js_1.red;
401
- process.stdout.write(` ${(0, colors_js_1.dim)('Trust Score')} ${scoreColor(`${report.trustScore} / 100`)} ${(0, colors_js_1.dim)('[Grade:')} ${scoreColor(report.grade)}${(0, colors_js_1.dim)(']')}\n`);
402
- // Shield posture
403
- const postureColor = report.postureScore >= 70 ? colors_js_1.green
404
- : report.postureScore >= 40 ? colors_js_1.yellow
405
- : colors_js_1.red;
406
- const riskColor = report.riskLevel === 'SECURE' || report.riskLevel === 'LOW' ? colors_js_1.green
407
- : report.riskLevel === 'MEDIUM' ? colors_js_1.yellow
782
+ // --- Findings section ---
783
+ if (report.findings.length > 0) {
784
+ process.stdout.write((0, colors_js_1.bold)(' Findings') + '\n');
785
+ process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(47)) + '\n');
786
+ const maxFindings = verbose ? report.findings.length : 5;
787
+ const displayFindings = report.findings.slice(0, maxFindings);
788
+ for (const finding of displayFindings) {
789
+ const sevTag = finding.severity === 'critical' ? (0, colors_js_1.red)('CRITICAL')
790
+ : finding.severity === 'high' ? (0, colors_js_1.yellow)('HIGH ')
791
+ : finding.severity === 'medium' ? (0, colors_js_1.cyan)('MEDIUM ')
792
+ : (0, colors_js_1.dim)('LOW ');
793
+ const countPrefix = finding.count > 1 ? `${finding.count} ` : '';
794
+ process.stdout.write(` ${sevTag} ${countPrefix}${(0, colors_js_1.bold)(finding.title)}\n`);
795
+ if (finding.explanation) {
796
+ const wrapped = (0, format_js_1.wordWrap)(finding.explanation, 70, 12);
797
+ process.stdout.write((0, colors_js_1.dim)(wrapped) + '\n');
798
+ }
799
+ // Show file locations (max 3 unless verbose)
800
+ if (finding.locations.length > 0) {
801
+ const maxLocs = verbose ? finding.locations.length : 3;
802
+ const locs = finding.locations.slice(0, maxLocs);
803
+ const locStrings = locs.map(l => {
804
+ const rel = path.relative(report.directory, l.file);
805
+ return `${rel}:${l.line}`;
806
+ });
807
+ process.stdout.write((0, colors_js_1.dim)(' ' + locStrings.join(' ')) + '\n');
808
+ if (finding.locations.length > maxLocs) {
809
+ process.stdout.write((0, colors_js_1.dim)(` +${finding.locations.length - maxLocs} more`) + '\n');
810
+ }
811
+ }
812
+ // Verification command
813
+ const verifyCmd = getVerificationCommand(finding, report.directory);
814
+ if (verifyCmd) {
815
+ process.stdout.write(` ${(0, colors_js_1.dim)('Verify:')} ${(0, colors_js_1.cyan)(verifyCmd)}\n`);
816
+ }
817
+ // Tool recommendation
818
+ const toolRec = getToolRecommendation(finding.findingId);
819
+ if (toolRec) {
820
+ process.stdout.write(` ${(0, colors_js_1.dim)('Fix:')} ${(0, colors_js_1.cyan)(toolRec.command)}\n`);
821
+ }
822
+ process.stdout.write('\n');
823
+ }
824
+ if (!verbose && report.findings.length > maxFindings) {
825
+ const remaining = report.findings.length - maxFindings;
826
+ process.stdout.write((0, colors_js_1.dim)(` [+${remaining} more finding${remaining === 1 ? '' : 's'} -- run with --verbose to see all]`) + '\n');
827
+ process.stdout.write('\n');
828
+ }
829
+ }
830
+ else {
831
+ process.stdout.write((0, colors_js_1.green)(' No security findings detected.') + '\n\n');
832
+ }
833
+ // --- Security Score ---
834
+ const scoreColor = report.securityScore >= 80 ? colors_js_1.green
835
+ : report.securityScore >= 60 ? colors_js_1.yellow
408
836
  : colors_js_1.red;
409
- process.stdout.write(` ${(0, colors_js_1.dim)('Shield Posture')} ${postureColor(`${report.postureScore} / 100`)} ${(0, colors_js_1.dim)('[Risk:')} ${riskColor(report.riskLevel)}${(0, colors_js_1.dim)(']')}\n`);
410
- process.stdout.write(` ${(0, colors_js_1.dim)('Products')} ${report.activeProducts} / ${report.totalProducts} active\n`);
837
+ const breakdown = report.scoreBreakdown;
838
+ process.stdout.write(` ${(0, colors_js_1.bold)('Security Score:')} ${scoreColor(`${report.securityScore}`)} ${(0, colors_js_1.dim)('/ 100')}\n`);
411
839
  process.stdout.write('\n');
412
- // Next steps
413
- if (report.nextSteps.length > 0) {
414
- process.stdout.write((0, colors_js_1.bold)(' Next Steps') + '\n');
840
+ // Breakdown as factual deductions
841
+ if (breakdown.credentials.deduction > 0) {
842
+ process.stdout.write(` ${(0, colors_js_1.dim)('Credentials')} ${(0, colors_js_1.red)(`-${breakdown.credentials.deduction}`)} ${(0, colors_js_1.dim)(breakdown.credentials.detail)}\n`);
843
+ }
844
+ if (breakdown.environment.deduction > 0) {
845
+ process.stdout.write(` ${(0, colors_js_1.dim)('Environment')} ${(0, colors_js_1.red)(`-${breakdown.environment.deduction}`)} ${(0, colors_js_1.dim)(breakdown.environment.detail)}\n`);
846
+ }
847
+ if (breakdown.configuration.deduction > 0) {
848
+ process.stdout.write(` ${(0, colors_js_1.dim)('Configuration')} ${(0, colors_js_1.red)(`-${breakdown.configuration.deduction}`)} ${(0, colors_js_1.dim)(breakdown.configuration.detail)}\n`);
849
+ }
850
+ else if (breakdown.configuration.deduction < 0) {
851
+ process.stdout.write(` ${(0, colors_js_1.dim)('Configuration')} ${(0, colors_js_1.green)(`+${Math.abs(breakdown.configuration.deduction)}`)} ${(0, colors_js_1.dim)(breakdown.configuration.detail)}\n`);
852
+ }
853
+ // "After fixes" line when improvements are possible
854
+ const totalRecoverable = breakdown.credentials.deduction
855
+ + breakdown.environment.deduction
856
+ + Math.max(0, breakdown.configuration.deduction);
857
+ const potentialScore = Math.min(100, report.securityScore + totalRecoverable);
858
+ if (totalRecoverable > 0 && potentialScore > report.securityScore) {
859
+ process.stdout.write('\n');
860
+ process.stdout.write(` ${(0, colors_js_1.dim)('After fixes:')} ${report.securityScore} ${(0, colors_js_1.dim)('->')} ${(0, colors_js_1.green)(String(potentialScore))}\n`);
861
+ }
862
+ process.stdout.write('\n');
863
+ // --- Recommendations section ---
864
+ if (report.actions.length > 0) {
865
+ process.stdout.write((0, colors_js_1.bold)(' Recommendations') + '\n');
415
866
  process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(47)) + '\n');
416
- for (const step of report.nextSteps) {
417
- const severityTag = step.severity === 'critical' ? (0, colors_js_1.red)(`[CRITICAL]`)
418
- : step.severity === 'high' ? (0, colors_js_1.yellow)(`[HIGH]`)
419
- : step.severity === 'medium' ? (0, colors_js_1.cyan)(`[MEDIUM]`)
420
- : (0, colors_js_1.dim)(`[LOW]`);
421
- process.stdout.write(` ${severityTag.padEnd(22)} ${step.description}\n`);
422
- process.stdout.write(` ${' '.repeat(12)} ${(0, colors_js_1.dim)(step.command)}\n\n`);
867
+ for (let i = 0; i < report.actions.length; i++) {
868
+ const action = report.actions[i];
869
+ process.stdout.write(` ${(0, colors_js_1.bold)(`${i + 1}.`)} ${action.description}\n`);
870
+ // Prose explanation as a paragraph (word-wrapped)
871
+ const wrapped = (0, format_js_1.wordWrap)(action.why, 70, 5);
872
+ process.stdout.write((0, colors_js_1.dim)(wrapped) + '\n');
873
+ // Command at bottom with $ prefix
874
+ process.stdout.write(` ${(0, colors_js_1.cyan)('$ ' + action.command)}\n`);
875
+ process.stdout.write('\n');
423
876
  }
424
877
  process.stdout.write((0, colors_js_1.gray)(' ' + '-'.repeat(47)) + '\n');
425
878
  }
426
879
  process.stdout.write('\n');
427
- // Quick start hints for new users
428
- process.stdout.write((0, colors_js_1.dim)(' Tip: Try these commands to explore further:') + '\n');
429
- process.stdout.write((0, colors_js_1.dim)(' opena2a shield status View Shield product status') + '\n');
430
- process.stdout.write((0, colors_js_1.dim)(' opena2a shield report Generate security posture report') + '\n');
431
- process.stdout.write((0, colors_js_1.dim)(' opena2a shield monitor Start ARP runtime monitoring') + '\n');
432
- process.stdout.write((0, colors_js_1.dim)(' opena2a ~<query> Search commands (e.g. opena2a ~drift)') + '\n');
880
+ // Contextual tip
881
+ const tip = getContextualTip(report);
882
+ process.stdout.write((0, colors_js_1.dim)(` Tip: ${tip.command} -- ${tip.text}`) + '\n');
433
883
  process.stdout.write('\n');
434
884
  }
435
885
  //# sourceMappingURL=init.js.map