@greenarmor/ges-web-dashboard 1.2.3 → 1.2.5
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 +59 -39
- package/dist/template.js +365 -57
- 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, FixHistoryEntry } from "@greenarmor/ges-core";
|
|
2
|
+
import type { ScoreFile, Control, FixHistoryEntry, ActivityLogEntry } from "@greenarmor/ges-core";
|
|
3
3
|
import type { Finding } from "@greenarmor/ges-audit-engine";
|
|
4
4
|
export interface DashboardOptions {
|
|
5
5
|
port?: number;
|
|
@@ -77,6 +77,7 @@ export interface DashboardData {
|
|
|
77
77
|
findings: Finding[];
|
|
78
78
|
packs: PackSummary[];
|
|
79
79
|
fixHistory: FixHistoryEntry[];
|
|
80
|
+
activityLog: ActivityLogEntry[];
|
|
80
81
|
lastAudit: string;
|
|
81
82
|
}
|
|
82
83
|
export declare function collectDashboardData(projectPath: string): DashboardData;
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,8 @@ import * as path from "node:path";
|
|
|
4
4
|
import { runAudit, deduplicateFindings } from "@greenarmor/ges-audit-engine";
|
|
5
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
|
+
import { loadFixHistory, loadActivityLog, loadControlsFromDisk, loadControlOverrides, applyOverridesToControls } from "@greenarmor/ges-core";
|
|
8
|
+
import { getInstalledPackIds as getInstalledPackIdsFromDisk } from "@greenarmor/ges-core";
|
|
8
9
|
import { renderDashboard } from "./template.js";
|
|
9
10
|
function loadConfig(projectPath) {
|
|
10
11
|
const configPath = path.join(projectPath, ".ges", "config.json");
|
|
@@ -31,21 +32,13 @@ function loadControlsForConfig(projectPath, config) {
|
|
|
31
32
|
const fwLower = new Set(config.frameworks.map(f => f.toLowerCase()));
|
|
32
33
|
const allPacks = getAllPacks();
|
|
33
34
|
const packs = allPacks.filter(pack => fwLower.has(pack.id.toLowerCase()));
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
control.status = override.status;
|
|
42
|
-
for (const check of control.checks) {
|
|
43
|
-
check.status = override.status;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return controls;
|
|
35
|
+
const inMemoryControls = packs.flatMap(p => p.controls);
|
|
36
|
+
const diskControls = loadControlsFromDisk(projectPath);
|
|
37
|
+
const seenIds = new Set(inMemoryControls.map((c) => c.id));
|
|
38
|
+
const extraFromDisk = diskControls.filter(c => !seenIds.has(c.id));
|
|
39
|
+
const controls = [...inMemoryControls, ...extraFromDisk];
|
|
40
|
+
const overrides = loadControlOverrides(projectPath);
|
|
41
|
+
return applyOverridesToControls(controls, overrides);
|
|
49
42
|
}
|
|
50
43
|
catch {
|
|
51
44
|
return [];
|
|
@@ -139,32 +132,40 @@ function getInstalledPackIds(projectPath, config) {
|
|
|
139
132
|
}
|
|
140
133
|
}
|
|
141
134
|
}
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
const entries = fs.readdirSync(controlsDir, { withFileTypes: true });
|
|
145
|
-
for (const entry of entries) {
|
|
146
|
-
if (entry.isDirectory()) {
|
|
147
|
-
const ctrlFile = path.join(controlsDir, entry.name, "controls.json");
|
|
148
|
-
if (fs.existsSync(ctrlFile)) {
|
|
149
|
-
ids.add(entry.name);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
// controls dir may not exist
|
|
135
|
+
for (const id of getInstalledPackIdsFromDisk(projectPath)) {
|
|
136
|
+
ids.add(id);
|
|
156
137
|
}
|
|
157
138
|
return ids;
|
|
158
139
|
}
|
|
140
|
+
function getFrameworksFromControls(controls) {
|
|
141
|
+
const fwSet = new Set();
|
|
142
|
+
for (const c of controls) {
|
|
143
|
+
if (c.framework)
|
|
144
|
+
fwSet.add(c.framework);
|
|
145
|
+
}
|
|
146
|
+
return [...fwSet];
|
|
147
|
+
}
|
|
159
148
|
export function collectDashboardData(projectPath) {
|
|
160
149
|
const config = loadConfig(projectPath);
|
|
161
150
|
let score = loadScore(projectPath);
|
|
162
|
-
|
|
151
|
+
let baseControls;
|
|
152
|
+
let frameworks;
|
|
153
|
+
if (config) {
|
|
154
|
+
baseControls = loadControlsForConfig(projectPath, config);
|
|
155
|
+
frameworks = config.frameworks;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
baseControls = loadControlsFromDisk(projectPath);
|
|
159
|
+
frameworks = [];
|
|
160
|
+
}
|
|
163
161
|
const findings = loadFindings(projectPath);
|
|
164
162
|
const controls = updateControlsFromFindings(baseControls, findings);
|
|
165
|
-
if (config) {
|
|
163
|
+
if (config || controls.length > 0) {
|
|
166
164
|
try {
|
|
167
|
-
const
|
|
165
|
+
const scoreFrameworks = frameworks.length > 0
|
|
166
|
+
? frameworks
|
|
167
|
+
: getFrameworksFromControls(controls);
|
|
168
|
+
const freshScore = generateScoreFile(controls, scoreFrameworks, findings);
|
|
168
169
|
score = freshScore;
|
|
169
170
|
}
|
|
170
171
|
catch {
|
|
@@ -176,6 +177,7 @@ export function collectDashboardData(projectPath) {
|
|
|
176
177
|
const installedPacks = getInstalledPackIds(projectPath, config || undefined);
|
|
177
178
|
const packs = allPacks.map(p => buildPackSummary(p, controls, findings, installedPacks));
|
|
178
179
|
const fixHistory = loadFixHistory(projectPath);
|
|
180
|
+
const activityLog = loadActivityLog(projectPath);
|
|
179
181
|
const metadataPath = path.join(projectPath, ".ges", "metadata.json");
|
|
180
182
|
let lastAudit = "";
|
|
181
183
|
try {
|
|
@@ -185,16 +187,22 @@ export function collectDashboardData(projectPath) {
|
|
|
185
187
|
catch {
|
|
186
188
|
lastAudit = new Date().toISOString();
|
|
187
189
|
}
|
|
190
|
+
const allFrameworks = new Set(frameworks);
|
|
191
|
+
for (const c of controls) {
|
|
192
|
+
if (c.framework)
|
|
193
|
+
allFrameworks.add(c.framework);
|
|
194
|
+
}
|
|
188
195
|
return {
|
|
189
196
|
projectName: config?.project_name || "Unknown Project",
|
|
190
197
|
projectType: config?.project_type || "unknown",
|
|
191
|
-
frameworks:
|
|
192
|
-
gesfVersion: "1.2.
|
|
198
|
+
frameworks: [...allFrameworks],
|
|
199
|
+
gesfVersion: "1.2.5",
|
|
193
200
|
score,
|
|
194
201
|
controls,
|
|
195
202
|
findings,
|
|
196
203
|
packs,
|
|
197
204
|
fixHistory,
|
|
205
|
+
activityLog,
|
|
198
206
|
lastAudit,
|
|
199
207
|
};
|
|
200
208
|
}
|
|
@@ -203,7 +211,9 @@ export function collectPackDetail(projectPath, packId) {
|
|
|
203
211
|
if (!pack)
|
|
204
212
|
return null;
|
|
205
213
|
const config = loadConfig(projectPath);
|
|
206
|
-
const baseControls = config
|
|
214
|
+
const baseControls = config
|
|
215
|
+
? loadControlsForConfig(projectPath, config)
|
|
216
|
+
: loadControlsFromDisk(projectPath);
|
|
207
217
|
const findings = loadFindings(projectPath);
|
|
208
218
|
const controls = updateControlsFromFindings(baseControls, findings);
|
|
209
219
|
const packControlIds = new Set(pack.controls.map(c => c.id));
|
|
@@ -281,9 +291,9 @@ export function collectPackDetail(projectPath, packId) {
|
|
|
281
291
|
}
|
|
282
292
|
export function collectControlDetail(projectPath, controlId) {
|
|
283
293
|
const config = loadConfig(projectPath);
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
294
|
+
const baseControls = config
|
|
295
|
+
? loadControlsForConfig(projectPath, config)
|
|
296
|
+
: loadControlsFromDisk(projectPath);
|
|
287
297
|
const findings = loadFindings(projectPath);
|
|
288
298
|
const controls = updateControlsFromFindings(baseControls, findings);
|
|
289
299
|
const control = controls.find(c => c.id === controlId);
|
|
@@ -380,6 +390,16 @@ export function startDashboard(options) {
|
|
|
380
390
|
}
|
|
381
391
|
return;
|
|
382
392
|
}
|
|
393
|
+
if (pathname === "/api/activity-log") {
|
|
394
|
+
try {
|
|
395
|
+
const data = collectDashboardData(options.projectPath);
|
|
396
|
+
jsonResponse(res, data.activityLog);
|
|
397
|
+
}
|
|
398
|
+
catch (err) {
|
|
399
|
+
jsonError(res, err instanceof Error ? err.message : String(err));
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
383
403
|
const packMatch = pathname.match(/^\/api\/packs\/([a-z0-9-]+)$/);
|
|
384
404
|
if (packMatch) {
|
|
385
405
|
try {
|
package/dist/template.js
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
function matchPackForControl(controlId, packs) {
|
|
2
|
+
const idUpper = controlId.toUpperCase();
|
|
3
|
+
for (const p of packs) {
|
|
4
|
+
if (p.id === "gdpr" && idUpper.startsWith("GDPR-"))
|
|
5
|
+
return p;
|
|
6
|
+
if (p.id === "owasp" && idUpper.startsWith("OWASP-"))
|
|
7
|
+
return p;
|
|
8
|
+
if (p.id === "cis" && idUpper.startsWith("CIS-"))
|
|
9
|
+
return p;
|
|
10
|
+
if (p.id === "nist" && idUpper.startsWith("NIST-"))
|
|
11
|
+
return p;
|
|
12
|
+
if (p.id === "ai" && idUpper.startsWith("AI-"))
|
|
13
|
+
return p;
|
|
14
|
+
if (p.id === "blockchain" && idUpper.startsWith("BC-"))
|
|
15
|
+
return p;
|
|
16
|
+
if (p.id === "government" && idUpper.startsWith("GOV-"))
|
|
17
|
+
return p;
|
|
18
|
+
if (p.id === "iso27001" && idUpper.startsWith("ISO27K-"))
|
|
19
|
+
return p;
|
|
20
|
+
if (p.id === "iso27701" && idUpper.startsWith("ISO277-"))
|
|
21
|
+
return p;
|
|
22
|
+
if (p.id === "hipaa" && idUpper.startsWith("HIPAA-"))
|
|
23
|
+
return p;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
1
27
|
function gradeColor(grade) {
|
|
2
28
|
switch (grade) {
|
|
3
29
|
case "A": return "#22c55e";
|
|
@@ -106,6 +132,45 @@ export function renderDashboard(data) {
|
|
|
106
132
|
...p,
|
|
107
133
|
_controls: undefined,
|
|
108
134
|
})));
|
|
135
|
+
const complianceIssues = controls
|
|
136
|
+
.filter(c => c.status !== "pass" && c.status !== "not-applicable")
|
|
137
|
+
.map(c => {
|
|
138
|
+
const pack = matchPackForControl(c.id, packs);
|
|
139
|
+
const auditFindings = findings.filter(f => f.controlIds.includes(c.id));
|
|
140
|
+
return {
|
|
141
|
+
controlId: c.id,
|
|
142
|
+
controlName: c.name,
|
|
143
|
+
severity: c.severity,
|
|
144
|
+
status: c.status,
|
|
145
|
+
category: c.category,
|
|
146
|
+
framework: c.framework,
|
|
147
|
+
article: c.article,
|
|
148
|
+
description: c.description,
|
|
149
|
+
implementation_guidance: c.implementation_guidance,
|
|
150
|
+
packId: pack?.id || "",
|
|
151
|
+
packName: pack?.name || "Direct",
|
|
152
|
+
passedChecks: c.checks.filter(ch => ch.status === "pass").length,
|
|
153
|
+
totalChecks: c.checks.length,
|
|
154
|
+
auditFindings,
|
|
155
|
+
};
|
|
156
|
+
})
|
|
157
|
+
.sort((a, b) => {
|
|
158
|
+
const sevOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
159
|
+
return (sevOrder[a.severity] ?? 4) - (sevOrder[b.severity] ?? 4);
|
|
160
|
+
});
|
|
161
|
+
const issuesBySeverity = {
|
|
162
|
+
critical: complianceIssues.filter(i => i.severity === "critical").length,
|
|
163
|
+
high: complianceIssues.filter(i => i.severity === "high").length,
|
|
164
|
+
medium: complianceIssues.filter(i => i.severity === "medium").length,
|
|
165
|
+
low: complianceIssues.filter(i => i.severity === "low").length,
|
|
166
|
+
};
|
|
167
|
+
const issuesByPackId = {};
|
|
168
|
+
for (const issue of complianceIssues) {
|
|
169
|
+
const key = issue.packId || "direct";
|
|
170
|
+
if (!issuesByPackId[key])
|
|
171
|
+
issuesByPackId[key] = [];
|
|
172
|
+
issuesByPackId[key].push(issue);
|
|
173
|
+
}
|
|
109
174
|
return `<!DOCTYPE html>
|
|
110
175
|
<html lang="en">
|
|
111
176
|
<head>
|
|
@@ -252,8 +317,8 @@ export function renderDashboard(data) {
|
|
|
252
317
|
|
|
253
318
|
<div class="header">
|
|
254
319
|
<div>
|
|
255
|
-
<h1
|
|
256
|
-
<div class="subtitle"
|
|
320
|
+
<h1>${escapeHtml(data.projectName)}</h1>
|
|
321
|
+
<div class="subtitle">GESF v${escapeHtml(data.gesfVersion)}</div>
|
|
257
322
|
</div>
|
|
258
323
|
<div class="nav-tabs">
|
|
259
324
|
<button class="nav-tab active" onclick="showPage('overview', this)">Overview</button>
|
|
@@ -261,6 +326,7 @@ export function renderDashboard(data) {
|
|
|
261
326
|
<button class="nav-tab" onclick="showPage('fixes', this)">Fixes Detail</button>
|
|
262
327
|
<button class="nav-tab" onclick="showPage('findings', this)">Findings</button>
|
|
263
328
|
<button class="nav-tab" onclick="showPage('traceability', this)">Traceability</button>
|
|
329
|
+
<button class="nav-tab" onclick="showPage('activity', this)">Activity Log</button>
|
|
264
330
|
</div>
|
|
265
331
|
</div>
|
|
266
332
|
|
|
@@ -405,100 +471,97 @@ export function renderDashboard(data) {
|
|
|
405
471
|
<div id="page-fixes" class="page">
|
|
406
472
|
<div class="tab-bar" style="margin-bottom:0;">
|
|
407
473
|
<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 (${
|
|
474
|
+
<button class="tab-btn" onclick="showFixesTab('pending', this)">Pending Fixes (${complianceIssues.length})</button>
|
|
409
475
|
</div>
|
|
410
476
|
|
|
411
477
|
<div id="fixes-tab-history" class="tab-panel active">
|
|
412
|
-
${renderFixHistorySection(data.fixHistory)}
|
|
478
|
+
${renderFixHistorySection(data.fixHistory, complianceIssues)}
|
|
413
479
|
</div>
|
|
414
480
|
|
|
415
481
|
<div id="fixes-tab-pending" class="tab-panel">
|
|
416
|
-
${
|
|
482
|
+
${renderComplianceFixCards(complianceIssues, "fix")}
|
|
417
483
|
</div>
|
|
418
484
|
</div>
|
|
419
485
|
|
|
420
486
|
<div id="page-findings" class="page">
|
|
421
487
|
<div id="findings-main">
|
|
422
|
-
<h2 style="font-size:20px;font-weight:700;margin-bottom:
|
|
488
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Compliance Findings & Issues</h2>
|
|
489
|
+
<p style="color:#6b7280;font-size:14px;margin-bottom:20px;">Every control that is not passing is a compliance finding. Code-level audit evidence is shown where available.</p>
|
|
490
|
+
|
|
491
|
+
<div class="grid grid-4" style="margin-bottom:20px;">
|
|
492
|
+
<div class="card stat"><div class="num" style="color:${complianceIssues.length > 0 ? '#ef4444' : '#22c55e'};">${complianceIssues.length}</div><div class="label">Total Issues</div></div>
|
|
493
|
+
<div class="card stat"><div class="num" style="color:#ef4444;">${issuesBySeverity.critical}</div><div class="label">Critical</div></div>
|
|
494
|
+
<div class="card stat"><div class="num" style="color:#f97316;">${issuesBySeverity.high}</div><div class="label">High</div></div>
|
|
495
|
+
<div class="card stat"><div class="num">${findings.length}</div><div class="label">Audit Evidence</div></div>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
423
498
|
<div class="tab-bar">
|
|
424
|
-
<button class="tab-btn active" onclick="showFindingsTab('all', this)">All (${
|
|
425
|
-
<button class="tab-btn" onclick="showFindingsTab('critical', this)">Critical (${
|
|
426
|
-
<button class="tab-btn" onclick="showFindingsTab('high', this)">High (${
|
|
427
|
-
<button class="tab-btn" onclick="showFindingsTab('medium', this)">Medium (${
|
|
428
|
-
<button class="tab-btn" onclick="showFindingsTab('low', this)">Low (${
|
|
499
|
+
<button class="tab-btn active" onclick="showFindingsTab('all', this)">All Issues (${complianceIssues.length})</button>
|
|
500
|
+
<button class="tab-btn" onclick="showFindingsTab('critical', this)">Critical (${issuesBySeverity.critical})</button>
|
|
501
|
+
<button class="tab-btn" onclick="showFindingsTab('high', this)">High (${issuesBySeverity.high})</button>
|
|
502
|
+
<button class="tab-btn" onclick="showFindingsTab('medium', this)">Medium (${issuesBySeverity.medium})</button>
|
|
503
|
+
<button class="tab-btn" onclick="showFindingsTab('low', this)">Low (${issuesBySeverity.low})</button>
|
|
429
504
|
<button class="tab-btn" onclick="showFindingsTab('bypack', this)">By Pack</button>
|
|
505
|
+
${findings.length > 0 ? `<button class="tab-btn" onclick="showFindingsTab('evidence', this)">Audit Evidence (${findings.length})</button>` : ''}
|
|
430
506
|
</div>
|
|
431
507
|
|
|
432
508
|
<div id="findings-tab-all" class="tab-panel active">
|
|
433
|
-
${
|
|
509
|
+
${renderComplianceIssuesTable(complianceIssues)}
|
|
434
510
|
</div>
|
|
435
|
-
<div id="findings-tab-critical" class="tab-panel">${
|
|
436
|
-
<div id="findings-tab-high" class="tab-panel">${
|
|
437
|
-
<div id="findings-tab-medium" class="tab-panel">${
|
|
438
|
-
<div id="findings-tab-low" class="tab-panel">${
|
|
511
|
+
<div id="findings-tab-critical" class="tab-panel">${renderComplianceIssuesTable(complianceIssues.filter(i => i.severity === "critical"))}</div>
|
|
512
|
+
<div id="findings-tab-high" class="tab-panel">${renderComplianceIssuesTable(complianceIssues.filter(i => i.severity === "high"))}</div>
|
|
513
|
+
<div id="findings-tab-medium" class="tab-panel">${renderComplianceIssuesTable(complianceIssues.filter(i => i.severity === "medium"))}</div>
|
|
514
|
+
<div id="findings-tab-low" class="tab-panel">${renderComplianceIssuesTable(complianceIssues.filter(i => i.severity === "low"))}</div>
|
|
439
515
|
<div id="findings-tab-bypack" class="tab-panel">
|
|
440
|
-
${packs.filter(p => (
|
|
516
|
+
${packs.filter(p => (issuesByPackId[p.id] || []).length > 0).length > 0 ? packs.filter(p => (issuesByPackId[p.id] || []).length > 0).map(p => `
|
|
441
517
|
<div class="card" style="margin-bottom:16px;">
|
|
442
518
|
<div class="card-title" style="cursor:pointer;" onclick="loadPackDetail('${p.id}')">
|
|
443
|
-
${escapeHtml(p.name)} — ${(
|
|
519
|
+
${escapeHtml(p.name)} — ${(issuesByPackId[p.id] || []).length} issues
|
|
444
520
|
<span style="float:right;color:#0f766e;font-weight:400;font-size:11px;">View pack details →</span>
|
|
445
521
|
</div>
|
|
446
|
-
${
|
|
522
|
+
${renderComplianceIssuesTable(issuesByPackId[p.id] || [])}
|
|
447
523
|
</div>
|
|
448
|
-
`).join('') : '<div class="empty-state"><div class="msg">No
|
|
524
|
+
`).join('') : '<div class="empty-state"><div class="msg">No compliance issues mapped to policy packs</div></div>'}
|
|
449
525
|
</div>
|
|
526
|
+
${findings.length > 0 ? `<div id="findings-tab-evidence" class="tab-panel">${renderFindingsTable(findings)}</div>` : ''}
|
|
450
527
|
</div>
|
|
451
528
|
<div id="finding-detail" style="display:none;"></div>
|
|
452
529
|
</div>
|
|
453
530
|
|
|
454
531
|
<div id="page-traceability" class="page">
|
|
455
|
-
<h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">
|
|
456
|
-
<p style="color:#6b7280;font-size:14px;margin-bottom:20px;">
|
|
532
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Compliance Traceability Matrix</h2>
|
|
533
|
+
<p style="color:#6b7280;font-size:14px;margin-bottom:20px;">Full traceability: Control → Framework → Policy Pack → Severity → Fix Guidance for every compliance issue.</p>
|
|
457
534
|
<div class="tab-bar">
|
|
458
|
-
<button class="tab-btn active" onclick="showTraceTab('matrix', this)">Matrix</button>
|
|
535
|
+
<button class="tab-btn active" onclick="showTraceTab('matrix', this)">Matrix (${complianceIssues.length})</button>
|
|
459
536
|
<button class="tab-btn" onclick="showTraceTab('fixes', this)">Prioritized Fixes</button>
|
|
460
537
|
<button class="tab-btn" onclick="showTraceTab('controls', this)">Control Coverage</button>
|
|
461
538
|
</div>
|
|
462
539
|
|
|
463
540
|
<div id="trace-tab-matrix" class="tab-panel active">
|
|
464
|
-
${
|
|
541
|
+
${complianceIssues.length > 0 ? `<div class="card">
|
|
465
542
|
<table>
|
|
466
|
-
<thead><tr><th>
|
|
543
|
+
<thead><tr><th>Control</th><th>Severity</th><th>Framework</th><th>Policy Pack</th><th>Status</th><th>Checks</th><th>Audit</th><th>Fix Guidance</th></tr></thead>
|
|
467
544
|
<tbody>
|
|
468
|
-
${
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
<div style="font-weight:600;font-size:13px;">${escapeHtml(f.title)}</div>
|
|
482
|
-
<div style="font-size:11px;color:#6b7280;">${escapeHtml(f.ruleId)}</div>
|
|
483
|
-
</td>
|
|
484
|
-
<td><span class="badge badge-sev" style="background:${severityColor(f.severity)}">${f.severity.toUpperCase()}</span></td>
|
|
485
|
-
<td style="font-family:monospace;font-size:11px;">${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</td>
|
|
486
|
-
<td>${linkedControls.length > 0 ? linkedControls.map(c => `<div style="margin-bottom:2px;"><span class="link" onclick="showControlDetail('${escapeHtml(c.id)}')">${escapeHtml(c.id)}</span> <span style="color:#6b7280;font-size:11px;">${escapeHtml(c.name)}</span></div>`).join('') : '<span style="color:#9ca3af;">No linked controls</span>'}</td>
|
|
487
|
-
<td>${linkedPackIds.size > 0 ? [...linkedPackIds].map(pid => {
|
|
488
|
-
const pk = packs.find(pp => pp.id === pid);
|
|
489
|
-
return pk ? `<span class="tag" style="cursor:pointer;" onclick="loadPackDetail('${pk.id}')">${escapeHtml(pk.name)}</span>` : '';
|
|
490
|
-
}).join(' ') : '<span style="color:#9ca3af;">-</span>'}</td>
|
|
491
|
-
<td style="max-width:300px;font-size:12px;color:#374151;">${escapeHtml(f.fix)}</td>
|
|
492
|
-
</tr>`;
|
|
493
|
-
}).join('')}
|
|
545
|
+
${complianceIssues.map(issue => `<tr>
|
|
546
|
+
<td>
|
|
547
|
+
<span class="link" onclick="showControlDetail('${escapeHtml(issue.controlId)}')">${escapeHtml(issue.controlId)}</span>
|
|
548
|
+
<div style="font-size:12px;color:#4b5563;">${escapeHtml(issue.controlName)}</div>
|
|
549
|
+
</td>
|
|
550
|
+
<td><span class="badge badge-sev" style="background:${severityColor(issue.severity)}">${issue.severity.toUpperCase()}</span></td>
|
|
551
|
+
<td style="font-size:12px;">${escapeHtml(issue.framework)}${issue.article ? '<br><span style="color:#6b7280;">' + escapeHtml(issue.article) + '</span>' : ''}</td>
|
|
552
|
+
<td>${issue.packId ? `<span class="tag" style="cursor:pointer;" onclick="loadPackDetail('${issue.packId}')">${escapeHtml(issue.packName)}</span>` : '<span style="color:#9ca3af;">-</span>'}</td>
|
|
553
|
+
<td><span class="badge badge-status" style="background:${statusColor(issue.status)}">${statusLabel(issue.status)}</span></td>
|
|
554
|
+
<td style="font-size:12px;">${issue.passedChecks}/${issue.totalChecks}</td>
|
|
555
|
+
<td>${issue.auditFindings.length > 0 ? `<span style="color:#ef4444;font-weight:600;">${issue.auditFindings.length}</span>` : '<span style="color:#9ca3af;">0</span>'}</td>
|
|
556
|
+
<td style="max-width:280px;font-size:12px;color:#374151;">${escapeHtml(issue.implementation_guidance.substring(0, 150))}${issue.implementation_guidance.length > 150 ? '...' : ''}</td>
|
|
557
|
+
</tr>`).join('')}
|
|
494
558
|
</tbody>
|
|
495
559
|
</table>
|
|
496
|
-
|
|
497
|
-
</div>` : '<div class="card"><div class="empty-state"><div class="icon">✓</div><div class="msg" style="color:#22c55e;">No findings to trace</div><div class="sub">All clear</div></div></div>'}
|
|
560
|
+
</div>` : '<div class="card"><div class="empty-state"><div class="icon">✓</div><div class="msg" style="color:#22c55e;">No issues to trace</div><div class="sub">All controls passing</div></div></div>'}
|
|
498
561
|
</div>
|
|
499
562
|
|
|
500
563
|
<div id="trace-tab-fixes" class="tab-panel">
|
|
501
|
-
${
|
|
564
|
+
${renderComplianceFixCards(complianceIssues, "trace")}
|
|
502
565
|
</div>
|
|
503
566
|
|
|
504
567
|
<div id="trace-tab-controls" class="tab-panel">
|
|
@@ -527,12 +590,16 @@ export function renderDashboard(data) {
|
|
|
527
590
|
</div>
|
|
528
591
|
</div>
|
|
529
592
|
|
|
593
|
+
<div id="page-activity" class="page">
|
|
594
|
+
${renderActivityLogSection(data.activityLog || [])}
|
|
595
|
+
</div>
|
|
596
|
+
|
|
530
597
|
<div id="control-detail-modal" style="display:none;"></div>
|
|
531
598
|
|
|
532
599
|
</div>
|
|
533
600
|
|
|
534
601
|
<div class="footer">
|
|
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>
|
|
602
|
+
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> | <a href="/api/activity-log">Activity Log API</a>
|
|
536
603
|
</div>
|
|
537
604
|
|
|
538
605
|
<script>
|
|
@@ -553,7 +620,7 @@ export function renderDashboard(data) {
|
|
|
553
620
|
}
|
|
554
621
|
};
|
|
555
622
|
|
|
556
|
-
var navTabMap = { overview: 0, packs: 1, fixes: 2, findings: 3, traceability: 4 };
|
|
623
|
+
var navTabMap = { overview: 0, packs: 1, fixes: 2, findings: 3, traceability: 4, activity: 5 };
|
|
557
624
|
|
|
558
625
|
window.navigateToPage = function(page) {
|
|
559
626
|
var tabs = document.querySelectorAll('.nav-tab');
|
|
@@ -597,6 +664,11 @@ export function renderDashboard(data) {
|
|
|
597
664
|
if (btn) btn.classList.add('active');
|
|
598
665
|
};
|
|
599
666
|
|
|
667
|
+
window.goToPendingFixes = function() {
|
|
668
|
+
var btns = document.querySelectorAll('#page-fixes .tab-btn');
|
|
669
|
+
showFixesTab('pending', btns.length > 1 ? btns[1] : (btns[0] || null));
|
|
670
|
+
};
|
|
671
|
+
|
|
600
672
|
window.showTraceTab = function(tab, btn) {
|
|
601
673
|
var panels = document.querySelectorAll('#page-traceability .tab-panel');
|
|
602
674
|
for (var i = 0; i < panels.length; i++) panels[i].classList.remove('active');
|
|
@@ -1040,7 +1112,7 @@ function renderDetailedFixesList(findings, controls, packs) {
|
|
|
1040
1112
|
}
|
|
1041
1113
|
return html;
|
|
1042
1114
|
}
|
|
1043
|
-
function renderFixHistorySection(entries) {
|
|
1115
|
+
function renderFixHistorySection(entries, complianceIssues = []) {
|
|
1044
1116
|
if (entries.length === 0) {
|
|
1045
1117
|
return `<div class="card">
|
|
1046
1118
|
<h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Compliance Fix History</h2>
|
|
@@ -1049,6 +1121,7 @@ function renderFixHistorySection(entries) {
|
|
|
1049
1121
|
<div class="icon">📋</div>
|
|
1050
1122
|
<div class="msg">No fixes recorded yet</div>
|
|
1051
1123
|
<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>
|
|
1124
|
+
${complianceIssues.length > 0 ? `<div style="margin-top:16px;"><span class="badge badge-status" style="background:#f97316;font-size:12px;padding:4px 12px;">${complianceIssues.length} pending fixes</span> <span class="link" style="font-size:13px;" onclick="goToPendingFixes()">View pending fixes →</span></div>` : ''}
|
|
1052
1125
|
</div>
|
|
1053
1126
|
</div>`;
|
|
1054
1127
|
}
|
|
@@ -1193,6 +1266,241 @@ function renderFixHistorySection(entries) {
|
|
|
1193
1266
|
html += `</div>`;
|
|
1194
1267
|
return html;
|
|
1195
1268
|
}
|
|
1269
|
+
function renderComplianceIssuesTable(issues) {
|
|
1270
|
+
if (issues.length === 0) {
|
|
1271
|
+
return '<div class="empty-state"><div class="icon">✓</div><div class="msg" style="color:#22c55e;">No compliance issues in this category</div></div>';
|
|
1272
|
+
}
|
|
1273
|
+
return `<table>
|
|
1274
|
+
<thead><tr><th>Severity</th><th>Control</th><th>Framework</th><th>Policy Pack</th><th>Status</th><th>Checks</th><th>Audit</th><th>Fix Guidance</th></tr></thead>
|
|
1275
|
+
<tbody>
|
|
1276
|
+
${issues.map(issue => `<tr>
|
|
1277
|
+
<td><span class="badge badge-sev" style="background:${severityColor(issue.severity)}">${issue.severity.toUpperCase()}</span></td>
|
|
1278
|
+
<td>
|
|
1279
|
+
<span class="link" onclick="showControlDetail('${escapeHtml(issue.controlId)}')">${escapeHtml(issue.controlId)}</span>
|
|
1280
|
+
<div style="font-size:12px;color:#4b5563;">${escapeHtml(issue.controlName)}</div>
|
|
1281
|
+
</td>
|
|
1282
|
+
<td style="font-size:12px;">${escapeHtml(issue.framework)}${issue.article ? '<br><span style="color:#6b7280;">' + escapeHtml(issue.article) + '</span>' : ''}</td>
|
|
1283
|
+
<td>${issue.packId ? `<span class="tag" style="cursor:pointer;" onclick="loadPackDetail('${issue.packId}')">${escapeHtml(issue.packName)}</span>` : '<span style="color:#9ca3af;">Direct</span>'}</td>
|
|
1284
|
+
<td><span class="badge badge-status" style="background:${statusColor(issue.status)}">${statusLabel(issue.status)}</span></td>
|
|
1285
|
+
<td style="font-size:12px;">${issue.passedChecks}/${issue.totalChecks}</td>
|
|
1286
|
+
<td>${issue.auditFindings.length > 0 ? `<span style="color:#ef4444;font-weight:600;">${issue.auditFindings.length}</span>` : '<span style="color:#9ca3af;">0</span>'}</td>
|
|
1287
|
+
<td style="max-width:280px;font-size:12px;color:#4b5563;">${escapeHtml(issue.implementation_guidance.substring(0, 200))}${issue.implementation_guidance.length > 200 ? '...' : ''}</td>
|
|
1288
|
+
</tr>`).join('')}
|
|
1289
|
+
</tbody>
|
|
1290
|
+
</table>`;
|
|
1291
|
+
}
|
|
1292
|
+
function renderComplianceFixCards(issues, idPrefix) {
|
|
1293
|
+
if (issues.length === 0) {
|
|
1294
|
+
return '<div class="card"><div class="empty-state"><div class="icon">✓</div><div class="msg" style="color:#22c55e;">All controls passing</div><div class="sub">No fixes needed</div></div></div>';
|
|
1295
|
+
}
|
|
1296
|
+
const totalAuditFindings = issues.reduce((sum, i) => sum + i.auditFindings.length, 0);
|
|
1297
|
+
const criticalCount = issues.filter(i => i.severity === "critical").length;
|
|
1298
|
+
const highCount = issues.filter(i => i.severity === "high").length;
|
|
1299
|
+
let html = '';
|
|
1300
|
+
html += `<div style="margin-bottom:20px;">`;
|
|
1301
|
+
html += `<div class="grid grid-4" style="margin-bottom:20px;">`;
|
|
1302
|
+
html += `<div class="card stat"><div class="num" style="color:#ef4444;">${issues.length}</div><div class="label">Controls to Fix</div></div>`;
|
|
1303
|
+
html += `<div class="card stat"><div class="num" style="color:#ef4444;">${criticalCount}</div><div class="label">Critical</div></div>`;
|
|
1304
|
+
html += `<div class="card stat"><div class="num" style="color:#f97316;">${highCount}</div><div class="label">High</div></div>`;
|
|
1305
|
+
html += `<div class="card stat"><div class="num">${totalAuditFindings}</div><div class="label">Audit Findings</div></div>`;
|
|
1306
|
+
html += `</div>`;
|
|
1307
|
+
html += `</div>`;
|
|
1308
|
+
for (let i = 0; i < issues.length; i++) {
|
|
1309
|
+
const issue = issues[i];
|
|
1310
|
+
const fixId = `${idPrefix}-${i}`;
|
|
1311
|
+
html += `<div class="fix-detail-card">`;
|
|
1312
|
+
html += `<div class="fix-detail-header ${issue.severity}" onclick="toggleFix('${fixId}')">`;
|
|
1313
|
+
html += `<div class="fix-detail-num" style="color:${severityColor(issue.severity)};">${i + 1}</div>`;
|
|
1314
|
+
html += `<div class="fix-detail-info">`;
|
|
1315
|
+
html += `<div class="fix-detail-title">${escapeHtml(issue.controlName)}</div>`;
|
|
1316
|
+
html += `<div class="fix-detail-meta">${escapeHtml(issue.controlId)} | ${escapeHtml(issue.category)} | ${escapeHtml(issue.framework)}${issue.article ? ' | ' + escapeHtml(issue.article) : ''} | Pack: ${escapeHtml(issue.packName)}</div>`;
|
|
1317
|
+
html += `</div>`;
|
|
1318
|
+
html += `<div class="fix-detail-badges">`;
|
|
1319
|
+
html += `<span class="badge badge-sev" style="background:${severityColor(issue.severity)}">${issue.severity.toUpperCase()}</span>`;
|
|
1320
|
+
html += `<span class="badge badge-status" style="background:${statusColor(issue.status)}">${statusLabel(issue.status)}</span>`;
|
|
1321
|
+
html += `<span style="font-size:12px;color:#6b7280;">${issue.passedChecks}/${issue.totalChecks} checks</span>`;
|
|
1322
|
+
if (issue.auditFindings.length > 0) {
|
|
1323
|
+
html += `<span style="font-size:12px;color:#ef4444;font-weight:600;">${issue.auditFindings.length} evidence</span>`;
|
|
1324
|
+
}
|
|
1325
|
+
html += `<span class="fix-toggle" id="${fixId}-toggle">Expand</span>`;
|
|
1326
|
+
html += `</div></div>`;
|
|
1327
|
+
html += `<div class="fix-detail-body" id="${fixId}">`;
|
|
1328
|
+
html += `<div class="fix-section"><div class="fix-section-title">Description</div>`;
|
|
1329
|
+
html += `<div style="font-size:13px;color:#4b5563;line-height:1.6;">${escapeHtml(issue.description)}</div>`;
|
|
1330
|
+
html += `</div>`;
|
|
1331
|
+
if (issue.auditFindings.length > 0) {
|
|
1332
|
+
html += `<div class="fix-section"><div class="fix-section-title">Audit Evidence (${issue.auditFindings.length})</div>`;
|
|
1333
|
+
for (const f of issue.auditFindings) {
|
|
1334
|
+
html += `<div class="fix-finding-item ${f.severity}">`;
|
|
1335
|
+
html += `<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">`;
|
|
1336
|
+
html += `<span class="badge badge-sev" style="background:${severityColor(f.severity)};font-size:10px;">${f.severity.toUpperCase()}</span>`;
|
|
1337
|
+
html += `<strong style="font-size:13px;">${escapeHtml(f.title)}</strong></div>`;
|
|
1338
|
+
html += `<div style="font-size:12px;color:#6b7280;"><span style="font-family:monospace;font-weight:600;">${escapeHtml(f.ruleId)}</span> — <span style="font-family:monospace;">${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</span></div>`;
|
|
1339
|
+
if (f.description)
|
|
1340
|
+
html += `<div style="font-size:12px;color:#4b5563;margin-top:4px;">${escapeHtml(f.description)}</div>`;
|
|
1341
|
+
if (f.evidence)
|
|
1342
|
+
html += `<div class="fix-evidence">${escapeHtml(f.evidence)}</div>`;
|
|
1343
|
+
html += `</div>`;
|
|
1344
|
+
}
|
|
1345
|
+
html += `</div>`;
|
|
1346
|
+
}
|
|
1347
|
+
html += `<div class="fix-section"><div class="fix-section-title">Fix Guidance</div>`;
|
|
1348
|
+
html += `<div class="fix-guidance-box"><strong>How to fix:</strong> ${escapeHtml(issue.implementation_guidance)}</div>`;
|
|
1349
|
+
for (const f of issue.auditFindings) {
|
|
1350
|
+
if (f.fix) {
|
|
1351
|
+
html += `<div class="fix-guidance-box" style="margin-top:8px;background:#eff6ff;border-color:#bfdbfe;"><strong>Fix for ${escapeHtml(f.ruleId)}:</strong> ${escapeHtml(f.fix)}</div>`;
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
html += `</div>`;
|
|
1355
|
+
html += `<div class="fix-section"><div class="fix-section-title">Traceability</div>`;
|
|
1356
|
+
html += `<table><tbody>`;
|
|
1357
|
+
html += `<tr><td style="font-weight:600;width:160px;">Control</td><td><span class="link" onclick="showControlDetail('${escapeHtml(issue.controlId)}')">${escapeHtml(issue.controlId)}</span> — ${escapeHtml(issue.controlName)}</td></tr>`;
|
|
1358
|
+
html += `<tr><td style="font-weight:600;">Category</td><td>${escapeHtml(issue.category)}</td></tr>`;
|
|
1359
|
+
html += `<tr><td style="font-weight:600;">Framework</td><td>${escapeHtml(issue.framework)}${issue.article ? ' / ' + escapeHtml(issue.article) : ''}</td></tr>`;
|
|
1360
|
+
html += `<tr><td style="font-weight:600;">Policy Pack</td><td>${issue.packId ? `<span class="tag" style="cursor:pointer;" onclick="loadPackDetail('${issue.packId}')">${escapeHtml(issue.packName)}</span>` : 'Direct'}</td></tr>`;
|
|
1361
|
+
html += `<tr><td style="font-weight:600;">Severity</td><td><span class="badge badge-sev" style="background:${severityColor(issue.severity)}">${issue.severity.toUpperCase()}</span></td></tr>`;
|
|
1362
|
+
html += `<tr><td style="font-weight:600;">Status</td><td><span class="badge badge-status" style="background:${statusColor(issue.status)}">${statusLabel(issue.status)}</span></td></tr>`;
|
|
1363
|
+
html += `<tr><td style="font-weight:600;">Checks</td><td>${issue.passedChecks}/${issue.totalChecks} passed</td></tr>`;
|
|
1364
|
+
html += `<tr><td style="font-weight:600;">Audit Evidence</td><td>${issue.auditFindings.length} finding(s)</td></tr>`;
|
|
1365
|
+
html += `</tbody></table>`;
|
|
1366
|
+
html += `</div>`;
|
|
1367
|
+
html += `</div></div>`;
|
|
1368
|
+
}
|
|
1369
|
+
return html;
|
|
1370
|
+
}
|
|
1371
|
+
function renderActivityLogSection(entries) {
|
|
1372
|
+
if (!entries || entries.length === 0) {
|
|
1373
|
+
return `<div class="card">
|
|
1374
|
+
<h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Activity Log</h2>
|
|
1375
|
+
<p style="color:#6b7280;font-size:14px;margin-bottom:16px;">Every GESF operation performed via CLI or MCP is recorded here — the single source of truth for what GESF did to your project.</p>
|
|
1376
|
+
<div class="empty-state">
|
|
1377
|
+
<div class="icon">📋</div>
|
|
1378
|
+
<div class="msg">No activity recorded yet</div>
|
|
1379
|
+
<div class="sub">Operations will appear here as you use GESF commands (<code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:12px;">ges init</code>, <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:12px;">ges audit</code>, <code style="background:#f3f4f6;padding:2px 6px;border-radius:4px;font-size:12px;">ges fix</code>, MCP tools, etc.)</div>
|
|
1380
|
+
</div>
|
|
1381
|
+
</div>`;
|
|
1382
|
+
}
|
|
1383
|
+
const sorted = [...entries].sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
1384
|
+
const bySource = { cli: entries.filter(e => e.source === "cli").length, mcp: entries.filter(e => e.source === "mcp").length };
|
|
1385
|
+
const byStatus = {
|
|
1386
|
+
success: entries.filter(e => e.status === "success").length,
|
|
1387
|
+
partial: entries.filter(e => e.status === "partial").length,
|
|
1388
|
+
failed: entries.filter(e => e.status === "failed").length,
|
|
1389
|
+
info: entries.filter(e => e.status === "info").length,
|
|
1390
|
+
};
|
|
1391
|
+
const byAction = {};
|
|
1392
|
+
for (const e of entries) {
|
|
1393
|
+
byAction[e.action] = (byAction[e.action] || 0) + 1;
|
|
1394
|
+
}
|
|
1395
|
+
const actionLabels = {
|
|
1396
|
+
init: "Project Init",
|
|
1397
|
+
audit: "Audit Run",
|
|
1398
|
+
fix: "Auto-Fix",
|
|
1399
|
+
policy_install: "Pack Installed",
|
|
1400
|
+
policy_remove: "Pack Removed",
|
|
1401
|
+
control_override: "Control Override",
|
|
1402
|
+
implement_control: "Control Implemented",
|
|
1403
|
+
score: "Score Generated",
|
|
1404
|
+
scan: "Scanners Run",
|
|
1405
|
+
validate: "Validation",
|
|
1406
|
+
generate: "Docs Generated",
|
|
1407
|
+
hooks_install: "Hooks Installed",
|
|
1408
|
+
hooks_uninstall: "Hooks Removed",
|
|
1409
|
+
dashboard_start: "Dashboard Started",
|
|
1410
|
+
badge_generate: "Badge Generated",
|
|
1411
|
+
};
|
|
1412
|
+
const actionColors = {
|
|
1413
|
+
init: "#0f766e",
|
|
1414
|
+
audit: "#3b82f6",
|
|
1415
|
+
fix: "#22c55e",
|
|
1416
|
+
policy_install: "#8b5cf6",
|
|
1417
|
+
policy_remove: "#ef4444",
|
|
1418
|
+
control_override: "#eab308",
|
|
1419
|
+
implement_control: "#22c55e",
|
|
1420
|
+
score: "#3b82f6",
|
|
1421
|
+
scan: "#f97316",
|
|
1422
|
+
validate: "#6b7280",
|
|
1423
|
+
generate: "#0f766e",
|
|
1424
|
+
hooks_install: "#6b7280",
|
|
1425
|
+
hooks_uninstall: "#6b7280",
|
|
1426
|
+
dashboard_start: "#8b5cf6",
|
|
1427
|
+
badge_generate: "#0f766e",
|
|
1428
|
+
};
|
|
1429
|
+
const statusColors = {
|
|
1430
|
+
success: "#22c55e",
|
|
1431
|
+
partial: "#eab308",
|
|
1432
|
+
failed: "#ef4444",
|
|
1433
|
+
info: "#3b82f6",
|
|
1434
|
+
};
|
|
1435
|
+
let html = '';
|
|
1436
|
+
html += `<h2 style="font-size:20px;font-weight:700;margin-bottom:8px;">Activity Log</h2>`;
|
|
1437
|
+
html += `<p style="color:#6b7280;font-size:14px;margin-bottom:16px;">Every GESF operation performed via CLI or MCP is recorded here — the single source of truth for what GESF did to your project.</p>`;
|
|
1438
|
+
html += `<div class="grid grid-4" style="margin-bottom:20px;">`;
|
|
1439
|
+
html += `<div class="card stat"><div class="num">${entries.length}</div><div class="label">Total Operations</div></div>`;
|
|
1440
|
+
html += `<div class="card stat"><div class="num" style="color:#22c55e;">${byStatus.success}</div><div class="label">Successful</div></div>`;
|
|
1441
|
+
html += `<div class="card stat"><div class="num" style="color:#ef4444;">${byStatus.failed + byStatus.partial}</div><div class="label">Failed/Partial</div></div>`;
|
|
1442
|
+
html += `<div class="card stat"><div class="num" style="color:#0f766e;">${bySource.cli}</div><div class="label">CLI Source</div></div>`;
|
|
1443
|
+
html += `</div>`;
|
|
1444
|
+
html += `<div class="grid grid-2" style="margin-bottom:20px;">`;
|
|
1445
|
+
html += `<div class="card"><div class="card-title">Operations by Type</div>`;
|
|
1446
|
+
for (const [action, count] of Object.entries(byAction).sort((a, b) => b[1] - a[1])) {
|
|
1447
|
+
const label = actionLabels[action] || action;
|
|
1448
|
+
const color = actionColors[action] || "#6b7280";
|
|
1449
|
+
html += `<div class="framework-row"><div class="framework-name" style="min-width:160px;font-size:13px;"><span class="badge" style="background:${color};font-size:10px;margin-right:6px;">${label}</span></div><div style="flex:1;"></div><div class="pct-text">${count}</div></div>`;
|
|
1450
|
+
}
|
|
1451
|
+
html += `</div>`;
|
|
1452
|
+
html += `<div class="card"><div class="card-title">Sources & Status</div>`;
|
|
1453
|
+
html += `<div style="display:flex;flex-wrap:wrap;gap:16px;margin-top:8px;">`;
|
|
1454
|
+
html += `<div class="stat"><div class="num" style="font-size:20px;">${bySource.cli}</div><div class="label">CLI</div></div>`;
|
|
1455
|
+
html += `<div class="stat"><div class="num" style="font-size:20px;">${bySource.mcp}</div><div class="label">MCP</div></div>`;
|
|
1456
|
+
html += `<div class="stat"><div class="num" style="font-size:20px;color:#22c55e;">${byStatus.success}</div><div class="label">Success</div></div>`;
|
|
1457
|
+
html += `<div class="stat"><div class="num" style="font-size:20px;color:#ef4444;">${byStatus.failed}</div><div class="label">Failed</div></div>`;
|
|
1458
|
+
html += `</div></div>`;
|
|
1459
|
+
html += `</div>`;
|
|
1460
|
+
html += `<div class="card"><div class="card-title">Timeline (newest first)</div>`;
|
|
1461
|
+
html += `<table><thead><tr><th>Time</th><th>Source</th><th>Action</th><th>Status</th><th>Description</th><th>Impact</th></tr></thead><tbody>`;
|
|
1462
|
+
for (const entry of sorted) {
|
|
1463
|
+
const time = new Date(entry.timestamp).toLocaleString();
|
|
1464
|
+
const sourceBadge = entry.source === "mcp"
|
|
1465
|
+
? '<span class="badge" style="background:#7c3aed;font-size:10px;">MCP</span>'
|
|
1466
|
+
: '<span class="badge" style="background:#0f766e;font-size:10px;">CLI</span>';
|
|
1467
|
+
const actionLabel = actionLabels[entry.action] || entry.action;
|
|
1468
|
+
const actionColor = actionColors[entry.action] || "#6b7280";
|
|
1469
|
+
const actionBadge = `<span class="badge" style="background:${actionColor};font-size:10px;">${escapeHtml(actionLabel)}</span>`;
|
|
1470
|
+
const statusBadge = `<span class="badge badge-status" style="background:${statusColors[entry.status] || '#6b7280'};font-size:10px;">${entry.status.toUpperCase()}</span>`;
|
|
1471
|
+
const impactParts = [];
|
|
1472
|
+
if (entry.details.findings_count !== undefined)
|
|
1473
|
+
impactParts.push(`${entry.details.findings_count} findings`);
|
|
1474
|
+
if (entry.details.fixes_applied !== undefined)
|
|
1475
|
+
impactParts.push(`${entry.details.fixes_applied} fixes`);
|
|
1476
|
+
if (entry.details.packs_affected && entry.details.packs_affected.length > 0)
|
|
1477
|
+
impactParts.push(`Packs: ${entry.details.packs_affected.join(", ")}`);
|
|
1478
|
+
if (entry.details.controls_affected && entry.details.controls_affected.length > 0)
|
|
1479
|
+
impactParts.push(`Controls: ${entry.details.controls_affected.length}`);
|
|
1480
|
+
if (entry.details.files_created && entry.details.files_created.length > 0)
|
|
1481
|
+
impactParts.push(`${entry.details.files_created.length} files created`);
|
|
1482
|
+
if (entry.details.frameworks_added && entry.details.frameworks_added.length > 0)
|
|
1483
|
+
impactParts.push(`Added: ${entry.details.frameworks_added.join(", ")}`);
|
|
1484
|
+
if (entry.details.score !== undefined)
|
|
1485
|
+
impactParts.push(`Score: ${entry.details.score}%`);
|
|
1486
|
+
const impactHtml = impactParts.length > 0
|
|
1487
|
+
? impactParts.map(p => `<div style="font-size:11px;color:#6b7280;margin-bottom:2px;">${escapeHtml(p)}</div>`).join('')
|
|
1488
|
+
: '<span style="color:#9ca3af;">-</span>';
|
|
1489
|
+
html += `<tr>
|
|
1490
|
+
<td style="font-size:11px;white-space:nowrap;">${time}</td>
|
|
1491
|
+
<td>${sourceBadge}</td>
|
|
1492
|
+
<td>${actionBadge}</td>
|
|
1493
|
+
<td>${statusBadge}</td>
|
|
1494
|
+
<td>
|
|
1495
|
+
<div style="font-weight:600;font-size:13px;">${escapeHtml(entry.title)}</div>
|
|
1496
|
+
<div style="font-size:12px;color:#6b7280;">${escapeHtml(entry.description)}</div>
|
|
1497
|
+
</td>
|
|
1498
|
+
<td style="max-width:200px;">${impactHtml}</td>
|
|
1499
|
+
</tr>`;
|
|
1500
|
+
}
|
|
1501
|
+
html += `</tbody></table></div>`;
|
|
1502
|
+
return html;
|
|
1503
|
+
}
|
|
1196
1504
|
function escapeHtml(str) {
|
|
1197
1505
|
return str
|
|
1198
1506
|
.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.5",
|
|
4
|
+
"@greenarmor/ges-core": "1.2.5",
|
|
5
|
+
"@greenarmor/ges-policy-engine": "1.2.5",
|
|
6
|
+
"@greenarmor/ges-scoring-engine": "1.2.5"
|
|
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.5",
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsc",
|
|
46
46
|
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|