feat(web): mitre_url deeplinks + lazy groups subpanel in TTPInspector

Every technique_id in TechniqueBar and TTPInspector now links to its
canonical attack.mitre.org page. The inspector drawer gains a GROUPS
subpanel that lazy-fetches the new /ttp/techniques/{id}/groups endpoint
and renders each MITRE-tracked intrusion-set with deeplink and aliases.

Centralizes TTP row interfaces into src/types/ttp.ts and API wrappers
into src/utils/ttpApi.ts to give the new GroupRef type a clean home and
avoid a third inline fetch declaration.
This commit is contained in:
2026-05-09 06:57:10 -04:00
parent 1d3086a5c7
commit c4d6eb5bb3
5 changed files with 240 additions and 54 deletions

View File

@@ -3,6 +3,7 @@ import { Crosshair, Download, Target } from '../icons';
import api from '../utils/api';
import EmptyState from './EmptyState/EmptyState';
import TTPInspector from './TTPInspector';
import type { TechniqueRow } from '../types/ttp';
/*
* TTPsObservedSection — shared between IdentityDetail (primary) and
@@ -16,18 +17,6 @@ import TTPInspector from './TTPInspector';
* operator rule administration.
*/
interface TechniqueRow {
technique_id: string;
technique_name: string | null;
sub_technique_id: string | null;
sub_technique_name: string | null;
tactic: string;
count: number;
first_seen: string;
last_seen: string;
confidence_max: number;
}
const TACTIC_LABEL: Record<string, string> = {
TA0043: 'RECONNAISSANCE',
TA0042: 'RESOURCE DEVELOPMENT',
@@ -168,6 +157,7 @@ const TTPsObservedSection: React.FC<Props> = ({ scope, uuid }) => {
tactic={selected.tactic}
count={selected.count}
confidenceMax={selected.confidence_max}
mitre_url={selected.mitre_url}
onClose={() => setSelected(null)}
/>
)}
@@ -221,7 +211,20 @@ const TechniqueBar: React.FC<{
textOverflow: 'ellipsis',
}}
title={label}
>{label}</span>
>
{row.mitre_url ? (
<>
<a
href={row.mitre_url}
target="_blank"
rel="noopener noreferrer"
className="ttp-mitre-link"
onClick={(e) => e.stopPropagation()}
>{id} </a>
{name ? `${name}` : ''}
</>
) : label}
</span>
<div
style={{
height: 6,