@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 +2 -1
- package/dist/index.js +37 -14
- package/dist/template.js +208 -25
- package/package.json +5 -5
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,
|
|
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
|
|
33
|
-
const
|
|
34
|
-
const 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 (
|
|
121
|
+
if (config) {
|
|
113
122
|
try {
|
|
114
|
-
|
|
123
|
+
const freshScore = generateScoreFile(controls, config.frameworks, findings);
|
|
124
|
+
score = freshScore;
|
|
115
125
|
}
|
|
116
126
|
catch {
|
|
117
|
-
score
|
|
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.
|
|
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(
|
|
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
|
-
|
|
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 → Fix → Control → 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
587
|
+
if (btn) btn.classList.add('active');
|
|
568
588
|
};
|
|
569
589
|
|
|
570
|
-
window.
|
|
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
|
-
|
|
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;">×</button>';
|
|
747
|
-
html += '<div class="breadcrumb"><span onclick="
|
|
777
|
+
html += '<div class="breadcrumb"><span onclick="navigateToPage(\\'packs\\')">Policy Packs</span> › <span onclick="loadPackDetail(\\'' + data.packId + '\\')">' + esc(data.packName) + '</span> › ' + 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">📋</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> — ${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, "&")
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"dependencies": {
|
|
3
|
-
"@greenarmor/ges-audit-engine": "1.2.
|
|
4
|
-
"@greenarmor/ges-core": "1.2.
|
|
5
|
-
"@greenarmor/ges-policy-engine": "1.2.
|
|
6
|
-
"@greenarmor/ges-scoring-engine": "1.2.
|
|
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.
|
|
43
|
+
"version": "1.2.1",
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsc",
|
|
46
46
|
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|