feat(defender-for-iot): dashboard layout and outcome marking
Adapted pick of 08771d5. findingOutcomeMark supplies the outcome mark in
the Work card and the Finding Reader header, including sources without a
status column. The Defender panel uses the Client Info header and a
six-column metrics row.
Unlike the original, tiles stay on the shared tile and text tokens
instead of --client-info-* colours, border: 0 and --btn-radius.
This commit is contained in:
parent
0b5dcfbcc6
commit
a181d264bf
|
|
@ -4231,6 +4231,7 @@ Verifiziert 2026-09-08 (Phase 7/13B): GPO-Quellenkorrektur fuer Work und Client
|
|||
| 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. |
|
||||
| 2026-10-02 | Phase 7: `08771d5` angepasst uebernommen; `c056c21` (CWE-Links, baut auf der ausgelassenen CVE-Anreicherung auf) ausgelassen, seine Scoring-Zeile fuer `securityToolId` folgt mit dem Result-DB-Werkzeugeintrag. `findingOutcomeMark` liefert die Outcome-Marke fuer Work-Karte und Finding-Reader-Kopf; Werkzeuge ohne Statusspalte erhalten sie aus `missingStatusOutcome` (Defender: `Warning`), alle anderen behalten ihr eigenes Statuswort. Die GPO-Diagnoseplaketten der Karte und die Vergleichs-Outcome-Ableitung des Readers bleiben unveraendert. Das Defender-Panel nutzt den Client-Info-Kopf (`Client`, `Assessment`, `Source`) und eine sechsspaltige Kennzahlzeile mit Container-Queries. Abweichend vom Original bleiben Kacheln, Sichten und Drilldowns auf den gemeinsamen Rollen `--tile-surface`, `--tile-border`, `--tile-radius`, `--text`, `--heading` und `--muted` statt `--client-info-*`, `border: 0` und `--btn-radius`, damit die verbindliche Kachelregel durchgehend gilt. Nicht im Browser geprueft, weil die lokale Datenbank keine Defender-Importe enthaelt. Keine API-, SQL- oder Persistenzaenderung. Verifikation: beide Typechecks, Gesamtlauf mit 1.893 bestandenen und 17 uebersprungenen Tests, Local-only-/Designpruefung und beide Produktionsbuilds erfolgreich. |
|
||||
| 2026-10-02 | Phase 13A: Aus `33d2019` nur die Dokumentation uebernommen. `docs/CANONICAL_MODEL.md` beschreibt `vulnerability_finding` als additive ASSESSOR-Erweiterung des externen `result_type`-Katalogs; der Kommentar an `CanonicalResultType` verweist darauf. Nicht uebernommen: die Backfill-Migration (frueher 023) samt Katalogeintrag und Schema-Assertions, weil die neu angelegte lokale Datenbank keine Defender-Occurrences enthaelt, sowie auf Nutzerentscheidung der lesende Outcome-Fallback in `unified-result.ts` mit seinem Test; neue Importe erhalten `warning` bereits ueber `missingStatusOutcome`. Keine Verhaltens-, API-, SQL- oder Persistenzaenderung. Verifikation: beide Typechecks, Gesamtlauf mit 1.890 bestandenen und 17 uebersprungenen Tests, Local-only-/Designpruefung und beide Produktionsbuilds erfolgreich. |
|
||||
| 2026-10-02 | Phasen 6/13A: `fe71246` uebernommen; `e048295` (NVD-CVE-Anreicherung) auf Nutzerentscheidung vorerst ausgelassen. Der Result-Contract kennt den Typ `vulnerability_finding`; Defender-Findings erhalten ihn. Ein Profil kann `missingStatusOutcome` deklarieren, Defender setzt `warning`, weil jede CVE-Zeile selbst der Befund ist. Das Defender-Profil steigt auf 1.1.0 mit Kompatibilitaetsschluessel `-v2` (semantic-breaking, Reimport erforderlich; die lokale Datenbank enthaelt keine Defender-Importe), 1.0.0 ist nicht mehr aktuell. Der Work-Filter `Deviation` aller Scanner nutzt statt der Regex-Liste `UNIVERSAL_DEVIATION_PATTERNS` nun `isDeviationRecord` und damit das kanonische Result-DB-Praedikat; die GPO-Statuslogik, der Statuscache und die uebrigen Statusfilter der Basis bleiben unveraendert davor. Alle bestehenden Work-, Filter- und Scannertests bestehen unveraendert; eine vollstaendige Gleichwertigkeit fuer jeden realen Statuswert ist damit nicht bewiesen. Migration `027_vulnerability_finding_result_type.sql` (frueher 022) erweitert nur den Check-Constraint und schreibt keine Occurrences um; ohne die ausgelassene CVE-Tabelle. Vor der Anwendung wurde das verifizierte Backup `backups/assessor-pre-027-20261002-123608.dump` erstellt. Verifikation: beide Typechecks, Gesamtlauf mit 1.890 bestandenen und 17 uebersprungenen Tests, Local-only-/Designpruefung und beide Produktionsbuilds erfolgreich. |
|
||||
| 2026-10-02 | Phasen 6/7/13A: `5fbbda2` uebernommen. Der Defender-Extractor erzeugt nun ein Finding je CVE statt je Device-mal-CVE und liefert `Affected Device Count` sowie die Tabelle `Affected Devices` (Device, Scanner, Site); das Profil bildet die Anzahl auf die Asset-Rolle ab, nutzt `Affected Devices` als Ergebnisabschnitt und vergleicht nur noch ueber die CVE. Der Finding Reader blendet das Rohfeld `Source Observations` fuer Defender aus, es bleibt als Provenienz im Datensatz. Nicht uebernommen: die Umbenennung der Asset-Zeile im Result-DB-Zeilendetail, weil die Disclosure-Policy `affectedAsset` fuer jede Zeile zurueckhaelt und eine Device-Anzahl dort nicht erscheinen soll. Die Finding-Identitaet aendert sich wie im Original ohne Profilversionssprung; die neu angelegte lokale Datenbank enthaelt keine Defender-Importe, der Versionssprung folgt mit `c7c716c`. Keine API-, SQL- oder Persistenzmigration. Verifikation: beide Typechecks, Gesamtlauf mit 1.883 bestandenen und 17 uebersprungenen Tests, Local-only-/Designpruefung und beide Produktionsbuilds erfolgreich. |
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import { memo } from 'react';
|
|||
import { RelatedFindingRing, useRelatedFindings } from '../../features/related-findings/RelatedFindings';
|
||||
import type { FindingRecord } from '../../domain/finding';
|
||||
import { normalizeSeverity } from '../../profiles/purple-knight/presentation';
|
||||
import { outcomeForStatusValue, toolIdForRecord } from '../../importers/status-normalization';
|
||||
import { resultStatusFieldFor } from '../../features/linked-results/result-message';
|
||||
import { toolIdForRecord } from '../../importers/status-normalization';
|
||||
import { findingOutcomeMark, resultStatusFieldFor } from '../../features/linked-results/result-message';
|
||||
import { policyDiagnosticBadges } from '../../profiles/group-policy/categories';
|
||||
|
||||
import { highlightText } from '../HighlightText/highlight-text';
|
||||
|
|
@ -26,9 +26,8 @@ function FindingCardComponent({ record, title, bookmarked, selected, toolId, sea
|
|||
const groupMarked = useRelatedFindings()?.selection?.group.recordIds.includes(record.id);
|
||||
const severityField = record.fields.find(field => /^(severity|criticality|priority)$/i.test(field.label) && field.value);
|
||||
const severity = normalizeSeverity(severityField?.value);
|
||||
const statusField = findingCardStatus(record, toolId);
|
||||
const outcomeMark = findingOutcomeMark(record, toolId);
|
||||
const resolvedToolId = toolIdForRecord(record, toolId);
|
||||
const outcome = outcomeForStatusValue(resolvedToolId, statusField?.value);
|
||||
const diagnostics = policyDiagnosticBadges(record, resolvedToolId);
|
||||
return (
|
||||
<div data-record-id={record.id} className={`record-card-with-group${groupMarked ? ' is-group-marked' : ''}`}>
|
||||
|
|
@ -39,8 +38,8 @@ function FindingCardComponent({ record, title, bookmarked, selected, toolId, sea
|
|||
onClick={onOpen}
|
||||
>
|
||||
<span className="record-title">{highlightText(title, search)}{bookmarked && <span className="action-pill">Workspace</span>}</span>
|
||||
{(statusField || diagnostics.length > 0) && <span className="record-meta-line">
|
||||
{statusField && <span className="record-meta outcome-mark" data-outcome={outcome}>{highlightText(statusField.value, search)}</span>}
|
||||
{(outcomeMark || diagnostics.length > 0) && <span className="record-meta-line">
|
||||
{outcomeMark && <span className="record-meta outcome-mark" data-outcome={outcomeMark.outcome}>{highlightText(outcomeMark.label, search)}</span>}
|
||||
{diagnostics.map(diagnostic => <span className="record-diagnostic-badge" data-policy-diagnostic={diagnostic.toLowerCase()} key={diagnostic}>{diagnostic}</span>)}
|
||||
</span>}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,9 +54,13 @@ export function DefenderVulnerabilityPanel({ dataset, navigation }: { dataset: I
|
|||
const affectedDevices = model.devices.filter(device => device.occurrenceIds.length > 0 || (device.totalCves ?? 0) > 0).length;
|
||||
const occurrenceSeverity = Object.fromEntries(severities.map(item => [item, model.occurrences.filter(occurrence => occurrence.severity === item).length])) as Record<DefenderSeverity, number>;
|
||||
|
||||
return <div className="defender-page">
|
||||
return <div className="dashboard-page client-command-center defender-page">
|
||||
{navigation}
|
||||
<p className="defender-context"><strong>Microsoft Defender for IoT vulnerabilities</strong><span>{model.summaryRowCount.toLocaleString('en-US')} source asset rows</span><span>{model.diagnostics.length.toLocaleString('en-US')} diagnostics</span><WorkTimeslotControl /></p>
|
||||
<header className="client-overview-header defender-header">
|
||||
<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-source defender-source"><span>Source:</span><strong>Microsoft Defender for IoT</strong></div>
|
||||
</header>
|
||||
<section className="defender-metrics" aria-label="Vulnerability metrics">
|
||||
<article><span>Assets</span><strong><Count value={model.devices.length} /></strong></article>
|
||||
<article><span>Unique CVEs</span><strong><Count value={model.cves.length} /></strong></article>
|
||||
|
|
@ -65,6 +69,7 @@ export function DefenderVulnerabilityPanel({ dataset, navigation }: { dataset: I
|
|||
<article><span>Raw sensor observations</span><strong><Count value={model.observations.length} /></strong></article>
|
||||
<article><span>Aggregate-only vulnerabilities</span><strong><Count value={model.aggregateOnlyCount} /></strong></article>
|
||||
</section>
|
||||
<p className="defender-context"><span>{model.summaryRowCount.toLocaleString('en-US')} source asset rows</span><span>{model.diagnostics.length.toLocaleString('en-US')} diagnostics</span></p>
|
||||
<nav className="defender-tabs" aria-label="Defender vulnerability views">
|
||||
{([['cves', 'CVEs'], ['devices', 'Devices'], ['sites', 'Sites']] as const).map(([id, title]) => <button type="button" key={id} aria-pressed={view === id} onClick={() => setView(id)}>{title}</button>)}
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import { isFileReference } from '../../profiles/purple-knight/result-linker';
|
|||
import { highlightText } from '../../components/HighlightText/highlight-text';
|
||||
import { BookmarkIcon } from '../../components/BookmarkIcon';
|
||||
import { TriangleIcon } from '../../components/TriangleIcon';
|
||||
import { resultSectionFields, resultStatusFieldFor } from '../linked-results/result-message';
|
||||
import { findingOutcomeMark, resultSectionFields, resultStatusFieldFor } from '../linked-results/result-message';
|
||||
import { cisBenchmarkCategories } from '../../profiles/cis-cat/cis-benchmark-hierarchy';
|
||||
import { cisCategoryRecordTitle } from '../../profiles/cis-cat/cis-benchmark-section-titles';
|
||||
import { policyComparisonDisplayLines, policyValueName } from '../../profiles/group-policy/presentation';
|
||||
|
|
@ -428,6 +428,7 @@ export function FindingReader({ record, title, review: storedReview, previousPol
|
|||
const comparisonOutcome = statusField?.value ? outcomeForStatusValue(resolvedToolId, statusField.value) : 'unknown';
|
||||
const problem = resolvedToolId === 'group-policy' && !policyReadiness ? policyProblemSummary(record) : undefined;
|
||||
const firstRead = resolvedToolId !== 'group-policy' ? findingFirstRead(record, resolvedToolId, linkedSheets, comparisonOutcome) : undefined;
|
||||
const outcomeMark = findingOutcomeMark(record, toolId);
|
||||
const policyHeading = policyComparisonFields.map(field => policyValueName(field.value)).find(Boolean);
|
||||
// Nur echte Dateiverweise gehoeren in den Hinweis. Werkzeuge, die ihr
|
||||
// Ergebnis als Fliesstext oder Tabelle in dasselbe Feld schreiben, haetten
|
||||
|
|
@ -456,7 +457,7 @@ export function FindingReader({ record, title, review: storedReview, previousPol
|
|||
<div className="finding-badges">
|
||||
{/* Ergebnisstatus als Outcome, nicht als Meta-Plakette: sonst ist
|
||||
`Investigate` optisch nicht von `Passed` zu unterscheiden. */}
|
||||
{statusField?.value && <span className="outcome-mark" data-outcome={outcomeForStatusValue(resolvedToolId, statusField.value)}>{highlightText(statusField.value, search)}</span>}
|
||||
{outcomeMark && <span className="outcome-mark" data-outcome={outcomeMark.outcome}>{highlightText(outcomeMark.label, search)}</span>}
|
||||
{/* The heading already names the CIS section; each number badge names its section on hover. */}
|
||||
{derivedCategories.map(category => <span className="status-badge" data-derived-category="cis" title={cisCategoryRecordTitle(record, category)} key={category}>{highlightText(category, search)}</span>)}
|
||||
{badges.map(field => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { FindingRecord, RecordField } from '../../domain/finding';
|
||||
import type { DatasetSheet } from '../../domain/dataset';
|
||||
import type { CanonicalOutcome } from '../../domain/canonical-model';
|
||||
import { outcomeForStatusValue, toolIdForRecord } from '../../importers/status-normalization';
|
||||
import { normalizeStatusDimensionsFor, outcomeForStatusValue, toolIdForRecord } from '../../importers/status-normalization';
|
||||
import { toolProfileDefinitions } from '../../profiles/profile-definition-catalog';
|
||||
import { parseMaesterExecutionError, type MaesterExecutionError } from './maester-error';
|
||||
import { pingCastleAffectedDetailRow } from '../../profiles/pingcastle/result-evidence';
|
||||
|
|
@ -204,6 +204,37 @@ export function resultStatusFieldFor(record: FindingRecord, fallbackToolId?: str
|
|||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Menschlich lesbare Marke je kanonischem Outcome (Rueckfall ohne Quellwort). */
|
||||
const OUTCOME_MARK_LABELS: Record<CanonicalOutcome, string> = {
|
||||
fail: 'Failed', pass: 'Passed', warning: 'Warning', informational: 'Informational',
|
||||
execution_error: 'Error', not_run: 'Not run', not_applicable: 'Not applicable',
|
||||
not_evaluated: 'Not evaluated', unknown: 'Unknown'
|
||||
};
|
||||
|
||||
export interface FindingOutcomeMark {
|
||||
outcome: CanonicalOutcome;
|
||||
label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Outcome-Marke eines rohen Work-Datensatzes fuer die kompakten Kartenlisten
|
||||
* und den Reader-Kopf. Fuehrt das Werkzeug ein eigenes Statuswort, bleibt dieses
|
||||
* die Beschriftung. Liefert ein Werkzeug seine Funde ohne Statusspalte (jede
|
||||
* Defender-CVE ist eine Abweichung), traegt das Profil den Ausfall-Outcome
|
||||
* (`normalization.missingStatusOutcome`) nach, damit die Liste nicht ohne
|
||||
* Outcome-Hinweis bleibt. Werkzeuge ohne deklarierten Ausfall-Outcome behalten
|
||||
* das bisherige Verhalten - ohne Statusfeld keine Marke.
|
||||
*/
|
||||
export function findingOutcomeMark(record: FindingRecord, fallbackToolId?: string): FindingOutcomeMark | undefined {
|
||||
const toolId = toolIdForRecord(record, fallbackToolId);
|
||||
const statusField = resultStatusFieldFor(record, fallbackToolId);
|
||||
if (statusField) {
|
||||
return { outcome: outcomeForStatusValue(toolId, statusField.value) ?? 'unknown', label: statusField.value };
|
||||
}
|
||||
const outcome = normalizeStatusDimensionsFor(toolId, '').outcome;
|
||||
return outcome === 'unknown' ? undefined : { outcome, label: OUTCOME_MARK_LABELS[outcome] };
|
||||
}
|
||||
/**
|
||||
* Felder, die als eigener benannter Abschnitt in das Results-Fenster gehoeren
|
||||
* (siehe `resultSectionHeaders` im Toolprofil).
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
.defender-page { box-sizing: border-box; display: grid; align-content: start; gap: var(--segment-gap); width: 100%; min-height: 100%; padding: var(--panel-inset); color: var(--text); background: var(--surface-app); }
|
||||
.defender-page { box-sizing: border-box; display: grid; align-content: start; gap: var(--space-4); width: 100%; min-height: 100%; padding: var(--panel-inset); color: var(--text); background: var(--surface-app); }
|
||||
.defender-header { order: 0; }
|
||||
.defender-source strong { color: var(--heading); font-weight: var(--fw-regular); }
|
||||
.defender-context { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-2) var(--space-5); margin: 0; color: var(--muted); font-size: var(--type-meta-size); }
|
||||
.defender-context strong { color: var(--heading); font-size: var(--type-h4-size); font-weight: var(--fw-semibold); }
|
||||
.defender-context .work-timeslot-select { margin-left: auto; }
|
||||
.defender-metrics { display: grid; grid-template-columns: repeat(auto-fill, var(--tile-compact-width)); justify-content: start; gap: var(--segment-gap); padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-surface); }
|
||||
.defender-metrics article { display: grid; align-content: space-between; gap: var(--space-3); min-height: 128px; padding: var(--tile-inset); border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
|
||||
.defender-metrics span { color: var(--heading); font-size: var(--type-label-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-regular); font-variant-numeric: tabular-nums; }
|
||||
.defender-metrics { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); justify-content: start; gap: var(--space-2); min-width: 0; }
|
||||
.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-label-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-view { display: grid; gap: var(--space-4); min-width: 0; padding: var(--segment-inset); border: var(--border-hair) solid var(--segment-border); border-radius: var(--segment-radius); background: var(--segment-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); }
|
||||
.defender-view > header span { color: var(--muted); font-size: var(--type-meta-size); }
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
.defender-severity i { width: 4px; height: 18px; background: var(--severity, var(--muted)); }
|
||||
.defender-conflict, .defender-warning { color: var(--warning); }
|
||||
.defender-unavailable, .defender-empty { color: var(--muted); }
|
||||
.defender-drilldown { display: grid; gap: var(--space-3); padding: var(--tile-inset); border: var(--border-hair) solid var(--tile-border); border-radius: var(--tile-radius); background: var(--tile-surface); box-shadow: var(--tile-card-shadow); }
|
||||
.defender-drilldown { display: grid; gap: var(--space-3); 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-drilldown h3, .defender-drilldown h4 { margin: 0; color: var(--heading); font-weight: var(--fw-semibold); }
|
||||
.defender-drilldown h3 { font-size: var(--type-detail-title-size); }
|
||||
.defender-drilldown h4 { font-size: var(--type-h4-size); }
|
||||
|
|
@ -42,4 +42,6 @@
|
|||
.defender-aggregate-summary span, .defender-severity-strip strong { font-size: var(--type-table-body-size); }
|
||||
.defender-severity-strip article { grid-template-columns: minmax(0, 1fr) auto; align-items: center; }
|
||||
.defender-severity-strip strong { color: var(--heading); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; }
|
||||
@media (max-width: 760px) { .defender-view > header { display: grid; } .defender-context .work-timeslot-select { margin-left: 0; } }
|
||||
@container (max-width: 1100px) { .defender-metrics { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
@container (max-width: 760px) { .defender-view > header { display: grid; } .defender-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@container (max-width: 480px) { .defender-metrics { grid-template-columns: 1fr; } }
|
||||
|
|
|
|||
|
|
@ -48,10 +48,15 @@ describe('Defender vulnerability dashboard projection', () => {
|
|||
expect(component).toContain('Site dashboard');
|
||||
expect(component).toContain('Source row');
|
||||
expect(component).toContain('aggregate-only');
|
||||
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} />');
|
||||
expect(styles).toContain('padding: var(--panel-inset)');
|
||||
expect(styles).toContain('grid-template-columns: repeat(6, minmax(0, 1fr))');
|
||||
expect(styles).toContain('background: var(--tile-surface)');
|
||||
expect(styles).toContain('outline: var(--bw-emphasis) solid var(--marking-color)');
|
||||
expect(styles).toContain('outline: var(--bw-emphasis) solid var(--focus-ring)');
|
||||
expect(styles).not.toContain('text-overflow: ellipsis');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import type { FindingRecord } from '../../src/domain/finding';
|
||||
import { promotedResultSheetTitlesFor, resultMessageFor, resultSectionsFor, resultSheetRowHighlightedFor, resultSheetTitleFor, resultStatusFieldFor, showResultSheetTitlesFor } from '../../src/features/linked-results/result-message';
|
||||
import { findingOutcomeMark, promotedResultSheetTitlesFor, resultMessageFor, resultSectionsFor, resultSheetRowHighlightedFor, resultSheetTitleFor, resultStatusFieldFor, showResultSheetTitlesFor } from '../../src/features/linked-results/result-message';
|
||||
|
||||
function maesterRecord(fields: Array<[string, string]>): FindingRecord {
|
||||
const built = fields.map(([label, value], index) => ({ label, value, index }));
|
||||
|
|
@ -148,6 +148,30 @@ describe('resultStatusFieldFor', () => {
|
|||
expect(resultStatusFieldFor(record, 'purple-knight')).toMatchObject({ label: 'Status', value: expected });
|
||||
});
|
||||
|
||||
describe('findingOutcomeMark', () => {
|
||||
it('traegt bei Defender-Schwachstellen ohne Statusspalte den Ausfall-Outcome nach', () => {
|
||||
const record = maesterRecord([
|
||||
['CVE', 'CVE-2026-25786'],
|
||||
['Severity', 'Critical'],
|
||||
['Beschreibung', 'Synthetic description']
|
||||
]);
|
||||
|
||||
expect(findingOutcomeMark(record, 'defender-for-iot-vulnerabilities')).toEqual({ outcome: 'warning', label: 'Warning' });
|
||||
});
|
||||
|
||||
it('behaelt das Quell-Statuswort, wo das Werkzeug eine Statusspalte fuehrt', () => {
|
||||
const record = maesterRecord([['Status', 'IOE Found'], ['Result', 'SI000067']]);
|
||||
|
||||
expect(findingOutcomeMark(record, 'purple-knight')).toEqual({ outcome: 'fail', label: 'Failed' });
|
||||
});
|
||||
|
||||
it('bleibt ohne Statusfeld und ohne deklarierten Ausfall-Outcome leer', () => {
|
||||
const record = maesterRecord([['ID', 'MT.1011'], ['Name', 'Some finding']]);
|
||||
|
||||
expect(findingOutcomeMark(record, 'maester')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('promotedResultSheetTitlesFor', () => {
|
||||
it('ordnet mehrere Controls derselben Framework-Ueberschrift ihren Tabellen in Reihenfolge zu', () => {
|
||||
const sections = [{
|
||||
|
|
|
|||
Loading…
Reference in New Issue