Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions application/frontend/src/pages/MyOpenCRE/MyOpenCRE.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.myopencre-upload {
margin-top: 2rem;
}
214 changes: 214 additions & 0 deletions application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import React, { useState } from 'react';
import { Button, Container, Form, Header, Message } from 'semantic-ui-react';

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

export const MyOpenCRE = () => {
const { apiUrl } = useEnvironment();

<<<<<<< Updated upstream
const downloadCreCsv = async () => {
try {
const baseUrl = apiUrl || window.location.origin;
const backendUrl = baseUrl.includes('localhost') ? 'http://127.0.0.1:5000' : baseUrl;

const response = await fetch(`${backendUrl}/cre_csv`, {
method: 'GET',
headers: {
Accept: 'text/csv',
},
=======
/**
* Upload is enabled only in local/dev environments.
* In prod, apiUrl === '/rest/v1'
*/
const isUploadEnabled = apiUrl !== '/rest/v1';

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

/* ------------------ CSV DOWNLOAD ------------------ */

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

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

<<<<<<< Updated upstream
// Upload enabled locally, disabled on hosted OpenCRE (Heroku)
const isUploadEnabled = !apiUrl.includes('opencre.org');

const [selectedFile, setSelectedFile] = useState<File | null>(null);

const onFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
=======
/* ------------------ FILE SELECTION ------------------ */

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

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

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

<<<<<<< Updated upstream
// Client-side CSV validation
if (!file.name.toLowerCase().endsWith('.csv')) {
alert('Please upload a valid CSV file.');
=======
// Enforce CSV only
if (!file.name.toLowerCase().endsWith('.csv')) {
setError('Please upload a valid CSV file.');
>>>>>>> Stashed changes
e.target.value = '';
setSelectedFile(null);
return;
}

setSelectedFile(file);
};

<<<<<<< Updated upstream
=======
/* ------------------ CSV UPLOAD ------------------ */

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

setLoading(true);
setError(null);
setSuccess(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.'
);
}

if (!response.ok) {
const text = await response.text();
throw new Error(text || 'CSV import failed');
}

const result = await response.json();
setSuccess(result);
setSelectedFile(null);
} catch (err: any) {
setError(err.message || 'Unexpected error during import');
} finally {
setLoading(false);
}
};

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

>>>>>>> Stashed changes
return (
<Container style={{ marginTop: '3rem' }}>
<Header as="h1">MyOpenCRE</Header>

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

<p>
Start by downloading the CRE catalogue below, then map your standard’s controls or sections to CRE IDs
in the spreadsheet.
</p>

<Button primary onClick={downloadCreCsv}>
Download CRE Catalogue (CSV)
</Button>

<Header as="h3" style={{ marginTop: '2rem' }}>
Upload Mapping CSV
</Header>

<p>Upload your completed mapping spreadsheet to import your standard into OpenCRE.</p>

{!isUploadEnabled && (
<Message info>
CSV upload is disabled on hosted environments due to resource constraints.
<br />
Please run OpenCRE locally to enable standard imports.
</Message>
)}

<<<<<<< Updated upstream
<Form>
<Form.Field>
<input type="file" accept=".csv" disabled={!isUploadEnabled} onChange={onFileChange} />
=======
{error && <Message negative>{error}</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>
)}

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

<Button
primary
<<<<<<< Updated upstream
disabled={!isUploadEnabled || !selectedFile}
onClick={() => alert('CSV import will be implemented in a follow-up PR.')}
=======
loading={loading}
disabled={!isUploadEnabled || !selectedFile || loading}
onClick={uploadCsv}
>>>>>>> Stashed changes
>
Upload CSV
</Button>
</Form>
</Container>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@
color: var(--muted-foreground);
height: 1rem;
width: 1rem;
pointer-events: none;
z-index: 1;
}

input {
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
7 changes: 7 additions & 0 deletions application/frontend/src/scaffolding/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Button } from 'semantic-ui-react';

import { ClearFilterButton } from '../../components/FilterButton/FilterButton';
import { useLocationFromOutsideRoute } from '../../hooks/useLocationFromOutsideRoute';
import { MyOpenCRE } from '../../pages/MyOpenCRE/MyOpenCRE';
import { SearchBar } from '../../pages/Search/components/SearchBar';

export const Header = () => {
Expand Down Expand Up @@ -49,6 +50,9 @@ export const Header = () => {
<a href="/explorer" className="nav-link">
Explorer
</a>
<Link to="/myopencre" className="nav-link">
MyOpenCRE
</Link>
</div>

<div>
Expand Down Expand Up @@ -137,6 +141,9 @@ export const Header = () => {
<a href="/explorer" className="nav-link" onClick={closeMobileMenu}>
Explorer
</a>
<a href="/myopencre" className="nav-link" onClick={MyOpenCRE}>
MyOpenCRE
</a>
</div>

<div className="mobile-auth">
Expand Down
13 changes: 0 additions & 13 deletions application/frontend/src/scaffolding/Header/header.scss
Original file line number Diff line number Diff line change
Expand Up @@ -301,16 +301,3 @@
display: block;
}
}

.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
2 changes: 1 addition & 1 deletion application/frontend/www/bundle.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions application/tests/data/osib_example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ schema: v0.90
date: 2021/11/04
children:
"OWASP":
aliases: ["Open Worldwide Application Security Project", "owasp"]
aliases: ["Open Web Application Security Project", "owasp"]
attributes:
sources_i18n:
en:
name: "Open Worldwide Application Security Project&reg; (OWASP)"
name: "Open Web Application Security Project&reg; (OWASP)"
children:
"ZAP":
attributes:
Expand Down
Loading