@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 +66 -6
- package/dist/index.js +316 -52
- package/dist/template.js +1066 -135
- package/package.json +22 -22
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
return JSON.parse(raw);
|
|
14
14
|
}
|
|
15
15
|
catch {
|
|
16
|
-
|
|
16
|
+
return null;
|
|
17
17
|
}
|
|
18
|
-
|
|
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
|
-
|
|
23
|
+
return JSON.parse(raw);
|
|
23
24
|
}
|
|
24
25
|
catch {
|
|
25
|
-
|
|
26
|
+
return null;
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
return controls;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
}
|
|
54
|
+
function loadFindings(projectPath) {
|
|
54
55
|
try {
|
|
55
56
|
const result = runAudit(projectPath);
|
|
56
|
-
|
|
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
|
-
|
|
112
|
+
// controls dir may not exist
|
|
60
113
|
}
|
|
61
|
-
|
|
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
|
-
|
|
123
|
+
const freshScore = generateScoreFile(controls, config.frameworks, findings);
|
|
124
|
+
score = freshScore;
|
|
64
125
|
}
|
|
65
126
|
catch {
|
|
66
|
-
score
|
|
127
|
+
if (!score)
|
|
128
|
+
score = null;
|
|
67
129
|
}
|
|
68
130
|
}
|
|
69
131
|
const allPacks = getAllPacks();
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
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.
|
|
148
|
+
gesfVersion: "1.2.1",
|
|
89
149
|
score,
|
|
90
150
|
controls,
|
|
91
151
|
findings,
|
|
92
|
-
packs
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
124
|
-
|
|
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
|
|
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 (
|
|
133
|
-
res
|
|
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);
|