141 lines
9.2 KiB
TypeScript
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 };
|
|
}
|