@greenarmor/ges-web-dashboard 1.2.4 → 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 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 controls = packs.flatMap(p => p.controls);
35
- const overridesPath = path.join(projectPath, ".ges", "control-overrides.json");
36
- if (fs.existsSync(overridesPath)) {
37
- const overrides = JSON.parse(fs.readFileSync(overridesPath, "utf-8"));
38
- for (const override of overrides) {
39
- const control = controls.find((c) => c.id === override.control_id);
40
- if (control) {
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 controlsDir = path.join(projectPath, "controls");
143
- try {
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
- const baseControls = config ? loadControlsForConfig(projectPath, config) : [];
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 freshScore = generateScoreFile(controls, config.frameworks, findings);
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: config?.frameworks || [],
192
- gesfVersion: "1.2.4",
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 ? loadControlsForConfig(projectPath, 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
- if (!config)
285
- return null;
286
- const baseControls = loadControlsForConfig(projectPath, config);
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
@@ -317,8 +317,8 @@ export function renderDashboard(data) {
317
317
 
318
318
  <div class="header">
319
319
  <div>
320
- <h1>GESF Compliance Dashboard</h1>
321
- <div class="subtitle">${escapeHtml(data.projectName)} | ${escapeHtml(data.projectType)} | GESF v${escapeHtml(data.gesfVersion)}</div>
320
+ <h1>${escapeHtml(data.projectName)}</h1>
321
+ <div class="subtitle">GESF v${escapeHtml(data.gesfVersion)}</div>
322
322
  </div>
323
323
  <div class="nav-tabs">
324
324
  <button class="nav-tab active" onclick="showPage('overview', this)">Overview</button>
@@ -326,6 +326,7 @@ export function renderDashboard(data) {
326
326
  <button class="nav-tab" onclick="showPage('fixes', this)">Fixes Detail</button>
327
327
  <button class="nav-tab" onclick="showPage('findings', this)">Findings</button>
328
328
  <button class="nav-tab" onclick="showPage('traceability', this)">Traceability</button>
329
+ <button class="nav-tab" onclick="showPage('activity', this)">Activity Log</button>
329
330
  </div>
330
331
  </div>
331
332
 
@@ -589,12 +590,16 @@ export function renderDashboard(data) {
589
590
  </div>
590
591
  </div>
591
592
 
593
+ <div id="page-activity" class="page">
594
+ ${renderActivityLogSection(data.activityLog || [])}
595
+ </div>
596
+
592
597
  <div id="control-detail-modal" style="display:none;"></div>
593
598
 
594
599
  </div>
595
600
 
596
601
  <div class="footer">
597
- 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>
598
603
  </div>
599
604
 
600
605
  <script>
@@ -615,7 +620,7 @@ export function renderDashboard(data) {
615
620
  }
616
621
  };
617
622
 
618
- 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 };
619
624
 
620
625
  window.navigateToPage = function(page) {
621
626
  var tabs = document.querySelectorAll('.nav-tab');
@@ -1363,6 +1368,139 @@ function renderComplianceFixCards(issues, idPrefix) {
1363
1368
  }
1364
1369
  return html;
1365
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 &mdash; the single source of truth for what GESF did to your project.</p>
1376
+ <div class="empty-state">
1377
+ <div class="icon">&#128203;</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 &mdash; 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
+ }
1366
1504
  function escapeHtml(str) {
1367
1505
  return str
1368
1506
  .replace(/&/g, "&amp;")
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "dependencies": {
3
- "@greenarmor/ges-audit-engine": "1.2.4",
4
- "@greenarmor/ges-core": "1.2.4",
5
- "@greenarmor/ges-policy-engine": "1.2.4",
6
- "@greenarmor/ges-scoring-engine": "1.2.4"
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.4",
43
+ "version": "1.2.5",
44
44
  "scripts": {
45
45
  "build": "tsc",
46
46
  "clean": "rm -rf dist tsconfig.tsbuildinfo",