feat(evaluations): enrich eval tests with attack path, criteria and data sources
Aegis CI / lint-and-test (push) Has been cancelled

- Capture Step.Description (HTML stripped), step name/number, substep ref,
  criteria, and data sources from MITRE ATT&CK Evaluations API
- _aggregate_by_technique() now accumulates ALL occurrences per technique
  (multiple substep refs, criteria, step contexts) instead of keeping only
  the best-scoring one
- New helper functions _build_procedure_text(), _build_description(),
  _build_red_summary() generate rich narratives from accumulated occurrences
- New re_enrich_evaluation_round() service function + POST endpoint
  /system/attck-evaluations/re-enrich to update already-imported tests
  without changing detection results or validation state
- Frontend: Re-enrich button per imported round + result banner in SystemPage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-06-08 11:42:08 +02:00
parent 72983a022b
commit 7703c36ed7
4 changed files with 366 additions and 26 deletions
+18
View File
@@ -109,6 +109,14 @@ export interface BulkApproveResult {
message: string;
}
export interface ReEnrichResult {
updated: number;
skipped: number;
adversary: string;
eval_round: number;
message: string;
}
/** Bulk-approve all in-review evaluation tests (Blue Team side). */
export async function bulkApproveEvaluationTests(): Promise<BulkApproveResult> {
const { data } = await client.post<BulkApproveResult>("/system/attck-evaluations/bulk-approve");
@@ -120,3 +128,13 @@ export async function getEvalPendingCount(): Promise<{ pending: number }> {
const { data } = await client.get<{ pending: number }>("/system/attck-evaluations/pending-count");
return data;
}
/** Re-enrich an already-imported round with attack path, criteria and data sources. */
export async function reEnrichEvaluationRound(payload: {
adversary_name: string;
adversary_display: string;
eval_round: number;
}): Promise<ReEnrichResult> {
const { data } = await client.post<ReEnrichResult>("/system/attck-evaluations/re-enrich", payload);
return data;
}
+63 -1
View File
@@ -39,6 +39,7 @@ import {
checkNewEvaluationRound,
bulkApproveEvaluationTests,
getEvalPendingCount,
reEnrichEvaluationRound,
type SyncMitreResponse,
type IntelScanResponse,
type EvaluationRound,
@@ -46,6 +47,7 @@ import {
type EvaluationImportResult,
type NewRoundCheckResult,
type BulkApproveResult,
type ReEnrichResult,
} from "../api/system";
import {
getTemplateStats,
@@ -74,6 +76,8 @@ export default function SystemPage() {
const [evalImportingRound, setEvalImportingRound] = useState<string | null>(null);
const [showBulkApproveModal, setShowBulkApproveModal] = useState(false);
const [bulkApproveResult, setBulkApproveResult] = useState<BulkApproveResult | null>(null);
const [reEnrichingRound, setReEnrichingRound] = useState<string | null>(null);
const [reEnrichResult, setReEnrichResult] = useState<ReEnrichResult | null>(null);
// ── Existing queries ─────────────────────────────────────────────
const {
@@ -241,6 +245,18 @@ export default function SystemPage() {
},
});
const reEnrichMutation = useMutation({
mutationFn: (payload: { adversary_name: string; adversary_display: string; eval_round: number }) =>
reEnrichEvaluationRound(payload),
onSuccess: (data) => {
setReEnrichResult(data);
setReEnrichingRound(null);
},
onError: () => {
setReEnrichingRound(null);
},
});
const formatNextRun = (dateStr: string | null) => {
if (!dateStr) return "Not scheduled";
const date = new Date(dateStr);
@@ -561,6 +577,33 @@ export default function SystemPage() {
</div>
)}
{/* Re-enrich result */}
{reEnrichResult && (
<div className="mb-4 rounded-lg border border-blue-500/30 bg-blue-900/20 p-4">
<div className="flex items-center gap-2 mb-2">
<Sparkles className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-blue-400">Re-enrichment complete</span>
</div>
<div className="grid grid-cols-2 gap-3 text-center text-sm">
<div>
<p className="text-xl font-bold text-white">{reEnrichResult.updated}</p>
<p className="text-xs text-gray-400">Tests enriched</p>
</div>
<div>
<p className="text-sm font-medium text-blue-400 truncate">{reEnrichResult.adversary}</p>
<p className="text-xs text-gray-400">Round {reEnrichResult.eval_round}</p>
</div>
</div>
<p className="mt-2 text-xs text-gray-400">{reEnrichResult.message}</p>
<button
onClick={() => setReEnrichResult(null)}
className="mt-2 text-xs text-gray-500 hover:text-gray-400 underline"
>
Dismiss
</button>
</div>
)}
{/* Import result feedback */}
{evalImportResult && (
<div className="mb-4 rounded-lg border border-green-500/30 bg-green-900/20 p-4">
@@ -685,7 +728,26 @@ export default function SystemPage() {
</td>
<td className="py-3 pl-4">
{round.imported ? (
<span className="text-xs text-gray-600 italic">Already imported</span>
<button
onClick={() => {
setReEnrichingRound(round.name);
reEnrichMutation.mutate({
adversary_name: round.name,
adversary_display: round.display_name,
eval_round: round.eval_round,
});
}}
disabled={reEnrichMutation.isPending || importRoundMutation.isPending}
className="flex items-center gap-1.5 rounded-lg border border-blue-500/30 bg-blue-900/20 px-3 py-1.5 text-xs font-medium text-blue-400 hover:bg-blue-900/40 disabled:opacity-50 transition-colors"
title="Update existing tests with attack path, criteria and data sources from MITRE API"
>
{reEnrichMutation.isPending && reEnrichingRound === round.name ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Sparkles className="h-3.5 w-3.5" />
)}
Re-enrich
</button>
) : (
<button
onClick={() => {