Assessor_2.0/server/timeslot-comparison-input.ts

109 lines
6.0 KiB
TypeScript

import type { Assessment, CanonicalAssessmentModel } from '../src/domain/canonical-model';
import type { StoredGpoBackupInstance } from '../src/domain/gpo-analysis-persistence';
import type { StoredPolicyEvaluation } from '../src/domain/policy-evaluation';
import type { PolicyAnalysisOverview } from '../src/domain/policy-analysis-overview';
import type { UnifiedResultQuery, UnifiedResultQueryResult } from '../src/domain/unified-result';
import { queryAllUnifiedResults } from '../src/exports/unified-result-jsonl';
import { buildPolicyWorkResults, type PolicyWorkResult } from '../src/evaluation/policy-work-results';
import type { ImportedDataset } from '../src/domain/dataset';
import { canonicalModelForScanRuns, currentPolicyEvaluationsForWork, loadCanonicalProjectWorkspace } from '../src/features/project-import/load-canonical-project-workspace';
import { RepositoryNotFoundError } from '../src/persistence/repository-errors';
/** One timeslot as Data Hub groups it: its assessments and the scanner runs selected inside them. */
export interface TimeslotSelection {
assessmentIds: string[];
scanRunIds: string[];
}
export interface TimeslotInputReader {
listAssessments(projectId: string): Promise<Assessment[]>;
loadAssessmentModel(assessmentId: string): Promise<CanonicalAssessmentModel | null>;
queryUnifiedResults(query?: UnifiedResultQuery): Promise<UnifiedResultQueryResult>;
listPolicyEvaluations(projectId: string): Promise<StoredPolicyEvaluation[]>;
listGpoBackupInstances(projectId: string): Promise<StoredGpoBackupInstance[]>;
getPolicyAnalysisOverview?(projectId: string): Promise<Pick<PolicyAnalysisOverview, 'current'>>;
}
export interface TimeslotComparisonInput {
assessments: Assessment[];
models: CanonicalAssessmentModel[];
scanRunIds: string[];
gpoResults: PolicyWorkResult[];
/** Latest GPO backup creation time; undefined unless every contributing backup states one. */
gpoObservedAt?: string;
}
/**
* Core-owned adapter: builds one timeslot exactly as Work reads it (scanner models scoped to the
* selected runs, GPO settings through the same current-evaluation selection and Work verdict
* builder). Only the resulting records reach the Comparison Engine. Import times are never used as
* observation times.
*/
export async function loadTimeslotComparisonInput(
reader: TimeslotInputReader,
projectId: string,
selection: TimeslotSelection
): Promise<TimeslotComparisonInput> {
const projectAssessments = await reader.listAssessments(projectId);
const assessments = selection.assessmentIds.map(id => {
const assessment = projectAssessments.find(item => item.id === id);
if (!assessment) throw new RepositoryNotFoundError(`Assessment ${id} is not in this project.`);
return assessment;
});
const loaded = await Promise.all(assessments.map(async assessment => {
const model = await reader.loadAssessmentModel(assessment.id);
if (!model || model.project.id !== projectId) throw new RepositoryNotFoundError(`Assessment ${assessment.id} is not in this project.`);
return model;
}));
const models = loaded.map(model => canonicalModelForScanRuns(model, selection.scanRunIds))
.filter((model): model is CanonicalAssessmentModel => model !== null);
const availableRuns = new Set(models.flatMap(model => model.scanRuns.map(run => run.id)));
const missingRun = selection.scanRunIds.find(id => !availableRuns.has(id));
if (missingRun) throw new RepositoryNotFoundError(`Scan run ${missingRun} is not in the selected assessments.`);
const observations = await queryAllUnifiedResults(query => reader.queryUnifiedResults(query), {
audience: 'internal-analysis', projectId, assessmentIds: selection.assessmentIds, sourceKind: ['policy-observation']
});
let gpoResults: PolicyWorkResult[] = [];
let gpoObservedAt: string | undefined;
if (observations.length) {
const [evaluations, overview, backups] = await Promise.all([
reader.listPolicyEvaluations(projectId),
reader.getPolicyAnalysisOverview?.(projectId),
reader.listGpoBackupInstances(projectId)
]);
gpoResults = buildPolicyWorkResults(observations, currentPolicyEvaluationsForWork(evaluations,
new Set(observations.map(item => item.sourceResultId)), overview ? new Set(overview.current.evaluationRunIds) : undefined));
const backupIds = new Set(observations.flatMap(item => item.policyBackupInstanceId ?? []));
const times = backups.filter(item => backupIds.has(item.instance.id)).map(item => item.instance.backupCreatedAt);
if (backupIds.size && times.length === backupIds.size && times.every(time => time && Number.isFinite(Date.parse(time)))) {
gpoObservedAt = new Date(Math.max(...times.map(time => Date.parse(time!)))).toISOString();
}
}
return { assessments, models, scanRunIds: [...selection.scanRunIds], gpoResults, ...(gpoObservedAt ? { gpoObservedAt } : {}) };
}
/**
* Core-owned adapter: the Work dataset of one timeslot, built by the same loader Client Info uses, so the
* Comparison Engine reads Client Info's records and result states for its Security Domain values.
*/
export async function loadTimeslotWorkspace(
reader: TimeslotInputReader,
projectId: string,
selection: TimeslotSelection
): Promise<ImportedDataset | null> {
const { dataset } = await loadCanonicalProjectWorkspace({
listProjectAssessments: id => reader.listAssessments(id),
loadAssessmentModel: async assessmentId => {
const model = await reader.loadAssessmentModel(assessmentId);
if (!model || model.project.id !== projectId) throw new RepositoryNotFoundError(`Assessment ${assessmentId} is not in this project.`);
return model;
},
queryUnifiedResults: query => reader.queryUnifiedResults(query),
listPolicyEvaluations: async id => ({ records: await reader.listPolicyEvaluations(id) }),
...(reader.getPolicyAnalysisOverview ? { getPolicyAnalysisOverview: (id: string) => reader.getPolicyAnalysisOverview!(id) } : {})
}, projectId, selection.scanRunIds.length ? selection.scanRunIds : undefined, selection.assessmentIds,
{ includeHistoricalScores: false, includeBaselineGuidance: false });
return dataset;
}