feat(phase-29): add compliance framework mapping, reports and UI (T-227 to T-229)

This commit is contained in:
2026-02-09 18:41:24 +01:00
parent 12f33307fd
commit 2ac8e7f4a5
12 changed files with 1516 additions and 0 deletions

View File

@@ -0,0 +1,189 @@
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Loader2, AlertCircle, Download, FileText } from "lucide-react";
import {
getComplianceFrameworks,
getFrameworkStatus,
downloadComplianceCSV,
type ComplianceFrameworkSummary,
} from "../api/compliance";
import ComplianceGauge from "../components/compliance/ComplianceGauge";
import ControlsTable from "../components/compliance/ControlsTable";
export default function CompliancePage() {
const [selectedFrameworkId, setSelectedFrameworkId] = useState<string | null>(null);
// Fetch available frameworks
const {
data: frameworks,
isLoading: loadingFrameworks,
} = useQuery({
queryKey: ["compliance-frameworks"],
queryFn: getComplianceFrameworks,
});
// Auto-select first framework
const activeFrameworkId = selectedFrameworkId || frameworks?.[0]?.id || null;
// Fetch framework status
const {
data: frameworkStatus,
isLoading: loadingStatus,
} = useQuery({
queryKey: ["compliance-status", activeFrameworkId],
queryFn: () => getFrameworkStatus(activeFrameworkId!),
enabled: !!activeFrameworkId,
});
const isLoading = loadingFrameworks || loadingStatus;
const summary = frameworkStatus?.summary;
const controls = frameworkStatus?.controls || [];
const handleExportCSV = async () => {
if (activeFrameworkId) {
await downloadComplianceCSV(activeFrameworkId);
}
};
const handleExportJSON = async () => {
if (!frameworkStatus) return;
const json = JSON.stringify(frameworkStatus, null, 2);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `compliance_${frameworkStatus.framework.name.replace(/\s+/g, "_")}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
if (isLoading && !frameworkStatus) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-cyan-400" />
</div>
);
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-white">Compliance</h1>
<p className="mt-1 text-sm text-gray-400">
Map ATT&CK coverage to compliance framework controls
</p>
</div>
<div className="flex items-center gap-2">
{/* Framework selector */}
<select
value={activeFrameworkId || ""}
onChange={(e) => setSelectedFrameworkId(e.target.value)}
className="rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-cyan-500 focus:outline-none"
>
{(frameworks || []).map((fw) => (
<option key={fw.id} value={fw.id}>
{fw.name}
</option>
))}
</select>
{/* Export buttons */}
<button
onClick={handleExportCSV}
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-xs font-medium text-gray-300 hover:border-cyan-500/50 hover:text-white"
>
<Download className="h-3.5 w-3.5" />
Export CSV
</button>
<button
onClick={handleExportJSON}
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-xs font-medium text-gray-300 hover:border-cyan-500/50 hover:text-white"
>
<FileText className="h-3.5 w-3.5" />
Export JSON
</button>
</div>
</div>
{/* Summary cards */}
{summary && (
<div className="grid grid-cols-2 gap-4 lg:grid-cols-5">
{/* Gauge */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4 flex flex-col items-center justify-center">
<ComplianceGauge percentage={summary.compliance_percentage} size="md" />
<p className="mt-2 text-xs text-gray-500">Overall Compliance</p>
</div>
{/* Covered */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<p className="text-xs text-gray-500 uppercase tracking-wider">Covered</p>
<p className="mt-2 text-3xl font-bold text-green-400">{summary.covered}</p>
<div className="mt-2 h-1.5 w-full rounded-full bg-gray-800">
<div
className="h-full rounded-full bg-green-500"
style={{ width: `${summary.total_controls > 0 ? (summary.covered / summary.total_controls) * 100 : 0}%` }}
/>
</div>
</div>
{/* Partial */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<p className="text-xs text-gray-500 uppercase tracking-wider">Partial</p>
<p className="mt-2 text-3xl font-bold text-yellow-400">
{summary.partially_covered}
</p>
<div className="mt-2 h-1.5 w-full rounded-full bg-gray-800">
<div
className="h-full rounded-full bg-yellow-500"
style={{ width: `${summary.total_controls > 0 ? (summary.partially_covered / summary.total_controls) * 100 : 0}%` }}
/>
</div>
</div>
{/* Not Covered */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<p className="text-xs text-gray-500 uppercase tracking-wider">Not Covered</p>
<p className="mt-2 text-3xl font-bold text-red-400">{summary.not_covered}</p>
<div className="mt-2 h-1.5 w-full rounded-full bg-gray-800">
<div
className="h-full rounded-full bg-red-500"
style={{ width: `${summary.total_controls > 0 ? (summary.not_covered / summary.total_controls) * 100 : 0}%` }}
/>
</div>
</div>
{/* Not Evaluated */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<p className="text-xs text-gray-500 uppercase tracking-wider">Not Evaluated</p>
<p className="mt-2 text-3xl font-bold text-gray-400">{summary.not_evaluated}</p>
<div className="mt-2 h-1.5 w-full rounded-full bg-gray-800">
<div
className="h-full rounded-full bg-gray-500"
style={{ width: `${summary.total_controls > 0 ? (summary.not_evaluated / summary.total_controls) * 100 : 0}%` }}
/>
</div>
</div>
</div>
)}
{/* Controls table */}
{controls.length > 0 ? (
<ControlsTable controls={controls} />
) : (
!isLoading && (
<div className="rounded-xl border border-gray-800 bg-gray-900 p-8 text-center">
<AlertCircle className="mx-auto h-10 w-10 text-gray-600" />
<p className="mt-3 text-gray-400">
No compliance data available. Import a compliance framework from the System page.
</p>
</div>
)
)}
</div>
);
}