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

@@ -1,5 +1,6 @@
import { NavLink } from "react-router-dom";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import {
LayoutDashboard,
FlaskConical,
@@ -19,8 +20,10 @@ import {
ShieldCheck,
GitCompareArrows,
ScrollText,
ClipboardCheck,
} from "lucide-react";
import { useAuth } from "../context/AuthContext";
import { getTechniques } from "../api/techniques";
interface NavItem {
to: string;
@@ -35,6 +38,7 @@ const mainLinks: NavItem[] = [
{ to: "/dashboard", label: "Dashboard", icon: LayoutDashboard },
{ to: "/executive-dashboard", label: "Executive Dashboard", icon: Gauge, roles: ["admin", "red_lead", "blue_lead", "viewer"] },
{ to: "/matrix", label: "ATT&CK Matrix", icon: Grid3X3 },
{ to: "/techniques/review-queue", label: "Review Queue", icon: ClipboardCheck, roles: ["admin", "red_lead", "blue_lead"] },
{
to: "/tests",
label: "Tests",
@@ -60,7 +64,7 @@ const systemLinks: NavItem[] = [
{ to: "/audit", label: "Audit Log", icon: FileText },
];
function SidebarLink({ item }: { item: NavItem }) {
function SidebarLink({ item, badge }: { item: NavItem; badge?: number }) {
const [expanded, setExpanded] = useState(false);
if (item.children) {
@@ -104,6 +108,7 @@ function SidebarLink({ item }: { item: NavItem }) {
return (
<NavLink
to={item.to}
end
className={({ isActive }) =>
`flex items-center gap-3 rounded-lg px-3 py-2.5 text-sm font-medium transition-colors ${
isActive
@@ -112,8 +117,13 @@ function SidebarLink({ item }: { item: NavItem }) {
}`
}
>
<item.icon className="h-5 w-5" />
{item.label}
<item.icon className="h-5 w-5 shrink-0" />
<span className="flex-1">{item.label}</span>
{badge !== undefined && badge > 0 && (
<span className="rounded-full bg-amber-500 px-1.5 py-0.5 text-[10px] font-bold text-white leading-none">
{badge}
</span>
)}
</NavLink>
);
}
@@ -123,10 +133,22 @@ export default function Sidebar() {
const role = user?.role ?? "";
const isAdmin = role === "admin";
const canSeeReviewQueue =
isAdmin || role === "red_lead" || role === "blue_lead";
// Fetch review queue count for the badge (only for roles that can see it)
const { data: reviewQueue } = useQuery({
queryKey: ["techniques", "review-queue"],
queryFn: () => getTechniques({ review_required: true }),
enabled: canSeeReviewQueue,
staleTime: 5 * 60 * 1000, // 5 min — don't hammer the API
});
const reviewCount = reviewQueue?.length ?? 0;
/** Returns true when the current user is allowed to see `item`. */
const canSee = (item: NavItem) => {
if (!item.roles) return true; // no restriction
if (isAdmin) return true; // admin sees everything
if (!item.roles) return true;
if (isAdmin) return true;
return item.roles.includes(role);
};
@@ -143,7 +165,11 @@ export default function Sidebar() {
{/* Main nav */}
<nav className="flex-1 space-y-1 overflow-y-auto px-3 py-4">
{mainLinks.filter(canSee).map((item) => (
<SidebarLink key={item.to + item.label} item={item} />
<SidebarLink
key={item.to + item.label}
item={item}
badge={item.to === "/techniques/review-queue" ? reviewCount : undefined}
/>
))}
{/* System / Administration section — admin only */}