Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
944ca22
feat(myopencre): add initial MyOpenCRE page and CSV template download
PRAteek-singHWY Dec 15, 2025
c3a747e
Merge branch 'main' into myopencre-csv-download
PRAteek-singHWY Dec 16, 2025
54235ad
feat(myopencre): add initial MyOpenCRE page and CSV template download
PRAteek-singHWY Dec 17, 2025
64c8b44
Merge branch 'main' into myopencre-csv-download
PRAteek-singHWY Dec 17, 2025
a5f56ef
Resolve nav conflicts and standardize MyOpenCRE links
PRAteek-singHWY Dec 26, 2025
a4f988f
chore: resolve merge conflicts with upstream main
PRAteek-singHWY Dec 26, 2025
35d5a0f
chore: resolve header merge conflict
PRAteek-singHWY Dec 26, 2025
b285409
fix(header): restore sr-only utility for accessibility
PRAteek-singHWY Dec 26, 2025
e9d84f2
feat(myopencre): enable CSV download of all CREs
PRAteek-singHWY Dec 17, 2025
8c67e6d
feat(myopencre): add CSV upload UI and wire to existing import endpoint
PRAteek-singHWY Dec 18, 2025
288a8b3
fix: correct SCSS block after merge conflict
PRAteek-singHWY Dec 26, 2025
53b85b0
feat(myopencre): add export-compatible CSV import validation
PRAteek-singHWY Dec 28, 2025
865e5d5
feat(myopencre): surface CSV import errors in UI
PRAteek-singHWY Dec 28, 2025
49d4513
feat(myopencre): improve UI handling for no-op CSV imports
PRAteek-singHWY Dec 29, 2025
756e128
style(myopencre): move container spacing to SCSS
PRAteek-singHWY Dec 31, 2025
4cf74df
feat(myopencre): add CSV import preview and confirmation flow
PRAteek-singHWY Dec 31, 2025
b6c8402
ui(myopencre): add inline help and clarify CSV preparation
PRAteek-singHWY Jan 2, 2026
cda3090
feat(myopencre): improve backend CSV validation and error reporting
PRAteek-singHWY Jan 3, 2026
059d718
fix(myopencre): surface backend CSV validation examples in UI
PRAteek-singHWY Jan 3, 2026
c6e7909
Merge branch 'main' into myopencre-csv-validation-errors
PRAteek-singHWY Jan 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.11
3.11.9
2 changes: 1 addition & 1 deletion application/defs/osib_defs.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ def paths_to_osib(
attributes=Node_attributes(
sources_i18n={
Lang("en"): _Source(
name="Open Worldwide Application Security Project",
name="Open Web Application Security Project",
source="https://owasp.org",
)
}
Expand Down
26 changes: 26 additions & 0 deletions application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.myopencre-container {
margin-top: 3rem;
}

.myopencre-section {
margin-top: 2rem;
}

.myopencre-upload {
margin-top: 1.5rem;
}

.myopencre-disabled {
opacity: 0.7;
}
.myopencre-preview {
margin-bottom: 1rem;
}
.myopencre-intro {
font-size: 1.05rem;
font-weight: 400;
margin-bottom: 0.5rem;
}
.cursor-pointer summary {
cursor: pointer;
}
358 changes: 358 additions & 0 deletions application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
import './MyOpenCRE.scss';

import React, { useRef, useState } from 'react';
import { Button, Container, Form, Header, Message } from 'semantic-ui-react';

import { useEnvironment } from '../../hooks';

type RowValidationError = {
row: number;
code: string;
message: string;
column?: string;
example?: string;
};

type ImportErrorResponse = {
success: false;
type: string;
message?: string;
errors?: RowValidationError[];
};

export const MyOpenCRE = () => {
const { apiUrl } = useEnvironment();
const isUploadEnabled = apiUrl !== '/rest/v1';

const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<ImportErrorResponse | null>(null);
const [success, setSuccess] = useState<any | null>(null);

const [preview, setPreview] = useState<{
rows: number;
creMappings: number;
uniqueSections: number;
creColumns: string[];
} | null>(null);

const [info, setInfo] = useState<string | null>(null);
const [confirmedImport, setConfirmedImport] = useState(false);

const fileInputRef = useRef<HTMLInputElement | null>(null);

/* ------------------ FILE SELECTION ------------------ */

const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setError(null);
setSuccess(null);
setInfo(null);
setConfirmedImport(false);
setPreview(null);

if (!e.target.files || e.target.files.length === 0) return;

const file = e.target.files[0];

if (!file.name.toLowerCase().endsWith('.csv')) {
setError({
success: false,
type: 'FILE_ERROR',
message: 'Please upload a valid CSV file.',
});
e.target.value = '';
setSelectedFile(null);
return;
}

setSelectedFile(file);
generateCsvPreview(file);
};
/* ------------------ CSV DOWNLOAD ------------------ */

const downloadCreCsv = async () => {
try {
const response = await fetch(`${apiUrl}/cre_csv`, {
method: 'GET',
headers: { Accept: 'text/csv' },
});

if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}

const blob = await response.blob();
const url = window.URL.createObjectURL(blob);

const link = document.createElement('a');
link.href = url;
link.download = 'opencre-cre-mapping.csv';
document.body.appendChild(link);
link.click();

document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (err) {
console.error('CSV download failed:', err);
alert('Failed to download CRE CSV');
}
};
/* ------------------ CSV UPLOAD ------------------ */

const uploadCsv = async () => {
if (!selectedFile || !confirmedImport) return;

setLoading(true);
setError(null);
setSuccess(null);
setInfo(null);

const formData = new FormData();
formData.append('cre_csv', selectedFile);

try {
const response = await fetch(`${apiUrl}/cre_csv_import`, {
method: 'POST',
body: formData,
});

if (response.status === 403) {
throw new Error(
'CSV import is disabled on hosted environments. Run OpenCRE locally with CRE_ALLOW_IMPORT=true.'
);
}

const payload = await response.json();

if (!response.ok) {
setError(payload);
setPreview(null);
setConfirmedImport(false);
return;
}

if (payload.import_type === 'noop') {
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);
}

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);
}
};

/* ------------------ ERROR RENDERING ------------------ */

const renderErrorMessage = () => {
if (!error) return null;

if (error.errors && error.errors.length > 0) {
return (
<Message negative>
<strong>Import failed due to validation errors</strong>
<ul>
{error.errors.map((e, idx) => (
<li key={idx}>
<strong>Row {e.row}:</strong> {e.message}
{e.example && (
<div style={{ marginTop: '4px', opacity: 0.8 }}>
<em>Example:</em> <code>{e.example}</code>
</div>
)}
</li>
))}
</ul>
</Message>
);
}

return <Message negative>{error.message || 'Import failed'}</Message>;
};

/* ------------------ 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<string>();

rows.forEach((line) => {
const values = line.split(',');
const rowObj: Record<string, string> = {};

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,
});
};

/* ------------------ UI ------------------ */

return (
<Container className="myopencre-container">
<Header as="h1">MyOpenCRE</Header>

<p className="myopencre-intro">
MyOpenCRE allows you to map your own security standard (e.g. SOC2) to OpenCRE Common Requirements
using a CSV spreadsheet.
</p>

<p className="myopencre-intro">
Start by downloading the CRE catalogue below, then map your standard’s controls or sections to CRE IDs
in the spreadsheet.
</p>
<div className="myopencre-section">
<Button primary onClick={downloadCreCsv}>
Download CRE Catalogue (CSV)
</Button>
</div>

<div className="myopencre-section myopencre-upload">
<Header as="h3">Upload Mapping CSV</Header>
<Message info className="cursor-pointer">
<details>
<summary>
<strong>How to prepare your CSV</strong>
</summary>

<ul>
<li>Start from the downloaded CRE Catalogue CSV.</li>
<li>
Fill <code>standard|name</code> and <code>standard|id</code> for your standard.
</li>
<li>
Map your controls using CRE columns (<code>CRE 0</code>, <code>CRE 1</code>, …).
</li>

<li>
CRE values must be in the format <code>&lt;CRE-ID&gt;|&lt;Name&gt;</code>
<br />
<em>Example:</em> <code>616-305|Development processes for security</code>
</li>
</ul>
</details>
</Message>
{renderErrorMessage()}
{info && <Message info>{info}</Message>}
{success && (
<Message positive>
<strong>Import successful</strong>
<ul>
<li>New CREs added: {success.new_cres?.length ?? 0}</li>
<li>Standards imported: {success.new_standards}</li>
</ul>
</Message>
)}

{confirmedImport && !loading && !success && !error && (
<Message positive>
CSV validated successfully. Click <strong>Upload CSV</strong> to start importing.
</Message>
)}

{preview && (
<Message info className="myopencre-preview">
<strong>Import Preview</strong>
<ul>
<li>Rows detected: {preview.rows}</li>
<li>CRE mappings found: {preview.creMappings}</li>
<li>Unique standard sections: {preview.uniqueSections}</li>
<li>CRE columns detected: {preview.creColumns.join(', ')}</li>
</ul>

<Button
primary
size="small"
onClick={() => {
setPreview(null);
setConfirmedImport(true);
}}
>
Confirm Import
</Button>

<Button
size="small"
onClick={() => {
setPreview(null);
setConfirmedImport(false);
setSelectedFile(null);
if (fileInputRef.current) fileInputRef.current.value = '';
}}
>
Cancel
</Button>
</Message>
)}

<Form>
<Form.Field>
<input
ref={fileInputRef}
type="file"
accept=".csv"
disabled={!isUploadEnabled || loading || !!preview}
onChange={onFileChange}
/>
</Form.Field>

<Button
primary
loading={loading}
disabled={!isUploadEnabled || !selectedFile || !confirmedImport || loading}
onClick={uploadCsv}
>
Upload CSV
</Button>
</Form>
</div>
</Container>
);
};
Loading