feat(evaluations): ATT&CK Evaluations importer for CrowdStrike Falcon [FASE-6.1]
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- Migration b048: evaluation_imports table (adversary, round, status, tests_created)
- EvaluationImport SQLAlchemy model
- attck_evaluations_service: fetch rounds from evals.mitre.org API, import per-technique
  detection results (Technique/Tactic/Telemetry -> detected/partially/not_detected)
- All imported tests land in in_review state with lab-environment disclaimer
- Idempotency guard prevents duplicate round imports
- 4 new endpoints: list rounds, import specific, import latest, check-new
- Weekly APScheduler cron (Mon 06:00) auto-checks and imports new rounds
- SystemPage UI: rounds table, import buttons, check-new, result feedback
- Disclaimer callout reminding admins these are lab results not org coverage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-06-05 15:57:03 +02:00
parent cfc48ccd2b
commit e3e79be35a
7 changed files with 1067 additions and 1 deletions

View File

@@ -40,3 +40,59 @@ export async function getSchedulerStatus(): Promise<SchedulerStatusResponse> {
const { data } = await client.get<SchedulerStatusResponse>("/system/scheduler-status");
return data;
}
// ── ATT&CK Evaluations ─────────────────────────────────────────────
export interface EvaluationRound {
name: string;
display_name: string;
eval_round: number;
imported: boolean;
imported_at: string | null;
tests_created: number | null;
techniques_covered: number | null;
}
export interface EvaluationImportResult {
message: string;
created: number;
skipped: number;
techniques_covered: number;
adversary: string;
eval_round: number;
}
export interface NewRoundCheckResult {
new_round_available: boolean;
already_imported: boolean;
latest_round: { name: string; display_name: string; eval_round: number } | null;
error?: string;
}
/** List all public CrowdStrike evaluation rounds with import status. */
export async function listEvaluationRounds(): Promise<EvaluationRound[]> {
const { data } = await client.get<EvaluationRound[]>("/system/attck-evaluations/rounds");
return data;
}
/** Import a specific evaluation round. */
export async function importEvaluationRound(payload: {
adversary_name: string;
adversary_display: string;
eval_round: number;
}): Promise<EvaluationImportResult> {
const { data } = await client.post<EvaluationImportResult>("/system/attck-evaluations/import", payload);
return data;
}
/** Import the latest available round automatically. */
export async function importLatestEvaluation(): Promise<EvaluationImportResult> {
const { data } = await client.post<EvaluationImportResult>("/system/attck-evaluations/import-latest");
return data;
}
/** Check if a new round is available. */
export async function checkNewEvaluationRound(): Promise<NewRoundCheckResult> {
const { data } = await client.get<NewRoundCheckResult>("/system/attck-evaluations/check-new");
return data;
}

View File

@@ -21,14 +21,26 @@ import {
Download,
Upload,
PackageOpen,
Swords,
Sparkles,
AlertTriangle,
ExternalLink,
CalendarCheck,
} from "lucide-react";
import client from "../api/client";
import {
triggerMitreSync,
triggerIntelScan,
getSchedulerStatus,
listEvaluationRounds,
importEvaluationRound,
importLatestEvaluation,
checkNewEvaluationRound,
type SyncMitreResponse,
type IntelScanResponse,
type EvaluationRound,
type EvaluationImportResult,
type NewRoundCheckResult,
} from "../api/system";
import {
getTemplateStats,
@@ -51,6 +63,11 @@ export default function SystemPage() {
const [bulkConfirm, setBulkConfirm] = useState<"activate" | "deactivate" | null>(null);
const [selectedTemplateId, setSelectedTemplateId] = useState<string | null>(null);
// ── ATT&CK Evaluations state ─────────────────────────────────────
const [evalImportResult, setEvalImportResult] = useState<EvaluationImportResult | null>(null);
const [evalCheckResult, setEvalCheckResult] = useState<NewRoundCheckResult | null>(null);
const [evalImportingRound, setEvalImportingRound] = useState<string | null>(null);
// ── Existing queries ─────────────────────────────────────────────
const {
data: schedulerStatus,
@@ -145,6 +162,53 @@ export default function SystemPage() {
},
});
// ── ATT&CK Evaluations queries & mutations ───────────────────────
const {
data: evalRounds,
isLoading: evalRoundsLoading,
refetch: refetchEvalRounds,
} = useQuery<EvaluationRound[]>({
queryKey: ["eval-rounds"],
queryFn: listEvaluationRounds,
});
const checkNewRoundMutation = useMutation({
mutationFn: checkNewEvaluationRound,
onSuccess: (data) => {
setEvalCheckResult(data);
refetchEvalRounds();
},
});
const importLatestMutation = useMutation({
mutationFn: importLatestEvaluation,
onSuccess: (data) => {
setEvalImportResult(data);
setEvalImportingRound(null);
refetchEvalRounds();
queryClient.invalidateQueries({ queryKey: ["techniques"] });
queryClient.invalidateQueries({ queryKey: ["metrics"] });
},
onError: () => {
setEvalImportingRound(null);
},
});
const importRoundMutation = useMutation({
mutationFn: (payload: { adversary_name: string; adversary_display: string; eval_round: number }) =>
importEvaluationRound(payload),
onSuccess: (data) => {
setEvalImportResult(data);
setEvalImportingRound(null);
refetchEvalRounds();
queryClient.invalidateQueries({ queryKey: ["techniques"] });
queryClient.invalidateQueries({ queryKey: ["metrics"] });
},
onError: () => {
setEvalImportingRound(null);
},
});
const formatNextRun = (dateStr: string | null) => {
if (!dateStr) return "Not scheduled";
const date = new Date(dateStr);
@@ -294,6 +358,264 @@ export default function SystemPage() {
</div>
</div>
{/* ────────────────────────────────────────────────────────────────
ATT&CK EVALUATIONS — CrowdStrike
──────────────────────────────────────────────────────────────── */}
<div className="rounded-xl border border-orange-500/30 bg-gray-900 p-6">
{/* Header */}
<div className="flex flex-wrap items-start justify-between gap-4 mb-5">
<div className="flex items-start gap-4">
<div className="rounded-lg bg-orange-500/10 p-3 mt-0.5">
<Swords className="h-6 w-6 text-orange-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white">
ATT&CK Evaluations CrowdStrike Falcon
</h2>
<p className="mt-1 text-sm text-gray-400 max-w-2xl">
Seed the platform with real detection data from MITRE Engenuity's public ATT&CK
Evaluations. Results are imported as <span className="text-yellow-400 font-medium">In Review</span> tests —
Blue Leads must validate each one before it counts as coverage.
</p>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={() => checkNewRoundMutation.mutate()}
disabled={checkNewRoundMutation.isPending || evalRoundsLoading}
className="flex items-center gap-2 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 disabled:opacity-50 transition-colors"
>
{checkNewRoundMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
Check for new rounds
</button>
<button
onClick={() => {
setEvalImportingRound("latest");
importLatestMutation.mutate();
}}
disabled={importLatestMutation.isPending || importRoundMutation.isPending}
className="flex items-center gap-2 rounded-lg bg-orange-600 px-3 py-2 text-sm font-medium text-white hover:bg-orange-500 disabled:opacity-50 transition-colors"
>
{importLatestMutation.isPending && evalImportingRound === "latest" ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Sparkles className="h-4 w-4" />
)}
Import Latest Round
</button>
</div>
</div>
{/* Disclaimer callout */}
<div className="mb-5 flex items-start gap-3 rounded-lg border border-yellow-500/30 bg-yellow-900/10 p-4">
<AlertTriangle className="h-5 w-5 text-yellow-400 flex-shrink-0 mt-0.5" />
<div className="text-sm">
<p className="font-medium text-yellow-400 mb-1">Lab environment data — validation required</p>
<p className="text-gray-400">
These results reflect CrowdStrike Falcon performance in a controlled MITRE lab against
simulated adversaries. They do <strong className="text-white">not</strong> represent
your organisation's actual detection capability. All imported tests require Blue Lead
validation before contributing to programme coverage.
</p>
<a
href="https://evals.mitre.org"
target="_blank"
rel="noopener noreferrer"
className="mt-2 inline-flex items-center gap-1 text-xs text-orange-400 hover:text-orange-300 transition-colors"
>
<ExternalLink className="h-3 w-3" />
View official evaluation reports at evals.mitre.org
</a>
</div>
</div>
{/* New round check result */}
{evalCheckResult && (
<div className={`mb-4 rounded-lg border p-3 ${
evalCheckResult.error
? "border-red-500/30 bg-red-900/20"
: evalCheckResult.new_round_available
? "border-green-500/30 bg-green-900/20"
: "border-gray-700 bg-gray-800/50"
}`}>
{evalCheckResult.error ? (
<div className="flex items-center gap-2 text-sm text-red-400">
<XCircle className="h-4 w-4 flex-shrink-0" />
<span>Check failed: {evalCheckResult.error}</span>
</div>
) : evalCheckResult.new_round_available ? (
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="h-4 w-4 text-green-400 flex-shrink-0" />
<span className="text-green-400 font-medium">
New round available: {evalCheckResult.latest_round?.display_name} (Round {evalCheckResult.latest_round?.eval_round})
</span>
</div>
) : (
<div className="flex items-center gap-2 text-sm">
<CalendarCheck className="h-4 w-4 text-gray-400 flex-shrink-0" />
<span className="text-gray-400">
Up to date latest round ({evalCheckResult.latest_round?.display_name}) is already imported.
</span>
</div>
)}
</div>
)}
{/* Import result feedback */}
{evalImportResult && (
<div className="mb-4 rounded-lg border border-green-500/30 bg-green-900/20 p-4">
<div className="flex items-center gap-2 mb-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="grid grid-cols-3 gap-3 text-center text-sm">
<div>
<p className="text-xl font-bold text-white">{evalImportResult.created}</p>
<p className="text-xs text-gray-400">Tests created</p>
</div>
<div>
<p className="text-xl font-bold text-white">{evalImportResult.techniques_covered}</p>
<p className="text-xs text-gray-400">Techniques covered</p>
</div>
<div>
<p className="text-sm font-medium text-orange-400 truncate">{evalImportResult.adversary}</p>
<p className="text-xs text-gray-400">Round {evalImportResult.eval_round}</p>
</div>
</div>
<p className="mt-3 text-xs text-yellow-400 flex items-center gap-1">
<AlertTriangle className="h-3 w-3" />
All tests are in <strong className="ml-1">Review Queue</strong> Blue Leads must validate before they count as coverage.
</p>
</div>
)}
{/* Import error */}
{(importLatestMutation.isError || importRoundMutation.isError) && (
<div className="mb-4 rounded-lg border border-red-500/30 bg-red-900/20 p-3">
<div className="flex items-center gap-2 text-sm text-red-400">
<XCircle className="h-4 w-4 flex-shrink-0" />
<span>
{((importLatestMutation.error || importRoundMutation.error) as Error)?.message ??
"Import failed. This round may already be imported."}
</span>
</div>
</div>
)}
{/* Rounds table */}
<div>
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-500">
Available Rounds
</p>
{evalRoundsLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-orange-400" />
</div>
) : evalRounds && evalRounds.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">Round</th>
<th className="pb-3 px-4 font-medium text-gray-400">Adversary</th>
<th className="pb-3 px-4 font-medium text-gray-400">Status</th>
<th className="pb-3 px-4 font-medium text-gray-400">Imported</th>
<th className="pb-3 px-4 font-medium text-gray-400">Tests</th>
<th className="pb-3 pl-4 font-medium text-gray-400">Action</th>
</tr>
</thead>
<tbody>
{evalRounds.map((round) => (
<tr
key={round.name}
className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors"
>
<td className="py-3 pr-4">
<span className="inline-flex items-center justify-center rounded-full border border-orange-500/30 bg-orange-900/20 px-2.5 py-0.5 text-xs font-bold text-orange-400">
R{round.eval_round}
</span>
</td>
<td className="py-3 px-4">
<p className="font-medium text-gray-200">{round.display_name}</p>
<p className="text-xs text-gray-500 font-mono">{round.name}</p>
</td>
<td className="py-3 px-4">
{round.imported ? (
<span className="inline-flex items-center gap-1.5 rounded-full border border-green-500/30 bg-green-900/20 px-2.5 py-0.5 text-xs font-medium text-green-400">
<CheckCircle className="h-3 w-3" />
Imported
</span>
) : (
<span className="inline-flex items-center gap-1.5 rounded-full border border-gray-600 bg-gray-800/50 px-2.5 py-0.5 text-xs font-medium text-gray-400">
Not imported
</span>
)}
</td>
<td className="py-3 px-4 text-xs text-gray-400">
{round.imported_at
? new Date(round.imported_at).toLocaleDateString("en-GB", {
day: "numeric",
month: "short",
year: "numeric",
})
: "—"}
</td>
<td className="py-3 px-4 text-center">
{round.imported ? (
<div className="text-center">
<p className="text-sm font-medium text-gray-200">{round.tests_created ?? "—"}</p>
<p className="text-xs text-gray-500">{round.techniques_covered ?? ""} techniques</p>
</div>
) : (
<span className="text-gray-600"></span>
)}
</td>
<td className="py-3 pl-4">
{round.imported ? (
<span className="text-xs text-gray-600 italic">Already imported</span>
) : (
<button
onClick={() => {
setEvalImportingRound(round.name);
importRoundMutation.mutate({
adversary_name: round.name,
adversary_display: round.display_name,
eval_round: round.eval_round,
});
}}
disabled={
importLatestMutation.isPending ||
importRoundMutation.isPending
}
className="flex items-center gap-1.5 rounded-lg border border-orange-500/30 bg-orange-900/20 px-3 py-1.5 text-xs font-medium text-orange-400 hover:bg-orange-900/40 disabled:opacity-50 transition-colors"
>
{importRoundMutation.isPending && evalImportingRound === round.name ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Download className="h-3.5 w-3.5" />
)}
Import
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="flex items-center justify-center gap-2 py-8 text-gray-500 text-sm">
<AlertCircle className="h-4 w-4" />
<span>Unable to load evaluation rounds. Check network connectivity to evals.mitre.org.</span>
</div>
)}
</div>
</div>
{/* ────────────────────────────────────────────────────────────────
TEMPLATE ADMINISTRATION (T-124)
──────────────────────────────────────────────────────────────── */}