lula2 0.4.0 → 0.5.0-nightly.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_app/immutable/assets/0.Dfpe5goI.css +1 -0
- package/dist/_app/immutable/chunks/{d2wclEM1.js → B3DV5AB9.js} +1 -1
- package/dist/_app/immutable/chunks/{DTNJlR4l.js → BBW9xKix.js} +19 -19
- package/dist/_app/immutable/chunks/{B8sFn9qB.js → BIY1u1I9.js} +1 -1
- package/dist/_app/immutable/chunks/{DPYPcVpy.js → BN4ish10.js} +1 -1
- package/dist/_app/immutable/chunks/{D6AXzSy_.js → BtwnwKFn.js} +1 -1
- package/dist/_app/immutable/chunks/{CgOk0Ct0.js → C2de20AA.js} +3 -3
- package/dist/_app/immutable/chunks/Cyp5c8fY.js +1 -0
- package/dist/_app/immutable/chunks/{DL7cUWpq.js → D6NghQtU.js} +1 -1
- package/dist/_app/immutable/chunks/{AMsv4DTs.js → DBN1r830.js} +1 -1
- package/dist/_app/immutable/entry/{app.DCeR7c2L.js → app.ChNLcnhL.js} +2 -2
- package/dist/_app/immutable/entry/start.CuWGVoVi.js +1 -0
- package/dist/_app/immutable/nodes/0.DnmE0r6A.js +2 -0
- package/dist/_app/immutable/nodes/{1.DZuIkAUv.js → 1.CPF_x4ZW.js} +1 -1
- package/dist/_app/immutable/nodes/{2.CaMZrOwr.js → 2.dJ7_0KZr.js} +1 -1
- package/dist/_app/immutable/nodes/{3.BCB0DxLi.js → 3.BhR2ddch.js} +1 -1
- package/dist/_app/immutable/nodes/{4.BlOrY_m9.js → 4.Djg06sYy.js} +1 -1
- package/dist/_app/version.json +1 -1
- package/dist/cli/commands/ui.js +265 -49
- package/dist/cli/server/index.js +265 -49
- package/dist/cli/server/server.js +265 -49
- package/dist/cli/server/spreadsheetRoutes.js +282 -59
- package/dist/cli/server/websocketServer.js +265 -49
- package/dist/index.html +10 -10
- package/dist/index.js +265 -49
- package/package.json +22 -23
- package/src/lib/components/dialogs/ExportColumnDialog.svelte +163 -0
- package/src/lib/components/dialogs/index.ts +4 -0
- package/src/routes/+layout.svelte +67 -5
- package/src/routes/control/[id]/+page.svelte +0 -1
- package/dist/_app/immutable/assets/0.D6CB7gA7.css +0 -1
- package/dist/_app/immutable/chunks/CBRQrpza.js +0 -1
- package/dist/_app/immutable/entry/start.Ca2qbK08.js +0 -1
- package/dist/_app/immutable/nodes/0.B2WQMmWy.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 =
|
|
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 =
|
|
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(
|
|
352
|
-
const controlsDir =
|
|
353
|
-
const mappingsDir =
|
|
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 =
|
|
356
|
-
const familyMappingsDir =
|
|
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 =
|
|
378
|
+
const filePath = join2(familyDir, fileName2);
|
|
372
379
|
const mappingFileName = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`;
|
|
373
|
-
const mappingFilePath =
|
|
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
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
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
|
|
595
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
612
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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) {
|