109 lines
6.0 KiB
TypeScript
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;
|
|
}
|