feat(compliance): executive descriptions and mapping rationale for all 5 frameworks
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,51 +184,73 @@ 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">
|
||||||
{control.techniques.length === 0 ? (
|
<div className="space-y-4">
|
||||||
<p className="text-xs text-gray-500 italic">No techniques mapped to this control.</p>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<p className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
|
||||||
Mapped Techniques ({control.techniques.length})
|
|
||||||
</p>
|
|
||||||
<div className="grid gap-1.5 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
{control.techniques.map((tech) => {
|
|
||||||
const techStyle =
|
|
||||||
tech.score >= 70 ? "border-green-500/20 bg-green-500/5 text-green-400"
|
|
||||||
: tech.score >= 30 ? "border-yellow-500/20 bg-yellow-500/5 text-yellow-400"
|
|
||||||
: tech.score > 0 ? "border-red-500/20 bg-red-500/5 text-red-400"
|
|
||||||
: "border-gray-700 bg-gray-800/30 text-gray-500";
|
|
||||||
|
|
||||||
return (
|
{/* Executive description */}
|
||||||
<div
|
{control.description && (
|
||||||
key={tech.mitre_id}
|
<div className="flex gap-3 rounded-xl border border-blue-500/15 bg-blue-500/5 p-4">
|
||||||
className={`flex items-center justify-between rounded-lg border px-3 py-2 cursor-pointer hover:brightness-125 transition-all ${techStyle}`}
|
<Info className="mt-0.5 h-4 w-4 shrink-0 text-blue-400" />
|
||||||
onClick={(e) => {
|
<div>
|
||||||
e.stopPropagation();
|
<p className="mb-1 text-[10px] font-semibold uppercase tracking-wider text-blue-400">
|
||||||
navigate(`/techniques/${tech.mitre_id}`);
|
What this control requires — and why it matters
|
||||||
}}
|
</p>
|
||||||
>
|
<p className="text-xs leading-relaxed text-gray-300">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
{control.description}
|
||||||
<span className="shrink-0 font-mono text-xs font-semibold text-cyan-400">
|
</p>
|
||||||
{tech.mitre_id}
|
</div>
|
||||||
</span>
|
|
||||||
<span className="truncate text-xs text-gray-300">
|
|
||||||
{tech.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex shrink-0 items-center gap-2 pl-2">
|
|
||||||
<span className="text-[10px] text-gray-500 capitalize hidden sm:block">
|
|
||||||
{tech.status.replace(/_/g, " ")}
|
|
||||||
</span>
|
|
||||||
<ExternalLink className="h-3 w-3 text-gray-600" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
{/* Techniques grid */}
|
||||||
|
{control.techniques.length === 0 ? (
|
||||||
|
<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>
|
||||||
|
<p className="mb-2 text-[10px] font-semibold uppercase tracking-wider text-gray-500">
|
||||||
|
ATT&CK techniques covered ({control.techniques.length}) — sorted by coverage score
|
||||||
|
</p>
|
||||||
|
<div className="grid gap-1.5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{control.techniques.map((tech) => {
|
||||||
|
const techStyle =
|
||||||
|
tech.score >= 70 ? "border-green-500/20 bg-green-500/5 text-green-400"
|
||||||
|
: tech.score >= 30 ? "border-yellow-500/20 bg-yellow-500/5 text-yellow-400"
|
||||||
|
: tech.score > 0 ? "border-red-500/20 bg-red-500/5 text-red-400"
|
||||||
|
: "border-gray-700 bg-gray-800/30 text-gray-500";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={tech.mitre_id}
|
||||||
|
className={`flex items-center justify-between rounded-lg border px-3 py-2 cursor-pointer hover:brightness-125 transition-all ${techStyle}`}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigate(`/techniques/${tech.mitre_id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
<span className="shrink-0 font-mono text-xs font-semibold text-cyan-400">
|
||||||
|
{tech.mitre_id}
|
||||||
|
</span>
|
||||||
|
<span className="truncate text-xs text-gray-300">
|
||||||
|
{tech.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 items-center gap-2 pl-2">
|
||||||
|
<span className="text-[10px] font-medium tabular-nums">
|
||||||
|
{tech.score.toFixed(0)}
|
||||||
|
</span>
|
||||||
|
<ExternalLink className="h-3 w-3 text-gray-600" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user