feat(defender-for-iot): introduce vulnerability finding result type
Adapted pick offe71246. Adds the vulnerability_finding result type, missingStatusOutcome for sources whose rows are the finding, and Defender profile 1.1.0. The Work deviation filter uses the canonical predicate through isDeviationRecord; Michael's GPO status logic and status cache stay in front of it. The constraint migration is renumbered from 022 to 027. The CVE enrichment commite048295is skipped for now.
This commit is contained in:
parent
426a138e55
commit
550947d3b1
|
|
@ -4462,6 +4462,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: 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. |
|
||||||
|
| 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. |
|
| 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. |
|
||||||
| 2026-10-02 | Phasen 6/7/13A: `7d3ef5f` (Microsoft Defender for IoT, Grundlage) auf Michaels Basis uebernommen. Profil `defender-for-iot-vulnerabilities` 1.0.0 mit Manifestzeile, Source Extractor, Domain-Modell, Client-Info-Panel sowie Titel je Datensatzprofil (`recordTitleFor`) in Work und Pathfinder. Angepasst: Das Profil deklariert die auf dieser Basis verpflichtenden `securityScoring` (kein nativer Gesamtscore, mit Begruendung), `priorityBasis` (native CVE-Severity) und `timeslotComparison` (nicht verfuegbar). Client Info fuehrt Defender als dritten Tab neben Overview und AI Overview; ein reiner Defender-Timeslot startet in der Defender-Sicht. `recordTitleFor` ersetzt nur den Titelkandidaten, die Maester-/CIS-Titelregeln bleiben. `canonicalModelsToDataset` laesst zusaetzlich Modelle mit befuellten Quelltabellen zu, das leere All-Records-Blatt bleibt bestehen. Die Kompatibilitaetspruefung bevorzugt den gespeicherten `processedWith.profileId`; der sichtbare Importvertrag steigt auf 1.4.0. Die kopierten `.client-info-tabs`-Regeln sind aus `ai-overview.css` entfernt und liegen nur noch in `defender-for-iot.css`. Nicht uebernommen: `.gitignore`-Eintrag, `--tile-inset`, der bereits vorhandene `fileProfile`-Vorrang und die alte Versionspruefung. Neues Profil, daher kein Reimport bestehender Importe; keine API-, SQL- oder Persistenzmigration. Die Produktversion wird am Ende der Defender-Serie einmal neu bewertet. Verifikation: beide Typechecks, Gesamtlauf mit 1.883 bestandenen und 17 uebersprungenen Tests, Local-only-/Designpruefung und beide Produktionsbuilds erfolgreich. |
|
| 2026-10-02 | Phasen 6/7/13A: `7d3ef5f` (Microsoft Defender for IoT, Grundlage) auf Michaels Basis uebernommen. Profil `defender-for-iot-vulnerabilities` 1.0.0 mit Manifestzeile, Source Extractor, Domain-Modell, Client-Info-Panel sowie Titel je Datensatzprofil (`recordTitleFor`) in Work und Pathfinder. Angepasst: Das Profil deklariert die auf dieser Basis verpflichtenden `securityScoring` (kein nativer Gesamtscore, mit Begruendung), `priorityBasis` (native CVE-Severity) und `timeslotComparison` (nicht verfuegbar). Client Info fuehrt Defender als dritten Tab neben Overview und AI Overview; ein reiner Defender-Timeslot startet in der Defender-Sicht. `recordTitleFor` ersetzt nur den Titelkandidaten, die Maester-/CIS-Titelregeln bleiben. `canonicalModelsToDataset` laesst zusaetzlich Modelle mit befuellten Quelltabellen zu, das leere All-Records-Blatt bleibt bestehen. Die Kompatibilitaetspruefung bevorzugt den gespeicherten `processedWith.profileId`; der sichtbare Importvertrag steigt auf 1.4.0. Die kopierten `.client-info-tabs`-Regeln sind aus `ai-overview.css` entfernt und liegen nur noch in `defender-for-iot.css`. Nicht uebernommen: `.gitignore`-Eintrag, `--tile-inset`, der bereits vorhandene `fileProfile`-Vorrang und die alte Versionspruefung. Neues Profil, daher kein Reimport bestehender Importe; keine API-, SQL- oder Persistenzmigration. Die Produktversion wird am Ende der Defender-Serie einmal neu bewertet. Verifikation: beide Typechecks, Gesamtlauf mit 1.883 bestandenen und 17 uebersprungenen Tests, Local-only-/Designpruefung und beide Produktionsbuilds erfolgreich. |
|
||||||
| 2026-10-02 | Phase 7/13F: Aus `95ec322` die AI-Overview- und ISO-Mapping-Korrekturen uebernommen. Schlaegt eine Regeneration fehl oder laeuft sie noch, liefert der AI-Overview-Service zusaetzlich `previousSuccessfulRun`; das Panel zeigt die letzte gespeicherte Uebersicht mit der Fehlermeldung weiter, `Stale` gilt nur fuer einen erfolgreichen, veralteten Lauf. Ein gespeichertes ISO-Mapping bleibt lesbar, wenn Qdrant oder der Embeddingdienst nicht erreichbar ist; die Generierung bleibt dann deaktiviert. Synthetische Service- und Paneltests. Die Defender-Geraete-Risikoakzeptanz desselben Commits folgt mit den Defender-Commits. |
|
| 2026-10-02 | Phase 7/13F: Aus `95ec322` die AI-Overview- und ISO-Mapping-Korrekturen uebernommen. Schlaegt eine Regeneration fehl oder laeuft sie noch, liefert der AI-Overview-Service zusaetzlich `previousSuccessfulRun`; das Panel zeigt die letzte gespeicherte Uebersicht mit der Fehlermeldung weiter, `Stale` gilt nur fuer einen erfolgreichen, veralteten Lauf. Ein gespeichertes ISO-Mapping bleibt lesbar, wenn Qdrant oder der Embeddingdienst nicht erreichbar ist; die Generierung bleibt dann deaktiviert. Synthetische Service- und Paneltests. Die Defender-Geraete-Risikoakzeptanz desselben Commits folgt mit den Defender-Commits. |
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import referenceRecognition from './migrations/023_reference_recognition.sql?raw
|
||||||
import resultDbMaterialization from './migrations/024_result_db_materialization.sql?raw';
|
import resultDbMaterialization from './migrations/024_result_db_materialization.sql?raw';
|
||||||
import aiOverviewRuns from './migrations/025_ai_overview_runs.sql?raw';
|
import aiOverviewRuns from './migrations/025_ai_overview_runs.sql?raw';
|
||||||
import regulatoryMapping from './migrations/026_regulatory_mapping.sql?raw';
|
import regulatoryMapping from './migrations/026_regulatory_mapping.sql?raw';
|
||||||
|
import vulnerabilityFindingResultType from './migrations/027_vulnerability_finding_result_type.sql?raw';
|
||||||
|
|
||||||
export interface DatabaseMigration {
|
export interface DatabaseMigration {
|
||||||
version: string;
|
version: string;
|
||||||
|
|
@ -53,5 +54,6 @@ export const databaseMigrations: readonly DatabaseMigration[] = [
|
||||||
{ version: '023', name: 'reference-recognition', sql: referenceRecognition },
|
{ version: '023', name: 'reference-recognition', sql: referenceRecognition },
|
||||||
{ version: '024', name: 'result-db-materialization', sql: resultDbMaterialization },
|
{ version: '024', name: 'result-db-materialization', sql: resultDbMaterialization },
|
||||||
{ version: '025', name: 'ai-overview-runs', sql: aiOverviewRuns },
|
{ version: '025', name: 'ai-overview-runs', sql: aiOverviewRuns },
|
||||||
{ version: '026', name: 'regulatory-mapping', sql: regulatoryMapping }
|
{ version: '026', name: 'regulatory-mapping', sql: regulatoryMapping },
|
||||||
|
{ version: '027', name: 'vulnerability-finding-result-type', sql: vulnerabilityFindingResultType }
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
-- ASSESSOR-Erweiterung des geschlossenen RESULT_CONTRACT-result_type-Katalogs
|
||||||
|
-- fuer Scannerbefunde zu konkreten Schwachstellen. Bestehende Occurrences
|
||||||
|
-- werden nicht umgedeutet; der neue Typ gilt fuer neu verarbeitete Importe.
|
||||||
|
|
||||||
|
ALTER TABLE assessor_finding_occurrences
|
||||||
|
DROP CONSTRAINT assessor_occurrences_result_type_check,
|
||||||
|
ADD CONSTRAINT assessor_occurrences_result_type_check CHECK (
|
||||||
|
result_type IN (
|
||||||
|
'exposure_finding', 'vulnerability_finding', 'compliance_check',
|
||||||
|
'policy_deviation', 'informational', 'unknown'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
@ -8,7 +8,7 @@ import type {
|
||||||
import { processingManifest } from './processing-compatibility';
|
import { processingManifest } from './processing-compatibility';
|
||||||
|
|
||||||
const RESULT_CONTRACT_RESULT_TYPES = new Set([
|
const RESULT_CONTRACT_RESULT_TYPES = new Set([
|
||||||
'exposure_finding', 'compliance_check', 'policy_deviation', 'informational', 'unknown'
|
'exposure_finding', 'vulnerability_finding', 'compliance_check', 'policy_deviation', 'informational', 'unknown'
|
||||||
]);
|
]);
|
||||||
const RESULT_CONTRACT_EXPOSURE_STATES = new Set([
|
const RESULT_CONTRACT_EXPOSURE_STATES = new Set([
|
||||||
'detected', 'not_detected', 'not_evaluated', 'execution_error', 'unknown'
|
'detected', 'not_detected', 'not_evaluated', 'execution_error', 'unknown'
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,8 @@ export interface ProcessingVersionManifest extends ToolProcessingContract {
|
||||||
*/
|
*/
|
||||||
export const PROCESSING_VERSION_MANIFESTS: readonly ProcessingVersionManifest[] = [
|
export const PROCESSING_VERSION_MANIFESTS: readonly ProcessingVersionManifest[] = [
|
||||||
{ profileId: 'generic', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'generic-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: true },
|
{ profileId: 'generic', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'generic-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: true },
|
||||||
{ profileId: 'defender-for-iot-vulnerabilities', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'defender-for-iot-vulnerabilities-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: true },
|
{ profileId: 'defender-for-iot-vulnerabilities', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'defender-for-iot-vulnerabilities-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: [], current: false },
|
||||||
|
{ profileId: 'defender-for-iot-vulnerabilities', profileVersion: '1.1.0', mappingVersion: '1.0.0', normalizationVersion: '1.1.0', presentationVersion: '1.0.0', compatibilityKey: 'defender-for-iot-vulnerabilities-v2', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'resultType', 'deviation'], current: true },
|
||||||
{ profileId: 'purple-knight', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'purple-knight-canonical-v1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: false },
|
{ profileId: 'purple-knight', profileVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0', compatibilityKey: 'purple-knight-canonical-v1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: false },
|
||||||
{ profileId: 'purple-knight', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: false },
|
{ profileId: 'purple-knight', profileVersion: '1.1.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'semantic-breaking', reimportRequired: true, affectedDimensions: ['outcome', 'isIoe'], current: false },
|
||||||
{ profileId: 'purple-knight', profileVersion: '1.2.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: false },
|
{ profileId: 'purple-knight', profileVersion: '1.2.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.1.0', compatibilityKey: 'purple-knight-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: false },
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ export const RESULT_CONTRACT_MAPPING_VERSION = '1.0.0';
|
||||||
|
|
||||||
export type ResultContractResultType =
|
export type ResultContractResultType =
|
||||||
| 'exposure_finding'
|
| 'exposure_finding'
|
||||||
|
| 'vulnerability_finding'
|
||||||
| 'compliance_check'
|
| 'compliance_check'
|
||||||
| 'policy_deviation'
|
| 'policy_deviation'
|
||||||
| 'informational'
|
| 'informational'
|
||||||
|
|
@ -50,6 +51,9 @@ export interface ResultContractIoeDerivation {
|
||||||
*/
|
*/
|
||||||
const EXPOSURE_TOOL_IDS = new Set(['pingcastle', 'purple-knight', 'maester']);
|
const EXPOSURE_TOOL_IDS = new Set(['pingcastle', 'purple-knight', 'maester']);
|
||||||
|
|
||||||
|
/** Scanner, deren native Aussage eine konkrete Schwachstelle bezeichnet. */
|
||||||
|
const VULNERABILITY_TOOL_IDS = new Set(['defender-for-iot-vulnerabilities']);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Werkzeuge, deren native Aussage RESULT_CONTRACT als `compliance_check`
|
* Werkzeuge, deren native Aussage RESULT_CONTRACT als `compliance_check`
|
||||||
* einordnet.
|
* einordnet.
|
||||||
|
|
@ -66,6 +70,7 @@ const COMPLIANCE_TOOL_IDS = new Set(['cis-cat']);
|
||||||
export function deriveResultType(toolId: string): ResultContractResultType {
|
export function deriveResultType(toolId: string): ResultContractResultType {
|
||||||
const normalized = toolId.trim().toLocaleLowerCase('en-US');
|
const normalized = toolId.trim().toLocaleLowerCase('en-US');
|
||||||
if (EXPOSURE_TOOL_IDS.has(normalized)) return 'exposure_finding';
|
if (EXPOSURE_TOOL_IDS.has(normalized)) return 'exposure_finding';
|
||||||
|
if (VULNERABILITY_TOOL_IDS.has(normalized)) return 'vulnerability_finding';
|
||||||
if (COMPLIANCE_TOOL_IDS.has(normalized)) return 'compliance_check';
|
if (COMPLIANCE_TOOL_IDS.has(normalized)) return 'compliance_check';
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { CanonicalFieldRole, CanonicalFindingStatus, CanonicalSeverity } from './canonical-model';
|
import type { CanonicalFieldRole, CanonicalFindingStatus, CanonicalOutcome, CanonicalSeverity } from './canonical-model';
|
||||||
|
|
||||||
export type ProfileSignalScope = 'file-name' | 'raw-text' | 'table-name' | 'header' | 'cell-value';
|
export type ProfileSignalScope = 'file-name' | 'raw-text' | 'table-name' | 'header' | 'cell-value';
|
||||||
|
|
||||||
|
|
@ -161,6 +161,8 @@ export interface ToolProfileDefinition {
|
||||||
normalization: {
|
normalization: {
|
||||||
severity: Array<ProfileValueMapping<CanonicalSeverity>>;
|
severity: Array<ProfileValueMapping<CanonicalSeverity>>;
|
||||||
status: Array<ProfileValueMapping<CanonicalFindingStatus>>;
|
status: Array<ProfileValueMapping<CanonicalFindingStatus>>;
|
||||||
|
/** Outcome fuer Finding-Quellen, deren Zeilen selbst der Befund sind und keinen Status liefern. */
|
||||||
|
missingStatusOutcome?: CanonicalOutcome;
|
||||||
};
|
};
|
||||||
presentation: {
|
presentation: {
|
||||||
titleHeaders: RegExp[];
|
titleHeaders: RegExp[];
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import type {
|
||||||
CanonicalTechnicalExecutionState
|
CanonicalTechnicalExecutionState
|
||||||
} from '../domain/canonical-model';
|
} from '../domain/canonical-model';
|
||||||
import type { FindingRecord } from '../domain/finding';
|
import type { FindingRecord } from '../domain/finding';
|
||||||
|
import { isDeviation } from '../domain/deviation-predicate';
|
||||||
|
import { deriveExposureState, deriveIsIoe, deriveResultType } from '../domain/result-contract-mapping';
|
||||||
import { outcomeFromLegacyStatus, technicalStateFromLegacyStatus } from '../domain/unified-result';
|
import { outcomeFromLegacyStatus, technicalStateFromLegacyStatus } from '../domain/unified-result';
|
||||||
import { toolProfileDefinitions } from '../profiles/profile-definition-catalog';
|
import { toolProfileDefinitions } from '../profiles/profile-definition-catalog';
|
||||||
import { clean } from '../shared/text';
|
import { clean } from '../shared/text';
|
||||||
|
|
@ -80,9 +82,12 @@ function dimensionsFor(legacy: CanonicalFindingStatus, outcome?: CanonicalOutcom
|
||||||
*/
|
*/
|
||||||
export function normalizeStatusDimensionsFor(toolId: string | undefined, value: string): StatusDimensions {
|
export function normalizeStatusDimensionsFor(toolId: string | undefined, value: string): StatusDimensions {
|
||||||
const cleaned = clean(value);
|
const cleaned = clean(value);
|
||||||
if (!cleaned) return UNKNOWN_DIMENSIONS;
|
const definition = toolId ? definitionsByToolId.get(toolId) : undefined;
|
||||||
|
if (!cleaned) return definition?.normalization.missingStatusOutcome
|
||||||
|
? { legacy: 'unknown', outcome: definition.normalization.missingStatusOutcome, technical: 'completed' }
|
||||||
|
: UNKNOWN_DIMENSIONS;
|
||||||
|
|
||||||
const declared = toolId ? definitionsByToolId.get(toolId)?.normalization.status : undefined;
|
const declared = definition?.normalization.status;
|
||||||
const declaredMatch = declared?.find(mapping => mapping.patterns.some(pattern => pattern.test(cleaned)));
|
const declaredMatch = declared?.find(mapping => mapping.patterns.some(pattern => pattern.test(cleaned)));
|
||||||
if (declaredMatch) return dimensionsFor(declaredMatch.normalizedValue);
|
if (declaredMatch) return dimensionsFor(declaredMatch.normalizedValue);
|
||||||
|
|
||||||
|
|
@ -118,6 +123,29 @@ export function toolIdForRecord(record: FindingRecord, fallbackToolId?: string):
|
||||||
return toolName === 'gpos' ? 'group-policy' : toolIdByName.get(toolName) ?? fallbackToolId;
|
return toolName === 'gpos' ? 'group-policy' : toolIdByName.get(toolName) ?? fallbackToolId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function statusValueForRecord(record: FindingRecord, toolId: string | undefined): string {
|
||||||
|
const presentation = toolId ? definitionsByToolId.get(toolId)?.presentation : undefined;
|
||||||
|
const headers = presentation?.statusHeaders ?? [/^(?:status|evaluation|result)$/i];
|
||||||
|
for (const header of headers) {
|
||||||
|
const field = record.fields.find(candidate =>
|
||||||
|
candidate.value
|
||||||
|
&& header.test(candidate.label.trim())
|
||||||
|
&& !presentation?.resultMessageHeaders?.some(pattern => pattern.test(candidate.label.trim())));
|
||||||
|
if (field) return field.value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Spiegelt fuer rohe Work-Datensaetze das kanonische Result-DB-Praedikat. */
|
||||||
|
export function isDeviationRecord(record: FindingRecord, fallbackToolId?: string): boolean {
|
||||||
|
const toolId = toolIdForRecord(record, fallbackToolId);
|
||||||
|
const outcome = normalizeStatusDimensionsFor(toolId, statusValueForRecord(record, toolId)).outcome;
|
||||||
|
const resultType = deriveResultType(toolId ?? '');
|
||||||
|
const exposureState = deriveExposureState(resultType, outcome);
|
||||||
|
const isIoe = deriveIsIoe(resultType, exposureState).isIoe;
|
||||||
|
return isDeviation(isIoe, outcome);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outcome eines rohen Statusfeldes, wie es die Arbeitsansichten anzeigen.
|
* Outcome eines rohen Statusfeldes, wie es die Arbeitsansichten anzeigen.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@ export const defenderForIotDefinition: ToolProfileDefinition = {
|
||||||
reason: 'The Defender for IoT vulnerability export is a point-in-time CVE inventory without a stable scan scope or observation time for timeslot comparison.'
|
reason: 'The Defender for IoT vulnerability export is a point-in-time CVE inventory without a stable scan scope or observation time for timeslot comparison.'
|
||||||
},
|
},
|
||||||
name: 'Microsoft Defender for IoT',
|
name: 'Microsoft Defender for IoT',
|
||||||
version: '1.0.0',
|
version: '1.1.0',
|
||||||
processing: {
|
processing: {
|
||||||
mappingVersion: '1.0.0', normalizationVersion: '1.0.0', presentationVersion: '1.0.0',
|
mappingVersion: '1.0.0', normalizationVersion: '1.1.0', presentationVersion: '1.0.0',
|
||||||
compatibilityKey: 'defender-for-iot-vulnerabilities-v1', changeClass: 'compatible',
|
compatibilityKey: 'defender-for-iot-vulnerabilities-v2', changeClass: 'semantic-breaking',
|
||||||
reimportRequired: false, affectedDimensions: []
|
reimportRequired: true, affectedDimensions: ['outcome', 'resultType', 'deviation']
|
||||||
},
|
},
|
||||||
sourceSignals: [
|
sourceSignals: [
|
||||||
{ scope: 'header', pattern: /^TotalCVEs$/i, contribution: 0.45, message: 'The asset summary exposes TotalCVEs.' },
|
{ scope: 'header', pattern: /^TotalCVEs$/i, contribution: 0.45, message: 'The asset summary exposes TotalCVEs.' },
|
||||||
|
|
@ -47,7 +47,7 @@ export const defenderForIotDefinition: ToolProfileDefinition = {
|
||||||
{ role: 'asset', headers: [{ pattern: /^Affected Device Count$/i, contribution: 0.92, message: 'The aggregated affected-device count stands in for the affected object of this CVE.' }] },
|
{ role: 'asset', headers: [{ pattern: /^Affected Device Count$/i, contribution: 0.92, message: 'The aggregated affected-device count stands in for the affected object of this CVE.' }] },
|
||||||
{ role: 'evidence', headers: [{ pattern: /^Source Observations$/i, contribution: 1, message: 'The field preserves every sensor observation and row locator.' }] }
|
{ role: 'evidence', headers: [{ pattern: /^Source Observations$/i, contribution: 1, message: 'The field preserves every sensor observation and row locator.' }] }
|
||||||
],
|
],
|
||||||
normalization: { severity: commonSeverityMappings, status: commonStatusMappings },
|
normalization: { severity: commonSeverityMappings, status: commonStatusMappings, missingStatusOutcome: 'warning' },
|
||||||
presentation: {
|
presentation: {
|
||||||
titleHeaders: [/^CVE$/i],
|
titleHeaders: [/^CVE$/i],
|
||||||
severityHeaders: [/^Severity$/i],
|
severityHeaders: [/^Severity$/i],
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import type { DatasetSheet } from '../domain/dataset';
|
||||||
import type { FindingRecord } from '../domain/finding';
|
import type { FindingRecord } from '../domain/finding';
|
||||||
import type { ImportProfile, ProfileDetectionContext } from '../domain/import-profile';
|
import type { ImportProfile, ProfileDetectionContext } from '../domain/import-profile';
|
||||||
import type { ToolProfileDefinition, WeightedPattern } from '../domain/tool-profile-definition';
|
import type { ToolProfileDefinition, WeightedPattern } from '../domain/tool-profile-definition';
|
||||||
|
import { isDeviationRecord } from '../importers/status-normalization';
|
||||||
import { clean, plainTextValue } from '../shared/text';
|
import { clean, plainTextValue } from '../shared/text';
|
||||||
|
|
||||||
function matches(pattern: RegExp, value: string): boolean {
|
function matches(pattern: RegExp, value: string): boolean {
|
||||||
|
|
@ -24,25 +25,6 @@ function matches(pattern: RegExp, value: string): boolean {
|
||||||
*/
|
*/
|
||||||
const normalizedFieldValuesCache = new WeakMap<FindingRecord, string[]>();
|
const normalizedFieldValuesCache = new WeakMap<FindingRecord, string[]>();
|
||||||
|
|
||||||
/**
|
|
||||||
* Werkzeuguebergreifender Arbeitsfilter. Die Muster entsprechen der fachlichen
|
|
||||||
* Abweichung: fehlgeschlagene oder klaerungsbeduerftige Resultate sowie
|
|
||||||
* festgestellte Expositionen. Der Filter ist in jedem Profil verfuegbar und
|
|
||||||
* bildet den unveraenderlichen Startzustand aller Diagramme.
|
|
||||||
*/
|
|
||||||
const UNIVERSAL_DEVIATION_PATTERNS = [
|
|
||||||
/^fail(?:ed)?$/,
|
|
||||||
/^not passed?$/,
|
|
||||||
/^nicht bestanden$/,
|
|
||||||
/^ioes? (?:found|detected)$/,
|
|
||||||
/^matched$/,
|
|
||||||
/^detected$/,
|
|
||||||
/^open$/,
|
|
||||||
/^offen$/,
|
|
||||||
/^investigate$/,
|
|
||||||
/^warning$/
|
|
||||||
];
|
|
||||||
|
|
||||||
function normalizedFieldValues(record: FindingRecord): string[] {
|
function normalizedFieldValues(record: FindingRecord): string[] {
|
||||||
let cached = normalizedFieldValuesCache.get(record);
|
let cached = normalizedFieldValuesCache.get(record);
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
|
|
@ -125,6 +107,7 @@ export function createImportProfile(definition: ToolProfileDefinition): ImportPr
|
||||||
}
|
}
|
||||||
if (status === 'no-baseline-rule') return actualStatus === 'no baseline rule';
|
if (status === 'no-baseline-rule') return actualStatus === 'no baseline rule';
|
||||||
if (status === 'not-comparable') return actualStatus === 'not comparable';
|
if (status === 'not-comparable') return actualStatus === 'not comparable';
|
||||||
|
if (status === 'deviation') return isDeviationRecord(record, definition.id);
|
||||||
const filter = definition.presentation.statusFilters.find(item => item.value === status);
|
const filter = definition.presentation.statusFilters.find(item => item.value === status);
|
||||||
const statusHeaders = definition.presentation.statusHeaders;
|
const statusHeaders = definition.presentation.statusHeaders;
|
||||||
const preferredStatus = statusHeaders?.map(pattern => record.fields.find(field => field.value.trim() && matches(pattern, field.label.trim())))
|
const preferredStatus = statusHeaders?.map(pattern => record.fields.find(field => field.value.trim() && matches(pattern, field.label.trim())))
|
||||||
|
|
@ -132,9 +115,6 @@ export function createImportProfile(definition: ToolProfileDefinition): ImportPr
|
||||||
const values = statusHeaders?.length
|
const values = statusHeaders?.length
|
||||||
? preferredStatus ? [plainTextValue(preferredStatus.value).trim().toLowerCase()] : []
|
? preferredStatus ? [plainTextValue(preferredStatus.value).trim().toLowerCase()] : []
|
||||||
: normalizedFieldValues(record);
|
: normalizedFieldValues(record);
|
||||||
if (status === 'deviation' && !filter) {
|
|
||||||
return values.some(value => UNIVERSAL_DEVIATION_PATTERNS.some(pattern => matches(pattern, value)));
|
|
||||||
}
|
|
||||||
if (!filter) return true;
|
if (!filter) return true;
|
||||||
return values.some(value => filter.patterns.some(pattern => matches(pattern, value)));
|
return values.some(value => filter.patterns.some(pattern => matches(pattern, value)));
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';
|
||||||
import type { CanonicalOutcome } from '../../src/domain/canonical-model';
|
import type { CanonicalOutcome } from '../../src/domain/canonical-model';
|
||||||
import { DEVIATION_PREDICATE_VERSION, isDeviation } from '../../src/domain/deviation-predicate';
|
import { DEVIATION_PREDICATE_VERSION, isDeviation } from '../../src/domain/deviation-predicate';
|
||||||
import type { FindingRecord } from '../../src/domain/finding';
|
import type { FindingRecord } from '../../src/domain/finding';
|
||||||
import { normalizeStatusDimensionsFor, outcomeForStatusValue, toolIdForRecord } from '../../src/importers/status-normalization';
|
import { isDeviationRecord, normalizeStatusDimensionsFor, outcomeForStatusValue, toolIdForRecord } from '../../src/importers/status-normalization';
|
||||||
|
|
||||||
function recordWith(fields: Array<[string, string]>): FindingRecord {
|
function recordWith(fields: Array<[string, string]>): FindingRecord {
|
||||||
const built = fields.map(([label, value], index) => ({ label, value, index }));
|
const built = fields.map(([label, value], index) => ({ label, value, index }));
|
||||||
|
|
@ -69,6 +69,12 @@ describe('Maester-Statusvokabular', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Outcome eines rohen Statusfeldes fuer die Arbeitsansichten', () => {
|
describe('Outcome eines rohen Statusfeldes fuer die Arbeitsansichten', () => {
|
||||||
|
it('wendet in Work dasselbe Praedikat wie die Result DB an', () => {
|
||||||
|
expect(isDeviationRecord(recordWith([['Severity', 'Critical']]), 'defender-for-iot-vulnerabilities')).toBe(true);
|
||||||
|
expect(isDeviationRecord(recordWith([['Result', 'Failed']]), 'cis-cat')).toBe(true);
|
||||||
|
expect(isDeviationRecord(recordWith([['Result', 'Passed']]), 'cis-cat')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('unterscheidet die drei Maester-Ergebnisse voneinander', () => {
|
it('unterscheidet die drei Maester-Ergebnisse voneinander', () => {
|
||||||
expect(outcomeForStatusValue('maester', 'Passed')).toBe('pass');
|
expect(outcomeForStatusValue('maester', 'Passed')).toBe('pass');
|
||||||
expect(outcomeForStatusValue('maester', 'Failed')).toBe('fail');
|
expect(outcomeForStatusValue('maester', 'Failed')).toBe('fail');
|
||||||
|
|
@ -80,6 +86,11 @@ describe('Outcome eines rohen Statusfeldes fuer die Arbeitsansichten', () => {
|
||||||
expect(outcomeForStatusValue('maester', ' ')).toBeUndefined();
|
expect(outcomeForStatusValue('maester', ' ')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('normalisiert einen statuslosen Defender-CVE-Befund auf warning', () => {
|
||||||
|
expect(normalizeStatusDimensionsFor('defender-for-iot-vulnerabilities', ''))
|
||||||
|
.toEqual({ legacy: 'unknown', outcome: 'warning', technical: 'completed' });
|
||||||
|
});
|
||||||
|
|
||||||
it('bleibt ohne bekannte Werkzeug-ID ueber die generischen Regeln nutzbar', () => {
|
it('bleibt ohne bekannte Werkzeug-ID ueber die generischen Regeln nutzbar', () => {
|
||||||
expect(outcomeForStatusValue(undefined, 'Investigate')).toBe('warning');
|
expect(outcomeForStatusValue(undefined, 'Investigate')).toBe('warning');
|
||||||
expect(outcomeForStatusValue(undefined, 'Passed')).toBe('pass');
|
expect(outcomeForStatusValue(undefined, 'Passed')).toBe('pass');
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,16 @@ describe('processing compatibility contracts', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses the recorded profile when a caller supplies a different detected tool id', () => {
|
it('uses the recorded profile when a caller supplies a different detected tool id', () => {
|
||||||
const defender = file('defender', 'defender-for-iot-vulnerabilities', '1.0.0', '1.0.0');
|
const legacy = file('defender-old', 'defender-for-iot-vulnerabilities', '1.0.0', '1.0.0');
|
||||||
expect(processingReadiness('microsoft-defender-for-iot', defender.processedWith)).toBe('current');
|
const current = file('defender-current', 'defender-for-iot-vulnerabilities', '1.1.0', '1.0.0');
|
||||||
expect(resolvedCompatibilityKey('microsoft-defender-for-iot', defender.processedWith))
|
current.processedWith.normalizationVersion = '1.1.0';
|
||||||
.toBe('defender-for-iot-vulnerabilities-v1');
|
current.processedWith.presentationVersion = '1.0.0';
|
||||||
|
current.processedWith.compatibilityKey = 'defender-for-iot-vulnerabilities-v2';
|
||||||
|
|
||||||
|
expect(processingReadiness('microsoft-defender-for-iot', legacy.processedWith)).toBe('incompatible');
|
||||||
|
expect(processingReadiness('microsoft-defender-for-iot', current.processedWith)).toBe('current');
|
||||||
|
expect(resolvedCompatibilityKey('microsoft-defender-for-iot', current.processedWith))
|
||||||
|
.toBe('defender-for-iot-vulnerabilities-v2');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('blocks semantically incompatible versions in one scope', () => {
|
it('blocks semantically incompatible versions in one scope', () => {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,10 @@ describe('RESULT_CONTRACT result_type derivation', () => {
|
||||||
expect(deriveResultType('cis-cat')).toBe('compliance_check');
|
expect(deriveResultType('cis-cat')).toBe('compliance_check');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('maps Defender CVEs to vulnerability_finding', () => {
|
||||||
|
expect(deriveResultType('defender-for-iot-vulnerabilities')).toBe('vulnerability_finding');
|
||||||
|
});
|
||||||
|
|
||||||
it('falls back to unknown for unknown or generic tools, never inventing exposure/compliance', () => {
|
it('falls back to unknown for unknown or generic tools, never inventing exposure/compliance', () => {
|
||||||
expect(deriveResultType('generic')).toBe('unknown');
|
expect(deriveResultType('generic')).toBe('unknown');
|
||||||
expect(deriveResultType('some-future-tool')).toBe('unknown');
|
expect(deriveResultType('some-future-tool')).toBe('unknown');
|
||||||
|
|
@ -74,6 +78,10 @@ describe('RESULT_CONTRACT is_ioe invariant (never default true outside exposure_
|
||||||
expect(deriveIsIoe('unknown', undefined).isIoe).toBe(false);
|
expect(deriveIsIoe('unknown', undefined).isIoe).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('is false for vulnerability_finding', () => {
|
||||||
|
expect(deriveIsIoe('vulnerability_finding', undefined).isIoe).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('is false for exposure_finding without a detected exposure_state', () => {
|
it('is false for exposure_finding without a detected exposure_state', () => {
|
||||||
expect(deriveIsIoe('exposure_finding', 'not_detected').isIoe).toBe(false);
|
expect(deriveIsIoe('exposure_finding', 'not_detected').isIoe).toBe(false);
|
||||||
expect(deriveIsIoe('exposure_finding', 'not_evaluated').isIoe).toBe(false);
|
expect(deriveIsIoe('exposure_finding', 'not_evaluated').isIoe).toBe(false);
|
||||||
|
|
@ -91,8 +99,8 @@ describe('RESULT_CONTRACT is_ioe invariant (never default true outside exposure_
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('exposure_state only exists for exposure_finding', () => {
|
describe('exposure_state only exists for exposure_finding', () => {
|
||||||
it('is undefined for compliance_check, policy_deviation, informational and unknown', () => {
|
it('is undefined for non-exposure result types', () => {
|
||||||
(['compliance_check', 'policy_deviation', 'informational', 'unknown'] as const).forEach(resultType => {
|
(['vulnerability_finding', 'compliance_check', 'policy_deviation', 'informational', 'unknown'] as const).forEach(resultType => {
|
||||||
expect(deriveExposureState(resultType, 'fail')).toBeUndefined();
|
expect(deriveExposureState(resultType, 'fail')).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -125,9 +125,11 @@ describe('Datahub client processing readiness', () => {
|
||||||
|
|
||||||
it('assesses the stored profile contract independently from the detected tool identity', () => {
|
it('assesses the stored profile contract independently from the detected tool identity', () => {
|
||||||
const defender = withDetectedTool(
|
const defender = withDetectedTool(
|
||||||
file('run-defender', 'defender-for-iot-vulnerabilities', '1.0.0', 'defender-for-iot-vulnerabilities-v1'),
|
file('run-defender', 'defender-for-iot-vulnerabilities', '1.1.0', 'defender-for-iot-vulnerabilities-v2'),
|
||||||
'microsoft-defender-for-iot'
|
'microsoft-defender-for-iot'
|
||||||
);
|
);
|
||||||
|
defender.processedWith.mappingVersion = '1.0.0';
|
||||||
|
defender.processedWith.presentationVersion = '1.0.0';
|
||||||
expect(summarizeScanProcessingReadiness([defender])).toEqual({
|
expect(summarizeScanProcessingReadiness([defender])).toEqual({
|
||||||
state: 'ready', statuses: ['current'], explanation: 'The scan uses a current or compatible ASSESSOR processing profile.'
|
state: 'ready', statuses: ['current'], explanation: 'The scan uses a current or compatible ASSESSOR processing profile.'
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ describe('dynamic facets', () => {
|
||||||
// "Ergebnisstatus" diese Aufgabe. Zwei Filter fuer dieselbe Spalte waren
|
// "Ergebnisstatus" diese Aufgabe. Zwei Filter fuer dieselbe Spalte waren
|
||||||
// fuer die Bedienung nicht unterscheidbar.
|
// fuer die Bedienung nicht unterscheidbar.
|
||||||
expect(facets.some(facet => facet.id === 'field:2')).toBe(false);
|
expect(facets.some(facet => facet.id === 'field:2')).toBe(false);
|
||||||
expect(facets.find(facet => facet.id === 'profile:status')?.values.map(value => value.label)).toEqual(['Deviation']);
|
// "Open" normalisiert auf Outcome unknown und ist nach demselben
|
||||||
|
// kanonischen Praedikat wie in der Result DB keine Abweichung.
|
||||||
|
expect(facets.find(facet => facet.id === 'profile:status')?.values).toEqual([]);
|
||||||
expect(facets.some(facet => facet.id === 'field:3')).toBe(false);
|
expect(facets.some(facet => facet.id === 'field:3')).toBe(false);
|
||||||
expect(facets.some(facet => facet.id === 'field:4')).toBe(false);
|
expect(facets.some(facet => facet.id === 'field:4')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { importProfiles } from '../../src/profiles/profile-catalog';
|
||||||
import { confirmEntryMappings, previewEntryFromDataset } from '../../src/features/import-workflow/import-workflow';
|
import { confirmEntryMappings, previewEntryFromDataset } from '../../src/features/import-workflow/import-workflow';
|
||||||
import { canonicalModelToDataset } from '../../src/features/project-import/canonical-model-to-dataset';
|
import { canonicalModelToDataset } from '../../src/features/project-import/canonical-model-to-dataset';
|
||||||
import { summarizeScanProcessingReadiness } from '../../src/features/project-import/datahub-processing-readiness';
|
import { summarizeScanProcessingReadiness } from '../../src/features/project-import/datahub-processing-readiness';
|
||||||
|
import { createScannerUnifiedResult } from '../../src/domain/unified-result';
|
||||||
|
|
||||||
const summaryHeaders = ['Name', 'IP', 'Site', 'Sensor', 'TotalCVEs', 'Critical', 'High', 'Medium', 'Low'];
|
const summaryHeaders = ['Name', 'IP', 'Site', 'Sensor', 'TotalCVEs', 'Critical', 'High', 'Medium', 'Low'];
|
||||||
const detailHeaders = ['Name', 'IP', 'Site', 'Sensor', 'CVE', 'Severity', 'NULL', 'Beschreibung'];
|
const detailHeaders = ['Name', 'IP', 'Site', 'Sensor', 'CVE', 'Severity', 'NULL', 'Beschreibung'];
|
||||||
|
|
@ -129,7 +130,7 @@ describe('Microsoft Defender for IoT vulnerability model', () => {
|
||||||
const imported = await importFile(new File([bytes], 'defender-synthetic.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), { profiles: importProfiles });
|
const imported = await importFile(new File([bytes], 'defender-synthetic.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), { profiles: importProfiles });
|
||||||
|
|
||||||
expect(imported.profileId).toBe('defender-for-iot-vulnerabilities');
|
expect(imported.profileId).toBe('defender-for-iot-vulnerabilities');
|
||||||
expect(imported.profileVersion).toBe('1.0.0');
|
expect(imported.profileVersion).toBe('1.1.0');
|
||||||
expect(imported.extraction?.extractor).toMatchObject({ id: 'defender-for-iot-vulnerability-source-extractor', version: '1.0.0' });
|
expect(imported.extraction?.extractor).toMatchObject({ id: 'defender-for-iot-vulnerability-source-extractor', version: '1.0.0' });
|
||||||
// Two devices share one CVE: the Work view must show a single aggregated finding, not one row per device.
|
// Two devices share one CVE: the Work view must show a single aggregated finding, not one row per device.
|
||||||
expect(imported.extraction?.candidates).toHaveLength(1);
|
expect(imported.extraction?.candidates).toHaveLength(1);
|
||||||
|
|
@ -151,13 +152,32 @@ describe('Microsoft Defender for IoT vulnerability model', () => {
|
||||||
detectedTool: { id: 'defender-for-iot-vulnerabilities' },
|
detectedTool: { id: 'defender-for-iot-vulnerabilities' },
|
||||||
processedWith: {
|
processedWith: {
|
||||||
profileId: 'defender-for-iot-vulnerabilities',
|
profileId: 'defender-for-iot-vulnerabilities',
|
||||||
profileVersion: '1.0.0',
|
profileVersion: '1.1.0',
|
||||||
compatibilityKey: 'defender-for-iot-vulnerabilities-v1'
|
normalizationVersion: '1.1.0',
|
||||||
|
compatibilityKey: 'defender-for-iot-vulnerabilities-v2'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
expect(summarizeScanProcessingReadiness(canonical.importFiles)).toMatchObject({ state: 'ready', statuses: ['current'] });
|
expect(summarizeScanProcessingReadiness(canonical.importFiles)).toMatchObject({ state: 'ready', statuses: ['current'] });
|
||||||
expect(canonical.findingDefinitions).toHaveLength(1);
|
expect(canonical.findingDefinitions).toHaveLength(1);
|
||||||
expect(canonical.findingOccurrences).toHaveLength(1);
|
expect(canonical.findingOccurrences).toHaveLength(1);
|
||||||
|
expect(canonical.findingOccurrences[0]).toMatchObject({
|
||||||
|
outcome: { normalizedValue: 'warning' },
|
||||||
|
technicalExecutionState: { normalizedValue: 'completed' }
|
||||||
|
});
|
||||||
|
expect(createScannerUnifiedResult({
|
||||||
|
project: canonical.project,
|
||||||
|
assessment: canonical.assessment,
|
||||||
|
scanRun: canonical.scanRuns[0],
|
||||||
|
importFile: canonical.importFiles[0],
|
||||||
|
definition: canonical.findingDefinitions[0],
|
||||||
|
occurrence: canonical.findingOccurrences[0],
|
||||||
|
reviews: canonical.reviews
|
||||||
|
})).toMatchObject({
|
||||||
|
resultType: 'vulnerability_finding',
|
||||||
|
outcomeNormalized: 'warning',
|
||||||
|
isIoe: false,
|
||||||
|
isDeviation: true
|
||||||
|
});
|
||||||
// The aggregated CVE carries its affected-device count as the affected asset (two devices here); the device list lives in the evidence table.
|
// The aggregated CVE carries its affected-device count as the affected asset (two devices here); the device list lives in the evidence table.
|
||||||
expect(canonical.findingOccurrences[0].affectedAsset?.normalizedValue).toBe('2');
|
expect(canonical.findingOccurrences[0].affectedAsset?.normalizedValue).toBe('2');
|
||||||
expect(canonical.findingOccurrences[0].score).toBeUndefined();
|
expect(canonical.findingOccurrences[0].score).toBeUndefined();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { resolve } from 'node:path';
|
||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { databaseMigrations } from '../../server/postgres/migration-catalog';
|
||||||
|
|
||||||
|
const migrationPath = resolve(
|
||||||
|
import.meta.dirname,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'server',
|
||||||
|
'postgres',
|
||||||
|
'migrations',
|
||||||
|
'027_vulnerability_finding_result_type.sql'
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('PostgreSQL vulnerability finding result type migration', () => {
|
||||||
|
it('registers migration 027 after regulatory mapping', () => {
|
||||||
|
expect(databaseMigrations.at(-1)).toMatchObject({
|
||||||
|
version: '027',
|
||||||
|
name: 'vulnerability-finding-result-type'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extends the closed result type constraint without rewriting occurrences', async () => {
|
||||||
|
const sql = await readFile(migrationPath, 'utf8');
|
||||||
|
expect(sql).toContain("'vulnerability_finding'");
|
||||||
|
expect(sql).toContain('assessor_occurrences_result_type_check');
|
||||||
|
expect(sql).not.toMatch(/UPDATE\s+assessor_finding_occurrences/i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -185,6 +185,14 @@ describe('declarative tool profiles', () => {
|
||||||
expect(profile?.matchesStatus(imported.sheets[0].analysis.records[0], 'deviation')).toBe(true);
|
expect(profile?.matchesStatus(imported.sheets[0].analysis.records[0], 'deviation')).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the canonical missing-status warning for Defender CVEs in Work', () => {
|
||||||
|
const profile = importProfiles.find(candidate => candidate.id === 'defender-for-iot-vulnerabilities');
|
||||||
|
const fields = [{ label: 'CVE', value: 'CVE-2026-0001', index: 0 }, { label: 'Severity', value: 'Critical', index: 1 }];
|
||||||
|
const record = { id: 'defender-record', index: 0, title: 'CVE-2026-0001', fields, filled: fields, meta: [] };
|
||||||
|
|
||||||
|
expect(profile?.matchesStatus(record, 'deviation')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('requires confirmation and keeps the generic profile when only the file name identifies a tool', async () => {
|
it('requires confirmation and keeps the generic profile when only the file name identifies a tool', async () => {
|
||||||
const imported = await importFile(new File(['Alpha;Beta\none;two'], 'CIS-CAT-SYNTHETIC.csv', { type: 'text/csv' }), options);
|
const imported = await importFile(new File(['Alpha;Beta\none;two'], 'CIS-CAT-SYNTHETIC.csv', { type: 'text/csv' }), options);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue