From 59d7386106f6b2a6f9efe548d727ecb9a2b0ceca Mon Sep 17 00:00:00 2001 From: PRATEEK SINGH <110760231+PRAteek-singHWY@users.noreply.github.com> Date: Sat, 28 Feb 2026 23:18:05 +0530 Subject: [PATCH 01/26] feat(myopencre): improve UI messaging for no-op CSV imports (#684) * feat(myopencre): add initial MyOpenCRE page and CSV template download * chore(osib): update OWASP name to Open Worldwide Application Security Project * feat(myopencre): enable CSV download of all CREs * refactor(csv-import): move CSV validation into spreadsheet parser * style(csv): format files with black * refactor(csv-import): move CSV validation into spreadsheet parser * style(myopencre): move container spacing to SCSS * refactor(myopencre): move CSV validation to parser and add noop import handling * style(csv): format files with black --- .../src/pages/MyOpenCRE/MyOpenCRE.scss | 6 +- .../src/pages/MyOpenCRE/MyOpenCRE.tsx | 76 ++++++++++++++++--- application/utils/spreadsheet_parsers.py | 4 +- application/web/web_main.py | 5 ++ 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss index ad0ed9258..072c10e8c 100644 --- a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss +++ b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss @@ -1,3 +1,7 @@ +.myopencre-container { + margin-top: 3rem; +} + .myopencre-section { margin-top: 2rem; } @@ -8,4 +12,4 @@ .myopencre-disabled { opacity: 0.7; -} +} \ No newline at end of file diff --git a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx index 0523ad028..10da0ac98 100644 --- a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx +++ b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx @@ -5,6 +5,20 @@ import { Button, Container, Form, Header, Message } from 'semantic-ui-react'; import { useEnvironment } from '../../hooks'; +type RowValidationError = { + row: number; + code: string; + message: string; + column?: string; +}; + +type ImportErrorResponse = { + success: false; + type: string; + message?: string; + errors?: RowValidationError[]; +}; + export const MyOpenCRE = () => { const { apiUrl } = useEnvironment(); @@ -13,8 +27,9 @@ export const MyOpenCRE = () => { const [selectedFile, setSelectedFile] = useState(null); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const [success, setSuccess] = useState(null); + const [info, setInfo] = useState(null); /* ------------------ CSV DOWNLOAD ------------------ */ @@ -54,13 +69,18 @@ export const MyOpenCRE = () => { const onFileChange = (e: React.ChangeEvent) => { setError(null); setSuccess(null); + setInfo(null); if (!e.target.files || e.target.files.length === 0) return; const file = e.target.files[0]; if (!file.name.toLowerCase().endsWith('.csv')) { - setError('Please upload a valid CSV file.'); + setError({ + success: false, + type: 'FILE_ERROR', + message: 'Please upload a valid CSV file.', + }); e.target.value = ''; setSelectedFile(null); return; @@ -77,6 +97,7 @@ export const MyOpenCRE = () => { setLoading(true); setError(null); setSuccess(null); + setInfo(null); const formData = new FormData(); formData.append('cre_csv', selectedFile); @@ -93,25 +114,59 @@ export const MyOpenCRE = () => { ); } + const payload = await response.json(); + if (!response.ok) { - const text = await response.text(); - throw new Error(text || 'CSV import failed'); + setError(payload); + return; } - const result = await response.json(); - setSuccess(result); + if (payload.import_type === 'noop') { + setInfo( + 'Import completed successfully, but no new CREs or standards were added because all mappings already exist.' + ); + } else { + setSuccess(payload); + } setSelectedFile(null); } catch (err: any) { - setError(err.message || 'Unexpected error during import'); + setError({ + success: false, + type: 'CLIENT_ERROR', + message: err.message || 'Unexpected error during import', + }); } finally { setLoading(false); } }; + /* ------------------ ERROR RENDERING ------------------ */ + + const renderErrorMessage = () => { + if (!error) return null; + + if (error.errors && error.errors.length > 0) { + return ( + + Import failed due to validation errors +
    + {error.errors.map((e, idx) => ( +
  • + Row {e.row}: {e.message} +
  • + ))} +
+
+ ); + } + + return {error.message || 'Import failed'}; + }; + /* ------------------ UI ------------------ */ return ( - +
MyOpenCRE

@@ -120,7 +175,7 @@ export const MyOpenCRE = () => {

- Start by downloading the CRE catalogue below, then map your standard’s controls or sections to CRE IDs + Start by downloading the CRE catalogue below, then map your standard's controls or sections to CRE IDs in the spreadsheet.

@@ -143,7 +198,8 @@ export const MyOpenCRE = () => { )} - {error && {error}} + {renderErrorMessage()} + {info && {info}} {success && ( diff --git a/application/utils/spreadsheet_parsers.py b/application/utils/spreadsheet_parsers.py index ffe72a64b..4c5397156 100644 --- a/application/utils/spreadsheet_parsers.py +++ b/application/utils/spreadsheet_parsers.py @@ -149,8 +149,8 @@ def validate_import_csv_rows(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]] headers = list(rows[0].keys()) - if not headers: - raise ValueError("CSV header row is missing") + if not headers or len(headers) < 2: + raise ValueError("Invalid CSV format or missing header row") if not any(h.startswith("CRE") for h in headers): raise ValueError("At least one CRE column is required") diff --git a/application/web/web_main.py b/application/web/web_main.py index 6738a0109..3837792d9 100644 --- a/application/web/web_main.py +++ b/application/web/web_main.py @@ -948,11 +948,16 @@ def import_from_cre_csv() -> Any: calculate_gap_analysis=calculate_gap_analysis, ) + import_type = "created" + if not new_cres and not standards: + import_type = "noop" + return jsonify( { "status": "success", "new_cres": [c.external_id for c in new_cres], "new_standards": len(standards), + "import_type": import_type, } ) From 13e5fdaabeb15597df6cd14822d0de513e472dfc Mon Sep 17 00:00:00 2001 From: PRATEEK SINGH <110760231+PRAteek-singHWY@users.noreply.github.com> Date: Mon, 2 Mar 2026 01:31:53 +0530 Subject: [PATCH 02/26] MyOpenCRE: add CSV import preview, confirmation step, and improved UX safeguards (#685) feat(myopencre): add CSV import preview and confirmation flow --- .../src/pages/MyOpenCRE/MyOpenCRE.scss | 4 + .../src/pages/MyOpenCRE/MyOpenCRE.tsx | 126 +++++++++++++++++- 2 files changed, 125 insertions(+), 5 deletions(-) diff --git a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss index 072c10e8c..0414469f4 100644 --- a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss +++ b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss @@ -12,4 +12,8 @@ .myopencre-disabled { opacity: 0.7; +} + +.myopencre-preview { + margin-top: 1rem; } \ No newline at end of file diff --git a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx index 10da0ac98..e9e3b9a1a 100644 --- a/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx +++ b/application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx @@ -1,6 +1,6 @@ import './MyOpenCRE.scss'; -import React, { useState } from 'react'; +import React, { useRef, useState } from 'react'; import { Button, Container, Form, Header, Message } from 'semantic-ui-react'; import { useEnvironment } from '../../hooks'; @@ -19,6 +19,13 @@ type ImportErrorResponse = { errors?: RowValidationError[]; }; +type CsvPreview = { + rows: number; + creMappings: number; + uniqueSections: number; + creColumns: string[]; +}; + export const MyOpenCRE = () => { const { apiUrl } = useEnvironment(); @@ -30,6 +37,10 @@ export const MyOpenCRE = () => { const [error, setError] = useState(null); const [success, setSuccess] = useState(null); const [info, setInfo] = useState(null); + const [preview, setPreview] = useState(null); + const [confirmedImport, setConfirmedImport] = useState(false); + + const fileInputRef = useRef(null); /* ------------------ CSV DOWNLOAD ------------------ */ @@ -64,12 +75,60 @@ export const MyOpenCRE = () => { } }; + /* ------------------ CSV PREVIEW ------------------ */ + + const generateCsvPreview = async (file: File) => { + const text = await file.text(); + const lines = text.split('\n').filter(Boolean); + + if (lines.length < 2) { + setPreview(null); + return; + } + + const headers = lines[0].split(',').map((h) => h.trim()); + const rows = lines.slice(1); + + const creColumns = headers.filter((h) => h.startsWith('CRE')); + let creMappings = 0; + const sectionSet = new Set(); + + rows.forEach((line) => { + const values = line.split(','); + const rowObj: Record = {}; + + headers.forEach((h, i) => { + rowObj[h] = (values[i] || '').trim(); + }); + + const name = (rowObj['standard|name'] || '').trim(); + const id = (rowObj['standard|id'] || '').trim(); + + if (name || id) { + sectionSet.add(`${name}|${id}`); + } + + creColumns.forEach((col) => { + if (rowObj[col]) creMappings += 1; + }); + }); + + setPreview({ + rows: rows.length, + creMappings, + uniqueSections: sectionSet.size, + creColumns, + }); + }; + /* ------------------ FILE SELECTION ------------------ */ const onFileChange = (e: React.ChangeEvent) => { setError(null); setSuccess(null); setInfo(null); + setPreview(null); + setConfirmedImport(false); if (!e.target.files || e.target.files.length === 0) return; @@ -87,12 +146,13 @@ export const MyOpenCRE = () => { } setSelectedFile(file); + generateCsvPreview(file); }; /* ------------------ CSV UPLOAD ------------------ */ const uploadCsv = async () => { - if (!selectedFile) return; + if (!selectedFile || !confirmedImport) return; setLoading(true); setError(null); @@ -125,16 +185,25 @@ export const MyOpenCRE = () => { setInfo( 'Import completed successfully, but no new CREs or standards were added because all mappings already exist.' ); + } else if (payload.import_type === 'empty') { + setInfo('The uploaded CSV did not contain any importable rows. No changes were made.'); } else { setSuccess(payload); } - setSelectedFile(null); + + setConfirmedImport(false); + setPreview(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } } catch (err: any) { setError({ success: false, type: 'CLIENT_ERROR', message: err.message || 'Unexpected error during import', }); + setPreview(null); + setConfirmedImport(false); } finally { setLoading(false); } @@ -211,15 +280,62 @@ export const MyOpenCRE = () => { )} + {confirmedImport && !loading && !success && !error && ( + + CSV validated successfully. Click Upload CSV to start importing. + + )} + + {preview && ( + + Import Preview +
    +
  • Rows detected: {preview.rows}
  • +
  • CRE mappings found: {preview.creMappings}
  • +
  • Unique standard sections: {preview.uniqueSections}
  • +
  • CRE columns detected: {preview.creColumns.join(', ')}
  • +
+ + + + +
+ )} +
- +