117 lines
6.5 KiB
TypeScript
117 lines
6.5 KiB
TypeScript
import type { EnvironmentObservation, EnvironmentSnapshot, ScannerReportSection, ScannerReportTable } from '../../domain/environment-context';
|
|
|
|
export type EnvironmentEvidenceGroup = 'all' | 'directory' | 'resilience' | 'inventory'
|
|
| 'password-policy' | 'certificates' | 'source-metrics' | 'operating-systems';
|
|
|
|
export interface EnvironmentEvidence {
|
|
sections: ScannerReportSection[];
|
|
observations: EnvironmentObservation[];
|
|
}
|
|
|
|
interface SubsectionLabels {
|
|
explicit: Set<string>;
|
|
rowCount: Set<string>;
|
|
}
|
|
|
|
const subsectionCache = new WeakMap<EnvironmentSnapshot, Map<string, SubsectionLabels>>();
|
|
const subsectionKey = (runId: string, fileId: string, sectionKey: string, tableIndex: number) =>
|
|
`${runId}\u0000${fileId}\u0000${sectionKey}\u0000${tableIndex}`;
|
|
|
|
function indexedSubsections(snapshot: EnvironmentSnapshot): Map<string, SubsectionLabels> {
|
|
const cached = subsectionCache.get(snapshot);
|
|
if (cached) return cached;
|
|
const index = new Map<string, SubsectionLabels>();
|
|
for (const observation of snapshot.observations) {
|
|
const tableIndex = observation.attributes.sourceTableIndex;
|
|
if (typeof tableIndex !== 'number' || !observation.sectionKey) continue;
|
|
const key = subsectionKey(observation.scanRunId, observation.importFileId, observation.sectionKey, tableIndex);
|
|
let labels = index.get(key);
|
|
if (!labels) { labels = { explicit: new Set(), rowCount: new Set() }; index.set(key, labels); }
|
|
const explicit = observation.attributes.subsectionLabel;
|
|
if (typeof explicit === 'string' && explicit.trim()) labels.explicit.add(explicit.trim());
|
|
if (/\.row_count$/.test(observation.property) && observation.subject.label?.trim())
|
|
labels.rowCount.add(observation.subject.label.trim());
|
|
}
|
|
subsectionCache.set(snapshot, index);
|
|
return index;
|
|
}
|
|
|
|
/** Retained native subsection headings are keyed by source table within one exact report section. */
|
|
export function environmentTableSubsection(
|
|
snapshot: EnvironmentSnapshot, section: ScannerReportSection, table: ScannerReportTable
|
|
): string | undefined {
|
|
if (table.sourceTableIndex === undefined) return undefined;
|
|
const labels = indexedSubsections(snapshot).get(subsectionKey(
|
|
section.scanRunId, section.importFileId, section.sectionKey, table.sourceTableIndex));
|
|
if (!labels) return undefined;
|
|
if (labels.explicit.size) return labels.explicit.size === 1 ? [...labels.explicit][0] : undefined;
|
|
return labels.rowCount.size === 1 ? [...labels.rowCount][0] : undefined;
|
|
}
|
|
|
|
/** A section labelled with its first subsection must not appear to own all later siblings. */
|
|
export function environmentSectionTitle(snapshot: EnvironmentSnapshot, section: ScannerReportSection): string {
|
|
const subsectionLabels = new Set(section.tables
|
|
.map(table => environmentTableSubsection(snapshot, section, table)).filter((label): label is string => Boolean(label)));
|
|
if (subsectionLabels.size > 1 && subsectionLabels.has(section.label)) {
|
|
const fromKey = section.sectionKey.replace(/^section/i, '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').trim();
|
|
if (fromKey && fromKey.toLocaleLowerCase('en-US') !== section.label.toLocaleLowerCase('en-US')) return fromKey;
|
|
}
|
|
return section.label || section.sectionKey;
|
|
}
|
|
|
|
const osTable = (table: ScannerReportTable) => /operating\s+systems?/i.test(table.name);
|
|
const resilienceProperty = /^(?:backup\.|laps\.|kerberos\.krbtgt\.|logging\.wef\.|domain\.recycle_bin_enabled$)/i;
|
|
const inventorySection = /^section(?:UserInformation|ComputerInformation|AdminGroups|Trustsdetails|ControlPathsAnalysis|neverexpiresuser|enabledtrusteddelegationcomputer)$/i;
|
|
|
|
function observationInGroup(observation: EnvironmentObservation, group: EnvironmentEvidenceGroup): boolean {
|
|
const property = observation.property;
|
|
const section = observation.sectionKey ?? '';
|
|
if (group === 'operating-systems') return /^operating_system\./i.test(property)
|
|
|| /operating\s+systems?/i.test(String(observation.attributes.tableName ?? ''));
|
|
if (group === 'resilience') return resilienceProperty.test(property);
|
|
if (group === 'password-policy') return /^password_policy\./i.test(property) || /^sectionPasswordPolicies$/i.test(section);
|
|
if (group === 'certificates') return /^pki\./i.test(property) || /^sectionPKI$/i.test(section);
|
|
if (group === 'directory') return /^(?:domain\.|forest\.|entra\.)/i.test(property)
|
|
|| /^section(?:DomainInformation|Infrastructure)$/i.test(section);
|
|
if (group === 'inventory') return /^(?:host\.|trust\.|user\.|computer\.|domain\.controller_count$)/i.test(property)
|
|
|| inventorySection.test(section);
|
|
if (group === 'source-metrics') return /^(?:report\.|risk\.|purple_knight\.|benchmark\.|maester\.|entra\.tenant_(?:id|name)$)/i.test(property)
|
|
|| /^(?:report-overview|purple-knight-assessment-summary)$/i.test(section);
|
|
return true;
|
|
}
|
|
|
|
function sectionForGroup(section: ScannerReportSection, group: EnvironmentEvidenceGroup): ScannerReportSection | undefined {
|
|
if (group === 'all') return section;
|
|
const key = section.sectionKey;
|
|
if (group === 'directory' && /^section(?:DomainInformation|Infrastructure)$/i.test(key)) return section;
|
|
if (group === 'inventory' && inventorySection.test(key)) return section;
|
|
if (group === 'password-policy' && /^sectionPasswordPolicies$/i.test(key)) return section;
|
|
if (group === 'certificates' && /^sectionPKI$/i.test(key)) return section;
|
|
if (group === 'source-metrics' && /^(?:report-overview|purple-knight-assessment-summary)$/i.test(key)) return section;
|
|
if (group === 'operating-systems' && /^sectionComputerInformation$/i.test(key)) {
|
|
const tables = section.tables.filter(osTable);
|
|
return tables.length ? { ...section, tables, prose: undefined } : undefined;
|
|
}
|
|
if (group === 'resilience' && /^sectionAnomalies$/i.test(key)) {
|
|
const tables = section.tables.filter(table => /backup|laps|krbtgt|event\s+forwarding|\bwef\b|recycle/i.test(table.name));
|
|
return tables.length ? { ...section, tables, prose: undefined } : undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/** Read-only, source-backed projection. Every retained row remains available in `all`. */
|
|
export function environmentEvidenceForGroup(
|
|
snapshot: EnvironmentSnapshot | undefined,
|
|
group: EnvironmentEvidenceGroup
|
|
): EnvironmentEvidence {
|
|
if (!snapshot) return { sections: [], observations: [] };
|
|
return {
|
|
sections: snapshot.sections.flatMap(section => {
|
|
const selected = sectionForGroup(section, group);
|
|
return selected ? [selected] : [];
|
|
}),
|
|
observations: group === 'all' ? snapshot.observations
|
|
: snapshot.observations.filter(observation => observationInGroup(observation, group))
|
|
};
|
|
}
|