feat(evaluations): enrich eval tests with attack path, criteria and data sources
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
Reference in New Issue
Block a user