Assessor_2.0/src/features/mitre-graph/mitre-stix.ts

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 };
}