@greenarmor/ges-web-dashboard 1.1.0

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.
@@ -0,0 +1,25 @@
1
+ import * as http from "node:http";
2
+ import type { ScoreFile, Control } from "@greenarmor/ges-core";
3
+ import type { Finding } from "@greenarmor/ges-audit-engine";
4
+ export interface DashboardOptions {
5
+ port?: number;
6
+ host?: string;
7
+ projectPath: string;
8
+ }
9
+ export interface DashboardData {
10
+ projectName: string;
11
+ projectType: string;
12
+ frameworks: string[];
13
+ gesfVersion: string;
14
+ score: ScoreFile | null;
15
+ controls: Control[];
16
+ findings: Finding[];
17
+ packs: {
18
+ id: string;
19
+ name: string;
20
+ controlCount: number;
21
+ }[];
22
+ lastAudit: string;
23
+ }
24
+ export declare function collectDashboardData(projectPath: string): DashboardData;
25
+ export declare function startDashboard(options: DashboardOptions): http.Server;
package/dist/index.js ADDED
@@ -0,0 +1,141 @@
1
+ import * as http from "node:http";
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { runAudit, deduplicateFindings } from "@greenarmor/ges-audit-engine";
5
+ import { getAllPacks, getPacksForProjectType } from "@greenarmor/ges-policy-engine";
6
+ import { generateScoreFile } from "@greenarmor/ges-scoring-engine";
7
+ import { renderDashboard } from "./template.js";
8
+ export function collectDashboardData(projectPath) {
9
+ const configPath = path.join(projectPath, ".ges", "config.json");
10
+ let config = null;
11
+ try {
12
+ const raw = fs.readFileSync(configPath, "utf-8");
13
+ config = JSON.parse(raw);
14
+ }
15
+ catch {
16
+ config = null;
17
+ }
18
+ let score = null;
19
+ try {
20
+ const scorePath = path.join(projectPath, ".ges", "score.json");
21
+ const raw = fs.readFileSync(scorePath, "utf-8");
22
+ score = JSON.parse(raw);
23
+ }
24
+ catch {
25
+ score = null;
26
+ }
27
+ let controls = [];
28
+ if (config) {
29
+ try {
30
+ const packs = getPacksForProjectType(config.project_type);
31
+ const fwLower = new Set(config.frameworks.map(f => f.toLowerCase()));
32
+ const DOMAIN_PACKS = new Set(["ai", "blockchain", "government"]);
33
+ const filtered = packs.filter(pack => DOMAIN_PACKS.has(pack.id.toLowerCase()) || fwLower.has(pack.id.toLowerCase()));
34
+ controls = filtered.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
+ }
49
+ catch {
50
+ controls = [];
51
+ }
52
+ }
53
+ let findings = [];
54
+ try {
55
+ const result = runAudit(projectPath);
56
+ findings = deduplicateFindings(result.findings);
57
+ }
58
+ catch {
59
+ findings = [];
60
+ }
61
+ if (!score && config) {
62
+ try {
63
+ score = generateScoreFile(controls, config.frameworks, findings);
64
+ }
65
+ catch {
66
+ score = null;
67
+ }
68
+ }
69
+ const allPacks = getAllPacks();
70
+ const packsList = allPacks.map(p => ({
71
+ id: p.id,
72
+ name: p.name,
73
+ controlCount: p.controls.length,
74
+ }));
75
+ const metadataPath = path.join(projectPath, ".ges", "metadata.json");
76
+ let lastAudit = "";
77
+ try {
78
+ const meta = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
79
+ lastAudit = meta.last_audit || meta.initialized_at || new Date().toISOString();
80
+ }
81
+ catch {
82
+ lastAudit = new Date().toISOString();
83
+ }
84
+ return {
85
+ projectName: config?.project_name || "Unknown Project",
86
+ projectType: config?.project_type || "unknown",
87
+ frameworks: config?.frameworks || [],
88
+ gesfVersion: "1.1.0",
89
+ score,
90
+ controls,
91
+ findings,
92
+ packs: packsList,
93
+ lastAudit,
94
+ };
95
+ }
96
+ export function startDashboard(options) {
97
+ const port = options.port || 3001;
98
+ const host = options.host || "localhost";
99
+ const server = http.createServer((req, res) => {
100
+ if (!req.url) {
101
+ res.writeHead(400);
102
+ res.end("Bad request");
103
+ return;
104
+ }
105
+ const url = new URL(req.url, `http://${host}:${port}`);
106
+ if (url.pathname === "/" || url.pathname === "/index.html") {
107
+ try {
108
+ const data = collectDashboardData(options.projectPath);
109
+ const html = renderDashboard(data);
110
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
111
+ res.end(html);
112
+ }
113
+ catch (err) {
114
+ res.writeHead(500, { "Content-Type": "text/plain" });
115
+ res.end(`Dashboard error: ${err instanceof Error ? err.message : String(err)}`);
116
+ }
117
+ return;
118
+ }
119
+ if (url.pathname === "/api/data") {
120
+ try {
121
+ const data = collectDashboardData(options.projectPath);
122
+ res.writeHead(200, { "Content-Type": "application/json" });
123
+ res.end(JSON.stringify(data));
124
+ }
125
+ catch (err) {
126
+ res.writeHead(500, { "Content-Type": "application/json" });
127
+ res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
128
+ }
129
+ return;
130
+ }
131
+ if (url.pathname === "/health") {
132
+ res.writeHead(200, { "Content-Type": "application/json" });
133
+ res.end(JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }));
134
+ return;
135
+ }
136
+ res.writeHead(404);
137
+ res.end("Not found");
138
+ });
139
+ server.listen(port, host);
140
+ return server;
141
+ }
@@ -0,0 +1,2 @@
1
+ import type { DashboardData } from "./index.js";
2
+ export declare function renderDashboard(data: DashboardData): string;
@@ -0,0 +1,272 @@
1
+ function gradeColor(grade) {
2
+ switch (grade) {
3
+ case "A": return "#22c55e";
4
+ case "B": return "#84cc16";
5
+ case "C": return "#eab308";
6
+ case "D": return "#f97316";
7
+ case "F": return "#ef4444";
8
+ default: return "#6b7280";
9
+ }
10
+ }
11
+ function severityBadge(severity) {
12
+ const colors = {
13
+ critical: "#ef4444",
14
+ high: "#f97316",
15
+ medium: "#eab308",
16
+ low: "#3b82f6",
17
+ };
18
+ const color = colors[severity] || "#6b7280";
19
+ return `<span style="background:${color};color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;">${severity.toUpperCase()}</span>`;
20
+ }
21
+ function statusBadge(status) {
22
+ const colors = {
23
+ pass: "#22c55e",
24
+ fail: "#ef4444",
25
+ warning: "#eab308",
26
+ "not-implemented": "#6b7280",
27
+ "not-applicable": "#9ca3af",
28
+ };
29
+ const labels = {
30
+ pass: "PASS",
31
+ fail: "FAIL",
32
+ warning: "WARN",
33
+ "not-implemented": "NOT IMPL",
34
+ "not-applicable": "N/A",
35
+ };
36
+ const color = colors[status] || "#6b7280";
37
+ const label = labels[status] || status.toUpperCase();
38
+ return `<span style="background:${color};color:white;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:600;">${label}</span>`;
39
+ }
40
+ function scoreBar(score) {
41
+ const color = score >= 80 ? "#22c55e" : score >= 60 ? "#eab308" : score >= 40 ? "#f97316" : "#ef4444";
42
+ return `<div style="width:100%;background:#e5e7eb;border-radius:4px;height:8px;margin-top:4px;">
43
+ <div style="width:${score}%;background:${color};height:8px;border-radius:4px;"></div>
44
+ </div>`;
45
+ }
46
+ function donutSvg(passed, total) {
47
+ const r = 54;
48
+ const cx = 70;
49
+ const cy = 70;
50
+ const circumference = 2 * Math.PI * r;
51
+ const pct = total > 0 ? passed / total : 0;
52
+ const offset = circumference * (1 - pct);
53
+ const color = pct >= 0.8 ? "#22c55e" : pct >= 0.6 ? "#eab308" : pct >= 0.4 ? "#f97316" : "#ef4444";
54
+ return `<svg width="140" height="140" viewBox="0 0 140 140">
55
+ <circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#e5e7eb" stroke-width="12"/>
56
+ <circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="${color}" stroke-width="12"
57
+ stroke-dasharray="${circumference}" stroke-dashoffset="${offset}"
58
+ stroke-linecap="round" transform="rotate(-90 ${cx} ${cy})"/>
59
+ <text x="${cx}" y="${cy - 4}" text-anchor="middle" font-size="28" font-weight="700" fill="#1f2937">${Math.round(pct * 100)}%</text>
60
+ <text x="${cx}" y="${cy + 16}" text-anchor="middle" font-size="11" fill="#6b7280">${passed}/${total} passed</text>
61
+ </svg>`;
62
+ }
63
+ export function renderDashboard(data) {
64
+ const score = data.score;
65
+ const overall = score?.overall ?? 0;
66
+ const overallGrade = score?.overall_grade ?? "F";
67
+ const frameworks = score?.frameworks || {};
68
+ const findings = data.findings;
69
+ const controls = data.controls;
70
+ const findingsBySeverity = {
71
+ critical: findings.filter(f => f.severity === "critical").length,
72
+ high: findings.filter(f => f.severity === "high").length,
73
+ medium: findings.filter(f => f.severity === "medium").length,
74
+ low: findings.filter(f => f.severity === "low").length,
75
+ };
76
+ const controlsByStatus = {
77
+ pass: controls.filter(c => c.status === "pass").length,
78
+ fail: controls.filter(c => c.status === "fail").length,
79
+ warning: controls.filter(c => c.status === "warning").length,
80
+ "not-implemented": controls.filter(c => c.status === "not-implemented").length,
81
+ "not-applicable": controls.filter(c => c.status === "not-applicable").length,
82
+ };
83
+ const frameworkKeys = Object.keys(frameworks);
84
+ const missingControls = controls.filter(c => c.status !== "pass" && c.status !== "not-applicable");
85
+ return `<!DOCTYPE html>
86
+ <html lang="en">
87
+ <head>
88
+ <meta charset="UTF-8">
89
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
90
+ <title>GESF Dashboard - ${escapeHtml(data.projectName)}</title>
91
+ <style>
92
+ * { margin: 0; padding: 0; box-sizing: border-box; }
93
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif; background: #f3f4f6; color: #1f2937; line-height: 1.6; }
94
+ .header { background: linear-gradient(135deg, #0f766e 0%, #14b8a6 100%); color: white; padding: 24px 32px; }
95
+ .header h1 { font-size: 24px; font-weight: 700; }
96
+ .header .subtitle { font-size: 14px; opacity: 0.9; margin-top: 4px; }
97
+ .container { max-width: 1200px; margin: 0 auto; padding: 24px; }
98
+ .grid { display: grid; gap: 20px; }
99
+ .grid-2 { grid-template-columns: 1fr 1fr; }
100
+ .grid-3 { grid-template-columns: repeat(3, 1fr); }
101
+ .grid-4 { grid-template-columns: repeat(4, 1fr); }
102
+ @media (max-width: 768px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }
103
+ .card { background: white; border-radius: 12px; padding: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
104
+ .card-title { font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: #6b7280; margin-bottom: 12px; }
105
+ .big-number { font-size: 42px; font-weight: 700; }
106
+ .flex-center { display: flex; align-items: center; justify-content: center; }
107
+ .stat { text-align: center; }
108
+ .stat .num { font-size: 32px; font-weight: 700; }
109
+ .stat .label { font-size: 12px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; }
110
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
111
+ th { text-align: left; padding: 10px 8px; border-bottom: 2px solid #e5e7eb; font-weight: 600; color: #6b7280; font-size: 11px; text-transform: uppercase; }
112
+ td { padding: 10px 8px; border-bottom: 1px solid #f3f4f6; }
113
+ tr:hover td { background: #f9fafb; }
114
+ .badge-row { display: flex; gap: 6px; flex-wrap: wrap; }
115
+ .framework-row { display: flex; align-items: center; justify-content: space-between; padding: 12px 0; border-bottom: 1px solid #f3f4f6; }
116
+ .framework-row:last-child { border-bottom: none; }
117
+ .framework-name { font-size: 14px; font-weight: 600; min-width: 140px; }
118
+ .footer { text-align: center; padding: 24px; color: #9ca3af; font-size: 12px; }
119
+ a { color: #0f766e; text-decoration: none; }
120
+ a:hover { text-decoration: underline; }
121
+ .pct-text { font-size: 13px; font-weight: 600; min-width: 50px; text-align: right; }
122
+ .bar-container { flex: 1; margin: 0 12px; }
123
+ .tag { display: inline-block; background: #e0f2fe; color: #0369a1; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; margin: 2px; }
124
+ </style>
125
+ </head>
126
+ <body>
127
+
128
+ <div class="header">
129
+ <h1>GESF Compliance Dashboard</h1>
130
+ <div class="subtitle">${escapeHtml(data.projectName)} | ${escapeHtml(data.projectType)} | GESF v${escapeHtml(data.gesfVersion)}</div>
131
+ </div>
132
+
133
+ <div class="container">
134
+ <div class="grid">
135
+
136
+ <div class="grid grid-3">
137
+ <div class="card stat">
138
+ ${donutSvg(score ? Object.values(frameworks).reduce((n, f) => n + f.passed_controls, 0) : 0, controls.length || 1)}
139
+ <div class="label">Overall Compliance</div>
140
+ </div>
141
+ <div class="card">
142
+ <div class="card-title">Overall Score</div>
143
+ <div class="big-number" style="color:${gradeColor(overallGrade)};">${overall}%</div>
144
+ <div style="margin-top:4px;"><span style="background:${gradeColor(overallGrade)};color:white;padding:4px 16px;border-radius:6px;font-weight:700;">Grade: ${overallGrade}</span></div>
145
+ ${scoreBar(overall)}
146
+ </div>
147
+ <div class="card">
148
+ <div class="card-title">Security Findings</div>
149
+ <div class="big-number" style="color:${findings.length > 0 ? '#ef4444' : '#22c55e'};">${findings.length}</div>
150
+ <div style="margin-top:8px;font-size:13px;color:#6b7280;">
151
+ <span style="color:#ef4444;font-weight:600;">${findingsBySeverity.critical} critical</span> |
152
+ <span style="color:#f97316;font-weight:600;">${findingsBySeverity.high} high</span> |
153
+ <span style="color:#eab308;font-weight:600;">${findingsBySeverity.medium} medium</span> |
154
+ <span style="color:#3b82f6;font-weight:600;">${findingsBySeverity.low} low</span>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
159
+ <div class="grid grid-2">
160
+
161
+ <div class="card">
162
+ <div class="card-title">Framework Scores</div>
163
+ ${frameworkKeys.length > 0 ? frameworkKeys.map(fw => {
164
+ const f = frameworks[fw];
165
+ const pct = f.score;
166
+ const color = pct >= 80 ? '#22c55e' : pct >= 60 ? '#eab308' : pct >= 40 ? '#f97316' : '#ef4444';
167
+ return `<div class="framework-row">
168
+ <div class="framework-name">${escapeHtml(fw)}</div>
169
+ <div class="bar-container">${scoreBar(pct)}</div>
170
+ <div class="pct-text" style="color:${color};">${pct}% (${f.grade})</div>
171
+ </div>`;
172
+ }).join('') : '<div style="padding:20px;text-align:center;color:#9ca3af;">No framework data. Run "ges score".</div>'}
173
+ </div>
174
+
175
+ <div class="card">
176
+ <div class="card-title">Control Status Breakdown</div>
177
+ <div class="grid grid-3" style="gap:12px;">
178
+ <div class="stat">
179
+ <div class="num" style="color:#22c55e;">${controlsByStatus.pass}</div>
180
+ <div class="label">Pass</div>
181
+ </div>
182
+ <div class="stat">
183
+ <div class="num" style="color:#ef4444;">${controlsByStatus.fail}</div>
184
+ <div class="label">Fail</div>
185
+ </div>
186
+ <div class="stat">
187
+ <div class="num" style="color:#eab308;">${controlsByStatus.warning}</div>
188
+ <div class="label">Warning</div>
189
+ </div>
190
+ <div class="stat">
191
+ <div class="num" style="color:#6b7280;">${controlsByStatus["not-implemented"]}</div>
192
+ <div class="label">Not Impl</div>
193
+ </div>
194
+ <div class="stat">
195
+ <div class="num" style="color:#9ca3af;">${controlsByStatus["not-applicable"]}</div>
196
+ <div class="label">N/A</div>
197
+ </div>
198
+ <div class="stat">
199
+ <div class="num">${controls.length}</div>
200
+ <div class="label">Total</div>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="grid grid-2">
207
+
208
+ <div class="card">
209
+ <div class="card-title">Security Findings Detail (${findings.length})</div>
210
+ ${findings.length > 0 ? `<table>
211
+ <thead><tr><th>Severity</th><th>Rule</th><th>File</th><th>Issue</th></tr></thead>
212
+ <tbody>
213
+ ${findings.slice(0, 15).map(f => `<tr>
214
+ <td>${severityBadge(f.severity)}</td>
215
+ <td style="font-family:monospace;font-size:11px;">${escapeHtml(f.ruleId)}</td>
216
+ <td style="font-family:monospace;font-size:11px;">${escapeHtml(f.file)}${f.line ? ':' + f.line : ''}</td>
217
+ <td>${escapeHtml(f.title)}</td>
218
+ </tr>`).join('')}
219
+ </tbody>
220
+ </table>
221
+ ${findings.length > 15 ? `<div style="text-align:center;padding:8px;color:#9ca3af;font-size:12px;">... and ${findings.length - 15} more findings</div>` : ''}` : '<div style="padding:24px;text-align:center;color:#22c55e;font-weight:600;">No security findings. Project is clean.</div>'}
222
+ </div>
223
+
224
+ <div class="card">
225
+ <div class="card-title">Missing Controls (${missingControls.length})</div>
226
+ ${missingControls.length > 0 ? `<table>
227
+ <thead><tr><th>ID</th><th>Name</th><th>Severity</th><th>Status</th></tr></thead>
228
+ <tbody>
229
+ ${missingControls.slice(0, 15).map(c => `<tr>
230
+ <td style="font-family:monospace;font-size:11px;">${escapeHtml(c.id)}</td>
231
+ <td>${escapeHtml(c.name)}</td>
232
+ <td>${severityBadge(c.severity)}</td>
233
+ <td>${statusBadge(c.status)}</td>
234
+ </tr>`).join('')}
235
+ </tbody>
236
+ </table>
237
+ ${missingControls.length > 15 ? `<div style="text-align:center;padding:8px;color:#9ca3af;font-size:12px;">... and ${missingControls.length - 15} more</div>` : ''}` : '<div style="padding:24px;text-align:center;color:#22c55e;font-weight:600;">All controls are passing.</div>'}
238
+ </div>
239
+ </div>
240
+
241
+ <div class="card">
242
+ <div class="card-title">Installed Policy Packs (${data.packs.length})</div>
243
+ <div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;">
244
+ ${data.packs.map(p => `<span class="tag">${escapeHtml(p.id)} (${p.controlCount} controls)</span>`).join('')}
245
+ </div>
246
+ </div>
247
+
248
+ <div class="card">
249
+ <div class="card-title">Active Frameworks</div>
250
+ <div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;">
251
+ ${data.frameworks.map(fw => `<span class="tag" style="background:#d1fae5;color:#065f46;">${escapeHtml(fw)}</span>`).join('') || '<span style="color:#9ca3af;">No frameworks configured</span>'}
252
+ </div>
253
+ </div>
254
+
255
+ </div>
256
+
257
+ <div class="footer">
258
+ Generated by GESF v${escapeHtml(data.gesfVersion)} | Last audit: ${escapeHtml(new Date(data.lastAudit).toLocaleString())} | <a href="/api/data">JSON API</a>
259
+ </div>
260
+ </div>
261
+
262
+ </body>
263
+ </html>`;
264
+ }
265
+ function escapeHtml(str) {
266
+ return str
267
+ .replace(/&/g, "&amp;")
268
+ .replace(/</g, "&lt;")
269
+ .replace(/>/g, "&gt;")
270
+ .replace(/"/g, "&quot;")
271
+ .replace(/'/g, "&#039;");
272
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@greenarmor/ges-web-dashboard",
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "description": "GESF Web Dashboard - Visual compliance dashboard for teams",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "clean": "rm -rf dist tsconfig.tsbuildinfo",
17
+ "test": "vitest run"
18
+ },
19
+ "dependencies": {
20
+ "@greenarmor/ges-core": "workspace:*",
21
+ "@greenarmor/ges-audit-engine": "workspace:*",
22
+ "@greenarmor/ges-policy-engine": "workspace:*",
23
+ "@greenarmor/ges-scoring-engine": "workspace:*"
24
+ },
25
+ "devDependencies": {
26
+ "typescript": "^6.0.0",
27
+ "@types/node": "^22.0.0",
28
+ "vitest": "^4.1.8"
29
+ },
30
+ "keywords": [
31
+ "gdpr",
32
+ "compliance",
33
+ "security",
34
+ "dashboard",
35
+ "visualization",
36
+ "gesf",
37
+ "green-armor"
38
+ ],
39
+ "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "https://github.com/greenarmor/gesf"
43
+ },
44
+ "homepage": "https://github.com/greenarmor/gesf"
45
+ }