SentinelScan-WSS / frontend /src /pages /OrganizationPage.jsx
larxius's picture
Update frontend/src/pages/OrganizationPage.jsx
2f8096f verified
Raw
History Blame Contribute Delete
21.1 kB
import { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../components/AuthContext';
import { Shield, Activity, Users, Globe, Lock, ShieldAlert, ArrowLeft, BarChart3, PieChart as PieChartIcon } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell, Legend, LabelList, ComposedChart } from 'recharts';
const SEVERITY_COLORS = ['#EF4444', '#F97316', '#EAB308', '#3B82F6']; // Critical, High, Medium, Low
const SCAN_TYPE_COLORS = ['#3B82F6', '#8B5CF6', '#10B981', '#F59E0B'];
const CustomChartTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="bg-[#0f172a] border border-[#334155] rounded-lg px-3 py-1.5 shadow-xl text-left pointer-events-none">
{label && <div className="text-[12px] font-bold text-sky-400 leading-tight mb-1">{label}</div>}
{payload.map((item, index) => (
<div key={index} className="text-[12px] font-medium text-slate-200 leading-tight">
<span style={{ color: item.color || '#38bdf8' }}>{item.name || item.dataKey}</span> : <span className="font-bold text-white">{item.value}</span>
</div>
))}
</div>
);
}
return null;
};
export const OrganizationPage = () => {
const { user, token, loading: authLoading } = useAuth();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [summaryData, setSummaryData] = useState(null);
const [scanHistory, setScanHistory] = useState([]);
const [filterOrg, setFilterOrg] = useState('All');
const [filterWebsite, setFilterWebsite] = useState('All');
const getToken = useCallback(() => localStorage.getItem('wss_token') || localStorage.getItem('wss_token') || token, [token]);
const fetchData = useCallback(async () => {
try {
const activeToken = getToken();
if (!activeToken) {
setLoading(false);
return;
}
const [summaryRes, historyRes] = await Promise.all([
fetch('/api/vulnerabilities/summary?global=true', { headers: { 'Authorization': `Bearer ${activeToken}` } }),
fetch('/api/scans/history?global=true&limit=100', { headers: { 'Authorization': `Bearer ${activeToken}` } })
]);
if (!summaryRes.ok || !historyRes.ok) return;
const summaryJson = await summaryRes.json();
const historyJson = await historyRes.json();
setSummaryData(summaryJson.summary);
setScanHistory(historyJson.scans || []);
} catch (err) {
console.error("Failed to fetch organization data:", err);
} finally {
setLoading(false);
}
}, [getToken]);
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 5000); // Polling real-time data every 5s
return () => clearInterval(interval);
}, [fetchData]);
if (loading || authLoading) {
return (
<div className="flex h-[80vh] items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
</div>
);
}
// Extract Unique Orgs & Websites for filters
const uniqueOrgs = [...new Set(scanHistory.map(s => s.org_name).filter(Boolean))];
const uniqueWebsites = [...new Set(scanHistory.map(s => s.target_url).filter(Boolean))];
// Apply Filters
const filteredScans = scanHistory
.filter(scan => filterOrg === 'All' || scan.org_name === filterOrg)
.filter(scan => filterWebsite === 'All' || scan.target_url === filterWebsite);
// Derive metrics dynamically from filteredScans
const completedScans = filteredScans.filter(s => s.status === 'completed' && s.security_score !== null);
const score = completedScans.length > 0
? Math.round(completedScans.reduce((sum, s) => sum + s.security_score, 0) / completedScans.length)
: null;
const totalVulnerabilities = filteredScans.reduce((sum, s) => {
const vc = s.vulnerabilities_count;
return sum + (vc ? (vc.critical + vc.high + vc.medium + vc.low) : (s.total_vulnerabilities || 0));
}, 0);
// Active Assets (unique target URLs)
const activeAssets = new Set(filteredScans.map(s => s.target_url).filter(Boolean)).size;
// Vulnerability Distribution Pie Chart Data
const vulnCounts = filteredScans.reduce((acc, s) => {
if (s.vulnerabilities_count) {
acc.critical += s.vulnerabilities_count.critical || 0;
acc.high += s.vulnerabilities_count.high || 0;
acc.medium += s.vulnerabilities_count.medium || 0;
acc.low += s.vulnerabilities_count.low || 0;
}
return acc;
}, { critical: 0, high: 0, medium: 0, low: 0 });
const vulnerabilityTypes = [
{ name: 'Critical', value: vulnCounts.critical },
{ name: 'High', value: vulnCounts.high },
{ name: 'Medium', value: vulnCounts.medium },
{ name: 'Low', value: vulnCounts.low },
].filter(v => v.value > 0);
if (vulnerabilityTypes.length === 0) {
vulnerabilityTypes.push({ name: 'Clean / No Risks', value: 1 });
}
// Risk Score Trend (Last 10 Scans)
const riskTrendData = [...filteredScans]
.filter(s => s.started_at)
.sort((a, b) => new Date(a.started_at) - new Date(b.started_at))
.slice(-10)
.map((scan, index) => {
const totalVulns = scan.vulnerabilities_count
? (scan.vulnerabilities_count.critical + scan.vulnerabilities_count.high + scan.vulnerabilities_count.medium + scan.vulnerabilities_count.low)
: (scan.total_vulnerabilities || 0);
return {
name: scan.target_url ? scan.target_url.replace('https://', '').replace('http://', '').replace(/\/$/, '') : `Scan ${index + 1}`,
securityScore: Math.round(scan.security_score ?? 100),
vulnerabilities: totalVulns
};
});
// Group Scans by Month for Bar Chart
const monthsData = {};
filteredScans.forEach(scan => {
const d = new Date(scan.started_at || scan.created_at);
if (isNaN(d.getTime())) return;
const month = d.toLocaleString('en-US', { month: 'short', year: 'numeric' });
if (!monthsData[month]) monthsData[month] = { month, scans: 0, issues: 0 };
monthsData[month].scans += 1;
const totalVulns = scan.vulnerabilities_count
? (scan.vulnerabilities_count.critical + scan.vulnerabilities_count.high + scan.vulnerabilities_count.medium + scan.vulnerabilities_count.low)
: (scan.total_vulnerabilities || 0);
monthsData[month].issues += totalVulns;
});
const scanHistoryChartData = Object.values(monthsData);
// Top Vulnerability Categories Chart Data
const categoriesData = Object.entries(summaryData?.by_category || {})
.map(([cat, count]) => ({ category: cat || 'General Security', count }))
.sort((a, b) => b.count - a.count)
.slice(0, 6);
// Extract Unique Orgs & Websites for filters is moved up.
// Scan Types Breakdown Chart Data (Filtered)
const scanTypeCounts = {};
filteredScans.forEach(scan => {
const type = scan.scan_type ? (scan.scan_type.charAt(0).toUpperCase() + scan.scan_type.slice(1)) + ' Scan' : 'Advanced Scan';
scanTypeCounts[type] = (scanTypeCounts[type] || 0) + 1;
});
const scanTypeChartData = Object.entries(scanTypeCounts).map(([name, value]) => ({ name, value }));
return (
<div className="w-full text-on-surface animate-fade-in pb-xl">
<div className="flex flex-col md:flex-row md:items-center justify-between mb-xl gap-sm border-b border-outline-variant/60 pb-md">
<div>
<h1 className="font-extrabold text-on-surface tracking-tight text-[24px] m-0 flex items-center gap-1.5">
{user?.role === 'support_engineer' ? (
<>Support Engineer <span className="text-primary">Operations</span></>
) : (
<>LarShield <span className="text-primary">Global Management</span></>
)}
</h1>
<p className="font-body-md text-on-surface-variant text-[13.5px] mt-1 m-0">
{user?.role === 'support_engineer'
? 'Client environment inspection, troubleshooting assistance, and system logs.'
: 'Centralized oversight for all client organizations, scans, and security nodes.'}
</p>
</div>
<div className="flex gap-sm flex-wrap items-center">
<button onClick={fetchData} className="flex items-center px-3.5 py-1.5 bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[12.5px] cursor-pointer shadow-2xs">
<Activity className={`w-3.5 h-3.5 mr-1.5 text-primary ${loading ? 'animate-spin' : ''}`} /> Sync Metrics
</button>
<button onClick={() => navigate(user?.role === 'support_engineer' ? '/support' : '/super-admin')} className="flex items-center px-3.5 py-1.5 bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[12.5px] cursor-pointer shadow-2xs">
<Shield className="w-3.5 h-3.5 mr-1.5 text-primary" /> Global Management
</button>
<button onClick={() => navigate('/super-admin/logs')} className="flex items-center px-3.5 py-1.5 bg-surface-container border border-outline-variant text-on-surface rounded-lg hover:bg-surface-container-high transition-colors font-bold text-[12.5px] cursor-pointer shadow-2xs">
<ShieldAlert className="w-3.5 h-3.5 mr-1.5 text-primary" /> Logs & Threats
</button>
<button onClick={() => navigate(-1)} className="flex items-center px-4 py-1.5 bg-primary text-white rounded-lg hover:brightness-110 transition-all font-bold text-[13px] border-0 cursor-pointer shadow-sm">
<ArrowLeft className="w-3.5 h-3.5 mr-1" /> Back
</button>
</div>
</div>
{/* Top Metrics Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-md mb-xl">
{[
{ title: 'SECURITY SCORE', value: score !== null ? `${score}/100` : 'N/A', icon: Shield, color: score === null ? 'text-slate-400' : score > 80 ? 'text-green-500' : score > 50 ? 'text-orange-500' : 'text-error', bg: score === null ? 'bg-slate-500/10 border-slate-500/20' : score > 80 ? 'bg-green-500/10 border-green-500/20' : score > 50 ? 'bg-orange-500/10 border-orange-500/20' : 'bg-error/10 border-error/20' },
{ title: 'ACTIVE ASSETS', value: activeAssets.toString(), icon: Globe, color: 'text-blue-500', bg: 'bg-blue-500/10 border-blue-500/20' },
{ title: 'TOTAL SCANS', value: filteredScans.length.toString(), icon: Activity, color: 'text-purple-500', bg: 'bg-purple-500/10 border-purple-500/20' },
{ title: 'OPEN RISKS', value: totalVulnerabilities.toString(), icon: ShieldAlert, color: totalVulnerabilities > 0 ? 'text-orange-500' : 'text-green-500', bg: totalVulnerabilities > 0 ? 'bg-orange-500/10 border-orange-500/20' : 'bg-green-500/10 border-green-500/20' }
].map((metric, i) => (
<div key={i} className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-2xs hover:shadow-md transition-all group">
<div className="flex justify-between items-center">
<div>
<p className="text-on-surface-variant font-bold text-[12px] uppercase tracking-wider mb-1.5">{metric.title}</p>
<h3 className="text-[30px] font-extrabold tracking-tight text-on-surface leading-none">{metric.value}</h3>
</div>
<div className={`${metric.bg} p-3 rounded-xl border group-hover:scale-110 transition-transform flex items-center justify-center`}>
<metric.icon className={`${metric.color} w-6 h-6`} />
</div>
</div>
</div>
))}
</div>
{/* Scan Engine Distribution */}
{scanTypeChartData.length > 0 && (
<div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm mb-xl">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-6 gap-4">
<h2 className="font-headline-sm font-bold text-on-surface text-[18px] flex items-center gap-2">
<PieChartIcon className="w-5 h-5 text-purple-400" />
Scan Mode Distribution
</h2>
<div className="flex gap-sm">
<select
value={filterOrg}
onChange={e => setFilterOrg(e.target.value)}
className="bg-surface-container border border-outline-variant text-on-surface rounded-lg px-3 py-1.5 text-sm font-bold outline-none"
>
<option value="All">All Organizations</option>
{uniqueOrgs.map(org => <option key={org} value={org}>{org}</option>)}
</select>
<select
value={filterWebsite}
onChange={e => setFilterWebsite(e.target.value)}
className="bg-surface-container border border-outline-variant text-on-surface rounded-lg px-3 py-1.5 text-sm font-bold outline-none"
>
<option value="All">All Websites</option>
{uniqueWebsites.map(web => <option key={web} value={web}>{web.replace(/^https?:\/\//, '')}</option>)}
</select>
</div>
</div>
<div className="h-[250px] w-full">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={scanTypeChartData}
cx="50%"
cy="50%"
outerRadius={85}
dataKey="value"
label={({ name, percent }) => `${name} ${(percent * 100).toFixed(0)}%`}
>
{scanTypeChartData.map((entry, index) => (
<Cell key={`scan-cell-${index}`} fill={SCAN_TYPE_COLORS[index % SCAN_TYPE_COLORS.length]} />
))}
</Pie>
<RechartsTooltip content={<CustomChartTooltip />} />
</PieChart>
</ResponsiveContainer>
</div>
</div>
)}
{/* Row 1: Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-lg mb-xl">
{/* Risk Trend Line Chart */}
<div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
<h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
<Activity className="w-5 h-5 text-primary" />
Security Score Trend (Recent {riskTrendData.length} Scans)
</h2>
<div className="h-[300px] w-full">
{riskTrendData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<LineChart data={riskTrendData}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" vertical={false} />
<XAxis dataKey="name" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 11 }} />
<YAxis stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} domain={[0, 100]} />
<RechartsTooltip content={<CustomChartTooltip />} />
<Line type="monotone" dataKey="securityScore" name="Security Score" stroke="#3B82F6" strokeWidth={3} dot={{ r: 4, fill: '#3B82F6', strokeWidth: 2 }} activeDot={{ r: 6 }} />
</LineChart>
</ResponsiveContainer>
) : (
<div className="flex h-full items-center justify-center text-on-surface-variant">No scan history available.</div>
)}
</div>
</div>
{/* Vulnerability Distribution Bar Chart */}
<div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
<h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-primary" />
Severity Level Breakdown
</h2>
<div className="h-[300px] w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={vulnerabilityTypes} margin={{ top: 20, right: 30, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" vertical={false} />
<XAxis dataKey="name" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
<YAxis stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
<RechartsTooltip content={<CustomChartTooltip />} />
<Bar dataKey="value" name="Risks" radius={[4, 4, 0, 0]}>
<LabelList dataKey="value" position="top" fill="#9CA3AF" fontSize={12} fontWeight="bold" />
{vulnerabilityTypes.map((entry, index) => (
<Cell key={`cell-${index}`} fill={entry.name.includes('Clean') ? '#10B981' : SEVERITY_COLORS[index % SEVERITY_COLORS.length]} />
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
</div>
</div>
{/* Row 2: Secondary Charts Grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-lg mb-xl">
{/* Scan Frequency vs Issues Found */}
<div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
<h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
<BarChart3 className="w-5 h-5 text-primary" />
Monthly Scans vs Issues Found
</h2>
<div className="h-[300px] w-full">
{scanHistoryChartData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={scanHistoryChartData} margin={{ top: 20, right: 30, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" vertical={false} />
<XAxis dataKey="month" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
<YAxis yAxisId="left" orientation="left" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
<YAxis yAxisId="right" orientation="right" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
<RechartsTooltip content={<CustomChartTooltip />} />
<Legend wrapperStyle={{ fontSize: '12px' }} />
<Bar yAxisId="left" dataKey="scans" name="Total Scans" fill="#3B82F6" radius={[4, 4, 0, 0]} maxBarSize={40}>
<LabelList dataKey="scans" position="insideTop" fill="#ffffff" fontSize={11} fontWeight="bold" offset={10} />
</Bar>
<Line yAxisId="right" type="monotone" dataKey="issues" name="Issues Found" stroke="#EF4444" strokeWidth={3} dot={{ r: 4, fill: '#EF4444', strokeWidth: 2 }} activeDot={{ r: 6 }}>
<LabelList dataKey="issues" position="top" fill="#EF4444" fontSize={12} fontWeight="bold" offset={10} />
</Line>
</ComposedChart>
</ResponsiveContainer>
) : (
<div className="flex h-full items-center justify-center text-on-surface-variant">No scan history available.</div>
)}
</div>
</div>
{/* Top Vulnerability Categories */}
<div className="bg-surface-container-lowest border border-outline-variant p-lg rounded-2xl shadow-sm">
<h2 className="font-headline-sm font-bold text-on-surface mb-6 text-[18px] flex items-center gap-2">
<ShieldAlert className="w-5 h-5 text-orange-500" />
Top Vulnerability Categories (OWASP)
</h2>
<div className="h-[300px] w-full">
{categoriesData.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<BarChart data={categoriesData} layout="vertical" margin={{ top: 10, right: 30, left: 40, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#374151" horizontal={false} />
<XAxis type="number" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 12 }} />
<YAxis dataKey="category" type="category" stroke="#9CA3AF" tick={{ fill: '#9CA3AF', fontSize: 11 }} width={110} />
<RechartsTooltip content={<CustomChartTooltip />} />
<Bar dataKey="count" name="Detections" fill="#F97316" radius={[0, 4, 4, 0]}>
<LabelList dataKey="count" position="right" fill="#9CA3AF" fontSize={11} fontWeight="bold" />
</Bar>
</BarChart>
</ResponsiveContainer>
) : (
<div className="flex h-full items-center justify-center text-on-surface-variant">No category breakdown available.</div>
)}
</div>
</div>
</div>
</div>
);
};