import { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type CSSProperties, type ReactNode } from 'react';
import { AssessorApiClient } from '../../api/assessor-api-client';
import { OperatingSystemInventory } from './OperatingSystemInventory';
import { operatingSystemInventory, type OperatingSystemInventoryGroup } from './operating-system-inventory';
import { useApp } from '../../app/AppProvider';
import { EmptyWorkspace } from '../../components/EmptyWorkspace/EmptyWorkspace';
import { DonutMetricCard } from '../../components/DonutMetricCard/DonutMetricCard';
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 { compareSecurityScores, 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 { DEFENDER_FOR_IOT_PROFILE_ID } from '../../domain/defender-for-iot';
import { DefenderVulnerabilityPanel } from '../defender-for-iot/DefenderVulnerabilityPanel';
import { defenderModelForDataset } from '../defender-for-iot/defender-dataset-model';
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';
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 roundedPercentage = Math.min(100, Math.max(0, Math.round(percentage)));
const content = <>
= 5 ? ' is-compact' : ''}`}>{value.toLocaleString('en-US')} {roundedPercentage}%
{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 displayValue(observation: EnvironmentObservation): string {
if (observation.state === 'explicitly-absent') return observation.originalValue === false ? 'Not detected' : 'Not present';
const value = observation.normalizedValue ?? observation.originalValue;
if (typeof value === 'boolean') return value ? 'Enabled' : 'Disabled';
if (value === null || value === '') return 'No value';
return String(value);
}
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 scanner context…
}
{failed && Scanner context 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 observationFor(snapshot: EnvironmentSnapshot | undefined, property: string, label?: string, sectionKey?: string) {
return snapshot?.observations.find(observation => observation.property === property
|| Boolean(label && observation.label === label && (!sectionKey || observation.sectionKey === sectionKey)));
}
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 sectionFor(snapshot: EnvironmentSnapshot | undefined, sectionKey: string) {
return snapshot?.sections.find(section => section.sectionKey === sectionKey);
}
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' }
]
};
}
/**
* Umgebungsdetails der geladenen Client Info.
*
* Die Zone beantwortet, was ausserhalb der Befunde ueber die Umgebung bekannt
* ist: Verzeichnis, Bestand, Passwortrichtlinie, PKI, Wiederanlauf und die
* werkzeugeigenen Kennzahlen. Unbekanntes bleibt ein Gedankenstrich - eine
* fehlende Angabe wird nie als Null dargestellt.
*/
export interface DashboardEnvironmentDetail {
operatingSystems: OperatingSystemInventoryGroup[];
directory: DashboardFact[];
inventory: DashboardFact[];
passwordPolicy: DashboardFact[];
certificates: DashboardFact[];
resilience: DashboardFact[];
toolContext: DashboardFact[];
restricted: string[];
}
export interface DashboardFact {
label: string;
value: string;
}
const UNKNOWN_FACT = '—';
/**
* PingCastle schreibt den Hilfetext einer Zelle als weitere Attribute in
* denselben Text ("Default Domain Policy " data-bs-html=..."). Fuer die
* Anzeige bleibt der Wert vor dem Attributrest stehen; der vollstaendige
* Originalwert bleibt in der Beobachtung erhalten.
*/
function factText(value: string | undefined): string {
const cleaned = (value ?? '').replace(/\s*"\s*data-bs[\s\S]*$/i, '').trim();
return cleaned || UNKNOWN_FACT;
}
function observationFact(snapshot: EnvironmentSnapshot | undefined, property: string, label?: string): string {
const observation = observationFor(snapshot, property, label);
return observation ? factText(displayValue(observation)) : UNKNOWN_FACT;
}
function optionalObservationNumber(snapshot: EnvironmentSnapshot | undefined, property: string, label?: string, sectionKey?: string): number | undefined {
const observation = observationFor(snapshot, property, label, sectionKey);
if (!observation) return undefined;
const value = observation.normalizedValue ?? observation.originalValue;
const numeric = typeof value === 'number' ? value : Number(value);
return Number.isFinite(numeric) ? numeric : undefined;
}
function numberFact(value: number | undefined): string {
return value === undefined ? UNKNOWN_FACT : value.toLocaleString('en-US');
}
function environmentTable(snapshot: EnvironmentSnapshot | undefined, sectionKey: string, tableName: RegExp) {
return sectionFor(snapshot, sectionKey)?.tables.find(table => tableName.test(table.name.trim()));
}
/** Erster Zeilenwert einer benannten Tabelle unter ihrer Spaltenueberschrift. */
function environmentTableValue(
snapshot: EnvironmentSnapshot | undefined, sectionKey: string, tableName: RegExp, header: RegExp
): string {
const table = environmentTable(snapshot, sectionKey, tableName);
const column = table?.headers.findIndex(value => header.test(value.trim())) ?? -1;
if (!table || column < 0) return UNKNOWN_FACT;
return factText(table.rows[0]?.[column] === null || table.rows[0]?.[column] === undefined ? '' : String(table.rows[0][column]));
}
function environmentTableRowCount(snapshot: EnvironmentSnapshot | undefined, sectionKey: string, tableName: RegExp): number | undefined {
return environmentTable(snapshot, sectionKey, tableName)?.rows.length;
}
function restrictedSections(snapshot: EnvironmentSnapshot | undefined): string[] {
return (snapshot?.sections ?? [])
.filter(section => ['license-restricted', 'access-denied'].includes(section.state))
.map(section => section.label);
}
export function dashboardMaesterTenantCount(snapshot?: EnvironmentSnapshot): number | undefined {
const tenants = new Set((snapshot?.observations ?? [])
.filter(observation => observation.tool.id === 'maester' && observation.property === 'entra.tenant_id' && observation.state === 'observed')
.map(observation => displayValue(observation).trim()).filter(Boolean));
return tenants.size || undefined;
}
export function dashboardEnvironmentDetail(snapshot?: EnvironmentSnapshot): DashboardEnvironmentDetail {
const userTable = (header: RegExp) => dashboardEnvironmentTableNumber(snapshot, 'sectionUserInformation', header);
const categoryScores = (snapshot?.observations ?? [])
.filter(observation => observation.property.startsWith('purple_knight.ad.category_score.'))
.map(observation => ({ label: observation.label, value: numberFact(optionalObservationNumber(snapshot, observation.property)) }));
return {
operatingSystems: operatingSystemInventory(snapshot),
directory: [
{ label: 'Domain', value: observationFact(snapshot, 'domain.fqdn', 'Domain') },
{ label: 'NetBIOS name', value: observationFact(snapshot, 'domain.netbios_name', 'Netbios Name') },
{ label: 'Forest', value: observationFact(snapshot, 'forest.name') },
{ label: 'Domains', value: numberFact(dashboardEnvironmentDomainCount(snapshot)) },
{ label: 'Domain functional level', value: observationFact(snapshot, 'domain.functional_level', 'Domain Functional Level') },
{ label: 'Forest functional level', value: observationFact(snapshot, 'forest.functional_level', 'Forest Functional Level') },
{ label: 'Schema version', value: observationFact(snapshot, 'forest.schema_version', 'Schema version') },
{ label: 'Domain created', value: observationFact(snapshot, 'domain.created_at', 'Creation date') },
{ label: 'Entra ID', value: observationFact(snapshot, 'entra.configuration_detected', 'Entra ID configuration detected') }
],
inventory: [
{ label: 'User accounts', value: numberFact(userTable(/^nb user accounts$/i)) },
{ label: 'Enabled / disabled', value: `${numberFact(userTable(/^nb enabled$/i))} / ${numberFact(userTable(/^nb disabled$/i))}` },
{ label: 'Password never expires', value: numberFact(userTable(/^nb pwd never expire$/i)) },
{ label: 'Computer objects', value: numberFact(dashboardEnvironmentTableNumber(snapshot, 'sectionComputerInformation', /^nb computer accounts$/i)) },
{ label: 'Domain controllers', value: observationFact(snapshot, 'domain.controller_count', 'DC count') },
{ label: 'Admin groups', value: numberFact(environmentTableRowCount(snapshot, 'sectionAdminGroups', /^admin groups list$/i)) },
{ label: 'Trusts', value: numberFact(environmentTableRowCount(snapshot, 'sectionTrustsdetails', /^trusts list$/i)) },
{ label: 'Assessed host', value: observationFact(snapshot, 'host.name') }
],
passwordPolicy: [
{ label: 'Policy', value: environmentTableValue(snapshot, 'sectionPasswordPolicies', /^password policies list$/i, /^policy name$/i) },
{ label: 'Complexity', value: environmentTableValue(snapshot, 'sectionPasswordPolicies', /^password policies list$/i, /^complexity$/i) },
{ label: 'Minimum length', value: environmentTableValue(snapshot, 'sectionPasswordPolicies', /^password policies list$/i, /^min password length$/i) },
{ label: 'History', value: environmentTableValue(snapshot, 'sectionPasswordPolicies', /^password policies list$/i, /^password history$/i) },
{ label: 'Maximum age', value: environmentTableValue(snapshot, 'sectionPasswordPolicies', /^password policies list$/i, /^max password age$/i) },
{ label: 'Lockout threshold', value: environmentTableValue(snapshot, 'sectionPasswordPolicies', /^password policies list$/i, /^lockout threshold$/i) }
],
certificates: [
{ label: 'Certificate templates', value: numberFact(environmentTableRowCount(snapshot, 'sectionPKI', /^certificate templates list$/i)) },
{ label: 'Certificates', value: numberFact(environmentTableRowCount(snapshot, 'sectionPKI', /^certificates list$/i)) },
{ label: 'DC certificates', value: numberFact(environmentTableRowCount(snapshot, 'sectionPKI', /^dc certificates list$/i)) },
{ label: 'PKI delegations', value: numberFact(environmentTableRowCount(snapshot, 'sectionPKI', /^delegations list$/i)) }
],
resilience: [
{ label: 'AD Recycle Bin', value: observationFact(snapshot, 'domain.recycle_bin_enabled', 'Recycle Bin enabled') },
{ label: 'Last AD backup', value: observationFact(snapshot, 'backup.last_detected_at', 'Last detected AD backup') },
{ label: 'krbtgt password', value: observationFact(snapshot, 'kerberos.krbtgt.password_last_changed', 'krbtgt password last changed') },
{ label: 'krbtgt version', value: observationFact(snapshot, 'kerberos.krbtgt.version', 'krbtgt version') },
{ label: 'Legacy LAPS', value: observationFact(snapshot, 'laps.legacy.installation_date', 'Legacy LAPS installation date') },
{ label: 'Windows LAPS', value: observationFact(snapshot, 'laps.windows.installation_date', 'Windows LAPS installation date') },
{ label: 'WEF configurations', value: observationFact(snapshot, 'logging.wef.configuration_count', 'WEF configuration count') }
],
toolContext: [
{ label: 'PingCastle engine', value: observationFact(snapshot, 'report.engine_version', 'PingCastle engine version') },
{ label: 'PingCastle domain risk', value: numberFact(optionalObservationNumber(snapshot, 'risk.domain') ?? optionalObservationNumber(snapshot, 'risk.global', 'Domain Risk Level')) },
{ label: 'Purple Knight posture', value: numberFact(optionalObservationNumber(snapshot, 'purple_knight.ad.security_posture_score')) },
{ label: 'Purple Knight IOEs', value: numberFact(optionalObservationNumber(snapshot, 'purple_knight.ad.ioes_found')) },
{ label: 'Purple Knight evaluated', value: numberFact(optionalObservationNumber(snapshot, 'purple_knight.ad.evaluated')) },
...categoryScores,
{ label: 'CIS-CAT benchmark', value: observationFact(snapshot, 'benchmark.name') },
{ label: 'CIS-CAT profile', value: observationFact(snapshot, 'benchmark.profile') },
{ label: 'CIS-CAT score', value: numberFact(optionalObservationNumber(snapshot, 'benchmark.score')) },
...(snapshot?.observations ?? [])
.filter(observation => observation.tool.id === 'maester' && ['entra.tenant_id', 'entra.tenant_name', 'report.generated_at', 'report.engine_version', 'maester.duration', 'maester.tests.actual_total', 'maester.runner.host', 'maester.runner.os', 'maester.powershell.version'].includes(observation.property))
.map(observation => ({ label: observation.label.startsWith('Maester') ? observation.label : `Maester ${observation.label}`, value: observation.state === 'observed' ? factText(displayValue(observation)) : UNKNOWN_FACT }))
],
restricted: restrictedSections(snapshot)
};
}
/**
* Kompakter, quellengestuetzter Verzeichniskontext fuer die erste Leselinie.
* Die Werte verwenden dieselben normalisierten Beobachtungen wie Environment
* Detail; fehlende Scannerangaben bleiben ausdruecklich unbekannt.
*/
export function dashboardDomainInformation(snapshot?: EnvironmentSnapshot): DashboardFact[] {
return [
{ label: 'Domain', value: observationFact(snapshot, 'domain.fqdn', 'Domain') },
{ label: 'NetBIOS name', value: observationFact(snapshot, 'domain.netbios_name', 'Netbios Name') },
{ label: 'Forest', value: observationFact(snapshot, 'forest.name') },
{ label: 'Domains', value: numberFact(dashboardEnvironmentDomainCount(snapshot)) },
{ label: 'Domain functional level', value: observationFact(snapshot, 'domain.functional_level', 'Domain Functional Level') },
{ label: 'Domain controllers', value: observationFact(snapshot, 'domain.controller_count', 'DC count') },
{ label: 'Forest functional level', value: observationFact(snapshot, 'forest.functional_level', 'Forest Functional Level') },
{ label: 'Schema version', value: observationFact(snapshot, 'forest.schema_version', 'Schema version') },
{ label: 'Domain created', value: observationFact(snapshot, 'domain.created_at', 'Creation date') },
{ label: 'Entra ID', value: observationFact(snapshot, 'entra.configuration_detected', 'Entra ID configuration detected') }
];
}
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} ;
}
/**
* Faktgruppe der Umgebungsdetails. Rein visuell: eine Gruppenregel und darunter
* ein flieszendes Raster gleich breiter Faktzellen. Ohne nebeneinanderstehende
* Karten kann keine hohe Gruppe ihre Nachbarn in die Hoehe strecken.
*
* Meldet die Quelle in einer Gruppe keinen einzigen Wert, bleibt sie als eine
* Zeile mit ausdruecklicher Aussage stehen und bleibt aufklappbar. Es wird kein
* Wert entfernt, zusammengefasst oder abgeleitet; Unbekanntes bleibt der
* Gedankenstrich der Datenschicht.
*/
function DashboardFactGroup({ title, items, glyph }: { title: string; items: DashboardFact[]; glyph: DashboardGlyphName }) {
const unreported = items.length > 0 && items.every(item => item.value === UNKNOWN_FACT);
const [expanded, setExpanded] = useState(false);
const listId = useId();
return
{title}
{unreported && <>
not reported by source
setExpanded(value => !value)}>
>}
{(!unreported || expanded) &&
{items.map(item =>
{item.label} {item.value} )}
}
;
}
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;
}
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;
}
function dashboardObservationNumber(snapshot: EnvironmentSnapshot | undefined, property: string): number | undefined {
const observation = snapshot?.observations.find(item => item.property === property && item.state === 'observed');
const value = observation?.normalizedValue ?? observation?.originalValue;
const number = typeof value === 'number' ? value : Number(value);
return Number.isFinite(number) ? number : undefined;
}
function dashboardEnvironmentTableNumber(snapshot: EnvironmentSnapshot | undefined, sectionKey: string, header: RegExp): number | undefined {
const values = snapshot?.sections
.filter(section => section.sectionKey === sectionKey)
.flatMap(section => section.tables.flatMap(table => {
const column = table.headers.findIndex(value => header.test(value.trim()));
if (column < 0) return [];
return table.rows.flatMap(row => {
const numeric = Number(row[column]);
return Number.isFinite(numeric) ? [numeric] : [];
});
})) ?? [];
return values.length ? values.reduce((sum, value) => sum + value, 0) : undefined;
}
/**
* Zahl der Domaenen. Nennt eine Quelle sie ausdruecklich (Purple Knight:
* "AD: number of domains"), gilt diese Angabe; sonst bleibt die Zahl der
* verschiedenen beobachteten Domaenennamen die belegbare Untergrenze.
*/
function dashboardEnvironmentDomainCount(snapshot: EnvironmentSnapshot | undefined): number | undefined {
const declared = dashboardObservationNumber(snapshot, 'domain.count');
if (declared !== undefined) return declared;
const domains = new Set(snapshot?.observations
.filter(item => item.property === 'domain.fqdn' && item.state === 'observed')
.map(item => String(item.normalizedValue ?? item.originalValue).trim().toLocaleLowerCase('en-US'))
.filter(Boolean));
return domains.size || undefined;
}
/**
* 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 } {
const [inventory, setInventory] = useState<{ gpoCount?: number; snapshots?: number }>({});
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 } : {});
})
.catch(() => { if (active) setInventory({}); });
return () => { active = false; };
}, [projectId]);
return inventory;
}
function dashboardGpoCount(records: FindingRecord[]): number | undefined {
const identifiers = new Set();
const pattern = /\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?/gi;
records.forEach(record => [record.sourceFile, ...record.fields.map(field => field.value)].filter(Boolean).forEach(value => {
String(value).match(pattern)?.forEach(identifier => identifiers.add(identifier.toLocaleLowerCase('en-US')));
}));
return identifiers.size || undefined;
}
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 tone = dashboardScoreTone(score);
const label = `Overall Score: ${score === undefined ? 'No data' : Math.round(score)}`;
const content = <>
{score !== undefined && }
{score === undefined ? '—' : Math.round(score)} {score !== undefined && /100 }ASSESSOR
>;
return onClick
? {content}
: {content}
;
}
function DashboardTopFact({ value, label, glyph }: { value?: number | string; label: string; glyph: DashboardGlyphName }) {
return {value ?? '—'} {label} ;
}
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}
} ;
}
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);
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 unclassifiedRecords = domainBuckets.get('unclassified') ?? NO_RECORDS;
const unclassifiedScore = scoreOf(unclassifiedRecords);
const unclassifiedDomain: DashboardDomainScore | undefined = unclassifiedRecords.length ? {
id: 'unclassified',
label: 'Unclassified',
glyph: 'scan',
records: unclassifiedRecords.filter(record => states.get(record) === 'deviation'),
reviewRecords: unclassifiedRecords.filter(record => ['review-required', 'reevaluation-required'].includes(states.get(record) ?? '')),
...unclassifiedScore
} : undefined;
const totalResult = dashboardScoreFromStates(records.map(record => states.get(record) ?? 'unknown'), records);
return {
total: records.length,
statusFacet,
severityFacet,
sourceFacet,
tools,
reports: sourceFacet?.values.length ?? 0,
statusCount: (value: string) => statusFacet?.values.find(item => item.value === value)?.count ?? 0,
sourceScores,
domains,
unclassifiedDomain,
totalResult,
overallScore: dashboardOverallScore(sourceScores)
};
}
}
export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
const app = useApp();
const { dataset, activeSheet, activeProfile, reviews, setStatusFilter, setFacetFilter, setPathFindingIds, resetFilters, setView } = app;
const environment = useEnvironmentSnapshot(app.targetProjectId, app.activeScanRunIds);
const gpoInventory = useGpoInventory(app.targetProjectId);
const sourceFilter = app.facetFilters['provenance:source-scan'];
const analysis = useDashboardAnalysis(activeSheet, dataset, activeProfile, reviews, sourceFilter);
const environmentDetail = useMemo(() => dashboardEnvironmentDetail(environment.snapshot), [environment.snapshot]);
const domainInformation = useMemo(() => dashboardDomainInformation(environment.snapshot), [environment.snapshot]);
const [scoringOpen, setScoringOpen] = useState(false);
const [restrictedOpen, setRestrictedOpen] = useState(false);
const overallScoreRef = useRef(null);
const environmentDetailRef = useRef(null);
const scoreComparison = useMemo(() => dataset?.securityScoring?.baseline
? compareSecurityScores(dataset.securityScoring.baseline, dataset.securityScoring.scans) : undefined, [dataset?.securityScoring]);
// 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]
);
useLayoutEffect(() => {
const syncEnvironmentTileHeight = () => {
const overallHeight = overallScoreRef.current?.getBoundingClientRect().height;
if (!overallHeight || !environmentDetailRef.current) return;
environmentDetailRef.current.style.setProperty('--client-environment-tile-height', `${overallHeight * 0.75}px`);
};
syncEnvironmentTileHeight();
window.addEventListener('resize', syncEnvironmentTileHeight);
const observer = overallScoreRef.current && typeof ResizeObserver !== 'undefined'
? new ResizeObserver(syncEnvironmentTileHeight) : undefined;
if (overallScoreRef.current && observer) observer.observe(overallScoreRef.current);
return () => {
window.removeEventListener('resize', syncEnvironmentTileHeight);
observer?.disconnect();
};
}, [dataset, activeSheet]);
const [clientInfoView, setClientInfoView] = useState<'overview' | 'defender'>('overview');
const hasDefender = useMemo(() => Boolean(dataset && defenderModelForDataset(dataset).available), [dataset]);
useEffect(() => {
if (!hasDefender) setClientInfoView('overview');
}, [hasDefender]);
if (!dataset || !activeSheet) return app.targetProjectId
?
: ;
if (dataset.profileId === DEFENDER_FOR_IOT_PROFILE_ID) return ;
const clientInfoTabs = hasDefender ?
{([['overview', 'Overview'], ['defender', 'Microsoft Defender for IoT']] as const).map(([id, title]) =>
setClientInfoView(id)}>{title} )}
: undefined;
if (clientInfoView === 'defender' && hasDefender) {
return ;
}
const {
total, statusFacet, severityFacet, sourceFacet, tools, reports, statusCount,
sourceScores, domains, unclassifiedDomain, totalResult, overallScore
} = analysis;
const deviations = totalResult.deviations;
const assessmentCoverage = totalResult.coverage;
const nativePingCastleRisk = dashboardObservationNumber(environment.snapshot, 'risk.domain')
?? dashboardObservationNumber(environment.snapshot, 'risk.global');
const userAccounts = dashboardEnvironmentTableNumber(environment.snapshot, 'sectionUserInformation', /^nb user accounts$/i);
const computerAccounts = dashboardEnvironmentTableNumber(environment.snapshot, 'sectionComputerInformation', /^nb computer accounts$/i);
const domainControllers = dashboardObservationNumber(environment.snapshot, 'domain.controller_count');
const maesterSource = sourceScores.find(source => /maester/i.test(source.tool.label));
const entraTenants = dashboardMaesterTenantCount(environment.snapshot)
?? dashboardObservationNumber(environment.snapshot, 'entra.configuration_detected')
?? (maesterSource?.records.length ? 1 : undefined);
const gpoCount = gpoInventory.gpoCount ?? derivedGpoCount;
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 = unclassifiedDomain?.deviations ? [...domains, unclassifiedDomain] : 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 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
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} />)}
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} )}
Domain Information
{domainInformation.map(item =>
{item.label} {item.value} )}
Assessment Gaps {totalResult.unassessed.toLocaleString('en-US')} Unassessed controls
{totalResult.reevaluationRequired > 0 &&
GPO re-evaluation required
{totalResult.reevaluationRequired.toLocaleString('en-US')} saved comparisons await re-evaluation. Open the GPO import in Data Hub and select Re-evaluate to calculate current results.
jump(() => setStatusFilter('all'))}>View saved results
}
Manual review{(totalResult.reviewRequired + statusCount('manual')).toLocaleString('en-US')}
{totalResult.reevaluationRequired > 0 &&
GPO re-evaluation{totalResult.reevaluationRequired.toLocaleString('en-US')} }
Missing evidence{Math.max(0, totalResult.missingEvidence - statusCount('manual')).toLocaleString('en-US')}
{scoreComparison &&
Awaiting revalidation{dataset.securityScoring?.historyUnavailable ? '—' : scoreComparison.awaitingRevalidation.reduce((sum, item) => sum + item.occurrenceIds.length, 0).toLocaleString('en-US')} }
jump(() => setStatusFilter(statusCount('not-run') ? 'not-run' : 'all'))}>Review gaps →
{scoringOpen &&
setScoringOpen(false)} />}
Environment Detail
{environment.failed ? 'Scanner context could not be loaded' : environment.snapshot
? `${environment.snapshot.observations.length.toLocaleString('en-US')} observations · ${environment.snapshot.sections.length.toLocaleString('en-US')} sections`
: 'Loading scanner context…'}
{environmentDetail.restricted.length > 0 && {environmentDetail.restricted.length.toLocaleString('en-US')} restricted sections }
{environmentDetail.restricted.length > 0 && setRestrictedOpen(value => !value)}>
{environmentDetail.restricted.length.toLocaleString('en-US')} restricted sections
}
{restrictedOpen && environmentDetail.restricted.length > 0
&&
Restricted by the source tool, not empty: {environmentDetail.restricted.join(', ')}
}
Source Overview setScoringOpen(true)}>Scoring methodology ↗
{orderedSourceScores.length ? {orderedSourceScores.map(source => jump(() => {
setFacetFilter(sourceFacet.id, { values: source.tool.sourceValues });
// A source drilldown opens its saved results, including GPO review,
// conflict and duplicate rows. Explicit deviation actions stay filtered.
if (/gpo/i.test(source.tool.label)) setStatusFilter('all');
}) : undefined} />)} : No source results available.
}
} />
jump(() => setPathFindingIds([recordId]))} />
);
}