@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 +64 -5
- package/dist/index.js +290 -49
- package/dist/template.js +883 -135
- package/package.json +22 -22
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
|
-
|
|
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
|
-
|
|
12
|
+
return JSON.parse(raw);
|
|
14
13
|
}
|
|
15
14
|
catch {
|
|
16
|
-
|
|
15
|
+
return null;
|
|
17
16
|
}
|
|
18
|
-
|
|
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
|
-
|
|
22
|
+
return JSON.parse(raw);
|
|
23
23
|
}
|
|
24
24
|
catch {
|
|
25
|
-
|
|
25
|
+
return null;
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
50
|
-
controls = [];
|
|
51
|
-
}
|
|
48
|
+
return controls;
|
|
52
49
|
}
|
|
53
|
-
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function loadFindings(projectPath) {
|
|
54
55
|
try {
|
|
55
56
|
const result = runAudit(projectPath);
|
|
56
|
-
|
|
57
|
+
return deduplicateFindings(result.findings);
|
|
57
58
|
}
|
|
58
59
|
catch {
|
|
59
|
-
|
|
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
|
|
71
|
-
|
|
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.
|
|
136
|
+
gesfVersion: "1.2.0",
|
|
89
137
|
score,
|
|
90
138
|
controls,
|
|
91
139
|
findings,
|
|
92
|
-
packs
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
124
|
-
|
|
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
|
|
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 (
|
|
133
|
-
res
|
|
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);
|