@greenarmor/ges-web-dashboard 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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"));
@@ -52,6 +52,17 @@ function loadControlsForConfig(projectPath, config) {
52
52
  }
53
53
  }
54
54
  function loadFindings(projectPath) {
55
+ try {
56
+ const auditPath = path.join(projectPath, ".ges", "last-audit.json");
57
+ if (fs.existsSync(auditPath)) {
58
+ const raw = fs.readFileSync(auditPath, "utf-8");
59
+ const data = JSON.parse(raw);
60
+ if (data.findings && Array.isArray(data.findings)) {
61
+ return data.findings;
62
+ }
63
+ }
64
+ }
65
+ catch { /* fall through to live audit */ }
55
66
  try {
56
67
  const result = runAudit(projectPath);
57
68
  return deduplicateFindings(result.findings);
@@ -60,11 +71,43 @@ function loadFindings(projectPath) {
60
71
  return [];
61
72
  }
62
73
  }
74
+ const SCANNABLE_CATEGORIES = new Set([
75
+ "encryption", "authentication", "audit", "security",
76
+ "database", "secrets", "injection", "xss",
77
+ "infrastructure", "dependencies",
78
+ ]);
79
+ function updateControlsFromFindings(controls, findings) {
80
+ const controlsWithFindings = new Set(findings.flatMap(f => f.controlIds));
81
+ return controls.map(control => {
82
+ if (control.status === "pass" || control.status === "not-applicable")
83
+ return control;
84
+ const relevantFindings = findings.filter(f => f.controlIds.includes(control.id));
85
+ if (relevantFindings.length === 0) {
86
+ if (SCANNABLE_CATEGORIES.has(control.category) && !controlsWithFindings.has(control.id)) {
87
+ return {
88
+ ...control,
89
+ checks: control.checks.map(check => ({ ...check, status: "pass" })),
90
+ status: "pass",
91
+ };
92
+ }
93
+ return control;
94
+ }
95
+ const hasCritical = relevantFindings.some(f => f.severity === "critical" || f.severity === "high");
96
+ return {
97
+ ...control,
98
+ checks: control.checks.map(check => ({
99
+ ...check,
100
+ status: hasCritical ? "fail" : "warning",
101
+ })),
102
+ status: hasCritical ? "fail" : "warning",
103
+ };
104
+ });
105
+ }
63
106
  function buildPackSummary(pack, controls, findings, installedPacks) {
64
107
  const packControlIds = new Set(pack.controls.map(c => c.id));
65
108
  const packControls = controls.filter(c => packControlIds.has(c.id));
66
109
  const packFindings = findings.filter(f => f.controlIds.some(cid => packControlIds.has(cid)));
67
- const passedCount = packControls.filter(c => c.status === "pass").length;
110
+ const passedCount = packControls.filter(c => c.status === "pass" || c.status === "not-applicable").length;
68
111
  const total = packControls.length || 1;
69
112
  const score = Math.round((passedCount / total) * 100);
70
113
  const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
@@ -85,9 +128,18 @@ function buildPackSummary(pack, controls, findings, installedPacks) {
85
128
  installed: installedPacks.has(pack.id),
86
129
  };
87
130
  }
88
- function getInstalledPackIds(projectPath) {
89
- const controlsDir = path.join(projectPath, "controls");
131
+ function getInstalledPackIds(projectPath, config) {
90
132
  const ids = new Set();
133
+ if (config) {
134
+ const fwLower = new Set(config.frameworks.map(f => f.toLowerCase()));
135
+ const allPacks = getAllPacks();
136
+ for (const pack of allPacks) {
137
+ if (fwLower.has(pack.id.toLowerCase())) {
138
+ ids.add(pack.id);
139
+ }
140
+ }
141
+ }
142
+ const controlsDir = path.join(projectPath, "controls");
91
143
  try {
92
144
  const entries = fs.readdirSync(controlsDir, { withFileTypes: true });
93
145
  for (const entry of entries) {
@@ -107,19 +159,23 @@ function getInstalledPackIds(projectPath) {
107
159
  export function collectDashboardData(projectPath) {
108
160
  const config = loadConfig(projectPath);
109
161
  let score = loadScore(projectPath);
110
- const controls = config ? loadControlsForConfig(projectPath, config) : [];
162
+ const baseControls = config ? loadControlsForConfig(projectPath, config) : [];
111
163
  const findings = loadFindings(projectPath);
112
- if (!score && config) {
164
+ const controls = updateControlsFromFindings(baseControls, findings);
165
+ if (config) {
113
166
  try {
114
- score = generateScoreFile(controls, config.frameworks, findings);
167
+ const freshScore = generateScoreFile(controls, config.frameworks, findings);
168
+ score = freshScore;
115
169
  }
116
170
  catch {
117
- score = null;
171
+ if (!score)
172
+ score = null;
118
173
  }
119
174
  }
120
175
  const allPacks = getAllPacks();
121
- const installedPacks = getInstalledPackIds(projectPath);
176
+ const installedPacks = getInstalledPackIds(projectPath, config || undefined);
122
177
  const packs = allPacks.map(p => buildPackSummary(p, controls, findings, installedPacks));
178
+ const fixHistory = loadFixHistory(projectPath);
123
179
  const metadataPath = path.join(projectPath, ".ges", "metadata.json");
124
180
  let lastAudit = "";
125
181
  try {
@@ -133,11 +189,12 @@ export function collectDashboardData(projectPath) {
133
189
  projectName: config?.project_name || "Unknown Project",
134
190
  projectType: config?.project_type || "unknown",
135
191
  frameworks: config?.frameworks || [],
136
- gesfVersion: "1.2.0",
192
+ gesfVersion: "1.2.2",
137
193
  score,
138
194
  controls,
139
195
  findings,
140
196
  packs,
197
+ fixHistory,
141
198
  lastAudit,
142
199
  };
143
200
  }
@@ -146,11 +203,12 @@ export function collectPackDetail(projectPath, packId) {
146
203
  if (!pack)
147
204
  return null;
148
205
  const config = loadConfig(projectPath);
149
- const controls = config ? loadControlsForConfig(projectPath, config) : [];
206
+ const baseControls = config ? loadControlsForConfig(projectPath, config) : [];
150
207
  const findings = loadFindings(projectPath);
208
+ const controls = updateControlsFromFindings(baseControls, findings);
151
209
  const packControlIds = new Set(pack.controls.map(c => c.id));
152
210
  const packControls = pack.controls;
153
- const installedPacks = getInstalledPackIds(projectPath);
211
+ const installedPacks = getInstalledPackIds(projectPath, config || undefined);
154
212
  const packSummary = buildPackSummary(pack, controls, findings, installedPacks);
155
213
  const findingsByControlId = {};
156
214
  for (const finding of findings) {
@@ -225,8 +283,9 @@ export function collectControlDetail(projectPath, controlId) {
225
283
  const config = loadConfig(projectPath);
226
284
  if (!config)
227
285
  return null;
228
- const controls = loadControlsForConfig(projectPath, config);
286
+ const baseControls = loadControlsForConfig(projectPath, config);
229
287
  const findings = loadFindings(projectPath);
288
+ const controls = updateControlsFromFindings(baseControls, findings);
230
289
  const control = controls.find(c => c.id === controlId);
231
290
  if (!control)
232
291
  return null;
@@ -311,6 +370,16 @@ export function startDashboard(options) {
311
370
  }
312
371
  return;
313
372
  }
373
+ if (pathname === "/api/fix-history") {
374
+ try {
375
+ const data = collectDashboardData(options.projectPath);
376
+ jsonResponse(res, data.fixHistory);
377
+ }
378
+ catch (err) {
379
+ jsonError(res, err instanceof Error ? err.message : String(err));
380
+ }
381
+ return;
382
+ }
314
383
  const packMatch = pathname.match(/^\/api\/packs\/([a-z0-9-]+)$/);
315
384
  if (packMatch) {
316
385
  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.2",
4
+ "@greenarmor/ges-core": "1.2.2",
5
+ "@greenarmor/ges-policy-engine": "1.2.2",
6
+ "@greenarmor/ges-scoring-engine": "1.2.2"
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.2",
44
44
  "scripts": {
45
45
  "build": "tsc",
46
46
  "clean": "rm -rf dist tsconfig.tsbuildinfo",