feat(review-queue): MITRE update review queue for leads
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- New /techniques/review-queue page: lists all techniques flagged for
  review after a MITRE ATT&CK sync, grouped by tactic. Leads and admins
  can mark each one reviewed inline without leaving the page.
- Sidebar: 'Review Queue' link (admin/red_lead/blue_lead only) with an
  amber badge showing the live pending count.
- TechniqueDetailPage: amber banner when review_required=true explaining
  what happened and who can act; 'Mark as Reviewed' button now amber
  coloured for visual distinction. 'Leads only' chip shown for blue_tech.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-29 08:58:32 +02:00
parent 4881825fea
commit 20075305a5
4 changed files with 287 additions and 7 deletions

View File

@@ -0,0 +1,227 @@
import { useMemo } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import {
Loader2,
AlertCircle,
ClipboardCheck,
CheckCircle,
ExternalLink,
RefreshCw,
} from "lucide-react";
import { getTechniques, markTechniqueReviewed } from "../api/techniques";
import type { TechniqueSummary } from "../api/techniques";
import { useAuth } from "../context/AuthContext";
const STATUS_COLORS: Record<string, string> = {
validated: "bg-green-500/10 text-green-400 border-green-500/20",
partial: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20",
in_progress: "bg-blue-500/10 text-blue-400 border-blue-500/20",
not_covered: "bg-red-500/10 text-red-400 border-red-500/20",
not_evaluated: "bg-gray-500/10 text-gray-400 border-gray-600/20",
review_required:"bg-orange-500/10 text-orange-400 border-orange-500/20",
};
const STATUS_LABELS: Record<string, string> = {
validated: "Validated",
partial: "Partial",
in_progress: "In Progress",
not_covered: "Not Covered",
not_evaluated: "Not Evaluated",
};
function formatDate(dateStr: string | null | undefined) {
if (!dateStr) return "—";
const utc = dateStr.endsWith("Z") || dateStr.includes("+") ? dateStr : dateStr + "Z";
return new Date(utc).toLocaleDateString("es-ES", {
year: "numeric", month: "short", day: "numeric",
});
}
export default function ReviewQueuePage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const { user } = useAuth();
const canReview =
user?.role === "admin" ||
user?.role === "red_lead" ||
user?.role === "blue_lead";
const { data: techniques, isLoading, error, refetch } = useQuery({
queryKey: ["techniques", "review-queue"],
queryFn: () => getTechniques({ review_required: true }),
});
const reviewMutation = useMutation({
mutationFn: (mitreId: string) => markTechniqueReviewed(mitreId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["techniques", "review-queue"] });
queryClient.invalidateQueries({ queryKey: ["techniques"] });
},
});
// Group by tactic for a cleaner layout
const byTactic = useMemo(() => {
if (!techniques) return [];
const map = new Map<string, TechniqueSummary[]>();
for (const t of techniques) {
const tactic = t.tactic || "Unknown";
if (!map.has(tactic)) map.set(tactic, []);
map.get(tactic)!.push(t);
}
return Array.from(map.entries()).sort(([a], [b]) => a.localeCompare(b));
}, [techniques]);
if (isLoading) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-amber-400" />
</div>
);
}
if (error) {
return (
<div className="flex h-64 flex-col items-center justify-center gap-2">
<AlertCircle className="h-10 w-10 text-red-400" />
<p className="text-red-400">Failed to load review queue</p>
</div>
);
}
const total = techniques?.length ?? 0;
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<ClipboardCheck className="h-7 w-7 text-amber-400" />
<div>
<h1 className="text-2xl font-bold text-white">Technique Review Queue</h1>
<p className="mt-0.5 text-sm text-gray-400">
Techniques updated in MITRE ATT&CK that need to be reviewed
</p>
</div>
</div>
<div className="flex items-center gap-3">
{total > 0 && (
<span className="rounded-full border border-amber-500/30 bg-amber-500/10 px-3 py-1 text-sm font-medium text-amber-400">
{total} pending
</span>
)}
<button
onClick={() => refetch()}
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-400 hover:bg-gray-700 hover:text-white transition-colors"
>
<RefreshCw className="h-3.5 w-3.5" />
Refresh
</button>
</div>
</div>
{/* What does this mean? */}
{total > 0 && (
<div className="rounded-xl border border-amber-500/20 bg-amber-500/5 p-4">
<p className="text-sm text-amber-300">
<span className="font-semibold">What does this mean?</span>{" "}
The MITRE ATT&CK sync detected that these techniques were updated in the official
ATT&CK dataset. A lead or admin should review each one to confirm the change has
been acknowledged before marking it as reviewed.
</p>
</div>
)}
{/* Empty state */}
{total === 0 && (
<div className="rounded-xl border border-gray-800 bg-gray-900 p-16 text-center">
<CheckCircle className="mx-auto mb-3 h-12 w-12 text-green-500/50" />
<p className="text-lg font-medium text-white">All caught up!</p>
<p className="mt-1 text-sm text-gray-500">
No techniques are currently flagged for review.
</p>
</div>
)}
{/* Table grouped by tactic */}
{byTactic.map(([tactic, items]) => (
<div key={tactic} className="rounded-xl border border-gray-800 bg-gray-900">
{/* Tactic header */}
<div className="flex items-center justify-between border-b border-gray-800 px-5 py-3">
<h2 className="text-sm font-semibold capitalize text-gray-300">{tactic}</h2>
<span className="text-xs text-gray-500">{items.length}</span>
</div>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-800 text-xs uppercase tracking-wider text-gray-500">
<th className="px-5 py-3 text-left">MITRE ID</th>
<th className="px-5 py-3 text-left">Name</th>
<th className="px-5 py-3 text-left">Coverage</th>
<th className="px-5 py-3 text-left">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-800/50">
{items.map((tech) => (
<tr
key={tech.mitre_id}
className="hover:bg-gray-800/30 transition-colors"
>
<td className="px-5 py-3">
<span className="font-mono text-xs font-semibold text-cyan-400">
{tech.mitre_id}
</span>
</td>
<td className="px-5 py-3 text-gray-200 max-w-xs">
<span className="line-clamp-1">{tech.name}</span>
</td>
<td className="px-5 py-3">
<span
className={`inline-flex rounded-full border px-2 py-0.5 text-xs font-medium ${
STATUS_COLORS[tech.status_global] ?? STATUS_COLORS.not_evaluated
}`}
>
{STATUS_LABELS[tech.status_global] ?? tech.status_global}
</span>
</td>
<td className="px-5 py-3">
<div className="flex items-center gap-2">
{/* View detail */}
<button
onClick={() => navigate(`/techniques/${tech.mitre_id}`)}
className="flex items-center gap-1 rounded-lg border border-gray-700 px-2.5 py-1.5 text-xs text-gray-400 hover:border-gray-600 hover:text-white transition-colors"
title="Open technique"
>
<ExternalLink className="h-3 w-3" />
View
</button>
{/* Mark as reviewed — leads/admin only */}
{canReview && (
<button
onClick={() => reviewMutation.mutate(tech.mitre_id)}
disabled={reviewMutation.isPending && reviewMutation.variables === tech.mitre_id}
className="flex items-center gap-1 rounded-lg bg-amber-600 px-2.5 py-1.5 text-xs font-medium text-white hover:bg-amber-500 disabled:opacity-50 transition-colors"
>
{reviewMutation.isPending && reviewMutation.variables === tech.mitre_id ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<CheckCircle className="h-3 w-3" />
)}
Mark Reviewed
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</div>
);
}

View File

@@ -163,7 +163,7 @@ export default function TechniqueDetailPage() {
<button
onClick={() => reviewMutation.mutate()}
disabled={reviewMutation.isPending}
className="flex items-center gap-2 rounded-lg bg-cyan-600 px-4 py-2 text-sm font-medium text-white hover:bg-cyan-500 disabled:opacity-50 transition-colors"
className="flex items-center gap-2 rounded-lg bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-500 disabled:opacity-50 transition-colors shrink-0"
>
{reviewMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
@@ -176,6 +176,31 @@ export default function TechniqueDetailPage() {
</div>
</div>
{/* Review required banner */}
{technique.review_required && (
<div className="flex items-start gap-3 rounded-xl border border-amber-500/30 bg-amber-500/5 p-4">
<AlertTriangle className="mt-0.5 h-5 w-5 shrink-0 text-amber-400" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-amber-300">
This technique has been updated in MITRE ATT&CK
</p>
<p className="mt-0.5 text-xs text-amber-400/70">
The MITRE ATT&CK sync detected changes to this technique.
{technique.mitre_last_modified && (
<> Last modified in ATT&CK: <span className="font-mono">{technique.mitre_last_modified.slice(0, 10)}</span>.</>
)}
{" "}A lead or admin should review the changes and click{" "}
<span className="font-semibold">Mark as Reviewed</span> to acknowledge them.
</p>
</div>
{!canReview && (
<span className="shrink-0 rounded-full border border-amber-500/20 bg-amber-500/10 px-2 py-0.5 text-[10px] text-amber-400">
Leads only
</span>
)}
</div>
)}
{/* Info Section */}
<div className="grid gap-6 lg:grid-cols-3">
{/* Description */}