142 lines
9.8 KiB
TypeScript
142 lines
9.8 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { LocalAssessorRepository } from '../../src/persistence/local-assessor-repository';
|
|
import {
|
|
detectMicrosoftSecurityBaselineMetadata,
|
|
selectMicrosoftSecurityBaseline,
|
|
type MicrosoftSecurityBaselineRecord
|
|
} from '../../src/domain/microsoft-security-baseline';
|
|
import { MicrosoftSecurityBaselineService } from '../../server/gpo/microsoft-security-baseline-service';
|
|
import type { ArtifactStoreReconciliationRequest, BackendArtifactStore, ReadArtifactRequest, StoreArtifactRequest } from '../../server/artifacts/backend-artifact-store';
|
|
import { directGpoBackupWorkerExecutor } from '../../server/gpo/gpo-inventory-executor';
|
|
import { syntheticMicrosoftBaselineZip } from './microsoft-baseline-synthetic-fixture';
|
|
import { analyzeMicrosoftSecurityBaselinePackage } from '../../server/gpo/microsoft-security-baseline-parser';
|
|
import { MICROSOFT_BASELINE_SETTINGS_ANALYZER } from '../../src/domain/microsoft-security-baseline-analysis';
|
|
|
|
class MemoryArtifactStore implements BackendArtifactStore {
|
|
private readonly objects = new Map<string, Uint8Array>();
|
|
private sequence = 0;
|
|
async put(request: StoreArtifactRequest) {
|
|
const storageKey = `00000000-0000-4000-8000-${String(++this.sequence).padStart(12, '0')}`;
|
|
this.objects.set(storageKey, request.bytes.slice());
|
|
const { createHash } = await import('node:crypto');
|
|
return { version: 1 as const, storageKey, sha256: createHash('sha256').update(request.bytes).digest('hex'), sizeBytes: request.bytes.byteLength };
|
|
}
|
|
async read(request: ReadArtifactRequest) { return this.objects.get(request.storageKey)?.slice() ?? new Uint8Array(); }
|
|
async availability(storageKey: string) { return this.objects.has(storageKey) ? 'available' as const : 'missing' as const; }
|
|
async restore(request: ReadArtifactRequest & { bytes: Uint8Array }) { this.objects.set(request.storageKey, request.bytes.slice()); }
|
|
async remove(storageKey: string) { return this.objects.delete(storageKey); }
|
|
async reconcile(request: ArtifactStoreReconciliationRequest) { return { version: 1 as const, referencedObjects: request.referencedStorageKeys.length, retainedUnreferencedObjects: 0, removedUnreferencedObjects: 0, removedStagingFiles: 0 }; }
|
|
}
|
|
|
|
function record(id: string, family: string, version: string): MicrosoftSecurityBaselineRecord {
|
|
return { version: 1, id, fileName: `${family} ${version}.zip`, sha256: id.padEnd(64, 'a').slice(0, 64), sizeBytes: 4, storageKey: '00000000-0000-4000-8000-000000000001', importedAt: '2026-08-04T10:00:00.000Z', status: 'eligible', metadata: { productFamily: family, productVersion: version, roles: ['Any'], scopes: ['mixed'], source: 'detected' } };
|
|
}
|
|
|
|
describe('Microsoft Security Baseline catalog', () => {
|
|
it.each(['1.0.0', '1.1.0'] as const)('reprocesses a legacy %s analysis once and preserves its history', async legacyVersion => {
|
|
const repository = new LocalAssessorRepository();
|
|
const artifactStore = new MemoryArtifactStore();
|
|
const bytes = syntheticMicrosoftBaselineZip(4, [
|
|
'[Service General Setting]', '"SyntheticService",2,""',
|
|
'[Privilege Rights]', 'SeNetworkLogonRight=*S-1-5-32-544'
|
|
].join('\n'));
|
|
const catalog = new MicrosoftSecurityBaselineService(repository, artifactStore, 1024 * 1024);
|
|
const baseline = await catalog.submit({ fileName: 'Windows 11 24H2 Security Baseline.zip',
|
|
bytes, receivedAt: '2026-09-08T08:00:00Z' });
|
|
const legacy = analyzeMicrosoftSecurityBaselinePackage(bytes, { baselineId: baseline.id, analyzedAt: baseline.importedAt });
|
|
legacy.id = `synthetic-legacy-${legacyVersion}`;
|
|
legacy.analyzer = { ...legacy.analyzer, version: legacyVersion };
|
|
legacy.settings = legacy.settings.filter(item => item.observation.deliveryMechanism !== 'security-template');
|
|
await repository.saveMicrosoftSecurityBaselineAnalysis({ version: 1, analysis: legacy });
|
|
|
|
const service = new MicrosoftSecurityBaselineService(repository, artifactStore, 1024 * 1024, directGpoBackupWorkerExecutor);
|
|
const current = await service.reprocess(baseline.id);
|
|
expect(current).toMatchObject({ supersedesAnalysisId: legacy.id, analysis: {
|
|
analyzer: MICROSOFT_BASELINE_SETTINGS_ANALYZER, packageSha256: baseline.sha256, status: 'partial'
|
|
} });
|
|
expect(current.analysis.id).not.toBe(legacy.id);
|
|
expect(current.analysis.settings).toHaveLength(2);
|
|
const history = await service.listAnalyses(baseline.id);
|
|
expect(history).toHaveLength(2);
|
|
expect(history.find(item => item.analysis.id === legacy.id)?.analysis).toEqual(legacy);
|
|
const revision = repository.revision;
|
|
await expect(service.reprocess(baseline.id)).resolves.toEqual(current);
|
|
await expect(service.reprocessPending()).resolves.toEqual({ analyzed: 0, failed: 0 });
|
|
expect(repository.revision).toBe(revision);
|
|
});
|
|
it('detects product and version without using them as policy identity', () => {
|
|
expect(detectMicrosoftSecurityBaselineMetadata('Windows 11 v24H2 Security Baseline.zip')).toMatchObject({ productFamily: 'Windows 11', productVersion: '24H2', source: 'detected' });
|
|
expect(detectMicrosoftSecurityBaselineMetadata('Microsoft 365 Apps for Enterprise 2512.zip')).toMatchObject({ productFamily: 'Microsoft 365 Apps', productVersion: '2512' });
|
|
expect(detectMicrosoftSecurityBaselineMetadata('Windows 10 Version 1809 and Windows Server 2019 Security Baseline.zip').productContexts).toEqual([
|
|
{ productFamily: 'Windows 10', productVersion: '1809', roles: ['Any'], scopes: ['mixed'] },
|
|
{ productFamily: 'Windows Server', productVersion: '2019', roles: ['Any'], scopes: ['mixed'] }
|
|
]);
|
|
});
|
|
|
|
it('selects only one exact product/version match and fails closed on ambiguity', () => {
|
|
const first = record('1', 'Windows 11', '24H2');
|
|
const second = record('2', 'Windows 11', '23H2');
|
|
expect(selectMicrosoftSecurityBaseline([first, second], { productFamily: 'Windows 11', productVersion: '24H2' })).toMatchObject({ status: 'selected', selected: { id: '1' } });
|
|
const ambiguous = selectMicrosoftSecurityBaseline([first, { ...first, id: '3', sha256: '3'.repeat(64) }], { productFamily: 'Windows 11', productVersion: '24H2' });
|
|
expect(ambiguous.status).toBe('ambiguous');
|
|
expect(ambiguous.selected).toBeUndefined();
|
|
const combined = { ...first, metadata: detectMicrosoftSecurityBaselineMetadata('Windows 10 Version 1607 and Windows Server 2016 Security Baseline.zip') };
|
|
expect(selectMicrosoftSecurityBaseline([combined], { productFamily: 'Windows Server', productVersion: '2016' })).toMatchObject({ status: 'selected', selected: { id: '1' } });
|
|
});
|
|
|
|
it('persists deterministic ADMX catalog versions and baseline settings through the worker contract', async () => {
|
|
const repository = new LocalAssessorRepository();
|
|
const artifactStore = new MemoryArtifactStore();
|
|
const service = new MicrosoftSecurityBaselineService(repository, artifactStore, 1024 * 1024, directGpoBackupWorkerExecutor);
|
|
const baseline = await service.submit({
|
|
fileName: 'Windows Server 2022 Security Baseline.zip',
|
|
bytes: syntheticMicrosoftBaselineZip(),
|
|
receivedAt: '2026-08-04T10:00:00.000Z'
|
|
});
|
|
const analyses = await service.listAnalyses(baseline.id);
|
|
expect(analyses).toHaveLength(1);
|
|
expect(analyses[0]).toMatchObject({
|
|
analysis: {
|
|
baselineId: baseline.id,
|
|
status: 'complete',
|
|
admxCatalog: { status: 'complete', policies: [expect.objectContaining({ technicalId: 'Synthetic.Policies#Mode' })] },
|
|
settings: [expect.objectContaining({ identity: expect.objectContaining({ status: 'resolved' }) })]
|
|
}
|
|
});
|
|
const revision = repository.revision;
|
|
const repeated = await service.reprocess(baseline.id);
|
|
expect(repeated.analysis.id).toBe(analyses[0].analysis.id);
|
|
expect(repository.revision).toBe(revision);
|
|
const derivedState = repository as unknown as { state: {
|
|
policyEvaluations: Array<{ run: Record<string, unknown> }>;
|
|
policyFindingRuns: Array<{ run: Record<string, unknown> }>;
|
|
} };
|
|
derivedState.state.policyEvaluations.push({ run: {
|
|
id: 'derived-evaluation', projectId: 'derived-project', completedAt: '2026-08-04T10:01:00.000Z',
|
|
baselineAnalysisIds: [analyses[0].analysis.id]
|
|
} });
|
|
derivedState.state.policyFindingRuns.push({ run: {
|
|
id: 'derived-findings', projectId: 'derived-project', evaluationRunId: 'derived-evaluation',
|
|
completedAt: '2026-08-04T10:02:00.000Z'
|
|
} });
|
|
await expect(service.delete(baseline.id)).resolves.toMatchObject({ id: baseline.id });
|
|
await expect(service.list()).resolves.toEqual([]);
|
|
await expect(service.listAnalyses(baseline.id)).resolves.toEqual([]);
|
|
await expect(repository.listMicrosoftSecurityBaselineClassifications()).resolves.toEqual([]);
|
|
await expect(repository.listPolicyEvaluations('derived-project')).resolves.toEqual([]);
|
|
await expect(repository.listPolicyFindingRuns('derived-project')).resolves.toEqual([]);
|
|
await expect(artifactStore.read({ storageKey: baseline.storageKey, expectedSha256: baseline.sha256, maximumBytes: 1024 * 1024 })).resolves.toHaveLength(0);
|
|
});
|
|
it('stores ZIP originals once and persists catalog metadata', async () => {
|
|
const repository = new LocalAssessorRepository();
|
|
const service = new MicrosoftSecurityBaselineService(repository, new MemoryArtifactStore(), 1024);
|
|
const bytes = new Uint8Array([0x50, 0x4b, 0x03, 0x04, 0, 0]);
|
|
const first = await service.submit({ fileName: 'Windows Server 2022 Security Baseline.zip', bytes, receivedAt: '2026-08-04T10:00:00.000Z' });
|
|
const second = await service.submit({ fileName: 'duplicate.zip', bytes, receivedAt: '2026-08-04T11:00:00.000Z' });
|
|
expect(second.id).toBe(first.id);
|
|
await expect(service.list()).resolves.toHaveLength(1);
|
|
expect(first).toMatchObject({ status: 'eligible', metadata: { productFamily: 'Windows Server', productVersion: '2022' } });
|
|
await expect(repository.listMicrosoftSecurityBaselineClassifications()).resolves.toHaveLength(1);
|
|
});
|
|
});
|