@greenarmor/ges-web-dashboard 1.2.0 → 1.2.1

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.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as http from "node:http";
2
- import type { ScoreFile, Control } from "@greenarmor/ges-core";
2
+ import type { ScoreFile, Control, FixHistoryEntry } from "@greenarmor/ges-core";
3
3
  import type { Finding } from "@greenarmor/ges-audit-engine";
4
4
  export interface DashboardOptions {
5
5
  port?: number;
@@ -76,6 +76,7 @@ export interface DashboardData {
76
76
  controls: Control[];
77
77
  findings: Finding[];
78
78
  packs: PackSummary[];
79
+ fixHistory: FixHistoryEntry[];
79
80
  lastAudit: string;
80
81
  }
81
82
  export declare function collectDashboardData(projectPath: string): DashboardData;
package/dist/index.js CHANGED
@@ -2,8 +2,9 @@ import * as http from "node:http";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { runAudit, deduplicateFindings } from "@greenarmor/ges-audit-engine";
5
- import { getAllPacks, getPacksForProjectType, getPack } from "@greenarmor/ges-policy-engine";
5
+ import { getAllPacks, getPack } from "@greenarmor/ges-policy-engine";
6
6
  import { generateScoreFile } from "@greenarmor/ges-scoring-engine";
7
+ import { loadFixHistory } from "@greenarmor/ges-core";
7
8
  import { renderDashboard } from "./template.js";
8
9
  function loadConfig(projectPath) {
9
10
  const configPath = path.join(projectPath, ".ges", "config.json");
@@ -27,11 +28,10 @@ function loadScore(projectPath) {
27
28
  }
28
29
  function loadControlsForConfig(projectPath, config) {
29
30
  try {
30
- const packs = getPacksForProjectType(config.project_type);
31
31
  const fwLower = new Set(config.frameworks.map(f => f.toLowerCase()));
32
- const DOMAIN_PACKS = new Set(["ai", "blockchain", "government"]);
33
- const filtered = packs.filter(pack => DOMAIN_PACKS.has(pack.id.toLowerCase()) || fwLower.has(pack.id.toLowerCase()));
34
- const controls = filtered.flatMap(p => p.controls);
32
+ const allPacks = getAllPacks();
33
+ const packs = allPacks.filter(pack => fwLower.has(pack.id.toLowerCase()));
34
+ const controls = packs.flatMap(p => p.controls);
35
35
  const overridesPath = path.join(projectPath, ".ges", "control-overrides.json");
36
36
  if (fs.existsSync(overridesPath)) {
37
37
  const overrides = JSON.parse(fs.readFileSync(overridesPath, "utf-8"));
@@ -64,7 +64,7 @@ function buildPackSummary(pack, controls, findings, installedPacks) {
64
64
  const packControlIds = new Set(pack.controls.map(c => c.id));
65
65
  const packControls = controls.filter(c => packControlIds.has(c.id));
66
66
  const packFindings = findings.filter(f => f.controlIds.some(cid => packControlIds.has(cid)));
67
- const passedCount = packControls.filter(c => c.status === "pass").length;
67
+ const passedCount = packControls.filter(c => c.status === "pass" || c.status === "not-applicable").length;
68
68
  const total = packControls.length || 1;
69
69
  const score = Math.round((passedCount / total) * 100);
70
70
  const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
@@ -85,9 +85,18 @@ function buildPackSummary(pack, controls, findings, installedPacks) {
85
85
  installed: installedPacks.has(pack.id),
86
86
  };
87
87
  }
88
- function getInstalledPackIds(projectPath) {
89
- const controlsDir = path.join(projectPath, "controls");
88
+ function getInstalledPackIds(projectPath, config) {
90
89
  const ids = new Set();
90
+ if (config) {
91
+ const fwLower = new Set(config.frameworks.map(f => f.toLowerCase()));
92
+ const allPacks = getAllPacks();
93
+ for (const pack of allPacks) {
94
+ if (fwLower.has(pack.id.toLowerCase())) {
95
+ ids.add(pack.id);
96
+ }
97
+ }
98
+ }
99
+ const controlsDir = path.join(projectPath, "controls");
91
100
  try {
92
101
  const entries = fs.readdirSync(controlsDir, { withFileTypes: true });
93
102
  for (const entry of entries) {
@@ -109,17 +118,20 @@ export function collectDashboardData(projectPath) {
109
118
  let score = loadScore(projectPath);
110
119
  const controls = config ? loadControlsForConfig(projectPath, config) : [];
111
120
  const findings = loadFindings(projectPath);
112
- if (!score && config) {
121
+ if (config) {
113
122
  try {
114
- score = generateScoreFile(controls, config.frameworks, findings);
123
+ const freshScore = generateScoreFile(controls, config.frameworks, findings);
124
+ score = freshScore;
115
125
  }
116
126
  catch {
117
- score = null;
127
+ if (!score)
128
+ score = null;
118
129
  }
119
130
  }
120
131
  const allPacks = getAllPacks();
121
- const installedPacks = getInstalledPackIds(projectPath);
132
+ const installedPacks = getInstalledPackIds(projectPath, config || undefined);
122
133
  const packs = allPacks.map(p => buildPackSummary(p, controls, findings, installedPacks));
134
+ const fixHistory = loadFixHistory(projectPath);
123
135
  const metadataPath = path.join(projectPath, ".ges", "metadata.json");
124
136
  let lastAudit = "";
125
137
  try {
@@ -133,11 +145,12 @@ export function collectDashboardData(projectPath) {
133
145
  projectName: config?.project_name || "Unknown Project",
134
146
  projectType: config?.project_type || "unknown",
135
147
  frameworks: config?.frameworks || [],
136
- gesfVersion: "1.2.0",
148
+ gesfVersion: "1.2.1",
137
149
  score,
138
150
  controls,
139
151
  findings,
140
152
  packs,
153
+ fixHistory,
141
154
  lastAudit,
142
155
  };
143
156
  }
@@ -150,7 +163,7 @@ export function collectPackDetail(projectPath, packId) {
150
163
  const findings = loadFindings(projectPath);
151
164
  const packControlIds = new Set(pack.controls.map(c => c.id));
152
165
  const packControls = pack.controls;
153
- const installedPacks = getInstalledPackIds(projectPath);
166
+ const installedPacks = getInstalledPackIds(projectPath, config || undefined);
154
167
  const packSummary = buildPackSummary(pack, controls, findings, installedPacks);
155
168
  const findingsByControlId = {};
156
169
  for (const finding of findings) {
@@ -311,6 +324,16 @@ export function startDashboard(options) {
311
324
  }
312
325
  return;
313
326
  }
327
+ if (pathname === "/api/fix-history") {
328
+ try {
329
+ const data = collectDashboardData(options.projectPath);
330
+ jsonResponse(res, data.fixHistory);
331
+ }
332
+ catch (err) {
333
+ jsonError(res, err instanceof Error ? err.message : String(err));
334
+ }
335
+ return;
336
+ }
314
337
  const packMatch = pathname.match(/^\/api\/packs\/([a-z0-9-]+)$/);
315
338
  if (packMatch) {
316
339
  try {
package/dist/template.js CHANGED
@@ -256,11 +256,11 @@ export function renderDashboard(data) {
256
256
  <div class="subtitle">${escapeHtml(data.projectName)} | ${escapeHtml(data.projectType)} | GESF v${escapeHtml(data.gesfVersion)}</div>
257
257
  </div>
258
258
  <div class="nav-tabs">
259
- <button class="nav-tab active" onclick="showPage('overview')">Overview</button>
260
- <button class="nav-tab" onclick="showPage('packs')">Policy Packs</button>
261
- <button class="nav-tab" onclick="showPage('fixes')">Fixes Detail</button>
262
- <button class="nav-tab" onclick="showPage('findings')">Findings</button>
263
- <button class="nav-tab" onclick="showPage('traceability')">Traceability</button>
259
+ <button class="nav-tab active" onclick="showPage('overview', this)">Overview</button>
260
+ <button class="nav-tab" onclick="showPage('packs', this)">Policy Packs</button>
261
+ <button class="nav-tab" onclick="showPage('fixes', this)">Fixes Detail</button>
262
+ <button class="nav-tab" onclick="showPage('findings', this)">Findings</button>
263
+ <button class="nav-tab" onclick="showPage('traceability', this)">Traceability</button>
264
264
  </div>
265
265
  </div>
266
266
 
@@ -270,7 +270,7 @@ export function renderDashboard(data) {
270
270
  <div class="grid">
271
271
  <div class="grid grid-3">
272
272
  <div class="card stat">
273
- ${donutSvg(score ? Object.values(frameworks).reduce((n, f) => n + f.passed_controls, 0) : 0, controls.length || 1)}
273
+ ${donutSvg(controls.filter(c => c.status === "pass" || c.status === "not-applicable").length, controls.length || 1)}
274
274
  <div class="label">Overall Compliance</div>
275
275
  </div>
276
276
  <div class="card">
@@ -386,10 +386,11 @@ export function renderDashboard(data) {
386
386
  <div class="pack-desc">${escapeHtml(p.description)}</div>
387
387
  ${scoreBarHtml(pct)}
388
388
  <div class="pack-stats" style="margin-top:12px;">
389
- <span><span class="badge badge-status" style="background:#22c55e;">${p.passedCount}</span> pass</span>
389
+ <span><span class="badge badge-status" style="background:#22c55e;">${p.passedCount - p.notApplicableCount}</span> pass</span>
390
390
  <span><span class="badge badge-status" style="background:#ef4444;">${p.failedCount}</span> fail</span>
391
391
  <span><span class="badge badge-status" style="background:#eab308;">${p.warningCount}</span> warn</span>
392
392
  <span><span class="badge badge-status" style="background:#6b7280;">${p.notImplementedCount}</span> not impl</span>
393
+ <span><span class="badge badge-status" style="background:#9ca3af;">${p.notApplicableCount}</span> N/A</span>
393
394
  <span style="color:#ef4444;font-weight:600;">${p.findingsCount} findings</span>
394
395
  <span>${p.controlCount} controls</span>
395
396
  ${p.installed ? '<span style="color:#0f766e;font-weight:600;">Installed</span>' : '<span style="color:#9ca3af;">Not installed</span>'}
@@ -402,19 +403,30 @@ export function renderDashboard(data) {
402
403
  </div>
403
404
 
404
405
  <div id="page-fixes" class="page">
405
- ${renderDetailedFixesList(findings, controls, packs)}
406
+ <div class="tab-bar" style="margin-bottom:0;">
407
+ <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>
409
+ </div>
410
+
411
+ <div id="fixes-tab-history" class="tab-panel active">
412
+ ${renderFixHistorySection(data.fixHistory)}
413
+ </div>
414
+
415
+ <div id="fixes-tab-pending" class="tab-panel">
416
+ ${renderDetailedFixesList(findings, controls, packs)}
417
+ </div>
406
418
  </div>
407
419
 
408
420
  <div id="page-findings" class="page">
409
421
  <div id="findings-main">
410
422
  <h2 style="font-size:20px;font-weight:700;margin-bottom:16px;">Security Findings Report</h2>
411
423
  <div class="tab-bar">
412
- <button class="tab-btn active" onclick="showFindingsTab('all')">All (${findings.length})</button>
413
- <button class="tab-btn" onclick="showFindingsTab('critical')">Critical (${findingsBySeverity.critical})</button>
414
- <button class="tab-btn" onclick="showFindingsTab('high')">High (${findingsBySeverity.high})</button>
415
- <button class="tab-btn" onclick="showFindingsTab('medium')">Medium (${findingsBySeverity.medium})</button>
416
- <button class="tab-btn" onclick="showFindingsTab('low')">Low (${findingsBySeverity.low})</button>
417
- <button class="tab-btn" onclick="showFindingsTab('bypack')">By Pack</button>
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>
429
+ <button class="tab-btn" onclick="showFindingsTab('bypack', this)">By Pack</button>
418
430
  </div>
419
431
 
420
432
  <div id="findings-tab-all" class="tab-panel active">
@@ -443,9 +455,9 @@ export function renderDashboard(data) {
443
455
  <h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Fix Traceability Matrix</h2>
444
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>
445
457
  <div class="tab-bar">
446
- <button class="tab-btn active" onclick="showTraceTab('matrix')">Matrix</button>
447
- <button class="tab-btn" onclick="showTraceTab('fixes')">Prioritized Fixes</button>
448
- <button class="tab-btn" onclick="showTraceTab('controls')">Control Coverage</button>
458
+ <button class="tab-btn active" onclick="showTraceTab('matrix', this)">Matrix</button>
459
+ <button class="tab-btn" onclick="showTraceTab('fixes', this)">Prioritized Fixes</button>
460
+ <button class="tab-btn" onclick="showTraceTab('controls', this)">Control Coverage</button>
449
461
  </div>
450
462
 
451
463
  <div id="trace-tab-matrix" class="tab-panel active">
@@ -520,7 +532,7 @@ export function renderDashboard(data) {
520
532
  </div>
521
533
 
522
534
  <div class="footer">
523
- Generated by GESF v${escapeHtml(data.gesfVersion)} | Last audit: ${escapeHtml(new Date(data.lastAudit).toLocaleString())} | <a href="/api/data">JSON API</a> | <a href="/api/packs">Packs API</a>
535
+ Generated by GESF v${escapeHtml(data.gesfVersion)} | Last audit: ${escapeHtml(new Date(data.lastAudit).toLocaleString())} | <a href="/api/data">JSON API</a> | <a href="/api/packs">Packs API</a> | <a href="/api/fix-history">Fix History API</a>
524
536
  </div>
525
537
 
526
538
  <script>
@@ -541,13 +553,21 @@ export function renderDashboard(data) {
541
553
  }
542
554
  };
543
555
 
544
- window.showPage = function(page) {
556
+ var navTabMap = { overview: 0, packs: 1, fixes: 2, findings: 3, traceability: 4 };
557
+
558
+ window.navigateToPage = function(page) {
559
+ var tabs = document.querySelectorAll('.nav-tab');
560
+ var idx = navTabMap[page];
561
+ showPage(page, idx !== undefined ? tabs[idx] : null);
562
+ };
563
+
564
+ window.showPage = function(page, btn) {
545
565
  var pages = document.querySelectorAll('.page');
546
566
  for (var i = 0; i < pages.length; i++) pages[i].classList.remove('active');
547
567
  document.getElementById('page-' + page).classList.add('active');
548
568
  var tabs = document.querySelectorAll('.nav-tab');
549
569
  for (var i = 0; i < tabs.length; i++) tabs[i].classList.remove('active');
550
- event.target.classList.add('active');
570
+ if (btn) btn.classList.add('active');
551
571
  if (page === 'packs') {
552
572
  document.getElementById('packs-list').style.display = '';
553
573
  document.getElementById('pack-detail').style.display = 'none';
@@ -558,22 +578,32 @@ export function renderDashboard(data) {
558
578
  }
559
579
  };
560
580
 
561
- window.showFindingsTab = function(tab) {
581
+ window.showFindingsTab = function(tab, btn) {
562
582
  var panels = document.querySelectorAll('#page-findings .tab-panel');
563
583
  for (var i = 0; i < panels.length; i++) panels[i].classList.remove('active');
564
584
  document.getElementById('findings-tab-' + tab).classList.add('active');
565
585
  var btns = document.querySelectorAll('#page-findings .tab-btn');
566
586
  for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
567
- event.target.classList.add('active');
587
+ if (btn) btn.classList.add('active');
568
588
  };
569
589
 
570
- window.showTraceTab = function(tab) {
590
+ window.showFixesTab = function(tab, btn) {
591
+ var panels = document.querySelectorAll('#page-fixes .tab-panel');
592
+ for (var i = 0; i < panels.length; i++) panels[i].classList.remove('active');
593
+ var el = document.getElementById('fixes-tab-' + tab);
594
+ if (el) el.classList.add('active');
595
+ var btns = document.querySelectorAll('#page-fixes .tab-btn');
596
+ for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
597
+ if (btn) btn.classList.add('active');
598
+ };
599
+
600
+ window.showTraceTab = function(tab, btn) {
571
601
  var panels = document.querySelectorAll('#page-traceability .tab-panel');
572
602
  for (var i = 0; i < panels.length; i++) panels[i].classList.remove('active');
573
603
  document.getElementById('trace-tab-' + tab).classList.add('active');
574
604
  var btns = document.querySelectorAll('#page-traceability .tab-btn');
575
605
  for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active');
576
- event.target.classList.add('active');
606
+ if (btn) btn.classList.add('active');
577
607
  };
578
608
 
579
609
  window.loadPackDetail = function(packId) {
@@ -744,7 +774,7 @@ export function renderDashboard(data) {
744
774
  function renderControlModal(data, container) {
745
775
  var html = '<div class="card" style="position:relative;">';
746
776
  html += '<button onclick="document.getElementById(\\'control-detail-modal\\').style.display=\\'none\\'" style="position:absolute;top:16px;right:16px;background:none;border:none;font-size:20px;cursor:pointer;color:#6b7280;">&times;</button>';
747
- html += '<div class="breadcrumb"><span onclick="showPage(\\'packs\\')">Policy Packs</span> &rsaquo; <span onclick="loadPackDetail(\\'' + data.packId + '\\')">' + esc(data.packName) + '</span> &rsaquo; ' + esc(data.id) + '</div>';
777
+ html += '<div class="breadcrumb"><span onclick="navigateToPage(\\'packs\\')">Policy Packs</span> &rsaquo; <span onclick="loadPackDetail(\\'' + data.packId + '\\')">' + esc(data.packName) + '</span> &rsaquo; ' + esc(data.id) + '</div>';
748
778
  html += '<div class="detail-header">';
749
779
  html += '<div><div class="detail-title">' + esc(data.name) + '</div>';
750
780
  html += '<div class="detail-meta">' + esc(data.id) + ' | ' + esc(data.category) + ' | ' + esc(data.framework) + (data.article ? ' | ' + esc(data.article) : '') + '</div></div>';
@@ -1010,6 +1040,159 @@ function renderDetailedFixesList(findings, controls, packs) {
1010
1040
  }
1011
1041
  return html;
1012
1042
  }
1043
+ function renderFixHistorySection(entries) {
1044
+ if (entries.length === 0) {
1045
+ return `<div class="card">
1046
+ <h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Compliance Fix History</h2>
1047
+ <p style="color:#6b7280;font-size:14px;margin-bottom:16px;">Every autofix applied via CLI or MCP is recorded here with full compliance traceability.</p>
1048
+ <div class="empty-state">
1049
+ <div class="icon">&#128203;</div>
1050
+ <div class="msg">No fixes recorded yet</div>
1051
+ <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>
1052
+ </div>
1053
+ </div>`;
1054
+ }
1055
+ const applied = entries.filter(e => e.fix.applied);
1056
+ const failed = entries.filter(e => !e.fix.applied);
1057
+ const bySource = { cli: entries.filter(e => e.source === "cli").length, mcp: entries.filter(e => e.source === "mcp").length };
1058
+ const frameworksAffected = [...new Set(entries.flatMap(e => e.compliance_impact.frameworks_affected))];
1059
+ const totalControlsAddressed = entries.reduce((sum, e) => sum + e.compliance_impact.controls_addressed, 0);
1060
+ const bySeverity = {
1061
+ critical: entries.filter(e => e.compliance_impact.severity_resolved === "critical").length,
1062
+ high: entries.filter(e => e.compliance_impact.severity_resolved === "high").length,
1063
+ medium: entries.filter(e => e.compliance_impact.severity_resolved === "medium").length,
1064
+ low: entries.filter(e => e.compliance_impact.severity_resolved === "low").length,
1065
+ };
1066
+ let html = '';
1067
+ html += `<h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Compliance Fix History</h2>`;
1068
+ html += `<p style="color:#6b7280;font-size:14px;margin-bottom:16px;">Every autofix applied via CLI or MCP is recorded here with full compliance traceability.</p>`;
1069
+ html += `<div class="grid grid-4" style="margin-bottom:20px;">`;
1070
+ html += `<div class="card stat"><div class="num">${entries.length}</div><div class="label">Total Fixes</div></div>`;
1071
+ html += `<div class="card stat"><div class="num" style="color:#22c55e;">${applied.length}</div><div class="label">Applied</div></div>`;
1072
+ html += `<div class="card stat"><div class="num" style="color:#ef4444;">${failed.length}</div><div class="label">Failed</div></div>`;
1073
+ html += `<div class="card stat"><div class="num" style="color:#0f766e;">${totalControlsAddressed}</div><div class="label">Controls Addressed</div></div>`;
1074
+ html += `</div>`;
1075
+ html += `<div class="grid grid-3" style="margin-bottom:20px;">`;
1076
+ html += `<div class="card"><div class="card-title">Severity Breakdown</div>
1077
+ <div style="display:flex;flex-wrap:wrap;gap:12px;margin-top:8px;">
1078
+ <div class="stat"><div class="num" style="color:#ef4444;font-size:20px;">${bySeverity.critical}</div><div class="label">Critical</div></div>
1079
+ <div class="stat"><div class="num" style="color:#f97316;font-size:20px;">${bySeverity.high}</div><div class="label">High</div></div>
1080
+ <div class="stat"><div class="num" style="color:#eab308;font-size:20px;">${bySeverity.medium}</div><div class="label">Medium</div></div>
1081
+ <div class="stat"><div class="num" style="color:#3b82f6;font-size:20px;">${bySeverity.low}</div><div class="label">Low</div></div>
1082
+ </div></div>`;
1083
+ html += `<div class="card"><div class="card-title">Fix Sources</div>
1084
+ <div style="display:flex;gap:16px;margin-top:8px;">
1085
+ <div class="stat"><div class="num" style="font-size:20px;">${bySource.cli}</div><div class="label">CLI (ges fix)</div></div>
1086
+ <div class="stat"><div class="num" style="font-size:20px;">${bySource.mcp}</div><div class="label">MCP (auto_fix)</div></div>
1087
+ </div></div>`;
1088
+ html += `<div class="card"><div class="card-title">Frameworks Impacted</div>
1089
+ <div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;">
1090
+ ${frameworksAffected.length > 0 ? frameworksAffected.map(fw => `<span class="tag" style="background:#d1fae5;color:#065f46;">${escapeHtml(fw)}</span>`).join('') : '<span style="color:#9ca3af;">None</span>'}
1091
+ </div></div>`;
1092
+ html += `</div>`;
1093
+ const sorted = [...entries].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1094
+ html += `<div class="card"><div class="card-title">All Recorded Fixes (newest first)</div>`;
1095
+ html += `<table><thead><tr><th>Time</th><th>Source</th><th>Severity</th><th>Rule</th><th>Finding</th><th>Fix Action</th><th>Controls</th><th>Frameworks</th><th>Status</th></tr></thead><tbody>`;
1096
+ for (const entry of sorted) {
1097
+ const time = new Date(entry.timestamp).toLocaleString();
1098
+ const sourceBadge = entry.source === "mcp"
1099
+ ? '<span class="badge" style="background:#7c3aed;font-size:10px;">MCP</span>'
1100
+ : '<span class="badge" style="background:#0f766e;font-size:10px;">CLI</span>';
1101
+ const sevBadge = `<span class="badge badge-sev" style="background:${severityColor(entry.compliance_impact.severity_resolved)};font-size:10px;">${entry.compliance_impact.severity_resolved.toUpperCase()}</span>`;
1102
+ const statusBadge = entry.fix.applied
1103
+ ? '<span class="badge badge-status" style="background:#22c55e;font-size:10px;">APPLIED</span>'
1104
+ : `<span class="badge badge-status" style="background:#ef4444;font-size:10px;">FAILED</span>`;
1105
+ const controlsHtml = entry.controls.length > 0
1106
+ ? entry.controls.map(c => `<div style="margin-bottom:2px;"><span class="link" onclick="showControlDetail('${escapeHtml(c.id)}')">${escapeHtml(c.id)}</span></div>`).join('')
1107
+ : '<span style="color:#9ca3af;">-</span>';
1108
+ const frameworksHtml = entry.compliance_impact.frameworks_affected.length > 0
1109
+ ? entry.compliance_impact.frameworks_affected.map(f => `<span class="tag">${escapeHtml(f)}</span>`).join(' ')
1110
+ : '<span style="color:#9ca3af;">-</span>';
1111
+ html += `<tr>
1112
+ <td style="font-size:11px;white-space:nowrap;">${time}</td>
1113
+ <td>${sourceBadge}</td>
1114
+ <td>${sevBadge}</td>
1115
+ <td style="font-family:monospace;font-size:11px;">${escapeHtml(entry.finding.rule_id)}</td>
1116
+ <td style="max-width:200px;">
1117
+ <div style="font-weight:600;font-size:12px;">${escapeHtml(entry.finding.title)}</div>
1118
+ <div style="font-size:11px;color:#6b7280;">${escapeHtml(entry.finding.file)}${entry.finding.line ? ':' + entry.finding.line : ''}</div>
1119
+ </td>
1120
+ <td style="max-width:200px;">
1121
+ <div style="font-size:12px;"><span class="badge" style="background:#6b7280;font-size:9px;">${entry.fix.action_type.toUpperCase()}</span> ${escapeHtml(entry.fix.file_path)}</div>
1122
+ <div style="font-size:11px;color:#6b7280;">${escapeHtml(entry.fix.description)}</div>
1123
+ </td>
1124
+ <td style="max-width:150px;">${controlsHtml}</td>
1125
+ <td>${frameworksHtml}</td>
1126
+ <td>${statusBadge}</td>
1127
+ </tr>`;
1128
+ }
1129
+ html += `</tbody></table></div>`;
1130
+ html += `<div class="card" style="margin-top:20px;"><div class="card-title">Detailed Fix Records</div>`;
1131
+ for (let i = 0; i < sorted.length; i++) {
1132
+ const entry = sorted[i];
1133
+ const fixId = `histfix-${i}`;
1134
+ const sevClass = entry.compliance_impact.severity_resolved;
1135
+ html += `<div class="fix-detail-card" style="margin-bottom:8px;">`;
1136
+ html += `<div class="fix-detail-header ${sevClass}" onclick="toggleFix('${fixId}')">`;
1137
+ html += `<div class="fix-detail-num" style="color:${severityColor(sevClass)};">${i + 1}</div>`;
1138
+ html += `<div class="fix-detail-info">`;
1139
+ html += `<div class="fix-detail-title">${escapeHtml(entry.finding.title)}</div>`;
1140
+ html += `<div class="fix-detail-meta">${escapeHtml(entry.finding.rule_id)} | ${escapeHtml(entry.finding.file)}${entry.finding.line ? ':' + entry.finding.line : ''} | ${entry.source.toUpperCase()} | ${new Date(entry.timestamp).toLocaleString()}</div>`;
1141
+ html += `</div>`;
1142
+ html += `<div class="fix-detail-badges">`;
1143
+ html += `<span class="badge badge-sev" style="background:${severityColor(sevClass)};font-size:10px;">${sevClass.toUpperCase()}</span>`;
1144
+ if (entry.fix.applied) {
1145
+ html += `<span class="badge" style="background:#22c55e;font-size:10px;">APPLIED</span>`;
1146
+ }
1147
+ else {
1148
+ html += `<span class="badge" style="background:#ef4444;font-size:10px;">FAILED</span>`;
1149
+ }
1150
+ html += `<span class="badge" style="background:${entry.source === 'mcp' ? '#7c3aed' : '#0f766e'};font-size:10px;">${entry.source.toUpperCase()}</span>`;
1151
+ html += `<span class="fix-toggle" id="${fixId}-toggle">Expand</span>`;
1152
+ html += `</div></div>`;
1153
+ html += `<div class="fix-detail-body" id="${fixId}">`;
1154
+ html += `<div class="fix-section"><div class="fix-section-title">Finding Details</div>`;
1155
+ html += `<table><tbody>`;
1156
+ html += `<tr><td style="font-weight:600;width:140px;">Rule</td><td style="font-family:monospace;">${escapeHtml(entry.finding.rule_id)}</td></tr>`;
1157
+ html += `<tr><td style="font-weight:600;">Category</td><td>${escapeHtml(entry.finding.category)}</td></tr>`;
1158
+ html += `<tr><td style="font-weight:600;">Severity</td><td><span class="badge badge-sev" style="background:${severityColor(entry.compliance_impact.severity_resolved)}">${sevClass.toUpperCase()}</span></td></tr>`;
1159
+ html += `<tr><td style="font-weight:600;">File</td><td style="font-family:monospace;">${escapeHtml(entry.finding.file)}${entry.finding.line ? ':' + entry.finding.line : ''}</td></tr>`;
1160
+ html += `<tr><td style="font-weight:600;">Title</td><td>${escapeHtml(entry.finding.title)}</td></tr>`;
1161
+ if (entry.finding.description) {
1162
+ html += `<tr><td style="font-weight:600;">Description</td><td style="color:#4b5563;">${escapeHtml(entry.finding.description)}</td></tr>`;
1163
+ }
1164
+ if (entry.finding.evidence) {
1165
+ html += `<tr><td style="font-weight:600;">Evidence</td><td><div class="fix-evidence">${escapeHtml(entry.finding.evidence)}</div></td></tr>`;
1166
+ }
1167
+ html += `</tbody></table></div>`;
1168
+ html += `<div class="fix-section"><div class="fix-section-title">Fix Applied</div>`;
1169
+ html += `<table><tbody>`;
1170
+ html += `<tr><td style="font-weight:600;width:140px;">Action</td><td><span class="badge" style="background:#6b7280;">${entry.fix.action_type.toUpperCase()}</span></td></tr>`;
1171
+ html += `<tr><td style="font-weight:600;">Target File</td><td style="font-family:monospace;">${escapeHtml(entry.fix.file_path)}</td></tr>`;
1172
+ html += `<tr><td style="font-weight:600;">Description</td><td>${escapeHtml(entry.fix.description)}</td></tr>`;
1173
+ html += `<tr><td style="font-weight:600;">Status</td><td>${entry.fix.applied ? '<span style="color:#22c55e;font-weight:600;">Applied successfully</span>' : `<span style="color:#ef4444;font-weight:600;">Failed: ${escapeHtml(entry.fix.error || 'Unknown error')}</span>`}</td></tr>`;
1174
+ html += `<tr><td style="font-weight:600;">Source</td><td>${entry.source === 'mcp' ? 'MCP auto_fix tool' : 'CLI ges fix command'}</td></tr>`;
1175
+ html += `<tr><td style="font-weight:600;">Timestamp</td><td>${new Date(entry.timestamp).toLocaleString()}</td></tr>`;
1176
+ if (entry.dry_run) {
1177
+ html += `<tr><td style="font-weight:600;">Mode</td><td><span class="badge" style="background:#eab308;color:white;">DRY RUN</span></td></tr>`;
1178
+ }
1179
+ html += `</tbody></table></div>`;
1180
+ if (entry.fix.guidance) {
1181
+ html += `<div class="fix-section"><div class="fix-section-title">Fix Guidance</div>`;
1182
+ html += `<div class="fix-guidance-box">${escapeHtml(entry.fix.guidance)}</div></div>`;
1183
+ }
1184
+ html += `<div class="fix-section"><div class="fix-section-title">Compliance Traceability</div>`;
1185
+ html += `<table><tbody>`;
1186
+ html += `<tr><td style="font-weight:600;width:160px;">Controls Addressed</td><td>${entry.controls.length > 0 ? entry.controls.map(c => `<div style="margin-bottom:4px;"><span class="link" onclick="showControlDetail('${escapeHtml(c.id)}')">${escapeHtml(c.id)}</span> &mdash; ${escapeHtml(c.name)} <span style="color:#6b7280;font-size:11px;">(${escapeHtml(c.framework)}${c.article ? ' / ' + escapeHtml(c.article) : ''})</span></div>`).join('') : '<span style="color:#9ca3af;">No controls mapped</span>'}</td></tr>`;
1187
+ html += `<tr><td style="font-weight:600;">Frameworks Affected</td><td>${entry.compliance_impact.frameworks_affected.length > 0 ? entry.compliance_impact.frameworks_affected.map(f => `<span class="tag" style="background:#d1fae5;color:#065f46;">${escapeHtml(f)}</span>`).join(' ') : '<span style="color:#9ca3af;">-</span>'}</td></tr>`;
1188
+ html += `<tr><td style="font-weight:600;">Controls Count</td><td>${entry.compliance_impact.controls_addressed}</td></tr>`;
1189
+ html += `<tr><td style="font-weight:600;">Severity Resolved</td><td><span class="badge badge-sev" style="background:${severityColor(entry.compliance_impact.severity_resolved)}">${entry.compliance_impact.severity_resolved.toUpperCase()}</span></td></tr>`;
1190
+ html += `</tbody></table></div>`;
1191
+ html += `</div></div>`;
1192
+ }
1193
+ html += `</div>`;
1194
+ return html;
1195
+ }
1013
1196
  function escapeHtml(str) {
1014
1197
  return str
1015
1198
  .replace(/&/g, "&amp;")
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "dependencies": {
3
- "@greenarmor/ges-audit-engine": "1.2.0",
4
- "@greenarmor/ges-core": "1.2.0",
5
- "@greenarmor/ges-policy-engine": "1.2.0",
6
- "@greenarmor/ges-scoring-engine": "1.2.0"
3
+ "@greenarmor/ges-audit-engine": "1.2.1",
4
+ "@greenarmor/ges-core": "1.2.1",
5
+ "@greenarmor/ges-policy-engine": "1.2.1",
6
+ "@greenarmor/ges-scoring-engine": "1.2.1"
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.0",
43
+ "version": "1.2.1",
44
44
  "scripts": {
45
45
  "build": "tsc",
46
46
  "clean": "rm -rf dist tsconfig.tsbuildinfo",