90 lines
5.2 KiB
TypeScript
90 lines
5.2 KiB
TypeScript
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<string, Promise<AutomaticGpoAnalysisResult>>();
|
|
constructor(private readonly repository: AssessorRepository,
|
|
private readonly snapshots: GpoPolicySnapshotService,
|
|
private readonly evaluations: GpoPolicyEvaluationService,
|
|
private readonly findings: GpoPolicyFindingService) {}
|
|
|
|
run(projectId: string, processingRunId: string): Promise<AutomaticGpoAnalysisResult> {
|
|
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<AutomaticGpoAnalysisResult> {
|
|
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 };
|
|
}
|
|
}
|