import type { MitreGraphData, MitreEntity, MitreTechnique } from './mitre-graph'; interface StixObject { id?: string; type?: string; name?: string; revoked?: boolean; x_mitre_deprecated?: boolean; x_mitre_version?: string; x_mitre_shortname?: string; x_mitre_platforms?: string[]; tactic_refs?: string[]; kill_chain_phases?: { kill_chain_name?: string; phase_name?: string }[]; external_references?: { source_name?: string; external_id?: string; url?: string }[]; relationship_type?: string; source_ref?: string; target_ref?: string; } const active = (item: StixObject) => !item.revoked && !item.x_mitre_deprecated; const attackReference = (item: StixObject) => item.external_references?.find(reference => reference.source_name === 'mitre-attack'); const byId = (left: { id: string }, right: { id: string }) => left.id.localeCompare(right.id); /** Convert MITRE's Enterprise ATT&CK STIX 2.1 bundle to the compact Pathfinder catalog. */ export function convertEnterpriseStix(raw: unknown): MitreGraphData { if (!raw || typeof raw !== 'object' || !('type' in raw) || raw.type !== 'bundle' || !('objects' in raw) || !Array.isArray(raw.objects)) { throw new Error('Expected an Enterprise ATT&CK STIX bundle.'); } const objects = raw.objects as StixObject[]; const collection = objects.find(item => item.type === 'x-mitre-collection' && item.name === 'Enterprise ATT&CK'); const matrix = objects.find(item => item.type === 'x-mitre-matrix' && item.name === 'Enterprise ATT&CK' && active(item)); if (!collection?.x_mitre_version || !matrix?.tactic_refs?.length) { throw new Error('Enterprise ATT&CK collection version or tactic matrix is missing.'); } const tactics = new Map(objects.filter(item => item.type === 'x-mitre-tactic' && active(item)).map(item => [item.id, item])); const ordered = matrix.tactic_refs.map(id => tactics.get(id)); if (ordered.some(item => !item?.name || !item.x_mitre_shortname)) { throw new Error('Enterprise ATT&CK matrix references an unavailable tactic.'); } const tacticOrder = ordered.map(item => item!.name!); const tacticNames = new Map(ordered.map(item => [item!.x_mitre_shortname!, item!.name!])); const techniqueStixIds = new Map(); const techniques: MitreTechnique[] = objects.filter(item => item.type === 'attack-pattern' && active(item)).flatMap(item => { const reference = attackReference(item); if (!item.id || !item.name || !reference?.external_id || !/^T\d{4}(?:\.\d{3})?$/.test(reference.external_id)) return []; const mappedTactics = [...new Set((item.kill_chain_phases ?? []) .filter(phase => phase.kill_chain_name === 'mitre-attack') .map(phase => tacticNames.get(phase.phase_name ?? '')) .filter((name): name is string => Boolean(name)))]; if (!mappedTactics.length) return []; techniqueStixIds.set(item.id, reference.external_id); return [{ id: reference.external_id, name: item.name, tactics: tacticOrder.filter(name => mappedTactics.includes(name)), platforms: Array.isArray(item.x_mitre_platforms) ? item.x_mitre_platforms : [], parent: reference.external_id.includes('.') ? reference.external_id.split('.')[0] : null, url: reference.url ?? `https://attack.mitre.org/techniques/${reference.external_id.replace('.', '/')}`, sourceCount: item.external_references?.length ?? 0 }]; }).sort(byId); if (!techniques.length) throw new Error('Enterprise ATT&CK bundle contains no active techniques.'); const entityTypes: Record = { 'intrusion-set': 'group', campaign: 'campaign', malware: 'software', tool: 'software' }; const entitiesByStixId = new Map(); for (const item of objects) { const type = entityTypes[item.type ?? '']; const reference = attackReference(item); if (type && active(item) && item.id && item.name && reference?.external_id) { entitiesByStixId.set(item.id, { id: reference.external_id, name: item.name, type, techniques: [] }); } } for (const item of objects) { if (item.type !== 'relationship' || item.relationship_type !== 'uses' || !active(item)) continue; const entity = entitiesByStixId.get(item.source_ref ?? ''); const techniqueId = techniqueStixIds.get(item.target_ref ?? ''); if (entity && techniqueId && !entity.techniques.includes(techniqueId)) entity.techniques.push(techniqueId); } const entities = [...entitiesByStixId.values()].sort(byId).map(entity => ({ ...entity, techniques: entity.techniques.sort() })); return { version: collection.x_mitre_version, tacticOrder, techniques, entities }; }