feat(evaluations): ATT&CK Evaluations importer for CrowdStrike Falcon [FASE-6.1]
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
──────────────────────────────────────────────────────────────── */}
|
||||
|
||||
Reference in New Issue
Block a user