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
package/dist/index.js
CHANGED
|
@@ -2986,7 +2986,10 @@ function extractFamilyFromControlId(controlId) {
|
|
|
2986
2986
|
}
|
|
2987
2987
|
return controlId.substring(0, 2).toUpperCase();
|
|
2988
2988
|
}
|
|
2989
|
-
function exportAsCSV(controls, metadata, res) {
|
|
2989
|
+
function exportAsCSV(controls, metadata, mappingsColumn, res) {
|
|
2990
|
+
return exportAsCSVWithMapping(controls, metadata, { mappings: mappingsColumn }, res);
|
|
2991
|
+
}
|
|
2992
|
+
function exportAsCSVWithMapping(controls, metadata, columnMappings, res) {
|
|
2990
2993
|
const fieldSchema = metadata?.fieldSchema?.fields || {};
|
|
2991
2994
|
const controlIdField = metadata?.control_id_field || "id";
|
|
2992
2995
|
const allFields = /* @__PURE__ */ new Set();
|
|
@@ -2994,49 +2997,91 @@ function exportAsCSV(controls, metadata, res) {
|
|
|
2994
2997
|
Object.keys(control).forEach((key) => allFields.add(key));
|
|
2995
2998
|
});
|
|
2996
2999
|
const fieldMapping = [];
|
|
3000
|
+
const usedDisplayNames = /* @__PURE__ */ new Set();
|
|
2997
3001
|
if (allFields.has(controlIdField)) {
|
|
2998
3002
|
const idSchema = fieldSchema[controlIdField];
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
+
const displayName = idSchema?.original_name || "Control ID";
|
|
3004
|
+
let isMappingColumn = false;
|
|
3005
|
+
if (columnMappings["mappings"]) {
|
|
3006
|
+
const targetDisplayName = columnMappings["mappings"];
|
|
3007
|
+
if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
|
|
3008
|
+
isMappingColumn = true;
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
fieldMapping.push({ fieldName: controlIdField, displayName, isMappingColumn });
|
|
3012
|
+
usedDisplayNames.add(displayName.toLowerCase());
|
|
3003
3013
|
allFields.delete(controlIdField);
|
|
3004
3014
|
} else if (allFields.has("id")) {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3015
|
+
let isMappingColumn = false;
|
|
3016
|
+
const displayName = "Control ID";
|
|
3017
|
+
if (columnMappings["mappings"]) {
|
|
3018
|
+
const targetDisplayName = columnMappings["mappings"];
|
|
3019
|
+
if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
|
|
3020
|
+
isMappingColumn = true;
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
fieldMapping.push({ fieldName: "id", displayName, isMappingColumn });
|
|
3024
|
+
usedDisplayNames.add("control id");
|
|
3009
3025
|
allFields.delete("id");
|
|
3010
3026
|
}
|
|
3011
3027
|
if (allFields.has("family")) {
|
|
3012
3028
|
const familySchema = fieldSchema["family"];
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
});
|
|
3029
|
+
const displayName = familySchema?.original_name || "Family";
|
|
3030
|
+
fieldMapping.push({ fieldName: "family", displayName });
|
|
3031
|
+
usedDisplayNames.add(displayName.toLowerCase());
|
|
3017
3032
|
allFields.delete("family");
|
|
3018
3033
|
}
|
|
3019
3034
|
Array.from(allFields).filter((field) => field !== "mappings" && field !== "mappings_count").sort().forEach((field) => {
|
|
3020
3035
|
const schema = fieldSchema[field];
|
|
3021
|
-
const
|
|
3022
|
-
|
|
3036
|
+
const defaultDisplayName = schema?.original_name || field.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
3037
|
+
let isMappingColumn = false;
|
|
3038
|
+
let finalDisplayName = defaultDisplayName;
|
|
3039
|
+
if (columnMappings["mappings"]) {
|
|
3040
|
+
const targetDisplayName = columnMappings["mappings"];
|
|
3041
|
+
if (defaultDisplayName.toLowerCase() === targetDisplayName.toLowerCase()) {
|
|
3042
|
+
isMappingColumn = true;
|
|
3043
|
+
finalDisplayName = targetDisplayName;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
if (!usedDisplayNames.has(finalDisplayName.toLowerCase())) {
|
|
3047
|
+
fieldMapping.push({
|
|
3048
|
+
fieldName: field,
|
|
3049
|
+
displayName: finalDisplayName,
|
|
3050
|
+
isMappingColumn
|
|
3051
|
+
});
|
|
3052
|
+
usedDisplayNames.add(finalDisplayName.toLowerCase());
|
|
3053
|
+
}
|
|
3023
3054
|
});
|
|
3024
|
-
if (allFields.has("
|
|
3025
|
-
fieldMapping.push({ fieldName: "mappings_count", displayName: "Mappings Count" });
|
|
3026
|
-
}
|
|
3027
|
-
if (allFields.has("mappings")) {
|
|
3055
|
+
if (allFields.has("mappings") && (!columnMappings["mappings"] || columnMappings["mappings"] === "Mappings")) {
|
|
3028
3056
|
fieldMapping.push({ fieldName: "mappings", displayName: "Mappings" });
|
|
3029
3057
|
}
|
|
3030
3058
|
const csvRows = [];
|
|
3031
3059
|
csvRows.push(fieldMapping.map((field) => `"${field.displayName}"`).join(","));
|
|
3032
3060
|
controls.forEach((control) => {
|
|
3033
|
-
const row = fieldMapping.map(({ fieldName }) => {
|
|
3034
|
-
|
|
3061
|
+
const row = fieldMapping.map(({ fieldName, isMappingColumn }) => {
|
|
3062
|
+
let value;
|
|
3063
|
+
if (isMappingColumn) {
|
|
3064
|
+
const mappingsValue = control["mappings"];
|
|
3065
|
+
if (Array.isArray(mappingsValue) && mappingsValue.length > 0) {
|
|
3066
|
+
const mappingsStr = mappingsValue.map((m) => m.description || m.justification || "").filter((desc) => desc && desc.trim() !== "").join("\n");
|
|
3067
|
+
if (mappingsStr.trim() !== "") {
|
|
3068
|
+
value = mappingsStr;
|
|
3069
|
+
} else {
|
|
3070
|
+
value = control[fieldName];
|
|
3071
|
+
}
|
|
3072
|
+
} else {
|
|
3073
|
+
value = control[fieldName];
|
|
3074
|
+
}
|
|
3075
|
+
} else {
|
|
3076
|
+
value = control[fieldName];
|
|
3077
|
+
}
|
|
3035
3078
|
if (value === void 0 || value === null) return '""';
|
|
3036
3079
|
if (fieldName === "mappings" && Array.isArray(value)) {
|
|
3037
|
-
const mappingsStr = value.map(
|
|
3038
|
-
|
|
3039
|
-
|
|
3080
|
+
const mappingsStr = value.map((m) => {
|
|
3081
|
+
const justification = m.description || m.justification || "";
|
|
3082
|
+
const status = m.status || "Unknown";
|
|
3083
|
+
return justification.trim() !== "" ? justification : `[${status}]`;
|
|
3084
|
+
}).join("\n");
|
|
3040
3085
|
return `"${mappingsStr.replace(/"/g, '""')}"`;
|
|
3041
3086
|
}
|
|
3042
3087
|
if (Array.isArray(value)) return `"${value.join("; ").replace(/"/g, '""')}"`;
|
|
@@ -3051,32 +3096,101 @@ function exportAsCSV(controls, metadata, res) {
|
|
|
3051
3096
|
res.setHeader("Content-Disposition", `attachment; filename="${fileName}"`);
|
|
3052
3097
|
res.send(csvContent);
|
|
3053
3098
|
}
|
|
3054
|
-
async function exportAsExcel(controls, metadata, res) {
|
|
3099
|
+
async function exportAsExcel(controls, metadata, mappingsColumn, res) {
|
|
3100
|
+
return await exportAsExcelWithMapping(controls, metadata, { mappings: mappingsColumn }, res);
|
|
3101
|
+
}
|
|
3102
|
+
async function exportAsExcelWithMapping(controls, metadata, columnMappings, res) {
|
|
3055
3103
|
const fieldSchema = metadata?.fieldSchema?.fields || {};
|
|
3056
3104
|
const controlIdField = metadata?.control_id_field || "id";
|
|
3057
|
-
const
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3105
|
+
const allFields = /* @__PURE__ */ new Set();
|
|
3106
|
+
controls.forEach((control) => {
|
|
3107
|
+
Object.keys(control).forEach((key) => allFields.add(key));
|
|
3108
|
+
});
|
|
3109
|
+
const fieldMapping = [];
|
|
3110
|
+
const usedDisplayNames = /* @__PURE__ */ new Set();
|
|
3111
|
+
if (allFields.has(controlIdField)) {
|
|
3112
|
+
const idSchema = fieldSchema[controlIdField];
|
|
3113
|
+
const displayName = idSchema?.original_name || "Control ID";
|
|
3114
|
+
let isMappingColumn = false;
|
|
3115
|
+
if (columnMappings["mappings"]) {
|
|
3116
|
+
const targetDisplayName = columnMappings["mappings"];
|
|
3117
|
+
if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
|
|
3118
|
+
isMappingColumn = true;
|
|
3119
|
+
}
|
|
3065
3120
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3121
|
+
fieldMapping.push({ fieldName: controlIdField, displayName, isMappingColumn });
|
|
3122
|
+
usedDisplayNames.add(displayName.toLowerCase());
|
|
3123
|
+
allFields.delete(controlIdField);
|
|
3124
|
+
} else if (allFields.has("id")) {
|
|
3125
|
+
let isMappingColumn = false;
|
|
3126
|
+
const displayName = "Control ID";
|
|
3127
|
+
if (columnMappings["mappings"]) {
|
|
3128
|
+
const targetDisplayName = columnMappings["mappings"];
|
|
3129
|
+
if (displayName.toLowerCase() === targetDisplayName.toLowerCase()) {
|
|
3130
|
+
isMappingColumn = true;
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
fieldMapping.push({ fieldName: "id", displayName, isMappingColumn });
|
|
3134
|
+
usedDisplayNames.add("control id");
|
|
3135
|
+
allFields.delete("id");
|
|
3136
|
+
}
|
|
3137
|
+
if (allFields.has("family")) {
|
|
3138
|
+
const familySchema = fieldSchema["family"];
|
|
3139
|
+
const displayName = familySchema?.original_name || "Family";
|
|
3140
|
+
fieldMapping.push({ fieldName: "family", displayName });
|
|
3141
|
+
usedDisplayNames.add(displayName.toLowerCase());
|
|
3142
|
+
allFields.delete("family");
|
|
3143
|
+
}
|
|
3144
|
+
Array.from(allFields).filter((field) => field !== "mappings" && field !== "mappings_count").sort().forEach((field) => {
|
|
3145
|
+
const schema = fieldSchema[field];
|
|
3146
|
+
const defaultDisplayName = schema?.original_name || field.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
3147
|
+
let isMappingColumn = false;
|
|
3148
|
+
let finalDisplayName = defaultDisplayName;
|
|
3149
|
+
if (columnMappings["mappings"]) {
|
|
3150
|
+
const targetDisplayName = columnMappings["mappings"];
|
|
3151
|
+
if (defaultDisplayName.toLowerCase() === targetDisplayName.toLowerCase()) {
|
|
3152
|
+
isMappingColumn = true;
|
|
3153
|
+
finalDisplayName = targetDisplayName;
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
if (!usedDisplayNames.has(finalDisplayName.toLowerCase())) {
|
|
3157
|
+
fieldMapping.push({
|
|
3158
|
+
fieldName: field,
|
|
3159
|
+
displayName: finalDisplayName,
|
|
3160
|
+
isMappingColumn
|
|
3161
|
+
});
|
|
3162
|
+
usedDisplayNames.add(finalDisplayName.toLowerCase());
|
|
3070
3163
|
}
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3164
|
+
});
|
|
3165
|
+
if (allFields.has("mappings") && (!columnMappings["mappings"] || columnMappings["mappings"] === "Mappings")) {
|
|
3166
|
+
fieldMapping.push({ fieldName: "mappings", displayName: "Mappings" });
|
|
3167
|
+
}
|
|
3168
|
+
const worksheetData = controls.map((control) => {
|
|
3169
|
+
const exportControl = {};
|
|
3170
|
+
fieldMapping.forEach(({ fieldName, displayName, isMappingColumn }) => {
|
|
3171
|
+
let value;
|
|
3172
|
+
if (isMappingColumn) {
|
|
3173
|
+
const mappingsValue = control["mappings"];
|
|
3174
|
+
if (Array.isArray(mappingsValue) && mappingsValue.length > 0) {
|
|
3175
|
+
const mappingsStr = mappingsValue.map((m) => m.description || m.justification || "").filter((desc) => desc && desc.trim() !== "").join("\n");
|
|
3176
|
+
if (mappingsStr.trim() !== "") {
|
|
3177
|
+
value = mappingsStr;
|
|
3178
|
+
} else {
|
|
3179
|
+
value = control[fieldName];
|
|
3180
|
+
}
|
|
3181
|
+
} else {
|
|
3182
|
+
value = control[fieldName];
|
|
3183
|
+
}
|
|
3184
|
+
} else {
|
|
3185
|
+
value = control[fieldName];
|
|
3186
|
+
}
|
|
3187
|
+
if (fieldName === "mappings" && Array.isArray(value)) {
|
|
3188
|
+
const mappingsStr = value.map((m) => {
|
|
3189
|
+
const justification = m.description || m.justification || "";
|
|
3190
|
+
const status = m.status || "Unknown";
|
|
3191
|
+
return justification.trim() !== "" ? justification : `[${status}]`;
|
|
3192
|
+
}).join("\n");
|
|
3193
|
+
exportControl[displayName] = mappingsStr;
|
|
3080
3194
|
} else if (Array.isArray(value)) {
|
|
3081
3195
|
exportControl[displayName] = value.join("; ");
|
|
3082
3196
|
} else if (typeof value === "object" && value !== null) {
|
|
@@ -3490,6 +3604,7 @@ var init_spreadsheetRoutes = __esm({
|
|
|
3490
3604
|
router.get("/export-controls", async (req, res) => {
|
|
3491
3605
|
try {
|
|
3492
3606
|
const format = req.query.format || "csv";
|
|
3607
|
+
const mappingsColumn = req.query.mappingsColumn || "Mappings";
|
|
3493
3608
|
const state = getServerState();
|
|
3494
3609
|
const fileStore = state.fileStore;
|
|
3495
3610
|
if (!fileStore) {
|
|
@@ -3499,7 +3614,8 @@ var init_spreadsheetRoutes = __esm({
|
|
|
3499
3614
|
const mappings = await fileStore.loadMappings();
|
|
3500
3615
|
let metadata = {};
|
|
3501
3616
|
try {
|
|
3502
|
-
const
|
|
3617
|
+
const controlSetPath = getCurrentControlSetPath();
|
|
3618
|
+
const metadataPath2 = join4(controlSetPath, "lula.yaml");
|
|
3503
3619
|
if (existsSync3(metadataPath2)) {
|
|
3504
3620
|
const metadataContent = readFileSync3(metadataPath2, "utf8");
|
|
3505
3621
|
metadata = yaml4.load(metadataContent);
|
|
@@ -3527,10 +3643,10 @@ var init_spreadsheetRoutes = __esm({
|
|
|
3527
3643
|
debug(`Exporting ${controlsWithMappings.length} controls as ${format}`);
|
|
3528
3644
|
switch (format.toLowerCase()) {
|
|
3529
3645
|
case "csv":
|
|
3530
|
-
return exportAsCSV(controlsWithMappings, metadata, res);
|
|
3646
|
+
return exportAsCSV(controlsWithMappings, metadata, mappingsColumn, res);
|
|
3531
3647
|
case "excel":
|
|
3532
3648
|
case "xlsx":
|
|
3533
|
-
return await exportAsExcel(controlsWithMappings, metadata, res);
|
|
3649
|
+
return await exportAsExcel(controlsWithMappings, metadata, mappingsColumn, res);
|
|
3534
3650
|
case "json":
|
|
3535
3651
|
return exportAsJSON(controlsWithMappings, metadata, res);
|
|
3536
3652
|
default:
|
|
@@ -3541,6 +3657,106 @@ var init_spreadsheetRoutes = __esm({
|
|
|
3541
3657
|
res.status(500).json({ error: error.message });
|
|
3542
3658
|
}
|
|
3543
3659
|
});
|
|
3660
|
+
router.get("/export-column-headers", async (req, res) => {
|
|
3661
|
+
try {
|
|
3662
|
+
let metadata = {};
|
|
3663
|
+
try {
|
|
3664
|
+
const controlSetPath = getCurrentControlSetPath();
|
|
3665
|
+
const metadataPath2 = join4(controlSetPath, "lula.yaml");
|
|
3666
|
+
if (existsSync3(metadataPath2)) {
|
|
3667
|
+
const metadataContent = readFileSync3(metadataPath2, "utf8");
|
|
3668
|
+
metadata = yaml4.load(metadataContent);
|
|
3669
|
+
} else {
|
|
3670
|
+
return res.status(404).json({ error: `No lula.yaml file found in control set path: ${controlSetPath}` });
|
|
3671
|
+
}
|
|
3672
|
+
} catch {
|
|
3673
|
+
return res.status(500).json({ error: "Failed to read lula.yaml file" });
|
|
3674
|
+
}
|
|
3675
|
+
const fieldSchema = metadata?.fieldSchema?.fields || {};
|
|
3676
|
+
const controlIdField = metadata?.control_id_field || "id";
|
|
3677
|
+
const columnHeaders = [];
|
|
3678
|
+
if (fieldSchema[controlIdField]) {
|
|
3679
|
+
const idSchema = fieldSchema[controlIdField];
|
|
3680
|
+
const displayName = idSchema?.original_name || "Control ID";
|
|
3681
|
+
columnHeaders.push({
|
|
3682
|
+
value: displayName,
|
|
3683
|
+
label: displayName
|
|
3684
|
+
});
|
|
3685
|
+
} else {
|
|
3686
|
+
columnHeaders.push({ value: "Control ID", label: "Control ID" });
|
|
3687
|
+
}
|
|
3688
|
+
if (fieldSchema["family"]) {
|
|
3689
|
+
const familySchema = fieldSchema["family"];
|
|
3690
|
+
const displayName = familySchema?.original_name || "Family";
|
|
3691
|
+
columnHeaders.push({
|
|
3692
|
+
value: displayName,
|
|
3693
|
+
label: displayName
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
Object.entries(fieldSchema).forEach(([fieldName, schema]) => {
|
|
3697
|
+
if (fieldName === controlIdField || fieldName === "family" || fieldName === "mappings" || fieldName === "mappings_count") {
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
const displayName = schema?.original_name || fieldName.replace(/-/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
3701
|
+
columnHeaders.push({
|
|
3702
|
+
value: displayName,
|
|
3703
|
+
label: displayName
|
|
3704
|
+
});
|
|
3705
|
+
});
|
|
3706
|
+
columnHeaders.push({ value: "Mappings", label: "Mappings (Default)" });
|
|
3707
|
+
res.json({
|
|
3708
|
+
columnHeaders,
|
|
3709
|
+
defaultColumn: "Mappings"
|
|
3710
|
+
});
|
|
3711
|
+
} catch (error) {
|
|
3712
|
+
res.status(500).json({ error: error.message });
|
|
3713
|
+
}
|
|
3714
|
+
});
|
|
3715
|
+
router.post("/export-csv", async (req, res) => {
|
|
3716
|
+
try {
|
|
3717
|
+
const { format = "csv", _columns = [], columnMappings = {} } = req.body;
|
|
3718
|
+
const state = getServerState();
|
|
3719
|
+
const fileStore = state.fileStore;
|
|
3720
|
+
if (!fileStore) {
|
|
3721
|
+
return res.status(500).json({ error: "No control set loaded" });
|
|
3722
|
+
}
|
|
3723
|
+
const controls = await fileStore.loadAllControls();
|
|
3724
|
+
const mappings = await fileStore.loadMappings();
|
|
3725
|
+
let metadata = {};
|
|
3726
|
+
try {
|
|
3727
|
+
const controlSetPath = getCurrentControlSetPath();
|
|
3728
|
+
const metadataPath2 = join4(controlSetPath, "lula.yaml");
|
|
3729
|
+
if (existsSync3(metadataPath2)) {
|
|
3730
|
+
const metadataContent = readFileSync3(metadataPath2, "utf8");
|
|
3731
|
+
metadata = yaml4.load(metadataContent);
|
|
3732
|
+
}
|
|
3733
|
+
} catch (err) {
|
|
3734
|
+
debug("Could not load metadata:", err);
|
|
3735
|
+
}
|
|
3736
|
+
if (!controls || controls.length === 0) {
|
|
3737
|
+
return res.status(404).json({ error: "No controls found" });
|
|
3738
|
+
}
|
|
3739
|
+
const controlIdField = metadata?.control_id_field || "id";
|
|
3740
|
+
const controlsWithMappings = controls.map((control) => {
|
|
3741
|
+
const controlId = control[controlIdField] || control.id;
|
|
3742
|
+
const controlMappings = mappings.filter((m) => m.control_id === controlId);
|
|
3743
|
+
return {
|
|
3744
|
+
...control,
|
|
3745
|
+
mappings_count: controlMappings.length,
|
|
3746
|
+
mappings: controlMappings.map((m) => ({
|
|
3747
|
+
uuid: m.uuid,
|
|
3748
|
+
status: m.status,
|
|
3749
|
+
description: m.justification || ""
|
|
3750
|
+
}))
|
|
3751
|
+
};
|
|
3752
|
+
});
|
|
3753
|
+
debug(`Exporting ${controlsWithMappings.length} controls as ${format} with column mappings`);
|
|
3754
|
+
return exportAsCSVWithMapping(controlsWithMappings, metadata, columnMappings, res);
|
|
3755
|
+
} catch (error) {
|
|
3756
|
+
console.error("Export error:", error);
|
|
3757
|
+
res.status(500).json({ error: error.message });
|
|
3758
|
+
}
|
|
3759
|
+
});
|
|
3544
3760
|
router.post("/parse-excel", upload.single("file"), async (req, res) => {
|
|
3545
3761
|
try {
|
|
3546
3762
|
if (!req.file) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lula2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0-nightly.1",
|
|
4
4
|
"description": "A tool for managing compliance as code in your GitHub repositories.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"lula2": "./dist/lula2"
|
|
@@ -33,25 +33,6 @@
|
|
|
33
33
|
"!dist/**/*.test.js*",
|
|
34
34
|
"!dist/**/*.test.d.ts*"
|
|
35
35
|
],
|
|
36
|
-
"scripts": {
|
|
37
|
-
"dev": "vite dev --port 5173",
|
|
38
|
-
"dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
|
|
39
|
-
"dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
|
|
40
|
-
"build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
|
|
41
|
-
"build:svelte": "vite build",
|
|
42
|
-
"build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
|
|
43
|
-
"postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
|
|
44
|
-
"preview": "vite preview",
|
|
45
|
-
"prepare": "svelte-kit sync || echo ''",
|
|
46
|
-
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
|
|
47
|
-
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
48
|
-
"format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
49
|
-
"format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
50
|
-
"lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
|
|
51
|
-
"test": "npm run test:unit -- --run --coverage",
|
|
52
|
-
"test:integration": "vitest --config integration/vitest.config.integration.ts",
|
|
53
|
-
"test:unit": "vitest"
|
|
54
|
-
},
|
|
55
36
|
"dependencies": {
|
|
56
37
|
"@octokit/rest": "^22.0.0",
|
|
57
38
|
"@types/ws": "^8.18.1",
|
|
@@ -72,8 +53,8 @@
|
|
|
72
53
|
"yaml": "^2.8.1"
|
|
73
54
|
},
|
|
74
55
|
"devDependencies": {
|
|
75
|
-
"@commitlint/cli": "^
|
|
76
|
-
"@commitlint/config-conventional": "^
|
|
56
|
+
"@commitlint/cli": "^20.0.0",
|
|
57
|
+
"@commitlint/config-conventional": "^20.0.0",
|
|
77
58
|
"@eslint/compat": "^1.3.2",
|
|
78
59
|
"@eslint/eslintrc": "^3.3.1",
|
|
79
60
|
"@eslint/js": "^9.35.0",
|
|
@@ -124,5 +105,23 @@
|
|
|
124
105
|
"main",
|
|
125
106
|
"next"
|
|
126
107
|
]
|
|
108
|
+
},
|
|
109
|
+
"scripts": {
|
|
110
|
+
"dev": "vite dev --port 5173",
|
|
111
|
+
"dev:api": "tsx --watch index.ts --debug ui --port 3000 --no-open-browser",
|
|
112
|
+
"dev:full": "concurrently \"npm run dev:api\" \"npm run dev\"",
|
|
113
|
+
"build": "npm run build:svelte && npm run build:cli && npm run postbuild:cli",
|
|
114
|
+
"build:svelte": "vite build",
|
|
115
|
+
"build:cli": "esbuild index.ts cli/**/*.ts --bundle --platform=node --target=node22 --format=esm --outdir=dist --external:express --external:commander --external:js-yaml --external:yaml --external:isomorphic-git --external:glob --external:open --external:ws --external:cors --external:multer --external:@octokit/rest --external:undici --external:exceljs --external:csv-parse",
|
|
116
|
+
"postbuild:cli": "cp cli-wrapper.mjs dist/lula2 && chmod +x dist/lula2",
|
|
117
|
+
"preview": "vite preview",
|
|
118
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && tsc --noEmit",
|
|
119
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
|
120
|
+
"format": "prettier --write 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
121
|
+
"format:check": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts'",
|
|
122
|
+
"lint": "prettier --check 'src/**/*.{ts,js,svelte}' 'cli/**/*.ts' 'index.ts' 'tests/**/*.ts' && eslint src cli",
|
|
123
|
+
"test": "npm run test:unit -- --run --coverage",
|
|
124
|
+
"test:integration": "vitest --config integration/vitest.config.integration.ts",
|
|
125
|
+
"test:unit": "vitest"
|
|
127
126
|
}
|
|
128
|
-
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
|
2
|
+
<!-- SPDX-FileCopyrightText: 2023-Present The Lula Authors -->
|
|
3
|
+
|
|
4
|
+
<script lang="ts">
|
|
5
|
+
import { createEventDispatcher } from 'svelte';
|
|
6
|
+
import { Close } from 'carbon-icons-svelte';
|
|
7
|
+
|
|
8
|
+
interface ColumnHeader {
|
|
9
|
+
value: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ExportColumnDialogProps {
|
|
14
|
+
isOpen: boolean;
|
|
15
|
+
format: 'csv' | 'excel';
|
|
16
|
+
columnHeaders: ColumnHeader[];
|
|
17
|
+
defaultColumn: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
isOpen = $bindable(),
|
|
22
|
+
format,
|
|
23
|
+
columnHeaders = [],
|
|
24
|
+
defaultColumn = 'Mappings'
|
|
25
|
+
}: ExportColumnDialogProps = $props();
|
|
26
|
+
|
|
27
|
+
const dispatch = createEventDispatcher<{
|
|
28
|
+
export: { format: string; mappingsColumn: string };
|
|
29
|
+
cancel: void;
|
|
30
|
+
}>();
|
|
31
|
+
|
|
32
|
+
let selectedColumn = $state(defaultColumn);
|
|
33
|
+
|
|
34
|
+
// Reset selected column and focus when dialog opens
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (isOpen) {
|
|
37
|
+
selectedColumn = defaultColumn;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
function focusSelect(node: HTMLSelectElement) {
|
|
42
|
+
if (isOpen) {
|
|
43
|
+
setTimeout(() => node.focus(), 0);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
update(newIsOpen: boolean) {
|
|
47
|
+
if (newIsOpen) {
|
|
48
|
+
setTimeout(() => node.focus(), 0);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function handleExport() {
|
|
55
|
+
dispatch('export', { format, mappingsColumn: selectedColumn });
|
|
56
|
+
isOpen = false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleCancel() {
|
|
60
|
+
dispatch('cancel');
|
|
61
|
+
isOpen = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
65
|
+
if (event.key === 'Escape') {
|
|
66
|
+
handleCancel();
|
|
67
|
+
} else if (event.key === 'Enter') {
|
|
68
|
+
handleExport();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleBackdropClick(event: MouseEvent) {
|
|
73
|
+
if (event.target === event.currentTarget) {
|
|
74
|
+
handleCancel();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
</script>
|
|
78
|
+
|
|
79
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
80
|
+
|
|
81
|
+
{#if isOpen}
|
|
82
|
+
<!-- Modal Backdrop -->
|
|
83
|
+
<div
|
|
84
|
+
class="fixed inset-0 bg-gray-700 dark:bg-gray-900 bg-opacity-50 dark:bg-opacity-75 overflow-y-auto h-full w-full z-50 flex items-center justify-center p-4"
|
|
85
|
+
role="dialog"
|
|
86
|
+
aria-modal="true"
|
|
87
|
+
tabindex="-1"
|
|
88
|
+
onclick={handleBackdropClick}
|
|
89
|
+
onkeydown={(e) => e.key === 'Escape' && handleCancel()}
|
|
90
|
+
>
|
|
91
|
+
<!-- Modal Content -->
|
|
92
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
93
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
94
|
+
<div
|
|
95
|
+
class="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-md mx-auto"
|
|
96
|
+
role="document"
|
|
97
|
+
onclick={(e) => e.stopPropagation()}
|
|
98
|
+
>
|
|
99
|
+
<!-- Header -->
|
|
100
|
+
<div class="flex items-center justify-between mb-4">
|
|
101
|
+
<h2 class="text-xl font-semibold text-gray-900 dark:text-white">
|
|
102
|
+
Export as {format.toUpperCase()}
|
|
103
|
+
</h2>
|
|
104
|
+
<button
|
|
105
|
+
onclick={handleCancel}
|
|
106
|
+
class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300"
|
|
107
|
+
aria-label="Close dialog"
|
|
108
|
+
>
|
|
109
|
+
<Close class="w-6 h-6" />
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<!-- Content -->
|
|
114
|
+
<div class="mb-6">
|
|
115
|
+
<p class="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
|
116
|
+
Choose which column should contain the mappings data:
|
|
117
|
+
</p>
|
|
118
|
+
|
|
119
|
+
<div class="space-y-2">
|
|
120
|
+
<label
|
|
121
|
+
for="column-select"
|
|
122
|
+
class="block text-sm font-medium text-gray-700 dark:text-gray-300"
|
|
123
|
+
>
|
|
124
|
+
Mappings Column
|
|
125
|
+
</label>
|
|
126
|
+
<select
|
|
127
|
+
id="column-select"
|
|
128
|
+
use:focusSelect={isOpen}
|
|
129
|
+
bind:value={selectedColumn}
|
|
130
|
+
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
131
|
+
>
|
|
132
|
+
{#each columnHeaders as header}
|
|
133
|
+
<option value={header.value}>{header.label}</option>
|
|
134
|
+
{/each}
|
|
135
|
+
</select>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="mt-3 p-3 bg-blue-50 dark:bg-blue-900/20 rounded-md">
|
|
139
|
+
<p class="text-xs text-blue-600 dark:text-blue-400">
|
|
140
|
+
<strong>Note:</strong> Mappings data will be formatted as "status: description..." in the
|
|
141
|
+
selected column.
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<!-- Footer -->
|
|
147
|
+
<div class="flex justify-end space-x-3">
|
|
148
|
+
<button
|
|
149
|
+
onclick={handleCancel}
|
|
150
|
+
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-colors"
|
|
151
|
+
>
|
|
152
|
+
Cancel
|
|
153
|
+
</button>
|
|
154
|
+
<button
|
|
155
|
+
onclick={handleExport}
|
|
156
|
+
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 rounded-lg transition-colors"
|
|
157
|
+
>
|
|
158
|
+
Export {format.toUpperCase()}
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
{/if}
|