Skip to content
Open
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
52 changes: 52 additions & 0 deletions application/cmd/cre_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from alive_progress import alive_bar
from application.prompt_client import prompt_client as prompt_client
from application.utils import gap_analysis
from application.prompt_client.prompt_client import SIMILARITY_THRESHOLD

logging.basicConfig()
logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -314,6 +315,57 @@ def register_standard(
redis.wait_for_jobs(jobs)
conn.set(standard_hash, value="")

def suggest_cre_mappings(
standard_entries: List[defs.Standard],
collection: db.Node_collection,
confidence_threshold: float = SIMILARITY_THRESHOLD,
) -> Dict[str, Any]:
"""
Given a list of Standard entries, suggest CRE mappings using
cosine similarity on existing embeddings.

Returns high-confidence matches and flags low-confidence ones
for human review.

Args:
standard_entries: list of Standard nodes to map
collection: database connection
confidence_threshold: minimum similarity score to auto-map

Returns:
Dict with 'mapped' (high confidence) and 'needs_review' (low confidence) lists
"""
if not standard_entries:
logger.warning("suggest_cre_mappings() called with no standard_entries")
return {"mapped": [], "needs_review": []}

ph = prompt_client.PromptHandler(database=collection)
results: Dict[str, Any] = {"mapped": [], "needs_review": []}

for node in standard_entries:
text = " ".join(filter(None, [node.name, node.section, node.description]))
if not text.strip():
continue
embedding = ph.get_text_embeddings(text)
cre_id, similarity = ph.get_id_of_most_similar_cre_paginated(
embedding, similarity_threshold=confidence_threshold
)
entry = {
"standard": node.todict(),
"suggested_cre_id": cre_id,
"confidence": round(float(similarity), 4) if similarity else None,
}
if cre_id and similarity and similarity >= confidence_threshold:
results["mapped"].append(entry)
else:
results["needs_review"].append(entry)

logger.info(
f"suggest_cre_mappings: {len(results['mapped'])} mapped, "
f"{len(results['needs_review'])} need review"
)
return results


def parse_standards_from_spreadsheeet(
cre_file: List[Dict[str, Any]],
Expand Down
210 changes: 133 additions & 77 deletions application/frontend/src/pages/MyOpenCRE/MyOpenCRE.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,40 @@
import './MyOpenCRE.scss';

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

import { Button, Container, Form, Header, Label, Loader, Message, Table } from 'semantic-ui-react';
import { useEnvironment } from '../../hooks';

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

// Upload enabled only for local/dev
const isUploadEnabled = apiUrl !== '/rest/v1';

// CSV import state
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);
const [importLoading, setImportLoading] = useState(false);
const [importError, setImportError] = useState<string | null>(null);
const [importSuccess, setImportSuccess] = useState<any | null>(null);

/* ------------------ CSV DOWNLOAD ------------------ */
// AI suggest state
const [suggestLoading, setSuggestLoading] = useState(false);
const [suggestError, setSuggestError] = useState<string | null>(null);
const [mapped, setMapped] = useState<any[]>([]);
const [needsReview, setNeedsReview] = useState<any[]>([]);
const [suggestFileName, setSuggestFileName] = useState<string>('');

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

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) {
Expand All @@ -48,96 +45,92 @@ export const MyOpenCRE = () => {

const downloadTemplate = () => {
const headers = ['standard_name', 'standard_section', 'cre_id', 'notes'];

const csvContent = headers.join(',') + '\n';

const blob = new Blob([csvContent], {
type: 'text/csv;charset=utf-8;',
});

const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');

link.href = url;
link.setAttribute('download', 'myopencre_mapping_template.csv');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};

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

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

setImportError(null);
setImportSuccess(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.');
setImportError('Please upload a valid CSV file.');
e.target.value = '';
setSelectedFile(null);
return;
}

setSelectedFile(file);
};

/* ------------------ CSV UPLOAD ------------------ */

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

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

setImportLoading(true);
setImportError(null);
setImportSuccess(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.'
);
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);
setImportSuccess(result);
setSelectedFile(null);
} catch (err: any) {
setError(err.message || 'Unexpected error during import');
setImportError(err.message || 'Unexpected error during import');
} finally {
setLoading(false);
setImportLoading(false);
}
};

/* ------------------ UI ------------------ */
const handleSuggestUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setSuggestFileName(file.name);
setSuggestLoading(true);
setSuggestError(null);
setMapped([]);
setNeedsReview([]);
const formData = new FormData();
formData.append('cre_csv', file);
try {
const response = await fetch(`${apiUrl}/suggest_cre_mappings`, {
method: 'POST',
body: formData,
});
if (!response.ok) throw new Error(`Server error: ${response.status}`);
const result = await response.json();
setMapped(result.mapped || []);
setNeedsReview(result.needs_review || []);
} catch (err: any) {
setSuggestError(err.message || 'An error occurred while processing your file.');
} finally {
setSuggestLoading(false);
}
};

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

<div className="myopencre-section">
Expand All @@ -149,46 +142,109 @@ export const MyOpenCRE = () => {
</Button>
</div>

<div className="myopencre-section myopencre-upload">
<Header as="h3">Upload Mapping CSV</Header>
{/* AI Suggest Section */}
<div className="myopencre-section" style={{ marginTop: '2rem' }}>
<Header as="h3">AI-Suggested CRE Mappings</Header>
<p>Upload your standard's CSV to get automatic CRE mapping suggestions powered by AI.</p>

<Button as="label" htmlFor="suggest-upload" secondary>
{suggestFileName ? `Uploaded: ${suggestFileName}` : 'Upload Standard for AI Suggestions'}
<input id="suggest-upload" type="file" accept=".csv" hidden onChange={handleSuggestUpload} />
</Button>

{suggestLoading && <Loader active inline="centered" style={{ marginTop: '1rem' }} content="Analyzing your standard..." />}
{suggestError && <Message negative style={{ marginTop: '1rem' }}><Message.Header>Error</Message.Header><p>{suggestError}</p></Message>}

{mapped.length > 0 && (
<>
<Header as="h4" style={{ marginTop: '1.5rem' }}>
Suggested Mappings <Label color="green">{mapped.length} matched</Label>
</Header>
<Table celled compact>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Standard Section</Table.HeaderCell>
<Table.HeaderCell>Suggested CRE</Table.HeaderCell>
<Table.HeaderCell>Confidence</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{mapped.map((item, idx) => (
<Table.Row key={idx}>
<Table.Cell>{item.standard?.section || item.standard?.name}</Table.Cell>
<Table.Cell>
<a href={`/node/CRE/${item.suggested_cre_id}`} target="_blank" rel="noreferrer">
{item.suggested_cre_id}
</a>
</Table.Cell>
<Table.Cell>
<Label color={item.confidence >= 0.85 ? 'green' : 'yellow'}>
{(item.confidence * 100).toFixed(1)}%
</Label>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</>
)}

{needsReview.length > 0 && (
<>
<Header as="h4" style={{ marginTop: '1.5rem' }}>
Needs Review <Label color="orange">{needsReview.length} items</Label>
</Header>
<p>These controls could not be automatically mapped and require manual review.</p>
<Table celled compact>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Standard Section</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{needsReview.map((item, idx) => (
<Table.Row key={idx}>
<Table.Cell>{item.standard?.section || item.standard?.name}</Table.Cell>
<Table.Cell>{item.standard?.description || '—'}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</>
)}
</div>

{/* CSV Import Section */}
<div className="myopencre-section myopencre-upload" style={{ marginTop: '2rem' }}>
<Header as="h3">Import Standard into OpenCRE</Header>
<p>Upload your completed mapping spreadsheet to import your standard into OpenCRE.</p>

{!isUploadEnabled && (
<Message info className="myopencre-disabled">
CSV upload is disabled on hosted environments due to resource constraints.
<br />
Please run OpenCRE locally to enable standard imports.
CSV upload is disabled on hosted environments. Run OpenCRE locally with CRE_ALLOW_IMPORT=true.
</Message>
)}

{error && <Message negative>{error}</Message>}

{success && (
{importError && <Message negative>{importError}</Message>}
{importSuccess && (
<Message positive>
<strong>Import successful</strong>
<ul>
<li>New CREs added: {success.new_cres?.length ?? 0}</li>
<li>Standards imported: {success.new_standards}</li>
<li>New CREs added: {importSuccess.new_cres?.length ?? 0}</li>
<li>Standards imported: {importSuccess.new_standards}</li>
</ul>
</Message>
)}

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

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