fix: D3FEND expandable cards, System page cleanup, and multi-source improvements

- Make D3FEND defense cards clickable with expandable details and external link
- Fix D3FEND URLs to use PascalCase technique names matching the ontology
- Remove duplicate Import Atomic Red Team from System page (use Data Sources)
- Add bulk Activate All / Deactivate All buttons with confirmation modal
- Fix template admin list to show both active and inactive templates
- Add PATCH /test-templates/bulk-activate backend endpoint
- Auto-seed data sources on container startup via entrypoint.sh
- Fix SigmaHQ, CALDERA, GTFOBins import issues
- Register D3FEND sync handler in data sources router
- Add CIS Controls v8 compliance framework import
- Expand Test Catalog source filters (CALDERA, LOLBAS, GTFOBins)
- Campaign Generate from Threat Actor now opens actor selector modal
- Add coverage snapshot creation button to Comparison page
- Update README with accurate data source and feature documentation
This commit is contained in:
2026-02-10 13:22:23 +01:00
parent 8032b67fab
commit c2e9c687f4
19 changed files with 778 additions and 197 deletions

View File

@@ -11,7 +11,8 @@ import {
Filter,
Target,
} from "lucide-react";
import { listCampaigns, createCampaign, type CampaignSummary } from "../api/campaigns";
import { listCampaigns, createCampaign, generateCampaignFromThreatActor, type CampaignSummary } from "../api/campaigns";
import { getThreatActors, type ThreatActorSummary } from "../api/threat-actors";
import { useAuth } from "../context/AuthContext";
const statusColors: Record<string, string> = {
@@ -46,6 +47,8 @@ export default function CampaignsPage() {
search: "",
});
const [showCreateForm, setShowCreateForm] = useState(false);
const [showActorSelector, setShowActorSelector] = useState(false);
const [actorSearch, setActorSearch] = useState("");
const [newCampaign, setNewCampaign] = useState({
name: "",
description: "",
@@ -75,6 +78,22 @@ export default function CampaignsPage() {
},
});
// Threat actor selector data
const { data: actorsData, isLoading: isLoadingActors } = useQuery({
queryKey: ["threat-actors-for-campaign", actorSearch],
queryFn: () => getThreatActors({ search: actorSearch || undefined, limit: 50 }),
enabled: showActorSelector,
});
const generateMutation = useMutation({
mutationFn: (actorId: string) => generateCampaignFromThreatActor(actorId),
onSuccess: (campaign) => {
queryClient.invalidateQueries({ queryKey: ["campaigns"] });
setShowActorSelector(false);
navigate(`/campaigns/${campaign.id}`);
},
});
const formatDate = (dateStr: string | null) => {
if (!dateStr) return "";
return new Date(dateStr).toLocaleDateString("en-US", {
@@ -98,7 +117,7 @@ export default function CampaignsPage() {
{canCreate && (
<>
<button
onClick={() => navigate("/threat-actors")}
onClick={() => setShowActorSelector(true)}
className="flex items-center gap-1.5 rounded-lg border border-red-500/30 bg-red-500/10 px-4 py-2 text-sm font-medium text-red-400 hover:bg-red-500/20 transition-colors"
>
<Crosshair className="h-4 w-4" />
@@ -226,6 +245,93 @@ export default function CampaignsPage() {
</div>
)}
{/* Threat Actor Selector Modal */}
{showActorSelector && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="w-full max-w-lg rounded-xl border border-gray-800 bg-gray-900 p-6">
<h2 className="mb-1 text-lg font-semibold text-white">Generate Campaign from Threat Actor</h2>
<p className="mb-4 text-sm text-gray-400">
Select a threat actor to auto-generate a campaign with tests for their uncovered techniques.
</p>
{/* Search */}
<div className="relative mb-4">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-500" />
<input
value={actorSearch}
onChange={(e) => setActorSearch(e.target.value)}
placeholder="Search threat actors..."
className="w-full rounded-lg border border-gray-700 bg-gray-800 pl-10 pr-3 py-2 text-sm text-gray-200 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
/>
</div>
{/* Actor list */}
<div className="max-h-72 overflow-y-auto rounded-lg border border-gray-800">
{isLoadingActors ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-cyan-400" />
</div>
) : actorsData && actorsData.items.length > 0 ? (
actorsData.items.map((actor) => (
<button
key={actor.id}
onClick={() => generateMutation.mutate(actor.id)}
disabled={generateMutation.isPending}
className="flex w-full items-center justify-between px-4 py-3 text-left hover:bg-gray-800/50 transition-colors border-b border-gray-800/50 last:border-b-0 disabled:opacity-50"
>
<div>
<div className="flex items-center gap-2">
<Crosshair className="h-3.5 w-3.5 text-red-400" />
<span className="text-sm font-medium text-white">{actor.name}</span>
{actor.country && (
<span className="text-xs text-gray-500">{actor.country}</span>
)}
</div>
<div className="mt-0.5 flex items-center gap-3 text-xs text-gray-500">
<span>{actor.technique_count} techniques</span>
<span>Coverage: {actor.coverage_pct}%</span>
</div>
</div>
<Target className="h-4 w-4 text-gray-600" />
</button>
))
) : (
<div className="py-8 text-center text-sm text-gray-500">
No threat actors found
</div>
)}
</div>
{/* Error */}
{generateMutation.isError && (
<div className="mt-3 flex items-center gap-2 rounded-lg border border-red-500/30 bg-red-900/20 px-3 py-2 text-xs text-red-400">
<AlertCircle className="h-3.5 w-3.5" />
{(generateMutation.error as Error)?.message || "Failed to generate campaign"}
</div>
)}
{/* Loading indicator */}
{generateMutation.isPending && (
<div className="mt-3 flex items-center gap-2 text-sm text-cyan-400">
<Loader2 className="h-4 w-4 animate-spin" />
Generating campaign...
</div>
)}
{/* Cancel */}
<div className="mt-4 flex justify-end">
<button
onClick={() => { setShowActorSelector(false); setActorSearch(""); }}
disabled={generateMutation.isPending}
className="rounded-lg border border-gray-700 px-4 py-2 text-sm text-gray-400 hover:bg-gray-800 transition-colors"
>
Cancel
</button>
</div>
</div>
</div>
)}
{/* Campaign grid */}
{isLoading ? (
<div className="flex h-64 items-center justify-center">