1252 lines
53 KiB
JavaScript
1252 lines
53 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import axios from 'axios';
|
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
import { tomorrow } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
|
import './App.css';
|
|
|
|
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
|
|
|
function App() {
|
|
const [cves, setCves] = useState([]);
|
|
const [sigmaRules, setSigmaRules] = useState([]);
|
|
const [selectedCve, setSelectedCve] = useState(null);
|
|
const [stats, setStats] = useState({});
|
|
const [loading, setLoading] = useState(true);
|
|
const [activeTab, setActiveTab] = useState('dashboard');
|
|
const [fetchingCves, setFetchingCves] = useState(false);
|
|
const [testResult, setTestResult] = useState(null);
|
|
const [bulkJobs, setBulkJobs] = useState([]);
|
|
const [bulkStatus, setBulkStatus] = useState({});
|
|
const [pocStats, setPocStats] = useState({});
|
|
const [gitHubPocStats, setGitHubPocStats] = useState({});
|
|
const [exploitdbStats, setExploitdbStats] = useState({});
|
|
const [cisaKevStats, setCisaKevStats] = useState({});
|
|
const [bulkProcessing, setBulkProcessing] = useState(false);
|
|
const [hasRunningJobs, setHasRunningJobs] = useState(false);
|
|
const [runningJobTypes, setRunningJobTypes] = useState(new Set());
|
|
const [llmStatus, setLlmStatus] = useState({});
|
|
const [exploitSyncDropdownOpen, setExploitSyncDropdownOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetchData();
|
|
}, []);
|
|
|
|
// Close dropdown when clicking outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event) => {
|
|
if (exploitSyncDropdownOpen && !event.target.closest('.relative')) {
|
|
setExploitSyncDropdownOpen(false);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [exploitSyncDropdownOpen]);
|
|
|
|
// Helper functions to check if specific job types are running
|
|
const isJobTypeRunning = (jobType) => {
|
|
return runningJobTypes.has(jobType);
|
|
};
|
|
|
|
const isBulkSeedRunning = () => {
|
|
return isJobTypeRunning('nvd_bulk_seed') || isJobTypeRunning('bulk_seed');
|
|
};
|
|
|
|
const isIncrementalUpdateRunning = () => {
|
|
return isJobTypeRunning('incremental_update');
|
|
};
|
|
|
|
const isNomiSecSyncRunning = () => {
|
|
return isJobTypeRunning('nomi_sec_sync');
|
|
};
|
|
|
|
const isGitHubPocSyncRunning = () => {
|
|
return isJobTypeRunning('github_poc_sync');
|
|
};
|
|
|
|
const isExploitDBSyncRunning = () => {
|
|
return isJobTypeRunning('exploitdb_sync') || isJobTypeRunning('exploitdb_sync_local');
|
|
};
|
|
|
|
const isCISAKEVSyncRunning = () => {
|
|
return isJobTypeRunning('cisa_kev_sync');
|
|
};
|
|
|
|
const isRuleGenerationRunning = () => {
|
|
return isJobTypeRunning('rule_regeneration') || isJobTypeRunning('llm_rule_generation');
|
|
};
|
|
|
|
const areAnyExploitSyncsRunning = () => {
|
|
return isNomiSecSyncRunning() || isGitHubPocSyncRunning() || isExploitDBSyncRunning() || isCISAKEVSyncRunning();
|
|
};
|
|
|
|
const fetchData = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const [cvesRes, rulesRes, statsRes, bulkJobsRes, bulkStatusRes, pocStatsRes, githubPocStatsRes, exploitdbStatsRes, cisaKevStatsRes, llmStatusRes] = await Promise.all([
|
|
axios.get(`${API_BASE_URL}/api/cves`),
|
|
axios.get(`${API_BASE_URL}/api/sigma-rules`),
|
|
axios.get(`${API_BASE_URL}/api/stats`),
|
|
axios.get(`${API_BASE_URL}/api/bulk-jobs`),
|
|
axios.get(`${API_BASE_URL}/api/bulk-status`),
|
|
axios.get(`${API_BASE_URL}/api/poc-stats`),
|
|
axios.get(`${API_BASE_URL}/api/github-poc-stats`).catch(err => ({ data: {} })),
|
|
axios.get(`${API_BASE_URL}/api/exploitdb-stats`).catch(err => ({ data: {} })),
|
|
axios.get(`${API_BASE_URL}/api/cisa-kev-stats`).catch(err => ({ data: {} })),
|
|
axios.get(`${API_BASE_URL}/api/llm-status`).catch(err => ({ data: {} }))
|
|
]);
|
|
|
|
setCves(cvesRes.data);
|
|
setSigmaRules(rulesRes.data);
|
|
setStats(statsRes.data);
|
|
setBulkJobs(bulkJobsRes.data);
|
|
setBulkStatus(bulkStatusRes.data);
|
|
setPocStats(pocStatsRes.data);
|
|
setGitHubPocStats(githubPocStatsRes.data);
|
|
setExploitdbStats(exploitdbStatsRes.data);
|
|
setCisaKevStats(cisaKevStatsRes.data);
|
|
setLlmStatus(llmStatusRes.data);
|
|
|
|
// Update running jobs state
|
|
const runningJobs = bulkJobsRes.data.filter(job => job.status === 'running' || job.status === 'pending');
|
|
setHasRunningJobs(runningJobs.length > 0);
|
|
|
|
// Update specific job types that are running
|
|
const activeJobTypes = new Set(runningJobs.map(job => job.job_type));
|
|
setRunningJobTypes(activeJobTypes);
|
|
} catch (error) {
|
|
console.error('Error fetching data:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const cancelJob = async (jobId) => {
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/cancel-job/${jobId}`);
|
|
console.log('Cancel job response:', response.data);
|
|
// Refresh data after cancelling
|
|
setTimeout(() => {
|
|
fetchData();
|
|
}, 1000);
|
|
} catch (error) {
|
|
console.error('Error cancelling job:', error);
|
|
alert('Failed to cancel job. Please try again.');
|
|
}
|
|
};
|
|
|
|
const handleFetchCves = async () => {
|
|
try {
|
|
setFetchingCves(true);
|
|
const response = await axios.post(`${API_BASE_URL}/api/fetch-cves`);
|
|
console.log('Fetch response:', response.data);
|
|
// Show success message and refresh after delay
|
|
setTimeout(() => {
|
|
fetchData();
|
|
setFetchingCves(false);
|
|
}, 5000); // Wait a bit longer for background task to complete
|
|
} catch (error) {
|
|
console.error('Error fetching CVEs:', error);
|
|
setFetchingCves(false);
|
|
// Show error state
|
|
setTestResult({
|
|
status: 'error',
|
|
message: 'Failed to initiate CVE fetch. Check console logs.'
|
|
});
|
|
}
|
|
};
|
|
|
|
const testNvdConnection = async () => {
|
|
try {
|
|
const response = await axios.get(`${API_BASE_URL}/api/test-nvd`);
|
|
setTestResult(response.data);
|
|
} catch (error) {
|
|
console.error('Error testing NVD connection:', error);
|
|
setTestResult({
|
|
status: 'error',
|
|
message: 'Failed to test NVD connection'
|
|
});
|
|
}
|
|
};
|
|
|
|
const startBulkSeed = async (startYear = 2020, endYear = null) => {
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/bulk-seed`, {
|
|
start_year: startYear,
|
|
end_year: endYear,
|
|
skip_nomi_sec: true
|
|
});
|
|
console.log('Bulk seed response:', response.data);
|
|
// Refresh data immediately to show job started
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Error starting bulk seed:', error);
|
|
}
|
|
};
|
|
|
|
const startIncrementalUpdate = async () => {
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/incremental-update`);
|
|
console.log('Incremental update response:', response.data);
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Error starting incremental update:', error);
|
|
}
|
|
};
|
|
|
|
const syncNomiSec = async (cveId = null) => {
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/sync-nomi-sec`, {
|
|
cve_id: cveId
|
|
});
|
|
console.log('Nomi-sec sync response:', response.data);
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Error syncing nomi-sec:', error);
|
|
}
|
|
};
|
|
|
|
const syncGitHubPocs = async (cveId = null) => {
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/sync-github-pocs`, {
|
|
cve_id: cveId
|
|
});
|
|
console.log('GitHub PoC sync response:', response.data);
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Error syncing GitHub PoCs:', error);
|
|
}
|
|
};
|
|
|
|
const syncExploitDB = async (cveId = null) => {
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/sync-exploitdb`, {
|
|
cve_id: cveId,
|
|
batch_size: 30
|
|
});
|
|
console.log('ExploitDB sync response:', response.data);
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Error syncing ExploitDB:', error);
|
|
}
|
|
};
|
|
|
|
const syncCISAKEV = async (cveId = null) => {
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/sync-cisa-kev`, {
|
|
cve_id: cveId,
|
|
batch_size: 100
|
|
});
|
|
console.log('CISA KEV sync response:', response.data);
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Error syncing CISA KEV:', error);
|
|
}
|
|
};
|
|
|
|
const syncReferences = async () => {
|
|
try {
|
|
// Placeholder for future implementation
|
|
console.log('Sync References - Not implemented yet');
|
|
alert('Sync References functionality will be implemented in a future update');
|
|
} catch (error) {
|
|
console.error('Error syncing references:', error);
|
|
}
|
|
};
|
|
|
|
const regenerateRules = async (force = false) => {
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/regenerate-rules`, {
|
|
force: force
|
|
});
|
|
console.log('Rule regeneration response:', response.data);
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Error regenerating rules:', error);
|
|
}
|
|
};
|
|
|
|
const generateLlmRules = async (force = false) => {
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/llm-enhanced-rules`, {
|
|
force: force
|
|
});
|
|
console.log('LLM rule generation response:', response.data);
|
|
fetchData();
|
|
} catch (error) {
|
|
console.error('Error generating LLM-enhanced rules:', error);
|
|
}
|
|
};
|
|
|
|
const switchLlmProvider = async (provider, model) => {
|
|
try {
|
|
const response = await axios.post(`${API_BASE_URL}/api/llm-switch`, {
|
|
provider: provider,
|
|
model: model
|
|
});
|
|
console.log('LLM provider switch response:', response.data);
|
|
fetchData(); // Refresh to get updated status
|
|
} catch (error) {
|
|
console.error('Error switching LLM provider:', error);
|
|
alert('Failed to switch LLM provider. Please check configuration.');
|
|
}
|
|
};
|
|
|
|
const getSeverityColor = (severity) => {
|
|
switch (severity?.toLowerCase()) {
|
|
case 'critical': return 'bg-red-100 text-red-800';
|
|
case 'high': return 'bg-orange-100 text-orange-800';
|
|
case 'medium': return 'bg-yellow-100 text-yellow-800';
|
|
case 'low': return 'bg-green-100 text-green-800';
|
|
default: return 'bg-gray-100 text-gray-800';
|
|
}
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
return new Date(dateString).toLocaleDateString('en-US', {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
});
|
|
};
|
|
|
|
const Dashboard = () => (
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-6">
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<h3 className="text-lg font-medium text-gray-900">Total CVEs</h3>
|
|
<p className="text-3xl font-bold text-blue-600">{stats.total_cves || 0}</p>
|
|
<p className="text-sm text-gray-500">Bulk: {stats.bulk_processed_cves || 0}</p>
|
|
</div>
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<h3 className="text-lg font-medium text-gray-900">SIGMA Rules</h3>
|
|
<p className="text-3xl font-bold text-green-600">{stats.total_sigma_rules || 0}</p>
|
|
<p className="text-sm text-gray-500">Nomi-sec: {stats.nomi_sec_rules || 0}</p>
|
|
<p className="text-sm text-gray-500">GitHub PoCs: {gitHubPocStats.github_poc_rules || 0}</p>
|
|
<p className={`text-sm ${llmStatus.status === 'ready' ? 'text-green-600' : 'text-red-500'}`}>
|
|
LLM: {llmStatus.current_provider?.provider || 'Not Available'}
|
|
</p>
|
|
</div>
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<h3 className="text-lg font-medium text-gray-900">CVEs with PoCs</h3>
|
|
<p className="text-3xl font-bold text-purple-600">{stats.cves_with_pocs || 0}</p>
|
|
<p className="text-sm text-gray-500">{(stats.poc_coverage || 0).toFixed(1)}% coverage</p>
|
|
<p className="text-sm text-gray-500">GitHub PoCs: {gitHubPocStats.cves_with_github_pocs || 0}</p>
|
|
<p className="text-sm text-gray-500">ExploitDB: {exploitdbStats.total_exploitdb_cves || 0}</p>
|
|
<p className="text-sm text-gray-500">CISA KEV: {cisaKevStats.total_kev_cves || 0}</p>
|
|
</div>
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<h3 className="text-lg font-medium text-gray-900">Recent CVEs (7d)</h3>
|
|
<p className="text-3xl font-bold text-orange-600">{stats.recent_cves_7_days || 0}</p>
|
|
</div>
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<h3 className="text-lg font-medium text-gray-900">High Quality PoCs</h3>
|
|
<p className="text-3xl font-bold text-indigo-600">{pocStats.high_quality_cves || 0}</p>
|
|
<p className="text-sm text-gray-500">Avg: {(pocStats.avg_poc_count || 0).toFixed(1)}</p>
|
|
<p className="text-sm text-gray-500">GitHub: {(gitHubPocStats.average_quality_score || 0).toFixed(1)}</p>
|
|
<p className="text-sm text-gray-500">ExploitDB: {exploitdbStats.total_exploits || 0} exploits</p>
|
|
<p className="text-sm text-gray-500">CISA KEV: {(cisaKevStats.average_threat_score || 0).toFixed(1)} threat</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Data Synchronization Controls */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-6">Data Synchronization</h2>
|
|
|
|
{/* Phase 1: CVE Data Syncing */}
|
|
<div className="mb-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-3">Phase 1: CVE Data Syncing</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<button
|
|
onClick={() => startBulkSeed(2002)}
|
|
disabled={isBulkSeedRunning()}
|
|
className={`px-4 py-2 rounded-md text-white ${
|
|
isBulkSeedRunning()
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-blue-600 hover:bg-blue-700'
|
|
}`}
|
|
>
|
|
{isBulkSeedRunning() ? 'Processing...' : 'Sync NVD CVEs'}
|
|
</button>
|
|
<button
|
|
onClick={startIncrementalUpdate}
|
|
disabled={isIncrementalUpdateRunning()}
|
|
className={`px-4 py-2 rounded-md text-white ${
|
|
isIncrementalUpdateRunning()
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-green-600 hover:bg-green-700'
|
|
}`}
|
|
>
|
|
{isIncrementalUpdateRunning() ? 'Processing...' : 'Incremental Update'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Phase 2: Exploit Data Syncing */}
|
|
<div className="mb-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-3">Phase 2: Exploit Data Syncing</h3>
|
|
<div className="relative inline-block text-left">
|
|
<button
|
|
onClick={() => setExploitSyncDropdownOpen(!exploitSyncDropdownOpen)}
|
|
className={`inline-flex items-center justify-center w-full rounded-md border shadow-sm px-4 py-2 text-sm font-medium ${
|
|
areAnyExploitSyncsRunning()
|
|
? 'border-blue-300 bg-blue-50 text-blue-700'
|
|
: 'border-gray-300 bg-white text-gray-700 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{areAnyExploitSyncsRunning() ? 'Exploit Syncs Running...' : 'Sync Exploit Data'}
|
|
<svg className="-mr-1 ml-2 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
|
|
{exploitSyncDropdownOpen && (
|
|
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10">
|
|
<div className="py-1">
|
|
<button
|
|
onClick={() => {
|
|
if (!isNomiSecSyncRunning()) {
|
|
syncNomiSec();
|
|
setExploitSyncDropdownOpen(false);
|
|
}
|
|
}}
|
|
disabled={isNomiSecSyncRunning()}
|
|
className={`block w-full text-left px-4 py-2 text-sm ${
|
|
isNomiSecSyncRunning()
|
|
? 'text-gray-400 cursor-not-allowed bg-gray-50'
|
|
: 'text-gray-700 hover:bg-purple-50 hover:text-purple-900'
|
|
}`}
|
|
>
|
|
<span className={`inline-block w-3 h-3 rounded-full mr-2 ${
|
|
isNomiSecSyncRunning() ? 'bg-gray-400' : 'bg-purple-600'
|
|
}`}></span>
|
|
{isNomiSecSyncRunning() ? 'Syncing nomi-sec PoCs...' : 'Sync nomi-sec PoCs'}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (!isGitHubPocSyncRunning()) {
|
|
syncGitHubPocs();
|
|
setExploitSyncDropdownOpen(false);
|
|
}
|
|
}}
|
|
disabled={isGitHubPocSyncRunning()}
|
|
className={`block w-full text-left px-4 py-2 text-sm ${
|
|
isGitHubPocSyncRunning()
|
|
? 'text-gray-400 cursor-not-allowed bg-gray-50'
|
|
: 'text-gray-700 hover:bg-green-50 hover:text-green-900'
|
|
}`}
|
|
>
|
|
<span className={`inline-block w-3 h-3 rounded-full mr-2 ${
|
|
isGitHubPocSyncRunning() ? 'bg-gray-400' : 'bg-green-600'
|
|
}`}></span>
|
|
{isGitHubPocSyncRunning() ? 'Syncing GitHub PoCs...' : 'Sync GitHub PoCs'}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (!isExploitDBSyncRunning()) {
|
|
syncExploitDB();
|
|
setExploitSyncDropdownOpen(false);
|
|
}
|
|
}}
|
|
disabled={isExploitDBSyncRunning()}
|
|
className={`block w-full text-left px-4 py-2 text-sm ${
|
|
isExploitDBSyncRunning()
|
|
? 'text-gray-400 cursor-not-allowed bg-gray-50'
|
|
: 'text-gray-700 hover:bg-red-50 hover:text-red-900'
|
|
}`}
|
|
>
|
|
<span className={`inline-block w-3 h-3 rounded-full mr-2 ${
|
|
isExploitDBSyncRunning() ? 'bg-gray-400' : 'bg-red-600'
|
|
}`}></span>
|
|
{isExploitDBSyncRunning() ? 'Syncing ExploitDB...' : 'Sync ExploitDB'}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
if (!isCISAKEVSyncRunning()) {
|
|
syncCISAKEV();
|
|
setExploitSyncDropdownOpen(false);
|
|
}
|
|
}}
|
|
disabled={isCISAKEVSyncRunning()}
|
|
className={`block w-full text-left px-4 py-2 text-sm ${
|
|
isCISAKEVSyncRunning()
|
|
? 'text-gray-400 cursor-not-allowed bg-gray-50'
|
|
: 'text-gray-700 hover:bg-yellow-50 hover:text-yellow-900'
|
|
}`}
|
|
>
|
|
<span className={`inline-block w-3 h-3 rounded-full mr-2 ${
|
|
isCISAKEVSyncRunning() ? 'bg-gray-400' : 'bg-yellow-600'
|
|
}`}></span>
|
|
{isCISAKEVSyncRunning() ? 'Syncing CISA KEV...' : 'Sync CISA KEV'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Phase 3: Reference Data Syncing */}
|
|
<div className="mb-6">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-3">Phase 3: Reference Data Syncing</h3>
|
|
<button
|
|
onClick={syncReferences}
|
|
disabled={hasRunningJobs}
|
|
className={`px-4 py-2 rounded-md text-white ${
|
|
hasRunningJobs
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-orange-600 hover:bg-orange-700'
|
|
}`}
|
|
>
|
|
{hasRunningJobs ? 'Processing...' : 'Sync References (Coming Soon)'}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Phase 4: Rule Generation */}
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-3">Phase 4: Rule Generation</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<button
|
|
onClick={() => regenerateRules()}
|
|
disabled={isRuleGenerationRunning()}
|
|
className={`px-4 py-2 rounded-md text-white ${
|
|
isRuleGenerationRunning()
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-indigo-600 hover:bg-indigo-700'
|
|
}`}
|
|
>
|
|
{isRuleGenerationRunning() ? 'Processing...' : 'Regenerate Rules'}
|
|
</button>
|
|
<button
|
|
onClick={() => generateLlmRules()}
|
|
disabled={isRuleGenerationRunning() || llmStatus.status !== 'ready'}
|
|
className={`px-4 py-2 rounded-md text-white ${
|
|
isRuleGenerationRunning() || llmStatus.status !== 'ready'
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-violet-600 hover:bg-violet-700'
|
|
}`}
|
|
title={llmStatus.status !== 'ready' ? 'LLM not configured' : ''}
|
|
>
|
|
{isRuleGenerationRunning() ? 'Processing...' : 'Generate LLM Rules'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* LLM Configuration */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-xl font-bold text-gray-900 mb-4">LLM Configuration</h2>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Current Provider</h3>
|
|
<div className="space-y-2">
|
|
<p className="text-sm text-gray-600">
|
|
Provider: <span className="font-medium">{llmStatus.current_provider?.provider || 'Not configured'}</span>
|
|
</p>
|
|
<p className="text-sm text-gray-600">
|
|
Model: <span className="font-medium">{llmStatus.current_provider?.model || 'Not configured'}</span>
|
|
</p>
|
|
<p className={`text-sm ${llmStatus.status === 'ready' ? 'text-green-600' : 'text-red-500'}`}>
|
|
Status: <span className="font-medium">{llmStatus.status || 'Unknown'}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Available Providers</h3>
|
|
<div className="space-y-2">
|
|
{llmStatus.available_providers?.map(provider => (
|
|
<div key={provider.name} className="flex items-center justify-between p-2 bg-gray-50 rounded">
|
|
<div>
|
|
<span className="font-medium">{provider.name}</span>
|
|
<span className={`ml-2 text-xs px-2 py-1 rounded ${
|
|
provider.available ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
}`}>
|
|
{provider.available ? 'Available' : 'Not configured'}
|
|
</span>
|
|
</div>
|
|
{provider.available && provider.name !== llmStatus.current_provider?.provider && (
|
|
<button
|
|
onClick={() => switchLlmProvider(provider.name, provider.default_model)}
|
|
className="text-xs bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 rounded"
|
|
>
|
|
Switch
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-bold text-gray-900">Recent CVEs</h2>
|
|
<div className="flex space-x-3">
|
|
<button
|
|
onClick={testNvdConnection}
|
|
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm"
|
|
>
|
|
Test NVD API
|
|
</button>
|
|
<button
|
|
onClick={handleFetchCves}
|
|
disabled={fetchingCves}
|
|
className={`px-4 py-2 rounded-md text-white ${
|
|
fetchingCves
|
|
? 'bg-gray-400 cursor-not-allowed'
|
|
: 'bg-blue-600 hover:bg-blue-700'
|
|
}`}
|
|
>
|
|
{fetchingCves ? 'Fetching...' : 'Fetch New CVEs'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{testResult && (
|
|
<div className={`mb-4 p-4 rounded-md ${
|
|
testResult.status === 'success' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
|
}`}>
|
|
<div className="flex items-center">
|
|
<span className="font-medium">NVD API Test: </span>
|
|
<span className="ml-2">{testResult.message}</span>
|
|
</div>
|
|
{testResult.status === 'success' && (
|
|
<div className="mt-2 text-sm">
|
|
<p>✅ API Key: {testResult.has_api_key ? 'Present' : 'Not configured'}</p>
|
|
<p>✅ Available results: {testResult.total_results || 0}</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{fetchingCves && (
|
|
<div className="mb-4 p-4 bg-blue-100 text-blue-800 rounded-md">
|
|
<div className="flex items-center">
|
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
|
|
<span>Fetching CVEs from NVD API (30-day lookback)... This may take 1-2 minutes.</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<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">
|
|
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 Score
|
|
</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">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{cves.slice(0, 10).map((cve) => (
|
|
<tr key={cve.id}>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
|
{cve.cve_id}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getSeverityColor(cve.severity)}`}>
|
|
{cve.severity || 'N/A'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
{cve.cvss_score || 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{cve.published_date ? formatDate(cve.published_date) : 'N/A'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
<button
|
|
onClick={() => setSelectedCve(cve)}
|
|
className="text-blue-600 hover:text-blue-900"
|
|
>
|
|
View Details
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const CVEList = () => (
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h2 className="text-xl font-bold text-gray-900">All CVEs</h2>
|
|
</div>
|
|
<div className="divide-y divide-gray-200">
|
|
{cves.map((cve) => (
|
|
<div key={cve.id} className="p-6 hover:bg-gray-50">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1">
|
|
<h3 className="text-lg font-medium text-gray-900">{cve.cve_id}</h3>
|
|
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
|
|
{cve.description}
|
|
</p>
|
|
<div className="flex items-center mt-2 space-x-4">
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getSeverityColor(cve.severity)}`}>
|
|
{cve.severity || 'N/A'}
|
|
</span>
|
|
<span className="text-sm text-gray-500">
|
|
CVSS: {cve.cvss_score || 'N/A'}
|
|
</span>
|
|
<span className="text-sm text-gray-500">
|
|
{cve.published_date ? formatDate(cve.published_date) : 'N/A'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
onClick={() => setSelectedCve(cve)}
|
|
className="ml-4 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md"
|
|
>
|
|
View Details
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const SigmaRulesList = () => (
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h2 className="text-xl font-bold text-gray-900">Generated SIGMA Rules</h2>
|
|
</div>
|
|
<div className="divide-y divide-gray-200">
|
|
{sigmaRules.map((rule) => (
|
|
<div key={rule.id} className="p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900">{rule.rule_name}</h3>
|
|
<p className="text-sm text-gray-600">CVE: {rule.cve_id}</p>
|
|
<div className="flex items-center mt-2 space-x-4">
|
|
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
|
|
{rule.detection_type}
|
|
</span>
|
|
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
|
|
{rule.confidence_level}
|
|
</span>
|
|
{rule.auto_generated && (
|
|
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">
|
|
Auto-generated
|
|
</span>
|
|
)}
|
|
{rule.exploit_based && (
|
|
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800">
|
|
🔍 Exploit-Based
|
|
</span>
|
|
)}
|
|
</div>
|
|
{rule.github_repos && rule.github_repos.length > 0 && (
|
|
<div className="mt-2">
|
|
<p className="text-xs text-gray-500">
|
|
Based on {rule.github_repos.length} GitHub repository{rule.github_repos.length > 1 ? 's' : ''}:
|
|
</p>
|
|
<div className="flex flex-wrap gap-1 mt-1">
|
|
{rule.github_repos.slice(0, 3).map((repo, index) => (
|
|
<a
|
|
key={index}
|
|
href={repo}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-blue-600 hover:text-blue-800 underline"
|
|
>
|
|
{repo.split('/').slice(-2).join('/')}
|
|
</a>
|
|
))}
|
|
{rule.github_repos.length > 3 && (
|
|
<span className="text-xs text-gray-500">
|
|
+{rule.github_repos.length - 3} more
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<span className="text-sm text-gray-500">
|
|
{formatDate(rule.created_at)}
|
|
</span>
|
|
</div>
|
|
<div className="mt-4">
|
|
<SyntaxHighlighter
|
|
language="yaml"
|
|
style={tomorrow}
|
|
className="rounded-md"
|
|
customStyle={{ fontSize: '12px' }}
|
|
>
|
|
{rule.rule_content}
|
|
</SyntaxHighlighter>
|
|
</div>
|
|
{rule.exploit_indicators && (
|
|
<div className="mt-4 p-3 bg-gray-50 rounded-md">
|
|
<h4 className="text-sm font-medium text-gray-700 mb-2">Exploit Indicators Found:</h4>
|
|
<ExploitIndicators indicators={rule.exploit_indicators} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const ExploitIndicators = ({ indicators }) => {
|
|
try {
|
|
const parsed = JSON.parse(indicators);
|
|
return (
|
|
<div className="space-y-2">
|
|
{Object.entries(parsed).map(([category, items]) => (
|
|
items.length > 0 && (
|
|
<div key={category} className="flex flex-wrap items-center gap-2">
|
|
<span className="text-xs font-medium text-gray-600 capitalize min-w-0">
|
|
{category}:
|
|
</span>
|
|
<div className="flex flex-wrap gap-1">
|
|
{items.slice(0, 5).map((item, index) => (
|
|
<span
|
|
key={index}
|
|
className="inline-flex px-2 py-1 text-xs rounded bg-gray-200 text-gray-700 font-mono"
|
|
>
|
|
{typeof item === 'string' && item.length > 30 ? item.substring(0, 30) + '...' : item}
|
|
</span>
|
|
))}
|
|
{items.length > 5 && (
|
|
<span className="text-xs text-gray-500">+{items.length - 5} more</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
))}
|
|
</div>
|
|
);
|
|
} catch (e) {
|
|
return <p className="text-xs text-gray-500">Invalid indicator data</p>;
|
|
}
|
|
};
|
|
|
|
const CVEDetail = ({ cve, onClose }) => {
|
|
const [cveRules, setCveRules] = useState([]);
|
|
|
|
useEffect(() => {
|
|
if (cve) {
|
|
fetchCveRules(cve.cve_id);
|
|
}
|
|
}, [cve]);
|
|
|
|
const fetchCveRules = async (cveId) => {
|
|
try {
|
|
const response = await axios.get(`${API_BASE_URL}/api/sigma-rules/${cveId}`);
|
|
setCveRules(response.data);
|
|
} catch (error) {
|
|
console.error('Error fetching CVE rules:', error);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
|
<div className="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-2xl font-bold text-gray-900">{cve.cve_id}</h2>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Details</h3>
|
|
<div className="bg-gray-50 p-4 rounded-md">
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500">Severity:</span>
|
|
<span className={`ml-2 inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getSeverityColor(cve.severity)}`}>
|
|
{cve.severity || 'N/A'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500">CVSS Score:</span>
|
|
<span className="ml-2 text-sm text-gray-900">{cve.cvss_score || 'N/A'}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-sm font-medium text-gray-500">Published:</span>
|
|
<span className="ml-2 text-sm text-gray-900">
|
|
{cve.published_date ? formatDate(cve.published_date) : 'N/A'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Description</h3>
|
|
<p className="text-sm text-gray-700 bg-gray-50 p-4 rounded-md">
|
|
{cve.description}
|
|
</p>
|
|
</div>
|
|
|
|
{cve.affected_products && cve.affected_products.length > 0 && (
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Affected Products</h3>
|
|
<div className="bg-gray-50 p-4 rounded-md">
|
|
<ul className="list-disc list-inside space-y-1">
|
|
{cve.affected_products.slice(0, 5).map((product, index) => (
|
|
<li key={index} className="text-sm text-gray-700">{product}</li>
|
|
))}
|
|
{cve.affected_products.length > 5 && (
|
|
<li className="text-sm text-gray-500">... and {cve.affected_products.length - 5} more</li>
|
|
)}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Generated SIGMA Rules ({cveRules.length})</h3>
|
|
{cveRules.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{cveRules.map((rule) => (
|
|
<div key={rule.id} className="border border-gray-200 rounded-md p-4">
|
|
<div className="flex justify-between items-center mb-2">
|
|
<h4 className="font-medium text-gray-900">{rule.rule_name}</h4>
|
|
<div className="flex space-x-2">
|
|
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
|
|
{rule.detection_type}
|
|
</span>
|
|
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
|
|
{rule.confidence_level}
|
|
</span>
|
|
{rule.exploit_based && (
|
|
<span className="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-red-100 text-red-800">
|
|
🔍 Exploit-Based
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
{rule.github_repos && rule.github_repos.length > 0 && (
|
|
<div className="mb-3 p-2 bg-blue-50 rounded">
|
|
<p className="text-xs text-blue-700 font-medium mb-1">
|
|
Based on GitHub exploit analysis:
|
|
</p>
|
|
<div className="flex flex-wrap gap-1">
|
|
{rule.github_repos.slice(0, 2).map((repo, index) => (
|
|
<a
|
|
key={index}
|
|
href={repo}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-blue-600 hover:text-blue-800 underline"
|
|
>
|
|
{repo.split('/').slice(-2).join('/')}
|
|
</a>
|
|
))}
|
|
{rule.github_repos.length > 2 && (
|
|
<span className="text-xs text-blue-600">
|
|
+{rule.github_repos.length - 2} more
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<SyntaxHighlighter
|
|
language="yaml"
|
|
style={tomorrow}
|
|
className="rounded-md"
|
|
customStyle={{ fontSize: '12px' }}
|
|
>
|
|
{rule.rule_content}
|
|
</SyntaxHighlighter>
|
|
{rule.exploit_indicators && (
|
|
<div className="mt-3 p-2 bg-gray-50 rounded">
|
|
<p className="text-xs font-medium text-gray-700 mb-1">Exploit Indicators:</p>
|
|
<ExploitIndicators indicators={rule.exploit_indicators} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-gray-500 bg-gray-50 p-4 rounded-md">No SIGMA rules generated for this CVE yet.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const BulkJobsList = () => (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<h1 className="text-2xl font-bold text-gray-900">Bulk Processing Jobs</h1>
|
|
<button
|
|
onClick={fetchData}
|
|
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm"
|
|
>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
{/* Bulk Status Overview */}
|
|
<div className="bg-white rounded-lg shadow p-6">
|
|
<h2 className="text-lg font-bold text-gray-900 mb-4">System Status</h2>
|
|
{bulkStatus.database_stats && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-blue-600">{bulkStatus.database_stats.total_cves}</div>
|
|
<div className="text-sm text-gray-500">Total CVEs</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-green-600">{bulkStatus.database_stats.bulk_processed_cves}</div>
|
|
<div className="text-sm text-gray-500">Bulk Processed</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-purple-600">{bulkStatus.database_stats.cves_with_pocs}</div>
|
|
<div className="text-sm text-gray-500">With PoCs</div>
|
|
</div>
|
|
<div className="text-center">
|
|
<div className="text-2xl font-bold text-indigo-600">{bulkStatus.database_stats.nomi_sec_rules}</div>
|
|
<div className="text-sm text-gray-500">Enhanced Rules</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Running Jobs */}
|
|
{bulkJobs.some(job => job.status === 'running' || job.status === 'pending') && (
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h2 className="text-lg font-bold text-gray-900">Running Jobs</h2>
|
|
</div>
|
|
<div className="divide-y divide-gray-200">
|
|
{bulkJobs
|
|
.filter(job => job.status === 'running' || job.status === 'pending')
|
|
.map((job) => (
|
|
<div key={job.id} className="px-6 py-4 bg-blue-50">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center space-x-3">
|
|
<h3 className="text-lg font-medium text-gray-900">{job.job_type}</h3>
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
|
job.status === 'running' ? 'bg-blue-100 text-blue-800' :
|
|
'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{job.status}
|
|
</span>
|
|
</div>
|
|
<div className="mt-2 flex items-center space-x-6 text-sm text-gray-500">
|
|
<span>Started: {formatDate(job.started_at)}</span>
|
|
{job.year && <span>Year: {job.year}</span>}
|
|
</div>
|
|
{job.total_items > 0 && (
|
|
<div className="mt-2">
|
|
<div className="flex items-center space-x-4 text-sm text-gray-600">
|
|
<span>Progress: {job.processed_items}/{job.total_items}</span>
|
|
{job.failed_items > 0 && (
|
|
<span className="text-red-600">Failed: {job.failed_items}</span>
|
|
)}
|
|
</div>
|
|
<div className="mt-1 w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className="bg-blue-600 h-2 rounded-full"
|
|
style={{ width: `${(job.processed_items / job.total_items) * 100}%` }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex-shrink-0 ml-4">
|
|
<button
|
|
onClick={() => cancelJob(job.id)}
|
|
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded-md text-sm font-medium"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Recent Jobs */}
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h2 className="text-lg font-bold text-gray-900">Recent Jobs</h2>
|
|
</div>
|
|
<div className="divide-y divide-gray-200">
|
|
{bulkJobs.length === 0 ? (
|
|
<div className="px-6 py-8 text-center text-gray-500">
|
|
No bulk processing jobs found
|
|
</div>
|
|
) : (
|
|
bulkJobs.map((job) => (
|
|
<div key={job.id} className="px-6 py-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex-1">
|
|
<div className="flex items-center space-x-3">
|
|
<h3 className="text-lg font-medium text-gray-900">{job.job_type}</h3>
|
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
|
job.status === 'completed' ? 'bg-green-100 text-green-800' :
|
|
job.status === 'running' ? 'bg-blue-100 text-blue-800' :
|
|
job.status === 'failed' ? 'bg-red-100 text-red-800' :
|
|
job.status === 'cancelled' ? 'bg-orange-100 text-orange-800' :
|
|
'bg-gray-100 text-gray-800'
|
|
}`}>
|
|
{job.status}
|
|
</span>
|
|
</div>
|
|
<div className="mt-2 flex items-center space-x-6 text-sm text-gray-500">
|
|
<span>Started: {formatDate(job.started_at)}</span>
|
|
{job.completed_at && (
|
|
<span>Completed: {formatDate(job.completed_at)}</span>
|
|
)}
|
|
{job.year && (
|
|
<span>Year: {job.year}</span>
|
|
)}
|
|
</div>
|
|
{job.total_items > 0 && (
|
|
<div className="mt-2">
|
|
<div className="flex items-center space-x-4 text-sm text-gray-600">
|
|
<span>Progress: {job.processed_items}/{job.total_items}</span>
|
|
{job.failed_items > 0 && (
|
|
<span className="text-red-600">Failed: {job.failed_items}</span>
|
|
)}
|
|
</div>
|
|
<div className="mt-1 w-full bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className="bg-blue-600 h-2 rounded-full"
|
|
style={{ width: `${(job.processed_items / job.total_items) * 100}%` }}
|
|
></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{job.error_message && (
|
|
<div className="mt-2 p-2 bg-red-50 border border-red-200 rounded text-sm text-red-700">
|
|
{job.error_message}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex-shrink-0 ml-4">
|
|
{(job.status === 'running' || job.status === 'pending') && (
|
|
<button
|
|
onClick={() => cancelJob(job.id)}
|
|
className="bg-red-600 hover:bg-red-700 text-white px-3 py-1 rounded-md text-sm font-medium"
|
|
>
|
|
Cancel
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600"></div>
|
|
<p className="mt-4 text-gray-600">Loading...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-100">
|
|
<nav className="bg-white shadow">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between h-16">
|
|
<div className="flex">
|
|
<div className="flex-shrink-0 flex items-center">
|
|
<h1 className="text-xl font-bold text-gray-900">CVE-SIGMA Auto Generator</h1>
|
|
</div>
|
|
<div className="ml-6 flex space-x-8">
|
|
<button
|
|
onClick={() => setActiveTab('dashboard')}
|
|
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
|
|
activeTab === 'dashboard'
|
|
? 'border-blue-500 text-gray-900'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
Dashboard
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('cves')}
|
|
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
|
|
activeTab === 'cves'
|
|
? 'border-blue-500 text-gray-900'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
CVEs
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('rules')}
|
|
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
|
|
activeTab === 'rules'
|
|
? 'border-blue-500 text-gray-900'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
SIGMA Rules
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveTab('bulk-jobs')}
|
|
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium ${
|
|
activeTab === 'bulk-jobs'
|
|
? 'border-blue-500 text-gray-900'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
Bulk Jobs
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
|
|
<div className="px-4 py-6 sm:px-0">
|
|
{activeTab === 'dashboard' && <Dashboard />}
|
|
{activeTab === 'cves' && <CVEList />}
|
|
{activeTab === 'rules' && <SigmaRulesList />}
|
|
{activeTab === 'bulk-jobs' && <BulkJobsList />}
|
|
</div>
|
|
</main>
|
|
|
|
{selectedCve && (
|
|
<CVEDetail cve={selectedCve} onClose={() => setSelectedCve(null)} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|