lula2 0.4.0-nightly.0 → 0.5.0-nightly.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/_app/immutable/assets/0.Dfpe5goI.css +1 -0
  2. package/dist/_app/immutable/chunks/{d2wclEM1.js → B3DV5AB9.js} +1 -1
  3. package/dist/_app/immutable/chunks/{B8sFn9qB.js → BIY1u1I9.js} +1 -1
  4. package/dist/_app/immutable/chunks/{DPYPcVpy.js → BN4ish10.js} +1 -1
  5. package/dist/_app/immutable/chunks/B_RiH_9Y.js +1 -0
  6. package/dist/_app/immutable/chunks/{D6AXzSy_.js → BtwnwKFn.js} +1 -1
  7. package/dist/_app/immutable/chunks/{xZlTocHV.js → BwrF4PHr.js} +2 -2
  8. package/dist/_app/immutable/chunks/{DL7cUWpq.js → D6NghQtU.js} +1 -1
  9. package/dist/_app/immutable/chunks/{AMsv4DTs.js → DBN1r830.js} +1 -1
  10. package/dist/_app/immutable/chunks/{C3vRXKjP.js → Jk6WnwcK.js} +19 -19
  11. package/dist/_app/immutable/entry/{app.DG0UbF3Y.js → app.93CAMVUc.js} +2 -2
  12. package/dist/_app/immutable/entry/start.BB3zji-X.js +1 -0
  13. package/dist/_app/immutable/nodes/0.CpU4yZ2S.js +2 -0
  14. package/dist/_app/immutable/nodes/{1.C51hCwd8.js → 1.BJpxXRom.js} +1 -1
  15. package/dist/_app/immutable/nodes/{2.i7a08Ot_.js → 2.CpbaFjzu.js} +1 -1
  16. package/dist/_app/immutable/nodes/{3.BkiMJt_d.js → 3.CSxUon-f.js} +1 -1
  17. package/dist/_app/immutable/nodes/{4.DIgBotc9.js → 4.C2ryyNeM.js} +1 -1
  18. package/dist/_app/version.json +1 -1
  19. package/dist/cli/commands/ui.js +265 -49
  20. package/dist/cli/server/index.js +265 -49
  21. package/dist/cli/server/server.js +265 -49
  22. package/dist/cli/server/spreadsheetRoutes.js +282 -59
  23. package/dist/cli/server/websocketServer.js +265 -49
  24. package/dist/index.html +10 -10
  25. package/dist/index.js +265 -49
  26. package/package.json +3 -3
  27. package/src/lib/components/dialogs/ExportColumnDialog.svelte +163 -0
  28. package/src/lib/components/dialogs/index.ts +4 -0
  29. package/src/routes/+layout.svelte +67 -5
  30. package/src/routes/control/[id]/+page.svelte +0 -1
  31. package/dist/_app/immutable/assets/0.D6CB7gA7.css +0 -1
  32. package/dist/_app/immutable/chunks/C3_XvBfw.js +0 -1
  33. package/dist/_app/immutable/entry/start.sVNR7Hbg.js +0 -1
  34. package/dist/_app/immutable/nodes/0.BIF2SnDb.js +0 -1
@@ -7,7 +7,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
7
  import { glob } from "glob";
8
8
  import * as yaml4 from "js-yaml";
9
9
  import multer from "multer";
10
- import { dirname, join, relative } from "path";
10
+ import { dirname, join as join2, relative } from "path";
11
11
 
12
12
  // cli/utils/debug.ts
13
13
  var debugEnabled = false;
@@ -17,6 +17,9 @@ function debug(...args) {
17
17
  }
18
18
  }
19
19
 
20
+ // cli/server/serverState.ts
21
+ import { join } from "path";
22
+
20
23
  // cli/server/infrastructure/fileStore.ts
21
24
  import * as yaml2 from "js-yaml";
22
25
 
@@ -37,6 +40,10 @@ function getServerState() {
37
40
  }
38
41
  return serverState;
39
42
  }
43
+ function getCurrentControlSetPath() {
44
+ const state = getServerState();
45
+ return state.currentSubdir === "." ? state.CONTROL_SET_DIR : join(state.CONTROL_SET_DIR, state.currentSubdir);
46
+ }
40
47
 
41
48
  // cli/server/spreadsheetRoutes.ts
42
49
  var router = express.Router();
@@ -55,7 +62,7 @@ async function scanControlSets() {
55
62
  maxDepth: 5
56
63
  });
57
64
  const controlSets = files.map((file) => {
58
- const fullPath = join(baseDir, file);
65
+ const fullPath = join2(baseDir, file);
59
66
  const dirPath = dirname(fullPath);
60
67
  const relativePath = relative(baseDir, dirPath) || ".";
61
68
  try {
@@ -225,7 +232,7 @@ router.post("/import-spreadsheet", upload.single("file"), async (req, res) => {
225
232
  }
226
233
  const state = getServerState();
227
234
  const folderName = toKebabCase(controlSetName || "imported-controls");
228
- const baseDir = join(state.CONTROL_SET_DIR || process.cwd(), folderName);
235
+ const baseDir = join2(state.CONTROL_SET_DIR || process.cwd(), folderName);
229
236
  if (!existsSync(baseDir)) {
230
237
  mkdirSync(baseDir, { recursive: true });
231
238
  }
@@ -348,12 +355,12 @@ router.post("/import-spreadsheet", upload.single("file"), async (req, res) => {
348
355
  families: uniqueFamilies,
349
356
  fieldSchema
350
357
  };
351
- writeFileSync(join(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
352
- const controlsDir = join(baseDir, "controls");
353
- const mappingsDir = join(baseDir, "mappings");
358
+ writeFileSync(join2(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
359
+ const controlsDir = join2(baseDir, "controls");
360
+ const mappingsDir = join2(baseDir, "mappings");
354
361
  families.forEach((familyControls, family) => {
355
- const familyDir = join(controlsDir, family);
356
- const familyMappingsDir = join(mappingsDir, family);
362
+ const familyDir = join2(controlsDir, family);
363
+ const familyMappingsDir = join2(mappingsDir, family);
357
364
  if (!existsSync(familyDir)) {
358
365
  mkdirSync(familyDir, { recursive: true });
359
366
  }
@@ -368,9 +375,9 @@ router.post("/import-spreadsheet", upload.single("file"), async (req, res) => {
368
375
  }
369
376
  const controlIdStr = String(controlId).slice(0, 50);
370
377
  const fileName2 = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}.yaml`;
371
- const filePath = join(familyDir, fileName2);
378
+ const filePath = join2(familyDir, fileName2);
372
379
  const mappingFileName = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`;
373
- const mappingFilePath = join(familyMappingsDir, mappingFileName);
380
+ const mappingFilePath = join2(familyMappingsDir, mappingFileName);
374
381
  const filteredControl = {};
375
382
  const mappingData = {
376
383
  control_id: controlIdStr,
@@ -508,6 +515,7 @@ function extractFamilyFromControlId(controlId) {
508
515
  router.get("/export-controls", async (req, res) => {
509
516
  try {
510
517
  const format = req.query.format || "csv";
518
+ const mappingsColumn = req.query.mappingsColumn || "Mappings";
511
519
  const state = getServerState();
512
520
  const fileStore = state.fileStore;
513
521
  if (!fileStore) {
@@ -517,7 +525,8 @@ router.get("/export-controls", async (req, res) => {
517
525
  const mappings = await fileStore.loadMappings();
518
526
  let metadata = {};
519
527
  try {
520
- const metadataPath = join(state.CONTROL_SET_DIR, "lula.yaml");
528
+ const controlSetPath = getCurrentControlSetPath();
529
+ const metadataPath = join2(controlSetPath, "lula.yaml");
521
530
  if (existsSync(metadataPath)) {
522
531
  const metadataContent = readFileSync(metadataPath, "utf8");
523
532
  metadata = yaml4.load(metadataContent);
@@ -545,10 +554,10 @@ router.get("/export-controls", async (req, res) => {
545
554
  debug(`Exporting ${controlsWithMappings.length} controls as ${format}`);
546
555
  switch (format.toLowerCase()) {
547
556
  case "csv":
548
- return exportAsCSV(controlsWithMappings, metadata, res);
557
+ return exportAsCSV(controlsWithMappings, metadata, mappingsColumn, res);
549
558
  case "excel":
550
559
  case "xlsx":
551
- return await exportAsExcel(controlsWithMappings, metadata, res);
560
+ return await exportAsExcel(controlsWithMappings, metadata, mappingsColumn, res);
552
561
  case "json":
553
562
  return exportAsJSON(controlsWithMappings, metadata, res);
554
563
  default:
@@ -559,7 +568,10 @@ router.get("/export-controls", async (req, res) => {
559
568
  res.status(500).json({ error: error.message });
560
569
  }
561
570
  });
562
- function exportAsCSV(controls, metadata, res) {
571
+ function exportAsCSV(controls, metadata, mappingsColumn, res) {
572
+ return exportAsCSVWithMapping(controls, metadata, { mappings: mappingsColumn }, res);
573
+ }
574
+ function exportAsCSVWithMapping(controls, metadata, columnMappings, res) {
563
575
  const fieldSchema = metadata?.fieldSchema?.fields || {};
564
576
  const controlIdField = metadata?.control_id_field || "id";
565
577
  const allFields = /* @__PURE__ */ new Set();
@@ -567,49 +579,91 @@ function exportAsCSV(controls, metadata, res) {
567
579
  Object.keys(control).forEach((key) => allFields.add(key));
568
580
  });
569
581
  const fieldMapping = [];
582
+ const usedDisplayNames = /* @__PURE__ */ new Set();
570
583
  if (allFields.has(controlIdField)) {
571
584
  const idSchema = fieldSchema[controlIdField];
572
- fieldMapping.push({
573
- fieldName: controlIdField,
574
- displayName: idSchema?.original_name || "Control ID"
575
- });
585
+ const displayName = idSchema?.original_name || "Control ID";
586
+ let isMappingColumn = false;
587
+ if (columnMappings["mappings"]) {
588
+ const targetDisplayName = columnMappings["mappings"];
589
+ if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
590
+ isMappingColumn = true;
591
+ }
592
+ }
593
+ fieldMapping.push({ fieldName: controlIdField, displayName, isMappingColumn });
594
+ usedDisplayNames.add(displayName.toLowerCase());
576
595
  allFields.delete(controlIdField);
577
596
  } else if (allFields.has("id")) {
578
- fieldMapping.push({
579
- fieldName: "id",
580
- displayName: "Control ID"
581
- });
597
+ let isMappingColumn = false;
598
+ const displayName = "Control ID";
599
+ if (columnMappings["mappings"]) {
600
+ const targetDisplayName = columnMappings["mappings"];
601
+ if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
602
+ isMappingColumn = true;
603
+ }
604
+ }
605
+ fieldMapping.push({ fieldName: "id", displayName, isMappingColumn });
606
+ usedDisplayNames.add("control id");
582
607
  allFields.delete("id");
583
608
  }
584
609
  if (allFields.has("family")) {
585
610
  const familySchema = fieldSchema["family"];
586
- fieldMapping.push({
587
- fieldName: "family",
588
- displayName: familySchema?.original_name || "Family"
589
- });
611
+ const displayName = familySchema?.original_name || "Family";
612
+ fieldMapping.push({ fieldName: "family", displayName });
613
+ usedDisplayNames.add(displayName.toLowerCase());
590
614
  allFields.delete("family");
591
615
  }
592
616
  Array.from(allFields).filter((field) => field !== "mappings" && field !== "mappings_count").sort().forEach((field) => {
593
617
  const schema = fieldSchema[field];
594
- const displayName = schema?.original_name || field.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
595
- fieldMapping.push({ fieldName: field, displayName });
618
+ const defaultDisplayName = schema?.original_name || field.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
619
+ let isMappingColumn = false;
620
+ let finalDisplayName = defaultDisplayName;
621
+ if (columnMappings["mappings"]) {
622
+ const targetDisplayName = columnMappings["mappings"];
623
+ if (defaultDisplayName.toLowerCase() === targetDisplayName.toLowerCase()) {
624
+ isMappingColumn = true;
625
+ finalDisplayName = targetDisplayName;
626
+ }
627
+ }
628
+ if (!usedDisplayNames.has(finalDisplayName.toLowerCase())) {
629
+ fieldMapping.push({
630
+ fieldName: field,
631
+ displayName: finalDisplayName,
632
+ isMappingColumn
633
+ });
634
+ usedDisplayNames.add(finalDisplayName.toLowerCase());
635
+ }
596
636
  });
597
- if (allFields.has("mappings_count")) {
598
- fieldMapping.push({ fieldName: "mappings_count", displayName: "Mappings Count" });
599
- }
600
- if (allFields.has("mappings")) {
637
+ if (allFields.has("mappings") && (!columnMappings["mappings"] || columnMappings["mappings"] === "Mappings")) {
601
638
  fieldMapping.push({ fieldName: "mappings", displayName: "Mappings" });
602
639
  }
603
640
  const csvRows = [];
604
641
  csvRows.push(fieldMapping.map((field) => `"${field.displayName}"`).join(","));
605
642
  controls.forEach((control) => {
606
- const row = fieldMapping.map(({ fieldName }) => {
607
- const value = control[fieldName];
643
+ const row = fieldMapping.map(({ fieldName, isMappingColumn }) => {
644
+ let value;
645
+ if (isMappingColumn) {
646
+ const mappingsValue = control["mappings"];
647
+ if (Array.isArray(mappingsValue) && mappingsValue.length > 0) {
648
+ const mappingsStr = mappingsValue.map((m) => m.description || m.justification || "").filter((desc) => desc && desc.trim() !== "").join("\n");
649
+ if (mappingsStr.trim() !== "") {
650
+ value = mappingsStr;
651
+ } else {
652
+ value = control[fieldName];
653
+ }
654
+ } else {
655
+ value = control[fieldName];
656
+ }
657
+ } else {
658
+ value = control[fieldName];
659
+ }
608
660
  if (value === void 0 || value === null) return '""';
609
661
  if (fieldName === "mappings" && Array.isArray(value)) {
610
- const mappingsStr = value.map(
611
- (m) => `${m.status}: ${m.description.substring(0, 50)}${m.description.length > 50 ? "..." : ""}`
612
- ).join("; ");
662
+ const mappingsStr = value.map((m) => {
663
+ const justification = m.description || m.justification || "";
664
+ const status = m.status || "Unknown";
665
+ return justification.trim() !== "" ? justification : `[${status}]`;
666
+ }).join("\n");
613
667
  return `"${mappingsStr.replace(/"/g, '""')}"`;
614
668
  }
615
669
  if (Array.isArray(value)) return `"${value.join("; ").replace(/"/g, '""')}"`;
@@ -624,32 +678,101 @@ function exportAsCSV(controls, metadata, res) {
624
678
  res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
625
679
  res.send(csvContent);
626
680
  }
627
- async function exportAsExcel(controls, metadata, res) {
681
+ async function exportAsExcel(controls, metadata, mappingsColumn, res) {
682
+ return await exportAsExcelWithMapping(controls, metadata, { mappings: mappingsColumn }, res);
683
+ }
684
+ async function exportAsExcelWithMapping(controls, metadata, columnMappings, res) {
628
685
  const fieldSchema = metadata?.fieldSchema?.fields || {};
629
686
  const controlIdField = metadata?.control_id_field || "id";
687
+ const allFields = /* @__PURE__ */ new Set();
688
+ controls.forEach((control) => {
689
+ Object.keys(control).forEach((key) => allFields.add(key));
690
+ });
691
+ const fieldMapping = [];
692
+ const usedDisplayNames = /* @__PURE__ */ new Set();
693
+ if (allFields.has(controlIdField)) {
694
+ const idSchema = fieldSchema[controlIdField];
695
+ const displayName = idSchema?.original_name || "Control ID";
696
+ let isMappingColumn = false;
697
+ if (columnMappings["mappings"]) {
698
+ const targetDisplayName = columnMappings["mappings"];
699
+ if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
700
+ isMappingColumn = true;
701
+ }
702
+ }
703
+ fieldMapping.push({ fieldName: controlIdField, displayName, isMappingColumn });
704
+ usedDisplayNames.add(displayName.toLowerCase());
705
+ allFields.delete(controlIdField);
706
+ } else if (allFields.has("id")) {
707
+ let isMappingColumn = false;
708
+ const displayName = "Control ID";
709
+ if (columnMappings["mappings"]) {
710
+ const targetDisplayName = columnMappings["mappings"];
711
+ if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
712
+ isMappingColumn = true;
713
+ }
714
+ }
715
+ fieldMapping.push({ fieldName: "id", displayName, isMappingColumn });
716
+ usedDisplayNames.add("control id");
717
+ allFields.delete("id");
718
+ }
719
+ if (allFields.has("family")) {
720
+ const familySchema = fieldSchema["family"];
721
+ const displayName = familySchema?.original_name || "Family";
722
+ fieldMapping.push({ fieldName: "family", displayName });
723
+ usedDisplayNames.add(displayName.toLowerCase());
724
+ allFields.delete("family");
725
+ }
726
+ Array.from(allFields).filter((field) => field !== "mappings" && field !== "mappings_count").sort().forEach((field) => {
727
+ const schema = fieldSchema[field];
728
+ const defaultDisplayName = schema?.original_name || field.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
729
+ let isMappingColumn = false;
730
+ let finalDisplayName = defaultDisplayName;
731
+ if (columnMappings["mappings"]) {
732
+ const targetDisplayName = columnMappings["mappings"];
733
+ if (defaultDisplayName.toLowerCase() === targetDisplayName.toLowerCase()) {
734
+ isMappingColumn = true;
735
+ finalDisplayName = targetDisplayName;
736
+ }
737
+ }
738
+ if (!usedDisplayNames.has(finalDisplayName.toLowerCase())) {
739
+ fieldMapping.push({
740
+ fieldName: field,
741
+ displayName: finalDisplayName,
742
+ isMappingColumn
743
+ });
744
+ usedDisplayNames.add(finalDisplayName.toLowerCase());
745
+ }
746
+ });
747
+ if (allFields.has("mappings") && (!columnMappings["mappings"] || columnMappings["mappings"] === "Mappings")) {
748
+ fieldMapping.push({ fieldName: "mappings", displayName: "Mappings" });
749
+ }
630
750
  const worksheetData = controls.map((control) => {
631
751
  const exportControl = {};
632
- if (control[controlIdField]) {
633
- const idSchema = fieldSchema[controlIdField];
634
- const idDisplayName = idSchema?.original_name || "Control ID";
635
- exportControl[idDisplayName] = control[controlIdField];
636
- } else if (control.id) {
637
- exportControl["Control ID"] = control.id;
638
- }
639
- if (control.family) {
640
- const familySchema = fieldSchema["family"];
641
- const familyDisplayName = familySchema?.original_name || "Family";
642
- exportControl[familyDisplayName] = control.family;
643
- }
644
- Object.keys(control).forEach((key) => {
645
- if (key === controlIdField || key === "id" || key === "family") return;
646
- const schema = fieldSchema[key];
647
- const displayName = schema?.original_name || (key === "mappings_count" ? "Mappings Count" : key === "mappings" ? "Mappings" : key.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()));
648
- const value = control[key];
649
- if (key === "mappings" && Array.isArray(value)) {
650
- exportControl[displayName] = value.map(
651
- (m) => `${m.status}: ${m.description.substring(0, 100)}${m.description.length > 100 ? "..." : ""}`
652
- ).join("\n");
752
+ fieldMapping.forEach(({ fieldName, displayName, isMappingColumn }) => {
753
+ let value;
754
+ if (isMappingColumn) {
755
+ const mappingsValue = control["mappings"];
756
+ if (Array.isArray(mappingsValue) && mappingsValue.length > 0) {
757
+ const mappingsStr = mappingsValue.map((m) => m.description || m.justification || "").filter((desc) => desc && desc.trim() !== "").join("\n");
758
+ if (mappingsStr.trim() !== "") {
759
+ value = mappingsStr;
760
+ } else {
761
+ value = control[fieldName];
762
+ }
763
+ } else {
764
+ value = control[fieldName];
765
+ }
766
+ } else {
767
+ value = control[fieldName];
768
+ }
769
+ if (fieldName === "mappings" && Array.isArray(value)) {
770
+ const mappingsStr = value.map((m) => {
771
+ const justification = m.description || m.justification || "";
772
+ const status = m.status || "Unknown";
773
+ return justification.trim() !== "" ? justification : `[${status}]`;
774
+ }).join("\n");
775
+ exportControl[displayName] = mappingsStr;
653
776
  } else if (Array.isArray(value)) {
654
777
  exportControl[displayName] = value.join("; ");
655
778
  } else if (typeof value === "object" && value !== null) {
@@ -717,6 +840,106 @@ function exportAsJSON(controls, metadata, res) {
717
840
  res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
718
841
  res.json(exportData);
719
842
  }
843
+ router.get("/export-column-headers", async (req, res) => {
844
+ try {
845
+ let metadata = {};
846
+ try {
847
+ const controlSetPath = getCurrentControlSetPath();
848
+ const metadataPath = join2(controlSetPath, "lula.yaml");
849
+ if (existsSync(metadataPath)) {
850
+ const metadataContent = readFileSync(metadataPath, "utf8");
851
+ metadata = yaml4.load(metadataContent);
852
+ } else {
853
+ return res.status(404).json({ error: `No lula.yaml file found in control set path: ${controlSetPath}` });
854
+ }
855
+ } catch {
856
+ return res.status(500).json({ error: "Failed to read lula.yaml file" });
857
+ }
858
+ const fieldSchema = metadata?.fieldSchema?.fields || {};
859
+ const controlIdField = metadata?.control_id_field || "id";
860
+ const columnHeaders = [];
861
+ if (fieldSchema[controlIdField]) {
862
+ const idSchema = fieldSchema[controlIdField];
863
+ const displayName = idSchema?.original_name || "Control ID";
864
+ columnHeaders.push({
865
+ value: displayName,
866
+ label: displayName
867
+ });
868
+ } else {
869
+ columnHeaders.push({ value: "Control ID", label: "Control ID" });
870
+ }
871
+ if (fieldSchema["family"]) {
872
+ const familySchema = fieldSchema["family"];
873
+ const displayName = familySchema?.original_name || "Family";
874
+ columnHeaders.push({
875
+ value: displayName,
876
+ label: displayName
877
+ });
878
+ }
879
+ Object.entries(fieldSchema).forEach(([fieldName, schema]) => {
880
+ if (fieldName === controlIdField || fieldName === "family" || fieldName === "mappings" || fieldName === "mappings_count") {
881
+ return;
882
+ }
883
+ const displayName = schema?.original_name || fieldName.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
884
+ columnHeaders.push({
885
+ value: displayName,
886
+ label: displayName
887
+ });
888
+ });
889
+ columnHeaders.push({ value: "Mappings", label: "Mappings (Default)" });
890
+ res.json({
891
+ columnHeaders,
892
+ defaultColumn: "Mappings"
893
+ });
894
+ } catch (error) {
895
+ res.status(500).json({ error: error.message });
896
+ }
897
+ });
898
+ router.post("/export-csv", async (req, res) => {
899
+ try {
900
+ const { format = "csv", _columns = [], columnMappings = {} } = req.body;
901
+ const state = getServerState();
902
+ const fileStore = state.fileStore;
903
+ if (!fileStore) {
904
+ return res.status(500).json({ error: "No control set loaded" });
905
+ }
906
+ const controls = await fileStore.loadAllControls();
907
+ const mappings = await fileStore.loadMappings();
908
+ let metadata = {};
909
+ try {
910
+ const controlSetPath = getCurrentControlSetPath();
911
+ const metadataPath = join2(controlSetPath, "lula.yaml");
912
+ if (existsSync(metadataPath)) {
913
+ const metadataContent = readFileSync(metadataPath, "utf8");
914
+ metadata = yaml4.load(metadataContent);
915
+ }
916
+ } catch (err) {
917
+ debug("Could not load metadata:", err);
918
+ }
919
+ if (!controls || controls.length === 0) {
920
+ return res.status(404).json({ error: "No controls found" });
921
+ }
922
+ const controlIdField = metadata?.control_id_field || "id";
923
+ const controlsWithMappings = controls.map((control) => {
924
+ const controlId = control[controlIdField] || control.id;
925
+ const controlMappings = mappings.filter((m) => m.control_id === controlId);
926
+ return {
927
+ ...control,
928
+ mappings_count: controlMappings.length,
929
+ mappings: controlMappings.map((m) => ({
930
+ uuid: m.uuid,
931
+ status: m.status,
932
+ description: m.justification || ""
933
+ }))
934
+ };
935
+ });
936
+ debug(`Exporting ${controlsWithMappings.length} controls as ${format} with column mappings`);
937
+ return exportAsCSVWithMapping(controlsWithMappings, metadata, columnMappings, res);
938
+ } catch (error) {
939
+ console.error("Export error:", error);
940
+ res.status(500).json({ error: error.message });
941
+ }
942
+ });
720
943
  router.post("/parse-excel", upload.single("file"), async (req, res) => {
721
944
  try {
722
945
  if (!req.file) {