website (17 pages + 3 components): - multiplayer/wizard, middleware/wizard+test-wizard, communication - builds/wizard, staff-search, voice, sbom/wizard - foerderantrag, mail/tasks, tools/communication, sbom - compliance/evidence, uni-crawler, brandbook (already done) - CollectionsTab, IngestionTab, RiskHeatmap backend-lehrer (5 files): - letters_api (641 → 2), certificates_api (636 → 2) - alerts_agent/db/models (636 → 3) - llm_gateway/communication_service (614 → 2) - game/database already done in prior batch klausur-service (2 files): - hybrid_vocab_extractor (664 → 2) - klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2) voice-service (3 files): - bqas/rag_judge (618 → 3), runner (529 → 2) - enhanced_task_orchestrator (519 → 2) studio-v2 (6 files): - korrektur/[klausurId] (578 → 4), fairness (569 → 2) - AlertsWizard (552 → 2), OnboardingWizard (513 → 2) - korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
155 lines
7.4 KiB
TypeScript
155 lines
7.4 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* SBOM (Software Bill of Materials) Admin Page
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import AdminLayout from '@/components/admin/AdminLayout'
|
|
import {
|
|
Component,
|
|
SBOMData,
|
|
CategoryType,
|
|
INFRASTRUCTURE_COMPONENTS,
|
|
SECURITY_TOOLS,
|
|
PYTHON_PACKAGES,
|
|
GO_MODULES,
|
|
NODE_PACKAGES,
|
|
} from './_components/sbom-data'
|
|
import { SBOMTable } from './_components/SBOMTable'
|
|
|
|
export default function SBOMPage() {
|
|
const [sbomData, setSbomData] = useState<SBOMData | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [activeCategory, setActiveCategory] = useState<CategoryType>('all')
|
|
const [searchTerm, setSearchTerm] = useState('')
|
|
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
|
|
|
useEffect(() => {
|
|
const loadSBOM = async () => {
|
|
setLoading(true)
|
|
try { const res = await fetch(`${BACKEND_URL}/api/v1/security/sbom`); if (res.ok) setSbomData(await res.json()) }
|
|
catch (error) { console.error('Failed to load SBOM:', error) }
|
|
finally { setLoading(false) }
|
|
}
|
|
loadSBOM()
|
|
}, [])
|
|
|
|
const getFilteredComponents = () => {
|
|
let components: Component[]
|
|
if (activeCategory === 'infrastructure') components = INFRASTRUCTURE_COMPONENTS
|
|
else if (activeCategory === 'security-tools') components = SECURITY_TOOLS
|
|
else if (activeCategory === 'python') components = [...PYTHON_PACKAGES, ...(sbomData?.components || [])]
|
|
else if (activeCategory === 'go') components = GO_MODULES
|
|
else if (activeCategory === 'nodejs') components = NODE_PACKAGES
|
|
else {
|
|
components = [
|
|
...INFRASTRUCTURE_COMPONENTS.map(c => ({ ...c, category: c.category || 'infrastructure' })),
|
|
...SECURITY_TOOLS.map(c => ({ ...c, category: c.category || 'security-tool' })),
|
|
...PYTHON_PACKAGES.map(c => ({ ...c, category: 'python' })),
|
|
...GO_MODULES.map(c => ({ ...c, category: 'go' })),
|
|
...NODE_PACKAGES.map(c => ({ ...c, category: 'nodejs' })),
|
|
...(sbomData?.components || []).map(c => ({ ...c, category: 'python' })),
|
|
]
|
|
}
|
|
if (searchTerm) {
|
|
components = components.filter(c =>
|
|
c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
c.version.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(c.description?.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
)
|
|
}
|
|
return components
|
|
}
|
|
|
|
const stats = {
|
|
totalInfra: INFRASTRUCTURE_COMPONENTS.length,
|
|
totalSecurityTools: SECURITY_TOOLS.length,
|
|
totalPython: PYTHON_PACKAGES.length + (sbomData?.components?.length || 0),
|
|
totalGo: GO_MODULES.length,
|
|
totalNode: NODE_PACKAGES.length,
|
|
totalAll: INFRASTRUCTURE_COMPONENTS.length + SECURITY_TOOLS.length + PYTHON_PACKAGES.length + GO_MODULES.length + NODE_PACKAGES.length + (sbomData?.components?.length || 0),
|
|
databases: INFRASTRUCTURE_COMPONENTS.filter(c => c.category === 'database').length,
|
|
services: INFRASTRUCTURE_COMPONENTS.filter(c => c.category === 'application').length,
|
|
}
|
|
|
|
const categories = [
|
|
{ id: 'all', name: 'Alle', count: stats.totalAll },
|
|
{ id: 'infrastructure', name: 'Infrastruktur', count: stats.totalInfra },
|
|
{ id: 'security-tools', name: 'Security Tools', count: stats.totalSecurityTools },
|
|
{ id: 'python', name: 'Python', count: stats.totalPython },
|
|
{ id: 'go', name: 'Go', count: stats.totalGo },
|
|
{ id: 'nodejs', name: 'Node.js', count: stats.totalNode },
|
|
]
|
|
|
|
return (
|
|
<AdminLayout title="SBOM" description="Software Bill of Materials - Alle Komponenten & Abhaengigkeiten">
|
|
{/* Stats Cards */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-4 mb-6">
|
|
{[
|
|
{ v: stats.totalAll, l: 'Komponenten Total', c: 'text-slate-800' },
|
|
{ v: stats.totalInfra, l: 'Docker Services', c: 'text-purple-600' },
|
|
{ v: stats.totalSecurityTools, l: 'Security Tools', c: 'text-red-600' },
|
|
{ v: stats.totalPython, l: 'Python', c: 'text-emerald-600' },
|
|
{ v: stats.totalGo, l: 'Go', c: 'text-sky-600' },
|
|
{ v: stats.totalNode, l: 'Node.js', c: 'text-lime-600' },
|
|
{ v: stats.databases, l: 'Datenbanken', c: 'text-blue-600' },
|
|
{ v: stats.services, l: 'App Services', c: 'text-green-600' },
|
|
].map((s, i) => (
|
|
<div key={i} className="bg-white rounded-lg shadow p-4">
|
|
<div className={`text-3xl font-bold ${s.c}`}>{s.v}</div>
|
|
<div className="text-sm text-slate-500">{s.l}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Filters */}
|
|
<div className="bg-white rounded-lg shadow p-4 mb-6">
|
|
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
|
|
<div className="flex gap-2">
|
|
{categories.map((cat) => (
|
|
<button key={cat.id} onClick={() => setActiveCategory(cat.id as CategoryType)}
|
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeCategory === cat.id ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>
|
|
{cat.name} ({cat.count})
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="relative w-full md:w-64">
|
|
<input type="text" placeholder="Suchen..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" />
|
|
<svg className="absolute left-3 top-2.5 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* SBOM Metadata */}
|
|
{sbomData?.metadata && (
|
|
<div className="bg-slate-50 rounded-lg p-4 mb-6 text-sm">
|
|
<div className="flex flex-wrap gap-6">
|
|
<div><span className="text-slate-500">Format:</span><span className="ml-2 font-medium">{sbomData.bomFormat} {sbomData.specVersion}</span></div>
|
|
<div><span className="text-slate-500">Generiert:</span><span className="ml-2 font-medium">{sbomData.metadata.timestamp ? new Date(sbomData.metadata.timestamp).toLocaleString('de-DE') : '-'}</span></div>
|
|
<div><span className="text-slate-500">Anwendung:</span><span className="ml-2 font-medium">{sbomData.metadata.component?.name} v{sbomData.metadata.component?.version}</span></div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<SBOMTable components={getFilteredComponents()} loading={loading} />
|
|
|
|
{/* Export Button */}
|
|
<div className="mt-6 flex justify-end">
|
|
<button onClick={() => {
|
|
const data = JSON.stringify({ ...sbomData, infrastructure: INFRASTRUCTURE_COMPONENTS }, null, 2)
|
|
const blob = new Blob([data], { type: 'application/json' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a'); a.href = url; a.download = `breakpilot-sbom-${new Date().toISOString().split('T')[0]}.json`; a.click()
|
|
}} className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors flex items-center gap-2">
|
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
|
|
SBOM exportieren (JSON)
|
|
</button>
|
|
</div>
|
|
</AdminLayout>
|
|
)
|
|
}
|