Start repository

This commit is contained in:
tips-of-mine
2025-05-31 10:03:48 +02:00
commit 194322c9fc
57 changed files with 14723 additions and 0 deletions

79
src/App.tsx Normal file
View 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
View 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;

View 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;

View 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;

View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

417
src/js/cve.js Normal file
View 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
View 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
View 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
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />