@greenarmor/ges-web-dashboard 1.1.7 → 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 CHANGED
@@ -1,11 +1,72 @@
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;
6
6
  host?: string;
7
7
  projectPath: string;
8
8
  }
9
+ export interface PackSummary {
10
+ id: string;
11
+ name: string;
12
+ description: string;
13
+ version: string;
14
+ controlCount: number;
15
+ passedCount: number;
16
+ failedCount: number;
17
+ warningCount: number;
18
+ notImplementedCount: number;
19
+ notApplicableCount: number;
20
+ score: number;
21
+ grade: string;
22
+ findingsCount: number;
23
+ installed: boolean;
24
+ }
25
+ export interface ControlDetail {
26
+ id: string;
27
+ name: string;
28
+ description: string;
29
+ category: string;
30
+ framework: string;
31
+ article?: string;
32
+ status: string;
33
+ severity: string;
34
+ implementation_guidance: string;
35
+ checks: {
36
+ id: string;
37
+ description: string;
38
+ status: string;
39
+ evidence?: string;
40
+ }[];
41
+ relatedFindings: Finding[];
42
+ packId: string;
43
+ packName: string;
44
+ }
45
+ export interface PackDetailReport {
46
+ pack: PackSummary;
47
+ controls: ControlDetail[];
48
+ findingsByControl: Record<string, Finding[]>;
49
+ severityBreakdown: {
50
+ critical: number;
51
+ high: number;
52
+ medium: number;
53
+ low: number;
54
+ };
55
+ statusBreakdown: {
56
+ pass: number;
57
+ fail: number;
58
+ warning: number;
59
+ "not-implemented": number;
60
+ "not-applicable": number;
61
+ };
62
+ topFixes: {
63
+ controlId: string;
64
+ controlName: string;
65
+ severity: string;
66
+ findings: Finding[];
67
+ guidance: string;
68
+ }[];
69
+ }
9
70
  export interface DashboardData {
10
71
  projectName: string;
11
72
  projectType: string;
@@ -14,12 +75,11 @@ export interface DashboardData {
14
75
  score: ScoreFile | null;
15
76
  controls: Control[];
16
77
  findings: Finding[];
17
- packs: {
18
- id: string;
19
- name: string;
20
- controlCount: number;
21
- }[];
78
+ packs: PackSummary[];
79
+ fixHistory: FixHistoryEntry[];
22
80
  lastAudit: string;
23
81
  }
24
82
  export declare function collectDashboardData(projectPath: string): DashboardData;
83
+ export declare function collectPackDetail(projectPath: string, packId: string): PackDetailReport | null;
84
+ export declare function collectControlDetail(projectPath: string, controlId: string): ControlDetail | null;
25
85
  export declare function startDashboard(options: DashboardOptions): http.Server;
package/dist/index.js CHANGED
@@ -2,76 +2,136 @@ import * as http from "node:http";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { runAudit, deduplicateFindings } from "@greenarmor/ges-audit-engine";
5
- import { getAllPacks, getPacksForProjectType } from "@greenarmor/ges-policy-engine";
5
+ import { getAllPacks, getPack } from "@greenarmor/ges-policy-engine";
6
6
  import { generateScoreFile } from "@greenarmor/ges-scoring-engine";
7
+ import { loadFixHistory } from "@greenarmor/ges-core";
7
8
  import { renderDashboard } from "./template.js";
8
- export function collectDashboardData(projectPath) {
9
+ function loadConfig(projectPath) {
9
10
  const configPath = path.join(projectPath, ".ges", "config.json");
10
- let config = null;
11
11
  try {
12
12
  const raw = fs.readFileSync(configPath, "utf-8");
13
- config = JSON.parse(raw);
13
+ return JSON.parse(raw);
14
14
  }
15
15
  catch {
16
- config = null;
16
+ return null;
17
17
  }
18
- let score = null;
18
+ }
19
+ function loadScore(projectPath) {
19
20
  try {
20
21
  const scorePath = path.join(projectPath, ".ges", "score.json");
21
22
  const raw = fs.readFileSync(scorePath, "utf-8");
22
- score = JSON.parse(raw);
23
+ return JSON.parse(raw);
23
24
  }
24
25
  catch {
25
- score = null;
26
+ return null;
26
27
  }
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
- }
28
+ }
29
+ function loadControlsForConfig(projectPath, config) {
30
+ try {
31
+ const fwLower = new Set(config.frameworks.map(f => f.toLowerCase()));
32
+ const allPacks = getAllPacks();
33
+ 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;
45
44
  }
46
45
  }
47
46
  }
48
47
  }
49
- catch {
50
- controls = [];
51
- }
48
+ return controls;
49
+ }
50
+ catch {
51
+ return [];
52
52
  }
53
- let findings = [];
53
+ }
54
+ function loadFindings(projectPath) {
54
55
  try {
55
56
  const result = runAudit(projectPath);
56
- findings = deduplicateFindings(result.findings);
57
+ return deduplicateFindings(result.findings);
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }
63
+ function buildPackSummary(pack, controls, findings, installedPacks) {
64
+ const packControlIds = new Set(pack.controls.map(c => c.id));
65
+ const packControls = controls.filter(c => packControlIds.has(c.id));
66
+ const packFindings = findings.filter(f => f.controlIds.some(cid => packControlIds.has(cid)));
67
+ const passedCount = packControls.filter(c => c.status === "pass" || c.status === "not-applicable").length;
68
+ const total = packControls.length || 1;
69
+ const score = Math.round((passedCount / total) * 100);
70
+ const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : score >= 60 ? "D" : "F";
71
+ return {
72
+ id: pack.id,
73
+ name: pack.name,
74
+ description: pack.description,
75
+ version: pack.version,
76
+ controlCount: pack.controls.length,
77
+ passedCount,
78
+ failedCount: packControls.filter(c => c.status === "fail").length,
79
+ warningCount: packControls.filter(c => c.status === "warning").length,
80
+ notImplementedCount: packControls.filter(c => c.status === "not-implemented").length,
81
+ notApplicableCount: packControls.filter(c => c.status === "not-applicable").length,
82
+ score,
83
+ grade,
84
+ findingsCount: packFindings.length,
85
+ installed: installedPacks.has(pack.id),
86
+ };
87
+ }
88
+ function getInstalledPackIds(projectPath, config) {
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");
100
+ try {
101
+ const entries = fs.readdirSync(controlsDir, { withFileTypes: true });
102
+ for (const entry of entries) {
103
+ if (entry.isDirectory()) {
104
+ const ctrlFile = path.join(controlsDir, entry.name, "controls.json");
105
+ if (fs.existsSync(ctrlFile)) {
106
+ ids.add(entry.name);
107
+ }
108
+ }
109
+ }
57
110
  }
58
111
  catch {
59
- findings = [];
112
+ // controls dir may not exist
60
113
  }
61
- if (!score && config) {
114
+ return ids;
115
+ }
116
+ export function collectDashboardData(projectPath) {
117
+ const config = loadConfig(projectPath);
118
+ let score = loadScore(projectPath);
119
+ const controls = config ? loadControlsForConfig(projectPath, config) : [];
120
+ const findings = loadFindings(projectPath);
121
+ if (config) {
62
122
  try {
63
- score = generateScoreFile(controls, config.frameworks, findings);
123
+ const freshScore = generateScoreFile(controls, config.frameworks, findings);
124
+ score = freshScore;
64
125
  }
65
126
  catch {
66
- score = null;
127
+ if (!score)
128
+ score = null;
67
129
  }
68
130
  }
69
131
  const allPacks = getAllPacks();
70
- const packsList = allPacks.map(p => ({
71
- id: p.id,
72
- name: p.name,
73
- controlCount: p.controls.length,
74
- }));
132
+ const installedPacks = getInstalledPackIds(projectPath, config || undefined);
133
+ const packs = allPacks.map(p => buildPackSummary(p, controls, findings, installedPacks));
134
+ const fixHistory = loadFixHistory(projectPath);
75
135
  const metadataPath = path.join(projectPath, ".ges", "metadata.json");
76
136
  let lastAudit = "";
77
137
  try {
@@ -85,16 +145,138 @@ export function collectDashboardData(projectPath) {
85
145
  projectName: config?.project_name || "Unknown Project",
86
146
  projectType: config?.project_type || "unknown",
87
147
  frameworks: config?.frameworks || [],
88
- gesfVersion: "1.1.1",
148
+ gesfVersion: "1.2.1",
89
149
  score,
90
150
  controls,
91
151
  findings,
92
- packs: packsList,
152
+ packs,
153
+ fixHistory,
93
154
  lastAudit,
94
155
  };
95
156
  }
157
+ export function collectPackDetail(projectPath, packId) {
158
+ const pack = getPack(packId);
159
+ if (!pack)
160
+ return null;
161
+ const config = loadConfig(projectPath);
162
+ const controls = config ? loadControlsForConfig(projectPath, config) : [];
163
+ const findings = loadFindings(projectPath);
164
+ const packControlIds = new Set(pack.controls.map(c => c.id));
165
+ const packControls = pack.controls;
166
+ const installedPacks = getInstalledPackIds(projectPath, config || undefined);
167
+ const packSummary = buildPackSummary(pack, controls, findings, installedPacks);
168
+ const findingsByControlId = {};
169
+ for (const finding of findings) {
170
+ for (const cid of finding.controlIds) {
171
+ if (packControlIds.has(cid)) {
172
+ if (!findingsByControlId[cid])
173
+ findingsByControlId[cid] = [];
174
+ findingsByControlId[cid].push(finding);
175
+ }
176
+ }
177
+ }
178
+ const controlDetails = packControls.map(ctrl => {
179
+ const activeCtrl = controls.find(c => c.id === ctrl.id) || ctrl;
180
+ return {
181
+ id: activeCtrl.id,
182
+ name: activeCtrl.name,
183
+ description: activeCtrl.description,
184
+ category: activeCtrl.category,
185
+ framework: activeCtrl.framework,
186
+ article: activeCtrl.article,
187
+ status: activeCtrl.status,
188
+ severity: activeCtrl.severity,
189
+ implementation_guidance: activeCtrl.implementation_guidance,
190
+ checks: activeCtrl.checks.map(ch => ({
191
+ id: ch.id,
192
+ description: ch.description,
193
+ status: ch.status,
194
+ evidence: ch.evidence,
195
+ })),
196
+ relatedFindings: findingsByControlId[activeCtrl.id] || [],
197
+ packId: pack.id,
198
+ packName: pack.name,
199
+ };
200
+ });
201
+ const packFindings = findings.filter(f => f.controlIds.some(cid => packControlIds.has(cid)));
202
+ const severityBreakdown = {
203
+ critical: packFindings.filter(f => f.severity === "critical").length,
204
+ high: packFindings.filter(f => f.severity === "high").length,
205
+ medium: packFindings.filter(f => f.severity === "medium").length,
206
+ low: packFindings.filter(f => f.severity === "low").length,
207
+ };
208
+ const statusBreakdown = {
209
+ pass: controlDetails.filter(c => c.status === "pass").length,
210
+ fail: controlDetails.filter(c => c.status === "fail").length,
211
+ warning: controlDetails.filter(c => c.status === "warning").length,
212
+ "not-implemented": controlDetails.filter(c => c.status === "not-implemented").length,
213
+ "not-applicable": controlDetails.filter(c => c.status === "not-applicable").length,
214
+ };
215
+ const nonPassControls = controlDetails.filter(c => c.status !== "pass" && c.status !== "not-applicable");
216
+ const topFixes = nonPassControls
217
+ .sort((a, b) => {
218
+ const sevOrder = { critical: 0, high: 1, medium: 2, low: 3 };
219
+ return (sevOrder[a.severity] ?? 4) - (sevOrder[b.severity] ?? 4);
220
+ })
221
+ .map(ctrl => ({
222
+ controlId: ctrl.id,
223
+ controlName: ctrl.name,
224
+ severity: ctrl.severity,
225
+ findings: ctrl.relatedFindings,
226
+ guidance: ctrl.implementation_guidance,
227
+ }));
228
+ return {
229
+ pack: packSummary,
230
+ controls: controlDetails,
231
+ findingsByControl: findingsByControlId,
232
+ severityBreakdown,
233
+ statusBreakdown,
234
+ topFixes,
235
+ };
236
+ }
237
+ export function collectControlDetail(projectPath, controlId) {
238
+ const config = loadConfig(projectPath);
239
+ if (!config)
240
+ return null;
241
+ const controls = loadControlsForConfig(projectPath, config);
242
+ const findings = loadFindings(projectPath);
243
+ const control = controls.find(c => c.id === controlId);
244
+ if (!control)
245
+ return null;
246
+ const relatedFindings = findings.filter(f => f.controlIds.includes(controlId));
247
+ const allPacks = getAllPacks();
248
+ const matchingPack = allPacks.find(p => p.controls.some(c => c.id === controlId));
249
+ return {
250
+ id: control.id,
251
+ name: control.name,
252
+ description: control.description,
253
+ category: control.category,
254
+ framework: control.framework,
255
+ article: control.article,
256
+ status: control.status,
257
+ severity: control.severity,
258
+ implementation_guidance: control.implementation_guidance,
259
+ checks: control.checks.map(ch => ({
260
+ id: ch.id,
261
+ description: ch.description,
262
+ status: ch.status,
263
+ evidence: ch.evidence,
264
+ })),
265
+ relatedFindings,
266
+ packId: matchingPack?.id || "",
267
+ packName: matchingPack?.name || "",
268
+ };
269
+ }
270
+ function jsonResponse(res, data, status = 200) {
271
+ res.writeHead(status, { "Content-Type": "application/json" });
272
+ res.end(JSON.stringify(data));
273
+ }
274
+ function jsonError(res, message, status = 500) {
275
+ res.writeHead(status, { "Content-Type": "application/json" });
276
+ res.end(JSON.stringify({ error: message }));
277
+ }
96
278
  export function startDashboard(options) {
97
- const port = options.port || 3001;
279
+ const port = options.port ?? 3001;
98
280
  const host = options.host || "localhost";
99
281
  const proto = ["http", "//"].join(":");
100
282
  const server = http.createServer((req, res) => {
@@ -104,7 +286,12 @@ export function startDashboard(options) {
104
286
  return;
105
287
  }
106
288
  const url = new URL(req.url, `${proto}${host}:${port}`);
107
- if (url.pathname === "/" || url.pathname === "/index.html") {
289
+ const pathname = url.pathname;
290
+ if (req.method !== "GET") {
291
+ jsonError(res, "Method not allowed", 405);
292
+ return;
293
+ }
294
+ if (pathname === "/" || pathname === "/index.html") {
108
295
  try {
109
296
  const data = collectDashboardData(options.projectPath);
110
297
  const html = renderDashboard(data);
@@ -117,21 +304,98 @@ export function startDashboard(options) {
117
304
  }
118
305
  return;
119
306
  }
120
- if (url.pathname === "/api/data") {
307
+ if (pathname === "/api/data") {
308
+ try {
309
+ const data = collectDashboardData(options.projectPath);
310
+ jsonResponse(res, data);
311
+ }
312
+ catch (err) {
313
+ jsonError(res, err instanceof Error ? err.message : String(err));
314
+ }
315
+ return;
316
+ }
317
+ if (pathname === "/api/packs") {
121
318
  try {
122
319
  const data = collectDashboardData(options.projectPath);
123
- res.writeHead(200, { "Content-Type": "application/json" });
124
- res.end(JSON.stringify(data));
320
+ jsonResponse(res, data.packs);
321
+ }
322
+ catch (err) {
323
+ jsonError(res, err instanceof Error ? err.message : String(err));
324
+ }
325
+ return;
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
+ }
337
+ const packMatch = pathname.match(/^\/api\/packs\/([a-z0-9-]+)$/);
338
+ if (packMatch) {
339
+ try {
340
+ const detail = collectPackDetail(options.projectPath, packMatch[1]);
341
+ if (!detail) {
342
+ jsonError(res, `Pack not found: ${packMatch[1]}`, 404);
343
+ return;
344
+ }
345
+ jsonResponse(res, detail);
346
+ }
347
+ catch (err) {
348
+ jsonError(res, err instanceof Error ? err.message : String(err));
349
+ }
350
+ return;
351
+ }
352
+ const packControlsMatch = pathname.match(/^\/api\/packs\/([a-z0-9-]+)\/controls$/);
353
+ if (packControlsMatch) {
354
+ try {
355
+ const detail = collectPackDetail(options.projectPath, packControlsMatch[1]);
356
+ if (!detail) {
357
+ jsonError(res, `Pack not found: ${packControlsMatch[1]}`, 404);
358
+ return;
359
+ }
360
+ jsonResponse(res, detail.controls);
361
+ }
362
+ catch (err) {
363
+ jsonError(res, err instanceof Error ? err.message : String(err));
364
+ }
365
+ return;
366
+ }
367
+ const controlMatch = pathname.match(/^\/api\/controls\/([A-Z0-9-]+)$/);
368
+ if (controlMatch) {
369
+ try {
370
+ const detail = collectControlDetail(options.projectPath, controlMatch[1]);
371
+ if (!detail) {
372
+ jsonError(res, `Control not found: ${controlMatch[1]}`, 404);
373
+ return;
374
+ }
375
+ jsonResponse(res, detail);
376
+ }
377
+ catch (err) {
378
+ jsonError(res, err instanceof Error ? err.message : String(err));
379
+ }
380
+ return;
381
+ }
382
+ const findingsByControlMatch = pathname.match(/^\/api\/findings\/by-control\/([A-Z0-9-]+)$/);
383
+ if (findingsByControlMatch) {
384
+ try {
385
+ const detail = collectControlDetail(options.projectPath, findingsByControlMatch[1]);
386
+ if (!detail) {
387
+ jsonError(res, `Control not found: ${findingsByControlMatch[1]}`, 404);
388
+ return;
389
+ }
390
+ jsonResponse(res, detail.relatedFindings);
125
391
  }
126
392
  catch (err) {
127
- res.writeHead(500, { "Content-Type": "application/json" });
128
- res.end(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
393
+ jsonError(res, err instanceof Error ? err.message : String(err));
129
394
  }
130
395
  return;
131
396
  }
132
- if (url.pathname === "/health") {
133
- res.writeHead(200, { "Content-Type": "application/json" });
134
- res.end(JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }));
397
+ if (pathname === "/health") {
398
+ jsonResponse(res, { status: "ok", timestamp: new Date().toISOString() });
135
399
  return;
136
400
  }
137
401
  res.writeHead(404);