mirror of
https://github.com/tips-of-mine/GLPI-Plugin-CVE-Prototype.git
synced 2025-06-28 07:08:44 +02:00
Start repository
This commit is contained in:
79
src/App.tsx
Normal file
79
src/App.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { useState } from 'react';
|
||||
import { LayoutDashboard, Shield, Settings, Database, AlertCircle } from 'lucide-react';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import CVEList from './components/CVEList';
|
||||
import SourcesConfig from './components/SourcesConfig';
|
||||
import RulesConfig from './components/RulesConfig';
|
||||
|
||||
function App() {
|
||||
const [activeTab, setActiveTab] = useState<string>('dashboard');
|
||||
|
||||
const navigation = [
|
||||
{ id: 'dashboard', name: 'Dashboard', icon: <LayoutDashboard className="h-5 w-5" /> },
|
||||
{ id: 'cve', name: 'CVE Management', icon: <Shield className="h-5 w-5" /> },
|
||||
{ id: 'sources', name: 'Data Sources', icon: <Database className="h-5 w-5" /> },
|
||||
{ id: 'rules', name: 'Rules', icon: <Settings className="h-5 w-5" /> },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 flex">
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-white shadow-md hidden md:block">
|
||||
<div className="h-16 flex items-center px-6 border-b border-gray-200">
|
||||
<AlertCircle className="h-6 w-6 mr-2 text-blue-600" />
|
||||
<span className="font-bold text-lg">GLPI CVE Plugin</span>
|
||||
</div>
|
||||
<nav className="mt-6 px-3">
|
||||
{navigation.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
className={`flex items-center px-3 py-2 mt-1 rounded-md w-full text-left ${
|
||||
activeTab === item.id
|
||||
? 'bg-blue-50 text-blue-700'
|
||||
: 'text-gray-700 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
<span className="mr-3">{item.icon}</span>
|
||||
{item.name}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Mobile navbar */}
|
||||
<div className="md:hidden w-full bg-white p-4 flex border-b border-gray-200">
|
||||
<div className="flex items-center">
|
||||
<AlertCircle className="h-6 w-6 mr-2 text-blue-600" />
|
||||
<span className="font-bold text-lg">GLPI CVE Plugin</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile navigation */}
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex justify-around py-2 md:hidden">
|
||||
{navigation.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
className={`flex flex-col items-center px-3 py-2 text-sm ${
|
||||
activeTab === item.id ? 'text-blue-700' : 'text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{item.icon}
|
||||
<span className="mt-1">{item.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex-1 min-h-screen md:ml-64">
|
||||
{activeTab === 'dashboard' && <Dashboard />}
|
||||
{activeTab === 'cve' && <CVEList />}
|
||||
{activeTab === 'sources' && <SourcesConfig />}
|
||||
{activeTab === 'rules' && <RulesConfig />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
208
src/components/CVEList.tsx
Normal file
208
src/components/CVEList.tsx
Normal file
@ -0,0 +1,208 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Search, Filter, ChevronDown, ChevronUp, ExternalLink } from 'lucide-react';
|
||||
import { mockCVEs } from '../data/mockData';
|
||||
import { CVE } from '../types/cve';
|
||||
|
||||
const CVEList: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [filterSeverity, setFilterSeverity] = useState<string>('');
|
||||
const [filterStatus, setFilterStatus] = useState<string>('');
|
||||
const [sortField, setSortField] = useState<keyof CVE>('published_date');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc');
|
||||
|
||||
const handleSort = (field: keyof CVE) => {
|
||||
if (sortField === field) {
|
||||
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortField(field);
|
||||
setSortDirection('desc');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCVEs = mockCVEs.filter(cve => {
|
||||
return (
|
||||
(searchTerm === '' ||
|
||||
cve.cve_id.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
cve.description.toLowerCase().includes(searchTerm.toLowerCase())) &&
|
||||
(filterSeverity === '' || cve.severity === filterSeverity) &&
|
||||
(filterStatus === '' || cve.status === filterStatus)
|
||||
);
|
||||
}).sort((a, b) => {
|
||||
if (a[sortField] < b[sortField]) return sortDirection === 'asc' ? -1 : 1;
|
||||
if (a[sortField] > b[sortField]) return sortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const getSortIcon = (field: keyof CVE) => {
|
||||
if (sortField !== field) return null;
|
||||
return sortDirection === 'asc' ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold mb-1">CVE Management</h1>
|
||||
<p className="text-gray-600">Browse and manage Common Vulnerabilities and Exposures</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex flex-col md:flex-row gap-4">
|
||||
<div className="flex-1 relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Search className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Search CVE ID or description..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Filter className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<select
|
||||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
value={filterSeverity}
|
||||
onChange={(e) => setFilterSeverity(e.target.value)}
|
||||
>
|
||||
<option value="">All Severities</option>
|
||||
<option value="CRITICAL">Critical</option>
|
||||
<option value="HIGH">High</option>
|
||||
<option value="MEDIUM">Medium</option>
|
||||
<option value="LOW">Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<Filter className="h-5 w-5 text-gray-400" />
|
||||
</div>
|
||||
<select
|
||||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
value={filterStatus}
|
||||
onChange={(e) => setFilterStatus(e.target.value)}
|
||||
>
|
||||
<option value="">All Statuses</option>
|
||||
<option value="NEW">New</option>
|
||||
<option value="ANALYZED">Analyzed</option>
|
||||
<option value="ASSIGNED">Assigned</option>
|
||||
<option value="RESOLVED">Resolved</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white shadow-md rounded-lg overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => handleSort('cve_id')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
CVE ID
|
||||
{getSortIcon('cve_id')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => handleSort('severity')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Severity
|
||||
{getSortIcon('severity')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => handleSort('cvss_score')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
CVSS
|
||||
{getSortIcon('cvss_score')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => handleSort('published_date')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Published
|
||||
{getSortIcon('published_date')}
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||
onClick={() => handleSort('status')}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
Status
|
||||
{getSortIcon('status')}
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredCVEs.map((cve) => (
|
||||
<tr key={cve.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-blue-600">
|
||||
{cve.cve_id}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
cve.severity === 'CRITICAL' ? 'bg-red-100 text-red-800' :
|
||||
cve.severity === 'HIGH' ? 'bg-orange-100 text-orange-800' :
|
||||
cve.severity === 'MEDIUM' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
{cve.severity}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{cve.cvss_score.toFixed(1)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(cve.published_date).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
cve.status === 'NEW' ? 'bg-blue-100 text-blue-800' :
|
||||
cve.status === 'ANALYZED' ? 'bg-yellow-100 text-yellow-800' :
|
||||
cve.status === 'ASSIGNED' ? 'bg-purple-100 text-purple-800' :
|
||||
'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{cve.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button className="text-blue-600 hover:text-blue-900 mr-3">View</button>
|
||||
<button className="text-purple-600 hover:text-purple-900 mr-3">Create Ticket</button>
|
||||
<a
|
||||
href={cve.references[0]}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-gray-600 hover:text-gray-900 inline-flex items-center"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CVEList;
|
176
src/components/Dashboard.tsx
Normal file
176
src/components/Dashboard.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import React from 'react';
|
||||
import { PieChart as ChartPie, ShieldAlert, Clock, FileText } from 'lucide-react';
|
||||
import { mockCVEs } from '../data/mockData';
|
||||
|
||||
const severityColors = {
|
||||
CRITICAL: 'bg-red-500',
|
||||
HIGH: 'bg-orange-500',
|
||||
MEDIUM: 'bg-yellow-500',
|
||||
LOW: 'bg-blue-500'
|
||||
};
|
||||
|
||||
const statusCounts = {
|
||||
NEW: mockCVEs.filter(cve => cve.status === 'NEW').length,
|
||||
ANALYZED: mockCVEs.filter(cve => cve.status === 'ANALYZED').length,
|
||||
ASSIGNED: mockCVEs.filter(cve => cve.status === 'ASSIGNED').length,
|
||||
RESOLVED: mockCVEs.filter(cve => cve.status === 'RESOLVED').length,
|
||||
};
|
||||
|
||||
const severityCounts = {
|
||||
CRITICAL: mockCVEs.filter(cve => cve.severity === 'CRITICAL').length,
|
||||
HIGH: mockCVEs.filter(cve => cve.severity === 'HIGH').length,
|
||||
MEDIUM: mockCVEs.filter(cve => cve.severity === 'MEDIUM').length,
|
||||
LOW: mockCVEs.filter(cve => cve.severity === 'LOW').length,
|
||||
};
|
||||
|
||||
const Dashboard = () => {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold mb-1">CVE Dashboard</h1>
|
||||
<p className="text-gray-600">Overview of vulnerability management status</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="rounded-full bg-red-100 p-3 mr-4">
|
||||
<ShieldAlert className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm">Critical Vulnerabilities</p>
|
||||
<p className="text-2xl font-bold">{severityCounts.CRITICAL}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="bg-red-500 h-full" style={{ width: `${(severityCounts.CRITICAL / mockCVEs.length) * 100}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="rounded-full bg-orange-100 p-3 mr-4">
|
||||
<ShieldAlert className="h-6 w-6 text-orange-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm">High Risk Vulnerabilities</p>
|
||||
<p className="text-2xl font-bold">{severityCounts.HIGH}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="bg-orange-500 h-full" style={{ width: `${(severityCounts.HIGH / mockCVEs.length) * 100}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="rounded-full bg-blue-100 p-3 mr-4">
|
||||
<Clock className="h-6 w-6 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm">Pending Analysis</p>
|
||||
<p className="text-2xl font-bold">{statusCounts.NEW}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="bg-blue-500 h-full" style={{ width: `${(statusCounts.NEW / mockCVEs.length) * 100}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-center mb-4">
|
||||
<div className="rounded-full bg-green-100 p-3 mr-4">
|
||||
<FileText className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-gray-500 text-sm">Resolved Issues</p>
|
||||
<p className="text-2xl font-bold">{statusCounts.RESOLVED}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||
<div className="bg-green-500 h-full" style={{ width: `${(statusCounts.RESOLVED / mockCVEs.length) * 100}%` }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="col-span-2 bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold">Recent Vulnerabilities</h2>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-50">
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CVE ID</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Severity</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CVSS</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Published</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{mockCVEs.slice(0, 5).map((cve) => (
|
||||
<tr key={cve.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-blue-600">{cve.cve_id}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
cve.severity === 'CRITICAL' ? 'bg-red-100 text-red-800' :
|
||||
cve.severity === 'HIGH' ? 'bg-orange-100 text-orange-800' :
|
||||
cve.severity === 'MEDIUM' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
{cve.severity}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{cve.cvss_score.toFixed(1)}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{new Date(cve.published_date).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
cve.status === 'NEW' ? 'bg-blue-100 text-blue-800' :
|
||||
cve.status === 'ANALYZED' ? 'bg-yellow-100 text-yellow-800' :
|
||||
cve.status === 'ASSIGNED' ? 'bg-purple-100 text-purple-800' :
|
||||
'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{cve.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold">Severity Distribution</h2>
|
||||
</div>
|
||||
<div className="p-6 flex flex-col items-center">
|
||||
<div className="w-full h-48 mb-4 flex items-end justify-around">
|
||||
{Object.entries(severityCounts).map(([severity, count]) => (
|
||||
<div key={severity} className="flex flex-col items-center">
|
||||
<div
|
||||
className={`${severityColors[severity as keyof typeof severityColors]} rounded-t-lg w-16`}
|
||||
style={{ height: `${Math.max((count / mockCVEs.length) * 150, 20)}px` }}
|
||||
></div>
|
||||
<div className="mt-2 text-xs font-medium">{severity}</div>
|
||||
<div className="text-gray-600 font-semibold">{count}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 text-gray-500 text-sm text-center">
|
||||
Total vulnerabilities: {mockCVEs.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
362
src/components/RulesConfig.tsx
Normal file
362
src/components/RulesConfig.tsx
Normal file
@ -0,0 +1,362 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Save, Plus, Trash2 } from 'lucide-react';
|
||||
import { mockRules } from '../data/mockData';
|
||||
import { CVERule } from '../types/cve';
|
||||
|
||||
const RulesConfig: React.FC = () => {
|
||||
const [rules, setRules] = useState<CVERule[]>(mockRules);
|
||||
const [editingRule, setEditingRule] = useState<Partial<CVERule> | null>(null);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
|
||||
const handleEditRule = (rule: CVERule) => {
|
||||
setEditingRule({ ...rule });
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleNewRule = () => {
|
||||
setEditingRule({
|
||||
id: rules.length + 1,
|
||||
name: '',
|
||||
criteria: { severity: 'HIGH' },
|
||||
actions: { create_ticket: true, ticket_priority: 'NORMAL' },
|
||||
priority: rules.length + 1,
|
||||
is_active: true
|
||||
});
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||||
const { name, value, type } = e.target as HTMLInputElement;
|
||||
|
||||
if (name === 'severity' || name === 'ticket_priority') {
|
||||
setEditingRule({
|
||||
...editingRule,
|
||||
criteria: name === 'severity' ? { ...editingRule?.criteria, severity: value } : editingRule?.criteria,
|
||||
actions: name === 'ticket_priority' ? { ...editingRule?.actions, ticket_priority: value } : editingRule?.actions
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setEditingRule({
|
||||
...editingRule,
|
||||
[name]: type === 'checkbox'
|
||||
? (e.target as HTMLInputElement).checked
|
||||
: type === 'number'
|
||||
? parseInt(value)
|
||||
: value
|
||||
});
|
||||
};
|
||||
|
||||
const handleActionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, checked } = e.target;
|
||||
|
||||
if (editingRule) {
|
||||
setEditingRule({
|
||||
...editingRule,
|
||||
actions: {
|
||||
...editingRule.actions,
|
||||
[name]: checked
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (editingRule && editingRule.name) {
|
||||
if (editingRule.id && rules.some(r => r.id === editingRule.id)) {
|
||||
// Update existing rule
|
||||
setRules(rules.map(rule => rule.id === editingRule.id ? editingRule as CVERule : rule));
|
||||
} else {
|
||||
// Add new rule
|
||||
setRules([...rules, editingRule as CVERule]);
|
||||
}
|
||||
|
||||
setIsEditing(false);
|
||||
setEditingRule(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (ruleId: number) => {
|
||||
setRules(rules.filter(rule => rule.id !== ruleId));
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false);
|
||||
setEditingRule(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold mb-1">CVE Processing Rules</h1>
|
||||
<p className="text-gray-600">Configure automated actions for vulnerability management</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex justify-between">
|
||||
<p className="text-sm text-gray-500">
|
||||
Define rules that automatically process CVEs based on specific criteria. Rules are evaluated in priority order.
|
||||
</p>
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex items-center"
|
||||
onClick={handleNewRule}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" /> Add Rule
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isEditing && editingRule && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4">
|
||||
{editingRule.id && rules.some(r => r.id === editingRule.id) ? 'Edit Rule' : 'Add New Rule'}
|
||||
</h2>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Rule Name*
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={editingRule.name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-md font-medium mb-3">Criteria</h3>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Severity
|
||||
</label>
|
||||
<select
|
||||
name="severity"
|
||||
value={editingRule.criteria?.severity}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-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 className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Priority
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="priority"
|
||||
value={editingRule.priority}
|
||||
onChange={handleChange}
|
||||
min="1"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">Lower numbers are processed first</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-md font-medium mb-3">Actions</h3>
|
||||
|
||||
<div className="mb-4 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="create_ticket"
|
||||
name="create_ticket"
|
||||
checked={!!editingRule.actions?.create_ticket}
|
||||
onChange={handleActionChange}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="create_ticket" className="ml-2 block text-sm text-gray-900">
|
||||
Create ticket automatically
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{editingRule.actions?.create_ticket && (
|
||||
<div className="mb-4 ml-6">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Ticket Priority
|
||||
</label>
|
||||
<select
|
||||
name="ticket_priority"
|
||||
value={editingRule.actions?.ticket_priority}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="VERY HIGH">Very High</option>
|
||||
<option value="HIGH">High</option>
|
||||
<option value="NORMAL">Normal</option>
|
||||
<option value="LOW">Low</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="notify_admins"
|
||||
name="notify_admins"
|
||||
checked={!!editingRule.actions?.notify_admins}
|
||||
onChange={handleActionChange}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="notify_admins" className="ml-2 block text-sm text-gray-900">
|
||||
Send email notification
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="add_to_report"
|
||||
name="add_to_report"
|
||||
checked={!!editingRule.actions?.add_to_report}
|
||||
onChange={handleActionChange}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="add_to_report" className="ml-2 block text-sm text-gray-900">
|
||||
Add to vulnerability report
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="is_active"
|
||||
name="is_active"
|
||||
checked={!!editingRule.is_active}
|
||||
onChange={handleChange}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="is_active" className="ml-2 block text-sm text-gray-900">
|
||||
Active (enable this rule)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" /> Save Rule
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white shadow-md rounded-lg overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Priority
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Rule Name
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Criteria
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{rules.map((rule) => (
|
||||
<tr key={rule.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{rule.priority}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{rule.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{rule.criteria.severity && (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
rule.criteria.severity === 'CRITICAL' ? 'bg-red-100 text-red-800' :
|
||||
rule.criteria.severity === 'HIGH' ? 'bg-orange-100 text-orange-800' :
|
||||
rule.criteria.severity === 'MEDIUM' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
{rule.criteria.severity}
|
||||
</span>
|
||||
)}
|
||||
{rule.criteria.system_tags && (
|
||||
<span className="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
System Tags
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{rule.actions.create_ticket && (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800 mr-1">
|
||||
Create Ticket
|
||||
</span>
|
||||
)}
|
||||
{rule.actions.notify_admins && (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800 mr-1">
|
||||
Notify
|
||||
</span>
|
||||
)}
|
||||
{rule.actions.add_to_report && (
|
||||
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
Report
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
rule.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{rule.is_active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
className="text-indigo-600 hover:text-indigo-900 mr-3"
|
||||
onClick={() => handleEditRule(rule)}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="text-red-600 hover:text-red-900"
|
||||
onClick={() => handleDelete(rule.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RulesConfig;
|
286
src/components/SourcesConfig.tsx
Normal file
286
src/components/SourcesConfig.tsx
Normal file
@ -0,0 +1,286 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Save, Plus, Trash2, RefreshCw } from 'lucide-react';
|
||||
import { mockSources } from '../data/mockData';
|
||||
import { CVESource } from '../types/cve';
|
||||
|
||||
const SourcesConfig: React.FC = () => {
|
||||
const [sources, setSources] = useState<CVESource[]>(mockSources);
|
||||
const [newSource, setNewSource] = useState<boolean>(false);
|
||||
const [formData, setFormData] = useState<Partial<CVESource>>({
|
||||
name: '',
|
||||
url: '',
|
||||
api_key: '',
|
||||
is_active: true,
|
||||
sync_frequency: 24
|
||||
});
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||
const { name, value, type } = e.target as HTMLInputElement;
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked :
|
||||
type === 'number' ? parseInt(value) : value
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (formData.name && formData.url) {
|
||||
const newSourceObj: CVESource = {
|
||||
id: sources.length + 1,
|
||||
name: formData.name || '',
|
||||
url: formData.url || '',
|
||||
api_key: formData.api_key || '',
|
||||
is_active: formData.is_active || false,
|
||||
sync_frequency: formData.sync_frequency || 24,
|
||||
last_sync: 'Never',
|
||||
sync_status: 'PENDING'
|
||||
};
|
||||
|
||||
setSources([...sources, newSourceObj]);
|
||||
setNewSource(false);
|
||||
setFormData({
|
||||
name: '',
|
||||
url: '',
|
||||
api_key: '',
|
||||
is_active: true,
|
||||
sync_frequency: 24
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSync = (sourceId: number) => {
|
||||
// In a real implementation, this would trigger the sync process
|
||||
// For this prototype, we'll just update the last_sync time
|
||||
setSources(
|
||||
sources.map(source =>
|
||||
source.id === sourceId
|
||||
? {
|
||||
...source,
|
||||
last_sync: new Date().toISOString(),
|
||||
sync_status: 'SUCCESS' as const
|
||||
}
|
||||
: source
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const handleDelete = (sourceId: number) => {
|
||||
setSources(sources.filter(source => source.id !== sourceId));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold mb-1">CVE Data Sources</h1>
|
||||
<p className="text-gray-600">Configure and manage vulnerability data sources</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 flex justify-between">
|
||||
<p className="text-sm text-gray-500">
|
||||
Configure external data sources for CVE information. The plugin will automatically sync with these sources based on the configured frequency.
|
||||
</p>
|
||||
<button
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 flex items-center"
|
||||
onClick={() => setNewSource(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" /> Add Source
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{newSource && (
|
||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Add New Data Source</h2>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Source Name*
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
API URL*
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="url"
|
||||
value={formData.url}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
API Key (if required)
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="api_key"
|
||||
value={formData.api_key}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Sync Frequency (hours)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="sync_frequency"
|
||||
value={formData.sync_frequency}
|
||||
onChange={handleChange}
|
||||
min="1"
|
||||
max="168"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="is_active"
|
||||
name="is_active"
|
||||
checked={formData.is_active}
|
||||
onChange={handleChange}
|
||||
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||
/>
|
||||
<label htmlFor="is_active" className="ml-2 block text-sm text-gray-900">
|
||||
Active (enable synchronization)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Data Format
|
||||
</label>
|
||||
<select
|
||||
name="data_format"
|
||||
value={formData.data_format || 'JSON'}
|
||||
onChange={handleChange}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="JSON">JSON</option>
|
||||
<option value="XML">XML</option>
|
||||
<option value="CSV">CSV</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
className="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
onClick={() => setNewSource(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 flex items-center"
|
||||
>
|
||||
<Save className="h-4 w-4 mr-2" /> Save Source
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white shadow-md rounded-lg overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Source Name
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
URL
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Frequency
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Last Sync
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Active
|
||||
</th>
|
||||
<th scope="col" className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sources.map((source) => (
|
||||
<tr key={source.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{source.name}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{source.url}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
Every {source.sync_frequency} hours
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{source.last_sync === 'Never'
|
||||
? 'Never'
|
||||
: new Date(source.last_sync).toLocaleString()}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
source.sync_status === 'SUCCESS' ? 'bg-green-100 text-green-800' :
|
||||
source.sync_status === 'FAILED' ? 'bg-red-100 text-red-800' :
|
||||
source.sync_status === 'IN_PROGRESS' ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{source.sync_status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
source.is_active ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'
|
||||
}`}>
|
||||
{source.is_active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
className="text-blue-600 hover:text-blue-900 mr-3"
|
||||
onClick={() => handleSync(source.id)}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
className="text-red-600 hover:text-red-900"
|
||||
onClick={() => handleDelete(source.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourcesConfig;
|
502
src/css/cve.css
Normal file
502
src/css/cve.css
Normal file
@ -0,0 +1,502 @@
|
||||
/**
|
||||
* GLPI CVE Plugin - Main CSS
|
||||
* This file contains styles specific to the CVE plugin
|
||||
*/
|
||||
|
||||
/* General Layout */
|
||||
.cve-container {
|
||||
margin: 0;
|
||||
padding: 15px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.cve-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.cve-card-header {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
padding: 15px;
|
||||
font-weight: bold;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.cve-card-body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
/* Dashboard Components */
|
||||
.cve-stats-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cve-stat-card {
|
||||
position: relative;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.cve-stat-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.cve-stat-content {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.cve-stat-label {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.cve-stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cve-progress-bar {
|
||||
height: 8px;
|
||||
background-color: #e9ecef;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.cve-progress-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Severity colors */
|
||||
.cve-critical {
|
||||
background-color: #ffebee;
|
||||
}
|
||||
.cve-critical .cve-stat-icon {
|
||||
background-color: #ffcdd2;
|
||||
color: #c62828;
|
||||
}
|
||||
.cve-critical .cve-progress-bar-fill {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.cve-high {
|
||||
background-color: #fff3e0;
|
||||
}
|
||||
.cve-high .cve-stat-icon {
|
||||
background-color: #ffe0b2;
|
||||
color: #ef6c00;
|
||||
}
|
||||
.cve-high .cve-progress-bar-fill {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.cve-pending {
|
||||
background-color: #e3f2fd;
|
||||
}
|
||||
.cve-pending .cve-stat-icon {
|
||||
background-color: #bbdefb;
|
||||
color: #1565c0;
|
||||
}
|
||||
.cve-pending .cve-progress-bar-fill {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
.cve-resolved {
|
||||
background-color: #e8f5e9;
|
||||
}
|
||||
.cve-resolved .cve-stat-icon {
|
||||
background-color: #c8e6c9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
.cve-resolved .cve-progress-bar-fill {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.cve-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.cve-table th {
|
||||
background-color: #f5f5f5;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
.cve-table td {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.cve-table tr:hover {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.cve-sortable {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cve-sortable:after {
|
||||
content: '⇕';
|
||||
margin-left: 5px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cve-sortable.sort-asc:after {
|
||||
content: '↑';
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cve-sortable.sort-desc:after {
|
||||
content: '↓';
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Badges */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 3px 7px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background-color: #ffcdd2;
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: #ffe0b2;
|
||||
color: #ef6c00;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: #bbdefb;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background-color: #c5cae9;
|
||||
color: #283593;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background-color: #c8e6c9;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.badge-purple {
|
||||
background-color: #e1bee7;
|
||||
color: #6a1b9a;
|
||||
}
|
||||
|
||||
.badge-inactive {
|
||||
background-color: #e0e0e0;
|
||||
color: #616161;
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background-color: #fffde7;
|
||||
color: #f57f17;
|
||||
}
|
||||
|
||||
/* Filters */
|
||||
.cve-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cve-filter-group {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.cve-filter-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
.cve-filter-input,
|
||||
.cve-filter-select {
|
||||
width: 100%;
|
||||
padding: 8px 8px 8px 35px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.cve-btn-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
border: 1px solid transparent;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
border-radius: 4px;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn:focus, .btn:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
color: #fff;
|
||||
background-color: #2196f3;
|
||||
border-color: #2196f3;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0d8bf2;
|
||||
border-color: #0c83e9;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
color: #fff;
|
||||
background-color: #4caf50;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #43a047;
|
||||
border-color: #3d9142;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
color: #fff;
|
||||
background-color: #f44336;
|
||||
border-color: #f44336;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #e53935;
|
||||
border-color: #d32f2f;
|
||||
}
|
||||
|
||||
.btn-purple {
|
||||
color: #fff;
|
||||
background-color: #9c27b0;
|
||||
border-color: #9c27b0;
|
||||
}
|
||||
|
||||
.btn-purple:hover {
|
||||
background-color: #8e24aa;
|
||||
border-color: #7b1fa2;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
color: #fff;
|
||||
background-color: #757575;
|
||||
border-color: #757575;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #616161;
|
||||
border-color: #616161;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.cve-form-section {
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cve-form-title {
|
||||
font-size: 18px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cve-form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cve-form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.cve-form-label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cve-form-input,
|
||||
.cve-form-select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.cve-form-checkbox {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.cve-form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.cve-nav {
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.cve-nav-item {
|
||||
display: inline-block;
|
||||
padding: 10px 15px;
|
||||
margin-right: 5px;
|
||||
margin-bottom: -1px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
|
||||
.cve-nav-item:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.cve-nav-item.active {
|
||||
background-color: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-bottom-color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cve-nav-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Mobile responsiveness */
|
||||
@media (max-width: 768px) {
|
||||
.cve-stats-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cve-filters {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cve-table {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cve-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chart styles */
|
||||
.cve-chart-container {
|
||||
height: 300px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-around;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cve-chart-bar {
|
||||
width: 60px;
|
||||
border-radius: 4px 4px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transition: height 0.3s ease;
|
||||
}
|
||||
|
||||
.cve-chart-label {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cve-chart-value {
|
||||
margin-top: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cve-chart-critical {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.cve-chart-high {
|
||||
background-color: #ff9800;
|
||||
}
|
||||
|
||||
.cve-chart-medium {
|
||||
background-color: #ffeb3b;
|
||||
}
|
||||
|
||||
.cve-chart-low {
|
||||
background-color: #2196f3;
|
||||
}
|
181
src/data/mockData.ts
Normal file
181
src/data/mockData.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import { CVE, CVESource, CVERule } from '../types/cve';
|
||||
|
||||
export const mockCVEs: CVE[] = [
|
||||
{
|
||||
id: 1,
|
||||
cve_id: 'CVE-2024-1234',
|
||||
description: 'A remote code execution vulnerability in Apache Log4j library affecting versions 2.0-beta9 to 2.14.1',
|
||||
cvss_score: 10.0,
|
||||
cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H',
|
||||
severity: 'CRITICAL',
|
||||
published_date: '2024-05-12T00:00:00Z',
|
||||
modified_date: '2024-05-14T00:00:00Z',
|
||||
status: 'NEW',
|
||||
references: [
|
||||
'https://nvd.nist.gov/vuln/detail/CVE-2024-1234',
|
||||
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1234'
|
||||
],
|
||||
affected_products: ['Apache Log4j 2.0-2.14.1', 'Applications using affected Log4j versions'],
|
||||
date_creation: '2024-05-15T10:30:00Z',
|
||||
date_mod: '2024-05-15T10:30:00Z'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cve_id: 'CVE-2024-5678',
|
||||
description: 'SQL Injection vulnerability in WordPress plugin Contact Form 7 versions prior to 5.7.2',
|
||||
cvss_score: 8.8,
|
||||
cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H',
|
||||
severity: 'HIGH',
|
||||
published_date: '2024-05-10T00:00:00Z',
|
||||
modified_date: '2024-05-11T00:00:00Z',
|
||||
status: 'ANALYZED',
|
||||
references: [
|
||||
'https://nvd.nist.gov/vuln/detail/CVE-2024-5678',
|
||||
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-5678'
|
||||
],
|
||||
affected_products: ['WordPress Contact Form 7 < 5.7.2'],
|
||||
date_creation: '2024-05-12T08:15:00Z',
|
||||
date_mod: '2024-05-12T14:25:00Z'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cve_id: 'CVE-2024-9012',
|
||||
description: 'Privilege escalation vulnerability in Microsoft Windows 11 affecting kernel-mode drivers',
|
||||
cvss_score: 7.8,
|
||||
cvss_vector: 'CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H',
|
||||
severity: 'HIGH',
|
||||
published_date: '2024-05-08T00:00:00Z',
|
||||
modified_date: '2024-05-09T00:00:00Z',
|
||||
status: 'ASSIGNED',
|
||||
references: [
|
||||
'https://nvd.nist.gov/vuln/detail/CVE-2024-9012',
|
||||
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-9012'
|
||||
],
|
||||
affected_products: ['Microsoft Windows 11 21H2', 'Microsoft Windows 11 22H2'],
|
||||
date_creation: '2024-05-10T09:45:00Z',
|
||||
date_mod: '2024-05-13T11:20:00Z'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cve_id: 'CVE-2024-3456',
|
||||
description: 'Cross-site scripting (XSS) vulnerability in jQuery UI Dialog component prior to version 1.13.2',
|
||||
cvss_score: 5.4,
|
||||
cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N',
|
||||
severity: 'MEDIUM',
|
||||
published_date: '2024-05-05T00:00:00Z',
|
||||
modified_date: '2024-05-06T00:00:00Z',
|
||||
status: 'RESOLVED',
|
||||
references: [
|
||||
'https://nvd.nist.gov/vuln/detail/CVE-2024-3456',
|
||||
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3456'
|
||||
],
|
||||
affected_products: ['jQuery UI < 1.13.2'],
|
||||
date_creation: '2024-05-07T14:30:00Z',
|
||||
date_mod: '2024-05-15T09:10:00Z'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
cve_id: 'CVE-2024-7890',
|
||||
description: 'Information disclosure vulnerability in OpenSSL 3.0.0 through 3.1.1',
|
||||
cvss_score: 3.7,
|
||||
cvss_vector: 'CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N',
|
||||
severity: 'LOW',
|
||||
published_date: '2024-05-02T00:00:00Z',
|
||||
modified_date: '2024-05-03T00:00:00Z',
|
||||
status: 'ANALYZED',
|
||||
references: [
|
||||
'https://nvd.nist.gov/vuln/detail/CVE-2024-7890',
|
||||
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-7890'
|
||||
],
|
||||
affected_products: ['OpenSSL 3.0.0-3.1.1'],
|
||||
date_creation: '2024-05-04T16:45:00Z',
|
||||
date_mod: '2024-05-10T13:25:00Z'
|
||||
}
|
||||
];
|
||||
|
||||
export const mockSources: CVESource[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'National Vulnerability Database (NVD)',
|
||||
url: 'https://services.nvd.nist.gov/rest/json/cves/2.0',
|
||||
api_key: '********-****-****-****-************',
|
||||
source_type: 'NVD',
|
||||
data_format: 'JSON',
|
||||
is_active: true,
|
||||
sync_frequency: 4,
|
||||
last_sync: '2024-05-15T08:00:00Z',
|
||||
sync_status: 'SUCCESS'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'MITRE CVE Database',
|
||||
url: 'https://cveawg.mitre.org/api/',
|
||||
api_key: '',
|
||||
source_type: 'MITRE',
|
||||
data_format: 'JSON',
|
||||
is_active: true,
|
||||
sync_frequency: 24,
|
||||
last_sync: '2024-05-14T22:00:00Z',
|
||||
sync_status: 'SUCCESS'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'CISA KEV Catalog',
|
||||
url: 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json',
|
||||
api_key: '',
|
||||
source_type: 'CISA',
|
||||
data_format: 'JSON',
|
||||
is_active: true,
|
||||
sync_frequency: 12,
|
||||
last_sync: '2024-05-15T04:00:00Z',
|
||||
sync_status: 'SUCCESS'
|
||||
}
|
||||
];
|
||||
|
||||
export const mockRules: CVERule[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Critical Vulnerabilities - Immediate Ticket',
|
||||
criteria: {
|
||||
severity: 'CRITICAL',
|
||||
affected_systems: { min: 1 }
|
||||
},
|
||||
actions: {
|
||||
create_ticket: true,
|
||||
ticket_priority: 'VERY HIGH',
|
||||
notify_admins: true
|
||||
},
|
||||
priority: 1,
|
||||
is_active: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'High Risk - Production Systems',
|
||||
criteria: {
|
||||
severity: 'HIGH',
|
||||
system_tags: ['production', 'customer-facing']
|
||||
},
|
||||
actions: {
|
||||
create_ticket: true,
|
||||
ticket_priority: 'HIGH',
|
||||
notify_admins: false
|
||||
},
|
||||
priority: 2,
|
||||
is_active: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Medium Risk - Batch Report',
|
||||
criteria: {
|
||||
severity: 'MEDIUM',
|
||||
system_count: { min: 1 }
|
||||
},
|
||||
actions: {
|
||||
create_ticket: false,
|
||||
add_to_report: true,
|
||||
schedule_report: 'WEEKLY'
|
||||
},
|
||||
priority: 3,
|
||||
is_active: true
|
||||
}
|
||||
];
|
3
src/index.css
Normal file
3
src/index.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
417
src/js/cve.js
Normal file
417
src/js/cve.js
Normal file
@ -0,0 +1,417 @@
|
||||
/**
|
||||
* GLPI CVE Plugin - Main JavaScript
|
||||
* This file contains jQuery code for the CVE plugin functionality
|
||||
*/
|
||||
|
||||
$(document).ready(function() {
|
||||
// Navigation handling
|
||||
$('.cve-nav-item').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
const target = $(this).data('target');
|
||||
|
||||
// Update active state
|
||||
$('.cve-nav-item').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
|
||||
// Show relevant content
|
||||
$('.cve-content').hide();
|
||||
$(`#${target}`).show();
|
||||
|
||||
// Update URL hash for bookmarking
|
||||
window.location.hash = target;
|
||||
});
|
||||
|
||||
// Initialize based on hash or default to dashboard
|
||||
const initPage = () => {
|
||||
let target = window.location.hash.substring(1) || 'cve-dashboard';
|
||||
$(`.cve-nav-item[data-target="${target}"]`).trigger('click');
|
||||
};
|
||||
|
||||
initPage();
|
||||
|
||||
// CVE Search functionality
|
||||
$('#cve-search').on('keyup', function() {
|
||||
const searchTerm = $(this).val().toLowerCase();
|
||||
filterCVEs(searchTerm, $('#severity-filter').val(), $('#status-filter').val());
|
||||
});
|
||||
|
||||
// Severity and Status filters
|
||||
$('#severity-filter, #status-filter').on('change', function() {
|
||||
filterCVEs(
|
||||
$('#cve-search').val().toLowerCase(),
|
||||
$('#severity-filter').val(),
|
||||
$('#status-filter').val()
|
||||
);
|
||||
});
|
||||
|
||||
// Table sorting
|
||||
$('.cve-sortable').on('click', function() {
|
||||
const column = $(this).data('column');
|
||||
const currentDir = $(this).hasClass('sort-asc') ? 'asc' : 'desc';
|
||||
const newDir = currentDir === 'asc' ? 'desc' : 'asc';
|
||||
|
||||
// Update visual indicators
|
||||
$('.cve-sortable').removeClass('sort-asc sort-desc');
|
||||
$(this).addClass(`sort-${newDir}`);
|
||||
|
||||
sortCVETable(column, newDir);
|
||||
});
|
||||
|
||||
// Sources form handling
|
||||
$('#add-source-form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const newSource = {
|
||||
name: $('#source-name').val(),
|
||||
url: $('#source-url').val(),
|
||||
api_key: $('#source-api-key').val(),
|
||||
sync_frequency: parseInt($('#source-frequency').val()),
|
||||
is_active: $('#source-active').prop('checked')
|
||||
};
|
||||
|
||||
// In a real implementation, this would be an AJAX POST
|
||||
console.log('Adding new source:', newSource);
|
||||
|
||||
// For the prototype, just add to the table
|
||||
addSourceToTable(newSource);
|
||||
|
||||
// Reset form
|
||||
$(this).trigger('reset');
|
||||
$('#add-source-section').hide();
|
||||
});
|
||||
|
||||
// New source button
|
||||
$('#add-source-btn').on('click', function() {
|
||||
$('#add-source-section').toggle();
|
||||
});
|
||||
|
||||
// Rule form handling
|
||||
$('#add-rule-form').on('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const newRule = {
|
||||
name: $('#rule-name').val(),
|
||||
criteria: {
|
||||
severity: $('#rule-severity').val()
|
||||
},
|
||||
actions: {
|
||||
create_ticket: $('#create-ticket').prop('checked'),
|
||||
ticket_priority: $('#ticket-priority').val(),
|
||||
notify_admins: $('#notify-admins').prop('checked'),
|
||||
add_to_report: $('#add-to-report').prop('checked')
|
||||
},
|
||||
priority: parseInt($('#rule-priority').val()),
|
||||
is_active: $('#rule-active').prop('checked')
|
||||
};
|
||||
|
||||
// In a real implementation, this would be an AJAX POST
|
||||
console.log('Adding new rule:', newRule);
|
||||
|
||||
// For the prototype, just add to the table
|
||||
addRuleToTable(newRule);
|
||||
|
||||
// Reset form
|
||||
$(this).trigger('reset');
|
||||
$('#add-rule-section').hide();
|
||||
});
|
||||
|
||||
// New rule button
|
||||
$('#add-rule-btn').on('click', function() {
|
||||
$('#add-rule-section').toggle();
|
||||
});
|
||||
|
||||
// Helper functions
|
||||
function filterCVEs(searchTerm, severity, status) {
|
||||
$('#cve-table tbody tr').each(function() {
|
||||
const cveId = $(this).find('td:nth-child(1)').text().toLowerCase();
|
||||
const description = $(this).find('td:nth-child(2)').text().toLowerCase();
|
||||
const rowSeverity = $(this).find('td:nth-child(3)').text();
|
||||
const rowStatus = $(this).find('td:nth-child(6)').text();
|
||||
|
||||
const matchesSearch = !searchTerm || cveId.includes(searchTerm) || description.includes(searchTerm);
|
||||
const matchesSeverity = !severity || rowSeverity === severity;
|
||||
const matchesStatus = !status || rowStatus === status;
|
||||
|
||||
if (matchesSearch && matchesSeverity && matchesStatus) {
|
||||
$(this).show();
|
||||
} else {
|
||||
$(this).hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function sortCVETable(column, direction) {
|
||||
const rows = $('#cve-table tbody tr').toArray();
|
||||
|
||||
rows.sort(function(a, b) {
|
||||
let aValue = $(a).find(`td:nth-child(${getColumnIndex(column)})`).text();
|
||||
let bValue = $(b).find(`td:nth-child(${getColumnIndex(column)})`).text();
|
||||
|
||||
// Handle numeric values
|
||||
if (column === 'cvss') {
|
||||
aValue = parseFloat(aValue);
|
||||
bValue = parseFloat(bValue);
|
||||
}
|
||||
|
||||
// Handle dates
|
||||
if (column === 'published') {
|
||||
aValue = new Date(aValue);
|
||||
bValue = new Date(bValue);
|
||||
}
|
||||
|
||||
if (direction === 'asc') {
|
||||
return aValue > bValue ? 1 : -1;
|
||||
} else {
|
||||
return aValue < bValue ? 1 : -1;
|
||||
}
|
||||
});
|
||||
|
||||
// Reappend sorted rows
|
||||
$('#cve-table tbody').empty();
|
||||
$.each(rows, function(index, row) {
|
||||
$('#cve-table tbody').append(row);
|
||||
});
|
||||
}
|
||||
|
||||
function getColumnIndex(columnName) {
|
||||
const indices = {
|
||||
'cve_id': 1,
|
||||
'severity': 3,
|
||||
'cvss': 4,
|
||||
'published': 5,
|
||||
'status': 6
|
||||
};
|
||||
return indices[columnName] || 1;
|
||||
}
|
||||
|
||||
function addSourceToTable(source) {
|
||||
const now = new Date().toLocaleString();
|
||||
|
||||
const newRow = `
|
||||
<tr>
|
||||
<td>${source.name}</td>
|
||||
<td>${source.url}</td>
|
||||
<td>Every ${source.sync_frequency} hours</td>
|
||||
<td>Never</td>
|
||||
<td><span class="badge badge-pending">PENDING</span></td>
|
||||
<td><span class="badge badge-${source.is_active ? 'success' : 'inactive'}">${source.is_active ? 'Active' : 'Inactive'}</span></td>
|
||||
<td class="text-right">
|
||||
<button class="btn btn-sm btn-primary sync-source-btn"><i class="fas fa-sync"></i></button>
|
||||
<button class="btn btn-sm btn-danger delete-source-btn"><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
$('#sources-table tbody').append(newRow);
|
||||
}
|
||||
|
||||
function addRuleToTable(rule) {
|
||||
const severityClass = {
|
||||
'CRITICAL': 'danger',
|
||||
'HIGH': 'warning',
|
||||
'MEDIUM': 'info',
|
||||
'LOW': 'primary'
|
||||
}[rule.criteria.severity] || 'primary';
|
||||
|
||||
const actionLabels = [];
|
||||
if (rule.actions.create_ticket) actionLabels.push('<span class="badge badge-purple">Create Ticket</span>');
|
||||
if (rule.actions.notify_admins) actionLabels.push('<span class="badge badge-info">Notify</span>');
|
||||
if (rule.actions.add_to_report) actionLabels.push('<span class="badge badge-success">Report</span>');
|
||||
|
||||
const newRow = `
|
||||
<tr>
|
||||
<td>${rule.priority}</td>
|
||||
<td>${rule.name}</td>
|
||||
<td><span class="badge badge-${severityClass}">${rule.criteria.severity}</span></td>
|
||||
<td>${actionLabels.join(' ')}</td>
|
||||
<td><span class="badge badge-${rule.is_active ? 'success' : 'inactive'}">${rule.is_active ? 'Active' : 'Inactive'}</span></td>
|
||||
<td class="text-right">
|
||||
<button class="btn btn-sm btn-primary edit-rule-btn">Edit</button>
|
||||
<button class="btn btn-sm btn-danger delete-rule-btn"><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
$('#rules-table tbody').append(newRow);
|
||||
}
|
||||
|
||||
// Load mock data for the prototype
|
||||
loadMockData();
|
||||
|
||||
function loadMockData() {
|
||||
// This would be replaced by AJAX calls to the GLPI API in a real implementation
|
||||
const mockCVEs = [
|
||||
{
|
||||
id: 1,
|
||||
cve_id: 'CVE-2024-1234',
|
||||
description: 'A remote code execution vulnerability in Apache Log4j library affecting versions 2.0-beta9 to 2.14.1',
|
||||
cvss_score: 10.0,
|
||||
cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H',
|
||||
severity: 'CRITICAL',
|
||||
published_date: '2024-05-12',
|
||||
modified_date: '2024-05-14',
|
||||
status: 'NEW',
|
||||
references: [
|
||||
'https://nvd.nist.gov/vuln/detail/CVE-2024-1234',
|
||||
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1234'
|
||||
],
|
||||
affected_products: ['Apache Log4j 2.0-2.14.1', 'Applications using affected Log4j versions'],
|
||||
date_creation: '2024-05-15',
|
||||
date_mod: '2024-05-15'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
cve_id: 'CVE-2024-5678',
|
||||
description: 'SQL Injection vulnerability in WordPress plugin Contact Form 7 versions prior to 5.7.2',
|
||||
cvss_score: 8.8,
|
||||
cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H',
|
||||
severity: 'HIGH',
|
||||
published_date: '2024-05-10',
|
||||
modified_date: '2024-05-11',
|
||||
status: 'ANALYZED',
|
||||
references: [
|
||||
'https://nvd.nist.gov/vuln/detail/CVE-2024-5678',
|
||||
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-5678'
|
||||
],
|
||||
affected_products: ['WordPress Contact Form 7 < 5.7.2'],
|
||||
date_creation: '2024-05-12',
|
||||
date_mod: '2024-05-12'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
cve_id: 'CVE-2024-9012',
|
||||
description: 'Privilege escalation vulnerability in Microsoft Windows 11 affecting kernel-mode drivers',
|
||||
cvss_score: 7.8,
|
||||
cvss_vector: 'CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H',
|
||||
severity: 'HIGH',
|
||||
published_date: '2024-05-08',
|
||||
modified_date: '2024-05-09',
|
||||
status: 'ASSIGNED',
|
||||
references: [
|
||||
'https://nvd.nist.gov/vuln/detail/CVE-2024-9012',
|
||||
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-9012'
|
||||
],
|
||||
affected_products: ['Microsoft Windows 11 21H2', 'Microsoft Windows 11 22H2'],
|
||||
date_creation: '2024-05-10',
|
||||
date_mod: '2024-05-13'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
cve_id: 'CVE-2024-3456',
|
||||
description: 'Cross-site scripting (XSS) vulnerability in jQuery UI Dialog component prior to version 1.13.2',
|
||||
cvss_score: 5.4,
|
||||
cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N',
|
||||
severity: 'MEDIUM',
|
||||
published_date: '2024-05-05',
|
||||
modified_date: '2024-05-06',
|
||||
status: 'RESOLVED',
|
||||
references: [
|
||||
'https://nvd.nist.gov/vuln/detail/CVE-2024-3456',
|
||||
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3456'
|
||||
],
|
||||
affected_products: ['jQuery UI < 1.13.2'],
|
||||
date_creation: '2024-05-07',
|
||||
date_mod: '2024-05-15'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
cve_id: 'CVE-2024-7890',
|
||||
description: 'Information disclosure vulnerability in OpenSSL 3.0.0 through 3.1.1',
|
||||
cvss_score: 3.7,
|
||||
cvss_vector: 'CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N',
|
||||
severity: 'LOW',
|
||||
published_date: '2024-05-02',
|
||||
modified_date: '2024-05-03',
|
||||
status: 'ANALYZED',
|
||||
references: [
|
||||
'https://nvd.nist.gov/vuln/detail/CVE-2024-7890',
|
||||
'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-7890'
|
||||
],
|
||||
affected_products: ['OpenSSL 3.0.0-3.1.1'],
|
||||
date_creation: '2024-05-04',
|
||||
date_mod: '2024-05-10'
|
||||
}
|
||||
];
|
||||
|
||||
// Populate CVE table
|
||||
mockCVEs.forEach(cve => {
|
||||
const severityClass = {
|
||||
'CRITICAL': 'danger',
|
||||
'HIGH': 'warning',
|
||||
'MEDIUM': 'info',
|
||||
'LOW': 'primary'
|
||||
}[cve.severity];
|
||||
|
||||
const statusClass = {
|
||||
'NEW': 'info',
|
||||
'ANALYZED': 'warning',
|
||||
'ASSIGNED': 'primary',
|
||||
'RESOLVED': 'success'
|
||||
}[cve.status];
|
||||
|
||||
const row = `
|
||||
<tr>
|
||||
<td><a href="#" class="cve-link">${cve.cve_id}</a></td>
|
||||
<td>${cve.description.substring(0, 50)}...</td>
|
||||
<td><span class="badge badge-${severityClass}">${cve.severity}</span></td>
|
||||
<td>${cve.cvss_score.toFixed(1)}</td>
|
||||
<td>${cve.published_date}</td>
|
||||
<td><span class="badge badge-${statusClass}">${cve.status}</span></td>
|
||||
<td class="text-right">
|
||||
<button class="btn btn-sm btn-primary view-cve-btn">View</button>
|
||||
<button class="btn btn-sm btn-purple create-ticket-btn">Create Ticket</button>
|
||||
<a href="${cve.references[0]}" target="_blank" class="btn btn-sm btn-secondary"><i class="fas fa-external-link-alt"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
$('#cve-table tbody').append(row);
|
||||
});
|
||||
|
||||
// Update dashboard counters
|
||||
const criticalCount = mockCVEs.filter(cve => cve.severity === 'CRITICAL').length;
|
||||
const highCount = mockCVEs.filter(cve => cve.severity === 'HIGH').length;
|
||||
const pendingCount = mockCVEs.filter(cve => cve.status === 'NEW').length;
|
||||
const resolvedCount = mockCVEs.filter(cve => cve.status === 'RESOLVED').length;
|
||||
|
||||
$('#critical-count').text(criticalCount);
|
||||
$('#high-count').text(highCount);
|
||||
$('#pending-count').text(pendingCount);
|
||||
$('#resolved-count').text(resolvedCount);
|
||||
|
||||
// Set progress bars
|
||||
$('#critical-progress').css('width', `${(criticalCount / mockCVEs.length) * 100}%`);
|
||||
$('#high-progress').css('width', `${(highCount / mockCVEs.length) * 100}%`);
|
||||
$('#pending-progress').css('width', `${(pendingCount / mockCVEs.length) * 100}%`);
|
||||
$('#resolved-progress').css('width', `${(resolvedCount / mockCVEs.length) * 100}%`);
|
||||
|
||||
// Populate recent vulnerabilities table
|
||||
mockCVEs.slice(0, 5).forEach(cve => {
|
||||
const severityClass = {
|
||||
'CRITICAL': 'danger',
|
||||
'HIGH': 'warning',
|
||||
'MEDIUM': 'info',
|
||||
'LOW': 'primary'
|
||||
}[cve.severity];
|
||||
|
||||
const statusClass = {
|
||||
'NEW': 'info',
|
||||
'ANALYZED': 'warning',
|
||||
'ASSIGNED': 'primary',
|
||||
'RESOLVED': 'success'
|
||||
}[cve.status];
|
||||
|
||||
const row = `
|
||||
<tr>
|
||||
<td><a href="#" class="cve-link">${cve.cve_id}</a></td>
|
||||
<td><span class="badge badge-${severityClass}">${cve.severity}</span></td>
|
||||
<td>${cve.cvss_score.toFixed(1)}</td>
|
||||
<td>${cve.published_date}</td>
|
||||
<td><span class="badge badge-${statusClass}">${cve.status}</span></td>
|
||||
</tr>
|
||||
`;
|
||||
|
||||
$('#recent-cve-table tbody').append(row);
|
||||
});
|
||||
}
|
||||
});
|
10
src/main.tsx
Normal file
10
src/main.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
);
|
45
src/types/cve.ts
Normal file
45
src/types/cve.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export interface CVE {
|
||||
id: number;
|
||||
cve_id: string;
|
||||
description: string;
|
||||
cvss_score: number;
|
||||
cvss_vector: string;
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
||||
published_date: string;
|
||||
modified_date: string;
|
||||
status: 'NEW' | 'ANALYZED' | 'ASSIGNED' | 'RESOLVED';
|
||||
references: string[];
|
||||
affected_products: string[];
|
||||
date_creation: string;
|
||||
date_mod: string;
|
||||
}
|
||||
|
||||
export interface CVESource {
|
||||
id: number;
|
||||
name: string;
|
||||
url: string;
|
||||
api_key: string;
|
||||
source_type?: string;
|
||||
data_format?: string;
|
||||
is_active: boolean;
|
||||
sync_frequency: number;
|
||||
last_sync: string;
|
||||
sync_status: 'SUCCESS' | 'FAILED' | 'IN_PROGRESS' | 'PENDING';
|
||||
}
|
||||
|
||||
export interface CVERule {
|
||||
id: number;
|
||||
name: string;
|
||||
criteria: any;
|
||||
actions: any;
|
||||
priority: number;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface CVETicket {
|
||||
id: number;
|
||||
cves_id: number;
|
||||
tickets_id: number;
|
||||
creation_type: 'AUTO' | 'MANUAL';
|
||||
date_creation: string;
|
||||
}
|
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
Reference in New Issue
Block a user