220 lines
13 KiB
TypeScript
220 lines
13 KiB
TypeScript
import type { ImportedDataset } from '../../domain/dataset';
|
|
import type { UnifiedResultProjection } from '../../domain/unified-result';
|
|
import { POLICY_EVALUATION_ENGINE, type StoredPolicyEvaluation } from '../../domain/policy-evaluation';
|
|
import { buildPolicyWorkResults } from '../../evaluation/policy-work-results';
|
|
import { baselineVariantSummary } from '../../evaluation/policy-baseline-variants';
|
|
import { rowsToCsv } from '../../exports/csv-export';
|
|
import { makeSheet } from '../../importers/sheet-factory';
|
|
import { buildPolicyGuidance, type PolicyGuidanceMap } from '../../profiles/group-policy/guidance';
|
|
import { formatPolicySources } from '../../profiles/group-policy/source-presentation';
|
|
import { policySourceEvidence } from '../../profiles/group-policy/source-evidence';
|
|
import { policyBackupImpactSources } from './policy-backup-impact-sources';
|
|
import { rollupPolicyWorkRows } from './policy-work-row-rollup';
|
|
import { stableRecordId } from '../../shared/identifiers';
|
|
import { buildPolicyConflictPresentation, buildPolicyDuplicatePresentation, serializePolicyConflictPresentation, POLICY_CONFLICT_DETAILS, POLICY_DUPLICATE_DETAILS } from '../../profiles/group-policy/conflict-presentation';
|
|
|
|
function display(value: unknown): string {
|
|
if (value === undefined) return '';
|
|
if (value === null) return 'null';
|
|
return typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
}
|
|
|
|
const preferenceCseLabels: Record<string, string> = {
|
|
registry: 'registry', 'local-users-groups': 'local users and groups',
|
|
services: 'services', 'drive-maps': 'drive maps'
|
|
};
|
|
|
|
/**
|
|
* A Group Policy Preference has no comparable technical policy setting. The retained
|
|
* action and item-level targeting decide whether and how it applies, so both stay visible
|
|
* instead of an implied baseline comparison.
|
|
*/
|
|
function notComparableSummary(sources: UnifiedResultProjection[]): string {
|
|
const value = <T,>(pick: (source: UnifiedResultProjection) => T | undefined) =>
|
|
[...new Set(sources.flatMap(source => pick(source) !== undefined ? [String(pick(source))] : []))].sort();
|
|
const cse = value(source => source.policyPreferenceCse).map(item => preferenceCseLabels[item] ?? item);
|
|
const action = value(source => source.policyPreferenceAction);
|
|
const targeting = value(source => source.policyPreferenceTargeting);
|
|
return [
|
|
cse.length ? `Group Policy Preference (${cse.join(', ')})` : 'Group Policy Preference',
|
|
'no comparable technical policy setting, so it is not compared with a Microsoft baseline',
|
|
action.length ? `action: ${action.join(', ')}` : '',
|
|
targeting.includes('present') ? 'item-level targeting: present, effective application unknown'
|
|
: targeting.length ? 'item-level targeting: none' : ''
|
|
].filter(Boolean).join('. ') + '.';
|
|
}
|
|
|
|
export function appendPolicyObservationsToDataset(
|
|
dataset: ImportedDataset | null,
|
|
projectName: string,
|
|
results: UnifiedResultProjection[],
|
|
evaluations: StoredPolicyEvaluation[] = [],
|
|
guidance: PolicyGuidanceMap = buildPolicyGuidance(evaluations)
|
|
): ImportedDataset | null {
|
|
const entries = buildPolicyWorkResults(results, evaluations);
|
|
if (!entries.length) return dataset;
|
|
const previousVersions = new Map(evaluations.filter(evaluation => evaluation.run.evaluator?.version
|
|
&& evaluation.run.evaluator.version !== POLICY_EVALUATION_ENGINE.version)
|
|
.flatMap(evaluation => evaluation.results.map(result => [result.id, evaluation.run.evaluator.version] as const)));
|
|
const headers = [
|
|
'Title', 'Category', 'Status', 'Scope', 'Configured Value', 'Baseline value', 'Baseline',
|
|
'Comparison', 'Registry path', 'Sources', 'Technical Identity', 'Imported', 'Diagnostics', 'Policy type', 'Policy section',
|
|
'Description', 'Remediation', 'Description source', 'Conflict', POLICY_CONFLICT_DETAILS, 'Duplicates', POLICY_DUPLICATE_DETAILS,
|
|
'Evaluated', 'Source: Scan', 'Evaluation readiness', 'Historical result'
|
|
];
|
|
const policyEntries = rollupPolicyWorkRows(entries.map(entry => {
|
|
const comparison = entry.comparison;
|
|
const previousVersion = comparison && previousVersions.get(comparison.id);
|
|
const readiness = previousVersion
|
|
? `Re-evaluation required: historical result from engine ${previousVersion}; current engine is ${POLICY_EVALUATION_ENGINE.version}. Re-evaluate in Data Hub before using this result as a current assessment.`
|
|
: '';
|
|
const conflict = buildPolicyConflictPresentation(entry);
|
|
const duplicate = buildPolicyDuplicatePresentation(entry);
|
|
const configured = comparison?.comparator.actual
|
|
?? (comparison?.customerState === 'missing' ? 'Not configured'
|
|
: entry.sources.flatMap(source => source.policyValues ?? []));
|
|
const target = comparison?.identity.target;
|
|
const rawRegistry = entry.sources.flatMap(source => source.policyValues ?? [])
|
|
.find(value => value && typeof value === 'object' && !Array.isArray(value) && value.kind === 'registry-policy');
|
|
const registryPath = target && 'hive' in target && target.hive ? `${target.hive}\\${target.key}`
|
|
: rawRegistry && typeof rawRegistry === 'object' && !Array.isArray(rawRegistry)
|
|
? `${entry.scope === 'user' ? 'HKEY_CURRENT_USER' : 'HKEY_LOCAL_MACHINE'}\\${rawRegistry.key}` : '';
|
|
return {
|
|
entry,
|
|
historical: Boolean(previousVersion),
|
|
importedAt: entry.importedAt,
|
|
failingBackupSources: policyBackupImpactSources(entry),
|
|
sourceEvidence: policySourceEvidence(entry.sources,
|
|
previousVersion ? undefined : comparison?.baselineVariantAssessment),
|
|
row: [
|
|
entry.title, 'GPOs', previousVersion ? 'Review required' : entry.status, entry.scope, display(configured),
|
|
display(comparison?.comparator.baseline ?? comparison?.baselineVariantAssessment?.variants.map(variant => ({
|
|
settingId: variant.settingId, value: variant.comparator.baseline, relation: variant.comparator.relation
|
|
}))), comparison?.baselineLabel ?? '',
|
|
previousVersion ? 'Historical comparison — re-evaluation required'
|
|
: comparison?.baselineVariantAssessment ? baselineVariantSummary(comparison.baselineVariantAssessment)
|
|
: comparison?.baselineTemplateSelection?.status === 'no-match'
|
|
? 'No baseline template matches the specified comparison context'
|
|
: comparison?.diagnosticCodes.includes('policy-evaluation-template-context-required')
|
|
? 'Different template recommendations; customer target role not established'
|
|
: comparison?.diagnosticCodes.includes('policy-evaluation-baseline-consensus-conflict')
|
|
? 'Conflicting baseline recommendations'
|
|
: entry.status === 'Not comparable' ? notComparableSummary(entry.sources)
|
|
: entry.status === 'No baseline rule'
|
|
? 'No applicable Microsoft baseline rule for this technical setting; retained for inspection without a verdict'
|
|
: entry.conflict || entry.status === 'Conflicts' ? 'Different values or types for the same setting'
|
|
: entry.duplicate || entry.status === 'Duplicates' ? 'Identical setting in multiple source entries'
|
|
: comparison?.customerState === 'missing' ? 'Baseline setting is not configured'
|
|
: entry.status === 'Pass' ? 'Matches baseline' : 'Differs from baseline',
|
|
registryPath,
|
|
formatPolicySources(entry.sources),
|
|
entry.identity, entry.importedAt,
|
|
[...new Set([...(comparison?.diagnosticCodes ?? []),
|
|
...entry.sources.flatMap(source => source.policyDiagnosticCodes ?? [])])].join(', '),
|
|
target?.kind ?? entry.sources[0]?.policyDeliveryMechanism ?? '',
|
|
target && 'section' in target ? target.section
|
|
: preferenceCseLabels[entry.sources[0]?.policyPreferenceCse ?? ''] ?? entry.sources[0]?.policyPreferenceCse ?? '',
|
|
comparison ? guidance.get(comparison.id)?.description || (guidance.get(comparison.id)?.descriptionLoadFailed
|
|
? 'Baseline description could not be loaded. The saved comparison is retained.'
|
|
: 'Description unavailable in the referenced baseline metadata.') : '',
|
|
comparison ? guidance.get(comparison.id)?.remediation ?? '' : '',
|
|
comparison ? guidance.get(comparison.id)?.source ?? '' : '',
|
|
conflict?.summary ?? (comparison?.baselineVariantAssessment && entry.conflict ? 'Conflicting customer states; see original GPO source instructions.' : ''), conflict ? serializePolicyConflictPresentation(conflict) : '',
|
|
duplicate?.summary ?? (comparison?.baselineVariantAssessment && entry.duplicate ? 'Same setting contributed by multiple GPO backups.' : ''), duplicate ? serializePolicyConflictPresentation(duplicate) : '',
|
|
entry.evaluatedAt ?? '', entry.importedAt ? `GPOs · ${entry.importedAt}` : 'GPOs', readiness,
|
|
previousVersion ? `${entry.status} · Engine ${previousVersion}` : ''
|
|
]
|
|
};
|
|
}), headers);
|
|
const policyRows = policyEntries.map(entry => entry.row);
|
|
const rows = [headers, ...policyRows];
|
|
const sheet = makeSheet(
|
|
'GPOs',
|
|
rows,
|
|
rowsToCsv(rows),
|
|
projectName,
|
|
{ id: `gpo-policy-observations-${projectName}`, sourceFile: projectName, sourceSheet: 'GPOs', toolId: 'group-policy' }
|
|
);
|
|
sheet.analysis.records.forEach((record, index) => {
|
|
record.policyFailingBackupSources = policyEntries[index].failingBackupSources;
|
|
record.policySourceEvidence = policyEntries[index].sourceEvidence;
|
|
record.policyBaselineLabels = policyEntries[index].baselineLabels;
|
|
if (policyEntries[index].reviewMembers) {
|
|
record.id = `gpo-work-setting:${encodeURIComponent(policyEntries[index].entry.settingKey!)}`;
|
|
record.policyReviewMembers = policyEntries[index].reviewMembers!.map(member => ({ baseline: member.baseline,
|
|
id: stableRecordId({ sourceFile: projectName, sheetName: 'GPOs', headers, row: member.row }) }));
|
|
}
|
|
});
|
|
if (!dataset) {
|
|
const lastModified = Math.max(
|
|
...policyEntries.map(entry => Date.parse(entry.importedAt)).filter(Number.isFinite),
|
|
0
|
|
);
|
|
return {
|
|
fileName: `${projectName} · GPOs`,
|
|
fileSize: 0,
|
|
lastModified,
|
|
format: 'GPO Backup',
|
|
rawText: '',
|
|
sheets: [sheet],
|
|
profileId: 'generic',
|
|
profileVersion: 'gpo-work-comparison-1.2.1',
|
|
scanTool: 'GPOs'
|
|
};
|
|
}
|
|
const [first, ...rest] = dataset.sheets;
|
|
const aggregate = first?.isProjectSheet ? (() => {
|
|
const sourceHeaders = first.rows[0] ?? [];
|
|
const aggregateHeaders = [...sourceHeaders];
|
|
for (const header of headers) if (!aggregateHeaders.includes(header)) aggregateHeaders.push(header);
|
|
const projectRows = first.rows.slice(1).map(row => {
|
|
const source = new Map(sourceHeaders.map((header, index) => [header, row[index] ?? '']));
|
|
return aggregateHeaders.map(header => source.get(header) ?? '');
|
|
});
|
|
const aggregatePolicyRows = policyRows.map(row => {
|
|
const source = new Map(headers.map((header, index) => [header, row[index] ?? '']));
|
|
source.set('Source: File', projectName);
|
|
source.set('Source: Sheet', 'GPOs');
|
|
return aggregateHeaders.map(header => source.get(header) ?? '');
|
|
});
|
|
const aggregateRows = [aggregateHeaders, ...projectRows, ...aggregatePolicyRows];
|
|
const combined = makeSheet(first.name, aggregateRows, rowsToCsv(aggregateRows), first.sourceFile ?? projectName, {
|
|
id: first.id,
|
|
sourceFile: first.sourceFile,
|
|
sourceSheet: first.sourceSheet,
|
|
isProjectSheet: true
|
|
});
|
|
// Keep exact scanner provenance when policy rows extend the aggregate.
|
|
combined.analysis.records = combined.analysis.records.map((record, index) => {
|
|
const original = first.analysis.records[index];
|
|
const policyIndex = index - first.analysis.records.length;
|
|
const policyRecord = sheet.analysis.records[policyIndex];
|
|
if (!original && policyRecord?.policyReviewMembers) {
|
|
for (const member of policyEntries[policyIndex].reviewMembers ?? []) {
|
|
const values = new Map(headers.map((header, i) => [header, member.row[i] ?? '']));
|
|
values.set('Source: File', projectName);
|
|
values.set('Source: Sheet', 'GPOs');
|
|
policyRecord.policyReviewMembers.push({ baseline: member.baseline, id: stableRecordId({
|
|
sourceFile: first.sourceFile ?? projectName, sheetName: first.name, headers: aggregateHeaders,
|
|
row: aggregateHeaders.map(header => values.get(header) ?? '')
|
|
}) });
|
|
}
|
|
}
|
|
return original ? { ...record, findingOccurrenceId: original.findingOccurrenceId,
|
|
findingDefinitionId: original.findingDefinitionId, scanRunId: original.scanRunId, importFileId: original.importFileId,
|
|
policyFailingBackupSources: original.policyFailingBackupSources, policySourceEvidence: original.policySourceEvidence,
|
|
policyBaselineLabels: original.policyBaselineLabels, policyReviewMembers: original.policyReviewMembers }
|
|
: { ...record, ...(policyRecord?.policyReviewMembers ? { id: policyRecord.id, policyReviewMembers: policyRecord.policyReviewMembers } : {}),
|
|
policyFailingBackupSources: policyEntries[index - first.analysis.records.length]?.failingBackupSources,
|
|
policySourceEvidence: policyEntries[index - first.analysis.records.length]?.sourceEvidence,
|
|
policyBaselineLabels: policyEntries[index - first.analysis.records.length]?.baselineLabels };
|
|
});
|
|
return combined;
|
|
})() : first;
|
|
return {
|
|
...dataset,
|
|
sheets: aggregate ? [aggregate, sheet, ...rest] : [sheet],
|
|
scanTool: dataset.scanTool === 'GPOs' ? dataset.scanTool : 'Multiple Tools'
|
|
};
|
|
}
|