import { readEnginePreferences, useEnginePreferences } from '../../app/engine-preferences';
import { ClientInfoTabs } from './ClientInfoTabs';
import { ClientSecurityPanel, type SecurityFindingGroup } from './ClientSecurityPanel';
import { managementCategoryTexts } from '../../reporting/management-report-texts';
import { ClientContextHeatmap } from '../context-layer/ContextHeatmap';
import { CONTEXT_CLUSTER_FACET_ID } from '../context-layer/context-cluster-facet';
import { useAssessmentContext } from '../context-layer/use-assessment-context';
import { publishContextReport } from '../../assessment-context/context-report-publication';
import { managementFindingDistribution, managementWorkspaceDeviationKeys } from '../../reporting/management-business-impact';
import { businessImpactControls, BUSINESS_IMPACT_FACET_ID } from '../search-filter/business-impact-controls';
import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import { AssessorApiClient } from '../../api/assessor-api-client';
import { hasSelectedTimeslotScope } from '../../app/work-scope';
import { downloadText } from '../../exports/text-download';
import { browserDownloadAdapter } from '../../platform/browser/browser-download-adapter';
import { OperatingSystemInventory } from './OperatingSystemInventory';
import { EnvironmentResultsButton } from './EnvironmentResultsButton';
import { CloudInformationTile } from './CloudInformationTile';
import { EnvironmentEvidenceTables } from './EnvironmentEvidenceTables';
import { environmentEvidenceForGroup, type EnvironmentEvidenceGroup } from './environment-result-evidence';
import type { GpoBackupStatusRecord } from '../../api/assessor-api-contract';
import {
clientInfoOverview, dashboardDomainInformation, dashboardEntraTenantCount, dashboardEnvironmentDetail, dashboardEnvironmentDomainCount,
dashboardEnvironmentTableNumber, dashboardGpoCount, dashboardObservationNumber, displayValue, numberFact, observationFact, observationFor,
sectionFor, UNKNOWN_FACT, type DashboardFact
} from './client-info-facts';
// Former home of the Client Info fact builders; existing imports keep working.
export { dashboardDomainInformation, dashboardEntraTenantCount, dashboardEnvironmentDetail, type DashboardEnvironmentDetail, type DashboardFact } from './client-info-facts';
import { useApp } from '../../app/AppProvider';
import { EmptyWorkspace } from '../../components/EmptyWorkspace/EmptyWorkspace';
import { DonutMetricCard } from '../../components/DonutMetricCard/DonutMetricCard';
import { assessedResultPercentages } from '../../components/DonutMetricCard/assessed-result-percentages';
import { DonutSmallArcMarker, donutArcPath, donutPercentageLabel } from '../../components/DonutMetricCard/donut-arc';
import { PanelLoadingState } from '../../components/PanelLoadingState/PanelLoadingState';
import { TriangleIcon } from '../../components/TriangleIcon';
import type { ImportedDataset } from '../../domain/dataset';
import type { EnvironmentObservation, EnvironmentSnapshot } from '../../domain/environment-context';
import type { FindingRecord } from '../../domain/finding';
import type { ImportProfile } from '../../domain/import-profile';
import type { Review } from '../../domain/review';
import { weightedSecurityScore, type SecurityScoreContext, type SecuritySourceScore } from '../../domain/security-scoring';
import { dashboardSecuritySources } from './dashboard-security-score';
import { SECURITY_DOMAIN_FACET_ID, dashboardDomainAssignment, securityDomainCatalog } from './security-domains';
import { ScoringMethodology } from './ScoringMethodology';
import { RiskRegister } from './RiskRegister';
import { deriveDynamicFacets, matchesDynamicFacets, recountFacetsAgainstActiveFilters, sourceScanNavigationSheet, type DynamicFacet, type DynamicFacetFilter, type DynamicFacetValue } from '../search-filter/dynamic-facets';
import { WorkTimeslotControl } from '../search-filter/WorkTimeslotControl';
import { dashboardResultState, dashboardScoreFromStates, type DashboardResultState } from './dashboard-result-state';
export { dashboardResultState } from './dashboard-result-state';
import { AiOverviewPanel, useAiOverview } from './AiOverviewPanel';
const assessorApi = new AssessorApiClient();
export { dashboardDomainAssignment, dashboardDomainRecords } from './security-domains';
interface DashboardPanelProps {
onOpenWork(): void;
}
function shareStyle(value: number): CSSProperties {
return { '--share': `${value}%` } as unknown as CSSProperties;
}
function share(count: number, total: number): number {
return total > 0 ? (count / total) * 100 : 0;
}
export function dashboardExplicitIoeCount(records: FindingRecord[]): number {
return dashboardExplicitIoeRecordIds(records).length;
}
export function dashboardExplicitIoeRecordIds(records: FindingRecord[]): string[] {
return records
.filter(record => record.fields.some(field => /^ioes?\s+found$/i.test(field.value.trim())))
.map(record => record.id);
}
interface DashboardStatusMetricProps {
label: string;
value: number;
percentage: number;
tone: 'all' | 'deviation' | 'passed' | 'not-run';
large?: boolean;
helper?: string;
active?: boolean;
onClick?(): void;
}
function DashboardStatusMetric({ label, value, percentage, tone, large = false, helper, active, onClick }: DashboardStatusMetricProps) {
const percentageLabel = donutPercentageLabel(percentage);
const content = <>
{percentage > 0 && }
= 5 ? ' is-compact' : ''}`}>{value.toLocaleString('en-US')} {percentageLabel}
{label}
{helper && {helper} }
>;
const className = `dashboard-status-metric${large ? ' dashboard-status-metric-large' : ''}`;
return onClick
? {content}
: {content} ;
}
interface SegmentedDistributionProps {
heading: string;
total: number;
values: DynamicFacetValue[];
severity?: boolean;
formatLabel?(label: string): string;
onSelect(value: string): void;
}
export function dashboardScanSourceLabel(label: string): string {
const parts = label.split(/\s+·\s+/);
const timestamp = parts.at(-1) ?? '';
if (parts.length < 2 || !/^\d{4}-\d{2}-\d{2}(?:T.*)?$/.test(timestamp) || Number.isNaN(Date.parse(timestamp))) return label;
return parts.slice(0, -1).join(' · ');
}
export interface DashboardRiskTool {
id: string;
label: string;
sourceValues: string[];
count: number;
}
const dashboardCisCatSeverityValue = 'dashboard-cis-cat-without-severity';
export interface DashboardCisCatSeverityRemainder extends DynamicFacetValue {
sourceValues: string[];
}
export function dashboardRiskTools(values: DynamicFacetValue[]): DashboardRiskTool[] {
const tools = new Map();
values.forEach(value => {
const label = dashboardScanSourceLabel(value.label);
const id = label.toLocaleLowerCase('en-US');
const existing = tools.get(id);
if (existing) {
existing.sourceValues.push(value.value);
existing.count += value.count;
} else tools.set(id, { id, label, sourceValues: [value.value], count: value.count });
});
return [...tools.values()];
}
export function dashboardCisCatSeverityRemainder(
sourceValues: DynamicFacetValue[],
cisCatSeverityValues: DynamicFacetValue[]
): DashboardCisCatSeverityRemainder | undefined {
const tool = dashboardRiskTools(sourceValues)
.find(item => /^cis-cat(?:\s+pro)?$/i.test(item.label.trim()));
if (!tool) return undefined;
const classified = cisCatSeverityValues.reduce((sum, value) => sum + value.count, 0);
const count = Math.max(0, tool.count - classified);
return count > 0 ? {
value: dashboardCisCatSeverityValue,
label: 'CIS-CAT',
count,
sourceValues: tool.sourceValues
} : undefined;
}
/** Compact distribution based on the supplied dashboard reference. */
function SegmentedDistribution({ heading, total, values, severity = false, formatLabel = label => label, onSelect }: SegmentedDistributionProps) {
return (
{heading}
{values.length === 0 ? (
This scan report carries no such classification.
) : (
<>
{values.map((value, index) => (
))}
{values.map((value, index) => (
onSelect(value.value)}
title={`Open in Work, filtered by ${value.label}`}
>
{formatLabel(value.label)}
{value.count.toLocaleString('en-US')} · {share(value.count, total).toFixed(0)}%
))}
>
)}
);
}
const overviewProperties: Array<[string, string]> = [
['domain.fqdn', 'Domain'],
['domain.functional_level', 'Domain functional level'],
['forest.functional_level', 'Forest functional level'],
['forest.schema_version', 'Schema version'],
['domain.controller_count', 'Domain controllers'],
['domain.recycle_bin_enabled', 'AD Recycle Bin'],
['backup.last_detected_at', 'Last AD backup'],
['laps.legacy.installation_date', 'Legacy LAPS'],
['laps.windows.installation_date', 'Windows LAPS'],
['logging.wef.configuration_count', 'WEF configurations'],
['kerberos.krbtgt.password_last_changed', 'krbtgt password changed'],
['report.engine_version', 'PingCastle engine']
];
function useEnvironmentSnapshot(projectId?: string, scanRunIds?: string[]) {
const [snapshot, setSnapshot] = useState();
const [failed, setFailed] = useState(false);
useEffect(() => {
let active = true;
setSnapshot(undefined);
setFailed(false);
if (!projectId) return () => { active = false; };
assessorApi.getEnvironmentSnapshot({ projectId, scanRunIds })
.then(value => { if (active) setSnapshot(value); })
.catch(() => { if (active) setFailed(true); });
return () => { active = false; };
}, [projectId, scanRunIds?.join('|')]);
return { snapshot, failed };
}
function EnvironmentOverview({ projectId, snapshot, failed }: { projectId?: string; snapshot?: EnvironmentSnapshot; failed: boolean }) {
const observations = useMemo(() => {
const byProperty = new Map();
snapshot?.observations.forEach(observation => byProperty.set(observation.property, observation));
return overviewProperties
.map(([property, label]) => ({ label, observation: byProperty.get(property) }))
.filter((item): item is { label: string; observation: EnvironmentObservation } => Boolean(item.observation));
}, [snapshot]);
const operatingSystems = useMemo(() => [...new Set(snapshot?.observations
.filter(item => item.property === 'operating_system.name')
.map(displayValue) ?? [])], [snapshot]);
if (!projectId) return null;
return (
Directory overview
{snapshot &&
{snapshot.sections.length.toLocaleString('en-US')} sections }
{!snapshot && !failed && Loading Client Information…
}
{failed && Client Information could not be loaded.
}
{snapshot && snapshot.sections.length === 0 && This import predates structured scanner-context capture.
}
{snapshot && (
{snapshot.observations.length.toLocaleString('en-US')} typed observations
{(snapshot.coverage.empty + snapshot.coverage.explicitlyAbsent).toLocaleString('en-US')} empty or absent
{snapshot.coverage.restricted.toLocaleString('en-US')} restricted
{snapshot.coverage.incomplete.toLocaleString('en-US')} incomplete
)}
{snapshot && (observations.length > 0 || operatingSystems.length > 0) && (
{observations.map(({ label, observation }) => (
{label} {displayValue(observation)}
))}
{operatingSystems.length > 0 &&
OS {operatingSystems.join(', ')} }
)}
);
}
interface RiskProfileProps {
sourceFacet?: DynamicFacet;
categoryFacet?: DynamicFacet;
severityFacet?: DynamicFacet;
records: FindingRecord[];
profile: ImportProfile;
reviews: Record;
environmentSnapshot?: EnvironmentSnapshot;
onSelectTool(sourceValues: string[]): void;
onSelectCategory(value: string, sourceValues: string[]): void;
onSelectSeverity(value: string, sourceValues: string[]): void;
}
type RiskScoreObservation = Pick;
const pingCastleRiskCategoryCatalog = [
['risk.anomalies', 'Anomalies'],
['risk.stale_objects', 'Stale Objects'],
['risk.privileged_accounts', 'Privileged Accounts'],
['risk.trusts', 'Trusts']
] as const;
export function dashboardPingCastleRiskCategories(observations: RiskScoreObservation[], fallback: DynamicFacetValue[]): DynamicFacetValue[] {
return pingCastleRiskCategoryCatalog.map(([property, label]) => {
const observation = observations.find(item => item.property === property);
const sourceValue = observation?.normalizedValue ?? observation?.originalValue;
const numericValue = typeof sourceValue === 'number' ? sourceValue : Number(sourceValue);
const fallbackValue = fallback.find(value => value.label.toLocaleLowerCase('en-US') === label.toLocaleLowerCase('en-US'));
return {
value: fallbackValue?.value ?? label.toLocaleLowerCase('en-US'),
label,
count: Number.isFinite(numericValue) ? numericValue : fallbackValue?.count ?? 0
};
});
}
const fullRiskProfileId = 'full-risk-profile';
function RiskProfile({ sourceFacet, categoryFacet, severityFacet, records, profile, reviews, environmentSnapshot, onSelectTool, onSelectCategory, onSelectSeverity }: RiskProfileProps) {
const tools = useMemo(() => dashboardRiskTools(sourceFacet?.values ?? []), [sourceFacet]);
const [selectedToolId, setSelectedToolId] = useState('');
const selectedTool = tools.find(tool => tool.id === selectedToolId)
?? tools.find(tool => tool.id.includes('pingcastle'))
?? tools[0];
const fullProfile = selectedToolId === fullRiskProfileId;
const effectiveProfileId = fullProfile ? fullRiskProfileId : selectedTool?.id ?? fullRiskProfileId;
const scopedRecords = useMemo(() => fullProfile
? records
: selectedTool && sourceFacet
? records.filter(record => matchesDynamicFacets(record, [sourceFacet], { [sourceFacet.id]: { values: selectedTool.sourceValues } }, reviews))
: [], [fullProfile, records, reviews, selectedTool, sourceFacet]);
const scopedFacets = useMemo(() => recountFacetsAgainstActiveFilters({
facets: [categoryFacet, severityFacet].filter((facet): facet is DynamicFacet => Boolean(facet)),
records: scopedRecords,
filters: {},
reviews,
statusFilter: 'all',
matchesStatus: (record, status) => profile.matchesStatus(record, status)
}), [categoryFacet, profile, reviews, scopedRecords, severityFacet]);
const scopedCategories = scopedFacets.find(facet => facet.id === categoryFacet?.id)?.values ?? [];
const categories = fullProfile
? tools.map(tool => ({ value: tool.id, label: tool.label, count: tool.count }))
: selectedTool?.id.includes('pingcastle')
? dashboardPingCastleRiskCategories(
environmentSnapshot?.observations.filter(observation => observation.tool.id === 'pingcastle') ?? [],
scopedCategories
)
: [...scopedCategories].sort((left, right) => right.count - left.count || left.label.localeCompare(right.label));
const severities = scopedFacets.find(facet => facet.id === severityFacet?.id)?.values ?? [];
const deviations = scopedRecords.filter(record => profile.matchesStatus(record, 'deviation')).length;
const marked = scopedRecords.filter(record => reviews[record.id]?.include).length;
const severityTotal = severities.reduce((sum, value) => sum + value.count, 0);
let severityOffset = 0;
return (
{categories.length > 0 ? (
{categories.slice(0, 4).map(category => (
{
if (fullProfile) {
const tool = tools.find(item => item.id === category.value);
if (tool) onSelectTool(tool.sourceValues);
} else if (selectedTool) onSelectCategory(category.value, selectedTool.sourceValues);
}} title={`Open in Work, filtered by ${category.label}`}>
{category.label} {category.count.toLocaleString('en-US')}
))}
) :
No risk categories available.
}
{severities.length > 0 ? <>
{severities.map(severity => {
const percentage = share(severity.count, severityTotal);
const offset = severityOffset;
severityOffset += percentage;
return ;
})}
{severityTotal.toLocaleString('en-US')} 100%
{severities.map(severity => (
fullProfile
? onSelectSeverity(severity.value, [])
: selectedTool && onSelectSeverity(severity.value, selectedTool.sourceValues)} title={`Open in Work, filtered by ${severity.label}`}>
{severity.label}
{severity.count.toLocaleString('en-US')}
))}
> :
No severity distribution available.
}
);
}
interface DashboardEnvironmentMetric {
label: string;
value: number;
detail?: string;
restricted?: boolean;
tone?: string;
onClick?(): void;
}
interface DashboardEnvironmentSummary {
directoryFacts: Array<{ label: string; value: string }>;
coverage: DashboardEnvironmentMetric[];
identity: DashboardEnvironmentMetric[];
controlPaths: DashboardEnvironmentMetric[];
certificates: DashboardEnvironmentMetric[];
gpo: DashboardEnvironmentMetric[];
risk: DashboardEnvironmentMetric[];
resilienceFacts: Array<{ label: string; value: string }>;
}
function observationNumber(snapshot: EnvironmentSnapshot | undefined, property: string, label?: string, sectionKey?: string): number {
const observation = observationFor(snapshot, property, label, sectionKey);
const value = observation?.normalizedValue ?? observation?.originalValue;
const numeric = typeof value === 'number' ? value : Number(value);
return Number.isFinite(numeric) ? numeric : 0;
}
function observationText(snapshot: EnvironmentSnapshot | undefined, property: string, label?: string): string {
const observation = observationFor(snapshot, property, label);
return observation ? displayValue(observation) : 'No data';
}
function sectionObservationCount(snapshot: EnvironmentSnapshot | undefined, sectionKey: string): number {
return snapshot?.observations.filter(observation => observation.sectionKey === sectionKey).length ?? 0;
}
function sectionTableCount(snapshot: EnvironmentSnapshot | undefined, sectionKey: string): number {
return sectionFor(snapshot, sectionKey)?.tables.length ?? 0;
}
function sectionRowCount(snapshot: EnvironmentSnapshot | undefined, sectionKey: string): number {
return sectionFor(snapshot, sectionKey)?.tables.reduce((sum, table) => sum + table.rows.length, 0) ?? 0;
}
function isRestricted(snapshot: EnvironmentSnapshot | undefined, sectionKey: string): boolean {
return ['license-restricted', 'access-denied'].includes(sectionFor(snapshot, sectionKey)?.state ?? '');
}
export function dashboardEnvironmentSummary(snapshot?: EnvironmentSnapshot): DashboardEnvironmentSummary {
const operatingSystems = [...new Set(snapshot?.observations
.filter(observation => observation.property === 'operating_system.name')
.map(displayValue) ?? [])];
return {
directoryFacts: [
{ label: 'Domain', value: observationText(snapshot, 'domain.fqdn', 'Domain') },
{ label: 'Domain FL', value: observationText(snapshot, 'domain.functional_level', 'Domain Functional Level') },
{ label: 'Forest FL', value: observationText(snapshot, 'forest.functional_level', 'Forest Functional Level') },
{ label: 'Schema', value: observationText(snapshot, 'forest.schema_version', 'Schema version') },
{ label: 'Domain Controllers', value: observationText(snapshot, 'domain.controller_count', 'DC count') }
],
coverage: [
{ label: 'Observed', value: snapshot?.coverage.observed ?? 0 },
{ label: 'Empty', value: snapshot?.coverage.empty ?? 0 },
{ label: 'Explicitly absent', value: snapshot?.coverage.explicitlyAbsent ?? 0 },
{ label: 'Restricted', value: snapshot?.coverage.restricted ?? 0 },
{ label: 'Incomplete', value: snapshot?.coverage.incomplete ?? 0 }
],
identity: [
{ label: 'Admin groups', value: sectionObservationCount(snapshot, 'sectionAdminGroups'), detail: 'observations', restricted: isRestricted(snapshot, 'sectionAdminGroups') },
{ label: 'Never-expiring users', value: sectionObservationCount(snapshot, 'sectionneverexpiresuser') },
{ label: 'Account analysis', value: sectionObservationCount(snapshot, 'sectionComputerInformation'), restricted: isRestricted(snapshot, 'sectionComputerInformation') },
{ label: 'Honey Pot', value: sectionObservationCount(snapshot, 'sectionUserInformation'), restricted: isRestricted(snapshot, 'sectionUserInformation') },
{ label: 'Trusted delegation', value: sectionObservationCount(snapshot, 'sectionenabledtrusteddelegationcomputer') }
],
controlPaths: [
{ label: 'Control paths', value: sectionObservationCount(snapshot, 'sectionControlPathsAnalysis'), detail: 'observations' },
{ label: 'Indirect-path tables', value: sectionTableCount(snapshot, 'sectionControlPathsAnalysis') },
{ label: 'Foreign-domain rows', value: sectionRowCount(snapshot, 'sectionControlPathsAnalysis') },
{ label: 'Trusts score', value: observationNumber(snapshot, 'risk.trusts', 'Trusts score') },
{ label: 'Discovered domains', value: sectionRowCount(snapshot, 'sectionTrustsdetails') }
],
certificates: [
{ label: 'PKI', value: sectionObservationCount(snapshot, 'sectionPKI'), detail: 'observations' },
{ label: 'Certificate templates', value: observationNumber(snapshot, 'pki.certificate_template_count', 'Certificate Templates list row count', 'sectionPKI') },
{ label: 'Certificates', value: observationNumber(snapshot, 'pki.certificate_count', 'Certificates list row count', 'sectionPKI') },
{ label: 'DC certificates', value: observationNumber(snapshot, 'pki.dc_certificate_count', 'DC Certificates list row count', 'sectionPKI') },
{ label: 'Delegations', value: observationNumber(snapshot, 'pki.delegation_count', 'Delegations list row count', 'sectionPKI') }
],
gpo: [
{ label: 'GPO', value: sectionObservationCount(snapshot, 'sectionGPO'), detail: 'observations' },
{ label: 'Privilege assignments', value: observationNumber(snapshot, 'gpo.privilege_count', 'Privileges list row count', 'sectionGPO') },
{ label: 'Password policies', value: observationNumber(snapshot, 'password.policy_count', 'Password policies list row count', 'sectionPasswordPolicies') },
{ label: 'Audit settings', value: observationNumber(snapshot, 'gpo.audit_setting_count', 'Audit settings list row count', 'sectionGPO') },
{ label: 'Defender ASR', value: observationNumber(snapshot, 'gpo.defender_asr_count', 'Defender ASR row count', 'sectionGPO') },
{ label: 'Firewall rules', value: observationNumber(snapshot, 'gpo.firewall_rule_count', 'Firewall Rules row count', 'sectionGPO') }
],
risk: [
{ label: 'Domain risk', value: observationNumber(snapshot, 'risk.global', 'Domain Risk Level') },
{ label: 'Anomalies', value: observationNumber(snapshot, 'risk.anomalies', 'Anomalies score') },
{ label: 'Privileged accounts', value: observationNumber(snapshot, 'risk.privileged_accounts', 'Privileged Accounts score') },
{ label: 'Stale objects', value: observationNumber(snapshot, 'risk.stale_objects', 'Stale Objects score') },
{ label: 'Trusts', value: observationNumber(snapshot, 'risk.trusts', 'Trusts score') }
],
resilienceFacts: [
{ label: 'AD Recycle Bin', value: observationText(snapshot, 'domain.recycle_bin_enabled', 'Recycle Bin enabled') },
{ label: 'Last backup', value: observationText(snapshot, 'backup.last_detected_at', 'Last detected AD backup') },
{ label: 'krbtgt changed', value: observationText(snapshot, 'kerberos.krbtgt.password_last_changed', 'krbtgt password last changed') },
{ label: 'Legacy LAPS', value: observationText(snapshot, 'laps.legacy.installation_date', 'Legacy LAPS installation date') },
{ label: 'Windows LAPS', value: observationText(snapshot, 'laps.windows.installation_date', 'Windows LAPS installation date') },
{ label: 'WEF', value: observationText(snapshot, 'logging.wef.configuration_count', 'WEF configuration count') },
{ label: 'OS', value: operatingSystems.length ? operatingSystems.join(', ') : 'No data' }
]
};
}
interface DashboardMetricTileProps {
label: string;
value: string;
tone?: string;
emphasis?: boolean;
active?: boolean;
onClick?(): void;
}
function DashboardMetricTile({ label, value, tone = 'neutral', emphasis = false, active, onClick }: DashboardMetricTileProps) {
const content = <>{label} {value} >;
const className = `client-info-metric tone-${tone}${emphasis ? ' is-emphasis' : ''}`;
return onClick
? {content}
: {content} ;
}
/** Source-backed summary facts appear in the tiles and remain available in Results. */
function DashboardFactList({ items }: { items: DashboardFact[] }) {
return
{items.map(item =>
{item.label} {item.value} )}
;
}
function DashboardFactGroup({ title, items, overviewItems, glyph, snapshot, evidenceGroup }: {
title: string; items: DashboardFact[]; glyph: DashboardGlyphName;
overviewItems?: DashboardFact[];
snapshot?: EnvironmentSnapshot; evidenceGroup: EnvironmentEvidenceGroup;
}) {
const tileItems = (overviewItems ?? items).slice(0, 4);
return ;
}
interface DashboardDonutSegment {
label: string;
value: number;
tone: string;
onClick?(): void;
}
function DashboardDonut({ total, centerLabel, segments }: { total: number; centerLabel: string; segments: DashboardDonutSegment[] }) {
let offset = 0;
return
{segments.filter(segment => segment.value > 0).map(segment => {
const percentage = share(segment.value, total);
const segmentOffset = offset;
offset += percentage;
return
;
})}
{total.toLocaleString('en-US')} 100% {centerLabel}
{segments.map(segment => {segment.onClick
? {segment.label} {segment.value.toLocaleString('en-US')}
: {segment.label} {segment.value.toLocaleString('en-US')} } )}
;
}
export function DashboardMetricBars({ items, tone = 'accent' }: { items: DashboardEnvironmentMetric[]; tone?: string }) {
const maximum = Math.max(1, ...items.map(item => item.value));
const ordinalTones = ['critical', 'high', 'medium', 'accent', 'neutral'];
return {items.map((item, index) => {
const resolvedTone = item.tone ?? (tone === 'accent' ? ordinalTones[index % ordinalTones.length] : tone);
const content = <>
{item.label}
{item.value.toLocaleString('en-US')} {item.detail && {item.detail} }
{item.restricted ? 'Restricted' : '\u00a0'}
>;
return {item.onClick
? {content}
: {content}
}
;
})} ;
}
interface DashboardCardSummary {
label: string;
value: number | string;
detail?: string;
}
export function DashboardLargeCard({ title, meta, summary, className = '', details, initiallyExpanded = true, children }: { title: string; meta?: string; summary?: DashboardCardSummary; className?: string; details?: ReactNode; initiallyExpanded?: boolean; children?: ReactNode }) {
const [expanded, setExpanded] = useState(initiallyExpanded);
const detailsId = useId();
return
setExpanded(value => !value)}>
{title} {summary && — {summary.label} {typeof summary.value === 'number' ? summary.value.toLocaleString('en-US') : summary.value}{summary.detail ? ` ${summary.detail}` : ''} }{meta && {meta} }
{expanded &&
{children}
{details &&
{details}
}
}
;
}
export function DashboardDonutCard({ title, meta, initiallyExpanded = true, children }: { title: string; meta: string; initiallyExpanded?: boolean; children: ReactNode }) {
const [expanded, setExpanded] = useState(initiallyExpanded);
const detailsId = useId();
return
setExpanded(value => !value)}>
{title} — {meta}
{expanded && }
;
}
type DashboardGlyphName = 'domain' | 'cloud' | 'user' | 'computer' | 'server' | 'policy' | 'shield' | 'lock' | 'code' | 'chart' | 'clipboard' | 'folder' | 'scan';
interface DashboardSourceScore {
tool: DashboardRiskTool;
records: FindingRecord[];
passed: number;
deviations: number;
assessed: number;
reviewRequired?: number;
reevaluationRequired?: number;
score?: number;
weight?: number;
rating?: SecuritySourceScore;
}
function DashboardSourceResults({ sources, snapshot }: { sources: DashboardSourceScore[]; snapshot?: EnvironmentSnapshot }) {
const [visibleRecords, setVisibleRecords] = useState(100);
const quantity = (value?: number) => value === undefined ? '—' : value.toLocaleString('en-US');
const records = sources.flatMap(source => source.records.map(record => ({ source, record })));
return
Source result contributions
{sources.length ?
Tool Results Passed
Deviations Assessed Review required
Reevaluation required Score Weight
Scoring method Issues
{sources.map(source =>
{source.tool.label} {quantity(source.records.length)} {quantity(source.passed)}
{quantity(source.deviations)} {quantity(source.assessed)}
{quantity(source.reviewRequired)} {quantity(source.reevaluationRequired)}
{quantity(source.score)} {quantity(source.weight)}
{source.rating?.methods.join(', ') || '—'} {source.rating?.issues.join(' · ') || '—'}
)}
: No source results available.
}
{sources.flatMap(source => source.rating?.scans.map(scan => ({ source, scan })) ?? []).length > 0 &&
Scan score evidence
Tool Method Score
Native property Native value Passed
Deviations Assessed Eligible
Run IDs Import file IDs Issues
{sources.flatMap(source => source.rating?.scans.map(scan => ({ source, scan })) ?? [])
.map(({ source, scan }) =>
{source.tool.label} {scan.method} {quantity(scan.score)}
{scan.nativeProperty ?? '—'} {quantity(scan.nativeValue)}
{quantity(scan.passed)} {quantity(scan.deviations)} {quantity(scan.assessed)}
{quantity(scan.eligible)} {scan.runIds.join(', ') || '—'}
{scan.importFileIds.join(', ') || '—'} {scan.issues.join(' · ') || '—'}
)}
}
{records.length > 0 &&
Source finding records {quantity(records.length)}
Tool Finding Record ID
Original fields Filled fields Metadata
Scan run Import file Source sheet
Source file Policy source evidence Complete retained record
{records.slice(0, visibleRecords).map(({ source, record }) =>
{source.tool.label} {record.title} {record.id}
{JSON.stringify(record.fields)} {JSON.stringify(record.filled)} {JSON.stringify(record.meta)}
{record.scanRunId ?? '—'} {record.importFileId ?? '—'}
{record.sourceSheet ?? '—'} {record.sourceFile ?? '—'}
{record.policySourceEvidence ? JSON.stringify(record.policySourceEvidence) : '—'}
{JSON.stringify(record)}
)}
{visibleRecords < records.length && setVisibleRecords(value => value + 100)}>
Show more findings ({quantity(visibleRecords)} of {quantity(records.length)})
}
}
;
}
interface DashboardDomainScore {
id: string;
label: string;
glyph: DashboardGlyphName;
score?: number;
passed: number;
deviations: number;
notConfigured: number;
conflicts: number;
duplicates: number;
assessed: number;
records: FindingRecord[];
reviewRecords: FindingRecord[];
}
export function dashboardOverallScore(sourceScores: Array<{ score?: number; weight?: number }>): number | undefined {
return weightedSecurityScore(sourceScores);
}
function dashboardFieldValue(record: FindingRecord, pattern: RegExp): string | undefined {
return record.fields.find(field => pattern.test(field.label.trim()))?.value;
}
/**
* Zahl der erfassten Gruppenrichtlinien aus den verarbeiteten Sicherungen.
*
* Die frueher benutzte Zaehlung der GUIDs aus Findingtexten war eine
* Schaetzung: sie fand nur GPOs, die ueberhaupt einen Befund tragen, und
* zaehlte jede sonstige GUID mit. Die Verarbeitungsliste nennt die Zahl
* dagegen selbst.
*/
function useGpoInventory(projectId?: string): { gpoCount?: number; snapshots?: number; records?: GpoBackupStatusRecord[] } {
const [inventory, setInventory] = useState<{ gpoCount?: number; snapshots?: number; records?: GpoBackupStatusRecord[] }>({});
useEffect(() => {
let active = true;
setInventory({});
if (!projectId) return () => { active = false; };
assessorApi.listProjectGpoBackupStatus(projectId)
.then(response => {
if (!active) return;
const processed = response.records.filter(record => record.processingStatus);
const gpoCount = processed.reduce((sum, record) => sum + record.gpoCount, 0);
setInventory(processed.length ? { gpoCount, snapshots: processed.length, records: processed } : {});
})
.catch(() => { if (active) setInventory({}); });
return () => { active = false; };
}, [projectId]);
return inventory;
}
function DashboardGlyph({ name }: { name: DashboardGlyphName }) {
const content = name === 'user' ? <> >
: name === 'computer' ? <> >
: name === 'server' ? <> >
: name === 'policy' || name === 'clipboard' ? <> >
: name === 'cloud' ?
: name === 'shield' || name === 'scan' ? <> >
: name === 'lock' ? <> >
: name === 'code' ? <> >
: name === 'chart' ? <> >
: name === 'folder' ?
: <> >;
return {content} ;
}
/**
* Verbindungskurven zwischen Overall Score und den Security Domains.
*
* Die Kurven entstehen aus der tatsaechlichen Lage von Ring und Domainknoten,
* nicht aus angenommenen Koordinaten: Zuvor lagen feste Zielpunkte in einem
* ungleichmaessig gestreckten Koordinatensystem (`preserveAspectRatio="none"`),
* sodass die Linien nur bei genau einer Fenstergroesse an ihrem Knoten
* ankamen und sonst daneben endeten.
*/
export function DashboardPostureConnectors({ nodeCount }: { nodeCount: number }) {
// Gemessen wird ab dem eigenen Element: der Ref des umgebenden Abschnitts ist
// waehrend des Layouteffekts eines Kindes noch nicht gesetzt.
const svgRef = useRef(null);
const [geometry, setGeometry] = useState<{ width: number; height: number; paths: string[] }>({ width: 0, height: 0, paths: [] });
const measure = useCallback(() => {
const posture = svgRef.current?.parentElement;
const ring = posture?.querySelector('.client-overall-ring');
const nodes = [...(posture?.querySelectorAll('.client-domains .client-domain-score') ?? [])];
if (!posture || !ring || !nodes.length) {
setGeometry(current => (current.paths.length ? { width: 0, height: 0, paths: [] } : current));
return;
}
const frame = posture.getBoundingClientRect();
const ringBox = ring.getBoundingClientRect();
const start = { x: ringBox.right - frame.left, y: ringBox.top + ringBox.height / 2 - frame.top };
const paths = nodes.map((node, index) => {
const box = node.getBoundingClientRect();
const end = { x: box.left - frame.left, y: box.top + box.height / 2 - frame.top };
// Leichte Faecherung am Ring, damit die Kurven nicht als eine Linie
// starten; die Ankunft trifft die Knotenmitte exakt.
const originY = start.y + (index - (nodes.length - 1) / 2) * 6;
const reach = Math.max(24, (end.x - start.x) * 0.45);
return `M${start.x.toFixed(1)} ${originY.toFixed(1)} C${(start.x + reach).toFixed(1)} ${originY.toFixed(1)}, ${(end.x - reach).toFixed(1)} ${end.y.toFixed(1)}, ${end.x.toFixed(1)} ${end.y.toFixed(1)}`;
});
setGeometry(current => current.width === frame.width && current.height === frame.height
&& current.paths.length === paths.length && current.paths.every((path, index) => path === paths[index])
? current : { width: frame.width, height: frame.height, paths });
}, []);
useLayoutEffect(() => {
measure();
const posture = svgRef.current?.parentElement;
// Zwei Ausloeser mit unterschiedlicher Reichweite: der Beobachter meldet
// jede Groessenaenderung der beteiligten Flaechen, das Fensterereignis
// greift auch dann, wenn die Seite gerade nicht gezeichnet wird und der
// Beobachter deshalb keine Rueckrufe liefert.
window.addEventListener('resize', measure);
const observer = posture && typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => measure()) : undefined;
if (posture && observer) {
observer.observe(posture);
posture.querySelectorAll('.client-domains, .client-overall').forEach(element => observer.observe(element));
}
return () => {
window.removeEventListener('resize', measure);
observer?.disconnect();
};
}, [measure, nodeCount]);
return
{geometry.paths.map((path, index) => )}
;
}
function dashboardScoreTone(score?: number): 'unknown' | 'critical' | 'medium' | 'passed' {
return score === undefined ? 'unknown' : score < 50 ? 'critical' : score < 75 ? 'medium' : 'passed';
}
export function DashboardOverallRing({ score, onClick }: { score?: number; onClick?(): void }) {
const value = score ?? 0;
const scoreLabel = score !== undefined && score > 0 && score < 0.5 ? '<1' : Math.round(value).toString();
const tone = dashboardScoreTone(score);
const label = `Overall Score: ${score === undefined ? 'No data' : scoreLabel}`;
const content = <>
{score !== undefined && <>>}
{score === undefined ? '—' : scoreLabel} {score !== undefined && /100 }ASSESSOR
>;
return onClick
? {content}
: {content}
;
}
export function DashboardTopFact({ value, label, glyph, snapshot, evidenceGroup, backupRecords, fallback }: {
value?: number | string; label: string; glyph: DashboardGlyphName; snapshot?: EnvironmentSnapshot;
evidenceGroup: EnvironmentEvidenceGroup; backupRecords?: GpoBackupStatusRecord[]; fallback?: string;
}) {
const evidence = environmentEvidenceForGroup(snapshot, evidenceGroup);
return {value ?? '—'} {label} >}>
{fallback && {fallback}
}
{!!evidence.observations.length && Field Original value Source Scan run
{evidence.observations.map(item => {item.label} {typeof item.originalValue === 'string' ? item.originalValue : JSON.stringify(item.originalValue)} {item.tool.name} {item.scanRunId} )}
}
{!!backupRecords?.length && <>All processed client backups · the overview count includes every backup listed below.
Backup file GPOs Processing status Updated
{backupRecords.map(record => {record.fileName} {record.gpoCount} {record.processingStatus} {record.updatedAt} )}
>}
;
}
function dashboardDomainAmount(domain: DashboardDomainScore): number {
return domain.assessed === 0 && domain.reviewRecords.length > 0 ? domain.reviewRecords.length : domain.deviations;
}
function DashboardDomainRow({ domain, distributionTotal, onClick }: { domain: DashboardDomainScore; distributionTotal: number; onClick?(): void }) {
// Der Knoten traegt die Bestehensquote der Domaene. Ohne
// bewertete Ergebnisse bleibt das Sinnbild stehen - eine Null waere hier
// eine Aussage, die die Datenlage nicht deckt.
const tone = domain.score === undefined ? 'unknown' : domain.score < 50 ? 'critical' : domain.score < 75 ? 'medium' : 'passed';
const reviewOnly = domain.assessed === 0 && domain.reviewRecords.length > 0;
const amount = dashboardDomainAmount(domain);
const content = <>
{domain.score === undefined ? : {Math.round(domain.score)} }
{domain.label}
{amount.toLocaleString('en-US')}
>;
return {onClick ? {content} : {content}
} ;
}
const impactLabels = Object.fromEntries(businessImpactControls.map(theme => [theme.id, theme.label]));
export function DashboardBusinessImpact({ distribution, loading, onSelect }: {
distribution?: ReturnType; loading: boolean; onSelect?: (themeId: string) => void;
}) {
const titleId = useId();
const max = Math.max(1, ...(distribution?.themes.map(theme => theme.count) ?? []));
return
Possible Business Impact
{distribution ?
{distribution.themes.map(theme =>
onSelect?.(theme.id)} aria-label={`${impactLabels[theme.id]}: ${theme.count}`}>
{theme.count.toLocaleString('en-US')}
{impactLabels[theme.id]}
)}
: {loading ? 'Loading Business Impact…' : 'Business Impact is unavailable for this assessment.'}
}
;
}
export interface DashboardNativeMetrics {
pingCastleRisk?: number;
purpleKnightPosture?: number;
purpleKnightIoes?: number;
benchmarkScore?: number;
assessedHost?: string;
domainCount?: number;
gpoCount?: number;
}
function nativeMeta(parts: Array): string {
return parts.filter(Boolean).join(' · ');
}
/**
* Jede Quellenzeile nennt zuerst ihren vergleichbaren ASSESSOR-Score. Die
* werkzeugeigene Zahl steht daneben und immer mit ihrem Namen - eine
* PingCastle-Risikozahl (hoch ist schlecht) darf nie als Score gelesen werden.
*/
function DashboardSourceListItem({ source, native, onClick }: { source: DashboardSourceScore; native: DashboardNativeMetrics; onClick?(): void }) {
const normalized = source.tool.label.toLocaleLowerCase('en-US').replace(/[^a-z0-9]+/g, '');
const kind = normalized.includes('pingcastle') ? 'pingcastle' : normalized.includes('purple') ? 'purple' : normalized.includes('maester') ? 'maester' : normalized.includes('cis') ? 'cis' : normalized.includes('gpo') ? 'gpo' : 'other';
const score = source.score;
const tone = score === undefined ? 'unknown' : score < 50 ? 'critical' : score < 75 ? 'medium' : 'passed';
const assessorScore = { value: score === undefined ? '—' : Math.round(score), unit: '/100 ASSESSOR' };
const nativeScans = source.rating?.scans.filter(scan => scan.nativeValue !== undefined) ?? [];
const exactNative = nativeScans.length === 1 ? nativeScans[0].nativeValue : undefined;
const scoreMethod = source.reevaluationRequired && !source.assessed ? 'Re-evaluation required'
: source.reviewRequired && !source.assessed ? 'Review required'
: source.rating?.methods.length === 1
? ({ native: 'Native score', 'inverted-native': '100 − native risk', 'pass-rate': 'Pass rate', unavailable: 'Not scoreable' })[source.rating.methods[0]]
: source.rating ? 'Combined scan methods' : 'Pass rate';
const domains = native.domainCount === undefined ? undefined : `${native.domainCount} ${native.domainCount === 1 ? 'domain' : 'domains'}`;
const presentation = kind === 'pingcastle'
? { name: 'PingCastle', glyph: 'scan' as const, ...assessorScore,
meta: nativeMeta([exactNative === undefined ? undefined : `PingCastle risk ${exactNative}/100`, domains]) || 'Domain assessment' }
: kind === 'purple' ? { name: 'Purple Knight', glyph: 'shield' as const, ...assessorScore,
meta: nativeMeta([
exactNative === undefined ? undefined : `Posture ${exactNative}/100`,
`${source.deviations} deviations`
]) || 'Identity indicators' }
: kind === 'maester' ? { name: 'Maester', glyph: 'cloud' as const, ...assessorScore,
meta: `${source.passed.toLocaleString('en-US')} / ${source.assessed.toLocaleString('en-US')} tests passed` }
: kind === 'cis' ? { name: 'CIS-CAT', glyph: 'clipboard' as const, ...assessorScore,
meta: nativeMeta([
exactNative === undefined ? undefined : `Benchmark score ${exactNative}`,
`${source.assessed.toLocaleString('en-US')} controls`
]) }
: kind === 'gpo' ? { name: 'GPO Analysis', glyph: 'domain' as const, ...assessorScore,
meta: nativeMeta([source.reevaluationRequired ? `${source.reevaluationRequired.toLocaleString('en-US')} saved comparisons`
: source.reviewRequired && !source.assessed ? `${source.reviewRequired.toLocaleString('en-US')} comparisons require review`
: `${source.deviations.toLocaleString('en-US')} baseline deviations`, `${native.gpoCount ?? '—'} GPOs`]) }
: { name: source.tool.label, glyph: 'policy' as const, ...assessorScore, meta: `${source.assessed.toLocaleString('en-US')} controls assessed` };
const content = <>{presentation.name} {presentation.value}{presentation.unit} {scoreMethod} · {presentation.meta}{source.rating?.issues.length ? ' · Provisional' : ''} >;
return {onClick
? {content}
: {content}
}
;
}
const NO_RECORDS: FindingRecord[] = [];
/**
* Auswertung der geladenen Ergebnisse fuer die Client Info.
*
* Ein Mandantenblatt traegt hier mehrere tausend Datensaetze. Die Ansicht
* braucht davon Quellen-, Domain- und Prioritaetssichten gleichzeitig; jede
* einzeln berechnet las dieselben Datensaetze erneut und baute je Domain den
* Klassifikationstext neu auf. Bei acht Domains, fuenf Quellen und vierzehn
* Punkteberechnungen waren das mehrere hunderttausend Zugriffe - und zwar bei
* jedem Rendern, also auch bei jeder Zustandsaenderung der Anwendung.
*
* Deshalb entsteht die gesamte Ableitung hier in einem Durchlauf und nur dann
* neu, wenn sich Daten, Profil, Reviews, Facetten oder Quellenauswahl aendern.
* Zustand, Schweregrad und Domainzuordnung je Datensatz werden dabei einmal
* bestimmt und anschliessend nur noch nachgeschlagen.
*/
type DashboardAnalysis = ReturnType;
/**
* Die Auswertung ueberlebt den Ansichtswechsel. Ein `useMemo` gilt nur fuer die
* Lebensdauer der Komponente; Client Info wird beim Wechsel abgebaut und beim
* Zurueckkehren neu aufgebaut, sodass dieselbe Rechnung sonst bei jedem Besuch
* erneut liefe.
*/
const analysisCache = new WeakMap;
sourceFilter?: DynamicFacetFilter;
value: DashboardAnalysis;
}>();
/**
* Facetten und Auswertung entstehen gemeinsam und werden gemeinsam behalten.
*
* Die Facettenliste wird bei jedem Aufbau der Ansicht neu erzeugt; sie taugt
* deshalb nicht als Schluessel eines ansichtsuebergreifenden Zwischenspeichers.
* Massgeblich sind die Eingaben, aus denen beides entsteht: Blatt, Datensatz,
* Profil und Reviews.
*/
function useDashboardAnalysis(
activeSheet: ReturnType['activeSheet'],
dataset: ImportedDataset | null | undefined,
activeProfile: ImportProfile,
reviews: Record,
sourceFilter?: DynamicFacetFilter
) {
return useMemo(() => {
const cached = activeSheet && dataset ? analysisCache.get(activeSheet) : undefined;
if (cached && cached.dataset === dataset && cached.profile === activeProfile && cached.reviews === reviews && cached.sourceFilter === sourceFilter) {
return cached.value;
}
const facets = activeSheet ? deriveDynamicFacets(activeSheet, dataset, activeProfile, reviews) : [];
const value = dashboardAnalysis(activeSheet, activeProfile, reviews, facets, sourceFilter, dataset?.securityScoring);
if (activeSheet && dataset) analysisCache.set(activeSheet, { dataset, profile: activeProfile, reviews, sourceFilter, value });
return value;
}, [activeProfile, activeSheet, dataset, reviews, sourceFilter]);
}
export function dashboardAnalysis(
activeSheet: ReturnType['activeSheet'],
activeProfile: ImportProfile,
reviews: Record,
facets: DynamicFacet[],
sourceFilter?: DynamicFacetFilter,
securityScoring?: SecurityScoreContext
) {
{
const sourceFacet = facets.find(facet =>
/^(?:quelle|source):\s*scan$/i.test(facet.label.trim()) || facet.source === 'provenance');
const sourceFilters = sourceFacet && sourceFilter?.values?.length ? { [sourceFacet.id]: sourceFilter } : undefined;
const allRecords = activeSheet?.analysis.records ?? NO_RECORDS;
const records = sourceFilters ? allRecords.filter(record => matchesDynamicFacets(record, [sourceFacet!], sourceFilters, reviews)) : allRecords;
const countedFacets = sourceFilters ? recountFacetsAgainstActiveFilters({
facets, records, filters: {}, reviews, statusFilter: 'all',
matchesStatus: (record, status) => activeProfile.matchesStatus(record, status)
}) : facets;
const statusFacet = countedFacets.find(facet => facet.source === 'profile-status');
const severityFacet = countedFacets.find(facet => facet.severityScale);
const states = new Map();
records.forEach(record => {
const state = dashboardResultState(record, activeProfile);
states.set(record, state);
});
const domainBuckets = dashboardDomainAssignment(records, activeSheet?.toolId);
const scoreOf = (subset: FindingRecord[]) =>
dashboardScoreFromStates(subset.map(record => states.get(record) ?? 'unknown'), subset);
const tools = dashboardRiskTools(sourceFacet?.values ?? []);
const sourceScores: DashboardSourceScore[] = dashboardSecuritySources(tools.map(tool => {
const toolRecords = sourceFacet
? records.filter(record => matchesDynamicFacets(record, [sourceFacet], { [sourceFacet.id]: { values: tool.sourceValues } }, reviews))
: NO_RECORDS;
return { tool, records: toolRecords, ...scoreOf(toolRecords) };
}).filter(source => source.records.length > 0), securityScoring, Boolean(activeSheet?.isProjectSheet && !sourceFilters));
const domains: DashboardDomainScore[] = securityDomainCatalog.map(domain => {
const domainRecords = domainBuckets.get(domain.id) ?? NO_RECORDS;
const score = scoreOf(domainRecords);
return {
id: domain.id,
label: domain.label,
glyph: domain.glyph,
// The clickable population is exactly the number displayed. Passed
// and contextual GPO states remain visible only as secondary counts.
records: domainRecords.filter(record => states.get(record) === 'deviation'),
reviewRecords: domainRecords.filter(record => ['review-required', 'reevaluation-required'].includes(states.get(record) ?? '')),
...score
};
});
const totalResult = dashboardScoreFromStates(records.map(record => states.get(record) ?? 'unknown'), records);
const gapRecordIds = records.filter(record => !['passed', 'deviation', 'not-applicable'].includes(states.get(record) ?? 'unknown'))
.map(record => record.id);
return {
total: records.length,
records,
statusFacet,
severityFacet,
sourceFacet,
tools,
reports: sourceFacet?.values.length ?? 0,
statusCount: (value: string) => statusFacet?.values.find(item => item.value === value)?.count ?? 0,
sourceScores,
domains,
totalResult,
gapRecordIds,
overallScore: dashboardOverallScore(sourceScores)
};
}
}
export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
const app = useApp();
const { dataset, activeSheet, activeProfile, reviews, setStatusFilter, setFacetFilter, setPathFindingIds, resetFilters, setView } = app;
const [clientInfoView, setClientInfoView] = useState<'overview' | 'ai'>('overview');
const environment = useEnvironmentSnapshot(app.targetProjectId, app.activeScanRunIds);
const aiOverview = useAiOverview(app.targetProjectId, app.activeAssessmentIds ?? [], app.activeScanRunIds ?? []);
const showAiOverview = Boolean(aiOverview.loading || aiOverview.response?.generationEnabled || aiOverview.response?.run);
useEffect(() => {
if (clientInfoView === 'ai' && !showAiOverview) setClientInfoView('overview');
}, [clientInfoView, showAiOverview]);
const gpoInventory = useGpoInventory(app.targetProjectId);
const sourceFilter = app.facetFilters['provenance:source-scan'];
const analysis = useDashboardAnalysis(activeSheet, dataset, activeProfile, reviews, sourceFilter);
const engines = useEnginePreferences();
const impactContext = useAssessmentContext(Boolean(dataset), app.targetProjectId, app.activeAssessmentIds, app.activeScanRunIds, dataset);
const securityView = useMemo(() => {
const records = analysis.records.filter(record => dashboardResultState(record, activeProfile) === 'deviation');
const buckets = dashboardDomainAssignment(records, activeSheet?.toolId);
const domainCounts = securityDomainCatalog.map(domain => ({ id: domain.id, label: domain.label, count: buckets.get(domain.id)?.length ?? 0 }));
const domainIds = new Map([...buckets].flatMap(([id, members]) => members.map(record => [record.id, id] as const)));
const countsFor = (members: FindingRecord[]) => domainCounts.map(domain => ({ ...domain, count: members.filter(record => domainIds.get(record.id) === domain.id).length })).filter(domain => domain.count > 0);
const assigned = new Set();
const keys = new Map(records.map(record => [record.id, managementWorkspaceDeviationKeys([record], () => true)[0]]));
const groups: SecurityFindingGroup[] = impactContext.status === 'ready' ? publishContextReport(impactContext.context).clusters.map((cluster, index) => {
const members = new Set(cluster.findingKeys);
const matches = records.filter(record => !assigned.has(record.id) && members.has(keys.get(record.id)!));
matches.forEach(record => assigned.add(record.id));
const text = engines.report ? managementCategoryTexts[cluster.id] : undefined;
return { id: cluster.id, number: String(index + 1).padStart(2, '0'), label: cluster.label, severity: cluster.severity,
readingName: text?.name, situation: text?.situation ?? cluster.description, lever: text?.lever,
records: matches, domains: countsFor(matches) };
}) : [];
const remaining = records.filter(record => !assigned.has(record.id));
if (remaining.length) groups.push({ id: 'unassigned', label: impactContext.status === 'ready' ? 'Unassigned findings' : 'All findings', severity: 'unknown',
situation: impactContext.status === 'ready' ? 'These findings have no matching published category assignment in the current scope.' : undefined,
records: remaining, domains: countsFor(remaining) });
return { groups, domains: domainCounts, total: records.length };
}, [analysis, activeProfile, activeSheet?.toolId, impactContext.status === 'ready' ? impactContext.context : impactContext.status, engines.report]);
const impactDistribution = useMemo(() => {
if (!engines.report || impactContext.status !== 'ready') return undefined;
const keys = managementWorkspaceDeviationKeys(analysis.records, record => dashboardResultState(record, activeProfile) === 'deviation');
return managementFindingDistribution(publishContextReport(impactContext.context), analysis.totalResult.deviations, keys);
}, [impactContext.status === 'ready' ? impactContext.context : undefined, analysis, activeProfile, engines.report]);
const [reportExporting, setReportExporting] = useState<'management' | 'technical' | null>(null);
const [reportError, setReportError] = useState(null);
const reportScopeKey = JSON.stringify([app.targetProjectId, app.activeScanRunIds, app.activeAssessmentIds]);
const reportScopeRef = useRef(reportScopeKey);
reportScopeRef.current = reportScopeKey;
const exportReport = async (kind: 'management' | 'technical') => {
const projectId = app.targetProjectId;
if (!readEnginePreferences().report || !projectId || !dataset || !hasSelectedTimeslotScope(app.activeScanRunIds, app.activeAssessmentIds)) return;
if (kind === 'management' && !readEnginePreferences().context) {
setReportError('Management Report requires Context Engine. Enable it in Engines and try again.');
return;
}
const requestedScope = reportScopeKey;
setReportExporting(kind);
setReportError(null);
try {
// The separate Report Engine renders and archives on the server; the download is the archived file.
const run = await assessorApi.createReportRun(projectId, {
kind, scanRunIds: app.activeScanRunIds, assessmentIds: app.activeAssessmentIds
});
if (!readEnginePreferences().report || (kind === 'management' && !readEnginePreferences().context) || reportScopeRef.current !== requestedScope) return;
downloadText(browserDownloadAdapter, run.fileName, run.html, 'text/html;charset=utf-8');
} catch (reason) {
if (readEnginePreferences().report) setReportError(reason instanceof Error ? reason.message : 'Report could not be generated.');
} finally {
setReportExporting(null);
}
};
const environmentDetail = useMemo(() => dashboardEnvironmentDetail(environment.snapshot), [environment.snapshot]);
const domainInformation = useMemo(() => dashboardDomainInformation(environment.snapshot), [environment.snapshot]);
const [scoringOpen, setScoringOpen] = useState(false);
// Ersatzzaehlung aus Findingtexten, solange die Verarbeitungsliste der
// Sicherungen noch nicht geantwortet hat. Sie liest jeden Feldwert der
// GPO-Datensaetze und darf deshalb nicht bei jedem Rendern erneut laufen.
const derivedGpoCount = useMemo(
() => dashboardGpoCount(analysis.sourceScores.find(source => /gpo/i.test(source.tool.label))?.records ?? []),
[analysis]
);
if (!dataset || !activeSheet) return app.targetProjectId
?
: ;
const clientInfoViews: Array = [
['overview', 'Overview'] as const,
...(showAiOverview ? [['ai', 'AI Overview'] as const] : [])
];
const clientInfoTabs = clientInfoViews.length > 1 ?
{clientInfoViews.map(([id, title]) =>
setClientInfoView(id)}>{title} )}
: undefined;
if (clientInfoView === 'ai' && showAiOverview) return ;
const {
total, statusFacet, severityFacet, sourceFacet, tools, reports,
sourceScores, domains, totalResult, gapRecordIds, overallScore
} = analysis;
const deviations = totalResult.deviations;
const assessmentPercentages = assessedResultPercentages(totalResult.passed, deviations);
const assessmentCoverage = totalResult.coverage;
const nativePingCastleRisk = dashboardObservationNumber(environment.snapshot, 'risk.domain')
?? dashboardObservationNumber(environment.snapshot, 'risk.global');
const maesterSource = sourceScores.find(source => /maester/i.test(source.tool.label));
const gpoCount = gpoInventory.gpoCount ?? derivedGpoCount;
// The overview facts come from the same React-free builder the Technical Report reads.
const overview = clientInfoOverview(environment.snapshot, { gpoCount, inferredEntraTenant: Boolean(maesterSource?.records.length) });
const dataFact = (label: string) => overview.data.find(item => item.label === label)?.value;
const userAccounts = dataFact('User Accounts');
const computerAccounts = dataFact('Computer Objects');
const domainControllers = dataFact('Domain Controllers');
const entraTenants = dataFact('Entra Tenant');
const domainOverview = overview.domainInformation;
const inventoryOverview = overview.inventory;
const nativeMetrics: DashboardNativeMetrics = {
pingCastleRisk: nativePingCastleRisk,
purpleKnightPosture: dashboardObservationNumber(environment.snapshot, 'purple_knight.ad.security_posture_score'),
purpleKnightIoes: dashboardObservationNumber(environment.snapshot, 'purple_knight.ad.ioes_found'),
benchmarkScore: dashboardObservationNumber(environment.snapshot, 'benchmark.score'),
assessedHost: observationFor(environment.snapshot, 'host.name') ? observationFact(environment.snapshot, 'host.name') : undefined,
domainCount: dashboardEnvironmentDomainCount(environment.snapshot),
gpoCount
};
const sourceOrder = ['pingcastle', 'purpleknight', 'maester', 'cis-cat', 'gpos'];
const orderedSourceScores = sourceScores.filter(source =>
source.tool.label.toLocaleLowerCase('en-US').replace(/[^a-z0-9]+/g, '') !== 'tabulardata'
).sort((left, right) => {
const rank = (label: string) => {
const normalized = label.toLocaleLowerCase('en-US').replace(/[^a-z0-9]+/g, '');
const index = sourceOrder.findIndex(item => normalized.includes(item.replace(/[^a-z0-9]+/g, '')));
return index < 0 ? sourceOrder.length : index;
};
return rank(left.tool.label) - rank(right.tool.label) || left.tool.label.localeCompare(right.tool.label);
});
const displayedDomains = domains;
const domainDistributionTotal = displayedDomains.reduce((sum, domain) => sum + dashboardDomainAmount(domain), 0);
const overallTone = dashboardScoreTone(overallScore);
const jump = (apply: () => void, preserveSource = true) => {
resetFilters();
if (preserveSource && sourceFilter && sourceFacet) setFacetFilter(sourceFacet.id, sourceFilter);
app.setActionOnly(false);
setStatusFilter('deviation');
apply();
setView('columns');
onOpenWork();
};
const sourceOverviewContent = (dismissResults: () => void) => orderedSourceScores.length ? {orderedSourceScores.map(source =>
{ dismissResults(); jump(() => {
setFacetFilter(sourceFacet.id, { values: source.tool.sourceValues });
// The source drilldown includes saved GPO review/conflict/duplicate rows.
if (/gpo/i.test(source.tool.label)) setStatusFilter('all');
}); } : undefined} />
)} : No source results available.
;
const selectedSource = sourceFilter?.values?.length
? tools.find(tool => tool.sourceValues.length === sourceFilter.values!.length
&& tool.sourceValues.every(value => sourceFilter.values!.includes(value)))?.id ?? 'selection'
: 'all';
return (
{clientInfoTabs}
Overall Score
{dismissResults => <>
{sourceOverviewContent(dismissResults)}
{environmentDetail.restricted.length > 0 && Restricted by the source tool, not empty: {environmentDetail.restricted.join(', ')}
}
{ dismissResults(); setScoringOpen(true); }}>Scoring methodology ↗
>}
jump(() => undefined, false)} />
Assessment Coverage {assessmentCoverage === undefined ? '—' : `${assessmentCoverage}%`}
{totalResult.assessed.toLocaleString('en-US')} / {totalResult.eligible.toLocaleString('en-US')} eligible controls assessed
{displayedDomains.map(domain => jump(() => { setFacetFilter(SECURITY_DOMAIN_FACET_ID, { values: [domain.id] }); })
: domain.assessed === 0 && domain.reviewRecords.length ? () => jump(() => {
setStatusFilter('all');
setPathFindingIds(domain.reviewRecords.map(record => record.id));
}) : undefined} />)}
{engines.report && engines.context && jump(() => {
const contextFilter = app.facetFilters[CONTEXT_CLUSTER_FACET_ID];
if (contextFilter) setFacetFilter(CONTEXT_CLUSTER_FACET_ID, contextFilter);
setFacetFilter(BUSINESS_IMPACT_FACET_ID, { values: [themeId] });
})} />}
Data jump(() => undefined)} />
{([
['passed', 'Passed', totalResult.passed],
['not-configured', 'Not configured', totalResult.notConfigured],
['duplicates', 'Duplicates', totalResult.duplicates],
['conflicts', 'Conflicts', totalResult.conflicts]
] as const).map(([status, label, count]) => jump(() => setStatusFilter(status))}>{count.toLocaleString('en-US')} {label} )}
jump(() => {
setStatusFilter('all');
setPathFindingIds(gapRecordIds);
})}>{totalResult.unassessed.toLocaleString('en-US')} Gaps
{domainOverview.map(item =>
{item.label} {item.value} )}
jump(() => {
const targetSheet = sourceScanNavigationSheet(dataset, activeSheet);
if (targetSheet) {
app.setActiveSheet(targetSheet.id);
setStatusFilter('deviation');
}
setFacetFilter(CONTEXT_CLUSTER_FACET_ID, { values: [clusterId] });
}, false)} />
{scoringOpen && setScoringOpen(false)} />}
} snapshot={environment.snapshot} />
}
key={`cloud-information:${app.targetProjectId}:${app.activeAssessmentIds?.join(',')}:${app.activeScanRunIds?.join(',')}`}
scope={app.targetProjectId ? { projectId: app.targetProjectId, assessmentId: app.activeAssessmentIds?.length === 1 ? app.activeAssessmentIds[0] : undefined,
scanRunIds: app.activeScanRunIds } : undefined} />
jump(() => setPathFindingIds([recordId]))} />
}, { id: 'security', label: 'Security', content: }]} />
);
}