lula2 0.5.0-nightly.3 → 0.5.0-nightly.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.
@@ -90,58 +90,81 @@ async function scanControlSets() {
90
90
  }).filter((cs) => cs !== null);
91
91
  return { controlSets };
92
92
  }
93
+ function processImportParameters(reqBody) {
94
+ const {
95
+ controlIdField = "Control ID",
96
+ startRow = "1",
97
+ controlSetName = "Imported Control Set",
98
+ controlSetDescription = "Imported from spreadsheet"
99
+ } = reqBody;
100
+ let justificationFields = [];
101
+ if (reqBody.justificationFields) {
102
+ try {
103
+ justificationFields = JSON.parse(reqBody.justificationFields);
104
+ debug("Justification fields received:", justificationFields);
105
+ } catch (e) {
106
+ console.error("Failed to parse justification fields:", e);
107
+ }
108
+ }
109
+ let frontendFieldSchema = null;
110
+ if (reqBody.fieldSchema) {
111
+ try {
112
+ frontendFieldSchema = JSON.parse(reqBody.fieldSchema);
113
+ } catch (e) {
114
+ console.error("Failed to parse fieldSchema:", e);
115
+ }
116
+ }
117
+ debug("Import parameters received:", {
118
+ controlIdField,
119
+ startRow,
120
+ controlSetName,
121
+ controlSetDescription
122
+ });
123
+ return {
124
+ controlIdField,
125
+ startRow,
126
+ controlSetName,
127
+ controlSetDescription,
128
+ justificationFields,
129
+ namingConvention: "kebab-case",
130
+ skipEmpty: true,
131
+ skipEmptyRows: true,
132
+ frontendFieldSchema
133
+ };
134
+ }
135
+ async function parseUploadedFile(file) {
136
+ const fileName = file.originalname || "";
137
+ const isCSV = fileName.toLowerCase().endsWith(".csv");
138
+ let rawData = [];
139
+ if (isCSV) {
140
+ const csvContent = file.buffer.toString("utf-8");
141
+ rawData = parseCSV(csvContent);
142
+ } else {
143
+ const workbook = new ExcelJS.Workbook();
144
+ const buffer = Buffer.from(file.buffer);
145
+ await workbook.xlsx.load(buffer);
146
+ const worksheet = workbook.worksheets[0];
147
+ if (!worksheet) {
148
+ throw new Error("No worksheet found in file");
149
+ }
150
+ worksheet.eachRow({ includeEmpty: true }, (row, rowNumber) => {
151
+ const rowData = [];
152
+ row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
153
+ rowData[colNumber - 1] = cell.value;
154
+ });
155
+ rawData[rowNumber - 1] = rowData;
156
+ });
157
+ }
158
+ return rawData;
159
+ }
93
160
  router.post("/import-spreadsheet", upload.single("file"), async (req, res) => {
94
161
  try {
95
162
  if (!req.file) {
96
163
  return res.status(400).json({ error: "No file uploaded" });
97
164
  }
98
- const {
99
- controlIdField = "Control ID",
100
- startRow = "1",
101
- controlSetName = "Imported Control Set",
102
- controlSetDescription = "Imported from spreadsheet"
103
- } = req.body;
104
- let justificationFields = [];
105
- if (req.body.justificationFields) {
106
- try {
107
- justificationFields = JSON.parse(req.body.justificationFields);
108
- debug("Justification fields received:", justificationFields);
109
- } catch (e) {
110
- console.error("Failed to parse justification fields:", e);
111
- }
112
- }
113
- debug("Import parameters received:", {
114
- controlIdField,
115
- startRow,
116
- controlSetName,
117
- controlSetDescription
118
- });
119
- const namingConvention = "kebab-case";
120
- const skipEmpty = true;
121
- const skipEmptyRows = true;
122
- const fileName = req.file.originalname || "";
123
- const isCSV = fileName.toLowerCase().endsWith(".csv");
124
- let rawData = [];
125
- if (isCSV) {
126
- const csvContent = req.file.buffer.toString("utf-8");
127
- rawData = parseCSV(csvContent);
128
- } else {
129
- const workbook = new ExcelJS.Workbook();
130
- const buffer = Buffer.from(req.file.buffer);
131
- await workbook.xlsx.load(buffer);
132
- const worksheet = workbook.worksheets[0];
133
- if (!worksheet) {
134
- return res.status(400).json({ error: "No worksheet found in file" });
135
- }
136
- worksheet.eachRow({ includeEmpty: true }, (row, rowNumber) => {
137
- const rowData = [];
138
- row.eachCell({ includeEmpty: true }, (cell, colNumber) => {
139
- rowData[colNumber - 1] = cell.value;
140
- });
141
- rawData[rowNumber - 1] = rowData;
142
- });
143
- }
144
- const startRowIndex = parseInt(startRow) - 1;
165
+ const params = processImportParameters(req.body);
166
+ const rawData = await parseUploadedFile(req.file);
167
+ const startRowIndex = parseInt(params.startRow) - 1;
145
168
  if (rawData.length <= startRowIndex) {
146
169
  return res.status(400).json({ error: "Start row exceeds sheet data" });
147
170
  }
@@ -152,275 +175,288 @@ router.post("/import-spreadsheet", upload.single("file"), async (req, res) => {
152
175
  debug("Headers found:", headers);
153
176
  debug(
154
177
  "After conversion, looking for control ID field:",
155
- applyNamingConvention(controlIdField, namingConvention)
178
+ applyNamingConvention(params.controlIdField, params.namingConvention)
156
179
  );
157
- const controls = [];
158
- const families = /* @__PURE__ */ new Map();
159
- const fieldMetadata = /* @__PURE__ */ new Map();
160
- headers.forEach((header) => {
161
- if (header) {
162
- const cleanName = applyNamingConvention(header, namingConvention);
163
- fieldMetadata.set(cleanName, {
164
- originalName: header,
165
- cleanName,
166
- type: "string",
167
- maxLength: 0,
168
- hasMultipleLines: false,
169
- uniqueValues: /* @__PURE__ */ new Set(),
170
- emptyCount: 0,
171
- totalCount: 0,
172
- examples: []
173
- });
174
- }
180
+ const processedData = processSpreadsheetData(rawData, headers, startRowIndex, params);
181
+ const fieldSchema = buildFieldSchema(
182
+ processedData.fieldMetadata,
183
+ processedData.controls,
184
+ params,
185
+ processedData.families
186
+ );
187
+ const result = await createOutputStructure(processedData, fieldSchema, params);
188
+ res.json({
189
+ success: true,
190
+ controlCount: processedData.controls.length,
191
+ families: Array.from(processedData.families.keys()),
192
+ outputDir: result.folderName
175
193
  });
176
- for (let i = startRowIndex + 1; i < rawData.length; i++) {
177
- const row = rawData[i];
178
- if (!row || row.length === 0) continue;
179
- const control = {};
180
- let hasData = false;
181
- headers.forEach((header, index) => {
182
- if (header && row[index] !== void 0 && row[index] !== null) {
183
- const value = typeof row[index] === "string" ? row[index].trim() : row[index];
184
- const fieldName = applyNamingConvention(header, namingConvention);
185
- const metadata = fieldMetadata.get(fieldName);
186
- metadata.totalCount++;
187
- if (value === "" || value === null || value === void 0) {
188
- metadata.emptyCount++;
189
- if (skipEmpty) return;
190
- } else {
191
- const normalizedValue = typeof value === "string" ? value.trim() : value;
192
- if (normalizedValue !== "") {
193
- metadata.uniqueValues.add(normalizedValue);
194
- }
195
- const valueType = detectValueType(value);
196
- if (metadata.type === "string" || metadata.totalCount === 1) {
197
- metadata.type = valueType;
198
- } else if (metadata.type !== valueType) {
199
- metadata.type = "mixed";
200
- }
201
- if (typeof value === "string") {
202
- const length = value.length;
203
- if (length > metadata.maxLength) {
204
- metadata.maxLength = length;
205
- }
206
- if (value.includes("\n") || length > 100) {
207
- metadata.hasMultipleLines = true;
208
- }
194
+ } catch (error) {
195
+ console.error("Error processing spreadsheet:", error);
196
+ res.status(500).json({ error: "Failed to process spreadsheet" });
197
+ }
198
+ });
199
+ function processSpreadsheetData(rawData, headers, startRowIndex, params) {
200
+ const controls = [];
201
+ const families = /* @__PURE__ */ new Map();
202
+ const fieldMetadata = /* @__PURE__ */ new Map();
203
+ headers.forEach((header) => {
204
+ if (header) {
205
+ const cleanName = applyNamingConvention(header, params.namingConvention);
206
+ fieldMetadata.set(cleanName, {
207
+ originalName: header,
208
+ cleanName,
209
+ type: "string",
210
+ maxLength: 0,
211
+ hasMultipleLines: false,
212
+ uniqueValues: /* @__PURE__ */ new Set(),
213
+ emptyCount: 0,
214
+ totalCount: 0,
215
+ examples: []
216
+ });
217
+ }
218
+ });
219
+ for (let i = startRowIndex + 1; i < rawData.length; i++) {
220
+ const row = rawData[i];
221
+ if (!row || row.length === 0) continue;
222
+ const control = {};
223
+ let hasData = false;
224
+ headers.forEach((header, index) => {
225
+ if (header && row[index] !== void 0 && row[index] !== null) {
226
+ const value = typeof row[index] === "string" ? row[index].trim() : row[index];
227
+ const fieldName = applyNamingConvention(header, params.namingConvention);
228
+ const metadata = fieldMetadata.get(fieldName);
229
+ metadata.totalCount++;
230
+ if (value === "" || value === null || value === void 0) {
231
+ metadata.emptyCount++;
232
+ if (params.skipEmpty) return;
233
+ } else {
234
+ const normalizedValue = typeof value === "string" ? value.trim() : value;
235
+ if (normalizedValue !== "") {
236
+ metadata.uniqueValues.add(normalizedValue);
237
+ }
238
+ const valueType = detectValueType(value);
239
+ if (metadata.type === "string" || metadata.totalCount === 1) {
240
+ metadata.type = valueType;
241
+ } else if (metadata.type !== valueType) {
242
+ metadata.type = "mixed";
243
+ }
244
+ if (typeof value === "string") {
245
+ const length = value.length;
246
+ if (length > metadata.maxLength) {
247
+ metadata.maxLength = length;
209
248
  }
210
- if (metadata.examples.length < 3 && normalizedValue !== "") {
211
- metadata.examples.push(normalizedValue);
249
+ if (value.includes("\n") || length > 100) {
250
+ metadata.hasMultipleLines = true;
212
251
  }
213
252
  }
214
- control[fieldName] = value;
215
- hasData = true;
216
- }
217
- });
218
- if (hasData && (!skipEmptyRows || Object.keys(control).length > 0)) {
219
- const controlIdFieldName = applyNamingConvention(controlIdField, namingConvention);
220
- const controlId = control[controlIdFieldName];
221
- if (!controlId) {
222
- continue;
223
- }
224
- const family = extractFamilyFromControlId(controlId);
225
- control.family = family;
226
- controls.push(control);
227
- if (!families.has(family)) {
228
- families.set(family, []);
253
+ if (metadata.examples.length < 3 && normalizedValue !== "") {
254
+ metadata.examples.push(normalizedValue);
255
+ }
229
256
  }
230
- families.get(family).push(control);
257
+ control[fieldName] = value;
258
+ hasData = true;
231
259
  }
260
+ });
261
+ if (hasData && (!params.skipEmptyRows || Object.keys(control).length > 0)) {
262
+ const controlIdFieldName = applyNamingConvention(
263
+ params.controlIdField,
264
+ params.namingConvention
265
+ );
266
+ const controlId = control[controlIdFieldName];
267
+ if (!controlId) {
268
+ continue;
269
+ }
270
+ const family = extractFamilyFromControlId(controlId);
271
+ control.family = family;
272
+ controls.push(control);
273
+ if (!families.has(family)) {
274
+ families.set(family, []);
275
+ }
276
+ families.get(family).push(control);
232
277
  }
233
- const state = getServerState();
234
- const folderName = toKebabCase(controlSetName || "imported-controls");
235
- const baseDir = join2(state.CONTROL_SET_DIR || process.cwd(), folderName);
236
- if (!existsSync(baseDir)) {
237
- mkdirSync(baseDir, { recursive: true });
278
+ }
279
+ return { controls, families, fieldMetadata };
280
+ }
281
+ function buildFieldSchema(fieldMetadata, controls, params, families) {
282
+ const fields = {};
283
+ let displayOrder = 1;
284
+ const controlIdFieldNameClean = applyNamingConvention(
285
+ params.controlIdField,
286
+ params.namingConvention
287
+ );
288
+ const familyOptions = Array.from(families.keys()).filter((f) => f && f !== "UNKNOWN").sort();
289
+ fields["family"] = {
290
+ type: "string",
291
+ ui_type: familyOptions.length <= 50 ? "select" : "short_text",
292
+ is_array: false,
293
+ max_length: 10,
294
+ usage_count: controls.length,
295
+ usage_percentage: 100,
296
+ required: true,
297
+ visible: true,
298
+ show_in_table: true,
299
+ editable: false,
300
+ display_order: displayOrder++,
301
+ category: "core",
302
+ tab: "overview"
303
+ };
304
+ if (familyOptions.length <= 50) {
305
+ fields["family"].options = familyOptions;
306
+ }
307
+ fieldMetadata.forEach((metadata, fieldName) => {
308
+ if (fieldName === "family" || fieldName === "id" && controlIdFieldNameClean !== "id") {
309
+ return;
310
+ }
311
+ const frontendConfig = params.frontendFieldSchema?.find((f) => f.fieldName === fieldName);
312
+ if (params.frontendFieldSchema && !frontendConfig) {
313
+ return;
314
+ }
315
+ const usageCount = metadata.totalCount - metadata.emptyCount;
316
+ const usagePercentage = metadata.totalCount > 0 ? Math.round(usageCount / metadata.totalCount * 100) : 0;
317
+ let uiType = "short_text";
318
+ const nonEmptyCount = metadata.totalCount - metadata.emptyCount;
319
+ const isDropdownCandidate = metadata.uniqueValues.size > 0 && metadata.uniqueValues.size <= 20 && // Max 20 unique values for dropdown
320
+ nonEmptyCount >= 10 && // At least 10 non-empty values to be meaningful
321
+ metadata.maxLength <= 100 && // Reasonably short values only
322
+ metadata.uniqueValues.size / nonEmptyCount <= 0.3;
323
+ if (metadata.hasMultipleLines || metadata.maxLength > 500) {
324
+ uiType = "textarea";
325
+ } else if (isDropdownCandidate) {
326
+ uiType = "select";
327
+ } else if (metadata.type === "boolean") {
328
+ uiType = "checkbox";
329
+ } else if (metadata.type === "number") {
330
+ uiType = "number";
331
+ } else if (metadata.type === "date") {
332
+ uiType = "date";
333
+ } else if (metadata.maxLength <= 50) {
334
+ uiType = "short_text";
335
+ } else if (metadata.maxLength <= 200) {
336
+ uiType = "medium_text";
337
+ } else {
338
+ uiType = "long_text";
238
339
  }
239
- const uniqueFamilies = Array.from(families.keys()).filter((f) => f && f !== "UNKNOWN");
240
- let frontendFieldSchema = null;
241
- if (req.body.fieldSchema) {
242
- try {
243
- frontendFieldSchema = JSON.parse(req.body.fieldSchema);
244
- } catch {
340
+ let category = frontendConfig?.category || "custom";
341
+ if (!frontendConfig) {
342
+ if (fieldName.includes("status") || fieldName.includes("state")) {
343
+ category = "compliance";
344
+ } else if (fieldName.includes("title") || fieldName.includes("name") || fieldName.includes("description")) {
345
+ category = "core";
346
+ } else if (fieldName.includes("note") || fieldName.includes("comment")) {
347
+ category = "notes";
245
348
  }
246
349
  }
247
- const fields = {};
248
- let displayOrder = 1;
249
- const controlIdFieldNameClean = applyNamingConvention(controlIdField, namingConvention);
250
- const familyOptions = Array.from(families.keys()).filter((f) => f && f !== "UNKNOWN").sort();
251
- fields["family"] = {
252
- type: "string",
253
- ui_type: familyOptions.length <= 50 ? "select" : "short_text",
254
- // Make select if reasonable number of families
350
+ const isControlIdField = fieldName === controlIdFieldNameClean;
351
+ const fieldDef = {
352
+ type: metadata.type,
353
+ ui_type: uiType,
255
354
  is_array: false,
256
- max_length: 10,
257
- usage_count: controls.length,
258
- usage_percentage: 100,
259
- required: true,
260
- visible: true,
261
- show_in_table: true,
262
- editable: false,
263
- display_order: displayOrder++,
264
- category: "core",
265
- tab: "overview"
355
+ max_length: metadata.maxLength,
356
+ usage_count: usageCount,
357
+ usage_percentage: usagePercentage,
358
+ required: isControlIdField ? true : frontendConfig?.required ?? usagePercentage > 95,
359
+ visible: frontendConfig?.tab !== "hidden",
360
+ show_in_table: isControlIdField ? true : metadata.maxLength <= 100 && usagePercentage > 30,
361
+ editable: isControlIdField ? false : true,
362
+ display_order: isControlIdField ? 1 : frontendConfig?.displayOrder ?? displayOrder++,
363
+ category: isControlIdField ? "core" : category,
364
+ tab: isControlIdField ? "overview" : frontendConfig?.tab || void 0
266
365
  };
267
- if (familyOptions.length <= 50) {
268
- fields["family"].options = familyOptions;
366
+ if (uiType === "select") {
367
+ fieldDef.options = Array.from(metadata.uniqueValues).sort();
269
368
  }
270
- fieldMetadata.forEach((metadata, fieldName) => {
271
- if (fieldName === "family" || fieldName === "id" && controlIdFieldNameClean !== "id") {
272
- return;
273
- }
274
- const frontendConfig = frontendFieldSchema?.find((f) => f.fieldName === fieldName);
275
- if (frontendFieldSchema && !frontendConfig) {
369
+ if (frontendConfig?.originalName || metadata.originalName) {
370
+ fieldDef.original_name = frontendConfig?.originalName || metadata.originalName;
371
+ }
372
+ fields[fieldName] = fieldDef;
373
+ });
374
+ return {
375
+ fields,
376
+ total_controls: controls.length,
377
+ analyzed_at: (/* @__PURE__ */ new Date()).toISOString()
378
+ };
379
+ }
380
+ async function createOutputStructure(processedData, fieldSchema, params) {
381
+ const { controls, families } = processedData;
382
+ const state = getServerState();
383
+ const folderName = toKebabCase(params.controlSetName || "imported-controls");
384
+ const baseDir = join2(state.CONTROL_SET_DIR || process.cwd(), folderName);
385
+ if (!existsSync(baseDir)) {
386
+ mkdirSync(baseDir, { recursive: true });
387
+ }
388
+ const uniqueFamilies = Array.from(families.keys()).filter((f) => f && f !== "UNKNOWN");
389
+ const controlIdFieldNameClean = applyNamingConvention(
390
+ params.controlIdField,
391
+ params.namingConvention
392
+ );
393
+ const controlSetData = {
394
+ name: params.controlSetName,
395
+ description: params.controlSetDescription,
396
+ version: "1.0.0",
397
+ control_id_field: controlIdFieldNameClean,
398
+ controlCount: controls.length,
399
+ families: uniqueFamilies,
400
+ fieldSchema
401
+ };
402
+ writeFileSync(join2(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
403
+ const controlsDir = join2(baseDir, "controls");
404
+ const mappingsDir = join2(baseDir, "mappings");
405
+ families.forEach((familyControls, family) => {
406
+ const familyDir = join2(controlsDir, family);
407
+ const familyMappingsDir = join2(mappingsDir, family);
408
+ if (!existsSync(familyDir)) {
409
+ mkdirSync(familyDir, { recursive: true });
410
+ }
411
+ if (!existsSync(familyMappingsDir)) {
412
+ mkdirSync(familyMappingsDir, { recursive: true });
413
+ }
414
+ familyControls.forEach((control) => {
415
+ const controlId = control[controlIdFieldNameClean];
416
+ if (!controlId) {
417
+ console.error("Missing control ID for control:", control);
276
418
  return;
277
419
  }
278
- const usageCount = metadata.totalCount - metadata.emptyCount;
279
- const usagePercentage = metadata.totalCount > 0 ? Math.round(usageCount / metadata.totalCount * 100) : 0;
280
- let uiType = "short_text";
281
- const nonEmptyCount = metadata.totalCount - metadata.emptyCount;
282
- const isDropdownCandidate = metadata.uniqueValues.size > 0 && metadata.uniqueValues.size <= 20 && // Max 20 unique values for dropdown
283
- nonEmptyCount >= 10 && // At least 10 non-empty values to be meaningful
284
- metadata.maxLength <= 100 && // Reasonably short values only
285
- metadata.uniqueValues.size / nonEmptyCount <= 0.3;
286
- if (metadata.hasMultipleLines || metadata.maxLength > 500) {
287
- uiType = "textarea";
288
- } else if (isDropdownCandidate) {
289
- uiType = "select";
290
- } else if (metadata.type === "boolean") {
291
- uiType = "checkbox";
292
- } else if (metadata.type === "number") {
293
- uiType = "number";
294
- } else if (metadata.type === "date") {
295
- uiType = "date";
296
- } else if (metadata.maxLength <= 50) {
297
- uiType = "short_text";
298
- } else if (metadata.maxLength <= 200) {
299
- uiType = "medium_text";
300
- } else {
301
- uiType = "long_text";
302
- }
303
- let category = frontendConfig?.category || "custom";
304
- if (!frontendConfig) {
305
- if (fieldName.includes("status") || fieldName.includes("state")) {
306
- category = "compliance";
307
- } else if (fieldName.includes("title") || fieldName.includes("name") || fieldName.includes("description")) {
308
- category = "core";
309
- } else if (fieldName.includes("note") || fieldName.includes("comment")) {
310
- category = "notes";
311
- }
312
- }
313
- const isControlIdField = fieldName === controlIdFieldNameClean;
314
- const fieldDef = {
315
- type: metadata.type,
316
- ui_type: uiType,
317
- is_array: false,
318
- max_length: metadata.maxLength,
319
- usage_count: usageCount,
320
- usage_percentage: usagePercentage,
321
- required: isControlIdField ? true : frontendConfig?.required ?? usagePercentage > 95,
322
- // Control ID is always required
323
- visible: frontendConfig?.tab !== "hidden",
324
- show_in_table: isControlIdField ? true : metadata.maxLength <= 100 && usagePercentage > 30,
325
- // Always show control ID in table
326
- editable: isControlIdField ? false : true,
327
- // Control ID is not editable
328
- display_order: isControlIdField ? 1 : frontendConfig?.displayOrder ?? displayOrder++,
329
- // Control ID is always first
330
- category: isControlIdField ? "core" : category,
331
- // Control ID is always core
332
- tab: isControlIdField ? "overview" : frontendConfig?.tab || void 0
333
- // Use frontend config or default
420
+ const controlIdStr = String(controlId).slice(0, 50);
421
+ const fileName = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}.yaml`;
422
+ const filePath = join2(familyDir, fileName);
423
+ const mappingFileName = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`;
424
+ const mappingFilePath = join2(familyMappingsDir, mappingFileName);
425
+ const filteredControl = {};
426
+ const mappingData = {
427
+ control_id: controlIdStr,
428
+ justification: "",
429
+ uuid: crypto.randomUUID()
334
430
  };
335
- if (uiType === "select") {
336
- fieldDef.options = Array.from(metadata.uniqueValues).sort();
337
- }
338
- if (frontendConfig?.originalName || metadata.originalName) {
339
- fieldDef.original_name = frontendConfig?.originalName || metadata.originalName;
340
- }
341
- fields[fieldName] = fieldDef;
342
- });
343
- const fieldSchema = {
344
- fields,
345
- total_controls: controls.length,
346
- analyzed_at: (/* @__PURE__ */ new Date()).toISOString()
347
- };
348
- const controlSetData = {
349
- name: controlSetName,
350
- description: controlSetDescription,
351
- version: "1.0.0",
352
- control_id_field: controlIdFieldNameClean,
353
- // Add this to indicate which field is the control ID
354
- controlCount: controls.length,
355
- families: uniqueFamilies,
356
- fieldSchema
357
- };
358
- writeFileSync(join2(baseDir, "lula.yaml"), yaml4.dump(controlSetData));
359
- const controlsDir = join2(baseDir, "controls");
360
- const mappingsDir = join2(baseDir, "mappings");
361
- families.forEach((familyControls, family) => {
362
- const familyDir = join2(controlsDir, family);
363
- const familyMappingsDir = join2(mappingsDir, family);
364
- if (!existsSync(familyDir)) {
365
- mkdirSync(familyDir, { recursive: true });
366
- }
367
- if (!existsSync(familyMappingsDir)) {
368
- mkdirSync(familyMappingsDir, { recursive: true });
431
+ const justificationContents = [];
432
+ if (control.family !== void 0) {
433
+ filteredControl.family = control.family;
369
434
  }
370
- familyControls.forEach((control) => {
371
- const controlId = control[controlIdFieldNameClean];
372
- if (!controlId) {
373
- console.error("Missing control ID for control:", control);
374
- return;
375
- }
376
- const controlIdStr = String(controlId).slice(0, 50);
377
- const fileName2 = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}.yaml`;
378
- const filePath = join2(familyDir, fileName2);
379
- const mappingFileName = `${controlIdStr.replace(/[^a-zA-Z0-9-]/g, "_")}-mappings.yaml`;
380
- const mappingFilePath = join2(familyMappingsDir, mappingFileName);
381
- const filteredControl = {};
382
- const mappingData = {
383
- control_id: controlIdStr,
384
- justification: "",
385
- uuid: crypto.randomUUID()
386
- };
387
- const justificationContents = [];
388
- if (control.family !== void 0) {
389
- filteredControl.family = control.family;
390
- }
391
- Object.keys(control).forEach((fieldName) => {
392
- if (fieldName === "family") return;
393
- if (justificationFields.includes(fieldName) && control[fieldName] !== void 0 && control[fieldName] !== null) {
394
- justificationContents.push(control[fieldName]);
395
- }
396
- const isInFrontendSchema = frontendFieldSchema?.some((f) => f.fieldName === fieldName);
397
- const isInFieldsMetadata = fields.hasOwnProperty(fieldName);
398
- if (isInFrontendSchema || isInFieldsMetadata) {
399
- filteredControl[fieldName] = control[fieldName];
400
- }
401
- });
402
- writeFileSync(filePath, yaml4.dump(filteredControl));
403
- if (justificationContents.length > 0) {
404
- mappingData.justification = justificationContents.join("\n\n");
435
+ Object.keys(control).forEach((fieldName) => {
436
+ if (fieldName === "family") return;
437
+ if (params.justificationFields.includes(fieldName) && control[fieldName] !== void 0 && control[fieldName] !== null) {
438
+ justificationContents.push(control[fieldName]);
405
439
  }
406
- if (mappingData.justification && mappingData.justification.trim() !== "") {
407
- const mappingArray = [mappingData];
408
- writeFileSync(mappingFilePath, yaml4.dump(mappingArray));
440
+ const isInFrontendSchema = params.frontendFieldSchema?.some(
441
+ (f) => f.fieldName === fieldName
442
+ );
443
+ const isInFieldsMetadata = fieldSchema.fields.hasOwnProperty(fieldName);
444
+ if (isInFrontendSchema || isInFieldsMetadata) {
445
+ filteredControl[fieldName] = control[fieldName];
409
446
  }
410
447
  });
448
+ writeFileSync(filePath, yaml4.dump(filteredControl));
449
+ if (justificationContents.length > 0) {
450
+ mappingData.justification = justificationContents.join("\n\n");
451
+ }
452
+ if (mappingData.justification && mappingData.justification.trim() !== "") {
453
+ const mappingArray = [mappingData];
454
+ writeFileSync(mappingFilePath, yaml4.dump(mappingArray));
455
+ }
411
456
  });
412
- res.json({
413
- success: true,
414
- controlCount: controls.length,
415
- families: Array.from(families.keys()),
416
- outputDir: folderName
417
- // Return just the folder name, not full path
418
- });
419
- } catch (error) {
420
- console.error("Error processing spreadsheet:", error);
421
- res.status(500).json({ error: "Failed to process spreadsheet" });
422
- }
423
- });
457
+ });
458
+ return { folderName };
459
+ }
424
460
  function applyNamingConvention(fieldName, convention) {
425
461
  if (!fieldName) return fieldName;
426
462
  const cleanedName = fieldName.trim();
@@ -1039,6 +1075,18 @@ router.post("/parse-excel-sheet", upload.single("file"), async (req, res) => {
1039
1075
  });
1040
1076
  var spreadsheetRoutes_default = router;
1041
1077
  export {
1078
+ applyNamingConvention,
1079
+ buildFieldSchema,
1080
+ createOutputStructure,
1042
1081
  spreadsheetRoutes_default as default,
1043
- scanControlSets
1082
+ detectValueType,
1083
+ extractFamilyFromControlId,
1084
+ parseCSV,
1085
+ parseUploadedFile,
1086
+ processImportParameters,
1087
+ processSpreadsheetData,
1088
+ scanControlSets,
1089
+ toCamelCase,
1090
+ toKebabCase,
1091
+ toSnakeCase
1044
1092
  };