Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
8c929ce
Merge branch 'main' into myopencre-csv-download
PRAteek-singHWY Jan 3, 2026
3ddf7af
Merge branch 'main' into myopencre-csv-download
PRAteek-singHWY Jan 3, 2026
a83b918
chore(osib): update OWASP name to Open Worldwide Application Security…
PRAteek-singHWY Jan 3, 2026
735eb7b
feat(myopencre): enable CSV download of all CREs
PRAteek-singHWY Dec 17, 2025
57118a0
feat(myopencre): add CSV upload UI and wire to existing import endpoint
PRAteek-singHWY Dec 18, 2025
fcd3dbd
fix: correct SCSS block after merge conflict
PRAteek-singHWY Dec 26, 2025
197653b
feat(myopencre): add export-compatible CSV import validation
PRAteek-singHWY Dec 28, 2025
2bba24d
Merge branch 'main' into myopencre-csv-import-validation
PRAteek-singHWY Jan 3, 2026
d3e1ab4
refactor(csv-import): move CSV validation into spreadsheet parser
PRAteek-singHWY Jan 4, 2026
1f90183
feat(myopencre): surface CSV import errors in UI
PRAteek-singHWY Dec 28, 2025
1704449
refactor(csv-import): move validation into spreadsheet parser and fix…
PRAteek-singHWY Jan 4, 2026
5f19150
style(myopencre): move container spacing to SCSS
PRAteek-singHWY Dec 31, 2025
a819f1c
refactor(myopencre): move CSV validation to parser and add noop impor…
PRAteek-singHWY Jan 4, 2026
30374cf
feat(myopencre): improve UI handling for no-op CSV imports
PRAteek-singHWY Dec 29, 2025
9bf05b4
feat(myopencre): add CSV import preview and confirmation flow
PRAteek-singHWY Dec 31, 2025
ce385fa
ui(myopencre): adjust header spacing and clean up MyOpenCRE styles
PRAteek-singHWY Jan 4, 2026
b433dd0
chore(myopencre): drop changes that belong to other stacked branches
PRAteek-singHWY Jan 4, 2026
f43d781
refactor(csv): move import validation to spreadsheet parser entry point
PRAteek-singHWY Jan 4, 2026
c7b62c4
style(csv): format spreadsheet parser with black
PRAteek-singHWY Jan 4, 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
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;
}
330 changes: 330 additions & 0 deletions application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,330 @@
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;
};

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}
</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>

{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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@
color: var(--muted-foreground);
height: 1rem;
width: 1rem;
pointer-events: none;
z-index: 1;
}

input {
padding: 0.5rem 1rem 0.5rem 2.5rem;
width: 16rem;
Expand Down
7 changes: 7 additions & 0 deletions application/frontend/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ExplorerCircles } from './pages/Explorer/visuals/circles/circles';
import { ExplorerForceGraph } from './pages/Explorer/visuals/force-graph/forceGraph';
import { GapAnalysis } from './pages/GapAnalysis/GapAnalysis';
import { MembershipRequired } from './pages/MembershipRequired/MembershipRequired';
import { MyOpenCRE } from './pages/MyOpenCRE/MyOpenCRE';
import { SearchName } from './pages/Search/SearchName';
import { StandardSection } from './pages/Standard/StandardSection';

Expand All @@ -31,6 +32,12 @@ export interface IRoute {
}

export const ROUTES: IRoute[] = [
{
path: '/myopencre',
component: MyOpenCRE,
showFilter: false,
},

{
path: INDEX,
component: SearchPage,
Expand Down
Loading