fix: Add frontend files (not as submodule)

This commit is contained in:
2026-01-27 04:08:35 +00:00
parent 80f32b0412
commit 49ab6c6f71
20 changed files with 1551 additions and 1 deletions

Submodule frontend deleted from 4f0cb0a6cc

23
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

70
frontend/README.md Normal file
View File

@@ -0,0 +1,70 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
The page will reload when you make changes.\
You may also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can't go back!**
If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

40
frontend/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^13.5.0",
"lucide-react": "^0.563.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
frontend/public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
frontend/public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

38
frontend/src/App.css Normal file
View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

628
frontend/src/App.js Normal file
View File

@@ -0,0 +1,628 @@
import React, { useState, useEffect } from 'react';
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2 } from 'lucide-react';
const API_BASE = 'http://192.168.2.117:3001/api';
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
export default function App() {
const [searchQuery, setSearchQuery] = useState('');
const [selectedVendor, setSelectedVendor] = useState('All Vendors');
const [selectedSeverity, setSelectedSeverity] = useState('All Severities');
const [selectedCVE, setSelectedCVE] = useState(null);
const [selectedDocuments, setSelectedDocuments] = useState([]);
const [cves, setCves] = useState([]);
const [vendors, setVendors] = useState(['All Vendors']);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [cveDocuments, setCveDocuments] = useState({});
const [quickCheckCVE, setQuickCheckCVE] = useState('');
const [quickCheckResult, setQuickCheckResult] = useState(null);
const [showAddCVE, setShowAddCVE] = useState(false);
const [newCVE, setNewCVE] = useState({
cve_id: '',
vendor: '',
severity: 'Medium',
description: '',
published_date: new Date().toISOString().split('T')[0]
});
const [uploadingFile, setUploadingFile] = useState(false);
// Fetch CVEs from API
useEffect(() => {
fetchCVEs();
fetchVendors();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Refetch when filters change
useEffect(() => {
fetchCVEs();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchQuery, selectedVendor, selectedSeverity]);
const fetchCVEs = async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (searchQuery) params.append('search', searchQuery);
if (selectedVendor !== 'All Vendors') params.append('vendor', selectedVendor);
if (selectedSeverity !== 'All Severities') params.append('severity', selectedSeverity);
const response = await fetch(`${API_BASE}/cves?${params}`);
if (!response.ok) throw new Error('Failed to fetch CVEs');
const data = await response.json();
setCves(data);
} catch (err) {
setError(err.message);
console.error('Error fetching CVEs:', err);
} finally {
setLoading(false);
}
};
const fetchVendors = async () => {
try {
const response = await fetch(`${API_BASE}/vendors`);
if (!response.ok) throw new Error('Failed to fetch vendors');
const data = await response.json();
setVendors(['All Vendors', ...data]);
} catch (err) {
console.error('Error fetching vendors:', err);
}
};
const fetchDocuments = async (cveId) => {
if (cveDocuments[cveId]) return;
try {
const response = await fetch(`${API_BASE}/cves/${cveId}/documents`);
if (!response.ok) throw new Error('Failed to fetch documents');
const data = await response.json();
setCveDocuments(prev => ({ ...prev, [cveId]: data }));
} catch (err) {
console.error('Error fetching documents:', err);
}
};
const quickCheckCVEStatus = async () => {
if (!quickCheckCVE.trim()) return;
try {
const response = await fetch(`${API_BASE}/cves/check/${quickCheckCVE.trim()}`);
if (!response.ok) throw new Error('Failed to check CVE');
const data = await response.json();
setQuickCheckResult(data);
} catch (err) {
console.error('Error checking CVE:', err);
setQuickCheckResult({ error: err.message });
}
};
const handleViewDocuments = async (cveId) => {
if (selectedCVE === cveId) {
setSelectedCVE(null);
} else {
setSelectedCVE(cveId);
await fetchDocuments(cveId);
}
};
const getSeverityColor = (severity) => {
const colors = {
'Critical': 'bg-red-100 text-red-800',
'High': 'bg-orange-100 text-orange-800',
'Medium': 'bg-yellow-100 text-yellow-800',
'Low': 'bg-blue-100 text-blue-800'
};
return colors[severity] || 'bg-gray-100 text-gray-800';
};
const toggleDocumentSelection = (docId) => {
setSelectedDocuments(prev =>
prev.includes(docId)
? prev.filter(id => id !== docId)
: [...prev, docId]
);
};
const exportSelectedDocuments = () => {
alert(`Exporting ${selectedDocuments.length} documents for report attachment`);
};
const handleAddCVE = async (e) => {
e.preventDefault();
try {
const response = await fetch(`${API_BASE}/cves`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newCVE)
});
if (!response.ok) throw new Error('Failed to add CVE');
alert(`CVE ${newCVE.cve_id} added successfully!`);
setShowAddCVE(false);
setNewCVE({
cve_id: '',
vendor: '',
severity: 'Medium',
description: '',
published_date: new Date().toISOString().split('T')[0]
});
fetchCVEs();
} catch (err) {
alert(`Error: ${err.message}`);
}
};
const handleFileUpload = async (cveId, vendor) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.pdf,.png,.jpg,.jpeg,.txt,.doc,.docx';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const docType = prompt(
'Document type (advisory, email, screenshot, patch, other):',
'advisory'
);
if (!docType) return;
const notes = prompt('Notes (optional):');
setUploadingFile(true);
const formData = new FormData();
formData.append('file', file);
formData.append('cveId', cveId);
formData.append('vendor', vendor);
formData.append('type', docType);
if (notes) formData.append('notes', notes);
try {
const response = await fetch(`${API_BASE}/cves/${cveId}/documents`, {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Failed to upload document');
alert(`Document uploaded successfully!`);
delete cveDocuments[cveId];
await fetchDocuments(cveId);
fetchCVEs();
} catch (err) {
alert(`Error: ${err.message}`);
} finally {
setUploadingFile(false);
}
};
fileInput.click();
};
const handleDeleteDocument = async (docId, cveId) => {
if (!window.confirm('Are you sure you want to delete this document?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/documents/${docId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete document');
alert('Document deleted successfully!');
delete cveDocuments[cveId];
await fetchDocuments(cveId);
fetchCVEs();
} catch (err) {
alert(`Error: ${err.message}`);
}
};
const filteredCVEs = cves;
return (
<div className="min-h-screen bg-gray-100 p-6">
<div className="max-w-7xl mx-auto">
{/* Header with Charter Branding */}
<div className="mb-8 flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">CVE Dashboard</h1>
<p className="text-gray-600">Query vulnerabilities, manage vendors, and attach documentation</p>
</div>
<button
onClick={() => setShowAddCVE(true)}
className="px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors flex items-center gap-2 shadow-md"
>
<span className="text-xl">+</span>
Add New CVE
</button>
</div>
{/* Add CVE Modal */}
{showAddCVE && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold text-gray-900">Add New CVE</h2>
<button
onClick={() => setShowAddCVE(false)}
className="text-gray-400 hover:text-gray-600"
>
<XCircle className="w-6 h-6" />
</button>
</div>
<form onSubmit={handleAddCVE} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
CVE ID *
</label>
<input
type="text"
required
placeholder="CVE-2024-1234"
value={newCVE.cve_id}
onChange={(e) => setNewCVE({...newCVE, cve_id: e.target.value.toUpperCase()})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Vendor *
</label>
<input
type="text"
required
placeholder="Microsoft, Cisco, Oracle, etc."
value={newCVE.vendor}
onChange={(e) => setNewCVE({...newCVE, vendor: e.target.value})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Severity *
</label>
<select
value={newCVE.severity}
onChange={(e) => setNewCVE({...newCVE, severity: e.target.value})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
>
<option value="Critical">Critical</option>
<option value="High">High</option>
<option value="Medium">Medium</option>
<option value="Low">Low</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description *
</label>
<textarea
required
placeholder="Brief description of the vulnerability"
value={newCVE.description}
onChange={(e) => setNewCVE({...newCVE, description: e.target.value})}
rows={3}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Published Date *
</label>
<input
type="date"
required
value={newCVE.published_date}
onChange={(e) => setNewCVE({...newCVE, published_date: e.target.value})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
/>
</div>
<div className="flex gap-3 pt-4">
<button
type="submit"
className="flex-1 px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors font-medium shadow-md"
>
Add CVE
</button>
<button
type="button"
onClick={() => setShowAddCVE(false)}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
>
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Quick Check with Charter Blue */}
<div className="bg-gradient-to-r from-blue-50 to-blue-100 rounded-lg shadow-md p-6 mb-6 border-2 border-[#0476D9]">
<h2 className="text-lg font-semibold text-gray-900 mb-3">Quick CVE Status Check</h2>
<div className="flex gap-3">
<input
type="text"
placeholder="Enter CVE ID (e.g., CVE-2024-1234)"
value={quickCheckCVE}
onChange={(e) => setQuickCheckCVE(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && quickCheckCVEStatus()}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
/>
<button
onClick={quickCheckCVEStatus}
className="px-6 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors font-medium shadow-md"
>
Check Status
</button>
</div>
{quickCheckResult && (
<div className={`mt-4 p-4 rounded-lg ${quickCheckResult.exists ? 'bg-green-50 border border-green-200' : 'bg-yellow-50 border border-yellow-200'}`}>
{quickCheckResult.error ? (
<div className="flex items-start gap-3">
<XCircle className="w-5 h-5 text-red-600 mt-0.5" />
<div>
<p className="font-medium text-red-900">Error</p>
<p className="text-sm text-red-700">{quickCheckResult.error}</p>
</div>
</div>
) : quickCheckResult.exists ? (
<div className="flex items-start gap-3">
<CheckCircle className="w-5 h-5 text-green-600 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-green-900"> CVE Addressed</p>
<div className="mt-2 space-y-1 text-sm text-gray-700">
<p><strong>Vendor:</strong> {quickCheckResult.cve.vendor}</p>
<p><strong>Severity:</strong> {quickCheckResult.cve.severity}</p>
<p><strong>Status:</strong> {quickCheckResult.cve.status}</p>
<p><strong>Documents:</strong> {quickCheckResult.cve.total_documents} attached</p>
<div className="mt-2 flex gap-3">
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.advisory ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{quickCheckResult.compliance.advisory ? '✓' : '✗'} Advisory
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.email ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
{quickCheckResult.compliance.email ? '✓' : '○'} Email
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.screenshot ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
{quickCheckResult.compliance.screenshot ? '✓' : '○'} Screenshot
</span>
</div>
</div>
</div>
</div>
) : (
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-yellow-600 mt-0.5" />
<div>
<p className="font-medium text-yellow-900">Not Found</p>
<p className="text-sm text-yellow-700">This CVE has not been addressed yet. No entry exists in the database.</p>
</div>
</div>
)}
</div>
)}
</div>
{/* Search and Filters */}
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700 mb-2">
<Search className="inline w-4 h-4 mr-1" />
Search CVEs
</label>
<input
type="text"
placeholder="CVE ID or description..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<Filter className="inline w-4 h-4 mr-1" />
Vendor
</label>
<select
value={selectedVendor}
onChange={(e) => setSelectedVendor(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
>
{vendors.map(vendor => (
<option key={vendor} value={vendor}>{vendor}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<AlertCircle className="inline w-4 h-4 mr-1" />
Severity
</label>
<select
value={selectedSeverity}
onChange={(e) => setSelectedSeverity(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
>
{severityLevels.map(level => (
<option key={level} value={level}>{level}</option>
))}
</select>
</div>
</div>
</div>
{/* Results Summary */}
<div className="mb-4 flex justify-between items-center">
<p className="text-gray-600">
Found {filteredCVEs.length} CVE{filteredCVEs.length !== 1 ? 's' : ''}
</p>
{selectedDocuments.length > 0 && (
<button
onClick={exportSelectedDocuments}
className="flex items-center gap-2 px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors shadow-md"
>
<Download className="w-4 h-4" />
Export {selectedDocuments.length} Document{selectedDocuments.length !== 1 ? 's' : ''} for Report
</button>
)}
</div>
{/* CVE List */}
{loading ? (
<div className="bg-white rounded-lg shadow-md p-12 text-center">
<Loader className="w-12 h-12 text-[#0476D9] mx-auto mb-4 animate-spin" />
<p className="text-gray-600">Loading CVEs...</p>
</div>
) : error ? (
<div className="bg-white rounded-lg shadow-md p-12 text-center">
<XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Error Loading CVEs</h3>
<p className="text-gray-600 mb-4">{error}</p>
<button
onClick={fetchCVEs}
className="px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] shadow-md"
>
Retry
</button>
</div>
) : (
<div className="space-y-4">
{filteredCVEs.map(cve => {
const documents = cveDocuments[cve.cve_id] || [];
return (
<div key={cve.cve_id} className="bg-white rounded-lg shadow-md border border-gray-200">
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-xl font-semibold text-gray-900">{cve.cve_id}</h3>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getSeverityColor(cve.severity)}`}>
{cve.severity}
</span>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${cve.doc_status === 'Complete' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
{cve.doc_status === 'Complete' ? '✓ Docs Complete' : '⚠ Incomplete'}
</span>
</div>
<p className="text-gray-700 mb-2">{cve.description}</p>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>Vendor: <span className="font-medium text-gray-700">{cve.vendor}</span></span>
<span>Published: {cve.published_date}</span>
<span>Status: <span className="font-medium text-gray-700">{cve.status}</span></span>
<span className="flex items-center gap-1">
<FileText className="w-4 h-4" />
{cve.document_count} document{cve.document_count !== 1 ? 's' : ''}
</span>
</div>
</div>
<button
onClick={() => handleViewDocuments(cve.cve_id)}
className="px-4 py-2 text-[#0476D9] hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-2 border border-[#0476D9]"
>
<Eye className="w-4 h-4" />
{selectedCVE === cve.cve_id ? 'Hide' : 'View'} Documents
</button>
</div>
{/* Documents Section */}
{selectedCVE === cve.cve_id && (
<div className="mt-4 pt-4 border-t border-gray-200">
<h4 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<FileText className="w-4 h-4" />
Attached Documents ({documents.length})
</h4>
{documents.length > 0 ? (
<div className="space-y-2">
{documents.map(doc => (
<div
key={doc.id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
<div className="flex items-center gap-3 flex-1">
<input
type="checkbox"
checked={selectedDocuments.includes(doc.id)}
onChange={() => toggleDocumentSelection(doc.id)}
className="w-4 h-4 text-[#0476D9] rounded focus:ring-2 focus:ring-[#0476D9]"
/>
<FileText className="w-5 h-5 text-gray-400" />
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{doc.name}</p>
<p className="text-xs text-gray-500 capitalize">
{doc.type} {doc.file_size}
{doc.notes && `${doc.notes}`}
</p>
</div>
</div>
<div className="flex gap-2">
<a
href={`http://192.168.2.117:3001/${doc.file_path}`}
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1 text-sm text-[#0476D9] hover:bg-blue-50 rounded transition-colors border border-[#0476D9]"
>
View
</a>
<button
onClick={() => handleDeleteDocument(doc.id, cve.cve_id)}
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded transition-colors border border-red-600 flex items-center gap-1"
>
<Trash2 className="w-3 h-3" />
Delete
</button>
</div>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500 italic">No documents attached yet</p>
)}
<button
onClick={() => handleFileUpload(cve.cve_id, cve.vendor)}
disabled={uploadingFile}
className="mt-3 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50 border border-gray-300"
>
<Upload className="w-4 h-4" />
{uploadingFile ? 'Uploading...' : 'Upload New Document'}
</button>
</div>
)}
</div>
</div>
);
})}
</div>
)}
{filteredCVEs.length === 0 && !loading && (
<div className="bg-white rounded-lg shadow-md p-12 text-center">
<AlertCircle className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No CVEs Found</h3>
<p className="text-gray-600">Try adjusting your search criteria or filters</p>
</div>
)}
</div>
</div>
);
}

598
frontend/src/App.js.v1 Normal file
View File

@@ -0,0 +1,598 @@
import React, { useState, useEffect } from 'react';
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader } from 'lucide-react';
const API_BASE = 'http://192.168.2.117:3001/api';
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
export default function App() {
const [searchQuery, setSearchQuery] = useState('');
const [selectedVendor, setSelectedVendor] = useState('All Vendors');
const [selectedSeverity, setSelectedSeverity] = useState('All Severities');
const [selectedCVE, setSelectedCVE] = useState(null);
const [selectedDocuments, setSelectedDocuments] = useState([]);
const [cves, setCves] = useState([]);
const [vendors, setVendors] = useState(['All Vendors']);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [cveDocuments, setCveDocuments] = useState({});
const [quickCheckCVE, setQuickCheckCVE] = useState('');
const [quickCheckResult, setQuickCheckResult] = useState(null);
const [showAddCVE, setShowAddCVE] = useState(false);
const [newCVE, setNewCVE] = useState({
cve_id: '',
vendor: '',
severity: 'Medium',
description: '',
published_date: new Date().toISOString().split('T')[0]
});
const [uploadingFile, setUploadingFile] = useState(false);
// Fetch CVEs from API
useEffect(() => {
fetchCVEs();
fetchVendors();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// Refetch when filters change
useEffect(() => {
fetchCVEs();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchQuery, selectedVendor, selectedSeverity]);
const fetchCVEs = async () => {
setLoading(true);
setError(null);
try {
const params = new URLSearchParams();
if (searchQuery) params.append('search', searchQuery);
if (selectedVendor !== 'All Vendors') params.append('vendor', selectedVendor);
if (selectedSeverity !== 'All Severities') params.append('severity', selectedSeverity);
const response = await fetch(`${API_BASE}/cves?${params}`);
if (!response.ok) throw new Error('Failed to fetch CVEs');
const data = await response.json();
setCves(data);
} catch (err) {
setError(err.message);
console.error('Error fetching CVEs:', err);
} finally {
setLoading(false);
}
};
const fetchVendors = async () => {
try {
const response = await fetch(`${API_BASE}/vendors`);
if (!response.ok) throw new Error('Failed to fetch vendors');
const data = await response.json();
setVendors(['All Vendors', ...data]);
} catch (err) {
console.error('Error fetching vendors:', err);
}
};
const fetchDocuments = async (cveId) => {
if (cveDocuments[cveId]) return;
try {
const response = await fetch(`${API_BASE}/cves/${cveId}/documents`);
if (!response.ok) throw new Error('Failed to fetch documents');
const data = await response.json();
setCveDocuments(prev => ({ ...prev, [cveId]: data }));
} catch (err) {
console.error('Error fetching documents:', err);
}
};
const quickCheckCVEStatus = async () => {
if (!quickCheckCVE.trim()) return;
try {
const response = await fetch(`${API_BASE}/cves/check/${quickCheckCVE.trim()}`);
if (!response.ok) throw new Error('Failed to check CVE');
const data = await response.json();
setQuickCheckResult(data);
} catch (err) {
console.error('Error checking CVE:', err);
setQuickCheckResult({ error: err.message });
}
};
const handleViewDocuments = async (cveId) => {
if (selectedCVE === cveId) {
setSelectedCVE(null);
} else {
setSelectedCVE(cveId);
await fetchDocuments(cveId);
}
};
const getSeverityColor = (severity) => {
const colors = {
'Critical': 'bg-red-100 text-red-800',
'High': 'bg-orange-100 text-orange-800',
'Medium': 'bg-yellow-100 text-yellow-800',
'Low': 'bg-blue-100 text-blue-800'
};
return colors[severity] || 'bg-gray-100 text-gray-800';
};
const toggleDocumentSelection = (docId) => {
setSelectedDocuments(prev =>
prev.includes(docId)
? prev.filter(id => id !== docId)
: [...prev, docId]
);
};
const exportSelectedDocuments = () => {
alert(`Exporting ${selectedDocuments.length} documents for report attachment`);
};
const handleAddCVE = async (e) => {
e.preventDefault();
try {
const response = await fetch(`${API_BASE}/cves`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newCVE)
});
if (!response.ok) throw new Error('Failed to add CVE');
alert(`CVE ${newCVE.cve_id} added successfully!`);
setShowAddCVE(false);
setNewCVE({
cve_id: '',
vendor: '',
severity: 'Medium',
description: '',
published_date: new Date().toISOString().split('T')[0]
});
fetchCVEs();
} catch (err) {
alert(`Error: ${err.message}`);
}
};
const handleFileUpload = async (cveId, vendor) => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.pdf,.png,.jpg,.jpeg,.txt,.doc,.docx';
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
const docType = prompt(
'Document type (advisory, email, screenshot, patch, other):',
'advisory'
);
if (!docType) return;
const notes = prompt('Notes (optional):');
setUploadingFile(true);
const formData = new FormData();
formData.append('file', file);
formData.append('cveId', cveId);
formData.append('vendor', vendor);
formData.append('type', docType);
if (notes) formData.append('notes', notes);
try {
const response = await fetch(`${API_BASE}/cves/${cveId}/documents`, {
method: 'POST',
body: formData
});
if (!response.ok) throw new Error('Failed to upload document');
alert(`Document uploaded successfully!`);
delete cveDocuments[cveId];
await fetchDocuments(cveId);
fetchCVEs();
} catch (err) {
alert(`Error: ${err.message}`);
} finally {
setUploadingFile(false);
}
};
fileInput.click();
};
const filteredCVEs = cves;
return (
<div className="min-h-screen bg-gray-50 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8 flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">CVE Dashboard</h1>
<p className="text-gray-600">Query vulnerabilities, manage vendors, and attach documentation</p>
</div>
<button
onClick={() => setShowAddCVE(true)}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2"
>
<span className="text-xl">+</span>
Add New CVE
</button>
</div>
{/* Add CVE Modal */}
{showAddCVE && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold text-gray-900">Add New CVE</h2>
<button
onClick={() => setShowAddCVE(false)}
className="text-gray-400 hover:text-gray-600"
>
<XCircle className="w-6 h-6" />
</button>
</div>
<form onSubmit={handleAddCVE} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
CVE ID *
</label>
<input
type="text"
required
placeholder="CVE-2024-1234"
value={newCVE.cve_id}
onChange={(e) => setNewCVE({...newCVE, cve_id: e.target.value.toUpperCase()})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Vendor *
</label>
<input
type="text"
required
placeholder="Microsoft, Cisco, Oracle, etc."
value={newCVE.vendor}
onChange={(e) => setNewCVE({...newCVE, vendor: e.target.value})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Severity *
</label>
<select
value={newCVE.severity}
onChange={(e) => setNewCVE({...newCVE, severity: e.target.value})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="Critical">Critical</option>
<option value="High">High</option>
<option value="Medium">Medium</option>
<option value="Low">Low</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Description *
</label>
<textarea
required
placeholder="Brief description of the vulnerability"
value={newCVE.description}
onChange={(e) => setNewCVE({...newCVE, description: e.target.value})}
rows={3}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Published Date *
</label>
<input
type="date"
required
value={newCVE.published_date}
onChange={(e) => setNewCVE({...newCVE, published_date: e.target.value})}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex gap-3 pt-4">
<button
type="submit"
className="flex-1 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium"
>
Add CVE
</button>
<button
type="button"
onClick={() => setShowAddCVE(false)}
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
>
Cancel
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Quick Check */}
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg shadow-sm p-6 mb-6 border border-blue-200">
<h2 className="text-lg font-semibold text-gray-900 mb-3">Quick CVE Status Check</h2>
<div className="flex gap-3">
<input
type="text"
placeholder="Enter CVE ID (e.g., CVE-2024-1234)"
value={quickCheckCVE}
onChange={(e) => setQuickCheckCVE(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && quickCheckCVEStatus()}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
onClick={quickCheckCVEStatus}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
>
Check Status
</button>
</div>
{quickCheckResult && (
<div className={`mt-4 p-4 rounded-lg ${quickCheckResult.exists ? 'bg-green-50 border border-green-200' : 'bg-yellow-50 border border-yellow-200'}`}>
{quickCheckResult.error ? (
<div className="flex items-start gap-3">
<XCircle className="w-5 h-5 text-red-600 mt-0.5" />
<div>
<p className="font-medium text-red-900">Error</p>
<p className="text-sm text-red-700">{quickCheckResult.error}</p>
</div>
</div>
) : quickCheckResult.exists ? (
<div className="flex items-start gap-3">
<CheckCircle className="w-5 h-5 text-green-600 mt-0.5" />
<div className="flex-1">
<p className="font-medium text-green-900">✓ CVE Addressed</p>
<div className="mt-2 space-y-1 text-sm text-gray-700">
<p><strong>Vendor:</strong> {quickCheckResult.cve.vendor}</p>
<p><strong>Severity:</strong> {quickCheckResult.cve.severity}</p>
<p><strong>Status:</strong> {quickCheckResult.cve.status}</p>
<p><strong>Documents:</strong> {quickCheckResult.cve.total_documents} attached</p>
<div className="mt-2 flex gap-3">
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.advisory ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
{quickCheckResult.compliance.advisory ? '✓' : '✗'} Advisory
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.email ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
{quickCheckResult.compliance.email ? '✓' : '○'} Email
</span>
<span className={`px-2 py-1 rounded text-xs font-medium ${quickCheckResult.compliance.screenshot ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
{quickCheckResult.compliance.screenshot ? '✓' : '○'} Screenshot
</span>
</div>
</div>
</div>
</div>
) : (
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-yellow-600 mt-0.5" />
<div>
<p className="font-medium text-yellow-900">Not Found</p>
<p className="text-sm text-yellow-700">This CVE has not been addressed yet. No entry exists in the database.</p>
</div>
</div>
)}
</div>
)}
</div>
{/* Search and Filters */}
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="md:col-span-1">
<label className="block text-sm font-medium text-gray-700 mb-2">
<Search className="inline w-4 h-4 mr-1" />
Search CVEs
</label>
<input
type="text"
placeholder="CVE ID or description..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<Filter className="inline w-4 h-4 mr-1" />
Vendor
</label>
<select
value={selectedVendor}
onChange={(e) => setSelectedVendor(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
{vendors.map(vendor => (
<option key={vendor} value={vendor}>{vendor}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
<AlertCircle className="inline w-4 h-4 mr-1" />
Severity
</label>
<select
value={selectedSeverity}
onChange={(e) => setSelectedSeverity(e.target.value)}
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
>
{severityLevels.map(level => (
<option key={level} value={level}>{level}</option>
))}
</select>
</div>
</div>
</div>
{/* Results Summary */}
<div className="mb-4 flex justify-between items-center">
<p className="text-gray-600">
Found {filteredCVEs.length} CVE{filteredCVEs.length !== 1 ? 's' : ''}
</p>
{selectedDocuments.length > 0 && (
<button
onClick={exportSelectedDocuments}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<Download className="w-4 h-4" />
Export {selectedDocuments.length} Document{selectedDocuments.length !== 1 ? 's' : ''} for Report
</button>
)}
</div>
{/* CVE List */}
{loading ? (
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
<Loader className="w-12 h-12 text-blue-600 mx-auto mb-4 animate-spin" />
<p className="text-gray-600">Loading CVEs...</p>
</div>
) : error ? (
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
<XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">Error Loading CVEs</h3>
<p className="text-gray-600 mb-4">{error}</p>
<button
onClick={fetchCVEs}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Retry
</button>
</div>
) : (
<div className="space-y-4">
{filteredCVEs.map(cve => {
const documents = cveDocuments[cve.cve_id] || [];
return (
<div key={cve.cve_id} className="bg-white rounded-lg shadow-sm border border-gray-200">
<div className="p-6">
<div className="flex justify-between items-start mb-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-xl font-semibold text-gray-900">{cve.cve_id}</h3>
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getSeverityColor(cve.severity)}`}>
{cve.severity}
</span>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${cve.doc_status === 'Complete' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
{cve.doc_status === 'Complete' ? '✓ Docs Complete' : '⚠ Incomplete'}
</span>
</div>
<p className="text-gray-700 mb-2">{cve.description}</p>
<div className="flex items-center gap-4 text-sm text-gray-500">
<span>Vendor: <span className="font-medium text-gray-700">{cve.vendor}</span></span>
<span>Published: {cve.published_date}</span>
<span>Status: <span className="font-medium text-gray-700">{cve.status}</span></span>
<span className="flex items-center gap-1">
<FileText className="w-4 h-4" />
{cve.document_count} document{cve.document_count !== 1 ? 's' : ''}
</span>
</div>
</div>
<button
onClick={() => handleViewDocuments(cve.cve_id)}
className="px-4 py-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-2"
>
<Eye className="w-4 h-4" />
{selectedCVE === cve.cve_id ? 'Hide' : 'View'} Documents
</button>
</div>
{/* Documents Section */}
{selectedCVE === cve.cve_id && (
<div className="mt-4 pt-4 border-t border-gray-200">
<h4 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
<FileText className="w-4 h-4" />
Attached Documents ({documents.length})
</h4>
{documents.length > 0 ? (
<div className="space-y-2">
{documents.map(doc => (
<div
key={doc.id}
className="flex items-center justify-between p-3 bg-gray-50 rounded-lg hover:bg-gray-100 transition-colors"
>
<div className="flex items-center gap-3 flex-1">
<input
type="checkbox"
checked={selectedDocuments.includes(doc.id)}
onChange={() => toggleDocumentSelection(doc.id)}
className="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500"
/>
<FileText className="w-5 h-5 text-gray-400" />
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">{doc.name}</p>
<p className="text-xs text-gray-500 capitalize">
{doc.type} • {doc.file_size}
{doc.notes && ` • ${doc.notes}`}
</p>
</div>
</div>
<a
href={`http://localhost:3001/${doc.file_path}`}
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1 text-sm text-blue-600 hover:bg-blue-50 rounded transition-colors"
>
View
</a>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500 italic">No documents attached yet</p>
)}
<button
onClick={() => handleFileUpload(cve.cve_id, cve.vendor)}
disabled={uploadingFile}
className="mt-3 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50"
>
<Upload className="w-4 h-4" />
{uploadingFile ? 'Uploading...' : 'Upload New Document'}
</button>
</div>
)}
</div>
</div>
);
})}
</div>
)}
{filteredCVEs.length === 0 && !loading && (
<div className="bg-white rounded-lg shadow-sm p-12 text-center">
<AlertCircle className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No CVEs Found</h3>
<p className="text-gray-600">Try adjusting your search criteria or filters</p>
</div>
)}
</div>
</div>
);
}

8
frontend/src/App.test.js Normal file
View File

@@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -0,0 +1,25 @@
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;

13
frontend/src/index.css Normal file
View File

@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

17
frontend/src/index.js Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
frontend/src/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';