feat(compliance): executive descriptions and mapping rationale for all 5 frameworks
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Backend: expose description in control status response, add rich business-language
descriptions to all curated controls (ISO 27001, ISO 42001, CIS v8, DORA) explaining
requirements and ATT&CK mapping rationale. ISO 42001 includes infrastructure-mapping note.

Frontend: description field in type, info panel in ControlsTable expanded rows,
framework info banner with description and official standard link in CompliancePage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-06-03 16:28:16 +02:00
parent 0b82d96bcc
commit 1dcff4ad20
5 changed files with 1068 additions and 99 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -62,6 +62,7 @@ def _get_control_status(control: ComplianceControl, db: Session) -> dict[str, An
return { return {
"control_id": control.control_id, "control_id": control.control_id,
"title": control.title, "title": control.title,
"description": control.description,
"category": control.category, "category": control.category,
"status": "not_evaluated", "status": "not_evaluated",
"score": 0, "score": 0,
@@ -104,6 +105,7 @@ def _get_control_status(control: ComplianceControl, db: Session) -> dict[str, An
return { return {
"control_id": control.control_id, "control_id": control.control_id,
"title": control.title, "title": control.title,
"description": control.description,
"category": control.category, "category": control.category,
"status": status, "status": status,
"score": avg_score, "score": avg_score,

View File

@@ -22,6 +22,7 @@ export interface ComplianceTechniqueInfo {
export interface ComplianceControlStatus { export interface ComplianceControlStatus {
control_id: string; control_id: string;
title: string; title: string;
description: string | null;
category: string | null; category: string | null;
status: "covered" | "partially_covered" | "not_covered" | "not_evaluated"; status: "covered" | "partially_covered" | "not_covered" | "not_evaluated";
score: number; score: number;

View File

@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ChevronDown, ChevronRight, Search, Filter, ExternalLink } from "lucide-react"; import { ChevronDown, ChevronRight, Search, Filter, ExternalLink, Info, ShieldAlert } from "lucide-react";
import type { ComplianceControlStatus } from "../../api/compliance"; import type { ComplianceControlStatus } from "../../api/compliance";
interface ControlsTableProps { interface ControlsTableProps {
@@ -184,13 +184,34 @@ export default function ControlsTable({ controls }: ControlsTableProps) {
{/* Expanded detail row */} {/* Expanded detail row */}
{isExpanded && ( {isExpanded && (
<tr key={`${control.control_id}-expanded`} className="bg-gray-800/10"> <tr key={`${control.control_id}-expanded`} className="bg-gray-800/10">
<td colSpan={7} className="px-6 pb-4 pt-2"> <td colSpan={7} className="px-6 pb-5 pt-3">
<div className="space-y-4">
{/* Executive description */}
{control.description && (
<div className="flex gap-3 rounded-xl border border-blue-500/15 bg-blue-500/5 p-4">
<Info className="mt-0.5 h-4 w-4 shrink-0 text-blue-400" />
<div>
<p className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-blue-400">
What this control requires and why it matters
</p>
<p className="text-xs leading-relaxed text-gray-300">
{control.description}
</p>
</div>
</div>
)}
{/* Techniques grid */}
{control.techniques.length === 0 ? ( {control.techniques.length === 0 ? (
<p className="text-xs text-gray-500 italic">No techniques mapped to this control.</p> <div className="flex items-center gap-2 text-xs text-gray-500 italic">
<ShieldAlert className="h-4 w-4" />
No ATT&CK techniques mapped to this control yet.
</div>
) : ( ) : (
<div> <div>
<p className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500"> <p className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
Mapped Techniques ({control.techniques.length}) ATT&CK techniques covered ({control.techniques.length}) sorted by coverage score
</p> </p>
<div className="grid gap-1.5 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid gap-1.5 sm:grid-cols-2 lg:grid-cols-3">
{control.techniques.map((tech) => { {control.techniques.map((tech) => {
@@ -218,8 +239,8 @@ export default function ControlsTable({ controls }: ControlsTableProps) {
</span> </span>
</div> </div>
<div className="flex shrink-0 items-center gap-2 pl-2"> <div className="flex shrink-0 items-center gap-2 pl-2">
<span className="text-[10px] text-gray-500 capitalize hidden sm:block"> <span className="text-[10px] font-medium tabular-nums">
{tech.status.replace(/_/g, " ")} {tech.score.toFixed(0)}
</span> </span>
<ExternalLink className="h-3 w-3 text-gray-600" /> <ExternalLink className="h-3 w-3 text-gray-600" />
</div> </div>
@@ -229,6 +250,7 @@ export default function ControlsTable({ controls }: ControlsTableProps) {
</div> </div>
</div> </div>
)} )}
</div>
</td> </td>
</tr> </tr>
)} )}

View File

@@ -1,6 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Loader2, AlertCircle, Download, FileText, Plus } from "lucide-react"; import { Loader2, AlertCircle, Download, FileText, Plus, ExternalLink, BookOpen } from "lucide-react";
import { import {
getComplianceFrameworks, getComplianceFrameworks,
getFrameworkStatus, getFrameworkStatus,
@@ -47,6 +47,7 @@ export default function CompliancePage() {
const isLoading = loadingFrameworks || loadingStatus; const isLoading = loadingFrameworks || loadingStatus;
const summary = frameworkStatus?.summary; const summary = frameworkStatus?.summary;
const controls = frameworkStatus?.controls || []; const controls = frameworkStatus?.controls || [];
const activeFramework = frameworks?.find((f) => f.id === activeFrameworkId) ?? null;
const handleExportCSV = async () => { const handleExportCSV = async () => {
if (activeFrameworkId) { if (activeFrameworkId) {
@@ -166,6 +167,36 @@ export default function CompliancePage() {
</div> </div>
</div> </div>
{/* Framework info banner */}
{activeFramework?.description && (
<div className="flex items-start gap-3 rounded-xl border border-gray-700/60 bg-gray-900/60 px-5 py-4">
<BookOpen className="mt-0.5 h-4 w-4 shrink-0 text-cyan-400" />
<div className="flex-1 min-w-0">
<div className="flex flex-wrap items-center gap-2 mb-1">
<span className="text-sm font-semibold text-white">{activeFramework.name}</span>
{activeFramework.version && (
<span className="rounded-full border border-gray-700 bg-gray-800 px-2 py-0.5 text-[10px] text-gray-400">
v{activeFramework.version}
</span>
)}
{activeFramework.url && (
<a
href={activeFramework.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-[10px] text-cyan-400 hover:text-cyan-300 transition-colors"
onClick={(e) => e.stopPropagation()}
>
<ExternalLink className="h-3 w-3" />
Official standard
</a>
)}
</div>
<p className="text-xs leading-relaxed text-gray-400">{activeFramework.description}</p>
</div>
</div>
)}
{/* Summary cards */} {/* Summary cards */}
{summary && ( {summary && (
<div className="grid grid-cols-2 gap-4 lg:grid-cols-5"> <div className="grid grid-cols-2 gap-4 lg:grid-cols-5">