import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, expect, it, vi } from 'vitest'; import { MitreCatalogService } from '../../server/reference-data/mitre-catalog-service'; import { createAssessorHttpServer } from '../../server/assessor-http-server'; import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository'; import type { AddressInfo } from 'node:net'; const stix = (version: string) => ({ type: 'bundle', objects: [ { type: 'x-mitre-collection', name: 'Enterprise ATT&CK', x_mitre_version: version }, { type: 'x-mitre-matrix', name: 'Enterprise ATT&CK', tactic_refs: ['x-mitre-tactic--1'] }, { id: 'x-mitre-tactic--1', type: 'x-mitre-tactic', name: 'Execution', x_mitre_shortname: 'execution' }, { id: 'attack-pattern--1', type: 'attack-pattern', name: 'Example technique', external_references: [{ source_name: 'mitre-attack', external_id: 'T1234' }], kill_chain_phases: [{ kill_chain_name: 'mitre-attack', phase_name: 'execution' }] } ] }); let directory: string | undefined; afterEach(async () => { if (directory) await rm(directory, { recursive: true, force: true }); directory = undefined; }); it('updates the shared catalog only for a newer valid MITRE version and persists it', async () => { directory = await mkdtemp(join(tmpdir(), 'assessor-mitre-')); const path = join(directory, 'reference-data', 'enterprise.json'); let version = '19.3'; const fetcher = vi.fn(async (url: string) => { expect(url).toBe('https://raw.githubusercontent.com/mitre-attack/attack-stix-data/master/enterprise-attack/enterprise-attack.json'); return new Response(JSON.stringify(stix(version)), { status: 200, headers: { 'Content-Type': 'application/json' } }); }) as unknown as typeof fetch; const service = new MitreCatalogService(path, fetcher); expect((await service.current()).catalog.version).toBe('19.2'); const source = await service.officialSource(); expect(JSON.parse(source.bytes.toString('utf8'))).toEqual(stix('19.3')); expect((await service.current()).catalog.version).toBe('19.2'); expect(await service.refresh()).toMatchObject({ updated: true, origin: 'mitre', catalog: { version: '19.3' } }); expect((await new MitreCatalogService(path, fetcher).current()).catalog.version).toBe('19.3'); version = '19.2'; expect(await service.refresh()).toMatchObject({ updated: false, catalog: { version: '19.3' } }); expect((await service.current()).catalog.version).toBe('19.3'); }); it('keeps the active catalog when MITRE returns an invalid or oversized file', async () => { directory = await mkdtemp(join(tmpdir(), 'assessor-mitre-')); const path = join(directory, 'reference-data', 'enterprise.json'); const invalid = new MitreCatalogService(path, async () => new Response('{', { status: 200 })); await expect(invalid.refresh()).rejects.toMatchObject({ code: 'mitre_source_invalid' }); const oversized = new MitreCatalogService(path, async () => new Response('{}', { status: 200, headers: { 'Content-Length': String(81 * 1024 * 1024) } })); await expect(oversized.refresh()).rejects.toMatchObject({ code: 'mitre_source_too_large' }); expect((await invalid.current()).catalog.version).toBe('19.2'); }); it('serves the original MITRE download separately from the active catalog and protects refresh', async () => { directory = await mkdtemp(join(tmpdir(), 'assessor-mitre-')); const original = JSON.stringify(stix('19.3')); const service = new MitreCatalogService(join(directory, 'reference-data', 'enterprise.json'), async () => new Response(original, { status: 200 })); const server = createAssessorHttpServer({ repository: new LocalAssessorRepository(), staticRoot: directory!, mitreCatalogService: service }); await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); try { const base = `http://127.0.0.1:${(server.address() as AddressInfo).port}/api/v1/reference-data/mitre-attack`; const download = await fetch(`${base}/source`); expect(download.status).toBe(200); expect(download.headers.get('content-disposition')).toBe('attachment; filename="enterprise-attack-v19.3.json"'); expect(await download.text()).toBe(original); expect((await (await fetch(base)).json()).catalog.version).toBe('19.2'); const blocked = await fetch(`${base}/refresh`, { method: 'POST', body: '{}' }); expect(blocked.status).toBe(403); const updated = await fetch(`${base}/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Assessor-Request': '1', Origin: new URL(base).origin }, body: '{}' }); expect(updated.status).toBe(200); expect(await updated.json()).toMatchObject({ updated: true, catalog: { version: '19.3' } }); expect((await (await fetch(base)).json()).catalog.version).toBe('19.3'); } finally { await new Promise((resolve, reject) => server.close(error => error ? reject(error) : resolve())); } });