88 lines
4.6 KiB
TypeScript
88 lines
4.6 KiB
TypeScript
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<string, string>();
|
|
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<string, MitreEntity['type']> = {
|
|
'intrusion-set': 'group', campaign: 'campaign', malware: 'software', tool: 'software'
|
|
};
|
|
const entitiesByStixId = new Map<string, MitreEntity>();
|
|
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 };
|
|
}
|