feat(ttp): show canonical ATT&CK technique names in the TTPs UI
"T1595" alone is opaque; "T1595 — Active Scanning" tells you the story at a glance. The names come from a backend-side static catalogue pinned to the same ATT&CK release as the rule engine (_ATTACK_RELEASE = "v15.1") — names are the canonical MITRE labels, not author-supplied strings on rules, so a rule author can't typo a name and the entire fleet sees the typo. - New `decnet/ttp/attack_catalog.py` with `TECHNIQUE_NAMES` covering every technique_id + sub_technique_id emitted by `rules/ttp/` (R0001..R0058 → 69 IDs in the v0 pack). - `IdentityTechniqueRow` / `TechniqueRollupRow` / `CampaignTechniqueRow` / `TTPTagDetailRow` gain optional `technique_name` / `sub_technique_name` fields. Repo + router populate them from the catalogue at row-construction time. None when an ID isn't in the catalogue — UI falls back to the bare ID. - Coverage test (`tests/ttp/test_attack_catalog.py`) walks every YAML rule and asserts every emitted ID has a catalogue entry, so a future rule author who forgets to update the catalogue gets a loud failure rather than a silent UI fallback. Frontend: - `TTPsObservedSection` shows "T1595.002 — Active Scanning: Vulnerability Scanning" instead of just the ID, with overflow ellipsis + tooltip for narrow viewports. Inspector header / TECHNIQUE row also surface the names.
This commit is contained in:
@@ -26,7 +26,9 @@ export interface TTPTagDetailRow {
|
||||
decky_id: string | null;
|
||||
tactic: string;
|
||||
technique_id: string;
|
||||
technique_name: string | null;
|
||||
sub_technique_id: string | null;
|
||||
sub_technique_name: string | null;
|
||||
confidence: number;
|
||||
rule_id: string;
|
||||
rule_version: number;
|
||||
@@ -42,6 +44,8 @@ interface Props {
|
||||
uuid: string;
|
||||
techniqueId: string;
|
||||
subTechniqueId: string | null;
|
||||
techniqueName: string | null;
|
||||
subTechniqueName: string | null;
|
||||
tactic: string;
|
||||
count: number;
|
||||
confidenceMax: number;
|
||||
@@ -49,7 +53,8 @@ interface Props {
|
||||
}
|
||||
|
||||
const TTPInspector: React.FC<Props> = ({
|
||||
scope, uuid, techniqueId, subTechniqueId, tactic, count, confidenceMax, onClose,
|
||||
scope, uuid, techniqueId, subTechniqueId, techniqueName, subTechniqueName,
|
||||
tactic, count, confidenceMax, onClose,
|
||||
}) => {
|
||||
const panelRef = useRef<HTMLDivElement | null>(null);
|
||||
useEscapeKey(onClose, true);
|
||||
@@ -90,7 +95,9 @@ const TTPInspector: React.FC<Props> = ({
|
||||
return () => { cancelled = true; };
|
||||
}, [scope, uuid, techniqueId, subTechniqueId]);
|
||||
|
||||
const label = subTechniqueId ?? techniqueId;
|
||||
const id = subTechniqueId ?? techniqueId;
|
||||
const name = subTechniqueName ?? techniqueName;
|
||||
const headerLabel = name ? `${id} — ${name}` : id;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -101,7 +108,7 @@ const TTPInspector: React.FC<Props> = ({
|
||||
<div className="bd-head">
|
||||
<h3>
|
||||
<Crosshair size={14} />
|
||||
<span>{label}</span>
|
||||
<span>{headerLabel}</span>
|
||||
</h3>
|
||||
<button className="close-btn" onClick={onClose} aria-label="Close">
|
||||
<X size={16} />
|
||||
@@ -117,7 +124,14 @@ const TTPInspector: React.FC<Props> = ({
|
||||
<div className="k" style={{ color: 'var(--dim-color)' }}>TACTIC</div>
|
||||
<div className="v">{tactic}</div>
|
||||
<div className="k" style={{ color: 'var(--dim-color)' }}>TECHNIQUE</div>
|
||||
<div className="v">{techniqueId}{subTechniqueId ? ` / ${subTechniqueId}` : ''}</div>
|
||||
<div className="v">
|
||||
{techniqueId}{techniqueName ? ` — ${techniqueName}` : ''}
|
||||
{subTechniqueId && (
|
||||
<div style={{ marginTop: 2 }}>
|
||||
↳ {subTechniqueId}{subTechniqueName ? ` — ${subTechniqueName}` : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="k" style={{ color: 'var(--dim-color)' }}>FIRES</div>
|
||||
<div className="v">{count}</div>
|
||||
<div className="k" style={{ color: 'var(--dim-color)' }}>MAX CONF</div>
|
||||
|
||||
@@ -18,7 +18,9 @@ import TTPInspector from './TTPInspector';
|
||||
|
||||
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;
|
||||
@@ -161,6 +163,8 @@ const TTPsObservedSection: React.FC<Props> = ({ scope, uuid }) => {
|
||||
uuid={uuid}
|
||||
techniqueId={selected.technique_id}
|
||||
subTechniqueId={selected.sub_technique_id}
|
||||
techniqueName={selected.technique_name}
|
||||
subTechniqueName={selected.sub_technique_name}
|
||||
tactic={selected.tactic}
|
||||
count={selected.count}
|
||||
confidenceMax={selected.confidence_max}
|
||||
@@ -179,7 +183,12 @@ const TechniqueBar: React.FC<{
|
||||
// can never appear (repo confidence floor) so the bar always shows
|
||||
// some non-trivial fill.
|
||||
const pct = Math.round(Math.max(0, Math.min(1, row.confidence_max)) * 100);
|
||||
const label = row.sub_technique_id ?? row.technique_id;
|
||||
// Prefer the sub-technique label if present (more specific). Each
|
||||
// half is "T#### — Name" when the catalogue has a name, falling
|
||||
// back to the bare ID for techniques not yet catalogued.
|
||||
const id = row.sub_technique_id ?? row.technique_id;
|
||||
const name = row.sub_technique_name ?? row.technique_name;
|
||||
const label = name ? `${id} — ${name}` : id;
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
@@ -194,8 +203,8 @@ const TechniqueBar: React.FC<{
|
||||
title="Click to inspect underlying tags + evidence"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '160px 1fr 60px',
|
||||
gap: 8,
|
||||
gridTemplateColumns: 'minmax(280px, 2fr) 1fr 60px',
|
||||
gap: 10,
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
padding: '2px 4px',
|
||||
@@ -204,7 +213,15 @@ const TechniqueBar: React.FC<{
|
||||
onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(155,135,245,0.06)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
|
||||
>
|
||||
<span className="matrix-text">{label}</span>
|
||||
<span
|
||||
className="matrix-text"
|
||||
style={{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
title={label}
|
||||
>{label}</span>
|
||||
<div
|
||||
style={{
|
||||
height: 6,
|
||||
|
||||
Reference in New Issue
Block a user