Files
gdit-front/src/pages/ImageDetail.tsx

334 lines
12 KiB
TypeScript

import { useEffect, useState } from 'react'
import { useParams, Link } from 'react-router-dom'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { apiClient, Image, Vulnerability } from '@/lib/api'
import {
ArrowLeft,
Container,
Shield,
AlertTriangle,
RefreshCw,
Calendar,
ExternalLink
} from 'lucide-react'
const severityOrder = {
critical: 0,
high: 1,
medium: 2,
low: 3,
unspecified: 4
}
export function ImageDetail() {
const { id } = useParams<{ id: string }>()
const [image, setImage] = useState<Image | null>(null)
const [vulnerabilities, setVulnerabilities] = useState<Vulnerability[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchImageData = async () => {
if (!id) return
try {
setIsLoading(true)
setError(null)
// Fetch image details and vulnerabilities in parallel
const [imageData, vulnerabilitiesData] = await Promise.all([
apiClient.getImage(parseInt(id)),
apiClient.getImageVulnerabilities(parseInt(id))
])
setImage(imageData)
// Sort vulnerabilities by severity
const sortedVulnerabilities = vulnerabilitiesData.sort((a, b) => {
const severityA = severityOrder[a.severity as keyof typeof severityOrder] ?? 5
const severityB = severityOrder[b.severity as keyof typeof severityOrder] ?? 5
return severityA - severityB
})
setVulnerabilities(sortedVulnerabilities)
} catch (error) {
console.error('Failed to fetch image data:', error)
setError('Failed to load image information')
} finally {
setIsLoading(false)
}
}
useEffect(() => {
fetchImageData()
}, [id])
if (isLoading) {
return (
<div className="flex items-center justify-center h-64">
<RefreshCw className="h-8 w-8 animate-spin" />
</div>
)
}
if (error || !image) {
return (
<div className="text-center text-red-600">
<p>{error || 'Image not found'}</p>
<Link to="/images">
<Button variant="outline" className="mt-4">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Images
</Button>
</Link>
</div>
)
}
const vulnerabilityCounts = vulnerabilities.reduce((acc, vuln) => {
acc[vuln.severity] = (acc[vuln.severity] || 0) + 1
return acc
}, {} as Record<string, number>)
const formatCVSSScore = (score: string | null): string => {
if (!score) return 'N/A'
const numScore = parseFloat(score)
if (isNaN(numScore)) return score
return numScore.toFixed(1)
}
const getCVSSColor = (score: string | null): string => {
if (!score) return 'text-gray-500'
const numScore = parseFloat(score)
if (isNaN(numScore)) return 'text-gray-500'
if (numScore >= 9.0) return 'text-red-600 font-bold'
if (numScore >= 7.0) return 'text-orange-600 font-semibold'
if (numScore >= 4.0) return 'text-yellow-600'
if (numScore > 0) return 'text-blue-600'
return 'text-gray-500'
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<Link to="/images">
<Button variant="outline" size="sm">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Images
</Button>
</Link>
<div>
<h1 className="text-3xl font-bold tracking-tight">{image.image_name}</h1>
<p className="text-gray-600">{image.full_image_name}</p>
</div>
</div>
<div className="flex items-center space-x-2">
<Badge variant={image.is_active ? "default" : "secondary"}>
{image.is_active ? "Active" : "Inactive"}
</Badge>
</div>
</div>
{/* Image Information */}
<div className="grid gap-6 md:grid-cols-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Tag</CardTitle>
<Container className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{image.tag || 'latest'}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Registry</CardTitle>
<ExternalLink className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{image.registry || 'Docker Hub'}</div>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Usage Count</CardTitle>
<Shield className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{image.usage_count || 0}</div>
<p className="text-xs text-muted-foreground">
Files using this image
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Last Seen</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-sm">
{new Date(image.last_seen).toLocaleString()}
</div>
</CardContent>
</Card>
</div>
{/* Vulnerability Summary */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Shield className="h-5 w-5 mr-2" />
Vulnerability Summary
</CardTitle>
<CardDescription>
Security vulnerabilities found in this Docker image
</CardDescription>
</CardHeader>
<CardContent>
{vulnerabilities.length > 0 ? (
<div className="grid gap-4 md:grid-cols-5">
<div className="text-center">
<div className="text-2xl font-bold text-red-600">
{vulnerabilityCounts.critical || 0}
</div>
<Badge variant="critical" className="text-xs">Critical</Badge>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-orange-600">
{vulnerabilityCounts.high || 0}
</div>
<Badge variant="high" className="text-xs">High</Badge>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-yellow-600">
{vulnerabilityCounts.medium || 0}
</div>
<Badge variant="medium" className="text-xs">Medium</Badge>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-blue-600">
{vulnerabilityCounts.low || 0}
</div>
<Badge variant="low" className="text-xs">Low</Badge>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-600">
{vulnerabilityCounts.unspecified || 0}
</div>
<Badge variant="secondary" className="text-xs">Unspecified</Badge>
</div>
</div>
) : (
<div className="text-center py-8 text-gray-500">
No vulnerabilities found
</div>
)}
</CardContent>
</Card>
{/* Vulnerabilities List */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<AlertTriangle className="h-5 w-5 mr-2" />
Vulnerability Details
</CardTitle>
<CardDescription>
Detailed list of all vulnerabilities found in this image
</CardDescription>
</CardHeader>
<CardContent>
{vulnerabilities.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>CVE ID</TableHead>
<TableHead>Severity</TableHead>
<TableHead>CVSS Score</TableHead>
<TableHead>Title</TableHead>
<TableHead>Fixed Version</TableHead>
<TableHead>Published</TableHead>
<TableHead>Scan Job</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{vulnerabilities.map((vulnerability) => (
<TableRow key={vulnerability.id}>
<TableCell>
<code className="text-sm font-mono bg-gray-100 px-2 py-1 rounded">
{vulnerability.vulnerability_id}
</code>
</TableCell>
<TableCell>
<Badge
variant={vulnerability.severity as "critical" | "high" | "medium" | "low" | "default"}
>
{vulnerability.severity.charAt(0).toUpperCase() + vulnerability.severity.slice(1)}
</Badge>
</TableCell>
<TableCell>
<span className={getCVSSColor(vulnerability.cvss_score)}>
{formatCVSSScore(vulnerability.cvss_score)}
</span>
</TableCell>
<TableCell>
<div className="max-w-md">
<div className="font-medium truncate" title={vulnerability.title || undefined}>
{vulnerability.title || 'No title available'}
</div>
{vulnerability.description && (
<div className="text-sm text-gray-600 truncate" title={vulnerability.description}>
{vulnerability.description}
</div>
)}
</div>
</TableCell>
<TableCell>
{vulnerability.fixed_version ? (
<Badge variant="outline">{vulnerability.fixed_version}</Badge>
) : (
<span className="text-gray-400">Not available</span>
)}
</TableCell>
<TableCell className="text-sm text-gray-600">
{vulnerability.published_date
? new Date(vulnerability.published_date).toLocaleDateString()
: 'Unknown'
}
</TableCell>
<TableCell>
{vulnerability.scan_job_id ? (
<Link
to={`/scan-jobs`}
className="text-blue-600 hover:underline text-sm"
>
#{vulnerability.scan_job_id}
</Link>
) : (
<span className="text-gray-400 text-sm">-</span>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<div className="text-center py-8 text-gray-500">
No vulnerabilities found for this image
</div>
)}
</CardContent>
</Card>
</div>
)
}