@greenarmor/ges-web-dashboard 1.2.2 → 1.2.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.
package/dist/index.js CHANGED
@@ -189,7 +189,7 @@ export function collectDashboardData(projectPath) {
189
189
  projectName: config?.project_name || "Unknown Project",
190
190
  projectType: config?.project_type || "unknown",
191
191
  frameworks: config?.frameworks || [],
192
- gesfVersion: "1.2.2",
192
+ gesfVersion: "1.2.4",
193
193
  score,
194
194
  controls,
195
195
  findings,
package/dist/template.js CHANGED
@@ -1,3 +1,29 @@
1
+ function matchPackForControl(controlId, packs) {
2
+ const idUpper = controlId.toUpperCase();
3
+ for (const p of packs) {
4
+ if (p.id === "gdpr" && idUpper.startsWith("GDPR-"))
5
+ return p;
6
+ if (p.id === "owasp" && idUpper.startsWith("OWASP-"))
7
+ return p;
8
+ if (p.id === "cis" && idUpper.startsWith("CIS-"))
9
+ return p;
10
+ if (p.id === "nist" && idUpper.startsWith("NIST-"))
11
+ return p;
12
+ if (p.id === "ai" && idUpper.startsWith("AI-"))
13
+ return p;
14
+ if (p.id === "blockchain" && idUpper.startsWith("BC-"))
15
+ return p;
16
+ if (p.id === "government" && idUpper.startsWith("GOV-"))
17
+ return p;
18
+ if (p.id === "iso27001" && idUpper.startsWith("ISO27K-"))
19
+ return p;
20
+ if (p.id === "iso27701" && idUpper.startsWith("ISO277-"))
21
+ return p;
22
+ if (p.id === "hipaa" && idUpper.startsWith("HIPAA-"))
23
+ return p;
24
+ }
25
+ return undefined;
26
+ }
1
27
  function gradeColor(grade) {
2
28
  switch (grade) {
3
29
  case "A": return "#22c55e";
@@ -106,6 +132,45 @@ export function renderDashboard(data) {
106
132
  ...p,
107
133
  _controls: undefined,
108
134
  })));
135
+ const complianceIssues = controls
136
+ .filter(c => c.status !== "pass" && c.status !== "not-applicable")
137
+ .map(c => {
138
+ const pack = matchPackForControl(c.id, packs);
139
+ const auditFindings = findings.filter(f => f.controlIds.includes(c.id));
140
+ return {
141
+ controlId: c.id,
142
+ controlName: c.name,
143
+ severity: c.severity,
144
+ status: c.status,
145
+ category: c.category,
146
+ framework: c.framework,
147
+ article: c.article,
148
+ description: c.description,
149
+ implementation_guidance: c.implementation_guidance,
150
+ packId: pack?.id || "",
151
+ packName: pack?.name || "Direct",
152
+ passedChecks: c.checks.filter(ch => ch.status === "pass").length,
153
+ totalChecks: c.checks.length,
154
+ auditFindings,
155
+ };
156
+ })
157
+ .sort((a, b) => {
158
+ const sevOrder = { critical: 0, high: 1, medium: 2, low: 3 };
159
+ return (sevOrder[a.severity] ?? 4) - (sevOrder[b.severity] ?? 4);
160
+ });
161
+ const issuesBySeverity = {
162
+ critical: complianceIssues.filter(i => i.severity === "critical").length,
163
+ high: complianceIssues.filter(i => i.severity === "high").length,
164
+ medium: complianceIssues.filter(i => i.severity === "medium").length,
165
+ low: complianceIssues.filter(i => i.severity === "low").length,
166
+ };
167
+ const issuesByPackId = {};
168
+ for (const issue of complianceIssues) {
169
+ const key = issue.packId || "direct";
170
+ if (!issuesByPackId[key])
171
+ issuesByPackId[key] = [];
172
+ issuesByPackId[key].push(issue);
173
+ }
109
174
  return `<!DOCTYPE html>
110
175
  <html lang="en">
111
176
  <head>
@@ -405,100 +470,97 @@ export function renderDashboard(data) {
405
470
  <div id="page-fixes" class="page">
406
471
  <div class="tab-bar" style="margin-bottom:0;">
407
472
  <button class="tab-btn active" onclick="showFixesTab('history', this)">Fix History (${data.fixHistory.length})</button>
408
- <button class="tab-btn" onclick="showFixesTab('pending', this)">Pending Fixes (${findings.length})</button>
473
+ <button class="tab-btn" onclick="showFixesTab('pending', this)">Pending Fixes (${complianceIssues.length})</button>
409
474
  </div>
410
475
 
411
476
  <div id="fixes-tab-history" class="tab-panel active">
412
- ${renderFixHistorySection(data.fixHistory)}
477
+ ${renderFixHistorySection(data.fixHistory, complianceIssues)}
413
478
  </div>
414
479
 
415
480
  <div id="fixes-tab-pending" class="tab-panel">
416
- ${renderDetailedFixesList(findings, controls, packs)}
481
+ ${renderComplianceFixCards(complianceIssues, "fix")}
417
482
  </div>
418
483
  </div>
419
484
 
420
485
  <div id="page-findings" class="page">
421
486
  <div id="findings-main">
422
- <h2 style="font-size:20px;font-weight:700;margin-bottom:16px;">Security Findings Report</h2>
487
+ <h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Compliance Findings &amp; Issues</h2>
488
+ <p style="color:#6b7280;font-size:14px;margin-bottom:20px;">Every control that is not passing is a compliance finding. Code-level audit evidence is shown where available.</p>
489
+
490
+ <div class="grid grid-4" style="margin-bottom:20px;">
491
+ <div class="card stat"><div class="num" style="color:${complianceIssues.length > 0 ? '#ef4444' : '#22c55e'};">${complianceIssues.length}</div><div class="label">Total Issues</div></div>
492
+ <div class="card stat"><div class="num" style="color:#ef4444;">${issuesBySeverity.critical}</div><div class="label">Critical</div></div>
493
+ <div class="card stat"><div class="num" style="color:#f97316;">${issuesBySeverity.high}</div><div class="label">High</div></div>
494
+ <div class="card stat"><div class="num">${findings.length}</div><div class="label">Audit Evidence</div></div>
495
+ </div>
496
+
423
497
  <div class="tab-bar">
424
- <button class="tab-btn active" onclick="showFindingsTab('all', this)">All (${findings.length})</button>
425
- <button class="tab-btn" onclick="showFindingsTab('critical', this)">Critical (${findingsBySeverity.critical})</button>
426
- <button class="tab-btn" onclick="showFindingsTab('high', this)">High (${findingsBySeverity.high})</button>
427
- <button class="tab-btn" onclick="showFindingsTab('medium', this)">Medium (${findingsBySeverity.medium})</button>
428
- <button class="tab-btn" onclick="showFindingsTab('low', this)">Low (${findingsBySeverity.low})</button>
498
+ <button class="tab-btn active" onclick="showFindingsTab('all', this)">All Issues (${complianceIssues.length})</button>
499
+ <button class="tab-btn" onclick="showFindingsTab('critical', this)">Critical (${issuesBySeverity.critical})</button>
500
+ <button class="tab-btn" onclick="showFindingsTab('high', this)">High (${issuesBySeverity.high})</button>
501
+ <button class="tab-btn" onclick="showFindingsTab('medium', this)">Medium (${issuesBySeverity.medium})</button>
502
+ <button class="tab-btn" onclick="showFindingsTab('low', this)">Low (${issuesBySeverity.low})</button>
429
503
  <button class="tab-btn" onclick="showFindingsTab('bypack', this)">By Pack</button>
504
+ ${findings.length > 0 ? `<button class="tab-btn" onclick="showFindingsTab('evidence', this)">Audit Evidence (${findings.length})</button>` : ''}
430
505
  </div>
431
506
 
432
507
  <div id="findings-tab-all" class="tab-panel active">
433
- ${renderFindingsTable(findings)}
508
+ ${renderComplianceIssuesTable(complianceIssues)}
434
509
  </div>
435
- <div id="findings-tab-critical" class="tab-panel">${renderFindingsTable(findings.filter(f => f.severity === "critical"))}</div>
436
- <div id="findings-tab-high" class="tab-panel">${renderFindingsTable(findings.filter(f => f.severity === "high"))}</div>
437
- <div id="findings-tab-medium" class="tab-panel">${renderFindingsTable(findings.filter(f => f.severity === "medium"))}</div>
438
- <div id="findings-tab-low" class="tab-panel">${renderFindingsTable(findings.filter(f => f.severity === "low"))}</div>
510
+ <div id="findings-tab-critical" class="tab-panel">${renderComplianceIssuesTable(complianceIssues.filter(i => i.severity === "critical"))}</div>
511
+ <div id="findings-tab-high" class="tab-panel">${renderComplianceIssuesTable(complianceIssues.filter(i => i.severity === "high"))}</div>
512
+ <div id="findings-tab-medium" class="tab-panel">${renderComplianceIssuesTable(complianceIssues.filter(i => i.severity === "medium"))}</div>
513
+ <div id="findings-tab-low" class="tab-panel">${renderComplianceIssuesTable(complianceIssues.filter(i => i.severity === "low"))}</div>
439
514
  <div id="findings-tab-bypack" class="tab-panel">
440
- ${packs.filter(p => (findingsByPackId[p.id] || []).length > 0).length > 0 ? packs.filter(p => (findingsByPackId[p.id] || []).length > 0).map(p => `
515
+ ${packs.filter(p => (issuesByPackId[p.id] || []).length > 0).length > 0 ? packs.filter(p => (issuesByPackId[p.id] || []).length > 0).map(p => `
441
516
  <div class="card" style="margin-bottom:16px;">
442
517
  <div class="card-title" style="cursor:pointer;" onclick="loadPackDetail('${p.id}')">
443
- ${escapeHtml(p.name)} &mdash; ${(findingsByPackId[p.id] || []).length} findings
518
+ ${escapeHtml(p.name)} &mdash; ${(issuesByPackId[p.id] || []).length} issues
444
519
  <span style="float:right;color:#0f766e;font-weight:400;font-size:11px;">View pack details &rarr;</span>
445
520
  </div>
446
- ${renderFindingsTable(findingsByPackId[p.id] || [])}
521
+ ${renderComplianceIssuesTable(issuesByPackId[p.id] || [])}
447
522
  </div>
448
- `).join('') : '<div class="empty-state"><div class="msg">No findings mapped to policy packs</div></div>'}
523
+ `).join('') : '<div class="empty-state"><div class="msg">No compliance issues mapped to policy packs</div></div>'}
449
524
  </div>
525
+ ${findings.length > 0 ? `<div id="findings-tab-evidence" class="tab-panel">${renderFindingsTable(findings)}</div>` : ''}
450
526
  </div>
451
527
  <div id="finding-detail" style="display:none;"></div>
452
528
  </div>
453
529
 
454
530
  <div id="page-traceability" class="page">
455
- <h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Fix Traceability Matrix</h2>
456
- <p style="color:#6b7280;font-size:14px;margin-bottom:20px;">Finding &rarr; Fix &rarr; Control &rarr; Policy Pack traceability for every security issue.</p>
531
+ <h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Compliance Traceability Matrix</h2>
532
+ <p style="color:#6b7280;font-size:14px;margin-bottom:20px;">Full traceability: Control &rarr; Framework &rarr; Policy Pack &rarr; Severity &rarr; Fix Guidance for every compliance issue.</p>
457
533
  <div class="tab-bar">
458
- <button class="tab-btn active" onclick="showTraceTab('matrix', this)">Matrix</button>
534
+ <button class="tab-btn active" onclick="showTraceTab('matrix', this)">Matrix (${complianceIssues.length})</button>
459
535
  <button class="tab-btn" onclick="showTraceTab('fixes', this)">Prioritized Fixes</button>
460
536
  <button class="tab-btn" onclick="showTraceTab('controls', this)">Control Coverage</button>
461
537
  </div>
462
538
 
463
539
  <div id="trace-tab-matrix" class="tab-panel active">
464
- ${findings.length > 0 ? `<div class="card">
540
+ ${complianceIssues.length > 0 ? `<div class="card">
465
541
  <table>
466
- <thead><tr><th>Finding</th><th>Severity</th><th>File</th><th>Linked Controls</th><th>Policy Pack</th><th>Fix Guidance</th></tr></thead>
542
+ <thead><tr><th>Control</th><th>Severity</th><th>Framework</th><th>Policy Pack</th><th>Status</th><th>Checks</th><th>Audit</th><th>Fix Guidance</th></tr></thead>
467
543
  <tbody>
468
- ${findings.slice(0, 50).map(f => {
469
- const linkedControls = controls.filter(c => f.controlIds.includes(c.id));
470
- const linkedPackIds = new Set();
471
- for (const ctrl of linkedControls) {
472
- const pk = packs.find(pp => {
473
- const pCtrls = getAllControlsForPack(pp.id, controls);
474
- return pCtrls.some(c2 => c2.id === ctrl.id);
475
- });
476
- if (pk)
477
- linkedPackIds.add(pk.id);
478
- }
479
- return `<tr>
480
- <td>
481
- <div style="font-weight:600;font-size:13px;">${escapeHtml(f.title)}</div>
482
- <div style="font-size:11px;color:#6b7280;">${escapeHtml(f.ruleId)}</div>
483
- </td>
484
- <td><span class="badge badge-sev" style="background:${severityColor(f.severity)}">${f.severity.toUpperCase()}</span></td>
485
- <td style="font-family:monospace;font-size:11px;">${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</td>
486
- <td>${linkedControls.length > 0 ? linkedControls.map(c => `<div style="margin-bottom:2px;"><span class="link" onclick="showControlDetail('${escapeHtml(c.id)}')">${escapeHtml(c.id)}</span> <span style="color:#6b7280;font-size:11px;">${escapeHtml(c.name)}</span></div>`).join('') : '<span style="color:#9ca3af;">No linked controls</span>'}</td>
487
- <td>${linkedPackIds.size > 0 ? [...linkedPackIds].map(pid => {
488
- const pk = packs.find(pp => pp.id === pid);
489
- return pk ? `<span class="tag" style="cursor:pointer;" onclick="loadPackDetail('${pk.id}')">${escapeHtml(pk.name)}</span>` : '';
490
- }).join(' ') : '<span style="color:#9ca3af;">-</span>'}</td>
491
- <td style="max-width:300px;font-size:12px;color:#374151;">${escapeHtml(f.fix)}</td>
492
- </tr>`;
493
- }).join('')}
544
+ ${complianceIssues.map(issue => `<tr>
545
+ <td>
546
+ <span class="link" onclick="showControlDetail('${escapeHtml(issue.controlId)}')">${escapeHtml(issue.controlId)}</span>
547
+ <div style="font-size:12px;color:#4b5563;">${escapeHtml(issue.controlName)}</div>
548
+ </td>
549
+ <td><span class="badge badge-sev" style="background:${severityColor(issue.severity)}">${issue.severity.toUpperCase()}</span></td>
550
+ <td style="font-size:12px;">${escapeHtml(issue.framework)}${issue.article ? '<br><span style="color:#6b7280;">' + escapeHtml(issue.article) + '</span>' : ''}</td>
551
+ <td>${issue.packId ? `<span class="tag" style="cursor:pointer;" onclick="loadPackDetail('${issue.packId}')">${escapeHtml(issue.packName)}</span>` : '<span style="color:#9ca3af;">-</span>'}</td>
552
+ <td><span class="badge badge-status" style="background:${statusColor(issue.status)}">${statusLabel(issue.status)}</span></td>
553
+ <td style="font-size:12px;">${issue.passedChecks}/${issue.totalChecks}</td>
554
+ <td>${issue.auditFindings.length > 0 ? `<span style="color:#ef4444;font-weight:600;">${issue.auditFindings.length}</span>` : '<span style="color:#9ca3af;">0</span>'}</td>
555
+ <td style="max-width:280px;font-size:12px;color:#374151;">${escapeHtml(issue.implementation_guidance.substring(0, 150))}${issue.implementation_guidance.length > 150 ? '...' : ''}</td>
556
+ </tr>`).join('')}
494
557
  </tbody>
495
558
  </table>
496
- ${findings.length > 50 ? `<div style="text-align:center;padding:8px;color:#9ca3af;font-size:12px;">Showing 50 of ${findings.length} findings</div>` : ''}
497
- </div>` : '<div class="card"><div class="empty-state"><div class="icon">&#10003;</div><div class="msg" style="color:#22c55e;">No findings to trace</div><div class="sub">All clear</div></div></div>'}
559
+ </div>` : '<div class="card"><div class="empty-state"><div class="icon">&#10003;</div><div class="msg" style="color:#22c55e;">No issues to trace</div><div class="sub">All controls passing</div></div></div>'}
498
560
  </div>
499
561
 
500
562
  <div id="trace-tab-fixes" class="tab-panel">
501
- ${renderDetailedFixesList(findings, controls, packs)}
563
+ ${renderComplianceFixCards(complianceIssues, "trace")}
502
564
  </div>
503
565
 
504
566
  <div id="trace-tab-controls" class="tab-panel">
@@ -597,6 +659,11 @@ export function renderDashboard(data) {
597
659
  if (btn) btn.classList.add('active');
598
660
  };
599
661
 
662
+ window.goToPendingFixes = function() {
663
+ var btns = document.querySelectorAll('#page-fixes .tab-btn');
664
+ showFixesTab('pending', btns.length > 1 ? btns[1] : (btns[0] || null));
665
+ };
666
+
600
667
  window.showTraceTab = function(tab, btn) {
601
668
  var panels = document.querySelectorAll('#page-traceability .tab-panel');
602
669
  for (var i = 0; i < panels.length; i++) panels[i].classList.remove('active');
@@ -703,7 +770,7 @@ export function renderDashboard(data) {
703
770
 
704
771
  html += '<div class="tab-bar">';
705
772
  html += '<button class="tab-btn active" onclick="showPackTab(\\'all\\',this)">All Controls (' + controls.length + ')</button>';
706
- html += '<button class="tab-btn" onclick="showPackTab(\\'failing\\',this)">Failing (' + (controls.filter(function(c){return c.status!==\\'pass\\'&&c.status!==\\'not-applicable\\'}).length) + ')</button>';
773
+ html += '<button class="tab-btn" onclick="showPackTab(\\'failing\\',this)">Failing (' + (controls.filter(function(c){return c.status!=="pass"&&c.status!=="not-applicable"}).length) + ')</button>';
707
774
  html += '<button class="tab-btn" onclick="showPackTab(\\'withfindings\\',this)">With Findings (' + (controls.filter(function(c){return c.relatedFindings.length>0}).length) + ')</button>';
708
775
  html += '</div>';
709
776
 
@@ -711,7 +778,7 @@ export function renderDashboard(data) {
711
778
  html += renderControlsTable(controls);
712
779
  html += '</div>';
713
780
  html += '<div id="pack-controls-failing" style="display:none;">';
714
- html += renderControlsTable(controls.filter(function(c){return c.status!==\\'pass\\'&&c.status!==\\'not-applicable\\'}));
781
+ html += renderControlsTable(controls.filter(function(c){return c.status!=="pass"&&c.status!=="not-applicable"}));
715
782
  html += '</div>';
716
783
  html += '<div id="pack-controls-withfindings" style="display:none;">';
717
784
  html += renderControlsTable(controls.filter(function(c){return c.relatedFindings.length>0}));
@@ -1040,7 +1107,7 @@ function renderDetailedFixesList(findings, controls, packs) {
1040
1107
  }
1041
1108
  return html;
1042
1109
  }
1043
- function renderFixHistorySection(entries) {
1110
+ function renderFixHistorySection(entries, complianceIssues = []) {
1044
1111
  if (entries.length === 0) {
1045
1112
  return `<div class="card">
1046
1113
  <h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Compliance Fix History</h2>
@@ -1049,6 +1116,7 @@ function renderFixHistorySection(entries) {
1049
1116
  <div class="icon">&#128203;</div>
1050
1117
  <div class="msg">No fixes recorded yet</div>
1051
1118
  <div class="sub">Run <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:12px;">ges fix</code> or use the MCP <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:12px;">auto_fix</code> tool to apply fixes. Each fix will be recorded here.</div>
1119
+ ${complianceIssues.length > 0 ? `<div style="margin-top:16px;"><span class="badge badge-status" style="background:#f97316;font-size:12px;padding:4px 12px;">${complianceIssues.length} pending fixes</span> <span class="link" style="font-size:13px;" onclick="goToPendingFixes()">View pending fixes &rarr;</span></div>` : ''}
1052
1120
  </div>
1053
1121
  </div>`;
1054
1122
  }
@@ -1193,6 +1261,108 @@ function renderFixHistorySection(entries) {
1193
1261
  html += `</div>`;
1194
1262
  return html;
1195
1263
  }
1264
+ function renderComplianceIssuesTable(issues) {
1265
+ if (issues.length === 0) {
1266
+ return '<div class="empty-state"><div class="icon">&#10003;</div><div class="msg" style="color:#22c55e;">No compliance issues in this category</div></div>';
1267
+ }
1268
+ return `<table>
1269
+ <thead><tr><th>Severity</th><th>Control</th><th>Framework</th><th>Policy Pack</th><th>Status</th><th>Checks</th><th>Audit</th><th>Fix Guidance</th></tr></thead>
1270
+ <tbody>
1271
+ ${issues.map(issue => `<tr>
1272
+ <td><span class="badge badge-sev" style="background:${severityColor(issue.severity)}">${issue.severity.toUpperCase()}</span></td>
1273
+ <td>
1274
+ <span class="link" onclick="showControlDetail('${escapeHtml(issue.controlId)}')">${escapeHtml(issue.controlId)}</span>
1275
+ <div style="font-size:12px;color:#4b5563;">${escapeHtml(issue.controlName)}</div>
1276
+ </td>
1277
+ <td style="font-size:12px;">${escapeHtml(issue.framework)}${issue.article ? '<br><span style="color:#6b7280;">' + escapeHtml(issue.article) + '</span>' : ''}</td>
1278
+ <td>${issue.packId ? `<span class="tag" style="cursor:pointer;" onclick="loadPackDetail('${issue.packId}')">${escapeHtml(issue.packName)}</span>` : '<span style="color:#9ca3af;">Direct</span>'}</td>
1279
+ <td><span class="badge badge-status" style="background:${statusColor(issue.status)}">${statusLabel(issue.status)}</span></td>
1280
+ <td style="font-size:12px;">${issue.passedChecks}/${issue.totalChecks}</td>
1281
+ <td>${issue.auditFindings.length > 0 ? `<span style="color:#ef4444;font-weight:600;">${issue.auditFindings.length}</span>` : '<span style="color:#9ca3af;">0</span>'}</td>
1282
+ <td style="max-width:280px;font-size:12px;color:#4b5563;">${escapeHtml(issue.implementation_guidance.substring(0, 200))}${issue.implementation_guidance.length > 200 ? '...' : ''}</td>
1283
+ </tr>`).join('')}
1284
+ </tbody>
1285
+ </table>`;
1286
+ }
1287
+ function renderComplianceFixCards(issues, idPrefix) {
1288
+ if (issues.length === 0) {
1289
+ return '<div class="card"><div class="empty-state"><div class="icon">&#10003;</div><div class="msg" style="color:#22c55e;">All controls passing</div><div class="sub">No fixes needed</div></div></div>';
1290
+ }
1291
+ const totalAuditFindings = issues.reduce((sum, i) => sum + i.auditFindings.length, 0);
1292
+ const criticalCount = issues.filter(i => i.severity === "critical").length;
1293
+ const highCount = issues.filter(i => i.severity === "high").length;
1294
+ let html = '';
1295
+ html += `<div style="margin-bottom:20px;">`;
1296
+ html += `<div class="grid grid-4" style="margin-bottom:20px;">`;
1297
+ html += `<div class="card stat"><div class="num" style="color:#ef4444;">${issues.length}</div><div class="label">Controls to Fix</div></div>`;
1298
+ html += `<div class="card stat"><div class="num" style="color:#ef4444;">${criticalCount}</div><div class="label">Critical</div></div>`;
1299
+ html += `<div class="card stat"><div class="num" style="color:#f97316;">${highCount}</div><div class="label">High</div></div>`;
1300
+ html += `<div class="card stat"><div class="num">${totalAuditFindings}</div><div class="label">Audit Findings</div></div>`;
1301
+ html += `</div>`;
1302
+ html += `</div>`;
1303
+ for (let i = 0; i < issues.length; i++) {
1304
+ const issue = issues[i];
1305
+ const fixId = `${idPrefix}-${i}`;
1306
+ html += `<div class="fix-detail-card">`;
1307
+ html += `<div class="fix-detail-header ${issue.severity}" onclick="toggleFix('${fixId}')">`;
1308
+ html += `<div class="fix-detail-num" style="color:${severityColor(issue.severity)};">${i + 1}</div>`;
1309
+ html += `<div class="fix-detail-info">`;
1310
+ html += `<div class="fix-detail-title">${escapeHtml(issue.controlName)}</div>`;
1311
+ html += `<div class="fix-detail-meta">${escapeHtml(issue.controlId)} | ${escapeHtml(issue.category)} | ${escapeHtml(issue.framework)}${issue.article ? ' | ' + escapeHtml(issue.article) : ''} | Pack: ${escapeHtml(issue.packName)}</div>`;
1312
+ html += `</div>`;
1313
+ html += `<div class="fix-detail-badges">`;
1314
+ html += `<span class="badge badge-sev" style="background:${severityColor(issue.severity)}">${issue.severity.toUpperCase()}</span>`;
1315
+ html += `<span class="badge badge-status" style="background:${statusColor(issue.status)}">${statusLabel(issue.status)}</span>`;
1316
+ html += `<span style="font-size:12px;color:#6b7280;">${issue.passedChecks}/${issue.totalChecks} checks</span>`;
1317
+ if (issue.auditFindings.length > 0) {
1318
+ html += `<span style="font-size:12px;color:#ef4444;font-weight:600;">${issue.auditFindings.length} evidence</span>`;
1319
+ }
1320
+ html += `<span class="fix-toggle" id="${fixId}-toggle">Expand</span>`;
1321
+ html += `</div></div>`;
1322
+ html += `<div class="fix-detail-body" id="${fixId}">`;
1323
+ html += `<div class="fix-section"><div class="fix-section-title">Description</div>`;
1324
+ html += `<div style="font-size:13px;color:#4b5563;line-height:1.6;">${escapeHtml(issue.description)}</div>`;
1325
+ html += `</div>`;
1326
+ if (issue.auditFindings.length > 0) {
1327
+ html += `<div class="fix-section"><div class="fix-section-title">Audit Evidence (${issue.auditFindings.length})</div>`;
1328
+ for (const f of issue.auditFindings) {
1329
+ html += `<div class="fix-finding-item ${f.severity}">`;
1330
+ html += `<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">`;
1331
+ html += `<span class="badge badge-sev" style="background:${severityColor(f.severity)};font-size:10px;">${f.severity.toUpperCase()}</span>`;
1332
+ html += `<strong style="font-size:13px;">${escapeHtml(f.title)}</strong></div>`;
1333
+ html += `<div style="font-size:12px;color:#6b7280;"><span style="font-family:monospace;font-weight:600;">${escapeHtml(f.ruleId)}</span> &mdash; <span style="font-family:monospace;">${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</span></div>`;
1334
+ if (f.description)
1335
+ html += `<div style="font-size:12px;color:#4b5563;margin-top:4px;">${escapeHtml(f.description)}</div>`;
1336
+ if (f.evidence)
1337
+ html += `<div class="fix-evidence">${escapeHtml(f.evidence)}</div>`;
1338
+ html += `</div>`;
1339
+ }
1340
+ html += `</div>`;
1341
+ }
1342
+ html += `<div class="fix-section"><div class="fix-section-title">Fix Guidance</div>`;
1343
+ html += `<div class="fix-guidance-box"><strong>How to fix:</strong> ${escapeHtml(issue.implementation_guidance)}</div>`;
1344
+ for (const f of issue.auditFindings) {
1345
+ if (f.fix) {
1346
+ html += `<div class="fix-guidance-box" style="margin-top:8px;background:#eff6ff;border-color:#bfdbfe;"><strong>Fix for ${escapeHtml(f.ruleId)}:</strong> ${escapeHtml(f.fix)}</div>`;
1347
+ }
1348
+ }
1349
+ html += `</div>`;
1350
+ html += `<div class="fix-section"><div class="fix-section-title">Traceability</div>`;
1351
+ html += `<table><tbody>`;
1352
+ html += `<tr><td style="font-weight:600;width:160px;">Control</td><td><span class="link" onclick="showControlDetail('${escapeHtml(issue.controlId)}')">${escapeHtml(issue.controlId)}</span> &mdash; ${escapeHtml(issue.controlName)}</td></tr>`;
1353
+ html += `<tr><td style="font-weight:600;">Category</td><td>${escapeHtml(issue.category)}</td></tr>`;
1354
+ html += `<tr><td style="font-weight:600;">Framework</td><td>${escapeHtml(issue.framework)}${issue.article ? ' / ' + escapeHtml(issue.article) : ''}</td></tr>`;
1355
+ html += `<tr><td style="font-weight:600;">Policy Pack</td><td>${issue.packId ? `<span class="tag" style="cursor:pointer;" onclick="loadPackDetail('${issue.packId}')">${escapeHtml(issue.packName)}</span>` : 'Direct'}</td></tr>`;
1356
+ html += `<tr><td style="font-weight:600;">Severity</td><td><span class="badge badge-sev" style="background:${severityColor(issue.severity)}">${issue.severity.toUpperCase()}</span></td></tr>`;
1357
+ html += `<tr><td style="font-weight:600;">Status</td><td><span class="badge badge-status" style="background:${statusColor(issue.status)}">${statusLabel(issue.status)}</span></td></tr>`;
1358
+ html += `<tr><td style="font-weight:600;">Checks</td><td>${issue.passedChecks}/${issue.totalChecks} passed</td></tr>`;
1359
+ html += `<tr><td style="font-weight:600;">Audit Evidence</td><td>${issue.auditFindings.length} finding(s)</td></tr>`;
1360
+ html += `</tbody></table>`;
1361
+ html += `</div>`;
1362
+ html += `</div></div>`;
1363
+ }
1364
+ return html;
1365
+ }
1196
1366
  function escapeHtml(str) {
1197
1367
  return str
1198
1368
  .replace(/&/g, "&amp;")
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "dependencies": {
3
- "@greenarmor/ges-audit-engine": "1.2.2",
4
- "@greenarmor/ges-core": "1.2.2",
5
- "@greenarmor/ges-policy-engine": "1.2.2",
6
- "@greenarmor/ges-scoring-engine": "1.2.2"
3
+ "@greenarmor/ges-audit-engine": "1.2.4",
4
+ "@greenarmor/ges-core": "1.2.4",
5
+ "@greenarmor/ges-policy-engine": "1.2.4",
6
+ "@greenarmor/ges-scoring-engine": "1.2.4"
7
7
  },
8
8
  "description": "GESF Web Dashboard - Visual compliance dashboard for teams",
9
9
  "devDependencies": {
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "type": "module",
42
42
  "types": "./dist/index.d.ts",
43
- "version": "1.2.2",
43
+ "version": "1.2.4",
44
44
  "scripts": {
45
45
  "build": "tsc",
46
46
  "clean": "rm -rf dist tsconfig.tsbuildinfo",