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:
parent
682ce43c45
commit
8d72805b27
|
|
@ -76,7 +76,7 @@
|
||||||
|
|
||||||
## Context layer (Phase 13E)
|
## 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.
|
- 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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4346,6 +4346,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.
|
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 |
|
| 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: 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 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. |
|
| 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. |
|
||||||
|
|
|
||||||
|
|
@ -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.
|
`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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
@ -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,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "assessor",
|
"name": "assessor",
|
||||||
"version": "2.24.2",
|
"version": "2.25.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "assessor",
|
"name": "assessor",
|
||||||
"version": "2.24.2",
|
"version": "2.25.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.3.0",
|
"@fontsource-variable/inter": "^5.3.0",
|
||||||
"jsdom": "^29.1.1",
|
"jsdom": "^29.1.1",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "assessor",
|
"name": "assessor",
|
||||||
"version": "2.24.2",
|
"version": "2.25.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -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. */
|
/** 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 }) {
|
export function ComplianceFrameworkSwitch({ value, onChange }: { value: ComplianceFramework; onChange(value: ComplianceFramework): void }) {
|
||||||
// The Client Info tab buttons: the approved selection control of this view.
|
// 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}
|
{FRAMEWORKS.map(([id, label]) => <button key={id} type="button" data-framework-view={id}
|
||||||
aria-pressed={value === id} onClick={() => onChange(id)}>{label}</button>)}
|
aria-pressed={value === id} onClick={() => onChange(id)}>{label}</button>)}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -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
|
<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
|
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>
|
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 => {
|
{FILTERS.map(option => {
|
||||||
const amount = review.items.filter(item => item.state === option.state).length;
|
const amount = review.items.filter(item => item.state === option.state).length;
|
||||||
return <button key={option.state} type="button" aria-pressed={filter === option.state}
|
return <button key={option.state} type="button" aria-pressed={filter === option.state}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ export interface ClientInfoTab {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
content?: ReactNode;
|
content?: ReactNode;
|
||||||
|
/** Mount the content only while the tab is selected, so a hidden tab starts no requests. */
|
||||||
|
lazy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shared attached-panel presentation for every Client Info tab. */
|
/** Shared attached-panel presentation for every Client Info tab. */
|
||||||
|
|
@ -32,6 +34,6 @@ export function ClientInfoTabs({ tabs }: { tabs: readonly ClientInfoTab[] }) {
|
||||||
</div>
|
</div>
|
||||||
{tabs.map(tab => <section key={tab.id} className="client-info-tabpanel" role="tabpanel"
|
{tabs.map(tab => <section key={tab.id} className="client-info-tabpanel" role="tabpanel"
|
||||||
id={`${prefix}-panel-${tab.id}`} aria-labelledby={`${prefix}-tab-${tab.id}`}
|
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.content}</section>)}
|
hidden={tab.id !== active} data-group-scroll={`client-info-${tab.id}`} tabIndex={0}>{tab.lazy && tab.id !== active ? null : tab.content}</section>)}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1147,7 +1147,6 @@ export function dashboardAnalysis(
|
||||||
export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
|
export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
const { dataset, activeSheet, activeProfile, reviews, setStatusFilter, setFacetFilter, setPathFindingIds, resetFilters, setView } = app;
|
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 hasDefender = useMemo(() => Boolean(dataset && defenderModelForDataset(dataset).available), [dataset]);
|
||||||
const pureDefender = dataset?.profileId === DEFENDER_FOR_IOT_PROFILE_ID;
|
const pureDefender = dataset?.profileId === DEFENDER_FOR_IOT_PROFILE_ID;
|
||||||
const environment = useEnvironmentSnapshot(app.targetProjectId, app.activeScanRunIds);
|
const environment = useEnvironmentSnapshot(app.targetProjectId, app.activeScanRunIds);
|
||||||
|
|
@ -1161,12 +1160,6 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
|
||||||
if (showCompliance && app.targetProjectId) preloadClientComplianceOverview(app.targetProjectId, app.activeAssessmentIds, app.activeScanRunIds);
|
if (showCompliance && app.targetProjectId) preloadClientComplianceOverview(app.targetProjectId, app.activeAssessmentIds, app.activeScanRunIds);
|
||||||
}, [showCompliance, 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 gpoInventory = useGpoInventory(app.targetProjectId);
|
||||||
const sourceFilter = app.facetFilters['provenance:source-scan'];
|
const sourceFilter = app.facetFilters['provenance:source-scan'];
|
||||||
const analysis = useDashboardAnalysis(activeSheet, dataset, activeProfile, reviews, sourceFilter);
|
const analysis = useDashboardAnalysis(activeSheet, dataset, activeProfile, reviews, sourceFilter);
|
||||||
|
|
@ -1251,19 +1244,8 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
|
||||||
setView('columns');
|
setView('columns');
|
||||||
onOpenWork();
|
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.
|
// 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(() => {
|
backendStatus={app.backendStatus} onOpenControl={controlId => jump(() => {
|
||||||
const targetSheet = sourceScanNavigationSheet(dataset, activeSheet);
|
const targetSheet = sourceScanNavigationSheet(dataset, activeSheet);
|
||||||
if (targetSheet) {
|
if (targetSheet) {
|
||||||
|
|
@ -1308,12 +1290,10 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
|
||||||
context={<header className="client-overview-header">
|
context={<header className="client-overview-header">
|
||||||
<div className="client-overview-identity"><span>Client:</span><strong>{dataset.fileName}</strong></div>
|
<div className="client-overview-identity"><span>Client:</span><strong>{dataset.fileName}</strong></div>
|
||||||
<div className="client-overview-assessment"><span>Assessment:</span><WorkTimeslotControl /></div>
|
<div className="client-overview-assessment"><span>Assessment:</span><WorkTimeslotControl /></div>
|
||||||
</header>}/>;
|
</header>}/> : null;
|
||||||
if (hasDefender && (clientInfoView === 'defender' || pureDefender)) {
|
const defenderContent = hasDefender ? <DefenderVulnerabilityPanel dataset={dataset} reviews={reviews}
|
||||||
return <DefenderVulnerabilityPanel dataset={dataset} navigation={clientInfoTabs} reviews={reviews}
|
|
||||||
scopedRiskAcceptances={app.scopedRiskAcceptances} activeScanRunIds={app.activeScanRunIds}
|
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 {
|
const {
|
||||||
total, statusFacet, severityFacet, sourceFacet, tools, reports,
|
total, statusFacet, severityFacet, sourceFacet, tools, reports,
|
||||||
|
|
@ -1373,8 +1353,7 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
|
||||||
: 'all';
|
: 'all';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClientInfoTabs tabs={[{ id: 'overview', label: 'Overview', content: <div className="dashboard-page client-command-center">
|
<ClientInfoTabs tabs={[...(pureDefender ? [] : [{ id: 'overview', label: 'Overview', content: <div className="dashboard-page client-command-center">
|
||||||
{clientInfoTabs}
|
|
||||||
<header className="client-overview-header">
|
<header className="client-overview-header">
|
||||||
<div className="client-overview-identity"><span>Client:</span><strong>{dataset.fileName}</strong></div>
|
<div className="client-overview-identity"><span>Client:</span><strong>{dataset.fileName}</strong></div>
|
||||||
<div className="client-overview-assessment"><span>Assessment:</span><WorkTimeslotControl /></div>
|
<div className="client-overview-assessment"><span>Assessment:</span><WorkTimeslotControl /></div>
|
||||||
|
|
@ -1501,6 +1480,9 @@ export function DashboardPanel({ onOpenWork }: DashboardPanelProps) {
|
||||||
|
|
||||||
<RiskRegister dataset={dataset} reviews={reviews} scopedRiskAcceptances={app.scopedRiskAcceptances} onOpenFinding={recordId => jump(() => setPathFindingIds([recordId]))} />
|
<RiskRegister dataset={dataset} reviews={reviews} scopedRiskAcceptances={app.scopedRiskAcceptances} onOpenFinding={recordId => jump(() => setPathFindingIds([recordId]))} />
|
||||||
</div> }, { id: 'security', label: 'Security', content: <ClientSecurityPanel key={reportScopeKey}
|
</div> }, { id: 'security', label: 'Security', content: <ClientSecurityPanel key={reportScopeKey}
|
||||||
dataset={dataset} sheetId={activeSheet.id} toolId={activeSheet.toolId} contextStatus={impactContext.status} {...securityView}/> }]} />
|
dataset={dataset} sheetId={activeSheet.id} toolId={activeSheet.toolId} contextStatus={impactContext.status} {...securityView}/> }]),
|
||||||
|
...(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 }] : [])]} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ export function RiskRegister({ dataset, reviews, scopedRiskAcceptances = [], onO
|
||||||
<span><strong>{entry.title}</strong><small>{entry.source}</small></span>
|
<span><strong>{entry.title}</strong><small>{entry.source}</small></span>
|
||||||
<em>{entry.state}</em>
|
<em>{entry.state}</em>
|
||||||
<time dateTime={entry.latest.details.reviewOn || entry.latest.recordedAt}>Review {displayDate(entry.latest.details.reviewOn)}</time>
|
<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>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<main className="client-risk-register-information">
|
<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><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>
|
<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>
|
</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>}
|
{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">
|
<dl className="risk-register-summary">
|
||||||
<DetailField label="Initial risk" value={selected.latest.details.initialRisk} />
|
<DetailField label="Initial risk" value={selected.latest.details.initialRisk} />
|
||||||
|
|
|
||||||
|
|
@ -73,10 +73,10 @@ export function factText(value: string | undefined): string {
|
||||||
*/
|
*/
|
||||||
function forestFact(snapshot: EnvironmentSnapshot | undefined): DashboardFact {
|
function forestFact(snapshot: EnvironmentSnapshot | undefined): DashboardFact {
|
||||||
const observed = (snapshot?.observations ?? []).filter(item => item.property === 'forest.name' && item.state === 'observed');
|
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 };
|
if (!observation) return { label: 'Forest', value: UNKNOWN_FACT };
|
||||||
const value = factText(displayValue(observation));
|
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, note: 'derived from configuration path' }
|
||||||
: { label: 'Forest', value };
|
: { label: 'Forest', value };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 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 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-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 { 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 { 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: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: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 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 { 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 { 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); }
|
.defender-view > header h2 { margin: 0; color: var(--heading); font-size: var(--type-h2-size); font-weight: var(--fw-semibold); }
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const components = readFileSync(new URL('../../src/styles/components.css', impor
|
||||||
|
|
||||||
describe('ASSESSOR version in the brand header', () => {
|
describe('ASSESSOR version in the brand header', () => {
|
||||||
it('shows the centrally maintained product version and opens About from the brand', () => {
|
it('shows the centrally maintained product version and opens About from the brand', () => {
|
||||||
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.24.2');
|
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.25.0');
|
||||||
expect(app).toContain("'db-structure': 'Data Flow'");
|
expect(app).toContain("'db-structure': 'Data Flow'");
|
||||||
expect(app).toContain("['db-structure', 'Data Flow']");
|
expect(app).toContain("['db-structure', 'Data Flow']");
|
||||||
expect(app).not.toContain("'DB Structure'");
|
expect(app).not.toContain("'DB Structure'");
|
||||||
|
|
|
||||||
|
|
@ -39,9 +39,8 @@ describe('Client Info AI Overview', () => {
|
||||||
|
|
||||||
it('adds the AI tab beside the Client Info 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');
|
const dashboard = readFileSync(new URL('../../src/features/dashboard/DashboardPanel.tsx', import.meta.url), 'utf8');
|
||||||
expect(dashboard).toContain("[['overview', 'Overview'] as const]");
|
expect(dashboard).toContain("[{ id: 'overview', label: 'Overview', content:");
|
||||||
expect(dashboard).toContain('{clientInfoTabs}');
|
// The AI tab is the last Client Info tab and mounts only while selected.
|
||||||
expect(dashboard).toContain("...(showAiOverview ? [['ai', 'AI Overview'] as const] : [])");
|
expect(dashboard).toContain("...(showAiOverview ? [{ id: 'ai', label: 'AI Overview', content: <AiOverviewPanel controller={aiOverview} />, lazy: true }] : [])]} />");
|
||||||
expect(dashboard).toContain("clientInfoView === 'ai'");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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', () => {
|
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');
|
const source = readFileSync(join(process.cwd(), 'src/features/dashboard/DashboardPanel.tsx'), 'utf8');
|
||||||
expect(source).toContain("...(showCompliance ? [['compliance', 'Compliance'] as const] : [])");
|
expect(source).toContain("...(complianceContent ? [{ id: 'compliance', label: 'Compliance', content: complianceContent, lazy: true }] : [])");
|
||||||
expect(source.indexOf("['compliance', 'Compliance']")).toBeLessThan(source.indexOf("['ai', 'AI Overview']"));
|
// Agreed order: Overview, Security, Microsoft Defender for IoT, Compliance, AI Overview.
|
||||||
const tab = source.slice(source.indexOf("if (clientInfoView === 'compliance' && showCompliance) return <ClientIsoControls"));
|
const order = ["{ id: 'overview'", "{ id: 'security'", "{ id: 'defender'", "{ id: 'compliance'", "{ id: 'ai'"].map(id => source.indexOf(id));
|
||||||
expect(tab.startsWith("if (clientInfoView === 'compliance' && showCompliance) return <ClientIsoControls navigation={clientInfoTabs}")).toBe(true);
|
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.
|
// The Overview page no longer renders the row.
|
||||||
expect(source.indexOf('<ClientIsoControls')).toBe(source.lastIndexOf('<ClientIsoControls'));
|
expect(source.indexOf('<ClientIsoControls')).toBe(source.lastIndexOf('<ClientIsoControls'));
|
||||||
expect(source.indexOf('<ClientIsoControls')).toBeLessThan(source.indexOf('<ClientContextHeatmap'));
|
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("setStatusFilter('deviation')");
|
||||||
expect(drilldown).toContain('setFacetFilter(ISO_CONTROL_FACET_ID, { values: [controlId] })');
|
expect(drilldown).toContain('setFacetFilter(ISO_CONTROL_FACET_ID, { values: [controlId] })');
|
||||||
expect(drilldown).toContain('setFacetFilter(ISO_THEME_FACET_ID, { values: [theme.toLowerCase()] })');
|
expect(drilldown).toContain('setFacetFilter(ISO_THEME_FACET_ID, { values: [theme.toLowerCase()] })');
|
||||||
|
|
|
||||||
|
|
@ -80,3 +80,28 @@ describe.each(['dark', 'light'])('Client Info attached tabs (%s)', theme => {
|
||||||
} finally { await act(async () => root.unmount()); container.remove(); vi.unstubAllGlobals(); }
|
} 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(); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -51,9 +51,9 @@ describe('Defender vulnerability dashboard projection', () => {
|
||||||
expect(component).toContain('className="dashboard-page client-command-center defender-page"');
|
expect(component).toContain('className="dashboard-page client-command-center defender-page"');
|
||||||
expect(component).toContain('className="client-overview-header defender-header"');
|
expect(component).toContain('className="client-overview-header defender-header"');
|
||||||
expect(component).toContain('<span>Source:</span><strong>Microsoft Defender for IoT</strong>');
|
expect(component).toContain('<span>Source:</span><strong>Microsoft Defender for IoT</strong>');
|
||||||
expect(dashboard).toContain("[['overview', 'Overview'] as const]");
|
expect(dashboard).toContain("...(pureDefender ? [] : [{ id: 'overview', label: 'Overview', content:");
|
||||||
expect(dashboard).toContain("[['defender', 'Microsoft Defender for IoT'] as const]");
|
expect(dashboard).toContain("...(defenderContent ? [{ id: 'defender', label: 'Microsoft Defender for IoT', content: defenderContent, lazy: true }] : [])");
|
||||||
expect(dashboard).toContain('<DefenderVulnerabilityPanel dataset={dataset} navigation={clientInfoTabs} reviews={reviews}');
|
expect(dashboard).toContain('const defenderContent = hasDefender ? <DefenderVulnerabilityPanel dataset={dataset} reviews={reviews}');
|
||||||
expect(dashboard).toContain('scopedRiskAcceptances={app.scopedRiskAcceptances} activeScanRunIds={app.activeScanRunIds}');
|
expect(dashboard).toContain('scopedRiskAcceptances={app.scopedRiskAcceptances} activeScanRunIds={app.activeScanRunIds}');
|
||||||
expect(dashboard).toContain('onScopedRiskAcceptance={app.backendStatus === \'connected\' ? write => app.recordScopedRiskAcceptance(write) : undefined}');
|
expect(dashboard).toContain('onScopedRiskAcceptance={app.backendStatus === \'connected\' ? write => app.recordScopedRiskAcceptance(write) : undefined}');
|
||||||
expect(styles).toContain('padding: var(--panel-inset)');
|
expect(styles).toContain('padding: var(--panel-inset)');
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -196,5 +196,27 @@
|
||||||
"mediaType": "text/html",
|
"mediaType": "text/html",
|
||||||
"sha256": "2a080e0facc21626b51b91305e0163382d63e6bc24a8d26d0312300d580afe56",
|
"sha256": "2a080e0facc21626b51b91305e0163382d63e6bc24a8d26d0312300d580afe56",
|
||||||
"variant": "original"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ describe('local ASSESSOR HTTP server', () => {
|
||||||
|
|
||||||
const health = await fetch(`${baseUrl}/api/v1/health`);
|
const health = await fetch(`${baseUrl}/api/v1/health`);
|
||||||
expect(await health.json()).toEqual({
|
expect(await health.json()).toEqual({
|
||||||
status: 'ok', application: 'ASSESSOR', productVersion: '2.24.2', 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();
|
expect(health.headers.get('access-control-allow-origin')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue