feat(templates,campaigns): validate MITRE technique IDs, surface template suggestions first, manager can re-approve rejected campaigns
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

- TestTemplate/TemplateSuggestion creation and updates now reject any
  mitre_technique_id that doesn't match a real, already-synced MITRE
  ATT&CK technique. Frontend swaps the free-text ID input for a
  technique picker.
- Test Catalog now shows pending template suggestions before the
  catalog grid, with full field detail (platform, severity, tool,
  atomic ID, source URL, remediation) instead of just name/procedure.
- A manager can edit and directly re-approve a campaign they previously
  rejected, instead of needing the original lead to resubmit it.
This commit is contained in:
kitos
2026-07-16 14:29:01 +02:00
parent 4809c4a662
commit 82ac9c7014
9 changed files with 327 additions and 21 deletions
@@ -1,8 +1,9 @@
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { X, Loader2 } from "lucide-react";
import { createTemplate, type CreateTemplatePayload } from "../api/test-templates";
import { proposeTemplate } from "../api/template-suggestions";
import { getTechniques, type TechniqueSummary } from "../api/techniques";
import { useToast } from "./Toast";
const SEVERITY_OPTIONS = ["low", "medium", "high", "critical"];
@@ -22,6 +23,11 @@ export default function CreateTemplateModal({
const queryClient = useQueryClient();
const { showToast } = useToast();
const { data: techniques, isLoading: techniquesLoading } = useQuery({
queryKey: ["techniques"],
queryFn: () => getTechniques(),
});
const [form, setForm] = useState<CreateTemplatePayload>({
mitre_technique_id: "",
name: "",
@@ -91,15 +97,22 @@ export default function CreateTemplateModal({
<div className="grid grid-cols-2 gap-4">
<div>
<label className="mb-1 block text-xs font-medium text-gray-500">
MITRE Technique ID <span className="text-red-400">*</span>
MITRE Technique <span className="text-red-400">*</span>
</label>
<input
<select
required
disabled={techniquesLoading}
value={form.mitre_technique_id}
onChange={(e) => set("mitre_technique_id", e.target.value)}
placeholder="T1059.001"
className="w-full rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-200 focus:border-cyan-500 focus:outline-none"
/>
>
<option value="">Select a technique</option>
{techniques?.map((tech: TechniqueSummary) => (
<option key={tech.id} value={tech.mitre_id}>
{tech.mitre_id} - {tech.name}
</option>
))}
</select>
</div>
<div>
<label className="mb-1 block text-xs font-medium text-gray-500">