Assessor_2.0/src/features/reports/report-input-assembler.ts

141 lines
9.2 KiB
TypeScript

import type { AssessorRepository } from '../../persistence/assessor-repository';
import type { ReportDiagnostic, ReportProjectionInput, ReportProvenanceEntry, ReportScoreSource } from '../../domain/report-projection';
import { loadCanonicalProjectWorkspace } from '../project-import/load-canonical-project-workspace';
import { queryAllUnifiedResults } from '../../exports/unified-result-jsonl';
import { hydrateWorkspaceReviews } from '../review-workspace/database-review';
import { dashboardScoreFromStates, type DashboardResultState } from '../dashboard/dashboard-result-state';
import { securitySourceScores, weightedSecurityScore } from '../../domain/security-scoring';
import { securityToolId } from '../../scoring/canonical-security-score';
import { clientReportInput } from '../dashboard/client-report-projection';
export interface ReportAssemblyScope {
projectId: string;
assessmentId?: string;
assessmentIds?: string[];
scanRunIds?: string[];
generatedAt: string;
}
/** Repository-only, export-time assembly. No renderer derives a verdict or reads storage. */
export async function assembleReportProjectionInput(
repository: AssessorRepository,
scope: ReportAssemblyScope
): Promise<ReportProjectionInput> {
const project = await repository.getProject(scope.projectId);
if (!project) throw new Error('Project not found.');
const assessmentIds = scope.assessmentIds ?? (scope.assessmentId ? [scope.assessmentId] : undefined);
const workspace = await loadCanonicalProjectWorkspace({
listProjectAssessments: projectId => repository.listAssessments(projectId),
loadAssessmentModel: async assessmentId => {
const model = await repository.loadAssessmentModel(assessmentId);
if (!model) throw new Error(`Assessment ${assessmentId} not found.`);
return model;
},
queryUnifiedResults: query => repository.queryUnifiedResults(query),
listPolicyEvaluations: async projectId => ({ records: await repository.listPolicyEvaluations(projectId) }),
listMicrosoftSecurityBaselineAnalyses: async baselineId => ({
records: await repository.listMicrosoftSecurityBaselineAnalyses(baselineId)
})
}, scope.projectId, scope.scanRunIds, assessmentIds);
if (!workspace.dataset) throw new Error('No reportable assessment data in the selected scope.');
const [results, snapshot, storedReviews, processing, analyses, aggregations, evaluations] = await Promise.all([
queryAllUnifiedResults(query => repository.queryUnifiedResults(query), {
audience: 'internal-analysis', projectId: scope.projectId,
assessmentIds: workspace.assessmentIds, scanRunIds: scope.scanRunIds,
sourceKind: ['scanner-finding', 'policy-evaluation']
}),
repository.getEnvironmentSnapshot({ projectId: scope.projectId, assessmentId: scope.assessmentId, scanRunIds: scope.scanRunIds }),
repository.listReviews(scope.projectId),
repository.listProjectProcessingRuns(scope.projectId),
repository.listGpoPolicyAnalyses(scope.projectId),
repository.listPolicySnapshotAggregations(scope.projectId),
repository.listPolicyEvaluations(scope.projectId)
]);
const dataset = workspace.dataset;
const selectedObservationIds = new Set(results.filter(result => result.sourceKind === 'policy-observation')
.map(result => result.sourceResultId));
const scopedAnalyses = analyses.map(record => ({
...record, resolutions: record.resolutions.filter(item => selectedObservationIds.has(item.observationId))
})).filter(record => record.resolutions.length > 0);
const selectedEvaluationRunIds = new Set(results.flatMap(result => result.evaluationRunId ? [result.evaluationRunId] : []));
const scopedEvaluations = evaluations.filter(record => selectedEvaluationRunIds.has(record.run.id)
|| (selectedObservationIds.size > 0 && record.results.length > 0
&& record.results.some(result => result.observationIds.some(id => selectedObservationIds.has(id)))
&& record.results.every(result => result.observationIds.every(id => selectedObservationIds.has(id)))));
const selectedAggregationRunIds = new Set(scopedEvaluations.map(record => record.run.aggregationRunId));
const scopedAggregations = aggregations.filter(record => selectedAggregationRunIds.has(record.run.id));
const selectedArtifactIds = new Set(results.flatMap(result => result.importArtifactId ? [result.importArtifactId] : []));
const scopedProcessing = processing.filter(run => selectedArtifactIds.has(run.artifactId));
const reviews = hydrateWorkspaceReviews(dataset, storedReviews, {}).reviews;
const preliminary = clientReportInput({
project, assessmentIds: workspace.assessmentIds, generatedAt: scope.generatedAt,
results, dataset, environment: snapshot, reviews
});
const ratingByTool = new Map(securitySourceScores(dataset.securityScoring?.scans ?? []).map(item => [item.toolId, item]));
const resultsByTool = new Map<string, typeof results>();
results.forEach(result => {
const toolId = securityToolId(result.sourceTool ?? 'unknown');
const group = resultsByTool.get(toolId) ?? [];
group.push(result);
resultsByTool.set(toolId, group);
});
for (const toolId of ratingByTool.keys()) if (!resultsByTool.has(toolId)) resultsByTool.set(toolId, []);
const scoreSources: ReportScoreSource[] = [...resultsByTool].map(([toolId, items]) => {
const rating = ratingByTool.get(toolId);
const states: DashboardResultState[] = toolId === 'group-policy'
? (preliminary.policyEntries ?? []).map(entry => entry.verdict === 'Pass' ? 'passed'
: entry.verdict === 'Fail' ? 'deviation' : 'unknown')
: items.map(item => item.outcomeNormalized === 'pass' ? 'passed'
: item.isDeviation ? 'deviation'
: item.outcomeNormalized === 'not_run' ? 'not-run'
: item.outcomeNormalized === 'not_applicable' ? 'not-applicable' : 'unknown');
const counted = dashboardScoreFromStates(states);
return {
toolId, score: rating?.score ?? (rating ? undefined : counted.score),
method: rating?.methods.length === 1 ? rating.methods[0]
: rating ? 'unavailable' : counted.assessed ? 'pass-rate' : 'unavailable',
weight: rating?.weight ?? dataset.securityScoring?.policy?.weights[toolId]
?? dataset.securityScoring?.policy?.defaultWeight ?? 1,
nativeValue: rating?.scans.length === 1 ? rating.scans[0].nativeValue : undefined,
nativeProperty: rating?.scans.length === 1 ? rating.scans[0].nativeProperty : undefined,
passed: counted.passed, deviations: counted.deviations, assessed: counted.assessed,
observedAt: rating?.scans.map(scan => scan.observedAt).filter((date): date is string => Boolean(date)).sort().at(-1),
issues: rating?.issues ?? []
};
});
const input = { ...preliminary, scoreSources, overallScore: weightedSecurityScore(scoreSources) };
const diagnosticMap = new Map<string, ReportDiagnostic>();
const addDiagnostic = (code: string, origin: ReportDiagnostic['origin'], reason?: string) => {
const key = `${origin}\u0000${code}\u0000${reason ?? ''}`;
const previous = diagnosticMap.get(key);
diagnosticMap.set(key, { code, origin, count: (previous?.count ?? 0) + 1, reason });
};
workspace.models.forEach(model => model.diagnostics.forEach(item => addDiagnostic(item.code, 'import', item.message)));
scopedProcessing.forEach(run => run.diagnosticCodes.forEach(code => addDiagnostic(code, 'import')));
const seenResolverObservations = new Set<string>();
scopedAnalyses.forEach(record => record.resolutions.forEach(item => {
if (item.resolution.status !== 'unresolved' || seenResolverObservations.has(item.observationId)) return;
seenResolverObservations.add(item.observationId);
addDiagnostic(item.resolution.reasonCode, 'analysis', item.resolution.reasonCode);
}));
scopedAggregations.forEach(record => record.run.diagnosticCodes.forEach(code => addDiagnostic(code, 'aggregation')));
scopedEvaluations.forEach(record => record.run.diagnosticCodes.forEach(code => addDiagnostic(code, 'evaluation')));
const provenance: ReportProvenanceEntry[] = [
...input.provenance ?? [],
...workspace.models.flatMap(model => model.importFiles.map(file => ({
kind: `ImportFile: ${file.fileName}`, id: file.id,
version: [file.processedWith.profileVersion, file.processedWith.mappingVersion,
file.processedWith.normalizationVersion].filter(Boolean).join(' / '), observedAt: file.importedAt
}))),
...scopedProcessing.map(run => ({ kind: `ProcessingRun: ${run.status}`, id: run.id,
version: run.processors.map(item => `${item.id}@${item.version}`).join(', '), observedAt: run.completedAt })),
...scopedAnalyses.map(record => ({ kind: `PolicyAnalysisRun: ${record.run.status}`, id: record.run.id,
version: `${record.run.analyzer.id}@${record.run.analyzer.version}; ${record.run.identityResolver.id}@${record.run.identityResolver.version}`,
observedAt: record.run.completedAt })),
...scopedAggregations.map(record => ({ kind: 'AggregationRun', id: record.run.id, observedAt: record.run.completedAt })),
...scopedEvaluations.map(record => ({ kind: `EvaluationRun: ${record.run.status}`, id: record.run.id,
version: record.run.evaluator.version, observedAt: record.run.completedAt }))
];
return { ...input, diagnostics: [...diagnosticMap.values()], provenance };
}