Assessor_2.0/src/features/project-import/PolicyAnalysisPanel.tsx

183 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useEffect, useState } from 'react';
import { AssessorApiClient } from '../../api/assessor-api-client';
import type {
PolicyAnalysisOverview,
PolicyAnalysisReviewCategory,
PolicyAnalysisReviewItem
} from '../../domain/policy-analysis-overview';
import { PolicyAnalysisActions } from './PolicyAnalysisActions';
import { PanelLoadingState } from '../../components/PanelLoadingState/PanelLoadingState';
import { queryAllUnifiedResults } from '../../exports/unified-result-jsonl';
import { currentPolicyEvaluationsForWork } from './load-canonical-project-workspace';
import { buildPolicyWorkResults, countPolicyWorkResults, policyMisconfigurationImpact, type PolicyWorkCounts } from '../../evaluation/policy-work-results';
const assessorApi = new AssessorApiClient();
const categoryLabels: Record<PolicyAnalysisReviewCategory, string> = {
conflict: 'Conflict',
'unresolved-identity': 'Unresolved setting',
'catalog-gap': 'Baseline mapping unavailable',
'parser-gap': 'Unsupported setting format',
'partial-analysis': 'Evaluation incomplete',
'targeting-unknown': 'Targeting not resolved',
'incomplete-evidence': 'Source evidence incomplete',
other: 'Review required'
};
function formatTimestamp(value: string): string {
return new Intl.DateTimeFormat('en-US', { dateStyle: 'short', timeStyle: 'medium' }).format(new Date(value));
}
function ReviewRow({ item }: { item: PolicyAnalysisReviewItem }) {
return <details className={`policy-review-item category-${item.category}`}>
<summary>
<strong>{categoryLabels[item.category]}</strong>
<span>{item.observationCount} observation{item.observationCount === 1 ? '' : 's'} · {item.backupCount} GPO source{item.backupCount === 1 ? '' : 's'}</span>
</summary>
<div className="policy-review-detail">
<span>Source: {item.source}</span>
{item.identityId && <span>Technical identity: <code>{item.identityId}</code></span>}
<span>Diagnostic{item.codes.length === 1 ? '' : 's'}: {item.codes.map(code => <code key={code}>{code}</code>)}</span>
</div>
</details>;
}
export function PolicyAnalysisPanelView({ overview, counts, impact }: { overview: PolicyAnalysisOverview; counts?: PolicyWorkCounts; impact?: ReturnType<typeof policyMisconfigurationImpact> }) {
const visibleReviews = overview.current.reviewItems.slice(0, 100);
const currentSummary = overview.current.summary;
const profileById = new Map(overview.profiles.map(profile => [profile.id, profile]));
const hasPipelineData = overview.summary.identityAnalysisRuns > 0 || overview.summary.aggregationRuns > 0
|| overview.summary.evaluationRuns > 0 || overview.summary.findingRuns > 0;
return <section className="policy-analysis-panel" aria-label="Policy analysis results">
<div className="datahub-section-heading">
<div><span>GPO Analysis</span><h3>Baseline Evaluation</h3></div>
</div>
<p className="policy-analysis-boundary" role="note">
Backup settings are compared with the imported Microsoft Security Baselines. Effective application requires RSoP or GP Results.
</p>
{!hasPipelineData ? <div className="datahub-empty policy-analysis-empty">
<h3>No policy analysis runs yet</h3>
<p>Imported GPO backups remain processing evidence until snapshot aggregation and baseline evaluation have run.</p>
</div> : <>
<div className="policy-analysis-result-groups">
<section className="policy-analysis-result-group" aria-labelledby="gpo-import-summary">
<h4 id="gpo-import-summary">Processing</h4>
<div className="policy-analysis-summary">
<article><span>Analysis runs</span><strong>{currentSummary.identityAnalysisRuns}</strong></article>
<article><span>Evaluation runs</span><strong>{currentSummary.evaluationRuns}</strong></article>
<article><span>GPO backups with failing settings</span><strong>{impact?.backups ?? '–'}</strong><small>Inspect source-linked comparisons with the same-named filter in Work.</small></article>
</div>
</section>
<section className="policy-analysis-result-group" aria-labelledby="gpo-result-summary">
<h4 id="gpo-result-summary">Policy verdicts</h4>
<p className="policy-analysis-muted">One verdict per technical policy setting.</p>
<div className="policy-analysis-summary">
<article data-result="fail"><span>Misconfigured</span><strong>{counts?.Fail ?? '–'}</strong></article>
<article data-result="pass"><span>Pass</span><strong>{counts?.Pass ?? '–'}</strong></article>
<article><span>Not configured</span><strong>{counts?.['Not configured'] ?? '–'}</strong></article>
<article><span>Review required</span><strong>{counts?.['Review required'] ?? '–'}</strong></article>
<article><span>No baseline rule</span><strong>{counts?.['No baseline rule'] ?? '–'}</strong><small>Configured settings with no applicable Microsoft baseline recommendation. Score-neutral.</small></article>
<article><span>Not comparable</span><strong>{counts?.['Not comparable'] ?? '–'}</strong><small>Group Policy Preferences. No comparable technical policy setting. Score-neutral.</small></article>
</div>
</section>
<section className="policy-analysis-result-group" aria-labelledby="gpo-diagnostic-summary">
<h4 id="gpo-diagnostic-summary">Diagnostic flags</h4>
<p className="policy-analysis-muted">May overlap with each other and with policy verdicts. Score-neutral.</p>
<div className="policy-analysis-summary policy-analysis-problems">
<article><span>Conflicts</span><strong>{counts?.Conflicts ?? '–'}</strong><small>Different customer values for one technical setting.</small></article>
<article><span>Duplicates</span><strong>{counts?.Duplicates ?? '–'}</strong><small>Identical values from multiple GPO sources.</small></article>
</div>
</section>
<section className="policy-analysis-result-group" aria-labelledby="gpo-gap-summary">
<h4 id="gpo-gap-summary">Analysis gaps</h4>
<p className="policy-analysis-muted">Observation-level diagnostics. No score impact.</p>
<div className="policy-analysis-summary policy-analysis-problems">
<article><span>Unresolved identity</span><strong>{currentSummary.unresolvedIdentities}</strong><small>Observations that could not be mapped to one technical policy setting. Their settings appear in Work as Not comparable.</small></article>
<article><span>Comparison not supported</span><strong>{currentSummary.parserGaps}</strong><small>The retained value cannot yet be compared safely; the setting stays under Review required.</small></article>
</div>
</section>
</div>
<details className="policy-analysis-technical">
<summary>Technical details</summary>
<div className="policy-analysis-columns">
<section>
<div className="policy-analysis-subheading"><h4>Evaluation history</h4><span>{overview.evaluations.length}</span></div>
{!overview.evaluations.length ? <p className="policy-analysis-muted">No baseline evaluation has been persisted yet.</p>
: <div className="policy-evaluation-list">{overview.evaluations.map(run => <article key={run.evaluationRunId}>
<header><strong>{profileById.get(run.environmentProfileId)?.name ?? 'Environment profile'} · {run.status === 'complete' ? 'complete' : run.status}</strong><span>{formatTimestamp(run.completedAt)}</span></header>
<div><span>{run.matchCount} matches</span><span>{run.deviationCount} stored mismatches (including missing settings)</span><span>{run.notApplicableCount} not applicable</span><span>{run.reviewCount} review required</span></div>
<footer><strong>{run.findingCount} confirmed finding{run.findingCount === 1 ? '' : 's'}</strong>{run.rejectedFindingCount > 0 && <span>{run.rejectedFindingCount} factory rejection{run.rejectedFindingCount === 1 ? '' : 's'}</span>}</footer>
</article>)}</div>}
</section>
<section>
<div className="policy-analysis-subheading"><h4>Current review queue</h4><span>{overview.current.reviewItems.length}</span></div>
{!visibleReviews.length ? <p className="policy-analysis-muted">No conflicts or unresolved evidence in the persisted runs.</p>
: <div className="policy-review-list">{visibleReviews.map(item => <ReviewRow item={item} key={item.id}/>)}</div>}
{overview.current.reviewItems.length > visibleReviews.length && <p className="policy-analysis-muted">Showing the first 100 deterministic review hints.</p>}
</section>
</div>
</details>
</>}
</section>;
}
export function PolicyAnalysisPanel({ projectId }: { projectId: string }) {
const [overview, setOverview] = useState<PolicyAnalysisOverview>();
const [counts, setCounts] = useState<PolicyWorkCounts>();
const [impact, setImpact] = useState<ReturnType<typeof policyMisconfigurationImpact>>();
const [error, setError] = useState<string>();
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
try {
const [nextOverview, observations, evaluations] = await Promise.all([
assessorApi.getPolicyAnalysisOverview(projectId),
queryAllUnifiedResults(query => assessorApi.queryUnifiedResults(query), {
audience: 'internal-analysis', projectId, sourceKind: ['policy-observation']
}),
assessorApi.listPolicyEvaluations(projectId)
]);
setOverview(nextOverview);
const currentEvaluationIds = new Set(nextOverview.current.evaluationRunIds);
const workResults = buildPolicyWorkResults(observations,
currentPolicyEvaluationsForWork(evaluations.records, new Set(observations.map(source => source.sourceResultId)), currentEvaluationIds));
setCounts(countPolicyWorkResults(workResults));
setImpact(policyMisconfigurationImpact(workResults));
setError(undefined);
} catch (reason) {
setCounts(undefined);
setImpact(undefined);
setError(reason instanceof Error ? reason.message : 'Policy analysis could not be loaded.');
} finally {
setLoading(false);
}
}, [projectId]);
useEffect(() => {
void load();
const refresh = (event: Event) => {
if (event instanceof CustomEvent && event.detail?.projectId && event.detail.projectId !== projectId) return;
void load();
};
globalThis.addEventListener('assessor:gpo-changed', refresh);
globalThis.addEventListener('focus', refresh);
return () => {
globalThis.removeEventListener('assessor:gpo-changed', refresh);
globalThis.removeEventListener('focus', refresh);
};
}, [load, projectId]);
if (loading && !overview) return <section className="policy-analysis-panel"><PanelLoadingState message="Loading policy analysis …" compact/></section>;
if (error && !overview) return <section className="policy-analysis-panel"><div className="datahub-alert" role="alert"><span>{error}</span><button className="btn" type="button" onClick={() => void load()}>Reload</button></div></section>;
return overview ? <>
{error && <div className="datahub-alert" role="alert">Analysis could not be refreshed.</div>}
<PolicyAnalysisPanelView overview={overview} counts={counts} impact={impact}/>
<details className="policy-analysis-controls">
<summary>Processing controls</summary>
<PolicyAnalysisActions projectId={projectId} overview={overview} onChanged={async () => {
await load();
globalThis.dispatchEvent(new CustomEvent('assessor:gpo-changed', { detail: { projectId } }));
}}/>
</details>
</> : null;
}