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