feat(phase-16): enhanced Tests view, Red/Blue dashboard metrics, and Template admin panel (T-122, T-123, T-124)
This commit is contained in:
@@ -12,6 +12,13 @@ import {
|
||||
XCircle,
|
||||
Shield,
|
||||
Search,
|
||||
FlaskConical,
|
||||
Download,
|
||||
Plus,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
BarChart3,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
triggerMitreSync,
|
||||
@@ -20,12 +27,26 @@ import {
|
||||
type SyncMitreResponse,
|
||||
type IntelScanResponse,
|
||||
} from "../api/system";
|
||||
import {
|
||||
importAtomicTests,
|
||||
getTemplateStats,
|
||||
getAllTemplates,
|
||||
createTemplate,
|
||||
toggleTemplateActive,
|
||||
type ImportAtomicResponse,
|
||||
type TemplateStats,
|
||||
type CreateTemplatePayload,
|
||||
} from "../api/test-templates";
|
||||
import type { TestTemplate } from "../types/models";
|
||||
|
||||
export default function SystemPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const [syncResult, setSyncResult] = useState<SyncMitreResponse | null>(null);
|
||||
const [intelResult, setIntelResult] = useState<IntelScanResponse | null>(null);
|
||||
const [importResult, setImportResult] = useState<ImportAtomicResponse | null>(null);
|
||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
||||
|
||||
// ── Existing queries ─────────────────────────────────────────────
|
||||
const {
|
||||
data: schedulerStatus,
|
||||
isLoading: statusLoading,
|
||||
@@ -33,9 +54,27 @@ export default function SystemPage() {
|
||||
} = useQuery({
|
||||
queryKey: ["scheduler-status"],
|
||||
queryFn: getSchedulerStatus,
|
||||
refetchInterval: 30000, // Refresh every 30 seconds
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
|
||||
// ── Template queries ─────────────────────────────────────────────
|
||||
const {
|
||||
data: templateStats,
|
||||
isLoading: statsLoading,
|
||||
} = useQuery({
|
||||
queryKey: ["template-stats"],
|
||||
queryFn: getTemplateStats,
|
||||
});
|
||||
|
||||
const {
|
||||
data: templates,
|
||||
isLoading: templatesLoading,
|
||||
} = useQuery({
|
||||
queryKey: ["templates-admin"],
|
||||
queryFn: () => getAllTemplates({ limit: 100 }),
|
||||
});
|
||||
|
||||
// ── Mutations ────────────────────────────────────────────────────
|
||||
const mitreSyncMutation = useMutation({
|
||||
mutationFn: triggerMitreSync,
|
||||
onSuccess: (data) => {
|
||||
@@ -53,6 +92,35 @@ export default function SystemPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const importAtomicMutation = useMutation({
|
||||
mutationFn: importAtomicTests,
|
||||
onSuccess: (data) => {
|
||||
setImportResult(data);
|
||||
queryClient.invalidateQueries({ queryKey: ["template-stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["templates-admin"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["test-templates"] });
|
||||
},
|
||||
});
|
||||
|
||||
const toggleActiveMutation = useMutation({
|
||||
mutationFn: (id: string) => toggleTemplateActive(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["templates-admin"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["template-stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["test-templates"] });
|
||||
},
|
||||
});
|
||||
|
||||
const createTemplateMutation = useMutation({
|
||||
mutationFn: (payload: CreateTemplatePayload) => createTemplate(payload),
|
||||
onSuccess: () => {
|
||||
setShowCreateForm(false);
|
||||
queryClient.invalidateQueries({ queryKey: ["templates-admin"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["template-stats"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["test-templates"] });
|
||||
},
|
||||
});
|
||||
|
||||
const formatNextRun = (dateStr: string | null) => {
|
||||
if (!dateStr) return "Not scheduled";
|
||||
const date = new Date(dateStr);
|
||||
@@ -68,7 +136,7 @@ export default function SystemPage() {
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">System Administration</h1>
|
||||
<p className="mt-1 text-sm text-gray-400">
|
||||
Manage synchronization jobs and system status
|
||||
Manage synchronization jobs, templates, and system status
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +154,6 @@ export default function SystemPage() {
|
||||
Synchronize techniques from the MITRE ATT&CK framework via TAXII or GitHub fallback.
|
||||
</p>
|
||||
|
||||
{/* Status */}
|
||||
{schedulerStatus && (
|
||||
<div className="mt-4 flex items-center gap-2 text-sm">
|
||||
<Clock className="h-4 w-4 text-gray-500" />
|
||||
@@ -99,7 +166,6 @@ export default function SystemPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result */}
|
||||
{syncResult && (
|
||||
<div className="mt-4 rounded-lg border border-green-500/30 bg-green-900/20 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -158,7 +224,6 @@ export default function SystemPage() {
|
||||
Scan RSS feeds and security blogs for new threat intelligence related to techniques.
|
||||
</p>
|
||||
|
||||
{/* Status */}
|
||||
{schedulerStatus && (
|
||||
<div className="mt-4 flex items-center gap-2 text-sm">
|
||||
<Clock className="h-4 w-4 text-gray-500" />
|
||||
@@ -171,7 +236,6 @@ export default function SystemPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Result */}
|
||||
{intelResult && (
|
||||
<div className="mt-4 rounded-lg border border-green-500/30 bg-green-900/20 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -213,11 +277,278 @@ export default function SystemPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ────────────────────────────────────────────────────────────────
|
||||
TEMPLATE ADMINISTRATION (T-124)
|
||||
──────────────────────────────────────────────────────────────── */}
|
||||
|
||||
{/* Import Atomic Red Team + Stats */}
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Import Atomic Red Team */}
|
||||
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="rounded-lg bg-red-500/10 p-3">
|
||||
<Download className="h-6 w-6 text-red-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-lg font-semibold text-white">Import Atomic Red Team</h2>
|
||||
<p className="mt-1 text-sm text-gray-400">
|
||||
Import test templates from the Atomic Red Team repository by Red Canary, mapped to MITRE ATT&CK techniques.
|
||||
</p>
|
||||
|
||||
{importResult && (
|
||||
<div className="mt-4 rounded-lg border border-green-500/30 bg-green-900/20 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle className="h-4 w-4 text-green-400" />
|
||||
<span className="text-sm font-medium text-green-400">Import Complete</span>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-3 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-400">Imported:</span>
|
||||
<span className="ml-1 font-medium text-white">{importResult.imported}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Skipped:</span>
|
||||
<span className="ml-1 font-medium text-white">{importResult.skipped}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-400">Parsed:</span>
|
||||
<span className="ml-1 font-medium text-white">{importResult.total_parsed}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{importAtomicMutation.isError && (
|
||||
<div className="mt-4 rounded-lg border border-red-500/30 bg-red-900/20 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-red-400" />
|
||||
<span className="text-sm text-red-400">
|
||||
Import failed: {(importAtomicMutation.error as Error)?.message}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => importAtomicMutation.mutate()}
|
||||
disabled={importAtomicMutation.isPending}
|
||||
className="mt-4 flex items-center gap-2 rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-500 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{importAtomicMutation.isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="h-4 w-4" />
|
||||
)}
|
||||
{importAtomicMutation.isPending ? "Importing..." : "Import Now"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Template Catalog Stats */}
|
||||
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="rounded-lg bg-yellow-500/10 p-3">
|
||||
<BarChart3 className="h-6 w-6 text-yellow-400" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-lg font-semibold text-white">Catalog Statistics</h2>
|
||||
<p className="mt-1 text-sm text-gray-400">
|
||||
Overview of the test template catalog.
|
||||
</p>
|
||||
|
||||
{statsLoading ? (
|
||||
<div className="mt-4 flex items-center justify-center py-4">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-cyan-400" />
|
||||
</div>
|
||||
) : templateStats ? (
|
||||
<div className="mt-4 space-y-4">
|
||||
{/* Totals */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div className="rounded-lg border border-gray-700 bg-gray-800/50 p-3 text-center">
|
||||
<p className="text-2xl font-bold text-cyan-400">{templateStats.total}</p>
|
||||
<p className="text-xs text-gray-400">Total</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-700 bg-gray-800/50 p-3 text-center">
|
||||
<p className="text-2xl font-bold text-green-400">{templateStats.active}</p>
|
||||
<p className="text-xs text-gray-400">Active</p>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-700 bg-gray-800/50 p-3 text-center">
|
||||
<p className="text-2xl font-bold text-gray-400">{templateStats.inactive}</p>
|
||||
<p className="text-xs text-gray-400">Inactive</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* By source */}
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase text-gray-500 mb-2">By Source</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(templateStats.by_source).map(([source, count]) => (
|
||||
<span
|
||||
key={source}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-gray-700 bg-gray-800 px-2.5 py-1 text-xs text-gray-300"
|
||||
>
|
||||
{source.replace(/_/g, " ")}
|
||||
<span className="font-medium text-cyan-400">{count}</span>
|
||||
</span>
|
||||
))}
|
||||
{Object.keys(templateStats.by_source).length === 0 && (
|
||||
<span className="text-xs text-gray-500">No templates yet</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* By platform */}
|
||||
<div>
|
||||
<p className="text-xs font-medium uppercase text-gray-500 mb-2">By Platform</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{Object.entries(templateStats.by_platform).map(([platform, count]) => (
|
||||
<span
|
||||
key={platform}
|
||||
className="inline-flex items-center gap-1 rounded-full border border-gray-700 bg-gray-800 px-2.5 py-1 text-xs text-gray-300"
|
||||
>
|
||||
{platform}
|
||||
<span className="font-medium text-cyan-400">{count}</span>
|
||||
</span>
|
||||
))}
|
||||
{Object.keys(templateStats.by_platform).length === 0 && (
|
||||
<span className="text-xs text-gray-500">No templates yet</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create Custom Template Form (modal-style inline) */}
|
||||
{showCreateForm && (
|
||||
<CreateTemplateForm
|
||||
onClose={() => setShowCreateForm(false)}
|
||||
onSubmit={(payload) => createTemplateMutation.mutate(payload)}
|
||||
isPending={createTemplateMutation.isPending}
|
||||
error={createTemplateMutation.isError ? (createTemplateMutation.error as Error)?.message : null}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Templates Management Table */}
|
||||
<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 flex items-center gap-2">
|
||||
<FlaskConical className="h-5 w-5 text-cyan-400" />
|
||||
Manage Templates
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setShowCreateForm(!showCreateForm)}
|
||||
className="flex items-center gap-1.5 rounded-lg bg-cyan-600 px-3 py-2 text-sm font-medium text-white hover:bg-cyan-500 transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Create Custom Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{templatesLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-cyan-400" />
|
||||
</div>
|
||||
) : templates && templates.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800">
|
||||
<th className="pb-3 pr-4 font-medium text-gray-400">Name</th>
|
||||
<th className="pb-3 px-4 font-medium text-gray-400">Technique</th>
|
||||
<th className="pb-3 px-4 font-medium text-gray-400">Source</th>
|
||||
<th className="pb-3 px-4 font-medium text-gray-400">Platform</th>
|
||||
<th className="pb-3 px-4 font-medium text-gray-400">Status</th>
|
||||
<th className="pb-3 pl-4 font-medium text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{(templates as TestTemplate[]).map((tpl) => (
|
||||
<tr
|
||||
key={tpl.id}
|
||||
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
|
||||
>
|
||||
<td className="py-3 pr-4">
|
||||
<span className="font-medium text-gray-200 truncate block max-w-[200px]">
|
||||
{tpl.name}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="font-mono text-xs text-cyan-400">
|
||||
{tpl.mitre_technique_id}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span
|
||||
className={`inline-flex rounded-full border px-2 py-0.5 text-xs font-medium ${
|
||||
tpl.source === "atomic_red_team"
|
||||
? "bg-red-900/50 text-red-400 border-red-500/30"
|
||||
: tpl.source === "mitre"
|
||||
? "bg-blue-900/50 text-blue-400 border-blue-500/30"
|
||||
: "bg-gray-800/50 text-gray-400 border-gray-600/30"
|
||||
}`}
|
||||
>
|
||||
{tpl.source.replace(/_/g, " ")}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-gray-400 text-xs">
|
||||
{tpl.platform || "-"}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span
|
||||
className={`inline-flex rounded-full border px-2 py-0.5 text-xs font-medium ${
|
||||
tpl.is_active
|
||||
? "bg-green-900/50 text-green-400 border-green-500/30"
|
||||
: "bg-gray-800/50 text-gray-500 border-gray-600/30"
|
||||
}`}
|
||||
>
|
||||
{tpl.is_active ? "Active" : "Inactive"}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 pl-4">
|
||||
<button
|
||||
onClick={() => toggleActiveMutation.mutate(tpl.id)}
|
||||
disabled={toggleActiveMutation.isPending}
|
||||
className={`flex items-center gap-1 text-xs font-medium transition-colors ${
|
||||
tpl.is_active
|
||||
? "text-yellow-400 hover:text-yellow-300"
|
||||
: "text-green-400 hover:text-green-300"
|
||||
}`}
|
||||
title={tpl.is_active ? "Deactivate" : "Activate"}
|
||||
>
|
||||
{tpl.is_active ? (
|
||||
<>
|
||||
<ToggleRight className="h-4 w-4" />
|
||||
Deactivate
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ToggleLeft className="h-4 w-4" />
|
||||
Activate
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-8 text-center text-gray-400">
|
||||
No templates found. Import from Atomic Red Team or create a custom template.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* System Information */}
|
||||
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||
<h2 className="mb-4 text-lg font-semibold text-white">System Information</h2>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{/* Backend Status */}
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Server className="h-5 w-5 text-green-400" />
|
||||
@@ -227,8 +558,6 @@ export default function SystemPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Database Status */}
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="h-5 w-5 text-green-400" />
|
||||
@@ -238,8 +567,6 @@ export default function SystemPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MinIO Status */}
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<HardDrive className="h-5 w-5 text-green-400" />
|
||||
@@ -249,8 +576,6 @@ export default function SystemPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scheduler Status */}
|
||||
<div className="rounded-lg border border-gray-800 bg-gray-800/50 p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock
|
||||
@@ -368,3 +693,210 @@ export default function SystemPage() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── Create Template Form (inline modal) ──────────────────────────── */
|
||||
|
||||
function CreateTemplateForm({
|
||||
onClose,
|
||||
onSubmit,
|
||||
isPending,
|
||||
error,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
onSubmit: (payload: CreateTemplatePayload) => void;
|
||||
isPending: boolean;
|
||||
error: string | null;
|
||||
}) {
|
||||
const [form, setForm] = useState<CreateTemplatePayload>({
|
||||
mitre_technique_id: "",
|
||||
name: "",
|
||||
description: "",
|
||||
source: "custom",
|
||||
attack_procedure: "",
|
||||
expected_detection: "",
|
||||
platform: "",
|
||||
tool_suggested: "",
|
||||
severity: "",
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!form.mitre_technique_id || !form.name) return;
|
||||
onSubmit(form);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-cyan-500/30 bg-gray-900 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center gap-2">
|
||||
<Plus className="h-5 w-5 text-cyan-400" />
|
||||
Create Custom Template
|
||||
</h3>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{/* MITRE Technique ID */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
MITRE Technique ID *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.mitre_technique_id}
|
||||
onChange={(e) => setForm({ ...form, mitre_technique_id: e.target.value })}
|
||||
placeholder="e.g. T1059.001"
|
||||
required
|
||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Template Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
placeholder="Test template name"
|
||||
required
|
||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Platform */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Platform
|
||||
</label>
|
||||
<select
|
||||
value={form.platform || ""}
|
||||
onChange={(e) => setForm({ ...form, platform: e.target.value || undefined })}
|
||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
|
||||
>
|
||||
<option value="">Select platform...</option>
|
||||
<option value="windows">Windows</option>
|
||||
<option value="linux">Linux</option>
|
||||
<option value="macos">macOS</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Severity */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Severity
|
||||
</label>
|
||||
<select
|
||||
value={form.severity || ""}
|
||||
onChange={(e) => setForm({ ...form, severity: e.target.value || undefined })}
|
||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 focus:border-cyan-500 focus:outline-none"
|
||||
>
|
||||
<option value="">Select severity...</option>
|
||||
<option value="low">Low</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="high">High</option>
|
||||
<option value="critical">Critical</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={form.description || ""}
|
||||
onChange={(e) => setForm({ ...form, description: e.target.value })}
|
||||
placeholder="Template description..."
|
||||
rows={2}
|
||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Attack Procedure */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Attack Procedure
|
||||
</label>
|
||||
<textarea
|
||||
value={form.attack_procedure || ""}
|
||||
onChange={(e) => setForm({ ...form, attack_procedure: e.target.value })}
|
||||
placeholder="Steps for the red team to execute..."
|
||||
rows={3}
|
||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Expected Detection */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Expected Detection
|
||||
</label>
|
||||
<textarea
|
||||
value={form.expected_detection || ""}
|
||||
onChange={(e) => setForm({ ...form, expected_detection: e.target.value })}
|
||||
placeholder="What the blue team should detect..."
|
||||
rows={2}
|
||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tool Suggested */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-300 mb-1">
|
||||
Suggested Tool
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.tool_suggested || ""}
|
||||
onChange={(e) => setForm({ ...form, tool_suggested: e.target.value })}
|
||||
placeholder="e.g. PowerShell, Cobalt Strike"
|
||||
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="rounded-lg border border-red-500/30 bg-red-900/20 p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<XCircle className="h-4 w-4 text-red-400" />
|
||||
<span className="text-sm text-red-400">{error}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending || !form.mitre_technique_id || !form.name}
|
||||
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"
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Plus className="h-4 w-4" />
|
||||
)}
|
||||
{isPending ? "Creating..." : "Create Template"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="rounded-lg border border-gray-700 bg-gray-800 px-4 py-2 text-sm font-medium text-gray-300 hover:border-gray-600 hover:text-white transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user