79 lines
5.5 KiB
TypeScript
79 lines
5.5 KiB
TypeScript
import { expect, it, vi } from 'vitest';
|
|
import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository';
|
|
import { ReferenceRecognitionService } from '../../server/reference-data/reference-recognition-service';
|
|
import { bundledReferenceCatalogs, referenceSnapshot } from '../../server/reference-data/reference-catalogs';
|
|
import { seedReferenceScan } from '../fixtures/reference-recognition';
|
|
import { canonicalModelToDataset } from '../../src/features/project-import/canonical-model-to-dataset';
|
|
import { buildFindingPathItems, defaultMitreGraphData } from '../../src/features/mitre-graph/mitre-graph';
|
|
|
|
it('makes a previously used catalog current again without rewriting history, even when the clock moves backwards', async () => {
|
|
const repository = new LocalAssessorRepository();
|
|
const { project, assessment, model } = await seedReferenceScan(repository);
|
|
const { sha256: _, ...attack } = bundledReferenceCatalogs[0];
|
|
const alternate = [referenceSnapshot({ ...attack, version: '99.0' }), bundledReferenceCatalogs[1]];
|
|
let catalogs = bundledReferenceCatalogs;
|
|
const service = new ReferenceRecognitionService(repository, async () => catalogs);
|
|
const now = vi.spyOn(Date, 'now').mockReturnValue(Date.parse('2100-01-01T00:00:00Z'));
|
|
try {
|
|
await service.reprocess(project.id);
|
|
const original = await repository.listReferenceRuns(project.id);
|
|
now.mockReturnValue(Date.parse('2026-10-04T00:00:00Z'));
|
|
for (const selection of [alternate, bundledReferenceCatalogs, alternate, bundledReferenceCatalogs]) {
|
|
catalogs = selection;
|
|
expect(await service.status(project.id)).toMatchObject({ pendingScans: 1 });
|
|
expect(await service.reprocess(project.id)).toMatchObject({ processedScans: 1, unchangedScans: 0 });
|
|
expect(await service.status(project.id)).toMatchObject({ pendingScans: 0 });
|
|
const revision = repository.revision;
|
|
expect(await service.reprocess(project.id)).toMatchObject({ processedScans: 0, unchangedScans: 1 });
|
|
expect(repository.revision).toBe(revision);
|
|
}
|
|
const runs = await repository.listReferenceRuns(project.id);
|
|
expect(runs).toHaveLength(5);
|
|
expect(new Set(runs.map(run => run.id)).size).toBe(5);
|
|
expect(runs.find(run => run.id === original[0].id)).toEqual(original[0]);
|
|
const stored = (await repository.loadAssessmentModel(assessment.id))!;
|
|
expect(stored.findingOccurrences).toEqual(model.findingOccurrences);
|
|
expect(stored.findingDefinitions).toEqual(model.findingDefinitions);
|
|
expect(stored.reviews).toEqual(model.reviews);
|
|
} finally { now.mockRestore(); }
|
|
});
|
|
|
|
it('reprocesses retained fields, persists provenance and freezes catalog mappings until explicitly reprocessed', async () => {
|
|
const repository = new LocalAssessorRepository();
|
|
const { project, assessment, model } = await seedReferenceScan(repository);
|
|
let catalogs = bundledReferenceCatalogs;
|
|
const service = new ReferenceRecognitionService(repository, async () => catalogs);
|
|
expect(await service.status(project.id)).toMatchObject({ totalScans: 1, indexedScans: 0, pendingScans: 1 });
|
|
expect(await service.reprocess(project.id)).toEqual({ processedScans: 1, unchangedScans: 0, references: 4 });
|
|
const revision = repository.revision;
|
|
expect(await service.reprocess(project.id)).toMatchObject({ processedScans: 0, unchangedScans: 1 });
|
|
expect(repository.revision).toBe(revision);
|
|
const stored = (await repository.loadAssessmentModel(assessment.id))!;
|
|
expect(stored.findingOccurrences).toEqual(model.findingOccurrences);
|
|
expect(stored.findingDefinitions).toEqual(model.findingDefinitions);
|
|
expect(stored.reviews).toEqual(model.reviews);
|
|
const record = canonicalModelToDataset(stored)!.sheets[0].analysis.records[0];
|
|
const path = buildFindingPathItems([record], defaultMitreGraphData)[0];
|
|
expect(path.techniqueIds).toEqual(['T1059.001', 'T1003']);
|
|
expect(path.defenses).toEqual(['D3-CH · Credential Hardening']);
|
|
expect(path.mappingOrigin).toBe('native');
|
|
expect(record.referenceRecognition?.references[0].catalogSha256).toBe(catalogs[0].sha256);
|
|
const { sha256: _, ...attack } = catalogs[0];
|
|
catalogs = [referenceSnapshot({ ...attack, version: '99.0', entries: attack.entries.map(entry => entry.id === 'T1059.001' ? { ...entry, tactics: ['Synthetic future tactic'] } : entry) }), catalogs[1]];
|
|
expect(await service.status(project.id)).toMatchObject({ indexedScans: 1, pendingScans: 1 });
|
|
expect(buildFindingPathItems([record], { ...defaultMitreGraphData, techniques: [] })[0].tactics).toContain('Execution');
|
|
await service.reprocess(project.id);
|
|
expect(await repository.listReferenceRuns(project.id)).toHaveLength(2);
|
|
const exported = await repository.exportProject(project.id);
|
|
expect(exported.referenceCatalogs).toHaveLength(3);
|
|
expect(exported.referenceRuns).toHaveLength(2);
|
|
expect(exported.referenceRuns[0]).toEqual(stored.referenceRuns![0]);
|
|
expect(await repository.listReferenceRuns('another-client')).toEqual([]);
|
|
expect(await repository.listReferenceCatalogs('another-client')).toEqual([]);
|
|
await expect(repository.saveReferenceRun({ ...stored.referenceRuns![0], id: 'foreign', projectId: 'another-client' }, bundledReferenceCatalogs)).rejects.toThrow();
|
|
await expect(repository.saveReferenceRun({ ...stored.referenceRuns![0], inputSha256: 'b'.repeat(64) }, bundledReferenceCatalogs)).rejects.toThrow('unveränderlich');
|
|
await repository.deleteProject(project.id, { projectId: project.id, cascade: true });
|
|
expect(await repository.listReferenceRuns(project.id)).toEqual([]);
|
|
expect(await repository.listReferenceCatalogs(project.id)).toEqual([]);
|
|
});
|