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:
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;
|
Reference in New Issue
Block a user