import type { AssessorRepository } from '../../src/persistence/assessor-repository'; import { RepositoryNotFoundError } from '../../src/persistence/repository-errors'; import { inferGpoEnvironmentContexts } from './gpo-environment-context'; import { GpoPolicySnapshotService } from './gpo-policy-snapshot-service'; import { GpoPolicyEvaluationService } from './gpo-policy-evaluation-service'; import { GpoPolicyFindingService } from './gpo-policy-finding-service'; import { buildGpoAnalysisSnapshots } from '../../src/gpo/snapshot-builder'; export type AutomaticGpoAnalysisResult = { state: 'context-required' | 'baseline-required' | 'review' | 'evaluated'; evaluationRunId?: string; diagnosticCode?: string; }; /** One uploaded backup set, one timeslot. Never combines unrelated historical scans. */ export class GpoAutomaticAnalysisService { private readonly active = new Map>(); constructor(private readonly repository: AssessorRepository, private readonly snapshots: GpoPolicySnapshotService, private readonly evaluations: GpoPolicyEvaluationService, private readonly findings: GpoPolicyFindingService) {} run(projectId: string, processingRunId: string): Promise { const key = `${projectId}:${processingRunId}`; const existing = this.active.get(key); if (existing) return existing; const operation = this.execute(projectId, processingRunId).finally(() => this.active.delete(key)); this.active.set(key, operation); return operation; } async reprocessProject(projectId: string): Promise<{ evaluated: number; review: number; failed: number }> { let evaluated = 0; let review = 0; let failed = 0; const analyses = await this.repository.listGpoPolicyAnalyses(projectId); const superseded = new Set(analyses.flatMap(item => item.run.supersedesAnalysisRunId ? [item.run.supersedesAnalysisRunId] : [])); for (const analysis of analyses.filter(item => !superseded.has(item.run.id))) { try { const result = await this.run(projectId, analysis.run.sourceProcessingRunId); if (result.state === 'evaluated') evaluated += 1; else review += 1; } catch { failed += 1; } } return { evaluated, review, failed }; } private async execute(projectId: string, processingRunId: string): Promise { const analysis = (await this.repository.listGpoPolicyAnalyses(projectId)) .find(record => record.run.sourceProcessingRunId === processingRunId); if (!analysis) throw new RepositoryNotFoundError('GPO analysis missing.'); const artifact = (await this.repository.listImportArtifacts(projectId)).find(record => record.id === analysis.run.artifactId); if (!artifact?.assessmentId) return { state: 'context-required' }; const environment = await this.repository.getEnvironmentSnapshot({ projectId, assessmentId: artifact.assessmentId }); const observedContexts = inferGpoEnvironmentContexts(environment.observations); const contexts = [...observedContexts, { id: 'gpo-baseline-comparison', kind: 'operating-system' as const, productFamily: 'Group Policy', productVersion: 'All imported baselines', role: 'Any', scope: 'mixed' as const }].filter((context, index, all) => all.findIndex(candidate => candidate.id === context.id) === index); const instances = (await this.repository.listGpoBackupInstances(projectId)) .filter(item => item.processingRunId === processingRunId); const snapshotCandidates = buildGpoAnalysisSnapshots(instances.map(item => item.instance), { createdAt: analysis.run.completedAt }); if (snapshotCandidates.length !== 1) return { state: 'review', diagnosticCode: 'policy-aggregation-multiple-snapshots' }; const profile = await this.snapshots.createEnvironmentProfile(projectId, { name: `Detected environment ยท ${artifact.assessmentId}`, contexts }); const baselines = await this.repository.listMicrosoftSecurityBaselines(); const eligibleBaselineIds = new Set(baselines.filter(record => record.status === 'eligible').map(record => record.id)); if (!eligibleBaselineIds.size) return { state: 'baseline-required' }; const analyses = await this.repository.listMicrosoftSecurityBaselineAnalyses(); const superseded = new Set(analyses.flatMap(item => item.supersedesAnalysisId ? [item.supersedesAnalysisId] : [])); const baselineAnalysisIds = analyses.filter(item => eligibleBaselineIds.has(item.analysis.baselineId) && !superseded.has(item.analysis.id)) .map(item => item.analysis.id).sort(); if (!baselineAnalysisIds.length) return { state: 'baseline-required' }; const aggregation = await this.snapshots.aggregate(projectId, { environmentProfileId: profile.id, sourceAnalysisRunIds: [analysis.run.id] }); const evaluation = await this.evaluations.evaluate(projectId, { aggregationRunId: aggregation.run.id, baselineAnalysisIds: [...new Set(baselineAnalysisIds)].sort() }); await this.findings.create(projectId, { evaluationRunId: evaluation.run.id }); return { state: evaluation.run.status === 'complete' ? 'evaluated' : 'review', evaluationRunId: evaluation.run.id }; } }