85 lines
4.9 KiB
TypeScript
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()));
|
|
}
|
|
});
|