tlc-claude-code 1.3.0 → 1.4.0

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 (105) hide show
  1. package/dashboard/dist/components/AuditPane.d.ts +30 -0
  2. package/dashboard/dist/components/AuditPane.js +127 -0
  3. package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/AuditPane.test.js +339 -0
  5. package/dashboard/dist/components/CompliancePane.d.ts +39 -0
  6. package/dashboard/dist/components/CompliancePane.js +96 -0
  7. package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
  8. package/dashboard/dist/components/CompliancePane.test.js +183 -0
  9. package/dashboard/dist/components/SSOPane.d.ts +36 -0
  10. package/dashboard/dist/components/SSOPane.js +71 -0
  11. package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
  12. package/dashboard/dist/components/SSOPane.test.js +155 -0
  13. package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
  14. package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
  15. package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
  16. package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
  17. package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
  18. package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
  19. package/package.json +1 -1
  20. package/server/lib/access-control-doc.js +541 -0
  21. package/server/lib/access-control-doc.test.js +672 -0
  22. package/server/lib/adr-generator.js +423 -0
  23. package/server/lib/adr-generator.test.js +586 -0
  24. package/server/lib/agent-progress-monitor.js +223 -0
  25. package/server/lib/agent-progress-monitor.test.js +202 -0
  26. package/server/lib/audit-attribution.js +191 -0
  27. package/server/lib/audit-attribution.test.js +359 -0
  28. package/server/lib/audit-classifier.js +202 -0
  29. package/server/lib/audit-classifier.test.js +209 -0
  30. package/server/lib/audit-command.js +275 -0
  31. package/server/lib/audit-command.test.js +325 -0
  32. package/server/lib/audit-exporter.js +380 -0
  33. package/server/lib/audit-exporter.test.js +464 -0
  34. package/server/lib/audit-logger.js +236 -0
  35. package/server/lib/audit-logger.test.js +364 -0
  36. package/server/lib/audit-query.js +257 -0
  37. package/server/lib/audit-query.test.js +352 -0
  38. package/server/lib/audit-storage.js +269 -0
  39. package/server/lib/audit-storage.test.js +272 -0
  40. package/server/lib/bulk-repo-init.js +342 -0
  41. package/server/lib/bulk-repo-init.test.js +388 -0
  42. package/server/lib/compliance-checklist.js +866 -0
  43. package/server/lib/compliance-checklist.test.js +476 -0
  44. package/server/lib/compliance-command.js +616 -0
  45. package/server/lib/compliance-command.test.js +551 -0
  46. package/server/lib/compliance-reporter.js +692 -0
  47. package/server/lib/compliance-reporter.test.js +707 -0
  48. package/server/lib/data-flow-doc.js +665 -0
  49. package/server/lib/data-flow-doc.test.js +659 -0
  50. package/server/lib/ephemeral-storage.js +249 -0
  51. package/server/lib/ephemeral-storage.test.js +254 -0
  52. package/server/lib/evidence-collector.js +627 -0
  53. package/server/lib/evidence-collector.test.js +901 -0
  54. package/server/lib/flow-diagram-generator.js +474 -0
  55. package/server/lib/flow-diagram-generator.test.js +446 -0
  56. package/server/lib/idp-manager.js +626 -0
  57. package/server/lib/idp-manager.test.js +587 -0
  58. package/server/lib/memory-exclusion.js +326 -0
  59. package/server/lib/memory-exclusion.test.js +241 -0
  60. package/server/lib/mfa-handler.js +452 -0
  61. package/server/lib/mfa-handler.test.js +490 -0
  62. package/server/lib/oauth-flow.js +375 -0
  63. package/server/lib/oauth-flow.test.js +487 -0
  64. package/server/lib/oauth-registry.js +190 -0
  65. package/server/lib/oauth-registry.test.js +306 -0
  66. package/server/lib/readme-generator.js +490 -0
  67. package/server/lib/readme-generator.test.js +493 -0
  68. package/server/lib/repo-dependency-tracker.js +261 -0
  69. package/server/lib/repo-dependency-tracker.test.js +350 -0
  70. package/server/lib/retention-policy.js +281 -0
  71. package/server/lib/retention-policy.test.js +486 -0
  72. package/server/lib/role-mapper.js +236 -0
  73. package/server/lib/role-mapper.test.js +395 -0
  74. package/server/lib/saml-provider.js +765 -0
  75. package/server/lib/saml-provider.test.js +643 -0
  76. package/server/lib/security-policy-generator.js +682 -0
  77. package/server/lib/security-policy-generator.test.js +544 -0
  78. package/server/lib/sensitive-detector.js +112 -0
  79. package/server/lib/sensitive-detector.test.js +209 -0
  80. package/server/lib/service-interaction-diagram.js +700 -0
  81. package/server/lib/service-interaction-diagram.test.js +638 -0
  82. package/server/lib/service-summary.js +553 -0
  83. package/server/lib/service-summary.test.js +619 -0
  84. package/server/lib/session-purge.js +460 -0
  85. package/server/lib/session-purge.test.js +312 -0
  86. package/server/lib/sso-command.js +544 -0
  87. package/server/lib/sso-command.test.js +552 -0
  88. package/server/lib/sso-session.js +492 -0
  89. package/server/lib/sso-session.test.js +670 -0
  90. package/server/lib/workspace-command.js +249 -0
  91. package/server/lib/workspace-command.test.js +264 -0
  92. package/server/lib/workspace-config.js +270 -0
  93. package/server/lib/workspace-config.test.js +312 -0
  94. package/server/lib/workspace-docs-command.js +547 -0
  95. package/server/lib/workspace-docs-command.test.js +692 -0
  96. package/server/lib/workspace-memory.js +451 -0
  97. package/server/lib/workspace-memory.test.js +403 -0
  98. package/server/lib/workspace-scanner.js +452 -0
  99. package/server/lib/workspace-scanner.test.js +677 -0
  100. package/server/lib/workspace-test-runner.js +315 -0
  101. package/server/lib/workspace-test-runner.test.js +294 -0
  102. package/server/lib/zero-retention-command.js +439 -0
  103. package/server/lib/zero-retention-command.test.js +448 -0
  104. package/server/lib/zero-retention.js +322 -0
  105. package/server/lib/zero-retention.test.js +258 -0
@@ -0,0 +1,616 @@
1
+ /**
2
+ * Compliance Command - CLI command for compliance operations
3
+ *
4
+ * Features:
5
+ * - tlc compliance status - show compliance status
6
+ * - tlc compliance checklist - show SOC 2 checklist
7
+ * - tlc compliance evidence - collect evidence
8
+ * - tlc compliance report - generate report
9
+ * - tlc compliance policies - generate policies
10
+ * - tlc compliance gaps - show gaps
11
+ */
12
+
13
+ import {
14
+ createComplianceChecklist,
15
+ getSOC2Checklist,
16
+ getCompliancePercentage,
17
+ getComplianceGaps,
18
+ getControlsByCategory,
19
+ TSC_CATEGORIES,
20
+ } from './compliance-checklist.js';
21
+
22
+ import {
23
+ createReporter,
24
+ generateReadinessReport,
25
+ formatReportMarkdown,
26
+ formatReportHTML,
27
+ calculateCategoryScores,
28
+ } from './compliance-reporter.js';
29
+
30
+ import {
31
+ createEvidenceCollector,
32
+ collectPolicyDocuments,
33
+ collectConfigSnapshot,
34
+ collectAuditLogs,
35
+ } from './evidence-collector.js';
36
+
37
+ import {
38
+ generateAccessControlPolicy,
39
+ generateDataProtectionPolicy,
40
+ generateIncidentResponsePolicy,
41
+ generateAuthPolicy,
42
+ generateAcceptableUsePolicy,
43
+ exportAsMarkdown,
44
+ } from './security-policy-generator.js';
45
+
46
+ const VALID_SUBCOMMANDS = ['status', 'checklist', 'evidence', 'report', 'policies', 'gaps'];
47
+ const VALID_FORMATS = ['text', 'markdown', 'html', 'json'];
48
+
49
+ /**
50
+ * Parse command line arguments
51
+ * @param {string[]} args - Command line arguments
52
+ * @returns {Object} Parsed options
53
+ */
54
+ export function parseArgs(args) {
55
+ const result = {
56
+ subcommand: null,
57
+ category: null,
58
+ format: 'text',
59
+ output: null,
60
+ unknownSubcommand: null,
61
+ };
62
+
63
+ for (let i = 0; i < args.length; i++) {
64
+ const arg = args[i];
65
+
66
+ // Check for subcommands first (non-flag arguments)
67
+ if (!arg.startsWith('--') && !arg.startsWith('-') && result.subcommand === null && result.unknownSubcommand === null) {
68
+ if (VALID_SUBCOMMANDS.includes(arg)) {
69
+ result.subcommand = arg;
70
+ } else {
71
+ result.unknownSubcommand = arg;
72
+ }
73
+ continue;
74
+ }
75
+
76
+ // Parse flags
77
+ if (arg === '--category') {
78
+ result.category = args[i + 1];
79
+ i++;
80
+ } else if (arg.startsWith('--category=')) {
81
+ result.category = arg.split('=')[1];
82
+ } else if (arg === '--format') {
83
+ result.format = args[i + 1];
84
+ i++;
85
+ } else if (arg.startsWith('--format=')) {
86
+ result.format = arg.split('=')[1];
87
+ } else if (arg === '--output') {
88
+ result.output = args[i + 1];
89
+ i++;
90
+ } else if (arg.startsWith('--output=')) {
91
+ result.output = arg.split('=')[1];
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ /**
99
+ * ComplianceCommand class - handles tlc compliance command
100
+ */
101
+ export class ComplianceCommand {
102
+ /**
103
+ * Create a ComplianceCommand instance
104
+ * @param {Object} options - Configuration options
105
+ * @param {Object} options.checklist - Compliance checklist instance
106
+ * @param {Object} options.reporter - Compliance reporter instance
107
+ * @param {Object} options.evidenceCollector - Evidence collector instance
108
+ * @param {Object} options.policyGenerator - Policy generator (optional)
109
+ * @param {string} options.projectDir - Project directory
110
+ */
111
+ constructor(options = {}) {
112
+ this.checklist = options.checklist || createComplianceChecklist();
113
+ this.reporter = options.reporter || createReporter({ checklist: this.checklist });
114
+ this.evidenceCollector = options.evidenceCollector || createEvidenceCollector();
115
+ this.policyGenerator = options.policyGenerator || {
116
+ generateAccessControlPolicy,
117
+ generateDataProtectionPolicy,
118
+ generateIncidentResponsePolicy,
119
+ generateAuthPolicy,
120
+ generateAcceptableUsePolicy,
121
+ };
122
+ this.projectDir = options.projectDir || process.cwd();
123
+ }
124
+
125
+ /**
126
+ * Execute the compliance command
127
+ * @param {string[]} args - Command arguments
128
+ * @returns {Promise<Object>} Result { success, output, error? }
129
+ */
130
+ async execute(args) {
131
+ const options = parseArgs(args);
132
+
133
+ try {
134
+ // Check for unknown subcommand first
135
+ if (options.unknownSubcommand) {
136
+ return {
137
+ success: false,
138
+ output: '',
139
+ error: `Unknown subcommand: ${options.unknownSubcommand}. Valid commands: ${VALID_SUBCOMMANDS.join(', ')}`,
140
+ };
141
+ }
142
+
143
+ // Default to status if no subcommand
144
+ const subcommand = options.subcommand || 'status';
145
+
146
+ switch (subcommand) {
147
+ case 'status':
148
+ return await this.handleStatus(options);
149
+ case 'checklist':
150
+ return await this.handleChecklist(options);
151
+ case 'evidence':
152
+ return await this.handleEvidence(options);
153
+ case 'report':
154
+ return await this.handleReport(options);
155
+ case 'policies':
156
+ return await this.handlePolicies(options);
157
+ case 'gaps':
158
+ return await this.handleGaps(options);
159
+ default:
160
+ return {
161
+ success: false,
162
+ output: '',
163
+ error: `Unknown subcommand: ${subcommand}. Valid commands: ${VALID_SUBCOMMANDS.join(', ')}`,
164
+ };
165
+ }
166
+ } catch (error) {
167
+ return {
168
+ success: false,
169
+ output: '',
170
+ error: error.message,
171
+ };
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Handle status subcommand
177
+ * @param {Object} options - Parsed options
178
+ * @returns {Promise<Object>} Result
179
+ */
180
+ async handleStatus(options) {
181
+ const compliance = getCompliancePercentage(this.checklist);
182
+ const gaps = getComplianceGaps(this.checklist);
183
+ const output = this.formatStatus(compliance, gaps);
184
+
185
+ return {
186
+ success: true,
187
+ output,
188
+ };
189
+ }
190
+
191
+ /**
192
+ * Handle checklist subcommand
193
+ * @param {Object} options - Parsed options
194
+ * @returns {Promise<Object>} Result
195
+ */
196
+ async handleChecklist(options) {
197
+ let controls;
198
+
199
+ if (options.category) {
200
+ const byCategory = getControlsByCategory(this.checklist, options.category);
201
+ controls = byCategory[options.category] || [];
202
+ } else {
203
+ controls = getSOC2Checklist(this.checklist);
204
+ }
205
+
206
+ const output = this.formatChecklist(controls, options);
207
+
208
+ return {
209
+ success: true,
210
+ output,
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Handle evidence subcommand
216
+ * @param {Object} options - Parsed options
217
+ * @returns {Promise<Object>} Result
218
+ */
219
+ async handleEvidence(options) {
220
+ // Collect various evidence types
221
+ const evidenceItems = this.evidenceCollector.getAll ? this.evidenceCollector.getAll() : [];
222
+
223
+ const output = this.formatEvidence(evidenceItems);
224
+
225
+ return {
226
+ success: true,
227
+ output,
228
+ };
229
+ }
230
+
231
+ /**
232
+ * Handle report subcommand
233
+ * @param {Object} options - Parsed options
234
+ * @returns {Promise<Object>} Result
235
+ */
236
+ async handleReport(options) {
237
+ const report = this.reporter.generateReadinessReport
238
+ ? this.reporter.generateReadinessReport()
239
+ : generateReadinessReport(this.reporter);
240
+
241
+ let output;
242
+ switch (options.format) {
243
+ case 'markdown':
244
+ output = this.reporter.formatReportMarkdown
245
+ ? this.reporter.formatReportMarkdown(report)
246
+ : formatReportMarkdown(report);
247
+ break;
248
+ case 'html':
249
+ output = this.reporter.formatReportHTML
250
+ ? this.reporter.formatReportHTML(report)
251
+ : formatReportHTML(report);
252
+ break;
253
+ case 'json':
254
+ output = JSON.stringify(report, null, 2);
255
+ break;
256
+ default:
257
+ output = this.formatReportText(report);
258
+ }
259
+
260
+ return {
261
+ success: true,
262
+ output,
263
+ };
264
+ }
265
+
266
+ /**
267
+ * Handle policies subcommand
268
+ * @param {Object} options - Parsed options
269
+ * @returns {Promise<Object>} Result
270
+ */
271
+ async handlePolicies(options) {
272
+ const policies = [
273
+ { name: 'Access Control', policy: this.policyGenerator.generateAccessControlPolicy() },
274
+ { name: 'Data Protection', policy: this.policyGenerator.generateDataProtectionPolicy() },
275
+ { name: 'Incident Response', policy: this.policyGenerator.generateIncidentResponsePolicy() },
276
+ { name: 'Authentication', policy: this.policyGenerator.generateAuthPolicy() },
277
+ { name: 'Acceptable Use', policy: this.policyGenerator.generateAcceptableUsePolicy() },
278
+ ];
279
+
280
+ const output = this.formatPolicies(policies);
281
+
282
+ return {
283
+ success: true,
284
+ output,
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Handle gaps subcommand
290
+ * @param {Object} options - Parsed options
291
+ * @returns {Promise<Object>} Result
292
+ */
293
+ async handleGaps(options) {
294
+ const gaps = getComplianceGaps(this.checklist);
295
+ const output = this.formatGaps(gaps);
296
+
297
+ return {
298
+ success: true,
299
+ output,
300
+ };
301
+ }
302
+
303
+ /**
304
+ * Format compliance status for display
305
+ * @param {Object} compliance - Compliance metrics
306
+ * @param {Array} gaps - List of gaps
307
+ * @returns {string} Formatted output
308
+ */
309
+ formatStatus(compliance, gaps = []) {
310
+ const lines = [];
311
+
312
+ lines.push('Compliance Status');
313
+ lines.push('=================');
314
+ lines.push('');
315
+
316
+ // Overall score with progress bar
317
+ const percentage = Math.round(compliance.percentage);
318
+ const progressBar = this.createProgressBar(percentage);
319
+ lines.push(` Overall Score: ${percentage}% ${progressBar}`);
320
+
321
+ // Risk level based on compliance
322
+ const riskLevel = this.calculateRiskLevel(percentage);
323
+ lines.push(` Risk Level: ${riskLevel}`);
324
+ lines.push('');
325
+
326
+ // Category breakdown
327
+ lines.push(' Category Breakdown:');
328
+
329
+ if (compliance.byCategory) {
330
+ for (const [category, data] of Object.entries(compliance.byCategory)) {
331
+ const catPercentage = Math.round(data.percentage);
332
+ const catBar = this.createProgressBar(catPercentage, 20);
333
+ lines.push(` ${category.padEnd(22)} ${String(catPercentage).padStart(3)}% ${catBar}`);
334
+ }
335
+ }
336
+
337
+ lines.push('');
338
+
339
+ // Gap summary
340
+ const gapCount = gaps.length || compliance.notImplemented + (compliance.partial || 0);
341
+ lines.push(` Gaps: ${gapCount} controls need attention`);
342
+ lines.push(" Run 'tlc compliance gaps' for details");
343
+
344
+ return lines.join('\n');
345
+ }
346
+
347
+ /**
348
+ * Format checklist for display
349
+ * @param {Array} controls - List of controls
350
+ * @param {Object} options - Display options
351
+ * @returns {string} Formatted output
352
+ */
353
+ formatChecklist(controls, options = {}) {
354
+ const lines = [];
355
+
356
+ lines.push('SOC 2 Compliance Checklist');
357
+ lines.push('==========================');
358
+ lines.push('');
359
+
360
+ // Group by category if not filtered
361
+ const byCategory = {};
362
+ for (const control of controls) {
363
+ const cat = control.category || 'Uncategorized';
364
+ if (!byCategory[cat]) {
365
+ byCategory[cat] = [];
366
+ }
367
+ byCategory[cat].push(control);
368
+ }
369
+
370
+ for (const [category, categoryControls] of Object.entries(byCategory)) {
371
+ lines.push(`${category}`);
372
+ lines.push('-'.repeat(category.length));
373
+
374
+ for (const control of categoryControls) {
375
+ const statusIcon = this.getStatusIcon(control.status);
376
+ lines.push(` ${statusIcon} [${control.id}] ${control.name}`);
377
+ if (control.status !== 'implemented') {
378
+ lines.push(` Status: ${control.status}`);
379
+ }
380
+ }
381
+ lines.push('');
382
+ }
383
+
384
+ return lines.join('\n');
385
+ }
386
+
387
+ /**
388
+ * Format evidence for display
389
+ * @param {Array} evidenceItems - List of evidence items
390
+ * @returns {string} Formatted output
391
+ */
392
+ formatEvidence(evidenceItems) {
393
+ const lines = [];
394
+
395
+ lines.push('Evidence Collection');
396
+ lines.push('===================');
397
+ lines.push('');
398
+
399
+ if (!evidenceItems || evidenceItems.length === 0) {
400
+ lines.push(' No evidence items collected yet.');
401
+ lines.push('');
402
+ lines.push(' To collect evidence, run:');
403
+ lines.push(' tlc compliance evidence --collect');
404
+ return lines.join('\n');
405
+ }
406
+
407
+ // Group by type
408
+ const byType = {};
409
+ for (const item of evidenceItems) {
410
+ const type = item.type || 'other';
411
+ if (!byType[type]) {
412
+ byType[type] = [];
413
+ }
414
+ byType[type].push(item);
415
+ }
416
+
417
+ lines.push(` Total Items: ${evidenceItems.length}`);
418
+ lines.push('');
419
+
420
+ for (const [type, items] of Object.entries(byType)) {
421
+ lines.push(` ${type.charAt(0).toUpperCase() + type.slice(1)} (${items.length}):`);
422
+ for (const item of items.slice(0, 5)) {
423
+ lines.push(` - ${item.id}: ${item.title || item.type}`);
424
+ }
425
+ if (items.length > 5) {
426
+ lines.push(` ... and ${items.length - 5} more`);
427
+ }
428
+ lines.push('');
429
+ }
430
+
431
+ return lines.join('\n');
432
+ }
433
+
434
+ /**
435
+ * Format report as text
436
+ * @param {Object} report - Generated report
437
+ * @returns {string} Formatted output
438
+ */
439
+ formatReportText(report) {
440
+ const lines = [];
441
+
442
+ lines.push(report.title || 'SOC 2 Type II Readiness Report');
443
+ lines.push('='.repeat(40));
444
+ lines.push('');
445
+
446
+ if (report.summary) {
447
+ lines.push('Summary');
448
+ lines.push('-------');
449
+ lines.push(` Overall Score: ${report.summary.overallScore}%`);
450
+ lines.push(` Risk Level: ${report.summary.riskLevel}`);
451
+ lines.push(` Total Controls: ${report.summary.totalControls}`);
452
+ lines.push(` Implemented: ${report.summary.implemented}`);
453
+ lines.push(` Partial: ${report.summary.partial}`);
454
+ lines.push(` Gaps: ${report.summary.gaps}`);
455
+ lines.push('');
456
+ }
457
+
458
+ if (report.categories) {
459
+ lines.push('Categories');
460
+ lines.push('----------');
461
+ for (const [category, data] of Object.entries(report.categories)) {
462
+ lines.push(` ${category}: ${data.score}%`);
463
+ }
464
+ lines.push('');
465
+ }
466
+
467
+ if (report.recommendations && report.recommendations.length > 0) {
468
+ lines.push('Top Recommendations');
469
+ lines.push('-------------------');
470
+ for (const rec of report.recommendations.slice(0, 5)) {
471
+ lines.push(` - [${rec.controlId}] ${rec.action}`);
472
+ }
473
+ }
474
+
475
+ return lines.join('\n');
476
+ }
477
+
478
+ /**
479
+ * Format policies for display
480
+ * @param {Array} policies - List of policy objects
481
+ * @returns {string} Formatted output
482
+ */
483
+ formatPolicies(policies) {
484
+ const lines = [];
485
+
486
+ lines.push('Security Policies');
487
+ lines.push('=================');
488
+ lines.push('');
489
+
490
+ lines.push(' Available Policies:');
491
+ lines.push('');
492
+
493
+ for (const { name, policy } of policies) {
494
+ lines.push(` [*] ${name}`);
495
+ if (policy && policy.title) {
496
+ lines.push(` Title: ${policy.title}`);
497
+ }
498
+ if (policy && policy.sections) {
499
+ lines.push(` Sections: ${policy.sections.length}`);
500
+ }
501
+ }
502
+
503
+ lines.push('');
504
+ lines.push(' To export a policy, run:');
505
+ lines.push(' tlc compliance policies --export <type> --format markdown');
506
+
507
+ return lines.join('\n');
508
+ }
509
+
510
+ /**
511
+ * Format gaps for display
512
+ * @param {Array} gaps - List of gap controls
513
+ * @returns {string} Formatted output
514
+ */
515
+ formatGaps(gaps) {
516
+ const lines = [];
517
+
518
+ lines.push('Compliance Gaps');
519
+ lines.push('===============');
520
+ lines.push('');
521
+
522
+ if (!gaps || gaps.length === 0) {
523
+ lines.push(' No gaps found - all controls are implemented!');
524
+ return lines.join('\n');
525
+ }
526
+
527
+ // Group by severity
528
+ const bySeverity = {
529
+ high: [],
530
+ medium: [],
531
+ low: [],
532
+ };
533
+
534
+ for (const gap of gaps) {
535
+ const severity = gap.gapSeverity || 'medium';
536
+ if (bySeverity[severity]) {
537
+ bySeverity[severity].push(gap);
538
+ } else {
539
+ bySeverity.medium.push(gap);
540
+ }
541
+ }
542
+
543
+ // Display high priority first
544
+ if (bySeverity.high.length > 0) {
545
+ lines.push(' High Priority:');
546
+ for (const gap of bySeverity.high) {
547
+ lines.push(` [${gap.id}] ${gap.name} - ${gap.status}`);
548
+ }
549
+ lines.push('');
550
+ }
551
+
552
+ if (bySeverity.medium.length > 0) {
553
+ lines.push(' Medium Priority:');
554
+ for (const gap of bySeverity.medium) {
555
+ lines.push(` [${gap.id}] ${gap.name} - ${gap.status}`);
556
+ }
557
+ lines.push('');
558
+ }
559
+
560
+ if (bySeverity.low.length > 0) {
561
+ lines.push(' Low Priority:');
562
+ for (const gap of bySeverity.low) {
563
+ lines.push(` [${gap.id}] ${gap.name} - ${gap.status}`);
564
+ }
565
+ lines.push('');
566
+ }
567
+
568
+ lines.push(` Total gaps: ${gaps.length}`);
569
+
570
+ return lines.join('\n');
571
+ }
572
+
573
+ /**
574
+ * Create a progress bar string
575
+ * @param {number} percentage - Percentage (0-100)
576
+ * @param {number} width - Bar width in characters
577
+ * @returns {string} Progress bar
578
+ */
579
+ createProgressBar(percentage, width = 22) {
580
+ const filled = Math.round((percentage / 100) * width);
581
+ const empty = width - filled;
582
+ return '\u2588'.repeat(filled) + '\u2591'.repeat(empty);
583
+ }
584
+
585
+ /**
586
+ * Calculate risk level from compliance percentage
587
+ * @param {number} percentage - Compliance percentage
588
+ * @returns {string} Risk level
589
+ */
590
+ calculateRiskLevel(percentage) {
591
+ if (percentage >= 90) return 'Low';
592
+ if (percentage >= 70) return 'Medium';
593
+ if (percentage >= 50) return 'High';
594
+ return 'Critical';
595
+ }
596
+
597
+ /**
598
+ * Get status icon for a control
599
+ * @param {string} status - Control status
600
+ * @returns {string} Status icon
601
+ */
602
+ getStatusIcon(status) {
603
+ switch (status) {
604
+ case 'implemented':
605
+ return '[x]';
606
+ case 'partial':
607
+ return '[~]';
608
+ case 'not_implemented':
609
+ return '[ ]';
610
+ case 'not_applicable':
611
+ return '[N/A]';
612
+ default:
613
+ return '[ ]';
614
+ }
615
+ }
616
+ }