2.25.0: integrate the rebased branch with Michael's Remediation base

- Client Info: Defender, Compliance and AI Overview move into Michael's
  ClientInfoTabs (Overview, Security, Defender, Compliance, AI Overview);
  engine tabs are lazy so hidden tabs start no requests.
- Drop the old client-info-tabs nav styles; compliance switches use the
  shared defender-tabs segmented style.
- Port the derived-forest note into client-info-facts.ts; tolerate
  observations without attributes.
- Risk register ring opens the matched record (recordId is optional for
  scoped acceptances).
- Import gate: additive synthetic Defender vulnerability and alert contracts.
  PingCastle snapshots stay unchanged pending review.
- Version 2.25.0; AGENTS engine list names the Remediation Engine.
This commit is contained in:
Binh 2026-10-12 10:53:44 +02:00
parent 644f1c3287
commit 6f1abbd749
22 changed files with 1854 additions and 59 deletions

View File

@ -76,7 +76,7 @@
## Context layer (Phase 13E)
- Latest explicit user decision 2026-10-01 supersedes the preceding permanent-Context decision: the Work toolbar has an `Engines` button at the former Context-button position. It stays marked blue, including on hover and while the popup is closed (an explicit colour-role exception for this control only). The shared popup lists Core Engine, Context Engine, Report Engine and, since the explicit user decision of 2026-10-05, Compliance Engine with checkboxes. All default to active; Core Engine is always checked and cannot be disabled. User choices for optional engines are stored locally under the shared Core-owned engine-activation contract. Disabling Context removes its UI, stops new context requests and clears the active Context Cluster filter; disabling Report blocks new report generation from the UI; disabling Compliance removes the Finding Reader ISO action and panel, the Result DB `Compliance mapping` batch action and `Export JSON`, the Client Info `Compliance` and `AI Overview` tabs and the Data Hub ISO corpus tile and stops their requests, and re-enabling starts the Finding Reader ISO panel closed. As the only server-side effect of this local preference (explicit user decision 2026-10-08), disabling Compliance also switches the automatic ISO mapping of new uploads off for the whole server; re-enabling does not switch it on again. Already submitted backend work is not cancelled by this local preference; its late UI output is suppressed. Cluster badges still select the exact Work filter, and the Context detail section still starts collapsed. Engine independence and Core invariance remain binding.
- Latest explicit user decision 2026-10-01 supersedes the preceding permanent-Context decision: the Work toolbar has an `Engines` button at the former Context-button position. It stays marked blue, including on hover and while the popup is closed (an explicit colour-role exception for this control only). The shared popup lists Core Engine, Context Engine, Report Engine, Remediation Engine (see `Remediation Engine (2026-10-08)`) and, since the explicit user decision of 2026-10-05, Compliance Engine with checkboxes, in this order. All default to active; Core Engine is always checked and cannot be disabled. User choices for optional engines are stored locally under the shared Core-owned engine-activation contract. Disabling Context removes its UI, stops new context requests and clears the active Context Cluster filter; disabling Report blocks new report generation from the UI; disabling Compliance removes the Finding Reader ISO action and panel, the Result DB `Compliance mapping` batch action and `Export JSON`, the Client Info `Compliance` and `AI Overview` tabs and the Data Hub ISO corpus tile and stops their requests, and re-enabling starts the Finding Reader ISO panel closed. As the only server-side effect of this local preference (explicit user decision 2026-10-08), disabling Compliance also switches the automatic ISO mapping of new uploads off for the whole server; re-enabling does not switch it on again. Already submitted backend work is not cancelled by this local preference; its late UI output is suppressed. Cluster badges still select the exact Work filter, and the Context detail section still starts collapsed. Engine independence and Core invariance remain binding.
- Explicit user decision 2026-10-01 supersedes the earlier Context on/off, default-off, locally saved switch, hover-button and badge-scroll agreements: Context is permanently active and the global toolbar Context button is removed. Ignore obsolete locally saved on/off preferences. Clicking a Finding Reader Context cluster badge selects that exact `Context Cluster` Work facet value, replacing the previous cluster selection while preserving other filters. The Context detail section still starts collapsed for each finding and opens through its own summary. All read-only, provenance and verdict/score boundaries remain binding; only an explicit cluster-filter selection restricts the Work record set.

View File

@ -4577,6 +4577,7 @@ Verifiziert 2026-09-09 (Phase 7): Bereichswechsel beschleunigt und Performance-V
Verifiziert 2026-09-08 (Phase 7/13B): GPO-Quellenkorrektur fuer Work und Client Info. Aktuelle Evaluationen aus dem vorhandenen autoritativen Readmodell statt ergebnisabhaengiger Backupgruppierung; Import- und Auswertungszeit getrennt, Source-Scan-Provenienz auch auf GPO-only-Blaettern. 1.014 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und beide Builds erfolgreich. Dummy lesend bestaetigt: eine statt drei GPO-Quellenoptionen, 3.805 aktuelle Zeilen, elf historische Evaluationen unveraendert. Keine Persistenzmutation oder Neuanalyse.
| Datum | Änderung |
| 2026-10-12 | Integration (Core-Freigabe alle, Governance): Michaels Commit `614ac1b` (Remediation Engine, geschuetzte Importvertraege, Client-Info-Tab `Security`, Technical Report) ist die Basis; die 39 Commits seit `250c213` (AI/ISO/BSI-Compliance-Engine, Defender for IoT) sind darauf rebased (Branch `rebase/michael-backup`, `compliance-mapping` unveraendert). Beide Engines stehen nebeneinander in Engines-Popup (Core, Context, Report, Remediation, Compliance), Hook-Tabelle, Grenztest, Server und Work-Facetten (Work Package an der Stelle von Result Message, ISO/BSI-Facetten weiter direkt vor `Source: Scan`). Client Info nutzt Michaels `ClientInfoTabs` in der vereinbarten Reihenfolge Overview, Security, Microsoft Defender for IoT, Compliance, AI Overview; die drei Engine-/Defender-Tabs sind `lazy` und mounten nur ausgewaehlt (neuer Verhaltenstest), die alte Tab-Navigation und ihre CSS-Regeln entfallen. Finding Reader: `Map ISO controls` steht direkt vor Michaels fester Aktionsgruppe `AI search`/Bookmark/`Results`. Abgeleiteter Forest-Hinweis nach `client-info-facts.ts` portiert. Importgate: zwei additive synthetische Defender-Vertraege (Schwachstellen- und Alert-CSV, je 3 Findings); die Snapshots `pingcastle-html`/`pingcastle-xml` weichen durch die freigegebenen PingCastle-Aenderungen aus 2.19.1 ab (Profil 1.5.0 -> 1.7.0, XML-Domaenenfakten, Vergleichsschluessel) und bleiben bis zur Nutzerpruefung unveraendert, das Gate ist deshalb rot. Sechs weitere Testfehler (Context-Cluster-Reihenfolge, Pathfinder-Mock, Data-Hub-Donut- und DB-Structure-Layouttest) bestehen bereits auf `614ac1b` allein. Produktversion 2.25.0 (Minor: beide Funktionslinien vereint); Versionstests, beide Produktionsbuilds, Local-only- und Designpruefung bestanden. Vor dem ersten Serverstart legt die Remediation Engine das Schema `assessor_remediation` an; vorher Backup des kanonischen Datastores. |
| 2026-10-06 | Phase 7: Data Hub Deviation/Passed now use the same assessed-percentage helper as Client Info; all six donuts state their population basis. 29 focused tests and synthetic Chrome Dark/Light checks passed; absolute counts, other population shares and assessment semantics retained. Client build, design and UI/API version checks passed. Nine out-of-scope report test failures, a report Local-only failure and report server type errors prevent full release verification; attempted 2.16.9 reverted to 2.16.8 pending successful production builds. |
| 2026-10-06 | Phase 7: Client Info Deviations percentage now divides by Passed + Deviations and labels Of assessed checks; zero assessed shows a dash while preserving absolute zero. Permanent UI/click invariance tests in both themes and synthetic Chrome presentation verified; unchanged counts, verdicts, scores and other donuts. Patch 2.16.8: both builds and version contracts passed; full run 1,936 passed/15 skipped with one stale API-version assertion corrected and passed in the 43-test focused rerun; Local-only/design checks passed. |
| 2026-10-06 | Phase 13B: GPO folder read diagnostics retain only safe error names, stages and counts, distinguish empty/denied/unreadable selections, preserve cancellation and wrap complete timeslot messages. 27 focused GPO UI tests and synthetic Chrome Dark/Light checks passed; both builds and version contracts verified for patch 2.16.7. Full run: 1,932 passed, 15 skipped, one general import timeout passed on separate rerun; Local-only/design checks passed. Customer-specific read failure remains unconfirmed; no customer path/data used, Phase 13B remains in progress. |

View File

@ -45,3 +45,8 @@ All files below are deliberately small, synthetic, and free of customer or compa
`maester/application-json-binding.synthetic.html` protects the current static declaration in an application/json script. `maester/multi-tenant.synthetic.html` protects separate native tenant paths. Both contain only invented Dummy values.
Compatibility references use synthetic data only, including when a real report reveals a new structure.
## Microsoft Defender for IoT
- Files: `defender-for-iot/cves.synthetic.csv` (vulnerability export) and `defender-for-iot-alerts/alerts.synthetic.csv` (alert export)
- Basis: the same fictional devices, CVE IDs from the reserved `CVE-2099-*` range, TEST-NET addresses and documentation MACs as the Defender importer tests. Added 2026-10-12 as additive contracts when the protected gate met the existing Defender profiles.

View File

@ -0,0 +1,4 @@
Id,Title,First Detection,Last Detection,Last Activity,Severity,Description,Acknowledged,Additional Information,Source Address,Destination Address
A-1,Synthetic Unauthorized PLC Programming,11/09/2026 10:05:12,11/09/2026 10:07:11,11/09/2026 10:07:11,Critical,Synthetic programming from 192.0.2.41,False,192.0.2.21 over port CIP (44818),192.0.2.41,192.0.2.21
A-3,Synthetic Internet Connection,13/09/2026 09:00:00,13/09/2026 09:30:00,13/09/2026 09:30:00,Major,Synthetic outbound connection,False,198.51.100.7 over port HTTPS (443),00-00-5E-00-53-01,
A-4,Synthetic Unused Protocol,14/09/2026 07:00:00,14/09/2026 07:00:00,14/09/2026 07:05:00,Warning,Synthetic unused protocol,True,,192.0.2.50,192.0.2.51
1 Id Title First Detection Last Detection Last Activity Severity Description Acknowledged Additional Information Source Address Destination Address
2 A-1 Synthetic Unauthorized PLC Programming 11/09/2026 10:05:12 11/09/2026 10:07:11 11/09/2026 10:07:11 Critical Synthetic programming from 192.0.2.41 False 192.0.2.21 over port CIP (44818) 192.0.2.41 192.0.2.21
3 A-3 Synthetic Internet Connection 13/09/2026 09:00:00 13/09/2026 09:30:00 13/09/2026 09:30:00 Major Synthetic outbound connection False 198.51.100.7 over port HTTPS (443) 00-00-5E-00-53-01
4 A-4 Synthetic Unused Protocol 14/09/2026 07:00:00 14/09/2026 07:00:00 14/09/2026 07:05:00 Warning Synthetic unused protocol True 192.0.2.50 192.0.2.51

View File

@ -0,0 +1,4 @@
Device ID,Device Name,IP Address,Site,Sensor,CVE,CVSS Score,Severity,Component,Status
DEV-OT-001,PLC-LINE1-01,192.0.2.21,Riverbend Plant,SENS-RB-01,CVE-2099-1001,9.1,Critical,Synthetic component,Open
DEV-OT-002,HMI-LINE1-01,192.0.2.31,Riverbend Plant,SENS-RB-01,CVE-2099-1002,7.5,High,Synthetic component,Open
DEV-OT-004,HISTORIAN-01,192.0.2.51,Riverbend Plant,SENS-RB-01,CVE-2099-1005,8.2,High,Synthetic component,Mitigation planned
1 Device ID Device Name IP Address Site Sensor CVE CVSS Score Severity Component Status
2 DEV-OT-001 PLC-LINE1-01 192.0.2.21 Riverbend Plant SENS-RB-01 CVE-2099-1001 9.1 Critical Synthetic component Open
3 DEV-OT-002 HMI-LINE1-01 192.0.2.31 Riverbend Plant SENS-RB-01 CVE-2099-1002 7.5 High Synthetic component Open
4 DEV-OT-004 HISTORIAN-01 192.0.2.51 Riverbend Plant SENS-RB-01 CVE-2099-1005 8.2 High Synthetic component Mitigation planned

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "assessor",
"version": "2.25.6",
"version": "2.25.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "assessor",
"version": "2.25.6",
"version": "2.25.0",
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"jsdom": "^29.1.1",

View File

@ -1,6 +1,6 @@
{
"name": "assessor",
"version": "2.25.6",
"version": "2.25.0",
"private": true,
"type": "module",
"scripts": {

View File

@ -28,7 +28,7 @@ export const footprintStep = (findings: number) => findings >= 25 ? 4 : findings
/** Switches the overview row between the frameworks; the framework tiles below stay as they are. */
export function ComplianceFrameworkSwitch({ value, onChange }: { value: ComplianceFramework; onChange(value: ComplianceFramework): void }) {
// The Client Info tab buttons: the approved selection control of this view.
return <div className="client-info-tabs compliance-framework-switch" role="group" aria-label="Compliance overview framework">
return <div className="defender-tabs compliance-framework-switch" role="group" aria-label="Compliance overview framework">
{FRAMEWORKS.map(([id, label]) => <button key={id} type="button" data-framework-view={id}
aria-pressed={value === id} onClick={() => onChange(id)}>{label}</button>)}
</div>;

View File

@ -75,7 +75,7 @@ function ReviewList({ review, projectId, backendStatus, onChanged }: {
<p className="mapping-review__intro">Each rule compares the BSI requirements accepted in its direct BSI mapping with the requirements the BSI
mapping {review.crosswalkVersion} derives from its accepted ISO controls. Agreement shows consistency, not correctness. Nothing here changes a
decision until you accept or reject one.</p>
<div className="client-info-tabs mapping-review__filters" role="group" aria-label="Consistency state">
<div className="defender-tabs mapping-review__filters" role="group" aria-label="Consistency state">
{FILTERS.map(option => {
const amount = review.items.filter(item => item.state === option.state).length;
return <button key={option.state} type="button" aria-pressed={filter === option.state}

View File

@ -4,8 +4,10 @@ export interface ClientInfoTab {
id: string;
label: string;
content?: ReactNode;
/** Defer optional engine views until opened, then keep their local table state. */
/** Mount the content only while the tab is selected, so a hidden tab starts no requests. */
lazy?: boolean;
/** With `lazy`: keep the content mounted after its first visit, so its local table state survives. */
retain?: boolean;
}
/** Shared attached-panel presentation for every Client Info tab. */
@ -39,6 +41,6 @@ export function ClientInfoTabs({ tabs }: { tabs: readonly ClientInfoTab[] }) {
</div>
{tabs.map(tab => <section key={tab.id} className="client-info-tabpanel" role="tabpanel"
id={`${prefix}-panel-${tab.id}`} aria-labelledby={`${prefix}-tab-${tab.id}`}
hidden={tab.id !== active} data-group-scroll={`client-info-${tab.id}`} tabIndex={0}>{!tab.lazy || tab.id === active || visited.includes(tab.id) ? tab.content : null}</section>)}
hidden={tab.id !== active} data-group-scroll={`client-info-${tab.id}`} tabIndex={0}>{!tab.lazy || tab.id === active || (tab.retain && visited.includes(tab.id)) ? tab.content : null}</section>)}
</div>;
}

View File

@ -1151,7 +1151,6 @@ export function dashboardAnalysis(
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' | 'compliance' | 'ai'>('overview');
const hasDefender = useMemo(() => Boolean(dataset && defenderModelForDataset(dataset).available), [dataset]);
const pureDefender = dataset?.profileId === DEFENDER_FOR_IOT_PROFILE_ID;
const environment = useEnvironmentSnapshot(app.targetProjectId, app.activeScanRunIds);
@ -1165,12 +1164,6 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
if (showCompliance && app.targetProjectId) preloadClientComplianceOverview(app.targetProjectId, app.activeAssessmentIds, app.activeScanRunIds);
}, [showCompliance, app.targetProjectId, app.activeAssessmentIds, app.activeScanRunIds]);
useEffect(() => {
if (pureDefender && clientInfoView === 'overview') setClientInfoView('defender');
else if (clientInfoView === 'defender' && !hasDefender) setClientInfoView('overview');
else if (clientInfoView === 'ai' && !showAiOverview) setClientInfoView(pureDefender ? 'defender' : 'overview');
else if (clientInfoView === 'compliance' && !showCompliance) setClientInfoView(pureDefender ? 'defender' : 'overview');
}, [clientInfoView, hasDefender, pureDefender, showAiOverview, showCompliance]);
const gpoInventory = useGpoInventory(app.targetProjectId);
const sourceFilter = app.facetFilters['provenance:source-scan'];
const analysis = useDashboardAnalysis(activeSheet, dataset, activeProfile, reviews, sourceFilter);
@ -1263,19 +1256,8 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
setView('columns');
onOpenWork();
};
const clientInfoViews: Array<readonly ['overview' | 'defender' | 'compliance' | 'ai', string]> = [
...(pureDefender ? [] : [['overview', 'Overview'] as const]),
...(hasDefender ? [['defender', 'Microsoft Defender for IoT'] as const] : []),
...(showCompliance ? [['compliance', 'Compliance'] as const] : []),
...(showAiOverview ? [['ai', 'AI Overview'] as const] : [])
];
const clientInfoTabs = clientInfoViews.length > 1 ? <nav className="client-info-tabs" aria-label="Client Info views">
{clientInfoViews.map(([id, title]) =>
<button type="button" key={id} aria-pressed={clientInfoView === id} onClick={() => setClientInfoView(id)}>{title}</button>)}
</nav> : undefined;
if (clientInfoView === 'ai' && showAiOverview) return <AiOverviewPanel controller={aiOverview} navigation={clientInfoTabs} />;
// Compliance Engine tab: one tile per framework; a Results window opens a control or theme in Work.
if (clientInfoView === 'compliance' && showCompliance) return <ClientIsoControls navigation={clientInfoTabs} projectId={app.targetProjectId} assessmentIds={app.activeAssessmentIds} scanRunIds={app.activeScanRunIds}
const complianceContent = showCompliance ? <ClientIsoControls projectId={app.targetProjectId} assessmentIds={app.activeAssessmentIds} scanRunIds={app.activeScanRunIds}
backendStatus={app.backendStatus} onOpenControl={controlId => jump(() => {
const targetSheet = sourceScanNavigationSheet(dataset, activeSheet);
if (targetSheet) {
@ -1320,12 +1302,10 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
context={<header className="client-overview-header">
<div className="client-overview-identity"><span>Client:</span><strong>{dataset.fileName}</strong></div>
<div className="client-overview-assessment"><span>Assessment:</span><WorkTimeslotControl /></div>
</header>}/>;
if (hasDefender && (clientInfoView === 'defender' || pureDefender)) {
return <DefenderVulnerabilityPanel dataset={dataset} navigation={clientInfoTabs} reviews={reviews}
</header>}/> : null;
const defenderContent = hasDefender ? <DefenderVulnerabilityPanel dataset={dataset} reviews={reviews}
scopedRiskAcceptances={app.scopedRiskAcceptances} activeScanRunIds={app.activeScanRunIds}
onScopedRiskAcceptance={app.backendStatus === 'connected' ? write => app.recordScopedRiskAcceptance(write) : undefined} />;
}
onScopedRiskAcceptance={app.backendStatus === 'connected' ? write => app.recordScopedRiskAcceptance(write) : undefined} /> : null;
const {
total, statusFacet, severityFacet, sourceFacet, tools, reports,
@ -1498,8 +1478,7 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
}
return (
<><ClientInfoTabs tabs={[{ id: 'overview', label: 'Overview', content: <div className="dashboard-page client-command-center">
{clientInfoTabs}
<><ClientInfoTabs tabs={[...(pureDefender ? [] : [{ id: 'overview', label: 'Overview', content: <div className="dashboard-page client-command-center">
<header className="client-overview-header">
<div className="client-overview-identity"><span>Client:</span><strong>{dataset.fileName}</strong></div>
<div className="client-overview-assessment"><span>Assessment:</span><WorkTimeslotControl /></div>
@ -1623,8 +1602,12 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
</div>
<RiskRegister dataset={dataset} reviews={reviews} scopedRiskAcceptances={app.scopedRiskAcceptances} onOpenFinding={recordId => jump(() => setPathFindingIds([recordId]))} />
</div> }, { id: 'security', label: 'Security', content: securityPanelView }, ...(engines.remediation ? [{ id: 'work-packages', label: 'Work Packages', lazy: true,
content: <ClientWorkPackagesPanel key={reportScopeKey}/> }] : [])]} />
</div> }, { id: 'security', label: 'Security', content: securityPanelView }]),
...(engines.remediation ? [{ id: 'work-packages', label: 'Work Packages', lazy: true, retain: true,
content: <ClientWorkPackagesPanel key={reportScopeKey}/> }] : []),
...(defenderContent ? [{ id: 'defender', label: 'Microsoft Defender for IoT', content: defenderContent, lazy: true }] : []),
...(complianceContent ? [{ id: 'compliance', label: 'Compliance', content: complianceContent, lazy: true }] : []),
...(showAiOverview ? [{ id: 'ai', label: 'AI Overview', content: <AiOverviewPanel controller={aiOverview} />, lazy: true }] : [])]} />
{scoringOpen && <ScoringMethodology sources={scoringOpen === 'security' ? securityAnalysis.sourceScores : sourceScores} context={dataset.securityScoring} onClose={() => setScoringOpen(null)} />}</>
);
}

View File

@ -137,7 +137,7 @@ export function RiskRegister({ dataset, reviews, scopedRiskAcceptances = [], onO
<span><strong>{entry.title}</strong><small>{entry.source}</small></span>
<em>{entry.state}</em>
<time dateTime={entry.latest.details.reviewOn || entry.latest.recordedAt}>Review {displayDate(entry.latest.details.reviewOn)}</time>
</button>{recordFor(entry) && <RelatedFindingRing record={recordFor(entry)!} onGroupOpen={() => onOpenFinding?.(entry.recordId)}/>}</div>)}
</button>{recordFor(entry) && <RelatedFindingRing record={recordFor(entry)!} onGroupOpen={() => onOpenFinding?.(recordFor(entry)!.id)}/>}</div>)}
</nav>
</aside>
<main className="client-risk-register-information">
@ -146,7 +146,7 @@ export function RiskRegister({ dataset, reviews, scopedRiskAcceptances = [], onO
<div><strong>Risk Information</strong><span>{selected.source}</span></div>
<div className="risk-register-actions">{onOpenFinding && selected.recordId && <button className="btn primary" type="button" onClick={() => onOpenFinding(selected.recordId!)}>Open finding</button>}<strong className={`is-${stateTone(selected.state)}`}>{selected.state}</strong></div>
</header>
<section className="risk-register-title"><span>Finding</span><h2>{selected.title}</h2>{recordFor(selected) && <RelatedFindingRing record={recordFor(selected)!} onGroupOpen={() => onOpenFinding?.(selected.recordId)}/>}</section>
<section className="risk-register-title"><span>Finding</span><h2>{selected.title}</h2>{recordFor(selected) && <RelatedFindingRing record={recordFor(selected)!} onGroupOpen={() => onOpenFinding?.(recordFor(selected)!.id)}/>}</section>
{selected.coveredCves && <section className="risk-register-scoped-target"><span>Immutable system scope</span><p>{selected.device}</p><p>{selected.coveredCves.length.toLocaleString('en-US')} covered CVE{selected.coveredCves.length === 1 ? '' : 's'}: {selected.coveredCves.join(', ')}</p></section>}
<dl className="risk-register-summary">
<DetailField label="Initial risk" value={selected.latest.details.initialRisk} />

View File

@ -73,10 +73,10 @@ export function factText(value: string | undefined): string {
*/
function forestFact(snapshot: EnvironmentSnapshot | undefined): DashboardFact {
const observed = (snapshot?.observations ?? []).filter(item => item.property === 'forest.name' && item.state === 'observed');
const observation = observed.find(item => !item.attributes.derivation) ?? observed[0];
const observation = observed.find(item => !item.attributes?.derivation) ?? observed[0];
if (!observation) return { label: 'Forest', value: UNKNOWN_FACT };
const value = factText(displayValue(observation));
return observation.attributes.derivation === 'configuration-partition-dn' && value !== UNKNOWN_FACT
return observation.attributes?.derivation === 'configuration-partition-dn' && value !== UNKNOWN_FACT
? { label: 'Forest', value, note: 'derived from configuration path' }
: { label: 'Forest', value };
}

View File

@ -6,11 +6,11 @@
.defender-metrics article { display: grid; align-content: center; gap: var(--space-2); min-width: 0; min-height: 88px; padding: var(--tile-inset); border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: none; }
.defender-metrics span { color: var(--muted); font-size: var(--type-table-body-size); font-weight: var(--fw-semibold); line-height: 1.3; }
.defender-metrics strong { color: var(--heading); font-size: var(--type-h2-size); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; line-height: 1; }
.defender-tabs, .client-info-tabs { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.defender-tabs button, .client-info-tabs button { min-height: 36px; padding: var(--space-2) var(--space-4); border: var(--border-hair) solid transparent; border-radius: var(--btn-radius); color: var(--text); background: transparent; font-size: var(--ui-control-size); font-weight: var(--fw-semibold); cursor: pointer; }
.defender-tabs button:hover, .client-info-tabs button:hover { background: var(--tile-surface-hover); }
.defender-tabs button:focus-visible, .client-info-tabs button:focus-visible { outline: var(--bw-emphasis) solid var(--focus-ring); outline-offset: calc(-1 * var(--bw-emphasis)); }
.defender-tabs button[aria-pressed="true"], .client-info-tabs button[aria-pressed="true"] { outline: var(--bw-emphasis) solid var(--marking-color); outline-offset: calc(-1 * var(--bw-emphasis)); background: var(--tile-surface); }
.defender-tabs { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.defender-tabs button { min-height: 36px; padding: var(--space-2) var(--space-4); border: var(--border-hair) solid transparent; border-radius: var(--btn-radius); color: var(--text); background: transparent; font-size: var(--ui-control-size); font-weight: var(--fw-semibold); cursor: pointer; }
.defender-tabs button:hover { background: var(--tile-surface-hover); }
.defender-tabs button:focus-visible { outline: var(--bw-emphasis) solid var(--focus-ring); outline-offset: calc(-1 * var(--bw-emphasis)); }
.defender-tabs button[aria-pressed="true"] { outline: var(--bw-emphasis) solid var(--marking-color); outline-offset: calc(-1 * var(--bw-emphasis)); background: var(--tile-surface); }
.defender-view { display: grid; gap: var(--space-4); min-width: 0; padding: var(--tile-inset); border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); }
.defender-view > header { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: var(--space-2); }
.defender-view > header h2 { margin: 0; color: var(--heading); font-size: var(--type-h2-size); font-weight: var(--fw-semibold); }

View File

@ -9,7 +9,7 @@ const components = readFileSync(new URL('../../src/styles/components.css', impor
describe('ASSESSOR version in the brand header', () => {
it('shows the centrally maintained product version and opens About from the brand', () => {
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.25.6');
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.25.0');
expect(app).toContain("'db-structure': 'Data Flow'");
expect(app).toContain("['db-structure', 'Data Flow']");
expect(app).not.toContain("'DB Structure'");

View File

@ -39,9 +39,8 @@ describe('Client Info AI Overview', () => {
it('adds the AI tab beside the Client Info overview', () => {
const dashboard = readFileSync(new URL('../../src/features/dashboard/DashboardPanel.tsx', import.meta.url), 'utf8');
expect(dashboard).toContain("[['overview', 'Overview'] as const]");
expect(dashboard).toContain('{clientInfoTabs}');
expect(dashboard).toContain("...(showAiOverview ? [['ai', 'AI Overview'] as const] : [])");
expect(dashboard).toContain("clientInfoView === 'ai'");
expect(dashboard).toContain("[{ id: 'overview', label: 'Overview', content:");
// The AI tab is the last Client Info tab and mounts only while selected.
expect(dashboard).toContain("...(showAiOverview ? [{ id: 'ai', label: 'AI Overview', content: <AiOverviewPanel controller={aiOverview} />, lazy: true }] : [])]} />");
});
});

View File

@ -157,14 +157,17 @@ describe('Client Info ISO controls', () => {
it('is its own Client Info tab, not part of the Overview, and opens Work through the ISO control and theme filters', () => {
const source = readFileSync(join(process.cwd(), 'src/features/dashboard/DashboardPanel.tsx'), 'utf8');
expect(source).toContain("...(showCompliance ? [['compliance', 'Compliance'] as const] : [])");
expect(source.indexOf("['compliance', 'Compliance']")).toBeLessThan(source.indexOf("['ai', 'AI Overview']"));
const tab = source.slice(source.indexOf("if (clientInfoView === 'compliance' && showCompliance) return <ClientIsoControls"));
expect(tab.startsWith("if (clientInfoView === 'compliance' && showCompliance) return <ClientIsoControls navigation={clientInfoTabs}")).toBe(true);
expect(source).toContain("...(complianceContent ? [{ id: 'compliance', label: 'Compliance', content: complianceContent, lazy: true }] : [])");
// Agreed order: Overview, Security, Microsoft Defender for IoT, Compliance, AI Overview.
const order = ["{ id: 'overview'", "{ id: 'security'", "{ id: 'defender'", "{ id: 'compliance'", "{ id: 'ai'"].map(id => source.indexOf(id));
expect(order.every(index => index > 0)).toBe(true);
expect([...order].sort((a, b) => a - b)).toEqual(order);
const tab = source.slice(source.indexOf('const complianceContent = showCompliance ? <ClientIsoControls'));
expect(tab.startsWith('const complianceContent = showCompliance ? <ClientIsoControls projectId=')).toBe(true);
// The Overview page no longer renders the row.
expect(source.indexOf('<ClientIsoControls')).toBe(source.lastIndexOf('<ClientIsoControls'));
expect(source.indexOf('<ClientIsoControls')).toBeLessThan(source.indexOf('<ClientContextHeatmap'));
const drilldown = tab.slice(0, tab.indexOf('if (hasDefender'));
const drilldown = tab.slice(0, tab.indexOf('const defenderContent'));
expect(drilldown).toContain("setStatusFilter('deviation')");
expect(drilldown).toContain('setFacetFilter(ISO_CONTROL_FACET_ID, { values: [controlId] })');
expect(drilldown).toContain('setFacetFilter(ISO_THEME_FACET_ID, { values: [theme.toLowerCase()] })');

View File

@ -80,3 +80,28 @@ describe.each(['dark', 'light'])('Client Info attached tabs (%s)', theme => {
} finally { await act(async () => root.unmount()); container.remove(); vi.unstubAllGlobals(); }
});
});
describe('Client Info lazy engine tabs', () => {
it('mounts a lazy tab only while it is selected, so hidden engine tabs start no requests', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const mounted = vi.fn();
function Probe() { mounted(); return <p>Synthetic engine tab</p>; }
const host = document.createElement('div'); document.body.append(host);
const root = createRoot(host);
try {
await act(async () => root.render(<ClientInfoTabs tabs={[
{ id: 'overview', label: 'Overview', content: <p>Synthetic overview</p> },
{ id: 'compliance', label: 'Compliance', content: <Probe/>, lazy: true }
]}/>));
const tabs = host.querySelectorAll<HTMLButtonElement>('[role="tab"]');
const panels = host.querySelectorAll<HTMLElement>('[role="tabpanel"]');
expect(mounted).not.toHaveBeenCalled();
expect(panels[1].childNodes).toHaveLength(0);
await act(async () => tabs[1].click());
expect(mounted).toHaveBeenCalled();
expect(panels[1].textContent).toBe('Synthetic engine tab');
await act(async () => tabs[0].click());
expect(panels[1].childNodes).toHaveLength(0);
} finally { await act(async () => root.unmount()); host.remove(); vi.unstubAllGlobals(); }
});
});

View File

@ -51,9 +51,9 @@ describe('Defender vulnerability dashboard projection', () => {
expect(component).toContain('className="dashboard-page client-command-center defender-page"');
expect(component).toContain('className="client-overview-header defender-header"');
expect(component).toContain('<span>Source:</span><strong>Microsoft Defender for IoT</strong>');
expect(dashboard).toContain("[['overview', 'Overview'] as const]");
expect(dashboard).toContain("[['defender', 'Microsoft Defender for IoT'] as const]");
expect(dashboard).toContain('<DefenderVulnerabilityPanel dataset={dataset} navigation={clientInfoTabs} reviews={reviews}');
expect(dashboard).toContain("...(pureDefender ? [] : [{ id: 'overview', label: 'Overview', content:");
expect(dashboard).toContain("...(defenderContent ? [{ id: 'defender', label: 'Microsoft Defender for IoT', content: defenderContent, lazy: true }] : [])");
expect(dashboard).toContain('const defenderContent = hasDefender ? <DefenderVulnerabilityPanel dataset={dataset} reviews={reviews}');
expect(dashboard).toContain('scopedRiskAcceptances={app.scopedRiskAcceptances} activeScanRunIds={app.activeScanRunIds}');
expect(dashboard).toContain('onScopedRiskAcceptance={app.backendStatus === \'connected\' ? write => app.recordScopedRiskAcceptance(write) : undefined}');
expect(styles).toContain('padding: var(--panel-inset)');

File diff suppressed because it is too large Load Diff

View File

@ -196,5 +196,27 @@
"mediaType": "text/html",
"sha256": "2a080e0facc21626b51b91305e0163382d63e6bc24a8d26d0312300d580afe56",
"variant": "original"
},
{
"id": "defender-for-iot-vulnerabilities-csv",
"fixture": "fixtures/defender-for-iot/cves.synthetic.csv",
"tool": "defender-for-iot-vulnerabilities",
"format": "CSV",
"findings": 3,
"fileName": "cves.csv",
"mediaType": "text/csv",
"sha256": "cee1fb7363f1f065a89341b755a27ec9727866d60dc4bc46089561582eda1d8e",
"variant": "original"
},
{
"id": "defender-for-iot-alerts-csv",
"fixture": "fixtures/defender-for-iot-alerts/alerts.synthetic.csv",
"tool": "defender-for-iot-alerts",
"format": "CSV",
"findings": 3,
"fileName": "alerts.csv",
"mediaType": "text/csv",
"sha256": "c554f60079cf276f21c90b2b9c0b4b3fe6976945fd726b6d1ba1331391e3c184",
"variant": "original"
}
]

View File

@ -60,7 +60,7 @@ describe('local ASSESSOR HTTP server', () => {
const health = await fetch(`${baseUrl}/api/v1/health`);
expect(await health.json()).toEqual({
status: 'ok', application: 'ASSESSOR', productVersion: '2.25.6', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
status: 'ok', application: 'ASSESSOR', productVersion: '2.25.0', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
});
expect(health.headers.get('access-control-allow-origin')).toBeNull();
});