Assessor_2.0/tests/server/mitre-catalog-service.test.ts

85 lines
4.9 KiB
TypeScript

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<void>(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<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()));
}
});