Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
'use client';
|
||||
|
||||
import { useControls } from '@breakpilot/compliance-sdk-react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CheckCircle,
|
||||
Shield,
|
||||
FileCheck,
|
||||
AlertTriangle,
|
||||
Target,
|
||||
BarChart3,
|
||||
FileText,
|
||||
Download,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function CompliancePage() {
|
||||
const { controls, evidence, risks, isLoading } = useControls();
|
||||
|
||||
const domains = [
|
||||
{ id: 'access', name: 'Access Control', count: 5 },
|
||||
{ id: 'data', name: 'Data Protection', count: 6 },
|
||||
{ id: 'network', name: 'Network Security', count: 4 },
|
||||
{ id: 'incident', name: 'Incident Response', count: 5 },
|
||||
{ id: 'business', name: 'Business Continuity', count: 4 },
|
||||
{ id: 'vendor', name: 'Vendor Management', count: 4 },
|
||||
{ id: 'training', name: 'Security Training', count: 4 },
|
||||
{ id: 'physical', name: 'Physical Security', count: 6 },
|
||||
{ id: 'governance', name: 'Governance', count: 6 },
|
||||
];
|
||||
|
||||
const controlStats = {
|
||||
total: controls?.length ?? 44,
|
||||
implemented:
|
||||
controls?.filter((c) => c.implementationStatus === 'IMPLEMENTED').length ??
|
||||
32,
|
||||
inProgress:
|
||||
controls?.filter((c) => c.implementationStatus === 'IN_PROGRESS').length ??
|
||||
8,
|
||||
notImplemented:
|
||||
controls?.filter((c) => c.implementationStatus === 'NOT_IMPLEMENTED')
|
||||
.length ?? 4,
|
||||
};
|
||||
|
||||
const implementationRate = Math.round(
|
||||
(controlStats.implemented / controlStats.total) * 100
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-green-500/10">
|
||||
<CheckCircle className="h-6 w-6 text-green-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">Compliance Hub</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Controls, Evidence & Obligations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href="/compliance/export"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 border rounded-lg hover:bg-muted transition-colors"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Export Report
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container mx-auto px-6 py-8">
|
||||
{/* Stats Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Shield className="h-8 w-8 text-green-500" />
|
||||
<span className="text-3xl font-bold">{implementationRate}%</span>
|
||||
</div>
|
||||
<div className="text-sm font-medium">Implementation Rate</div>
|
||||
<div className="w-full bg-muted rounded-full h-2 mt-2">
|
||||
<div
|
||||
className="bg-green-500 h-2 rounded-full transition-all"
|
||||
style={{ width: `${implementationRate}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<div className="text-3xl font-bold text-green-500">
|
||||
{controlStats.implemented}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
Controls Implemented
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
of {controlStats.total} total
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<div className="text-3xl font-bold text-blue-500">
|
||||
{evidence?.length ?? 0}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">
|
||||
Evidence Items
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
Uploaded documents
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<div className="text-3xl font-bold text-yellow-500">
|
||||
{risks?.filter((r) => r.status !== 'MITIGATED').length ?? 0}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground mt-1">Open Risks</div>
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
Require attention
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<Link
|
||||
href="/compliance/controls"
|
||||
className="bg-card border rounded-xl p-6 hover:border-primary/50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 rounded-lg bg-green-500/10">
|
||||
<Shield className="h-6 w-6 text-green-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">Controls</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
44+ controls in 9 domains
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/compliance/evidence"
|
||||
className="bg-card border rounded-xl p-6 hover:border-primary/50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 rounded-lg bg-blue-500/10">
|
||||
<FileCheck className="h-6 w-6 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">Evidence</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{evidence?.length ?? 0} documents uploaded
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/compliance/risks"
|
||||
className="bg-card border rounded-xl p-6 hover:border-primary/50 transition-colors group"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="p-3 rounded-lg bg-yellow-500/10">
|
||||
<AlertTriangle className="h-6 w-6 text-yellow-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold">Risk Register</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{risks?.length ?? 0} risks tracked
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Control Domains */}
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold mb-6">Control Domains</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{domains.map((domain) => {
|
||||
const domainControls = controls?.filter(
|
||||
(c) => c.domain === domain.id
|
||||
);
|
||||
const implemented =
|
||||
domainControls?.filter(
|
||||
(c) => c.implementationStatus === 'IMPLEMENTED'
|
||||
).length ?? 0;
|
||||
const total = domain.count;
|
||||
const percent = Math.round((implemented / total) * 100);
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={domain.id}
|
||||
href={`/compliance/controls?domain=${domain.id}`}
|
||||
className="p-4 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-medium">{domain.name}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{implemented}/{total}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${
|
||||
percent === 100
|
||||
? 'bg-green-500'
|
||||
: percent >= 50
|
||||
? 'bg-blue-500'
|
||||
: 'bg-yellow-500'
|
||||
}`}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useDSGVO } from '@breakpilot/compliance-sdk-react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
ArrowLeft,
|
||||
ClipboardCheck,
|
||||
Plus,
|
||||
Search,
|
||||
Filter,
|
||||
MoreVertical,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function ConsentManagementPage() {
|
||||
const { consents, grantConsent, revokeConsent, isLoading } = useDSGVO();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterPurpose, setFilterPurpose] = useState<string>('all');
|
||||
|
||||
const purposes = [
|
||||
{ id: 'analytics', name: 'Analytics', description: 'Website usage tracking' },
|
||||
{ id: 'marketing', name: 'Marketing', description: 'Marketing communications' },
|
||||
{ id: 'functional', name: 'Functional', description: 'Enhanced features' },
|
||||
{ id: 'necessary', name: 'Necessary', description: 'Essential cookies' },
|
||||
];
|
||||
|
||||
const filteredConsents = consents?.filter((consent) => {
|
||||
const matchesSearch =
|
||||
consent.userId.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
consent.purpose.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesPurpose =
|
||||
filterPurpose === 'all' || consent.purpose === filterPurpose;
|
||||
return matchesSearch && matchesPurpose;
|
||||
});
|
||||
|
||||
const stats = {
|
||||
total: consents?.length ?? 0,
|
||||
granted: consents?.filter((c) => c.granted).length ?? 0,
|
||||
revoked: consents?.filter((c) => !c.granted).length ?? 0,
|
||||
pending: consents?.filter((c) => c.granted === null).length ?? 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/dsgvo"
|
||||
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10">
|
||||
<ClipboardCheck className="h-6 w-6 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">Einwilligungen</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Art. 6, 7 DSGVO - Consent Management
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container mx-auto px-6 py-8">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-card border rounded-lg p-4">
|
||||
<div className="text-2xl font-bold">{stats.total}</div>
|
||||
<div className="text-sm text-muted-foreground">Total</div>
|
||||
</div>
|
||||
<div className="bg-card border rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-green-500">
|
||||
{stats.granted}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Granted</div>
|
||||
</div>
|
||||
<div className="bg-card border rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-red-500">
|
||||
{stats.revoked}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Revoked</div>
|
||||
</div>
|
||||
<div className="bg-card border rounded-lg p-4">
|
||||
<div className="text-2xl font-bold text-yellow-500">
|
||||
{stats.pending}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Pending</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by user or purpose..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
<select
|
||||
value={filterPurpose}
|
||||
onChange={(e) => setFilterPurpose(e.target.value)}
|
||||
className="px-4 py-2 border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
>
|
||||
<option value="all">All Purposes</option>
|
||||
{purposes.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Consent List */}
|
||||
<div className="bg-card border rounded-xl overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-muted/50 border-b">
|
||||
<tr>
|
||||
<th className="text-left px-6 py-3 text-sm font-medium">
|
||||
User ID
|
||||
</th>
|
||||
<th className="text-left px-6 py-3 text-sm font-medium">
|
||||
Purpose
|
||||
</th>
|
||||
<th className="text-left px-6 py-3 text-sm font-medium">
|
||||
Status
|
||||
</th>
|
||||
<th className="text-left px-6 py-3 text-sm font-medium">
|
||||
Source
|
||||
</th>
|
||||
<th className="text-left px-6 py-3 text-sm font-medium">
|
||||
Created
|
||||
</th>
|
||||
<th className="text-right px-6 py-3 text-sm font-medium">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{filteredConsents?.map((consent) => (
|
||||
<tr key={consent.id} className="hover:bg-muted/30">
|
||||
<td className="px-6 py-4">
|
||||
<span className="font-mono text-sm">{consent.userId}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className="capitalize">{consent.purpose}</span>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{consent.granted ? (
|
||||
<span className="inline-flex items-center gap-1 text-green-500">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
Granted
|
||||
</span>
|
||||
) : consent.granted === false ? (
|
||||
<span className="inline-flex items-center gap-1 text-red-500">
|
||||
<XCircle className="h-4 w-4" />
|
||||
Revoked
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-yellow-500">
|
||||
<Clock className="h-4 w-4" />
|
||||
Pending
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-muted-foreground">
|
||||
{consent.source || 'Website'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-sm text-muted-foreground">
|
||||
{consent.createdAt
|
||||
? new Date(consent.createdAt).toLocaleDateString('de-DE')
|
||||
: '-'}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<button className="p-2 hover:bg-muted rounded-lg">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{(!filteredConsents || filteredConsents.length === 0) && (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={6}
|
||||
className="px-6 py-12 text-center text-muted-foreground"
|
||||
>
|
||||
No consents found
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import { useDSGVO } from '@breakpilot/compliance-sdk-react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Shield,
|
||||
Users,
|
||||
FileCheck,
|
||||
FileText,
|
||||
Lock,
|
||||
Trash2,
|
||||
ClipboardCheck,
|
||||
AlertCircle,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function DSGVOPage() {
|
||||
const { consents, dsrRequests, vvtActivities, toms, isLoading } = useDSGVO();
|
||||
|
||||
const modules = [
|
||||
{
|
||||
id: 'consent',
|
||||
title: 'Einwilligungen',
|
||||
description: 'Consent-Tracking Multi-Channel',
|
||||
icon: ClipboardCheck,
|
||||
href: '/dsgvo/consent',
|
||||
articles: 'Art. 6, 7',
|
||||
count: consents?.length ?? 0,
|
||||
color: 'bg-blue-500',
|
||||
},
|
||||
{
|
||||
id: 'dsr',
|
||||
title: 'Betroffenenrechte (DSR)',
|
||||
description: 'Auskunft, Loeschung, Berichtigung',
|
||||
icon: Users,
|
||||
href: '/dsgvo/dsr',
|
||||
articles: 'Art. 15-21',
|
||||
count: dsrRequests?.length ?? 0,
|
||||
color: 'bg-green-500',
|
||||
},
|
||||
{
|
||||
id: 'vvt',
|
||||
title: 'Verarbeitungsverzeichnis',
|
||||
description: 'Dokumentation aller Verarbeitungstaetigkeiten',
|
||||
icon: FileText,
|
||||
href: '/dsgvo/vvt',
|
||||
articles: 'Art. 30',
|
||||
count: vvtActivities?.length ?? 0,
|
||||
color: 'bg-purple-500',
|
||||
},
|
||||
{
|
||||
id: 'dsfa',
|
||||
title: 'Datenschutz-Folgenabschaetzung',
|
||||
description: 'Risk Assessment fuer Verarbeitungen',
|
||||
icon: AlertCircle,
|
||||
href: '/dsgvo/dsfa',
|
||||
articles: 'Art. 35, 36',
|
||||
count: 0,
|
||||
color: 'bg-yellow-500',
|
||||
},
|
||||
{
|
||||
id: 'tom',
|
||||
title: 'TOM',
|
||||
description: 'Technische & Organisatorische Massnahmen',
|
||||
icon: Lock,
|
||||
href: '/dsgvo/tom',
|
||||
articles: 'Art. 32',
|
||||
count: toms?.length ?? 0,
|
||||
color: 'bg-red-500',
|
||||
},
|
||||
{
|
||||
id: 'retention',
|
||||
title: 'Loeschfristen',
|
||||
description: 'Retention Policies & Automations',
|
||||
icon: Trash2,
|
||||
href: '/dsgvo/retention',
|
||||
articles: 'Art. 5, 17',
|
||||
count: 0,
|
||||
color: 'bg-orange-500',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10">
|
||||
<Shield className="h-6 w-6 text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">DSGVO Modul</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Datenschutz-Grundverordnung Compliance
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container mx-auto px-6 py-8">
|
||||
{/* Progress Overview */}
|
||||
<div className="bg-card border rounded-xl p-6 mb-8">
|
||||
<h2 className="text-lg font-medium mb-4">DSGVO Compliance Status</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-blue-500">
|
||||
{consents?.length ?? 0}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Active Consents
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-green-500">
|
||||
{dsrRequests?.filter((r) => r.status === 'PENDING').length ?? 0}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Pending DSRs</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-purple-500">
|
||||
{vvtActivities?.length ?? 0}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Processing Activities
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-3xl font-bold text-red-500">
|
||||
{toms?.filter((t) => t.implementationStatus === 'IMPLEMENTED').length ??
|
||||
0}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Implemented TOMs
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Module Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{modules.map((module) => {
|
||||
const Icon = module.icon;
|
||||
return (
|
||||
<Link
|
||||
key={module.id}
|
||||
href={module.href}
|
||||
className="group bg-card border rounded-xl p-6 hover:shadow-lg transition-all duration-200 hover:border-primary/50"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className={`p-3 rounded-lg ${module.color}/10`}>
|
||||
<Icon className={`h-6 w-6 ${module.color.replace('bg-', 'text-')}`} />
|
||||
</div>
|
||||
<span className="text-xs font-mono text-muted-foreground bg-muted px-2 py-1 rounded">
|
||||
{module.articles}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-1">{module.title}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{module.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{module.count}
|
||||
</span>{' '}
|
||||
Eintraege
|
||||
</span>
|
||||
<ArrowRight className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96.1%;
|
||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||
--muted: 210 40% 96.1%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96.1%;
|
||||
--accent-foreground: 222.2 47.4% 11.2%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 47.4% 11.2%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 224.3 76.3% 48%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { Providers } from './providers';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'BreakPilot Compliance Admin',
|
||||
description: 'Compliance Management Dashboard',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="de" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
265
breakpilot-compliance-sdk/apps/admin-dashboard/src/app/page.tsx
Normal file
265
breakpilot-compliance-sdk/apps/admin-dashboard/src/app/page.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
'use client';
|
||||
|
||||
import { useCompliance } from '@breakpilot/compliance-sdk-react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Shield,
|
||||
FileText,
|
||||
Lock,
|
||||
Search,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
ArrowRight,
|
||||
BarChart3,
|
||||
Settings,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { state, isLoading, error, progress } = useCompliance();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">Loading compliance data...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center text-destructive">
|
||||
<AlertTriangle className="h-12 w-12 mx-auto mb-4" />
|
||||
<p>Error loading compliance data</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const modules = [
|
||||
{
|
||||
id: 'dsgvo',
|
||||
title: 'DSGVO',
|
||||
description: 'Datenschutz-Grundverordnung Compliance',
|
||||
icon: Shield,
|
||||
href: '/dsgvo',
|
||||
stats: { completed: 8, total: 12 },
|
||||
color: 'text-blue-500',
|
||||
},
|
||||
{
|
||||
id: 'compliance',
|
||||
title: 'Compliance Hub',
|
||||
description: 'Controls, Evidence & Obligations',
|
||||
icon: CheckCircle,
|
||||
href: '/compliance',
|
||||
stats: { completed: 32, total: 44 },
|
||||
color: 'text-green-500',
|
||||
},
|
||||
{
|
||||
id: 'rag',
|
||||
title: 'Legal Assistant',
|
||||
description: 'AI-powered regulatory search',
|
||||
icon: Search,
|
||||
href: '/rag',
|
||||
stats: { documents: 21 },
|
||||
color: 'text-purple-500',
|
||||
},
|
||||
{
|
||||
id: 'sbom',
|
||||
title: 'SBOM',
|
||||
description: 'Software Bill of Materials',
|
||||
icon: FileText,
|
||||
href: '/sbom',
|
||||
stats: { components: 140 },
|
||||
color: 'text-orange-500',
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
title: 'Security',
|
||||
description: 'Vulnerability scanning & findings',
|
||||
icon: Lock,
|
||||
href: '/security',
|
||||
stats: { findings: 5, critical: 0 },
|
||||
color: 'text-red-500',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Shield className="h-8 w-8 text-primary" />
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">BreakPilot Compliance</h1>
|
||||
<p className="text-sm text-muted-foreground">Admin Dashboard</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/settings"
|
||||
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
||||
>
|
||||
<Settings className="h-5 w-5" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container mx-auto px-6 py-8">
|
||||
{/* Overall Progress */}
|
||||
<div className="mb-8">
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">Overall Compliance Score</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Based on all active modules
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-4xl font-bold text-primary">
|
||||
{state?.complianceScore ?? progress?.overall ?? 0}%
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full bg-muted rounded-full h-3">
|
||||
<div
|
||||
className="bg-primary h-3 rounded-full transition-all duration-500"
|
||||
style={{
|
||||
width: `${state?.complianceScore ?? progress?.overall ?? 0}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-5 gap-4 text-center text-sm">
|
||||
<div>
|
||||
<div className="text-muted-foreground">DSGVO</div>
|
||||
<div className="font-medium">{progress?.dsgvo ?? 0}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">Compliance</div>
|
||||
<div className="font-medium">{progress?.compliance ?? 0}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">Security</div>
|
||||
<div className="font-medium">{progress?.security ?? 0}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">SBOM</div>
|
||||
<div className="font-medium">{progress?.sbom ?? 0}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-muted-foreground">RAG</div>
|
||||
<div className="font-medium">{progress?.rag ?? 0}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Module Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{modules.map((module) => {
|
||||
const Icon = module.icon;
|
||||
return (
|
||||
<Link
|
||||
key={module.id}
|
||||
href={module.href}
|
||||
className="group bg-card border rounded-xl p-6 hover:shadow-lg transition-all duration-200 hover:border-primary/50"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className={`p-3 rounded-lg bg-muted ${module.color}`}>
|
||||
<Icon className="h-6 w-6" />
|
||||
</div>
|
||||
<ArrowRight className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold mb-1">{module.title}</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
{module.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 text-sm">
|
||||
{'completed' in module.stats && (
|
||||
<span className="text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{module.stats.completed}
|
||||
</span>
|
||||
/{module.stats.total} complete
|
||||
</span>
|
||||
)}
|
||||
{'documents' in module.stats && (
|
||||
<span className="text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{module.stats.documents}
|
||||
</span>{' '}
|
||||
documents
|
||||
</span>
|
||||
)}
|
||||
{'components' in module.stats && (
|
||||
<span className="text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{module.stats.components}
|
||||
</span>{' '}
|
||||
components
|
||||
</span>
|
||||
)}
|
||||
{'findings' in module.stats && (
|
||||
<span className="text-muted-foreground">
|
||||
<span className="font-medium text-foreground">
|
||||
{module.stats.findings}
|
||||
</span>{' '}
|
||||
findings
|
||||
{module.stats.critical > 0 && (
|
||||
<span className="text-destructive ml-1">
|
||||
({module.stats.critical} critical)
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="mt-8">
|
||||
<h2 className="text-lg font-medium mb-4">Quick Actions</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<Link
|
||||
href="/rag?action=ask"
|
||||
className="bg-card border rounded-lg p-4 text-center hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<Search className="h-6 w-6 mx-auto mb-2 text-purple-500" />
|
||||
<span className="text-sm font-medium">Ask Legal Question</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/security/scan"
|
||||
className="bg-card border rounded-lg p-4 text-center hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<Lock className="h-6 w-6 mx-auto mb-2 text-red-500" />
|
||||
<span className="text-sm font-medium">Run Security Scan</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/compliance/export"
|
||||
className="bg-card border rounded-lg p-4 text-center hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<BarChart3 className="h-6 w-6 mx-auto mb-2 text-green-500" />
|
||||
<span className="text-sm font-medium">Export Report</span>
|
||||
</Link>
|
||||
<Link
|
||||
href="/dsgvo/consent"
|
||||
className="bg-card border rounded-lg p-4 text-center hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<Shield className="h-6 w-6 mx-auto mb-2 text-blue-500" />
|
||||
<span className="text-sm font-medium">Manage Consents</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { ComplianceProvider } from '@breakpilot/compliance-sdk-react';
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
const apiEndpoint = process.env.NEXT_PUBLIC_API_ENDPOINT || 'http://localhost:8080/api/v1';
|
||||
const apiKey = process.env.NEXT_PUBLIC_API_KEY || '';
|
||||
const tenantId = process.env.NEXT_PUBLIC_TENANT_ID || 'default';
|
||||
|
||||
return (
|
||||
<ComplianceProvider
|
||||
apiEndpoint={apiEndpoint}
|
||||
apiKey={apiKey}
|
||||
tenantId={tenantId}
|
||||
options={{
|
||||
autoLoadState: true,
|
||||
persistState: true,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ComplianceProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRAG } from '@breakpilot/compliance-sdk-react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Search,
|
||||
MessageSquare,
|
||||
FileText,
|
||||
Upload,
|
||||
Send,
|
||||
Loader2,
|
||||
BookOpen,
|
||||
Scale,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function RAGPage() {
|
||||
const { search, ask, isSearching, isAsking, searchResults, chatHistory } =
|
||||
useRAG();
|
||||
const [query, setQuery] = useState('');
|
||||
const [mode, setMode] = useState<'search' | 'chat'>('chat');
|
||||
const [results, setResults] = useState<any[]>([]);
|
||||
const [answer, setAnswer] = useState<string>('');
|
||||
|
||||
const regulations = [
|
||||
{ id: 'dsgvo', name: 'DSGVO', chunks: 99 },
|
||||
{ id: 'ai-act', name: 'AI Act', chunks: 85 },
|
||||
{ id: 'nis2', name: 'NIS2', chunks: 46 },
|
||||
{ id: 'eprivacy', name: 'ePrivacy', chunks: 32 },
|
||||
{ id: 'tdddg', name: 'TDDDG', chunks: 28 },
|
||||
{ id: 'cra', name: 'CRA', chunks: 41 },
|
||||
];
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!query.trim()) return;
|
||||
|
||||
if (mode === 'search') {
|
||||
const res = await search(query);
|
||||
setResults(res || []);
|
||||
} else {
|
||||
const res = await ask(query);
|
||||
setAnswer(res?.answer || '');
|
||||
setResults(res?.sources || []);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background flex flex-col">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-purple-500/10">
|
||||
<Scale className="h-6 w-6 text-purple-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">Legal Assistant</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
AI-powered regulatory knowledge base
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-muted rounded-lg p-1">
|
||||
<button
|
||||
onClick={() => setMode('chat')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
mode === 'chat'
|
||||
? 'bg-background shadow text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<MessageSquare className="h-4 w-4 inline mr-2" />
|
||||
Chat
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setMode('search')}
|
||||
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
mode === 'search'
|
||||
? 'bg-background shadow text-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground'
|
||||
}`}
|
||||
>
|
||||
<Search className="h-4 w-4 inline mr-2" />
|
||||
Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex">
|
||||
{/* Sidebar - Regulations */}
|
||||
<aside className="w-64 border-r bg-card p-4">
|
||||
<h3 className="text-sm font-medium mb-4 flex items-center gap-2">
|
||||
<BookOpen className="h-4 w-4" />
|
||||
Indexed Regulations
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{regulations.map((reg) => (
|
||||
<div
|
||||
key={reg.id}
|
||||
className="flex items-center justify-between p-3 rounded-lg hover:bg-muted cursor-pointer"
|
||||
>
|
||||
<span className="text-sm font-medium">{reg.name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{reg.chunks} chunks
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-6 pt-6 border-t">
|
||||
<button className="w-full flex items-center justify-center gap-2 px-4 py-2 border rounded-lg text-sm hover:bg-muted transition-colors">
|
||||
<Upload className="h-4 w-4" />
|
||||
Upload Document
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Chat/Search Area */}
|
||||
<main className="flex-1 flex flex-col">
|
||||
{/* Results Area */}
|
||||
<div className="flex-1 overflow-auto p-6">
|
||||
{mode === 'chat' && answer && (
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="bg-card border rounded-xl p-6 mb-4">
|
||||
<div className="prose prose-sm max-w-none dark:prose-invert">
|
||||
{answer}
|
||||
</div>
|
||||
</div>
|
||||
{results.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium mb-3">Sources</h4>
|
||||
<div className="space-y-2">
|
||||
{results.map((source, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-muted/50 rounded-lg p-3 text-sm"
|
||||
>
|
||||
<div className="font-medium">{source.regulation}</div>
|
||||
<div className="text-muted-foreground text-xs mt-1">
|
||||
{source.text?.substring(0, 200)}...
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mode === 'search' && results.length > 0 && (
|
||||
<div className="max-w-3xl mx-auto space-y-4">
|
||||
{results.map((result, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-card border rounded-xl p-4 hover:border-primary/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<FileText className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="font-medium">{result.regulation}</span>
|
||||
<span className="text-xs bg-muted px-2 py-0.5 rounded">
|
||||
Score: {(result.score * 100).toFixed(0)}%
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{result.text}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!answer && results.length === 0 && (
|
||||
<div className="h-full flex items-center justify-center text-center">
|
||||
<div>
|
||||
<Scale className="h-16 w-16 text-muted-foreground/30 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium mb-2">
|
||||
Ask a Legal Question
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground max-w-md">
|
||||
Search through 21 indexed regulations including DSGVO, AI
|
||||
Act, NIS2, and more. Get AI-powered answers with source
|
||||
references.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="border-t bg-card p-4">
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={
|
||||
mode === 'chat'
|
||||
? 'Ask a question about compliance regulations...'
|
||||
: 'Search for specific terms or articles...'
|
||||
}
|
||||
rows={2}
|
||||
className="w-full px-4 py-3 pr-12 border rounded-xl bg-background resize-none focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={isSearching || isAsking || !query.trim()}
|
||||
className="absolute right-3 bottom-3 p-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSearching || isAsking ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Send className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2 text-center">
|
||||
Powered by local LLM (Qwen 2.5) with RAG over 21 legal documents
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useSecurity } from '@breakpilot/compliance-sdk-react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
ArrowLeft,
|
||||
Lock,
|
||||
AlertTriangle,
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
Play,
|
||||
Loader2,
|
||||
FileCode,
|
||||
Package,
|
||||
Database,
|
||||
Shield,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function SecurityPage() {
|
||||
const { sbom, findings, scan, isScanning, generateSbom, isGeneratingSbom } =
|
||||
useSecurity();
|
||||
const [scanTarget, setScanTarget] = useState('');
|
||||
|
||||
const handleScan = async () => {
|
||||
if (!scanTarget.trim()) return;
|
||||
await scan(scanTarget);
|
||||
setScanTarget('');
|
||||
};
|
||||
|
||||
const tools = [
|
||||
{
|
||||
id: 'gitleaks',
|
||||
name: 'Gitleaks',
|
||||
description: 'Secret Detection',
|
||||
icon: Lock,
|
||||
},
|
||||
{
|
||||
id: 'semgrep',
|
||||
name: 'Semgrep',
|
||||
description: 'SAST Analysis',
|
||||
icon: FileCode,
|
||||
},
|
||||
{
|
||||
id: 'bandit',
|
||||
name: 'Bandit',
|
||||
description: 'Python Security',
|
||||
icon: Shield,
|
||||
},
|
||||
{
|
||||
id: 'trivy',
|
||||
name: 'Trivy',
|
||||
description: 'Container Scanning',
|
||||
icon: Database,
|
||||
},
|
||||
{
|
||||
id: 'grype',
|
||||
name: 'Grype',
|
||||
description: 'Dependency Vulnerabilities',
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
id: 'syft',
|
||||
name: 'Syft',
|
||||
description: 'SBOM Generation',
|
||||
icon: FileCode,
|
||||
},
|
||||
];
|
||||
|
||||
const findingsBySeverity = {
|
||||
critical: findings?.filter((f) => f.severity === 'critical').length ?? 0,
|
||||
high: findings?.filter((f) => f.severity === 'high').length ?? 0,
|
||||
medium: findings?.filter((f) => f.severity === 'medium').length ?? 0,
|
||||
low: findings?.filter((f) => f.severity === 'low').length ?? 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Header */}
|
||||
<header className="border-b bg-card">
|
||||
<div className="container mx-auto px-6 py-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="p-2 hover:bg-muted rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded-lg bg-red-500/10">
|
||||
<Lock className="h-6 w-6 text-red-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold">Security</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Vulnerability Scanning & SBOM
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="container mx-auto px-6 py-8">
|
||||
{/* Findings Overview */}
|
||||
<div className="grid grid-cols-4 gap-4 mb-8">
|
||||
<div className="bg-card border rounded-xl p-4 border-l-4 border-l-red-500">
|
||||
<div className="text-2xl font-bold text-red-500">
|
||||
{findingsBySeverity.critical}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Critical</div>
|
||||
</div>
|
||||
<div className="bg-card border rounded-xl p-4 border-l-4 border-l-orange-500">
|
||||
<div className="text-2xl font-bold text-orange-500">
|
||||
{findingsBySeverity.high}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">High</div>
|
||||
</div>
|
||||
<div className="bg-card border rounded-xl p-4 border-l-4 border-l-yellow-500">
|
||||
<div className="text-2xl font-bold text-yellow-500">
|
||||
{findingsBySeverity.medium}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Medium</div>
|
||||
</div>
|
||||
<div className="bg-card border rounded-xl p-4 border-l-4 border-l-blue-500">
|
||||
<div className="text-2xl font-bold text-blue-500">
|
||||
{findingsBySeverity.low}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">Low</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Left Column - Scan & Tools */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Run Scan */}
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Run Security Scan</h2>
|
||||
<div className="flex gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={scanTarget}
|
||||
onChange={(e) => setScanTarget(e.target.value)}
|
||||
placeholder="Enter repository path or URL..."
|
||||
className="flex-1 px-4 py-2 border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
<button
|
||||
onClick={handleScan}
|
||||
disabled={isScanning || !scanTarget.trim()}
|
||||
className="px-6 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-2"
|
||||
>
|
||||
{isScanning ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Play className="h-4 w-4" />
|
||||
)}
|
||||
Scan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tools Grid */}
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Integrated Tools</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{tools.map((tool) => {
|
||||
const Icon = tool.icon;
|
||||
return (
|
||||
<div
|
||||
key={tool.id}
|
||||
className="p-4 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Icon className="h-5 w-5 text-muted-foreground" />
|
||||
<span className="font-medium">{tool.name}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{tool.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Findings */}
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Recent Findings</h2>
|
||||
<div className="space-y-3">
|
||||
{findings?.slice(0, 5).map((finding) => (
|
||||
<div
|
||||
key={finding.id}
|
||||
className="p-4 border rounded-lg hover:bg-muted/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{finding.severity === 'critical' && (
|
||||
<AlertCircle className="h-5 w-5 text-red-500 shrink-0" />
|
||||
)}
|
||||
{finding.severity === 'high' && (
|
||||
<AlertTriangle className="h-5 w-5 text-orange-500 shrink-0" />
|
||||
)}
|
||||
{finding.severity === 'medium' && (
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500 shrink-0" />
|
||||
)}
|
||||
{finding.severity === 'low' && (
|
||||
<CheckCircle className="h-5 w-5 text-blue-500 shrink-0" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium">{finding.title}</span>
|
||||
<span className="text-xs font-mono text-muted-foreground">
|
||||
{finding.tool}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{finding.description?.substring(0, 100)}...
|
||||
</p>
|
||||
{finding.filePath && (
|
||||
<p className="text-xs font-mono text-muted-foreground mt-2">
|
||||
{finding.filePath}
|
||||
{finding.lineNumber && `:${finding.lineNumber}`}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{(!findings || findings.length === 0) && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No findings yet. Run a scan to get started.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column - SBOM */}
|
||||
<div className="space-y-6">
|
||||
<div className="bg-card border rounded-xl p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold">SBOM</h2>
|
||||
<button
|
||||
onClick={() => generateSbom('.')}
|
||||
disabled={isGeneratingSbom}
|
||||
className="text-sm text-primary hover:underline inline-flex items-center gap-1"
|
||||
>
|
||||
{isGeneratingSbom ? (
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<Package className="h-3 w-3" />
|
||||
)}
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{sbom && (
|
||||
<div className="space-y-4">
|
||||
<div className="text-center p-4 bg-muted rounded-lg">
|
||||
<div className="text-3xl font-bold">
|
||||
{sbom.components?.length ?? 0}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Components
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-sm font-medium">By License</h3>
|
||||
<div className="space-y-1">
|
||||
{Object.entries(
|
||||
sbom.components?.reduce(
|
||||
(acc: Record<string, number>, c: any) => {
|
||||
const license = c.license || 'Unknown';
|
||||
acc[license] = (acc[license] || 0) + 1;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
) ?? {}
|
||||
)
|
||||
.slice(0, 5)
|
||||
.map(([license, count]) => (
|
||||
<div
|
||||
key={license}
|
||||
className="flex items-center justify-between text-sm"
|
||||
>
|
||||
<span>{license}</span>
|
||||
<span className="text-muted-foreground">
|
||||
{count as number}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-4 border-t space-y-2">
|
||||
<button className="w-full px-4 py-2 border rounded-lg text-sm hover:bg-muted transition-colors">
|
||||
Export CycloneDX
|
||||
</button>
|
||||
<button className="w-full px-4 py-2 border rounded-lg text-sm hover:bg-muted transition-colors">
|
||||
Export SPDX
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!sbom && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Package className="h-12 w-12 mx-auto mb-3 opacity-30" />
|
||||
<p className="text-sm">No SBOM generated yet</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user