134 lines
7.5 KiB
TypeScript
134 lines
7.5 KiB
TypeScript
import type { AssessmentProject } from '../domain/canonical-model';
|
|
import type { ImportedDataset } from '../domain/dataset';
|
|
import type { EnvironmentSnapshot } from '../domain/environment-context';
|
|
import type { FindingRecord } from '../domain/finding';
|
|
import type { Review } from '../domain/review';
|
|
import {
|
|
buildReportProjection, reportPriorityBases,
|
|
type ReportPolicyEntry, type ReportProjection, type ReportProjectionInput, type ReportScoreSource
|
|
} from './report-projection';
|
|
import type { UnifiedResultProjection } from '../domain/unified-result';
|
|
import { toolProfileDefinitions } from '../profiles/profile-definition-catalog';
|
|
import { securityDomainCatalog, securityDomainIdForRecord, securityDomainIdForUnifiedResult } from '../evaluation/security-domains';
|
|
import { summarizeWorkspaceResults } from '../features/project-import/datahub-scan-analysis';
|
|
import { profileForSheet } from '../features/project-import/workspace-profile';
|
|
import { dashboardResultState } from '../features/dashboard/dashboard-result-state';
|
|
import { managementWorkspaceDeviationKeys } from './management-business-impact';
|
|
|
|
function field(record: FindingRecord, label: string): string {
|
|
return record.fields.find(item => item.label.toLocaleLowerCase('en-US') === label.toLocaleLowerCase('en-US'))?.value?.trim() ?? '';
|
|
}
|
|
|
|
function policyEntry(record: FindingRecord): ReportPolicyEntry | undefined {
|
|
if (field(record, 'Category') !== 'GPOs') return undefined;
|
|
const status = field(record, 'Status');
|
|
const verdict = status === 'Deviation' ? 'Fail' : status === 'Passed' ? 'Pass' : status;
|
|
if (!['Fail', 'Pass', 'Not configured', 'No baseline rule', 'Not comparable', 'Review required'].includes(verdict)) return undefined;
|
|
return {
|
|
id: record.id,
|
|
verdict: verdict as ReportPolicyEntry['verdict'],
|
|
conflict: Boolean(field(record, 'Conflict')),
|
|
duplicate: Boolean(field(record, 'Duplicates')),
|
|
title: record.title,
|
|
scope: field(record, 'Scope'),
|
|
identity: field(record, 'Technical Identity'),
|
|
backupInstanceIds: [...new Set((record.policySourceEvidence ?? []).flatMap(source => source.backupInstanceId ? [source.backupInstanceId] : []))]
|
|
};
|
|
}
|
|
|
|
export interface ClientReportProjectionInput {
|
|
project: AssessmentProject;
|
|
assessmentIds?: string[];
|
|
generatedAt: string;
|
|
results: UnifiedResultProjection[];
|
|
dataset: ImportedDataset;
|
|
workspaceDataset?: ImportedDataset;
|
|
environment?: EnvironmentSnapshot;
|
|
overallScore?: number;
|
|
scoreSources?: ReportScoreSource[];
|
|
reviews?: Record<string, Review>;
|
|
}
|
|
|
|
/** One frozen, filter-independent cut for both HTML files. */
|
|
export function clientReportInput(input: ClientReportProjectionInput): ReportProjectionInput {
|
|
const projectSheet = input.dataset.sheets.find(sheet => sheet.isProjectSheet);
|
|
const countDataset = input.workspaceDataset ?? input.dataset;
|
|
const countSheet = countDataset.sheets.find(sheet => sheet.isProjectSheet);
|
|
const counts = countSheet ? summarizeWorkspaceResults(countSheet.analysis.records, profileForSheet(countDataset, countSheet)) : undefined;
|
|
const deviationKeys = countSheet ? managementWorkspaceDeviationKeys(countSheet.analysis.records, record =>
|
|
dashboardResultState(record, profileForSheet(countDataset, countSheet)) === 'deviation') : undefined;
|
|
const policyRecords = (projectSheet?.analysis.records ?? []).filter(record => field(record, 'Category') === 'GPOs');
|
|
const policyEntries = policyRecords.flatMap(record => {
|
|
const entry = policyEntry(record);
|
|
return entry ? [entry] : [];
|
|
});
|
|
const workRecordByOccurrence = new Map((projectSheet?.analysis.records ?? [])
|
|
.flatMap(record => record.findingOccurrenceId ? [[record.findingOccurrenceId, record] as const] : []));
|
|
const policyRecordByIdentity = new Map(policyRecords
|
|
.map(record => [`${field(record, 'Scope')}|${field(record, 'Technical Identity')}`, record] as const));
|
|
const reviewInputs = input.results.flatMap(result => {
|
|
const record = workRecordByOccurrence.get(result.sourceResultId);
|
|
const review = record && input.reviews?.[record.id];
|
|
if (!review) return [];
|
|
return [{
|
|
resultProjectionId: result.resultProjectionId,
|
|
include: review.include, status: review.status, priority: review.priority,
|
|
owner: review.owner || undefined, due: review.due || undefined,
|
|
prerequisites: review.prerequisites || undefined, access: review.access || undefined,
|
|
nextStep: review.nextStep || undefined,
|
|
markers: [review.markerQuickwin && 'quickwin', review.markerBlocked && 'blocked',
|
|
review.markerExternal && 'external', review.markerQuestion && 'question',
|
|
review.markerNotRelevant && 'notRelevant'].filter((value): value is string => Boolean(value)),
|
|
riskAccepted: Boolean(review.riskAcceptance)
|
|
}];
|
|
});
|
|
const environment = [
|
|
['Domain', 'domain.fqdn'], ['Forest', 'forest.name'], ['Domain Controller', 'domain.controller_count']
|
|
].map(([key, property]) => ({
|
|
key,
|
|
value: input.environment?.observations.find(item => item.property === property && item.state === 'observed')?.originalValue?.toString()
|
|
}));
|
|
const provenance = input.results.flatMap(result => [
|
|
{ kind: 'ScanRun', id: result.scanRunId ?? result.collectionRunId, observedAt: result.sourceObservedAt },
|
|
...(result.importArtifactId ? [{ kind: 'ImportArtifact', id: result.importArtifactId }] : []),
|
|
...(result.evaluationRunId ? [{ kind: 'EvaluationRun', id: result.evaluationRunId }] : []),
|
|
...(result.sourceProfileVersion ? [{ kind: `${result.sourceTool ?? 'Source'} profile`, id: result.sourceProfileId ?? 'unknown', version: result.sourceProfileVersion }] : []),
|
|
...(result.mappingRuleVersion ? [{ kind: 'Mapping', id: result.sourceTool ?? 'unknown', version: result.mappingRuleVersion }] : [])
|
|
]);
|
|
const uniqueProvenance = [...new Map(provenance.map(item => [`${item.kind}\u0000${item.id}\u0000${item.version ?? ''}`, item])).values()];
|
|
return {
|
|
generatedAt: input.generatedAt,
|
|
project: { id: input.project.id, name: input.project.name },
|
|
assessmentId: input.assessmentIds?.length === 1 ? input.assessmentIds[0] : undefined,
|
|
results: input.results,
|
|
domains: securityDomainCatalog.map(({ id, label }) => ({ id, label })),
|
|
// The same classifier and Raw Data record as Work, so a check has one domain everywhere.
|
|
domainOf: result => {
|
|
const record = result.sourceKind.startsWith('policy-')
|
|
? policyRecordByIdentity.get(`${result.policyScope ?? ''}|${result.policyIdentityKey ?? ''}`)
|
|
: workRecordByOccurrence.get(result.sourceResultId);
|
|
return record ? securityDomainIdForRecord(record) : securityDomainIdForUnifiedResult(result);
|
|
},
|
|
priorityBases: reportPriorityBases(toolProfileDefinitions),
|
|
overallScore: input.overallScore,
|
|
scoreSources: input.scoreSources,
|
|
workspaceCounts: counts ? { deviations: counts.deviation, assessed: counts.deviation + counts.passed } : undefined,
|
|
workspaceDeviationKeys: deviationKeys,
|
|
environment,
|
|
policyEntries,
|
|
reviews: reviewInputs,
|
|
technicalPolicySources: policyRecords.map(record => ({ settingId: record.id, sources: record.policySourceEvidence ?? [] })),
|
|
technicalFindingFields: input.results.flatMap(result => {
|
|
const record = workRecordByOccurrence.get(result.sourceResultId);
|
|
return record ? [{ resultProjectionId: result.resultProjectionId, fields: record.fields }] : [];
|
|
}),
|
|
policyScopeLimited: true,
|
|
provenance: uniqueProvenance,
|
|
priorityLimitPerDomain: 8
|
|
};
|
|
}
|
|
|
|
export function buildClientReportProjection(input: ClientReportProjectionInput): ReportProjection {
|
|
return buildReportProjection(clientReportInput(input));
|
|
}
|