diff --git a/frontend/src/pages/TestDetailPage.tsx b/frontend/src/pages/TestDetailPage.tsx index 8582b39..b5895ab 100644 --- a/frontend/src/pages/TestDetailPage.tsx +++ b/frontend/src/pages/TestDetailPage.tsx @@ -2,7 +2,7 @@ import { useParams, useNavigate } from "react-router-dom"; import MarkdownText from "../components/MarkdownText"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { useState, useEffect, useCallback } from "react"; -import { Loader2, AlertCircle, ArrowLeft } from "lucide-react"; +import { Loader2, AlertCircle, ArrowLeft, BookOpen, X, CheckCircle } from "lucide-react"; import { getTestById, @@ -30,6 +30,7 @@ import ValidationModal from "../components/test-detail/ValidationModal"; import ConfirmDialog from "../components/ConfirmDialog"; import JiraLinkPanel from "../components/JiraLinkPanel"; import TestPhaseTimeline from "../components/TestPhaseTimeline"; +import { createTemplate } from "../api/test-templates"; // ── Page Component ───────────────────────────────────────────────── @@ -361,6 +362,12 @@ export default function TestDetailPage() { test.state === "blue_evaluating" && (role === "blue_tech" || role === "blue_lead" || role === "admin"); + // Only leads and admins can create templates + const canSaveAsTemplate = + role === "red_lead" || role === "blue_lead" || role === "admin"; + + const [showTemplateModal, setShowTemplateModal] = useState(false); + // ── Render ───────────────────────────────────────────────────── return ( @@ -507,6 +514,18 @@ export default function TestDetailPage() { )} + + {canSaveAsTemplate && ( +
+ +
+ )} {/* Retest Chain */} @@ -595,6 +614,18 @@ export default function TestDetailPage() { /> )} + {/* Save as Template Modal */} + {showTemplateModal && ( + setShowTemplateModal(false)} + onSaved={() => { + setShowTemplateModal(false); + showToast("Template created successfully", "success"); + }} + /> + )} + {/* Toast notification */} {toast && (
); } + +/* ── SaveAsTemplateModal ─────────────────────────────────────────────── + Pre-fills template fields from an existing test. + Lets the user review / tweak before saving. + ────────────────────────────────────────────────────────────────── */ +function SaveAsTemplateModal({ + test, + onClose, + onSaved, +}: { + test: { name: string; description: string | null; platform: string | null; procedure_text: string | null; tool_used: string | null; technique_mitre_id: string | null; technique_name?: string | null }; + onClose: () => void; + onSaved: () => void; +}) { + const [name, setName] = useState(test.name); + const [description, setDescription] = useState(test.description ?? ""); + const [platform, setPlatform] = useState(test.platform ?? ""); + const [attackProcedure, setAttackProcedure] = useState(test.procedure_text ?? ""); + const [toolSuggested, setToolSuggested] = useState(test.tool_used ?? ""); + const [expectedDetection, setExpectedDetection] = useState(""); + const [severity, setSeverity] = useState("medium"); + + const saveMutation = useMutation({ + mutationFn: () => + createTemplate({ + mitre_technique_id: test.technique_mitre_id ?? "", + name: name.trim(), + description: description.trim() || undefined, + platform: platform.trim() || undefined, + attack_procedure: attackProcedure.trim() || undefined, + tool_suggested: toolSuggested.trim() || undefined, + expected_detection: expectedDetection.trim() || undefined, + severity: severity || undefined, + source: "custom", + }), + onSuccess: () => onSaved(), + }); + + const canSubmit = name.trim().length > 0 && !!test.technique_mitre_id; + + return ( +
+
+ {/* Header */} +
+
+ +

Save as Template

+
+ +
+ + {/* Body */} +
+ {/* Technique info banner */} +
+ Technique:{" "} + {test.technique_mitre_id} + {test.technique_name && ( + {test.technique_name} + )} +
+ + {/* Name */} +
+ + setName(e.target.value)} + className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-cyan-500 focus:outline-none" + placeholder="Template name" + /> +
+ + {/* Platform + Severity row */} +
+
+ + setPlatform(e.target.value)} + className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-cyan-500 focus:outline-none" + placeholder="windows, linux, macos…" + /> +
+
+ + +
+
+ + {/* Description */} +
+ +