Assessor_2.0/src/features/dashboard/DashboardPanel.tsx

738 lines
39 KiB
TypeScript

import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react';
import { AssessorApiClient } from '../../api/assessor-api-client';
import { useApp } from '../../app/AppProvider';
import { EmptyWorkspace } from '../../components/EmptyWorkspace/EmptyWorkspace';
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 { 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, type DynamicFacet, type DynamicFacetValue } from '../search-filter/dynamic-facets';
import { WorkTimeslotControl } from '../search-filter/WorkTimeslotControl';
const assessorApi = new AssessorApiClient();
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 = <>
<div className={`dashboard-status-ring dashboard-status-ring-${tone}`}>
<svg viewBox="0 0 120 120" aria-hidden="true">
<circle className="dashboard-status-ring-track" cx="60" cy="60" r="48" pathLength="100" />
<circle className="dashboard-status-ring-progress" cx="60" cy="60" r="48" pathLength="100" strokeDasharray={`${roundedPercentage} ${100 - roundedPercentage}`} />
</svg>
<span><span className={`dashboard-donut-primary${roundedPercentage >= 100 ? ' is-compact' : ''}`}><strong>{roundedPercentage}</strong><small>%</small></span></span>
</div>
<div className="dashboard-status-copy">
<span>{label}</span>
{!large && <strong>{value.toLocaleString('en-US')}</strong>}
{helper && <small>{helper}</small>}
</div>
</>;
const className = `dashboard-status-metric${large ? ' dashboard-status-metric-large' : ''}`;
return onClick
? <button className={className} type="button" aria-label={`${label}: ${value.toLocaleString('en-US')}, ${roundedPercentage}%`} aria-pressed={Boolean(active)} onClick={onClick}>{content}</button>
: <article className={className}>{content}</article>;
}
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<string, DashboardRiskTool>();
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 (
<section className="dashboard-block dashboard-distribution">
<h2>{heading}</h2>
{values.length === 0 ? (
<p className="dashboard-empty">This scan report carries no such classification.</p>
) : (
<>
<div className="dashboard-segmented-bar" aria-hidden="true">
{values.map((value, index) => (
<span
className={`dashboard-segment ${severity ? `severity-${value.value}` : `dashboard-viz-${index % 6 + 1}`}`}
key={value.value}
style={shareStyle(share(value.count, total))}
/>
))}
</div>
<ul className="dashboard-distribution-legend">
{values.map((value, index) => (
<li key={value.value}>
<button
className="dashboard-legend-button"
type="button"
onClick={() => onSelect(value.value)}
title={`Open in Work, filtered by ${value.label}`}
>
<span className={`dashboard-legend-swatch ${severity ? `severity-${value.value}` : `dashboard-viz-${index % 6 + 1}`}`} aria-hidden="true" />
<span className="dashboard-legend-copy">
<span>{formatLabel(value.label)}</span>
<strong>{value.count.toLocaleString('en-US')} <small>· {share(value.count, total).toFixed(0)}%</small></strong>
</span>
</button>
</li>
))}
</ul>
</>
)}
</section>
);
}
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<EnvironmentSnapshot>();
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<string, EnvironmentObservation>();
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 (
<section className="dashboard-environment" aria-labelledby="environment-overview-heading">
<div className="datahub-section-heading">
<div><h2 id="environment-overview-heading">Directory overview</h2></div>
{snapshot && <strong>{snapshot.sections.length.toLocaleString('en-US')} sections</strong>}
</div>
{!snapshot && !failed && <p className="dashboard-empty">Loading scanner context…</p>}
{failed && <p className="dashboard-empty">Scanner context could not be loaded.</p>}
{snapshot && snapshot.sections.length === 0 && <p className="dashboard-empty">This import predates structured scanner-context capture.</p>}
{snapshot && (
<div className="dashboard-coverage-summary" aria-label="Directory coverage summary">
<span><strong>{snapshot.observations.length.toLocaleString('en-US')}</strong><small>typed observations</small></span>
<span><strong>{(snapshot.coverage.empty + snapshot.coverage.explicitlyAbsent).toLocaleString('en-US')}</strong><small>empty or absent</small></span>
<span><strong>{snapshot.coverage.restricted.toLocaleString('en-US')}</strong><small>restricted</small></span>
<span><strong>{snapshot.coverage.incomplete.toLocaleString('en-US')}</strong><small>incomplete</small></span>
</div>
)}
{snapshot && (observations.length > 0 || operatingSystems.length > 0) && (
<dl className="dashboard-facts">
{observations.map(({ label, observation }) => (
<div key={observation.property}><dt>{label}</dt><dd>{displayValue(observation)}</dd></div>
))}
{operatingSystems.length > 0 && <div><dt>OS</dt><dd>{operatingSystems.join(', ')}</dd></div>}
</dl>
)}
</section>
);
}
interface RiskProfileProps {
sourceFacet?: DynamicFacet;
categoryFacet?: DynamicFacet;
severityFacet?: DynamicFacet;
records: FindingRecord[];
profile: ImportProfile;
reviews: Record<string, Review>;
environmentSnapshot?: EnvironmentSnapshot;
onSelectTool(sourceValues: string[]): void;
onSelectCategory(value: string, sourceValues: string[]): void;
onSelectSeverity(value: string, sourceValues: string[]): void;
}
type RiskScoreObservation = Pick<EnvironmentObservation, 'property' | 'normalizedValue' | 'originalValue'>;
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 (
<section className="dashboard-block dashboard-risk-profile">
<header className="dashboard-risk-header">
{tools.length > 0 ? <select className="dashboard-risk-tool" aria-label="Risk profile tool" value={effectiveProfileId} onChange={event => setSelectedToolId(event.target.value)}>
<option value={fullRiskProfileId}>Full Risk Profile</option>
{tools.map(tool => <option value={tool.id} key={tool.id}>{tool.label} Risk Profile</option>)}
</select> : <h2>Risk profile</h2>}
<div className="dashboard-marked-summary"><span>Marked for report</span><strong>{marked.toLocaleString('en-US')} / {deviations.toLocaleString('en-US')}</strong></div>
</header>
<div className="dashboard-risk-body">
{categories.length > 0 ? (
<ul className="dashboard-risk-categories">
{categories.slice(0, 4).map(category => (
<li key={category.value}>
<button type="button" onClick={() => {
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}`}>
<span>{category.label}</span><strong>{category.count.toLocaleString('en-US')}</strong>
</button>
</li>
))}
</ul>
) : <p className="dashboard-empty">No risk categories available.</p>}
<div className="dashboard-risk-severity">
{severities.length > 0 ? <>
<div className="dashboard-risk-donut" role="img" aria-label="Risk severity distribution: 100%">
<svg viewBox="0 0 120 120" aria-hidden="true">
<circle className="dashboard-risk-donut-track" cx="60" cy="60" r="48" pathLength="100" />
{severities.map(severity => {
const percentage = share(severity.count, severityTotal);
const offset = severityOffset;
severityOffset += percentage;
return <circle
className={`dashboard-risk-donut-segment severity-${severity.value}`}
cx="60"
cy="60"
r="48"
key={severity.value}
pathLength="100"
strokeDasharray={`${percentage} ${100 - percentage}`}
strokeDashoffset={-offset}
/>;
})}
</svg>
<span><span className="dashboard-donut-primary is-compact"><strong>100</strong><small>%</small></span></span>
</div>
<ul className="dashboard-risk-legend">
{severities.map(severity => (
<li key={severity.value}>
<button type="button" onClick={() => fullProfile
? onSelectSeverity(severity.value, [])
: selectedTool && onSelectSeverity(severity.value, selectedTool.sourceValues)} title={`Open in Work, filtered by ${severity.label}`}>
<i className={`severity-${severity.value}`} aria-hidden="true" />
<span>{severity.label}</span>
<strong>{severity.count.toLocaleString('en-US')}</strong>
</button>
</li>
))}
</ul>
</> : <p className="dashboard-empty">No severity distribution available.</p>}
</div>
</div>
</section>
);
}
interface DashboardEnvironmentMetric {
label: string;
value: number;
detail?: string;
restricted?: boolean;
}
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') },
{ label: 'PingCastle', value: observationText(snapshot, 'report.engine_version', 'PingCastle engine version') }
],
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;
active?: boolean;
onClick?(): void;
}
function DashboardMetricTile({ label, value, tone = 'neutral', active, onClick }: DashboardMetricTileProps) {
const content = <><span>{label}</span><strong>{value}</strong></>;
const className = `client-info-metric tone-${tone}`;
return onClick
? <button className={className} type="button" aria-pressed={Boolean(active)} onClick={onClick}>{content}</button>
: <article className={className}>{content}</article>;
}
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 <div className="client-info-donut-layout">
<div className="client-info-donut" role="img" aria-label={`${centerLabel}: ${total.toLocaleString('en-US')}`}>
<svg viewBox="0 0 120 120" aria-hidden="true">
<circle className="client-info-donut-track" cx="60" cy="60" r="48" pathLength="100" />
{segments.filter(segment => segment.value > 0).map(segment => {
const percentage = share(segment.value, total);
const segmentOffset = offset;
offset += percentage;
return <circle className={`client-info-donut-segment tone-${segment.tone}`} cx="60" cy="60" r="48" pathLength="100" key={segment.label}
strokeDasharray={`${percentage} ${100 - percentage}`} strokeDashoffset={-segmentOffset} />;
})}
</svg>
<span><strong>{total.toLocaleString('en-US')}</strong><small>{centerLabel}</small></span>
</div>
<ul className="client-info-legend">
{segments.map(segment => <li key={segment.label}>{segment.onClick
? <button type="button" onClick={segment.onClick}><i className={`tone-${segment.tone}`} /><span>{segment.label}</span><strong>{segment.value.toLocaleString('en-US')}</strong></button>
: <span className="client-info-legend-row"><i className={`tone-${segment.tone}`} /><span>{segment.label}</span><strong>{segment.value.toLocaleString('en-US')}</strong></span>}</li>)}
</ul>
</div>;
}
function DashboardFactList({ items }: { items: Array<{ label: string; value: string }> }) {
return <dl className="client-info-facts">{items.map(item => <div key={item.label}><dt>{item.label}</dt><dd>{item.value}</dd></div>)}</dl>;
}
function DashboardMetricBars({ items, tone = 'accent' }: { items: DashboardEnvironmentMetric[]; tone?: string }) {
const maximum = Math.max(1, ...items.map(item => item.value));
return <ul className="client-info-bars">{items.map(item => <li key={item.label}>
<span>{item.label}</span>
<span className="client-info-bar-track"><i className={`tone-${tone}`} style={shareStyle(share(item.value, maximum))} /></span>
<strong>{item.value.toLocaleString('en-US')}{item.detail ? ` ${item.detail}` : ''}</strong>
{item.restricted && <small title="Some source details are restricted">Restricted</small>}
</li>)}</ul>;
}
function DashboardLargeCard({ title, meta, className = '', children }: { title: string; meta?: string; className?: string; children: ReactNode }) {
return <section className={`client-info-card ${className}`.trim()}><header><h2>{title}</h2>{meta && <span>{meta}</span>}</header>{children}</section>;
}
export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
const app = useApp();
const { dataset, activeSheet, activeProfile, reviews, setStatusFilter, setFacetFilter, setPathFindingIds, resetFilters, setView } = app;
const [clientInfoView, setClientInfoView] = useState<'overview' | 'defender'>('overview');
const facets = useMemo(
() => (activeSheet ? deriveDynamicFacets(activeSheet, dataset, activeProfile, reviews) : []),
[activeProfile, activeSheet, dataset, reviews]
);
const environment = useEnvironmentSnapshot(app.targetProjectId, app.activeScanRunIds);
const hasDefender = useMemo(() => Boolean(dataset && defenderModelForDataset(dataset).available), [dataset]);
useEffect(() => {
if (!hasDefender) setClientInfoView('overview');
}, [hasDefender]);
if (!dataset || !activeSheet) return <EmptyWorkspace />;
if (dataset.profileId === DEFENDER_FOR_IOT_PROFILE_ID) return <DefenderVulnerabilityPanel dataset={dataset} />;
const clientInfoTabs = hasDefender ? <nav className="client-info-tabs" aria-label="Client Info views">
{([['overview', 'Overview'], ['defender', 'Microsoft Defender for IoT']] as const).map(([id, title]) =>
<button type="button" key={id} aria-pressed={clientInfoView === id} onClick={() => setClientInfoView(id)}>{title}</button>)}
</nav> : undefined;
if (clientInfoView === 'defender' && hasDefender) {
return <DefenderVulnerabilityPanel dataset={dataset} navigation={clientInfoTabs} />;
}
const total = activeSheet.analysis.records.length;
const statusFacet: DynamicFacet | undefined = facets.find(facet => facet.source === 'profile-status');
const severityFacet: DynamicFacet | undefined = facets.find(facet => facet.severityScale);
const categoryFacet: DynamicFacet | undefined = facets.find(facet => facet.role === 'category' || /^(?:category|kategorie)$/i.test(facet.label.trim()));
const sourceFacet: DynamicFacet | undefined = facets.find(facet =>
/^(?:quelle|source):\s*scan$/i.test(facet.label.trim()) || facet.source === 'provenance');
const cisCatTool = dashboardRiskTools(sourceFacet?.values ?? [])
.find(tool => /^cis-cat(?:\s+pro)?$/i.test(tool.label.trim()));
const cisCatSeverityValues = cisCatTool && sourceFacet && severityFacet
? recountFacetsAgainstActiveFilters({
facets: [sourceFacet, severityFacet],
records: activeSheet.analysis.records,
filters: { [sourceFacet.id]: { values: cisCatTool.sourceValues } },
reviews,
statusFilter: 'all',
matchesStatus: (record, status) => activeProfile.matchesStatus(record, status)
}).find(facet => facet.id === severityFacet.id)?.values ?? []
: [];
const cisCatSeverityRemainder = dashboardCisCatSeverityRemainder(sourceFacet?.values ?? [], cisCatSeverityValues);
const severityDistributionValues = [
...(severityFacet?.values ?? []),
...(cisCatSeverityRemainder ? [cisCatSeverityRemainder] : [])
];
const reports = sourceFacet?.values.length ?? 0;
const statusCount = (value: string) => statusFacet?.values.find(item => item.value === value)?.count ?? 0;
const deviations = statusCount('deviation');
const passed = statusCount('passed');
const notApplicable = statusCount('not-applicable');
const notRun = statusCount('not-run');
const errors = statusCount('error');
const explicitIoeRecordIds = dashboardExplicitIoeRecordIds(activeSheet.analysis.records);
const explicitIoes = explicitIoeRecordIds.length;
const complianceScore = passed + deviations > 0 ? Math.round((passed / (passed + deviations)) * 100) : 0;
const highSeverity = severityFacet?.values.find(value => value.value === 'high')?.count ?? 0;
const environmentSummary = dashboardEnvironmentSummary(environment.snapshot);
const typedObservationCount = environment.snapshot?.observations.length ?? 0;
const directorySectionCount = environment.snapshot?.sections.length ?? 0;
const jump = (apply: () => void) => {
resetFilters();
setStatusFilter(app.statusFilter);
apply();
setView('columns');
onOpenWork();
};
return (
<div className="dashboard-page">
{clientInfoTabs}
<p className="dashboard-context">
<strong>{dataset.fileName}</strong>
<span>{reports.toLocaleString('en-US')} scan {reports === 1 ? 'report' : 'reports'}</span>
<span>{total.toLocaleString('en-US')} records</span>
<WorkTimeslotControl />
</p>
<div className="client-info-tile-grid">
<section className="client-info-metric-grid" aria-label="Client metrics">
<DashboardMetricTile label="Compliance Score" value={`${complianceScore}%`} />
<DashboardMetricTile label="Deviation" value={deviations.toLocaleString('en-US')} tone="deviation" active={app.statusFilter === 'deviation'} onClick={() => jump(() => setStatusFilter('deviation'))} />
<DashboardMetricTile label="Passed" value={passed.toLocaleString('en-US')} tone="passed" active={app.statusFilter === 'passed'} onClick={() => jump(() => setStatusFilter('passed'))} />
<DashboardMetricTile label="High Severity" value={highSeverity.toLocaleString('en-US')} tone="high" onClick={() => severityFacet && jump(() => setFacetFilter(severityFacet.id, { values: ['high'] }))} />
<DashboardMetricTile label="Typed Observations" value={typedObservationCount.toLocaleString('en-US')} />
<DashboardMetricTile label="Directory Sections" value={directorySectionCount.toLocaleString('en-US')} />
</section>
<div className="client-info-card-grid">
<DashboardLargeCard title="Assessment Status" meta={`${total.toLocaleString('en-US')} records`}>
<DashboardDonut total={total} centerLabel="records" segments={[
{ label: 'Deviation', value: deviations, tone: 'deviation', onClick: () => jump(() => setStatusFilter('deviation')) },
{ label: 'Passed', value: passed, tone: 'passed', onClick: () => jump(() => setStatusFilter('passed')) },
{ label: 'Not Run', value: notRun, tone: 'not-run', onClick: () => jump(() => setStatusFilter('not-run')) },
{ label: 'Not Applicable', value: notApplicable, tone: 'neutral', onClick: () => jump(() => setStatusFilter('not-applicable')) },
{ label: 'Error', value: errors, tone: 'error', onClick: () => jump(() => setStatusFilter('error')) },
{ label: 'IOEs found', value: explicitIoes, tone: 'info', onClick: explicitIoes > 0 ? () => jump(() => { setStatusFilter('all'); setPathFindingIds(explicitIoeRecordIds); }) : undefined }
]} />
</DashboardLargeCard>
<DashboardLargeCard title="Severity Distribution" meta={`${severityDistributionValues.reduce((sum, value) => sum + value.count, 0).toLocaleString('en-US')} classified`}>
<DashboardDonut total={Math.max(1, severityDistributionValues.reduce((sum, value) => sum + value.count, 0))} centerLabel="classified" segments={severityDistributionValues.map(value => ({
label: value.label,
value: value.count,
tone: value.value,
onClick: () => {
if (value.value === dashboardCisCatSeverityValue && sourceFacet && cisCatSeverityRemainder) {
jump(() => setFacetFilter(sourceFacet.id, { values: cisCatSeverityRemainder.sourceValues }));
} else if (severityFacet) jump(() => setFacetFilter(severityFacet.id, { values: [value.value] }));
}
}))} />
</DashboardLargeCard>
<DashboardLargeCard title="Directory Overview" meta={environment.failed ? 'Unavailable' : undefined}>
{!environment.snapshot && !environment.failed ? <p className="dashboard-empty">Loading scanner context…</p> : <DashboardFactList items={environmentSummary.directoryFacts} />}
</DashboardLargeCard>
<DashboardLargeCard title="Directory Coverage" meta={`${typedObservationCount.toLocaleString('en-US')} observations`}>
<DashboardMetricBars items={environmentSummary.coverage} tone="coverage" />
<p className="client-info-card-caption">{typedObservationCount.toLocaleString('en-US')} typed observations · {directorySectionCount.toLocaleString('en-US')} sections</p>
</DashboardLargeCard>
<DashboardLargeCard title="Identity & Privilege">
<DashboardMetricBars items={environmentSummary.identity} />
</DashboardLargeCard>
<DashboardLargeCard title="Control Paths & Trusts">
<DashboardMetricBars items={environmentSummary.controlPaths} />
</DashboardLargeCard>
<DashboardLargeCard title="Certificates & PKI">
<DashboardMetricBars items={environmentSummary.certificates} tone="pki" />
</DashboardLargeCard>
<DashboardLargeCard title="GPO & Password Hygiene">
<DashboardMetricBars items={environmentSummary.gpo} tone="gpo" />
</DashboardLargeCard>
<DashboardLargeCard title="Infrastructure & Resilience" meta={`${activeSheet.analysis.records.filter(record => reviews[record.id]?.include).length.toLocaleString('en-US')} marked`} className="client-info-resilience">
<DashboardMetricBars items={environmentSummary.risk} tone="risk" />
<DashboardFactList items={environmentSummary.resilienceFacts} />
</DashboardLargeCard>
</div>
</div>
</div>
);
}