lula2 0.7.5-nightly.1 → 0.8.4
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/README.md +2 -2
- package/dist/_app/immutable/chunks/BHkKokgA.js +1 -0
- package/dist/_app/immutable/chunks/{B3K8aJnm.js → BNu54jRO.js} +1 -1
- package/dist/_app/immutable/chunks/{DveOLWfB.js → BzIr_GrC.js} +42 -42
- package/dist/_app/immutable/chunks/CC6oS456.js +1 -0
- package/dist/_app/immutable/chunks/DHuA7MQr.js +1 -0
- package/dist/_app/immutable/chunks/{BOr7AGz1.js → DJTXGU6C.js} +1 -1
- package/dist/_app/immutable/chunks/{C04NlUEE.js → DSxRA67V.js} +1 -1
- package/dist/_app/immutable/chunks/{BTEcqCut.js → DpCtGpHu.js} +1 -1
- package/dist/_app/immutable/chunks/{CYCiyXhX.js → DznG4VMX.js} +2 -2
- package/dist/_app/immutable/chunks/{Dqqytqkj.js → Ew6_cz_0.js} +1 -1
- package/dist/_app/immutable/chunks/{2BRL7VKY.js → kRA7ZCNG.js} +1 -1
- package/dist/_app/immutable/entry/{app.DQYVFQmH.js → app.CpHUD0XU.js} +2 -2
- package/dist/_app/immutable/entry/start.BavDkynd.js +1 -0
- package/dist/_app/immutable/nodes/{0.VqJIWUds.js → 0.D5TULpJI.js} +1 -1
- package/dist/_app/immutable/nodes/1.BBgWG9H0.js +1 -0
- package/dist/_app/immutable/nodes/{2.CwWiKEBj.js → 2.c2WlghKX.js} +1 -1
- package/dist/_app/immutable/nodes/{3.UIV2UOH1.js → 3.hNTFAKFs.js} +1 -1
- package/dist/_app/immutable/nodes/{4.BdgTjqjd.js → 4.C7MOPYAO.js} +1 -1
- package/dist/_app/version.json +1 -1
- package/dist/cli/commands/ui.js +98 -8
- package/dist/cli/server/index.js +98 -8
- package/dist/cli/server/server.js +98 -8
- package/dist/cli/server/serverState.js +40 -6
- package/dist/cli/server/websocketServer.js +1146 -1056
- package/dist/index.html +11 -11
- package/dist/index.js +98 -8
- package/package.json +128 -127
- package/src/lib/components/controls/tabs/MappingsTab.svelte +16 -5
- package/src/lib/websocket.ts +7 -0
- package/dist/_app/immutable/chunks/CvMmkcEK.js +0 -1
- package/dist/_app/immutable/chunks/DlvidjXv.js +0 -1
- package/dist/_app/immutable/chunks/OZt-i3RT.js +0 -1
- package/dist/_app/immutable/entry/start.CRaGL1oc.js +0 -1
- package/dist/_app/immutable/nodes/1.DOuy_I_c.js +0 -1
|
@@ -92,561 +92,13 @@ var init_controlHelpers = __esm({
|
|
|
92
92
|
}
|
|
93
93
|
});
|
|
94
94
|
|
|
95
|
-
// cli/server/infrastructure/fileStore.ts
|
|
96
|
-
var fileStore_exports = {};
|
|
97
|
-
__export(fileStore_exports, {
|
|
98
|
-
FileStore: () => FileStore
|
|
99
|
-
});
|
|
100
|
-
import {
|
|
101
|
-
existsSync as existsSync2,
|
|
102
|
-
promises as fs,
|
|
103
|
-
mkdirSync,
|
|
104
|
-
readdirSync,
|
|
105
|
-
readFileSync as readFileSync2,
|
|
106
|
-
statSync,
|
|
107
|
-
unlinkSync,
|
|
108
|
-
writeFileSync
|
|
109
|
-
} from "fs";
|
|
110
|
-
import * as yaml2 from "js-yaml";
|
|
111
|
-
import { join as join2 } from "path";
|
|
112
|
-
import { createHash } from "crypto";
|
|
113
|
-
var FileStore;
|
|
114
|
-
var init_fileStore = __esm({
|
|
115
|
-
"cli/server/infrastructure/fileStore.ts"() {
|
|
116
|
-
"use strict";
|
|
117
|
-
init_controlHelpers();
|
|
118
|
-
FileStore = class {
|
|
119
|
-
baseDir;
|
|
120
|
-
controlsDir;
|
|
121
|
-
mappingsDir;
|
|
122
|
-
// Simple cache - just control ID to filename mapping
|
|
123
|
-
controlMetadataCache = /* @__PURE__ */ new Map();
|
|
124
|
-
constructor(options) {
|
|
125
|
-
this.baseDir = options.baseDir;
|
|
126
|
-
this.controlsDir = join2(this.baseDir, "controls");
|
|
127
|
-
this.mappingsDir = join2(this.baseDir, "mappings");
|
|
128
|
-
if (existsSync2(this.controlsDir)) {
|
|
129
|
-
this.refreshControlsCache();
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Get simple filename from control ID
|
|
134
|
-
*/
|
|
135
|
-
getControlFilename(controlId) {
|
|
136
|
-
const sanitized = controlId.replace(/^([A-Z]+)-(.*)/, (match, prefix, suffix) => {
|
|
137
|
-
return `${prefix}-${suffix.replace(/[^\w]/g, "_")}`;
|
|
138
|
-
});
|
|
139
|
-
return `${sanitized}.yaml`;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Get family name from control ID
|
|
143
|
-
*/
|
|
144
|
-
getControlFamily(controlId) {
|
|
145
|
-
return controlId.split("-")[0];
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Ensure required directories exist
|
|
149
|
-
*/
|
|
150
|
-
ensureDirectories() {
|
|
151
|
-
if (!this.baseDir || this.baseDir === "." || this.baseDir === process.cwd()) {
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
const lulaConfigPath2 = join2(this.baseDir, "lula.yaml");
|
|
155
|
-
if (!existsSync2(lulaConfigPath2)) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (!existsSync2(this.controlsDir)) {
|
|
159
|
-
mkdirSync(this.controlsDir, { recursive: true });
|
|
160
|
-
}
|
|
161
|
-
if (!existsSync2(this.mappingsDir)) {
|
|
162
|
-
mkdirSync(this.mappingsDir, { recursive: true });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Get control metadata by ID
|
|
167
|
-
*/
|
|
168
|
-
getControlMetadata(controlId) {
|
|
169
|
-
return this.controlMetadataCache.get(controlId);
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Load a control by ID
|
|
173
|
-
*/
|
|
174
|
-
async loadControl(controlId) {
|
|
175
|
-
const sanitizedId = controlId.replace(/[^\w\-]/g, "_");
|
|
176
|
-
const possibleFlatPaths = [
|
|
177
|
-
join2(this.controlsDir, `${controlId}.yaml`),
|
|
178
|
-
join2(this.controlsDir, `${sanitizedId}.yaml`)
|
|
179
|
-
];
|
|
180
|
-
for (const flatFilePath of possibleFlatPaths) {
|
|
181
|
-
if (existsSync2(flatFilePath)) {
|
|
182
|
-
try {
|
|
183
|
-
const content = readFileSync2(flatFilePath, "utf8");
|
|
184
|
-
const parsed = yaml2.load(content);
|
|
185
|
-
if (!parsed.id) {
|
|
186
|
-
try {
|
|
187
|
-
parsed.id = getControlId(parsed, this.baseDir);
|
|
188
|
-
} catch {
|
|
189
|
-
parsed.id = controlId;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return parsed;
|
|
193
|
-
} catch (error) {
|
|
194
|
-
console.error(`Failed to load control ${controlId} from flat structure:`, error);
|
|
195
|
-
throw new Error(
|
|
196
|
-
`Failed to load control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
const family = this.getControlFamily(controlId);
|
|
202
|
-
const familyDir = join2(this.controlsDir, family);
|
|
203
|
-
const possibleFamilyPaths = [
|
|
204
|
-
join2(familyDir, `${controlId}.yaml`),
|
|
205
|
-
join2(familyDir, `${sanitizedId}.yaml`)
|
|
206
|
-
];
|
|
207
|
-
for (const filePath of possibleFamilyPaths) {
|
|
208
|
-
if (existsSync2(filePath)) {
|
|
209
|
-
try {
|
|
210
|
-
const content = readFileSync2(filePath, "utf8");
|
|
211
|
-
const parsed = yaml2.load(content);
|
|
212
|
-
if (!parsed.id) {
|
|
213
|
-
try {
|
|
214
|
-
parsed.id = getControlId(parsed, this.baseDir);
|
|
215
|
-
} catch {
|
|
216
|
-
parsed.id = controlId;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return parsed;
|
|
220
|
-
} catch (error) {
|
|
221
|
-
console.error(`Failed to load control ${controlId}:`, error);
|
|
222
|
-
throw new Error(
|
|
223
|
-
`Failed to load control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Save a control
|
|
232
|
-
*/
|
|
233
|
-
async saveControl(control) {
|
|
234
|
-
this.ensureDirectories();
|
|
235
|
-
const controlId = getControlId(control, this.baseDir);
|
|
236
|
-
const family = this.getControlFamily(controlId);
|
|
237
|
-
const filename = this.getControlFilename(controlId);
|
|
238
|
-
const familyDir = join2(this.controlsDir, family);
|
|
239
|
-
const filePath = join2(familyDir, filename);
|
|
240
|
-
if (!existsSync2(familyDir)) {
|
|
241
|
-
mkdirSync(familyDir, { recursive: true });
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
let yamlContent;
|
|
245
|
-
if (existsSync2(filePath)) {
|
|
246
|
-
const existingContent = readFileSync2(filePath, "utf8");
|
|
247
|
-
const existingControl = yaml2.load(existingContent);
|
|
248
|
-
const fieldsToUpdate = {};
|
|
249
|
-
for (const key in control) {
|
|
250
|
-
if (key === "timeline" || key === "unifiedHistory" || key === "_metadata" || key === "id") {
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
if (JSON.stringify(control[key]) !== JSON.stringify(existingControl[key])) {
|
|
254
|
-
fieldsToUpdate[key] = control[key];
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (Object.keys(fieldsToUpdate).length > 0) {
|
|
258
|
-
const updatedControl = { ...existingControl, ...fieldsToUpdate };
|
|
259
|
-
yamlContent = yaml2.dump(updatedControl, {
|
|
260
|
-
indent: 2,
|
|
261
|
-
lineWidth: 80,
|
|
262
|
-
noRefs: true,
|
|
263
|
-
sortKeys: false
|
|
264
|
-
});
|
|
265
|
-
} else {
|
|
266
|
-
yamlContent = existingContent;
|
|
267
|
-
}
|
|
268
|
-
} else {
|
|
269
|
-
const controlToSave = { ...control };
|
|
270
|
-
delete controlToSave.timeline;
|
|
271
|
-
delete controlToSave.unifiedHistory;
|
|
272
|
-
delete controlToSave._metadata;
|
|
273
|
-
delete controlToSave.id;
|
|
274
|
-
yamlContent = yaml2.dump(controlToSave, {
|
|
275
|
-
indent: 2,
|
|
276
|
-
lineWidth: 80,
|
|
277
|
-
noRefs: true,
|
|
278
|
-
sortKeys: false
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
writeFileSync(filePath, yamlContent, "utf8");
|
|
282
|
-
this.controlMetadataCache.set(controlId, {
|
|
283
|
-
controlId,
|
|
284
|
-
filename,
|
|
285
|
-
family
|
|
286
|
-
});
|
|
287
|
-
} catch (error) {
|
|
288
|
-
throw new Error(
|
|
289
|
-
`Failed to save control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
/**
|
|
294
|
-
* Delete a control
|
|
295
|
-
*/
|
|
296
|
-
async deleteControl(controlId) {
|
|
297
|
-
const family = this.getControlFamily(controlId);
|
|
298
|
-
const filename = this.getControlFilename(controlId);
|
|
299
|
-
const familyDir = join2(this.controlsDir, family);
|
|
300
|
-
const filePath = join2(familyDir, filename);
|
|
301
|
-
if (existsSync2(filePath)) {
|
|
302
|
-
unlinkSync(filePath);
|
|
303
|
-
this.controlMetadataCache.delete(controlId);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Load all controls
|
|
308
|
-
*/
|
|
309
|
-
async loadAllControls() {
|
|
310
|
-
if (!existsSync2(this.controlsDir)) {
|
|
311
|
-
return [];
|
|
312
|
-
}
|
|
313
|
-
let controlOrder = null;
|
|
314
|
-
try {
|
|
315
|
-
const lulaConfigPath2 = join2(this.baseDir, "lula.yaml");
|
|
316
|
-
if (existsSync2(lulaConfigPath2)) {
|
|
317
|
-
const content = readFileSync2(lulaConfigPath2, "utf8");
|
|
318
|
-
const metadata = yaml2.load(content);
|
|
319
|
-
controlOrder = metadata?.controlOrder || null;
|
|
320
|
-
}
|
|
321
|
-
} catch (error) {
|
|
322
|
-
console.error(`Failed to load lula.yaml for controlOrder (path: ${lulaConfigPath}):`, error);
|
|
323
|
-
}
|
|
324
|
-
const entries = readdirSync(this.controlsDir);
|
|
325
|
-
const yamlFiles = entries.filter((file) => file.endsWith(".yaml"));
|
|
326
|
-
if (yamlFiles.length > 0) {
|
|
327
|
-
const promises = yamlFiles.map(async (file) => {
|
|
328
|
-
try {
|
|
329
|
-
const filePath = join2(this.controlsDir, file);
|
|
330
|
-
const content = await fs.readFile(filePath, "utf8");
|
|
331
|
-
const parsed = yaml2.load(content);
|
|
332
|
-
if (!parsed.id) {
|
|
333
|
-
parsed.id = getControlId(parsed, this.baseDir);
|
|
334
|
-
}
|
|
335
|
-
return parsed;
|
|
336
|
-
} catch (error) {
|
|
337
|
-
console.error(`Failed to load control from file ${file}:`, error);
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
const results2 = await Promise.all(promises);
|
|
342
|
-
const controls2 = results2.filter((c) => c !== null);
|
|
343
|
-
if (controlOrder && controlOrder.length > 0) {
|
|
344
|
-
return this.sortControlsByOrder(controls2, controlOrder);
|
|
345
|
-
}
|
|
346
|
-
return controls2;
|
|
347
|
-
}
|
|
348
|
-
const families = entries.filter((name) => {
|
|
349
|
-
const familyPath = join2(this.controlsDir, name);
|
|
350
|
-
return statSync(familyPath).isDirectory();
|
|
351
|
-
});
|
|
352
|
-
const allPromises = [];
|
|
353
|
-
for (const family of families) {
|
|
354
|
-
const familyPath = join2(this.controlsDir, family);
|
|
355
|
-
const files = readdirSync(familyPath).filter((file) => file.endsWith(".yaml"));
|
|
356
|
-
const familyPromises = files.map(async (file) => {
|
|
357
|
-
try {
|
|
358
|
-
const controlId = file.replace(".yaml", "");
|
|
359
|
-
const control = await this.loadControl(controlId);
|
|
360
|
-
return control;
|
|
361
|
-
} catch (error) {
|
|
362
|
-
console.error(`Failed to load control from file ${file}:`, error);
|
|
363
|
-
return null;
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
allPromises.push(...familyPromises);
|
|
367
|
-
}
|
|
368
|
-
const results = await Promise.all(allPromises);
|
|
369
|
-
const controls = results.filter((c) => c !== null);
|
|
370
|
-
if (controlOrder && controlOrder.length > 0) {
|
|
371
|
-
return this.sortControlsByOrder(controls, controlOrder);
|
|
372
|
-
}
|
|
373
|
-
return controls;
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Sort controls based on the provided order array
|
|
377
|
-
*/
|
|
378
|
-
sortControlsByOrder(controls, controlOrder) {
|
|
379
|
-
const orderMap = /* @__PURE__ */ new Map();
|
|
380
|
-
controlOrder.forEach((controlId, index) => {
|
|
381
|
-
orderMap.set(controlId, index);
|
|
382
|
-
});
|
|
383
|
-
return controls.sort((a, b) => {
|
|
384
|
-
const aIndex = orderMap.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
|
385
|
-
const bIndex = orderMap.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
|
386
|
-
return aIndex - bIndex;
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
/**
|
|
390
|
-
* Load mappings from mappings directory
|
|
391
|
-
*/
|
|
392
|
-
async loadMappings() {
|
|
393
|
-
const mappings = [];
|
|
394
|
-
if (!existsSync2(this.mappingsDir)) {
|
|
395
|
-
return mappings;
|
|
396
|
-
}
|
|
397
|
-
const families = readdirSync(this.mappingsDir).filter((name) => {
|
|
398
|
-
const familyPath = join2(this.mappingsDir, name);
|
|
399
|
-
return statSync(familyPath).isDirectory();
|
|
400
|
-
});
|
|
401
|
-
for (const family of families) {
|
|
402
|
-
const familyPath = join2(this.mappingsDir, family);
|
|
403
|
-
const files = readdirSync(familyPath).filter((file) => file.endsWith("-mappings.yaml"));
|
|
404
|
-
for (const file of files) {
|
|
405
|
-
const mappingFile = join2(familyPath, file);
|
|
406
|
-
try {
|
|
407
|
-
const content = readFileSync2(mappingFile, "utf8");
|
|
408
|
-
const parsed = yaml2.load(content);
|
|
409
|
-
if (Array.isArray(parsed)) {
|
|
410
|
-
parsed.forEach((mapping) => {
|
|
411
|
-
mapping.hash = createHash("sha256").update(JSON.stringify(mapping)).digest("hex");
|
|
412
|
-
return mapping;
|
|
413
|
-
});
|
|
414
|
-
mappings.push(...parsed);
|
|
415
|
-
}
|
|
416
|
-
} catch (error) {
|
|
417
|
-
console.error(`Failed to load mappings from ${family}/${file}:`, error);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return mappings;
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Save a single mapping
|
|
425
|
-
*/
|
|
426
|
-
async saveMapping(mapping) {
|
|
427
|
-
this.ensureDirectories();
|
|
428
|
-
const controlId = mapping.control_id;
|
|
429
|
-
const family = this.getControlFamily(controlId);
|
|
430
|
-
const familyDir = join2(this.mappingsDir, family);
|
|
431
|
-
const mappingFile = join2(
|
|
432
|
-
familyDir,
|
|
433
|
-
`${controlId.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`
|
|
434
|
-
);
|
|
435
|
-
if (!existsSync2(familyDir)) {
|
|
436
|
-
mkdirSync(familyDir, { recursive: true });
|
|
437
|
-
}
|
|
438
|
-
let existingMappings = [];
|
|
439
|
-
if (existsSync2(mappingFile)) {
|
|
440
|
-
try {
|
|
441
|
-
const content = readFileSync2(mappingFile, "utf8");
|
|
442
|
-
existingMappings = yaml2.load(content) || [];
|
|
443
|
-
} catch (error) {
|
|
444
|
-
console.error(`Failed to parse existing mappings file: ${mappingFile}`, error);
|
|
445
|
-
existingMappings = [];
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
const cleanMapping = { ...mapping };
|
|
449
|
-
delete cleanMapping.hash;
|
|
450
|
-
existingMappings.push(cleanMapping);
|
|
451
|
-
try {
|
|
452
|
-
const yamlContent = yaml2.dump(existingMappings, {
|
|
453
|
-
indent: 2,
|
|
454
|
-
lineWidth: -1,
|
|
455
|
-
noRefs: true
|
|
456
|
-
});
|
|
457
|
-
writeFileSync(mappingFile, yamlContent, "utf8");
|
|
458
|
-
} catch (error) {
|
|
459
|
-
throw new Error(
|
|
460
|
-
`Failed to save mapping for control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Delete a single mapping
|
|
466
|
-
*/
|
|
467
|
-
async deleteMapping(compositeKey) {
|
|
468
|
-
const mappingFiles = this.getAllMappingFiles();
|
|
469
|
-
for (const file of mappingFiles) {
|
|
470
|
-
try {
|
|
471
|
-
const content = readFileSync2(file, "utf8");
|
|
472
|
-
let mappings = yaml2.load(content) || [];
|
|
473
|
-
const originalLength = mappings.length;
|
|
474
|
-
mappings = mappings.filter((m) => {
|
|
475
|
-
const hash = createHash("sha256").update(JSON.stringify(m)).digest("hex");
|
|
476
|
-
return `${m.control_id}:${hash}` !== compositeKey;
|
|
477
|
-
});
|
|
478
|
-
if (mappings.length < originalLength) {
|
|
479
|
-
if (mappings.length === 0) {
|
|
480
|
-
unlinkSync(file);
|
|
481
|
-
} else {
|
|
482
|
-
const yamlContent = yaml2.dump(mappings, {
|
|
483
|
-
indent: 2,
|
|
484
|
-
lineWidth: -1,
|
|
485
|
-
noRefs: true
|
|
486
|
-
});
|
|
487
|
-
writeFileSync(file, yamlContent, "utf8");
|
|
488
|
-
}
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
} catch (error) {
|
|
492
|
-
console.error(`Error processing mapping file ${file}:`, error);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* Get all mapping files
|
|
498
|
-
*/
|
|
499
|
-
getAllMappingFiles() {
|
|
500
|
-
const files = [];
|
|
501
|
-
if (!existsSync2(this.mappingsDir)) {
|
|
502
|
-
return files;
|
|
503
|
-
}
|
|
504
|
-
const flatFiles = readdirSync(this.mappingsDir).filter((file) => file.endsWith("-mappings.yaml")).map((file) => join2(this.mappingsDir, file));
|
|
505
|
-
files.push(...flatFiles);
|
|
506
|
-
const entries = readdirSync(this.mappingsDir, { withFileTypes: true });
|
|
507
|
-
for (const entry of entries) {
|
|
508
|
-
if (entry.isDirectory()) {
|
|
509
|
-
const familyDir = join2(this.mappingsDir, entry.name);
|
|
510
|
-
const familyFiles = readdirSync(familyDir).filter((file) => file.endsWith("-mappings.yaml")).map((file) => join2(familyDir, file));
|
|
511
|
-
files.push(...familyFiles);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
return files;
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Save mappings to per-control files
|
|
518
|
-
*/
|
|
519
|
-
async saveMappings(mappings) {
|
|
520
|
-
this.ensureDirectories();
|
|
521
|
-
const mappingsByControl = /* @__PURE__ */ new Map();
|
|
522
|
-
for (const mapping of mappings) {
|
|
523
|
-
const controlId = mapping.control_id;
|
|
524
|
-
if (!mappingsByControl.has(controlId)) {
|
|
525
|
-
mappingsByControl.set(controlId, []);
|
|
526
|
-
}
|
|
527
|
-
mappingsByControl.get(controlId).push(mapping);
|
|
528
|
-
}
|
|
529
|
-
for (const [controlId, controlMappings] of mappingsByControl) {
|
|
530
|
-
const family = this.getControlFamily(controlId);
|
|
531
|
-
const familyDir = join2(this.mappingsDir, family);
|
|
532
|
-
const mappingFile = join2(
|
|
533
|
-
familyDir,
|
|
534
|
-
`${controlId.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`
|
|
535
|
-
);
|
|
536
|
-
if (!existsSync2(familyDir)) {
|
|
537
|
-
mkdirSync(familyDir, { recursive: true });
|
|
538
|
-
}
|
|
539
|
-
const cleanMappings = controlMappings.map((m) => {
|
|
540
|
-
const clean = { ...m };
|
|
541
|
-
delete clean.hash;
|
|
542
|
-
return clean;
|
|
543
|
-
});
|
|
544
|
-
try {
|
|
545
|
-
const yamlContent = yaml2.dump(cleanMappings, {
|
|
546
|
-
indent: 2,
|
|
547
|
-
lineWidth: -1,
|
|
548
|
-
noRefs: true
|
|
549
|
-
});
|
|
550
|
-
writeFileSync(mappingFile, yamlContent, "utf8");
|
|
551
|
-
} catch (error) {
|
|
552
|
-
throw new Error(
|
|
553
|
-
`Failed to save mappings for control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
* Refresh controls cache
|
|
560
|
-
*/
|
|
561
|
-
refreshControlsCache() {
|
|
562
|
-
this.controlMetadataCache.clear();
|
|
563
|
-
if (!existsSync2(this.controlsDir)) {
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
const entries = readdirSync(this.controlsDir);
|
|
567
|
-
const yamlFiles = entries.filter((file) => file.endsWith(".yaml"));
|
|
568
|
-
if (yamlFiles.length > 0) {
|
|
569
|
-
for (const filename of yamlFiles) {
|
|
570
|
-
const controlId = filename.replace(".yaml", "");
|
|
571
|
-
const family = this.getControlFamily(controlId);
|
|
572
|
-
this.controlMetadataCache.set(controlId, {
|
|
573
|
-
controlId,
|
|
574
|
-
filename,
|
|
575
|
-
family
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
return;
|
|
579
|
-
}
|
|
580
|
-
const families = entries.filter((name) => {
|
|
581
|
-
const familyPath = join2(this.controlsDir, name);
|
|
582
|
-
return statSync(familyPath).isDirectory();
|
|
583
|
-
});
|
|
584
|
-
for (const family of families) {
|
|
585
|
-
const familyPath = join2(this.controlsDir, family);
|
|
586
|
-
const files = readdirSync(familyPath).filter((file) => file.endsWith(".yaml"));
|
|
587
|
-
for (const filename of files) {
|
|
588
|
-
try {
|
|
589
|
-
const filePath = join2(familyPath, filename);
|
|
590
|
-
const content = readFileSync2(filePath, "utf8");
|
|
591
|
-
const parsed = yaml2.load(content);
|
|
592
|
-
const controlId = getControlId(parsed, this.baseDir);
|
|
593
|
-
this.controlMetadataCache.set(controlId, {
|
|
594
|
-
controlId,
|
|
595
|
-
filename,
|
|
596
|
-
family
|
|
597
|
-
});
|
|
598
|
-
} catch (error) {
|
|
599
|
-
console.error(`Failed to read control metadata from ${family}/${filename}:`, error);
|
|
600
|
-
const controlId = filename.replace(".yaml", "").replace(/_/g, "/");
|
|
601
|
-
this.controlMetadataCache.set(controlId, {
|
|
602
|
-
controlId,
|
|
603
|
-
filename,
|
|
604
|
-
family
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
/**
|
|
611
|
-
* Get file store statistics
|
|
612
|
-
*/
|
|
613
|
-
getStats() {
|
|
614
|
-
const controlCount = this.controlMetadataCache.size;
|
|
615
|
-
let mappingCount = 0;
|
|
616
|
-
if (existsSync2(this.mappingsDir)) {
|
|
617
|
-
const families = readdirSync(this.mappingsDir).filter((name) => {
|
|
618
|
-
const familyPath = join2(this.mappingsDir, name);
|
|
619
|
-
return statSync(familyPath).isDirectory();
|
|
620
|
-
});
|
|
621
|
-
mappingCount = families.length;
|
|
622
|
-
}
|
|
623
|
-
const familyCount = new Set(
|
|
624
|
-
Array.from(this.controlMetadataCache.values()).map((meta) => meta.family)
|
|
625
|
-
).size;
|
|
626
|
-
return {
|
|
627
|
-
controlCount,
|
|
628
|
-
mappingCount,
|
|
629
|
-
familyCount
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Clear all caches
|
|
634
|
-
*/
|
|
635
|
-
clearCache() {
|
|
636
|
-
this.controlMetadataCache.clear();
|
|
637
|
-
this.refreshControlsCache();
|
|
638
|
-
}
|
|
639
|
-
};
|
|
640
|
-
}
|
|
641
|
-
});
|
|
642
|
-
|
|
643
95
|
// cli/server/infrastructure/yamlDiff.ts
|
|
644
|
-
import * as
|
|
96
|
+
import * as yaml2 from "js-yaml";
|
|
645
97
|
function createYamlDiff(oldYaml, newYaml, isArrayFile = false) {
|
|
646
98
|
try {
|
|
647
99
|
const emptyDefault = isArrayFile ? "[]" : "{}";
|
|
648
|
-
const oldData =
|
|
649
|
-
const newData =
|
|
100
|
+
const oldData = yaml2.load(oldYaml || emptyDefault);
|
|
101
|
+
const newData = yaml2.load(newYaml || emptyDefault);
|
|
650
102
|
const changes = compareValues(oldData, newData, "");
|
|
651
103
|
return {
|
|
652
104
|
hasChanges: changes.length > 0,
|
|
@@ -834,616 +286,1198 @@ function compareMappingArrays(oldArr, newArr, basePath) {
|
|
|
834
286
|
description: `Modified mapping`
|
|
835
287
|
});
|
|
836
288
|
}
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
if (changes.length === 0 && oldArr.length !== newArr.length) {
|
|
840
|
-
changes.push({
|
|
841
|
-
type: "modified",
|
|
842
|
-
path: basePath || "mappings",
|
|
843
|
-
oldValue: oldArr,
|
|
844
|
-
newValue: newArr,
|
|
845
|
-
description: `Mappings changed from ${oldArr.length} to ${newArr.length} items`
|
|
846
|
-
});
|
|
847
|
-
}
|
|
848
|
-
return changes;
|
|
849
|
-
}
|
|
850
|
-
function deepEqual(a, b) {
|
|
851
|
-
if (a === b) return true;
|
|
852
|
-
if (a === null || b === null) return false;
|
|
853
|
-
if (typeof a !== typeof b) return false;
|
|
854
|
-
if (Array.isArray(a) && Array.isArray(b)) {
|
|
855
|
-
if (a.length !== b.length) return false;
|
|
856
|
-
for (let i = 0; i < a.length; i++) {
|
|
857
|
-
if (!deepEqual(a[i], b[i])) return false;
|
|
858
|
-
}
|
|
859
|
-
return true;
|
|
860
|
-
}
|
|
861
|
-
if (typeof a === "object" && typeof b === "object") {
|
|
862
|
-
const aObj = a;
|
|
863
|
-
const bObj = b;
|
|
864
|
-
const aKeys = Object.keys(aObj);
|
|
865
|
-
const bKeys = Object.keys(bObj);
|
|
866
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
867
|
-
for (const key of aKeys) {
|
|
868
|
-
if (!bKeys.includes(key)) return false;
|
|
869
|
-
if (!deepEqual(aObj[key], bObj[key])) return false;
|
|
870
|
-
}
|
|
871
|
-
return true;
|
|
872
|
-
}
|
|
873
|
-
return false;
|
|
874
|
-
}
|
|
875
|
-
function generateSummary(changes) {
|
|
876
|
-
if (changes.length === 0) {
|
|
877
|
-
return "No changes detected";
|
|
878
|
-
}
|
|
879
|
-
const added = changes.filter((c) => c.type === "added").length;
|
|
880
|
-
const removed = changes.filter((c) => c.type === "removed").length;
|
|
881
|
-
const modified = changes.filter((c) => c.type === "modified").length;
|
|
882
|
-
const parts = [];
|
|
883
|
-
if (added > 0) parts.push(`${added} added`);
|
|
884
|
-
if (removed > 0) parts.push(`${removed} removed`);
|
|
885
|
-
if (modified > 0) parts.push(`${modified} modified`);
|
|
886
|
-
return parts.join(", ");
|
|
887
|
-
}
|
|
888
|
-
var init_yamlDiff = __esm({
|
|
889
|
-
"cli/server/infrastructure/yamlDiff.ts"() {
|
|
890
|
-
"use strict";
|
|
891
|
-
}
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
// cli/server/infrastructure/gitHistory.ts
|
|
895
|
-
import * as
|
|
896
|
-
import * as git from "isomorphic-git";
|
|
897
|
-
import { relative } from "path";
|
|
898
|
-
import { execSync } from "child_process";
|
|
899
|
-
var GitHistoryUtil;
|
|
900
|
-
var init_gitHistory = __esm({
|
|
901
|
-
"cli/server/infrastructure/gitHistory.ts"() {
|
|
902
|
-
"use strict";
|
|
903
|
-
init_yamlDiff();
|
|
904
|
-
GitHistoryUtil = class {
|
|
905
|
-
baseDir;
|
|
906
|
-
execSync;
|
|
907
|
-
constructor(baseDir, execSyncFn) {
|
|
908
|
-
this.baseDir = baseDir;
|
|
909
|
-
this.execSync = execSyncFn || execSync;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (changes.length === 0 && oldArr.length !== newArr.length) {
|
|
292
|
+
changes.push({
|
|
293
|
+
type: "modified",
|
|
294
|
+
path: basePath || "mappings",
|
|
295
|
+
oldValue: oldArr,
|
|
296
|
+
newValue: newArr,
|
|
297
|
+
description: `Mappings changed from ${oldArr.length} to ${newArr.length} items`
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
return changes;
|
|
301
|
+
}
|
|
302
|
+
function deepEqual(a, b) {
|
|
303
|
+
if (a === b) return true;
|
|
304
|
+
if (a === null || b === null) return false;
|
|
305
|
+
if (typeof a !== typeof b) return false;
|
|
306
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
307
|
+
if (a.length !== b.length) return false;
|
|
308
|
+
for (let i = 0; i < a.length; i++) {
|
|
309
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
310
|
+
}
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
314
|
+
const aObj = a;
|
|
315
|
+
const bObj = b;
|
|
316
|
+
const aKeys = Object.keys(aObj);
|
|
317
|
+
const bKeys = Object.keys(bObj);
|
|
318
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
319
|
+
for (const key of aKeys) {
|
|
320
|
+
if (!bKeys.includes(key)) return false;
|
|
321
|
+
if (!deepEqual(aObj[key], bObj[key])) return false;
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
function generateSummary(changes) {
|
|
328
|
+
if (changes.length === 0) {
|
|
329
|
+
return "No changes detected";
|
|
330
|
+
}
|
|
331
|
+
const added = changes.filter((c) => c.type === "added").length;
|
|
332
|
+
const removed = changes.filter((c) => c.type === "removed").length;
|
|
333
|
+
const modified = changes.filter((c) => c.type === "modified").length;
|
|
334
|
+
const parts = [];
|
|
335
|
+
if (added > 0) parts.push(`${added} added`);
|
|
336
|
+
if (removed > 0) parts.push(`${removed} removed`);
|
|
337
|
+
if (modified > 0) parts.push(`${modified} modified`);
|
|
338
|
+
return parts.join(", ");
|
|
339
|
+
}
|
|
340
|
+
var init_yamlDiff = __esm({
|
|
341
|
+
"cli/server/infrastructure/yamlDiff.ts"() {
|
|
342
|
+
"use strict";
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// cli/server/infrastructure/gitHistory.ts
|
|
347
|
+
import * as fs from "fs";
|
|
348
|
+
import * as git from "isomorphic-git";
|
|
349
|
+
import { relative } from "path";
|
|
350
|
+
import { execSync } from "child_process";
|
|
351
|
+
var GitHistoryUtil;
|
|
352
|
+
var init_gitHistory = __esm({
|
|
353
|
+
"cli/server/infrastructure/gitHistory.ts"() {
|
|
354
|
+
"use strict";
|
|
355
|
+
init_yamlDiff();
|
|
356
|
+
GitHistoryUtil = class {
|
|
357
|
+
baseDir;
|
|
358
|
+
execSync;
|
|
359
|
+
constructor(baseDir, execSyncFn) {
|
|
360
|
+
this.baseDir = baseDir;
|
|
361
|
+
this.execSync = execSyncFn || execSync;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Execute a git command using native git binary with credentials support
|
|
365
|
+
*/
|
|
366
|
+
async executeGitCommand(command, cwd) {
|
|
367
|
+
try {
|
|
368
|
+
const workingDir = cwd || await git.findRoot({ fs, filepath: process.cwd() });
|
|
369
|
+
const output = this.execSync(command, {
|
|
370
|
+
cwd: workingDir,
|
|
371
|
+
encoding: "utf8",
|
|
372
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
373
|
+
});
|
|
374
|
+
return { success: true, output: output.toString() };
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return {
|
|
377
|
+
success: false,
|
|
378
|
+
output: "",
|
|
379
|
+
error: error.stderr?.toString() || error.message
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Check if the directory is a git repository
|
|
385
|
+
*/
|
|
386
|
+
async isGitRepository() {
|
|
387
|
+
try {
|
|
388
|
+
const gitDir = await git.findRoot({ fs, filepath: process.cwd() });
|
|
389
|
+
return !!gitDir;
|
|
390
|
+
} catch {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Get git history for a specific file
|
|
396
|
+
*/
|
|
397
|
+
async getFileHistory(filePath, limit = 50) {
|
|
398
|
+
const isGitRepo = await this.isGitRepository();
|
|
399
|
+
if (!isGitRepo) {
|
|
400
|
+
return {
|
|
401
|
+
filePath,
|
|
402
|
+
commits: [],
|
|
403
|
+
totalCommits: 0,
|
|
404
|
+
firstCommit: null,
|
|
405
|
+
lastCommit: null
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
|
|
410
|
+
const relativePath = relative(gitRoot, filePath);
|
|
411
|
+
const commits = await git.log({
|
|
412
|
+
fs,
|
|
413
|
+
dir: gitRoot,
|
|
414
|
+
filepath: relativePath,
|
|
415
|
+
depth: limit
|
|
416
|
+
});
|
|
417
|
+
if (!commits || commits.length === 0) {
|
|
418
|
+
return {
|
|
419
|
+
filePath,
|
|
420
|
+
commits: [],
|
|
421
|
+
totalCommits: 0,
|
|
422
|
+
firstCommit: null,
|
|
423
|
+
lastCommit: null
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const gitCommits = await this.convertIsomorphicCommits(commits, relativePath, gitRoot);
|
|
427
|
+
return {
|
|
428
|
+
filePath,
|
|
429
|
+
commits: gitCommits,
|
|
430
|
+
totalCommits: gitCommits.length,
|
|
431
|
+
firstCommit: gitCommits[gitCommits.length - 1] || null,
|
|
432
|
+
lastCommit: gitCommits[0] || null
|
|
433
|
+
};
|
|
434
|
+
} catch (error) {
|
|
435
|
+
const err = error;
|
|
436
|
+
if (err?.code === "NotFoundError" || err?.message?.includes("Could not find file")) {
|
|
437
|
+
return {
|
|
438
|
+
filePath,
|
|
439
|
+
commits: [],
|
|
440
|
+
totalCommits: 0,
|
|
441
|
+
firstCommit: null,
|
|
442
|
+
lastCommit: null
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
console.error(`Unexpected error getting git history for ${filePath}:`, error);
|
|
446
|
+
return {
|
|
447
|
+
filePath,
|
|
448
|
+
commits: [],
|
|
449
|
+
totalCommits: 0,
|
|
450
|
+
firstCommit: null,
|
|
451
|
+
lastCommit: null
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Get the total number of commits for a file
|
|
457
|
+
*/
|
|
458
|
+
async getFileCommitCount(filePath) {
|
|
459
|
+
const isGitRepo = await this.isGitRepository();
|
|
460
|
+
if (!isGitRepo) {
|
|
461
|
+
return 0;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
|
|
465
|
+
const relativePath = relative(gitRoot, filePath);
|
|
466
|
+
const commits = await git.log({
|
|
467
|
+
fs,
|
|
468
|
+
dir: gitRoot,
|
|
469
|
+
filepath: relativePath
|
|
470
|
+
});
|
|
471
|
+
return commits.length;
|
|
472
|
+
} catch {
|
|
473
|
+
return 0;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Get the latest commit info for a file
|
|
478
|
+
*/
|
|
479
|
+
async getLatestCommit(filePath) {
|
|
480
|
+
const history = await this.getFileHistory(filePath, 1);
|
|
481
|
+
return history.lastCommit;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Get file content at a specific commit (public method)
|
|
485
|
+
*/
|
|
486
|
+
async getFileContentAtCommit(filePath, commitHash) {
|
|
487
|
+
const isGitRepo = await this.isGitRepository();
|
|
488
|
+
if (!isGitRepo) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
|
|
493
|
+
const relativePath = relative(gitRoot, filePath);
|
|
494
|
+
return await this.getFileAtCommit(commitHash, relativePath, gitRoot);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
console.error(`Error getting file content at commit ${commitHash}:`, error);
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Convert isomorphic-git commits to our GitCommit format
|
|
502
|
+
*/
|
|
503
|
+
async convertIsomorphicCommits(commits, relativePath, gitRoot) {
|
|
504
|
+
const gitCommits = [];
|
|
505
|
+
for (let i = 0; i < commits.length; i++) {
|
|
506
|
+
const commit = commits[i];
|
|
507
|
+
const includeDiff = i < 5;
|
|
508
|
+
let changes = { insertions: 0, deletions: 0, files: 1 };
|
|
509
|
+
let diff;
|
|
510
|
+
let yamlDiff;
|
|
511
|
+
if (includeDiff) {
|
|
512
|
+
try {
|
|
513
|
+
const diffResult = await this.getCommitDiff(commit.oid, relativePath, gitRoot);
|
|
514
|
+
changes = diffResult.changes;
|
|
515
|
+
diff = diffResult.diff;
|
|
516
|
+
yamlDiff = diffResult.yamlDiff;
|
|
517
|
+
} catch {
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
gitCommits.push({
|
|
521
|
+
hash: commit.oid,
|
|
522
|
+
shortHash: commit.oid.substring(0, 7),
|
|
523
|
+
author: commit.commit.author.name,
|
|
524
|
+
authorEmail: commit.commit.author.email,
|
|
525
|
+
date: new Date(commit.commit.author.timestamp * 1e3).toISOString(),
|
|
526
|
+
message: commit.commit.message,
|
|
527
|
+
changes,
|
|
528
|
+
...diff && { diff },
|
|
529
|
+
...yamlDiff && { yamlDiff }
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return gitCommits;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Get diff for a specific commit and file
|
|
536
|
+
*/
|
|
537
|
+
async getCommitDiff(commitOid, relativePath, gitRoot) {
|
|
538
|
+
try {
|
|
539
|
+
const commit = await git.readCommit({ fs, dir: gitRoot, oid: commitOid });
|
|
540
|
+
const parentOid = commit.commit.parent.length > 0 ? commit.commit.parent[0] : null;
|
|
541
|
+
if (!parentOid) {
|
|
542
|
+
const currentContent2 = await this.getFileAtCommit(commitOid, relativePath, gitRoot);
|
|
543
|
+
if (currentContent2) {
|
|
544
|
+
const lines = currentContent2.split("\n");
|
|
545
|
+
const isMappingFile2 = relativePath.includes("-mappings.yaml");
|
|
546
|
+
const yamlDiff2 = createYamlDiff("", currentContent2, isMappingFile2);
|
|
547
|
+
return {
|
|
548
|
+
changes: { insertions: lines.length, deletions: 0, files: 1 },
|
|
549
|
+
diff: `--- /dev/null
|
|
550
|
+
+++ b/${relativePath}
|
|
551
|
+
@@ -0,0 +1,${lines.length} @@
|
|
552
|
+
` + lines.map((line) => "+" + line).join("\n"),
|
|
553
|
+
yamlDiff: yamlDiff2
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
return { changes: { insertions: 0, deletions: 0, files: 1 } };
|
|
557
|
+
}
|
|
558
|
+
const currentContent = await this.getFileAtCommit(commitOid, relativePath, gitRoot);
|
|
559
|
+
const parentContent = await this.getFileAtCommit(parentOid, relativePath, gitRoot);
|
|
560
|
+
if (!currentContent && !parentContent) {
|
|
561
|
+
return { changes: { insertions: 0, deletions: 0, files: 1 } };
|
|
562
|
+
}
|
|
563
|
+
const currentLines = currentContent ? currentContent.split("\n") : [];
|
|
564
|
+
const parentLines = parentContent ? parentContent.split("\n") : [];
|
|
565
|
+
const diff = await this.createSimpleDiff(parentLines, currentLines, relativePath);
|
|
566
|
+
const { insertions, deletions } = this.countChanges(parentLines, currentLines);
|
|
567
|
+
const isMappingFile = relativePath.includes("-mappings.yaml");
|
|
568
|
+
const yamlDiff = createYamlDiff(parentContent || "", currentContent || "", isMappingFile);
|
|
569
|
+
return {
|
|
570
|
+
changes: { insertions, deletions, files: 1 },
|
|
571
|
+
diff,
|
|
572
|
+
yamlDiff
|
|
573
|
+
};
|
|
574
|
+
} catch {
|
|
575
|
+
return { changes: { insertions: 0, deletions: 0, files: 1 } };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Get file content at a specific commit
|
|
580
|
+
*/
|
|
581
|
+
async getFileAtCommit(commitOid, filepath, gitRoot) {
|
|
582
|
+
try {
|
|
583
|
+
const { blob } = await git.readBlob({
|
|
584
|
+
fs,
|
|
585
|
+
dir: gitRoot,
|
|
586
|
+
oid: commitOid,
|
|
587
|
+
filepath
|
|
588
|
+
});
|
|
589
|
+
return new TextDecoder().decode(blob);
|
|
590
|
+
} catch (_error) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
910
593
|
}
|
|
911
594
|
/**
|
|
912
|
-
*
|
|
595
|
+
* Create a simple unified diff between two file versions
|
|
913
596
|
*/
|
|
914
|
-
async
|
|
597
|
+
async createSimpleDiff(oldLines, newLines, filepath) {
|
|
598
|
+
const diffLines = [];
|
|
599
|
+
diffLines.push(`--- a/${filepath}`);
|
|
600
|
+
diffLines.push(`+++ b/${filepath}`);
|
|
601
|
+
const oldCount = oldLines.length;
|
|
602
|
+
const newCount = newLines.length;
|
|
603
|
+
diffLines.push(`@@ -1,${oldCount} +1,${newCount} @@`);
|
|
604
|
+
let i = 0, j = 0;
|
|
605
|
+
while (i < oldLines.length || j < newLines.length) {
|
|
606
|
+
if (i < oldLines.length && j < newLines.length && oldLines[i] === newLines[j]) {
|
|
607
|
+
diffLines.push(` ${oldLines[i]}`);
|
|
608
|
+
i++;
|
|
609
|
+
j++;
|
|
610
|
+
} else if (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
|
|
611
|
+
diffLines.push(`-${oldLines[i]}`);
|
|
612
|
+
i++;
|
|
613
|
+
} else if (j < newLines.length) {
|
|
614
|
+
diffLines.push(`+${newLines[j]}`);
|
|
615
|
+
j++;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return diffLines.join("\n");
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Count insertions and deletions between two file versions
|
|
622
|
+
*/
|
|
623
|
+
countChanges(oldLines, newLines) {
|
|
624
|
+
let insertions = 0;
|
|
625
|
+
let deletions = 0;
|
|
626
|
+
const maxLines = Math.max(oldLines.length, newLines.length);
|
|
627
|
+
for (let i = 0; i < maxLines; i++) {
|
|
628
|
+
const oldLine = i < oldLines.length ? oldLines[i] : null;
|
|
629
|
+
const newLine = i < newLines.length ? newLines[i] : null;
|
|
630
|
+
if (oldLine === null) {
|
|
631
|
+
insertions++;
|
|
632
|
+
} else if (newLine === null) {
|
|
633
|
+
deletions++;
|
|
634
|
+
} else if (oldLine !== newLine) {
|
|
635
|
+
insertions++;
|
|
636
|
+
deletions++;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return { insertions, deletions };
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Get git stats for the entire repository
|
|
643
|
+
*/
|
|
644
|
+
async getRepositoryStats() {
|
|
645
|
+
const isGitRepo = await this.isGitRepository();
|
|
646
|
+
if (!isGitRepo) {
|
|
647
|
+
return {
|
|
648
|
+
totalCommits: 0,
|
|
649
|
+
contributors: 0,
|
|
650
|
+
lastCommitDate: null,
|
|
651
|
+
firstCommitDate: null
|
|
652
|
+
};
|
|
653
|
+
}
|
|
915
654
|
try {
|
|
916
|
-
const
|
|
917
|
-
const
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
655
|
+
const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
|
|
656
|
+
const commits = await git.log({ fs, dir: gitRoot });
|
|
657
|
+
const contributorEmails = /* @__PURE__ */ new Set();
|
|
658
|
+
commits.forEach((commit) => {
|
|
659
|
+
contributorEmails.add(commit.commit.author.email);
|
|
921
660
|
});
|
|
922
|
-
|
|
661
|
+
const firstCommit = commits[commits.length - 1];
|
|
662
|
+
const lastCommit = commits[0];
|
|
663
|
+
return {
|
|
664
|
+
totalCommits: commits.length,
|
|
665
|
+
contributors: contributorEmails.size,
|
|
666
|
+
lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
|
|
667
|
+
firstCommitDate: firstCommit ? new Date(firstCommit.commit.author.timestamp * 1e3).toISOString() : null
|
|
668
|
+
};
|
|
923
669
|
} catch (error) {
|
|
670
|
+
console.error("Error getting repository stats:", error);
|
|
924
671
|
return {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
672
|
+
totalCommits: 0,
|
|
673
|
+
contributors: 0,
|
|
674
|
+
lastCommitDate: null,
|
|
675
|
+
firstCommitDate: null
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Get current branch name
|
|
681
|
+
*/
|
|
682
|
+
async getCurrentBranch() {
|
|
683
|
+
try {
|
|
684
|
+
const isGitRepo = await this.isGitRepository();
|
|
685
|
+
if (!isGitRepo) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
|
|
689
|
+
const branch = await git.currentBranch({ fs, dir: gitRoot });
|
|
690
|
+
return branch || null;
|
|
691
|
+
} catch (error) {
|
|
692
|
+
console.error("Error getting current branch:", error);
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Get git status information
|
|
698
|
+
*/
|
|
699
|
+
async getGitStatus() {
|
|
700
|
+
try {
|
|
701
|
+
const isGitRepo = await this.isGitRepository();
|
|
702
|
+
if (!isGitRepo) {
|
|
703
|
+
return {
|
|
704
|
+
isGitRepository: false,
|
|
705
|
+
currentBranch: null,
|
|
706
|
+
branchInfo: null,
|
|
707
|
+
canPull: false,
|
|
708
|
+
canPush: false
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const currentBranch2 = await this.getCurrentBranch();
|
|
712
|
+
if (!currentBranch2) {
|
|
713
|
+
return {
|
|
714
|
+
isGitRepository: true,
|
|
715
|
+
currentBranch: null,
|
|
716
|
+
branchInfo: null,
|
|
717
|
+
canPull: false,
|
|
718
|
+
canPush: false
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
const branchInfo = await this.getBranchInfo(currentBranch2);
|
|
722
|
+
return {
|
|
723
|
+
isGitRepository: true,
|
|
724
|
+
currentBranch: currentBranch2,
|
|
725
|
+
branchInfo,
|
|
726
|
+
canPull: branchInfo?.isBehind || false,
|
|
727
|
+
canPush: branchInfo?.isAhead || false
|
|
728
|
+
};
|
|
729
|
+
} catch (error) {
|
|
730
|
+
console.error("Error getting git status:", error);
|
|
731
|
+
return {
|
|
732
|
+
isGitRepository: false,
|
|
733
|
+
currentBranch: null,
|
|
734
|
+
branchInfo: null,
|
|
735
|
+
canPull: false,
|
|
736
|
+
canPush: false
|
|
928
737
|
};
|
|
929
738
|
}
|
|
930
739
|
}
|
|
931
740
|
/**
|
|
932
|
-
*
|
|
741
|
+
* Get branch comparison information
|
|
933
742
|
*/
|
|
934
|
-
async
|
|
743
|
+
async getBranchInfo(branchName) {
|
|
935
744
|
try {
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
745
|
+
const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
|
|
746
|
+
const localCommits = await git.log({ fs, dir: gitRoot, ref: branchName });
|
|
747
|
+
try {
|
|
748
|
+
const remotes = await git.listRemotes({ fs, dir: gitRoot });
|
|
749
|
+
for (const remote of remotes) {
|
|
750
|
+
try {
|
|
751
|
+
const fetchResult = await this.executeGitCommand(`git fetch ${remote.remote}`, gitRoot);
|
|
752
|
+
if (!fetchResult.success) {
|
|
753
|
+
console.warn(`Could not fetch from remote ${remote.remote}:`, fetchResult.error);
|
|
754
|
+
}
|
|
755
|
+
} catch (fetchError) {
|
|
756
|
+
console.warn(`Could not fetch from remote ${remote.remote}:`, fetchError);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
let remoteCommits = [];
|
|
760
|
+
let foundRemote = false;
|
|
761
|
+
for (const remote of remotes) {
|
|
762
|
+
const remoteBranchRef = `${remote.remote}/${branchName}`;
|
|
763
|
+
try {
|
|
764
|
+
remoteCommits = await git.log({ fs, dir: gitRoot, ref: remoteBranchRef });
|
|
765
|
+
foundRemote = true;
|
|
766
|
+
break;
|
|
767
|
+
} catch (error) {
|
|
768
|
+
console.warn(`Could not get commits for ${remoteBranchRef}: ${error}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (!foundRemote || remoteCommits.length === 0) {
|
|
772
|
+
const lastCommit2 = localCommits[0];
|
|
773
|
+
return {
|
|
774
|
+
currentBranch: branchName,
|
|
775
|
+
isAhead: false,
|
|
776
|
+
isBehind: false,
|
|
777
|
+
aheadCount: 0,
|
|
778
|
+
behindCount: 0,
|
|
779
|
+
lastCommitDate: lastCommit2 ? new Date(lastCommit2.commit.author.timestamp * 1e3).toISOString() : null,
|
|
780
|
+
lastCommitMessage: lastCommit2?.commit.message || null,
|
|
781
|
+
hasUnpushedChanges: false
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
const localHashes = new Set(localCommits.map((c) => c.oid));
|
|
785
|
+
const remoteHashes = new Set(remoteCommits.map((c) => c.oid));
|
|
786
|
+
const aheadCount = localCommits.filter((c) => !remoteHashes.has(c.oid)).length;
|
|
787
|
+
const behindCount = remoteCommits.filter((c) => !localHashes.has(c.oid)).length;
|
|
788
|
+
const lastCommit = localCommits[0];
|
|
789
|
+
return {
|
|
790
|
+
currentBranch: branchName,
|
|
791
|
+
isAhead: aheadCount > 0,
|
|
792
|
+
isBehind: behindCount > 0,
|
|
793
|
+
aheadCount,
|
|
794
|
+
behindCount,
|
|
795
|
+
lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
|
|
796
|
+
lastCommitMessage: lastCommit?.commit.message || null,
|
|
797
|
+
hasUnpushedChanges: aheadCount > 0
|
|
798
|
+
};
|
|
799
|
+
} catch {
|
|
800
|
+
const lastCommit = localCommits[0];
|
|
801
|
+
return {
|
|
802
|
+
currentBranch: branchName,
|
|
803
|
+
isAhead: false,
|
|
804
|
+
isBehind: false,
|
|
805
|
+
aheadCount: 0,
|
|
806
|
+
behindCount: 0,
|
|
807
|
+
lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
|
|
808
|
+
lastCommitMessage: lastCommit?.commit.message || null,
|
|
809
|
+
hasUnpushedChanges: false
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
} catch (error) {
|
|
813
|
+
console.error("Error getting branch info:", error);
|
|
814
|
+
return null;
|
|
940
815
|
}
|
|
941
816
|
}
|
|
942
817
|
/**
|
|
943
|
-
*
|
|
818
|
+
* Fetch updates from remote repositories using native git command
|
|
944
819
|
*/
|
|
945
|
-
async
|
|
946
|
-
|
|
947
|
-
|
|
820
|
+
async fetchFromRemotes() {
|
|
821
|
+
try {
|
|
822
|
+
const isGitRepo = await this.isGitRepository();
|
|
823
|
+
if (!isGitRepo) {
|
|
824
|
+
return { success: false, message: "Not a git repository", details: [] };
|
|
825
|
+
}
|
|
826
|
+
const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
|
|
827
|
+
const remotes = await git.listRemotes({ fs, dir: gitRoot });
|
|
828
|
+
if (remotes.length === 0) {
|
|
829
|
+
return { success: true, message: "No remotes configured", details: [] };
|
|
830
|
+
}
|
|
831
|
+
const details = [];
|
|
832
|
+
let hasErrors = false;
|
|
833
|
+
for (const remote of remotes) {
|
|
834
|
+
try {
|
|
835
|
+
const fetchResult = await this.executeGitCommand(`git fetch ${remote.remote}`, gitRoot);
|
|
836
|
+
if (fetchResult.success) {
|
|
837
|
+
details.push(`Fetched from ${remote.remote}`);
|
|
838
|
+
} else {
|
|
839
|
+
details.push(`Failed to fetch from ${remote.remote}: ${fetchResult.error}`);
|
|
840
|
+
hasErrors = true;
|
|
841
|
+
}
|
|
842
|
+
} catch (error) {
|
|
843
|
+
details.push(`Error fetching from ${remote.remote}: ${error}`);
|
|
844
|
+
hasErrors = true;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
948
847
|
return {
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
848
|
+
success: !hasErrors,
|
|
849
|
+
message: hasErrors ? "Fetch completed with some errors" : "Successfully fetched from all remotes",
|
|
850
|
+
details
|
|
851
|
+
};
|
|
852
|
+
} catch (error) {
|
|
853
|
+
console.error("Error fetching from remotes:", error);
|
|
854
|
+
return {
|
|
855
|
+
success: false,
|
|
856
|
+
message: error instanceof Error ? error.message : "Unknown error occurred",
|
|
857
|
+
details: []
|
|
954
858
|
};
|
|
955
859
|
}
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Pull changes from remote using native git command
|
|
863
|
+
*/
|
|
864
|
+
async pullChanges() {
|
|
956
865
|
try {
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
}
|
|
965
|
-
|
|
866
|
+
const isGitRepo = await this.isGitRepository();
|
|
867
|
+
if (!isGitRepo) {
|
|
868
|
+
return { success: false, message: "Not a git repository" };
|
|
869
|
+
}
|
|
870
|
+
const currentBranch2 = await this.getCurrentBranch();
|
|
871
|
+
if (!currentBranch2) {
|
|
872
|
+
return { success: false, message: "No current branch found" };
|
|
873
|
+
}
|
|
874
|
+
const gitRoot = await git.findRoot({ fs, filepath: process.cwd() });
|
|
875
|
+
const remotes = await git.listRemotes({ fs, dir: gitRoot });
|
|
876
|
+
if (remotes.length === 0) {
|
|
877
|
+
return { success: false, message: "No remotes configured" };
|
|
878
|
+
}
|
|
879
|
+
const targetRemote = remotes[0].remote;
|
|
880
|
+
const pullCommand = `git pull ${targetRemote} ${currentBranch2}`;
|
|
881
|
+
const pullResult = await this.executeGitCommand(pullCommand, gitRoot);
|
|
882
|
+
if (!pullResult.success) {
|
|
966
883
|
return {
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
totalCommits: 0,
|
|
970
|
-
firstCommit: null,
|
|
971
|
-
lastCommit: null
|
|
884
|
+
success: false,
|
|
885
|
+
message: `Failed to pull changes: ${pullResult.error}`
|
|
972
886
|
};
|
|
973
887
|
}
|
|
974
|
-
const gitCommits = await this.convertIsomorphicCommits(commits, relativePath, gitRoot);
|
|
975
888
|
return {
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
totalCommits: gitCommits.length,
|
|
979
|
-
firstCommit: gitCommits[gitCommits.length - 1] || null,
|
|
980
|
-
lastCommit: gitCommits[0] || null
|
|
889
|
+
success: true,
|
|
890
|
+
message: pullResult.output || "Successfully pulled changes"
|
|
981
891
|
};
|
|
982
892
|
} catch (error) {
|
|
983
|
-
|
|
984
|
-
if (err?.code === "NotFoundError" || err?.message?.includes("Could not find file")) {
|
|
985
|
-
return {
|
|
986
|
-
filePath,
|
|
987
|
-
commits: [],
|
|
988
|
-
totalCommits: 0,
|
|
989
|
-
firstCommit: null,
|
|
990
|
-
lastCommit: null
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
console.error(`Unexpected error getting git history for ${filePath}:`, error);
|
|
893
|
+
console.error("Error pulling changes:", error);
|
|
994
894
|
return {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
totalCommits: 0,
|
|
998
|
-
firstCommit: null,
|
|
999
|
-
lastCommit: null
|
|
895
|
+
success: false,
|
|
896
|
+
message: error instanceof Error ? error.message : "Unknown error occurred"
|
|
1000
897
|
};
|
|
1001
898
|
}
|
|
1002
899
|
}
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
// cli/server/infrastructure/fileStore.ts
|
|
905
|
+
var fileStore_exports = {};
|
|
906
|
+
__export(fileStore_exports, {
|
|
907
|
+
FileStore: () => FileStore
|
|
908
|
+
});
|
|
909
|
+
import { createHash } from "crypto";
|
|
910
|
+
import {
|
|
911
|
+
existsSync as existsSync2,
|
|
912
|
+
promises as fs2,
|
|
913
|
+
mkdirSync,
|
|
914
|
+
readdirSync,
|
|
915
|
+
readFileSync as readFileSync2,
|
|
916
|
+
statSync,
|
|
917
|
+
unlinkSync,
|
|
918
|
+
writeFileSync
|
|
919
|
+
} from "fs";
|
|
920
|
+
import * as yaml3 from "js-yaml";
|
|
921
|
+
import { join as join2 } from "path";
|
|
922
|
+
var FileStore;
|
|
923
|
+
var init_fileStore = __esm({
|
|
924
|
+
"cli/server/infrastructure/fileStore.ts"() {
|
|
925
|
+
"use strict";
|
|
926
|
+
init_controlHelpers();
|
|
927
|
+
FileStore = class {
|
|
928
|
+
baseDir;
|
|
929
|
+
controlsDir;
|
|
930
|
+
mappingsDir;
|
|
931
|
+
// Simple cache - just control ID to filename mapping
|
|
932
|
+
controlMetadataCache = /* @__PURE__ */ new Map();
|
|
933
|
+
constructor(options) {
|
|
934
|
+
this.baseDir = options.baseDir;
|
|
935
|
+
this.controlsDir = join2(this.baseDir, "controls");
|
|
936
|
+
this.mappingsDir = join2(this.baseDir, "mappings");
|
|
937
|
+
if (existsSync2(this.controlsDir)) {
|
|
938
|
+
this.refreshControlsCache();
|
|
939
|
+
}
|
|
940
|
+
}
|
|
1003
941
|
/**
|
|
1004
|
-
*
|
|
942
|
+
* Update a single mapping in place, preserving file order
|
|
1005
943
|
*/
|
|
1006
|
-
async
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
944
|
+
async updateMapping(oldCompositeKey, updatedMapping) {
|
|
945
|
+
const mappingFiles = this.getAllMappingFiles();
|
|
946
|
+
for (const file of mappingFiles) {
|
|
947
|
+
try {
|
|
948
|
+
const content = readFileSync2(file, "utf8");
|
|
949
|
+
let mappings = yaml3.load(content) || [];
|
|
950
|
+
let changed = false;
|
|
951
|
+
mappings = mappings.map((m) => {
|
|
952
|
+
const hash = createHash("sha256").update(JSON.stringify(m)).digest("hex");
|
|
953
|
+
if (`${m.control_id}:${hash}` === oldCompositeKey) {
|
|
954
|
+
const clean = { ...updatedMapping };
|
|
955
|
+
delete clean.hash;
|
|
956
|
+
changed = true;
|
|
957
|
+
return clean;
|
|
958
|
+
}
|
|
959
|
+
return m;
|
|
960
|
+
});
|
|
961
|
+
if (changed) {
|
|
962
|
+
const yamlContent = yaml3.dump(mappings, {
|
|
963
|
+
indent: 2,
|
|
964
|
+
lineWidth: -1,
|
|
965
|
+
noRefs: true
|
|
966
|
+
});
|
|
967
|
+
writeFileSync(file, yamlContent, "utf8");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
} catch (error) {
|
|
971
|
+
console.error(`Error processing mapping file ${file}:`, error);
|
|
972
|
+
}
|
|
1022
973
|
}
|
|
1023
974
|
}
|
|
1024
975
|
/**
|
|
1025
|
-
* Get
|
|
976
|
+
* Get simple filename from control ID
|
|
977
|
+
*/
|
|
978
|
+
getControlFilename(controlId) {
|
|
979
|
+
const sanitized = controlId.replace(/^([A-Z]+)-(.*)/, (match, prefix, suffix) => {
|
|
980
|
+
return `${prefix}-${suffix.replace(/[^\w]/g, "_")}`;
|
|
981
|
+
});
|
|
982
|
+
return `${sanitized}.yaml`;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Get family name from control ID
|
|
1026
986
|
*/
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
return history.lastCommit;
|
|
987
|
+
getControlFamily(controlId) {
|
|
988
|
+
return controlId.split("-")[0];
|
|
1030
989
|
}
|
|
1031
990
|
/**
|
|
1032
|
-
*
|
|
991
|
+
* Ensure required directories exist
|
|
1033
992
|
*/
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
return null;
|
|
993
|
+
ensureDirectories() {
|
|
994
|
+
if (!this.baseDir || this.baseDir === "." || this.baseDir === process.cwd()) {
|
|
995
|
+
return;
|
|
1038
996
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
997
|
+
const lulaConfigPath = join2(this.baseDir, "lula.yaml");
|
|
998
|
+
if (!existsSync2(lulaConfigPath)) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (!existsSync2(this.controlsDir)) {
|
|
1002
|
+
mkdirSync(this.controlsDir, { recursive: true });
|
|
1003
|
+
}
|
|
1004
|
+
if (!existsSync2(this.mappingsDir)) {
|
|
1005
|
+
mkdirSync(this.mappingsDir, { recursive: true });
|
|
1046
1006
|
}
|
|
1047
1007
|
}
|
|
1048
1008
|
/**
|
|
1049
|
-
*
|
|
1009
|
+
* Get control metadata by ID
|
|
1050
1010
|
*/
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1011
|
+
getControlMetadata(controlId) {
|
|
1012
|
+
return this.controlMetadataCache.get(controlId);
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Load a control by ID
|
|
1016
|
+
*/
|
|
1017
|
+
async loadControl(controlId) {
|
|
1018
|
+
const sanitizedId = controlId.replace(/[^\w\-]/g, "_");
|
|
1019
|
+
const possibleFlatPaths = [
|
|
1020
|
+
join2(this.controlsDir, `${controlId}.yaml`),
|
|
1021
|
+
join2(this.controlsDir, `${sanitizedId}.yaml`)
|
|
1022
|
+
];
|
|
1023
|
+
for (const flatFilePath of possibleFlatPaths) {
|
|
1024
|
+
if (existsSync2(flatFilePath)) {
|
|
1060
1025
|
try {
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1026
|
+
const content = readFileSync2(flatFilePath, "utf8");
|
|
1027
|
+
const parsed = yaml3.load(content);
|
|
1028
|
+
if (!parsed.id) {
|
|
1029
|
+
try {
|
|
1030
|
+
parsed.id = getControlId(parsed, this.baseDir);
|
|
1031
|
+
} catch {
|
|
1032
|
+
parsed.id = controlId;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return parsed;
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
console.error(`Failed to load control ${controlId} from flat structure:`, error);
|
|
1038
|
+
throw new Error(
|
|
1039
|
+
`Failed to load control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
|
|
1040
|
+
);
|
|
1066
1041
|
}
|
|
1067
1042
|
}
|
|
1068
|
-
gitCommits.push({
|
|
1069
|
-
hash: commit.oid,
|
|
1070
|
-
shortHash: commit.oid.substring(0, 7),
|
|
1071
|
-
author: commit.commit.author.name,
|
|
1072
|
-
authorEmail: commit.commit.author.email,
|
|
1073
|
-
date: new Date(commit.commit.author.timestamp * 1e3).toISOString(),
|
|
1074
|
-
message: commit.commit.message,
|
|
1075
|
-
changes,
|
|
1076
|
-
...diff && { diff },
|
|
1077
|
-
...yamlDiff && { yamlDiff }
|
|
1078
|
-
});
|
|
1079
1043
|
}
|
|
1080
|
-
|
|
1044
|
+
const family = this.getControlFamily(controlId);
|
|
1045
|
+
const familyDir = join2(this.controlsDir, family);
|
|
1046
|
+
const possibleFamilyPaths = [
|
|
1047
|
+
join2(familyDir, `${controlId}.yaml`),
|
|
1048
|
+
join2(familyDir, `${sanitizedId}.yaml`)
|
|
1049
|
+
];
|
|
1050
|
+
for (const filePath of possibleFamilyPaths) {
|
|
1051
|
+
if (existsSync2(filePath)) {
|
|
1052
|
+
try {
|
|
1053
|
+
const content = readFileSync2(filePath, "utf8");
|
|
1054
|
+
const parsed = yaml3.load(content);
|
|
1055
|
+
if (!parsed.id) {
|
|
1056
|
+
try {
|
|
1057
|
+
parsed.id = getControlId(parsed, this.baseDir);
|
|
1058
|
+
} catch {
|
|
1059
|
+
parsed.id = controlId;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return parsed;
|
|
1063
|
+
} catch (error) {
|
|
1064
|
+
console.error(`Failed to load control ${controlId}:`, error);
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
`Failed to load control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return null;
|
|
1081
1072
|
}
|
|
1082
1073
|
/**
|
|
1083
|
-
*
|
|
1074
|
+
* Save a control
|
|
1084
1075
|
*/
|
|
1085
|
-
async
|
|
1076
|
+
async saveControl(control) {
|
|
1077
|
+
this.ensureDirectories();
|
|
1078
|
+
const controlId = getControlId(control, this.baseDir);
|
|
1079
|
+
const family = this.getControlFamily(controlId);
|
|
1080
|
+
const filename = this.getControlFilename(controlId);
|
|
1081
|
+
const familyDir = join2(this.controlsDir, family);
|
|
1082
|
+
const filePath = join2(familyDir, filename);
|
|
1083
|
+
if (!existsSync2(familyDir)) {
|
|
1084
|
+
mkdirSync(familyDir, { recursive: true });
|
|
1085
|
+
}
|
|
1086
1086
|
try {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
const
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
@@ -0,0 +1,${lines.length} @@
|
|
1100
|
-
` + lines.map((line) => "+" + line).join("\n"),
|
|
1101
|
-
yamlDiff: yamlDiff2
|
|
1102
|
-
};
|
|
1087
|
+
let yamlContent;
|
|
1088
|
+
if (existsSync2(filePath)) {
|
|
1089
|
+
const existingContent = readFileSync2(filePath, "utf8");
|
|
1090
|
+
const existingControl = yaml3.load(existingContent);
|
|
1091
|
+
const fieldsToUpdate = {};
|
|
1092
|
+
for (const key in control) {
|
|
1093
|
+
if (key === "timeline" || key === "unifiedHistory" || key === "_metadata" || key === "id") {
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
if (JSON.stringify(control[key]) !== JSON.stringify(existingControl[key])) {
|
|
1097
|
+
fieldsToUpdate[key] = control[key];
|
|
1098
|
+
}
|
|
1103
1099
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1100
|
+
if (Object.keys(fieldsToUpdate).length > 0) {
|
|
1101
|
+
const updatedControl = { ...existingControl, ...fieldsToUpdate };
|
|
1102
|
+
yamlContent = yaml3.dump(updatedControl, {
|
|
1103
|
+
indent: 2,
|
|
1104
|
+
lineWidth: 80,
|
|
1105
|
+
noRefs: true,
|
|
1106
|
+
sortKeys: false
|
|
1107
|
+
});
|
|
1108
|
+
} else {
|
|
1109
|
+
yamlContent = existingContent;
|
|
1110
|
+
}
|
|
1111
|
+
} else {
|
|
1112
|
+
const controlToSave = { ...control };
|
|
1113
|
+
delete controlToSave.timeline;
|
|
1114
|
+
delete controlToSave.unifiedHistory;
|
|
1115
|
+
delete controlToSave._metadata;
|
|
1116
|
+
delete controlToSave.id;
|
|
1117
|
+
yamlContent = yaml3.dump(controlToSave, {
|
|
1118
|
+
indent: 2,
|
|
1119
|
+
lineWidth: 80,
|
|
1120
|
+
noRefs: true,
|
|
1121
|
+
sortKeys: false
|
|
1122
|
+
});
|
|
1110
1123
|
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
};
|
|
1122
|
-
} catch {
|
|
1123
|
-
return { changes: { insertions: 0, deletions: 0, files: 1 } };
|
|
1124
|
+
writeFileSync(filePath, yamlContent, "utf8");
|
|
1125
|
+
this.controlMetadataCache.set(controlId, {
|
|
1126
|
+
controlId,
|
|
1127
|
+
filename,
|
|
1128
|
+
family
|
|
1129
|
+
});
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
throw new Error(
|
|
1132
|
+
`Failed to save control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
|
|
1133
|
+
);
|
|
1124
1134
|
}
|
|
1125
1135
|
}
|
|
1126
1136
|
/**
|
|
1127
|
-
*
|
|
1137
|
+
* Delete a control
|
|
1128
1138
|
*/
|
|
1129
|
-
async
|
|
1139
|
+
async deleteControl(controlId) {
|
|
1140
|
+
const family = this.getControlFamily(controlId);
|
|
1141
|
+
const filename = this.getControlFilename(controlId);
|
|
1142
|
+
const familyDir = join2(this.controlsDir, family);
|
|
1143
|
+
const filePath = join2(familyDir, filename);
|
|
1144
|
+
if (existsSync2(filePath)) {
|
|
1145
|
+
unlinkSync(filePath);
|
|
1146
|
+
this.controlMetadataCache.delete(controlId);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Load all controls
|
|
1151
|
+
*/
|
|
1152
|
+
async loadAllControls() {
|
|
1153
|
+
if (!existsSync2(this.controlsDir)) {
|
|
1154
|
+
return [];
|
|
1155
|
+
}
|
|
1156
|
+
let controlOrder = null;
|
|
1157
|
+
const lulaConfigPath = join2(this.baseDir, "lula.yaml");
|
|
1130
1158
|
try {
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1159
|
+
if (existsSync2(lulaConfigPath)) {
|
|
1160
|
+
const content = readFileSync2(lulaConfigPath, "utf8");
|
|
1161
|
+
const metadata = yaml3.load(content);
|
|
1162
|
+
controlOrder = metadata?.controlOrder || null;
|
|
1163
|
+
}
|
|
1164
|
+
} catch (error) {
|
|
1165
|
+
console.error(`Failed to load lula.yaml for controlOrder (path: ${lulaConfigPath}):`, error);
|
|
1166
|
+
}
|
|
1167
|
+
const entries = readdirSync(this.controlsDir);
|
|
1168
|
+
const yamlFiles = entries.filter((file) => file.endsWith(".yaml"));
|
|
1169
|
+
if (yamlFiles.length > 0) {
|
|
1170
|
+
const promises = yamlFiles.map(async (file) => {
|
|
1171
|
+
try {
|
|
1172
|
+
const filePath = join2(this.controlsDir, file);
|
|
1173
|
+
const content = await fs2.readFile(filePath, "utf8");
|
|
1174
|
+
const parsed = yaml3.load(content);
|
|
1175
|
+
if (!parsed.id) {
|
|
1176
|
+
parsed.id = getControlId(parsed, this.baseDir);
|
|
1177
|
+
}
|
|
1178
|
+
return parsed;
|
|
1179
|
+
} catch (error) {
|
|
1180
|
+
console.error(`Failed to load control from file ${file}:`, error);
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
const results2 = await Promise.all(promises);
|
|
1185
|
+
const controls2 = results2.filter((c) => c !== null);
|
|
1186
|
+
if (controlOrder && controlOrder.length > 0) {
|
|
1187
|
+
return this.sortControlsByOrder(controls2, controlOrder);
|
|
1188
|
+
}
|
|
1189
|
+
return controls2;
|
|
1190
|
+
}
|
|
1191
|
+
const families = entries.filter((name) => {
|
|
1192
|
+
const familyPath = join2(this.controlsDir, name);
|
|
1193
|
+
return statSync(familyPath).isDirectory();
|
|
1194
|
+
});
|
|
1195
|
+
const allPromises = [];
|
|
1196
|
+
for (const family of families) {
|
|
1197
|
+
const familyPath = join2(this.controlsDir, family);
|
|
1198
|
+
const files = readdirSync(familyPath).filter((file) => file.endsWith(".yaml"));
|
|
1199
|
+
const familyPromises = files.map(async (file) => {
|
|
1200
|
+
try {
|
|
1201
|
+
const controlId = file.replace(".yaml", "");
|
|
1202
|
+
const control = await this.loadControl(controlId);
|
|
1203
|
+
return control;
|
|
1204
|
+
} catch (error) {
|
|
1205
|
+
console.error(`Failed to load control from file ${file}:`, error);
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1136
1208
|
});
|
|
1137
|
-
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1209
|
+
allPromises.push(...familyPromises);
|
|
1210
|
+
}
|
|
1211
|
+
const results = await Promise.all(allPromises);
|
|
1212
|
+
const controls = results.filter((c) => c !== null);
|
|
1213
|
+
if (controlOrder && controlOrder.length > 0) {
|
|
1214
|
+
return this.sortControlsByOrder(controls, controlOrder);
|
|
1140
1215
|
}
|
|
1216
|
+
return controls;
|
|
1141
1217
|
}
|
|
1142
1218
|
/**
|
|
1143
|
-
*
|
|
1219
|
+
* Sort controls based on the provided order array
|
|
1144
1220
|
*/
|
|
1145
|
-
|
|
1146
|
-
const
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
diffLines.push(` ${oldLines[i]}`);
|
|
1156
|
-
i++;
|
|
1157
|
-
j++;
|
|
1158
|
-
} else if (i < oldLines.length && (j >= newLines.length || oldLines[i] !== newLines[j])) {
|
|
1159
|
-
diffLines.push(`-${oldLines[i]}`);
|
|
1160
|
-
i++;
|
|
1161
|
-
} else if (j < newLines.length) {
|
|
1162
|
-
diffLines.push(`+${newLines[j]}`);
|
|
1163
|
-
j++;
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
return diffLines.join("\n");
|
|
1221
|
+
sortControlsByOrder(controls, controlOrder) {
|
|
1222
|
+
const orderMap = /* @__PURE__ */ new Map();
|
|
1223
|
+
controlOrder.forEach((controlId, index) => {
|
|
1224
|
+
orderMap.set(controlId, index);
|
|
1225
|
+
});
|
|
1226
|
+
return controls.sort((a, b) => {
|
|
1227
|
+
const aIndex = orderMap.get(a.id) ?? Number.MAX_SAFE_INTEGER;
|
|
1228
|
+
const bIndex = orderMap.get(b.id) ?? Number.MAX_SAFE_INTEGER;
|
|
1229
|
+
return aIndex - bIndex;
|
|
1230
|
+
});
|
|
1167
1231
|
}
|
|
1168
1232
|
/**
|
|
1169
|
-
*
|
|
1233
|
+
* Load mappings from mappings directory
|
|
1170
1234
|
*/
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1235
|
+
async loadMappings() {
|
|
1236
|
+
const mappings = [];
|
|
1237
|
+
if (!existsSync2(this.mappingsDir)) {
|
|
1238
|
+
return mappings;
|
|
1239
|
+
}
|
|
1240
|
+
const families = readdirSync(this.mappingsDir).filter((name) => {
|
|
1241
|
+
const familyPath = join2(this.mappingsDir, name);
|
|
1242
|
+
return statSync(familyPath).isDirectory();
|
|
1243
|
+
});
|
|
1244
|
+
for (const family of families) {
|
|
1245
|
+
const familyPath = join2(this.mappingsDir, family);
|
|
1246
|
+
const files = readdirSync(familyPath).filter((file) => file.endsWith("-mappings.yaml"));
|
|
1247
|
+
for (const file of files) {
|
|
1248
|
+
const mappingFile = join2(familyPath, file);
|
|
1249
|
+
try {
|
|
1250
|
+
const content = readFileSync2(mappingFile, "utf8");
|
|
1251
|
+
const parsed = yaml3.load(content);
|
|
1252
|
+
if (Array.isArray(parsed)) {
|
|
1253
|
+
parsed.forEach((mapping) => {
|
|
1254
|
+
mapping.hash = createHash("sha256").update(JSON.stringify(mapping)).digest("hex");
|
|
1255
|
+
return mapping;
|
|
1256
|
+
});
|
|
1257
|
+
mappings.push(...parsed);
|
|
1258
|
+
}
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
console.error(`Failed to load mappings from ${family}/${file}:`, error);
|
|
1261
|
+
}
|
|
1185
1262
|
}
|
|
1186
1263
|
}
|
|
1187
|
-
return
|
|
1264
|
+
return mappings;
|
|
1188
1265
|
}
|
|
1189
1266
|
/**
|
|
1190
|
-
*
|
|
1267
|
+
* Save a single mapping
|
|
1191
1268
|
*/
|
|
1192
|
-
async
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1269
|
+
async saveMapping(mapping) {
|
|
1270
|
+
this.ensureDirectories();
|
|
1271
|
+
const controlId = mapping.control_id;
|
|
1272
|
+
const family = this.getControlFamily(controlId);
|
|
1273
|
+
const familyDir = join2(this.mappingsDir, family);
|
|
1274
|
+
const mappingFile = join2(
|
|
1275
|
+
familyDir,
|
|
1276
|
+
`${controlId.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`
|
|
1277
|
+
);
|
|
1278
|
+
if (!existsSync2(familyDir)) {
|
|
1279
|
+
mkdirSync(familyDir, { recursive: true });
|
|
1280
|
+
}
|
|
1281
|
+
let existingMappings = [];
|
|
1282
|
+
if (existsSync2(mappingFile)) {
|
|
1283
|
+
try {
|
|
1284
|
+
const content = readFileSync2(mappingFile, "utf8");
|
|
1285
|
+
existingMappings = yaml3.load(content) || [];
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
console.error(`Failed to parse existing mappings file: ${mappingFile}`, error);
|
|
1288
|
+
existingMappings = [];
|
|
1289
|
+
}
|
|
1201
1290
|
}
|
|
1291
|
+
const cleanMapping = { ...mapping };
|
|
1292
|
+
delete cleanMapping.hash;
|
|
1293
|
+
existingMappings.push(cleanMapping);
|
|
1202
1294
|
try {
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
contributorEmails.add(commit.commit.author.email);
|
|
1295
|
+
const yamlContent = yaml3.dump(existingMappings, {
|
|
1296
|
+
indent: 2,
|
|
1297
|
+
lineWidth: -1,
|
|
1298
|
+
noRefs: true
|
|
1208
1299
|
});
|
|
1209
|
-
|
|
1210
|
-
const lastCommit = commits[0];
|
|
1211
|
-
return {
|
|
1212
|
-
totalCommits: commits.length,
|
|
1213
|
-
contributors: contributorEmails.size,
|
|
1214
|
-
lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
|
|
1215
|
-
firstCommitDate: firstCommit ? new Date(firstCommit.commit.author.timestamp * 1e3).toISOString() : null
|
|
1216
|
-
};
|
|
1300
|
+
writeFileSync(mappingFile, yamlContent, "utf8");
|
|
1217
1301
|
} catch (error) {
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
contributors: 0,
|
|
1222
|
-
lastCommitDate: null,
|
|
1223
|
-
firstCommitDate: null
|
|
1224
|
-
};
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`Failed to save mapping for control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
|
|
1304
|
+
);
|
|
1225
1305
|
}
|
|
1226
1306
|
}
|
|
1227
1307
|
/**
|
|
1228
|
-
*
|
|
1308
|
+
* Delete a single mapping
|
|
1229
1309
|
*/
|
|
1230
|
-
async
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1310
|
+
async deleteMapping(compositeKey) {
|
|
1311
|
+
const mappingFiles = this.getAllMappingFiles();
|
|
1312
|
+
for (const file of mappingFiles) {
|
|
1313
|
+
try {
|
|
1314
|
+
const content = readFileSync2(file, "utf8");
|
|
1315
|
+
let mappings = yaml3.load(content) || [];
|
|
1316
|
+
const originalLength = mappings.length;
|
|
1317
|
+
mappings = mappings.filter((m) => {
|
|
1318
|
+
const hash = createHash("sha256").update(JSON.stringify(m)).digest("hex");
|
|
1319
|
+
return `${m.control_id}:${hash}` !== compositeKey;
|
|
1320
|
+
});
|
|
1321
|
+
if (mappings.length < originalLength) {
|
|
1322
|
+
if (mappings.length === 0) {
|
|
1323
|
+
unlinkSync(file);
|
|
1324
|
+
} else {
|
|
1325
|
+
const yamlContent = yaml3.dump(mappings, {
|
|
1326
|
+
indent: 2,
|
|
1327
|
+
lineWidth: -1,
|
|
1328
|
+
noRefs: true
|
|
1329
|
+
});
|
|
1330
|
+
writeFileSync(file, yamlContent, "utf8");
|
|
1331
|
+
}
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
console.error(`Error processing mapping file ${file}:`, error);
|
|
1235
1336
|
}
|
|
1236
|
-
const gitRoot = await git.findRoot({ fs: fs2, filepath: process.cwd() });
|
|
1237
|
-
const branch = await git.currentBranch({ fs: fs2, dir: gitRoot });
|
|
1238
|
-
return branch || null;
|
|
1239
|
-
} catch (error) {
|
|
1240
|
-
console.error("Error getting current branch:", error);
|
|
1241
|
-
return null;
|
|
1242
1337
|
}
|
|
1243
1338
|
}
|
|
1244
1339
|
/**
|
|
1245
|
-
* Get
|
|
1340
|
+
* Get all mapping files
|
|
1246
1341
|
*/
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
if (!currentBranch2) {
|
|
1261
|
-
return {
|
|
1262
|
-
isGitRepository: true,
|
|
1263
|
-
currentBranch: null,
|
|
1264
|
-
branchInfo: null,
|
|
1265
|
-
canPull: false,
|
|
1266
|
-
canPush: false
|
|
1267
|
-
};
|
|
1342
|
+
getAllMappingFiles() {
|
|
1343
|
+
const files = [];
|
|
1344
|
+
if (!existsSync2(this.mappingsDir)) {
|
|
1345
|
+
return files;
|
|
1346
|
+
}
|
|
1347
|
+
const flatFiles = readdirSync(this.mappingsDir).filter((file) => file.endsWith("-mappings.yaml")).map((file) => join2(this.mappingsDir, file));
|
|
1348
|
+
files.push(...flatFiles);
|
|
1349
|
+
const entries = readdirSync(this.mappingsDir, { withFileTypes: true });
|
|
1350
|
+
for (const entry of entries) {
|
|
1351
|
+
if (entry.isDirectory()) {
|
|
1352
|
+
const familyDir = join2(this.mappingsDir, entry.name);
|
|
1353
|
+
const familyFiles = readdirSync(familyDir).filter((file) => file.endsWith("-mappings.yaml")).map((file) => join2(familyDir, file));
|
|
1354
|
+
files.push(...familyFiles);
|
|
1268
1355
|
}
|
|
1269
|
-
const branchInfo = await this.getBranchInfo(currentBranch2);
|
|
1270
|
-
return {
|
|
1271
|
-
isGitRepository: true,
|
|
1272
|
-
currentBranch: currentBranch2,
|
|
1273
|
-
branchInfo,
|
|
1274
|
-
canPull: branchInfo?.isBehind || false,
|
|
1275
|
-
canPush: branchInfo?.isAhead || false
|
|
1276
|
-
};
|
|
1277
|
-
} catch (error) {
|
|
1278
|
-
console.error("Error getting git status:", error);
|
|
1279
|
-
return {
|
|
1280
|
-
isGitRepository: false,
|
|
1281
|
-
currentBranch: null,
|
|
1282
|
-
branchInfo: null,
|
|
1283
|
-
canPull: false,
|
|
1284
|
-
canPush: false
|
|
1285
|
-
};
|
|
1286
1356
|
}
|
|
1357
|
+
return files;
|
|
1287
1358
|
}
|
|
1288
1359
|
/**
|
|
1289
|
-
*
|
|
1360
|
+
* Save mappings to per-control files
|
|
1290
1361
|
*/
|
|
1291
|
-
async
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1362
|
+
async saveMappings(mappings) {
|
|
1363
|
+
this.ensureDirectories();
|
|
1364
|
+
const mappingsByControl = /* @__PURE__ */ new Map();
|
|
1365
|
+
for (const mapping of mappings) {
|
|
1366
|
+
const controlId = mapping.control_id;
|
|
1367
|
+
if (!mappingsByControl.has(controlId)) {
|
|
1368
|
+
mappingsByControl.set(controlId, []);
|
|
1369
|
+
}
|
|
1370
|
+
mappingsByControl.get(controlId).push(mapping);
|
|
1371
|
+
}
|
|
1372
|
+
for (const [controlId, controlMappings] of mappingsByControl) {
|
|
1373
|
+
const family = this.getControlFamily(controlId);
|
|
1374
|
+
const familyDir = join2(this.mappingsDir, family);
|
|
1375
|
+
const mappingFile = join2(
|
|
1376
|
+
familyDir,
|
|
1377
|
+
`${controlId.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`
|
|
1378
|
+
);
|
|
1379
|
+
if (!existsSync2(familyDir)) {
|
|
1380
|
+
mkdirSync(familyDir, { recursive: true });
|
|
1381
|
+
}
|
|
1382
|
+
const cleanMappings = controlMappings.map((m) => {
|
|
1383
|
+
const clean = { ...m };
|
|
1384
|
+
delete clean.hash;
|
|
1385
|
+
return clean;
|
|
1386
|
+
});
|
|
1295
1387
|
try {
|
|
1296
|
-
const
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1307
|
-
let remoteCommits = [];
|
|
1308
|
-
let foundRemote = false;
|
|
1309
|
-
for (const remote of remotes) {
|
|
1310
|
-
const remoteBranchRef = `${remote.remote}/${branchName}`;
|
|
1311
|
-
try {
|
|
1312
|
-
remoteCommits = await git.log({ fs: fs2, dir: gitRoot, ref: remoteBranchRef });
|
|
1313
|
-
foundRemote = true;
|
|
1314
|
-
break;
|
|
1315
|
-
} catch (error) {
|
|
1316
|
-
console.warn(`Could not get commits for ${remoteBranchRef}: ${error}`);
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
if (!foundRemote || remoteCommits.length === 0) {
|
|
1320
|
-
const lastCommit2 = localCommits[0];
|
|
1321
|
-
return {
|
|
1322
|
-
currentBranch: branchName,
|
|
1323
|
-
isAhead: false,
|
|
1324
|
-
isBehind: false,
|
|
1325
|
-
aheadCount: 0,
|
|
1326
|
-
behindCount: 0,
|
|
1327
|
-
lastCommitDate: lastCommit2 ? new Date(lastCommit2.commit.author.timestamp * 1e3).toISOString() : null,
|
|
1328
|
-
lastCommitMessage: lastCommit2?.commit.message || null,
|
|
1329
|
-
hasUnpushedChanges: false
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
|
-
const localHashes = new Set(localCommits.map((c) => c.oid));
|
|
1333
|
-
const remoteHashes = new Set(remoteCommits.map((c) => c.oid));
|
|
1334
|
-
const aheadCount = localCommits.filter((c) => !remoteHashes.has(c.oid)).length;
|
|
1335
|
-
const behindCount = remoteCommits.filter((c) => !localHashes.has(c.oid)).length;
|
|
1336
|
-
const lastCommit = localCommits[0];
|
|
1337
|
-
return {
|
|
1338
|
-
currentBranch: branchName,
|
|
1339
|
-
isAhead: aheadCount > 0,
|
|
1340
|
-
isBehind: behindCount > 0,
|
|
1341
|
-
aheadCount,
|
|
1342
|
-
behindCount,
|
|
1343
|
-
lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
|
|
1344
|
-
lastCommitMessage: lastCommit?.commit.message || null,
|
|
1345
|
-
hasUnpushedChanges: aheadCount > 0
|
|
1346
|
-
};
|
|
1347
|
-
} catch {
|
|
1348
|
-
const lastCommit = localCommits[0];
|
|
1349
|
-
return {
|
|
1350
|
-
currentBranch: branchName,
|
|
1351
|
-
isAhead: false,
|
|
1352
|
-
isBehind: false,
|
|
1353
|
-
aheadCount: 0,
|
|
1354
|
-
behindCount: 0,
|
|
1355
|
-
lastCommitDate: lastCommit ? new Date(lastCommit.commit.author.timestamp * 1e3).toISOString() : null,
|
|
1356
|
-
lastCommitMessage: lastCommit?.commit.message || null,
|
|
1357
|
-
hasUnpushedChanges: false
|
|
1358
|
-
};
|
|
1388
|
+
const yamlContent = yaml3.dump(cleanMappings, {
|
|
1389
|
+
indent: 2,
|
|
1390
|
+
lineWidth: -1,
|
|
1391
|
+
noRefs: true
|
|
1392
|
+
});
|
|
1393
|
+
writeFileSync(mappingFile, yamlContent, "utf8");
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
throw new Error(
|
|
1396
|
+
`Failed to save mappings for control ${controlId}: ${error instanceof Error ? error.message : String(error)}`
|
|
1397
|
+
);
|
|
1359
1398
|
}
|
|
1360
|
-
} catch (error) {
|
|
1361
|
-
console.error("Error getting branch info:", error);
|
|
1362
|
-
return null;
|
|
1363
1399
|
}
|
|
1364
1400
|
}
|
|
1365
1401
|
/**
|
|
1366
|
-
*
|
|
1402
|
+
* Refresh controls cache
|
|
1367
1403
|
*/
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1404
|
+
refreshControlsCache() {
|
|
1405
|
+
this.controlMetadataCache.clear();
|
|
1406
|
+
if (!existsSync2(this.controlsDir)) {
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
const entries = readdirSync(this.controlsDir);
|
|
1410
|
+
const yamlFiles = entries.filter((file) => file.endsWith(".yaml"));
|
|
1411
|
+
if (yamlFiles.length > 0) {
|
|
1412
|
+
for (const filename of yamlFiles) {
|
|
1413
|
+
const controlId = filename.replace(".yaml", "");
|
|
1414
|
+
const family = this.getControlFamily(controlId);
|
|
1415
|
+
this.controlMetadataCache.set(controlId, {
|
|
1416
|
+
controlId,
|
|
1417
|
+
filename,
|
|
1418
|
+
family
|
|
1419
|
+
});
|
|
1378
1420
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const families = entries.filter((name) => {
|
|
1424
|
+
const familyPath = join2(this.controlsDir, name);
|
|
1425
|
+
return statSync(familyPath).isDirectory();
|
|
1426
|
+
});
|
|
1427
|
+
for (const family of families) {
|
|
1428
|
+
const familyPath = join2(this.controlsDir, family);
|
|
1429
|
+
const files = readdirSync(familyPath).filter((file) => file.endsWith(".yaml"));
|
|
1430
|
+
for (const filename of files) {
|
|
1382
1431
|
try {
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1432
|
+
const filePath = join2(familyPath, filename);
|
|
1433
|
+
const content = readFileSync2(filePath, "utf8");
|
|
1434
|
+
const parsed = yaml3.load(content);
|
|
1435
|
+
const controlId = getControlId(parsed, this.baseDir);
|
|
1436
|
+
this.controlMetadataCache.set(controlId, {
|
|
1437
|
+
controlId,
|
|
1438
|
+
filename,
|
|
1439
|
+
family
|
|
1440
|
+
});
|
|
1390
1441
|
} catch (error) {
|
|
1391
|
-
|
|
1392
|
-
|
|
1442
|
+
console.error(`Failed to read control metadata from ${family}/${filename}:`, error);
|
|
1443
|
+
const controlId = filename.replace(".yaml", "").replace(/_/g, "/");
|
|
1444
|
+
this.controlMetadataCache.set(controlId, {
|
|
1445
|
+
controlId,
|
|
1446
|
+
filename,
|
|
1447
|
+
family
|
|
1448
|
+
});
|
|
1393
1449
|
}
|
|
1394
1450
|
}
|
|
1395
|
-
return {
|
|
1396
|
-
success: !hasErrors,
|
|
1397
|
-
message: hasErrors ? "Fetch completed with some errors" : "Successfully fetched from all remotes",
|
|
1398
|
-
details
|
|
1399
|
-
};
|
|
1400
|
-
} catch (error) {
|
|
1401
|
-
console.error("Error fetching from remotes:", error);
|
|
1402
|
-
return {
|
|
1403
|
-
success: false,
|
|
1404
|
-
message: error instanceof Error ? error.message : "Unknown error occurred",
|
|
1405
|
-
details: []
|
|
1406
|
-
};
|
|
1407
1451
|
}
|
|
1408
1452
|
}
|
|
1409
1453
|
/**
|
|
1410
|
-
*
|
|
1454
|
+
* Get file store statistics
|
|
1411
1455
|
*/
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
const gitRoot = await git.findRoot({ fs: fs2, filepath: process.cwd() });
|
|
1423
|
-
const remotes = await git.listRemotes({ fs: fs2, dir: gitRoot });
|
|
1424
|
-
if (remotes.length === 0) {
|
|
1425
|
-
return { success: false, message: "No remotes configured" };
|
|
1426
|
-
}
|
|
1427
|
-
const targetRemote = remotes[0].remote;
|
|
1428
|
-
const pullCommand = `git pull ${targetRemote} ${currentBranch2}`;
|
|
1429
|
-
const pullResult = await this.executeGitCommand(pullCommand, gitRoot);
|
|
1430
|
-
if (!pullResult.success) {
|
|
1431
|
-
return {
|
|
1432
|
-
success: false,
|
|
1433
|
-
message: `Failed to pull changes: ${pullResult.error}`
|
|
1434
|
-
};
|
|
1435
|
-
}
|
|
1436
|
-
return {
|
|
1437
|
-
success: true,
|
|
1438
|
-
message: pullResult.output || "Successfully pulled changes"
|
|
1439
|
-
};
|
|
1440
|
-
} catch (error) {
|
|
1441
|
-
console.error("Error pulling changes:", error);
|
|
1442
|
-
return {
|
|
1443
|
-
success: false,
|
|
1444
|
-
message: error instanceof Error ? error.message : "Unknown error occurred"
|
|
1445
|
-
};
|
|
1456
|
+
getStats() {
|
|
1457
|
+
const controlCount = this.controlMetadataCache.size;
|
|
1458
|
+
let mappingCount = 0;
|
|
1459
|
+
if (existsSync2(this.mappingsDir)) {
|
|
1460
|
+
const families = readdirSync(this.mappingsDir).filter((name) => {
|
|
1461
|
+
const familyPath = join2(this.mappingsDir, name);
|
|
1462
|
+
return statSync(familyPath).isDirectory();
|
|
1463
|
+
});
|
|
1464
|
+
mappingCount = families.length;
|
|
1446
1465
|
}
|
|
1466
|
+
const familyCount = new Set(
|
|
1467
|
+
Array.from(this.controlMetadataCache.values()).map((meta) => meta.family)
|
|
1468
|
+
).size;
|
|
1469
|
+
return {
|
|
1470
|
+
controlCount,
|
|
1471
|
+
mappingCount,
|
|
1472
|
+
familyCount
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Clear all caches
|
|
1477
|
+
*/
|
|
1478
|
+
clearCache() {
|
|
1479
|
+
this.controlMetadataCache.clear();
|
|
1480
|
+
this.refreshControlsCache();
|
|
1447
1481
|
}
|
|
1448
1482
|
};
|
|
1449
1483
|
}
|
|
@@ -2653,12 +2687,12 @@ var init_spreadsheetRoutes = __esm({
|
|
|
2653
2687
|
import { readFileSync as readFileSync4 } from "fs";
|
|
2654
2688
|
init_debug();
|
|
2655
2689
|
init_controlHelpers();
|
|
2656
|
-
init_serverState();
|
|
2657
2690
|
init_gitHistory();
|
|
2691
|
+
init_serverState();
|
|
2658
2692
|
import * as yaml5 from "js-yaml";
|
|
2693
|
+
import crypto2 from "node:crypto";
|
|
2659
2694
|
import { join as join5 } from "path";
|
|
2660
2695
|
import { WebSocket, WebSocketServer } from "ws";
|
|
2661
|
-
import crypto2 from "node:crypto";
|
|
2662
2696
|
var WebSocketManager = class {
|
|
2663
2697
|
wss = null;
|
|
2664
2698
|
clients = /* @__PURE__ */ new Set();
|
|
@@ -2699,6 +2733,62 @@ var WebSocketManager = class {
|
|
|
2699
2733
|
}
|
|
2700
2734
|
break;
|
|
2701
2735
|
}
|
|
2736
|
+
case "update-mapping": {
|
|
2737
|
+
const state = getServerState();
|
|
2738
|
+
if (payload && payload.old_composite_key && payload.mapping) {
|
|
2739
|
+
const oldCompositeKey = payload.old_composite_key;
|
|
2740
|
+
const existing = state.mappingsCache.get(oldCompositeKey);
|
|
2741
|
+
if (!existing) {
|
|
2742
|
+
console.error("Mapping not found for update:", oldCompositeKey);
|
|
2743
|
+
break;
|
|
2744
|
+
}
|
|
2745
|
+
const incoming = payload.mapping;
|
|
2746
|
+
const updated = {
|
|
2747
|
+
...existing,
|
|
2748
|
+
...incoming,
|
|
2749
|
+
control_id: incoming.control_id || existing.control_id,
|
|
2750
|
+
uuid: incoming.uuid || existing.uuid
|
|
2751
|
+
};
|
|
2752
|
+
if (!updated.hash || updated.hash === "") {
|
|
2753
|
+
updated.hash = crypto2.createHash("sha256").update(JSON.stringify({ ...updated, hash: void 0 })).digest("hex");
|
|
2754
|
+
}
|
|
2755
|
+
const oldHash = existing.hash;
|
|
2756
|
+
const oldControlId = existing.control_id;
|
|
2757
|
+
const oldFamily = oldControlId.split("-")[0];
|
|
2758
|
+
const newHash = updated.hash;
|
|
2759
|
+
const newControlId = updated.control_id;
|
|
2760
|
+
const newFamily = newControlId.split("-")[0];
|
|
2761
|
+
const newCompositeKey = `${newControlId}:${newHash}`;
|
|
2762
|
+
await state.fileStore.updateMapping(oldCompositeKey, updated);
|
|
2763
|
+
const entries = Array.from(state.mappingsCache.entries());
|
|
2764
|
+
const oldIndex = entries.findIndex(([key]) => key === oldCompositeKey);
|
|
2765
|
+
if (oldIndex === -1) {
|
|
2766
|
+
state.mappingsCache.delete(oldCompositeKey);
|
|
2767
|
+
state.mappingsCache.set(newCompositeKey, updated);
|
|
2768
|
+
} else {
|
|
2769
|
+
entries[oldIndex] = [newCompositeKey, updated];
|
|
2770
|
+
state.mappingsCache = new Map(entries);
|
|
2771
|
+
}
|
|
2772
|
+
state.mappingsByFamily.get(oldFamily)?.delete(oldHash);
|
|
2773
|
+
state.mappingsByControl.get(oldControlId)?.delete(oldHash);
|
|
2774
|
+
if (!state.mappingsByFamily.has(newFamily)) {
|
|
2775
|
+
state.mappingsByFamily.set(newFamily, /* @__PURE__ */ new Set());
|
|
2776
|
+
}
|
|
2777
|
+
state.mappingsByFamily.get(newFamily).add(newHash);
|
|
2778
|
+
if (!state.mappingsByControl.has(newControlId)) {
|
|
2779
|
+
state.mappingsByControl.set(newControlId, /* @__PURE__ */ new Set());
|
|
2780
|
+
}
|
|
2781
|
+
state.mappingsByControl.get(newControlId).add(newHash);
|
|
2782
|
+
ws.send(
|
|
2783
|
+
JSON.stringify({
|
|
2784
|
+
type: "mapping-updated",
|
|
2785
|
+
payload: { uuid: updated.uuid, success: true }
|
|
2786
|
+
})
|
|
2787
|
+
);
|
|
2788
|
+
this.broadcastState();
|
|
2789
|
+
}
|
|
2790
|
+
break;
|
|
2791
|
+
}
|
|
2702
2792
|
case "refresh-controls": {
|
|
2703
2793
|
const state = getServerState();
|
|
2704
2794
|
state.controlsCache.clear();
|