feat(phase-15): add Test Catalog page, template instantiation, and auto-migration entrypoint (T-119, T-120, T-121)
T-119: TestCatalogPage with search, filters (source/platform/severity), template cards grid, and pagination T-120: TestFromTemplateForm modal with pre-filled fields from template, required field validation, and redirect on creation T-121: Integrate Available Test Templates section in TechniqueDetailPage with Run This Test buttons; fix missing testStateBadgeColors for new states Also: add backend entrypoint.sh for automatic Alembic migrations + seed on container startup, add curl to Dockerfile for healthcheck
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
@@ -13,9 +14,13 @@ import {
|
||||
Check,
|
||||
X,
|
||||
AlertTriangle,
|
||||
BookOpen,
|
||||
FlaskConical,
|
||||
} from "lucide-react";
|
||||
import { getTechniqueByMitreId, markTechniqueReviewed } from "../api/techniques";
|
||||
import { getTemplatesByTechnique } from "../api/test-templates";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import TestFromTemplateForm from "../components/TestFromTemplateForm";
|
||||
import type { TechniqueStatus, TestState, TestResult } from "../types/models";
|
||||
|
||||
const statusBadgeColors: Record<TechniqueStatus, string> = {
|
||||
@@ -29,6 +34,8 @@ const statusBadgeColors: Record<TechniqueStatus, string> = {
|
||||
|
||||
const testStateBadgeColors: Record<TestState, string> = {
|
||||
draft: "bg-gray-800/50 text-gray-400 border-gray-600/30",
|
||||
red_executing: "bg-orange-900/50 text-orange-400 border-orange-500/30",
|
||||
blue_evaluating: "bg-indigo-900/50 text-indigo-400 border-indigo-500/30",
|
||||
in_review: "bg-blue-900/50 text-blue-400 border-blue-500/30",
|
||||
validated: "bg-green-900/50 text-green-400 border-green-500/30",
|
||||
rejected: "bg-red-900/50 text-red-400 border-red-500/30",
|
||||
@@ -46,6 +53,8 @@ export default function TechniqueDetailPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useAuth();
|
||||
|
||||
const [templateFormId, setTemplateFormId] = useState<string | null>(null);
|
||||
|
||||
const canReview =
|
||||
user?.role === "admin" || user?.role === "red_lead" || user?.role === "blue_lead";
|
||||
|
||||
@@ -59,6 +68,12 @@ export default function TechniqueDetailPage() {
|
||||
enabled: !!mitreId,
|
||||
});
|
||||
|
||||
const { data: templates = [], isLoading: isTemplatesLoading } = useQuery({
|
||||
queryKey: ["templates-for-technique", mitreId],
|
||||
queryFn: () => getTemplatesByTechnique(mitreId!),
|
||||
enabled: !!mitreId,
|
||||
});
|
||||
|
||||
const reviewMutation = useMutation({
|
||||
mutationFn: () => markTechniqueReviewed(mitreId!),
|
||||
onSuccess: () => {
|
||||
@@ -327,6 +342,72 @@ export default function TechniqueDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Available Test Templates Section */}
|
||||
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-white">Available Test Templates</h2>
|
||||
<button
|
||||
onClick={() => navigate("/test-catalog")}
|
||||
className="flex items-center gap-1.5 text-sm text-gray-400 hover:text-cyan-400 transition-colors"
|
||||
>
|
||||
<BookOpen className="h-4 w-4" />
|
||||
Browse Full Catalog
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isTemplatesLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-gray-500" />
|
||||
</div>
|
||||
) : templates.length > 0 ? (
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{templates.map((tpl) => (
|
||||
<div
|
||||
key={tpl.id}
|
||||
className="flex items-center justify-between rounded-lg border border-gray-800 bg-gray-800/30 p-4 transition-colors hover:border-gray-700"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm font-medium text-gray-200">{tpl.name}</p>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
<span className="inline-flex rounded-full border border-gray-600/30 bg-gray-800/50 px-2 py-0.5 text-[10px] text-gray-400">
|
||||
{tpl.source === "atomic_red_team" ? "Atomic" : tpl.source}
|
||||
</span>
|
||||
{tpl.platform && (
|
||||
<span className="inline-flex rounded-full border border-gray-600/30 bg-gray-800/50 px-2 py-0.5 text-[10px] capitalize text-gray-400">
|
||||
{tpl.platform}
|
||||
</span>
|
||||
)}
|
||||
{tpl.severity && (
|
||||
<span className="inline-flex rounded-full border border-gray-600/30 bg-gray-800/50 px-2 py-0.5 text-[10px] capitalize text-gray-400">
|
||||
{tpl.severity}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setTemplateFormId(tpl.id)}
|
||||
className="ml-3 flex shrink-0 items-center gap-1 rounded-lg border border-cyan-500/30 bg-cyan-500/10 px-3 py-1.5 text-xs font-medium text-cyan-400 hover:bg-cyan-500/20 transition-colors"
|
||||
>
|
||||
<FlaskConical className="h-3.5 w-3.5" />
|
||||
Run This Test
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-8 text-gray-400">
|
||||
<BookOpen className="mb-2 h-8 w-8 text-gray-600" />
|
||||
<p className="text-sm">No test templates available for this technique.</p>
|
||||
<button
|
||||
onClick={() => navigate("/test-catalog")}
|
||||
className="mt-3 flex items-center gap-1 text-sm text-cyan-400 hover:underline"
|
||||
>
|
||||
Browse the full test catalog
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Intel Items Section */}
|
||||
{technique.intel_items && technique.intel_items.length > 0 && (
|
||||
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||
@@ -358,6 +439,15 @@ export default function TechniqueDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Template instantiation modal */}
|
||||
{templateFormId && technique && (
|
||||
<TestFromTemplateForm
|
||||
templateId={templateFormId}
|
||||
techniqueId={technique.id}
|
||||
onClose={() => setTemplateFormId(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user