feat(defender-for-iot): update to version 1.2.0 with new features and improvements

This commit is contained in:
Binh 2026-09-14 10:51:33 +02:00
parent b438f25fd8
commit c7c716c88b
10 changed files with 143 additions and 25 deletions

View File

@ -2,6 +2,10 @@
Status: lebendes Projektdokument Status: lebendes Projektdokument
Defender-Mock-CVE-Kontext 2026-09-14 (Phase 6/13A mit Phase-7-Projektion, Nutzerkorrektur): Die synthetische `ot-sensor/data-mining/cves.csv` enthaelt jetzt je CVE-Zeile die ueber `Device ID` konsistent aus dem Mock-Inventar uebernommenen Felder `Site` und `Sensor`. Dadurch zeigt die CVE-Ansicht `Riverbend Plant` und `SENS-RB-01` auch dann, wenn nur die CVE-Datei importiert wird; der vorhandene Inventarjoin bleibt fuer gemeinsame Mehrdateiimporte erhalten. Beide Spalten sind in `schema_metadata.csv` ausdruecklich als simulierte Fixture-Felder gekennzeichnet, Spaltenzahl und SHA-256 im Pack-Manifest wurden aktualisiert und der Validierungsbericht prueft den Join. Der Parser konnte beide Felder bereits verlustfrei lesen, daher keine neue Profil-/Extractorversion und keine Reimportregel fuer echte Scannerberichte. Verifikation ausschliesslich synthetisch: alle 19 Mock-CSVs stimmen in Zeilen-, Spalten-, Schema- und Hashangaben mit dem Manifest ueberein; fuenf CVE-Kontexte stimmen mit dem Inventar ueberein; Artifact-Tool-Inspektion ohne Fehlerfund; 1.245 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Keine Realdateninspektion, Datenbankmutation, Migration oder Backendneustart; Phasenstatus unveraendert.
Defender-Mock-CSV-Import 2026-09-11 (Phase 6/13A mit Phase-7-Projektion, Nutzerauftrag): Da der bearbeitete Defender-Excelbericht nicht mehr als Rohquelle verwendbar ist, verarbeitet das Defender-Profil 1.2.0 vorlaeufig die synthetischen `Mock-Defender`-Dateien `cves.csv` und `device_inventory.csv` ueber den bestehenden generischen CSV-Adapter. CVE- und Inventarzeilen werden nur ueber die explizite `Device ID` verbunden; Site und Sensor stammen nur aus vorhandenen Inventarfeldern, fehlende Beschreibungen bleiben leer. `CVSS Score`, `Component` und der Mock-Workflowstatus bleiben unveraenderte Evidenz und werden weder als nativer Gesamtscore noch als Pass/Fail-Ergebnis interpretiert. Der Source Extractor ist auf 1.1.0 und das append-only Defender-Profil auf 1.2.0 versioniert; Profil 1.1.0 bleibt lesekompatibel, wird aber gemaess Reimport-Policy als aktualisierbar markiert. Verifikation ausschliesslich mit synthetischen Daten: 1.245 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only- und Designpruefung sowie Client-/Serverbuild erfolgreich. Keine Realdateninspektion, Datenbankmutation, Migration oder Backendneustart; Phasenstatus unveraendert.
Defender-Device-CVE-Kurzliste 2026-09-11 (Phase 7, Nutzerpraezisierung): Das Device-Detail und die dort wiederverwendete Device-Liste innerhalb eines Site-Details zeigen je zugeordnetem CVE nur noch CVE-Kennung und Severity. Description und Reporting sensors entfallen aus dieser Tabelle, weil die CVE-Beschreibung in der CVE-Ansicht und der Sensor bereits im Device-Kontext verfuegbar sind. Eine synthetische Interaktionsregression sichert den exakten Zwei-Spalten-Vertrag. Verifikation: 1.243 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only- und Designpruefung sowie Client-/Serverbuild erfolgreich. Reine Darstellungsaenderung; Quelldaten bleiben im Modell erhalten, keine Datenmutation, API-, Persistenz-, Importer-, Profil-, Scoring- oder Reimportaenderung. Phasenstatus unveraendert. Defender-Device-CVE-Kurzliste 2026-09-11 (Phase 7, Nutzerpraezisierung): Das Device-Detail und die dort wiederverwendete Device-Liste innerhalb eines Site-Details zeigen je zugeordnetem CVE nur noch CVE-Kennung und Severity. Description und Reporting sensors entfallen aus dieser Tabelle, weil die CVE-Beschreibung in der CVE-Ansicht und der Sensor bereits im Device-Kontext verfuegbar sind. Eine synthetische Interaktionsregression sichert den exakten Zwei-Spalten-Vertrag. Verifikation: 1.243 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only- und Designpruefung sowie Client-/Serverbuild erfolgreich. Reine Darstellungsaenderung; Quelldaten bleiben im Modell erhalten, keine Datenmutation, API-, Persistenz-, Importer-, Profil-, Scoring- oder Reimportaenderung. Phasenstatus unveraendert.
Defender-CVE-Evidenzliste 2026-09-11 (Phase 7, Nutzerpraezisierung): Das geoeffnete CVE-Detail zeigt alle betroffenen Geraete und ihre Sensorbeobachtungen unmittelbar in einer gemeinsamen flachen Liste statt als einzeln aufklappbare Device-Gruppen. Die Liste enthaelt nur Device, IP, Site und Sensor; interne Source-row-Provenienz und die bereits durch das ausgewaehlte CVE feststehende Severity werden nicht redundant angezeigt. Die Detailprojektion wird nur fuer das aktuell geoeffnete CVE aufgebaut. Eine synthetische Regression mit zwei Geraeten fuer dasselbe CVE prueft fehlende Unter-Expander, beide sichtbaren Zeilen und den exakten Vier-Spalten-Vertrag. Verifikation: 1.243 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only- und Designpruefung sowie Client-/Serverbuild erfolgreich. Reine Darstellungsaenderung; Quellprovenienz und Severity bleiben im Modell erhalten, keine Datenmutation, API-, Persistenz-, Importer-, Profil-, Scoring- oder Reimportaenderung. Phasenstatus unveraendert. Defender-CVE-Evidenzliste 2026-09-11 (Phase 7, Nutzerpraezisierung): Das geoeffnete CVE-Detail zeigt alle betroffenen Geraete und ihre Sensorbeobachtungen unmittelbar in einer gemeinsamen flachen Liste statt als einzeln aufklappbare Device-Gruppen. Die Liste enthaelt nur Device, IP, Site und Sensor; interne Source-row-Provenienz und die bereits durch das ausgewaehlte CVE feststehende Severity werden nicht redundant angezeigt. Die Detailprojektion wird nur fuer das aktuell geoeffnete CVE aufgebaut. Eine synthetische Regression mit zwei Geraeten fuer dasselbe CVE prueft fehlende Unter-Expander, beide sichtbaren Zeilen und den exakten Vier-Spalten-Vertrag. Verifikation: 1.243 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only- und Designpruefung sowie Client-/Serverbuild erfolgreich. Reine Darstellungsaenderung; Quellprovenienz und Severity bleiben im Modell erhalten, keine Datenmutation, API-, Persistenz-, Importer-, Profil-, Scoring- oder Reimportaenderung. Phasenstatus unveraendert.
@ -1678,6 +1682,8 @@ Festlegung 2026-09-06 (Phase 13B/13C, ausdruecklicher Nutzerwunsch): Vor Rueckfr
Festlegung vom 2026-09-06 (Phase 11, lokaler Persistenzbetrieb): Der vorhandene Docker-Compose-Bestand `assessor-2_0` mit der Datenbank `assessor`, dem Container `assessor-2_0-postgres-1` und dem persistenten Volume `assessor-2_0_assessor-postgres-data` ist der einzige kanonische lokale ASSESSOR-Datenbestand fuer diesen Workspace. Lokale Starts verwenden ausschliesslich `127.0.0.1:5432`. Die Compose-Konfiguration bindet dieses Volume deshalb namentlich als `external`; bei fehlendem Bestand oder Portkonflikt muss der Start fehlschlagen, statt eine weitere ASSESSOR-Datenbank oder einen Ersatzport anzulegen. Diese ausdrueckliche Nutzerfestlegung ersetzt die bisherige implizite, verzeichnisabhaengige Compose-Projektidentitaet. Der interne Browser-/Backend-/PostgreSQL-Zielaufbau bleibt unveraendert. Festlegung vom 2026-09-06 (Phase 11, lokaler Persistenzbetrieb): Der vorhandene Docker-Compose-Bestand `assessor-2_0` mit der Datenbank `assessor`, dem Container `assessor-2_0-postgres-1` und dem persistenten Volume `assessor-2_0_assessor-postgres-data` ist der einzige kanonische lokale ASSESSOR-Datenbestand fuer diesen Workspace. Lokale Starts verwenden ausschliesslich `127.0.0.1:5432`. Die Compose-Konfiguration bindet dieses Volume deshalb namentlich als `external`; bei fehlendem Bestand oder Portkonflikt muss der Start fehlschlagen, statt eine weitere ASSESSOR-Datenbank oder einen Ersatzport anzulegen. Diese ausdrueckliche Nutzerfestlegung ersetzt die bisherige implizite, verzeichnisabhaengige Compose-Projektidentitaet. Der interne Browser-/Backend-/PostgreSQL-Zielaufbau bleibt unveraendert.
| Datum | Entscheidung | Begründung | | Datum | Entscheidung | Begründung |
| 2026-09-14 | Die synthetische Mock-CVE-Datei wiederholt `Site` und `Sensor` je Zeile als ausdruecklich simulierte, ueber `Device ID` konsistent abgeleitete Fixture-Felder. Der Parser behandelt sie wie vorhandene optionale CVE-Kontextfelder; der Mehrdatei-Inventarjoin bleibt zusaetzlich bestehen. | Die CVE-Datei muss den Mockup auch als Einzelimport vollstaendig speisen. Die Kennzeichnung verhindert, dass die Zusatzspalten als behaupteter nativer Defender-Exportvertrag missverstanden werden. |
| 2026-09-11 | Bis ein unveraenderter nativer Defender-for-IoT-Export vorliegt, ist das synthetische `Mock-Defender`-Paket die Entwicklungs- und Testquelle fuer den Defender-Importer. Actionable CVEs kommen ausschliesslich aus der strukturell validierten `cves.csv`; optionale Device-Inventare reichern nur ueber die explizite `Device ID` an. | Das bearbeitete Excel ist keine belastbare Rohquelle mehr. Der enge CSV-Vertrag ermoeglicht reproduzierbare Entwicklung ohne erfundene Site-, Sensor-, Beschreibungs- oder Scoresemantik und laesst sich spaeter anhand eines echten Exports versioniert validieren. |
| 2026-09-07 | Die geladene Client Info ist innerhalb der Wurzelklasse `.client-command-center` eine feste dunkle Command-Center-Ausnahme von der allgemeinen Kachelkomposition. Die genehmigte Hierarchie ist Client/Assessment/Source, sechs Icon-Fakten, Overall Score mit acht visuell verbundenen Security Domains, Priority Findings plus Assessment Gaps und abschliessend fuenf werkzeugspezifische Source-Flaechen. | Die ausdruecklich genehmigte Referenz ist ein zusammenhaengendes Erstgespraechs-Dashboard und keine Sammlung allgemeiner Anwendungskacheln. Eine eng begrenzte Ausnahme bildet diese Hierarchie originalgetreu ab, ohne Data Hub, Work, Result DB, Pathfinder oder Shell visuell zu veraendern. | | 2026-09-07 | Die geladene Client Info ist innerhalb der Wurzelklasse `.client-command-center` eine feste dunkle Command-Center-Ausnahme von der allgemeinen Kachelkomposition. Die genehmigte Hierarchie ist Client/Assessment/Source, sechs Icon-Fakten, Overall Score mit acht visuell verbundenen Security Domains, Priority Findings plus Assessment Gaps und abschliessend fuenf werkzeugspezifische Source-Flaechen. | Die ausdruecklich genehmigte Referenz ist ein zusammenhaengendes Erstgespraechs-Dashboard und keine Sammlung allgemeiner Anwendungskacheln. Eine eng begrenzte Ausnahme bildet diese Hierarchie originalgetreu ab, ohne Data Hub, Work, Result DB, Pathfinder oder Shell visuell zu veraendern. |
| 2026-09-07 | Die Nutzerpraezisierung erweitert die unmittelbar folgende Positionsentscheidung: `Source: Scan` ist eine workspaceweite Navigationsfacette mit stabiler ID und vollstaendiger Optionsliste aus dem projektweiten All-Records-Blatt. Eine Auswahl aus einem dedizierten GPO-/Fachblatt wechselt auf dieses Aggregat und aktiviert dort direkt den ausgewaehlten Scan; lokale Facettennachzaehlung darf die Liste nicht reduzieren. | Der Filter soll nicht nur die aktuell sichtbaren Records einschraenken, sondern jederzeit den direkten Wechsel zu jedem geoeffneten Scan ermoeglichen. Nur die projektweite Quelle und ein positionsunabhaengiger Identifikator verhindern, dass ein Fachblatt alle anderen Scanner aus der Auswahl verliert oder der gesetzte Filter beim Blattwechsel unbrauchbar wird. | | 2026-09-07 | Die Nutzerpraezisierung erweitert die unmittelbar folgende Positionsentscheidung: `Source: Scan` ist eine workspaceweite Navigationsfacette mit stabiler ID und vollstaendiger Optionsliste aus dem projektweiten All-Records-Blatt. Eine Auswahl aus einem dedizierten GPO-/Fachblatt wechselt auf dieses Aggregat und aktiviert dort direkt den ausgewaehlten Scan; lokale Facettennachzaehlung darf die Liste nicht reduzieren. | Der Filter soll nicht nur die aktuell sichtbaren Records einschraenken, sondern jederzeit den direkten Wechsel zu jedem geoeffneten Scan ermoeglichen. Nur die projektweite Quelle und ein positionsunabhaengiger Identifikator verhindern, dass ein Fachblatt alle anderen Scanner aus der Auswahl verliert oder der gesetzte Filter beim Blattwechsel unbrauchbar wird. |
| 2026-09-07 | `Source: Scan` ist ein permanenter Provenienzfilter jeder Work-/Graph-Ansicht und steht stets als letzter Filter ganz rechts. In der reinen GPO-Ansicht ruecken die fachlichen Filter linksbuendig zusammen; deren sichtbares Limit betraegt elf statt zehn, damit Status, neun GPO-Fachfacetten und die Source-Facette gleichzeitig erhalten bleiben. Ein fehlendes explizites Source-Feld wird praesentativ aus Scan-/Profilmetadaten ergaenzt. | Die Quellenherkunft muss unabhaengig vom aktiven Werkzeug jederzeit auffindbar und an einer stabilen Position bedienbar sein. Einwertige Source-Werte sind fuer Provenienz weiterhin relevant und duerfen weder durch die allgemeine Nuetzlichkeitspruefung noch durch das Facettenlimit verschwinden. | | 2026-09-07 | `Source: Scan` ist ein permanenter Provenienzfilter jeder Work-/Graph-Ansicht und steht stets als letzter Filter ganz rechts. In der reinen GPO-Ansicht ruecken die fachlichen Filter linksbuendig zusammen; deren sichtbares Limit betraegt elf statt zehn, damit Status, neun GPO-Fachfacetten und die Source-Facette gleichzeitig erhalten bleiben. Ein fehlendes explizites Source-Feld wird praesentativ aus Scan-/Profilmetadaten ergaenzt. | Die Quellenherkunft muss unabhaengig vom aktiven Werkzeug jederzeit auffindbar und an einer stabilen Position bedienbar sein. Einwertige Source-Werte sind fuer Provenienz weiterhin relevant und duerfen weder durch die allgemeine Nuetzlichkeitspruefung noch durch das Facettenlimit verschwinden. |
@ -2038,6 +2044,8 @@ Festgelegtes Betriebsrisiko vom 2026-09-06: Verzeichnisabhaengige Compose-Projek
| Ein lokal bereitgestellter Policy-Regelkatalog kann nach fachlicher Freigabe unbemerkt veraendert, nur teilweise konfiguriert, als Symlink umgebogen oder mit ungueltigen Runtime-Strukturen geladen werden und dadurch andere Findings erzeugen. | Pfad und SHA-256 als untrennbares Konfigurationspaar verlangen, nur regulaere Nicht-Symlink-Dateien bis 5 MiB lesen, Hash vor UTF-8-/JSON-Parsing vergleichen und danach den geschlossenen Katalogvertrag fail-closed validieren. Bei jedem Fehler vor Repository- und Serverstart abbrechen; Evaluation Runs bewahren weiterhin den vollstaendigen tatsaechlich verwendeten Katalog. | | Ein lokal bereitgestellter Policy-Regelkatalog kann nach fachlicher Freigabe unbemerkt veraendert, nur teilweise konfiguriert, als Symlink umgebogen oder mit ungueltigen Runtime-Strukturen geladen werden und dadurch andere Findings erzeugen. | Pfad und SHA-256 als untrennbares Konfigurationspaar verlangen, nur regulaere Nicht-Symlink-Dateien bis 5 MiB lesen, Hash vor UTF-8-/JSON-Parsing vergleichen und danach den geschlossenen Katalogvertrag fail-closed validieren. Bei jedem Fehler vor Repository- und Serverstart abbrechen; Evaluation Runs bewahren weiterhin den vollstaendigen tatsaechlich verwendeten Katalog. |
| Historische GPO-Reprocessing-Ketten koennen alte Konflikte, Reviewhinweise, Abweichungen und Findings mehrfach als aktuellen Zustand erscheinen lassen; wenn UI und Aktionsableitung unterschiedliche Auswahlregeln verwenden, bleiben ausserdem veraltete Evaluationen bedienbar. | Aktive Profile und nicht ersetzte Identity-Analysen je Artefakt als Wurzel einer gemeinsamen deterministischen Aktivkette verwenden; nur dazu passende Aggregationen, jeweils neueste Evaluationen und Finding-Runs in die aktuelle Sicht aufnehmen. Historie getrennt erhalten und dieselbe Auswahlfunktion fuer Summary, Review Queue sowie Evaluate-/Finding-Aktionen regressionspruefen. | | Historische GPO-Reprocessing-Ketten koennen alte Konflikte, Reviewhinweise, Abweichungen und Findings mehrfach als aktuellen Zustand erscheinen lassen; wenn UI und Aktionsableitung unterschiedliche Auswahlregeln verwenden, bleiben ausserdem veraltete Evaluationen bedienbar. | Aktive Profile und nicht ersetzte Identity-Analysen je Artefakt als Wurzel einer gemeinsamen deterministischen Aktivkette verwenden; nur dazu passende Aggregationen, jeweils neueste Evaluationen und Finding-Runs in die aktuelle Sicht aufnehmen. Historie getrennt erhalten und dieselbe Auswahlfunktion fuer Summary, Review Queue sowie Evaluate-/Finding-Aktionen regressionspruefen. |
|---|---| |---|---|
| Die in der Mock-CVE-Datei wiederholten Site-/Sensorwerte koennten vom Device-Inventar abweichen oder spaeter faelschlich als native CVE-Exportfelder gelten. | Beide Felder als `simulated_fixture_field` dokumentieren, ueber `Device ID` gegen genau eine Inventarzeile validieren und Manifesthash sowie Spaltenmetadaten bei jeder Aenderung gemeinsam aktualisieren. Ein echter unveraenderter Export bleibt vor Produktivnutzung erforderlich. |
| Die Mock-Defender-CSV-Header sind synthetisch und koennen von einem spaeteren nativen Produktexport abweichen; eine breite Heuristik koennte zudem fremde CVE- oder Inventarlisten als Defender erkennen. | Nur die vollstaendige bekannte Mock-CVE- beziehungsweise Inventarstruktur akzeptieren, Werte und Provenienz verlustfrei behalten und Device-Kontext ausschliesslich per `Device ID` verbinden. Einen echten unveraenderten Export vor Produktivnutzung mit synthetischen Regressionen validieren und jede notwendige Alias-/Semantikaenderung append-only versionieren. |
| Alte Defender-Importe mit Profil 1.0.0 tragen im gespeicherten Occurrence-JSON weiterhin `outcome=unknown`; wenn Work bereits die aktuelle Profilregel verwendet, koennten Projektion und SQL-Vorfilterung widerspruechlich werden. | Profil 1.1.0 als semantisch inkompatiblen Vertrag mit neuem Compatibility Key registrieren. Die unveraenderliche Migration 022 erweitert den Result-Type-Constraint; Migration 023 zieht ausschliesslich die abgeleiteten relationalen Read-Model-Spalten `result_type` und `outcome_normalized` fuer bestehende Defender-Vorkommen nach. Originalfelder, Occurrence-JSON und Profilprovenienz bleiben unveraendert. Die Leseprojektion behandelt einen nachweislich statuslosen historischen Vulnerability-Fund ebenso. Reimport/Reprocessing bleibt fuer einen vollstaendig aktuellen kanonischen Datensatz erforderlich. | | Alte Defender-Importe mit Profil 1.0.0 tragen im gespeicherten Occurrence-JSON weiterhin `outcome=unknown`; wenn Work bereits die aktuelle Profilregel verwendet, koennten Projektion und SQL-Vorfilterung widerspruechlich werden. | Profil 1.1.0 als semantisch inkompatiblen Vertrag mit neuem Compatibility Key registrieren. Die unveraenderliche Migration 022 erweitert den Result-Type-Constraint; Migration 023 zieht ausschliesslich die abgeleiteten relationalen Read-Model-Spalten `result_type` und `outcome_normalized` fuer bestehende Defender-Vorkommen nach. Originalfelder, Occurrence-JSON und Profilprovenienz bleiben unveraendert. Die Leseprojektion behandelt einen nachweislich statuslosen historischen Vulnerability-Fund ebenso. Reimport/Reprocessing bleibt fuer einen vollstaendig aktuellen kanonischen Datensatz erforderlich. |
| NVD kann nicht erreichbar oder rate-limitiert sein, unerwartet grosse Antworten liefern oder externe Referenz-URLs enthalten; ein API-Schluessel darf nicht an Browser oder Logs gelangen. Eine automatische Anreicherung waehrend des Imports wuerde zudem dessen Reproduzierbarkeit und Verfuegbarkeit an einen externen Dienst koppeln. | Enrichment ausschliesslich separat und nutzerausgeloest im Backend ausfuehren, nur exakt gespeicherte Defender-CVE-IDs akzeptieren, Zielorigin und API-Pfad fest verdrahten sowie Timeout und 8-MiB-Antwortgrenze erzwingen. Den optionalen Schluessel nur aus `NVD_API_KEY` als Serverheader senden, Antwortfelder normalisieren, Referenzen nur als HTTP(S)-Links darstellen und Importfehler strikt vom Enrichment-Ergebnis trennen. | | NVD kann nicht erreichbar oder rate-limitiert sein, unerwartet grosse Antworten liefern oder externe Referenz-URLs enthalten; ein API-Schluessel darf nicht an Browser oder Logs gelangen. Eine automatische Anreicherung waehrend des Imports wuerde zudem dessen Reproduzierbarkeit und Verfuegbarkeit an einen externen Dienst koppeln. | Enrichment ausschliesslich separat und nutzerausgeloest im Backend ausfuehren, nur exakt gespeicherte Defender-CVE-IDs akzeptieren, Zielorigin und API-Pfad fest verdrahten sowie Timeout und 8-MiB-Antwortgrenze erzwingen. Den optionalen Schluessel nur aus `NVD_API_KEY` als Serverheader senden, Antwortfelder normalisieren, Referenzen nur als HTTP(S)-Links darstellen und Importfehler strikt vom Enrichment-Ergebnis trennen. |
| Defender-for-IoT-Summarys koennen Severity-Zahlen ohne einzelne CVEs enthalten; wiederholte Sensorzeilen koennen dieselbe Device-CVE-Beziehung mehrfach melden, und pseudonymisierte oder wiederverwendete Adressen koennen Identitaeten mehrdeutig machen. | Aggregate ohne Detail niemals in Findings umwandeln, Device-mal-CVE vor der kanonischen Projektion deduplizieren, alle Sensorzeilen als Evidenz erhalten, Identitaet nach der dokumentierten MAC-/IP-/Name-Site-Reihenfolge bilden und Konflikte beziehungsweise Reconciliation-Abweichungen diagnostizieren statt still zusammenzufuehren. | | Defender-for-IoT-Summarys koennen Severity-Zahlen ohne einzelne CVEs enthalten; wiederholte Sensorzeilen koennen dieselbe Device-CVE-Beziehung mehrfach melden, und pseudonymisierte oder wiederverwendete Adressen koennen Identitaeten mehrdeutig machen. | Aggregate ohne Detail niemals in Findings umwandeln, Device-mal-CVE vor der kanonischen Projektion deduplizieren, alle Sensorzeilen als Evidenz erhalten, Identitaet nach der dokumentierten MAC-/IP-/Name-Site-Reihenfolge bilden und Konflikte beziehungsweise Reconciliation-Abweichungen diagnostizieren statt still zusammenzufuehren. |
@ -2272,6 +2280,8 @@ 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-09-14 | Phase 6/13A: `Site` und `Sensor` in der synthetischen Mock-Defender-`cves.csv` ergaenzt, sodass die Phase-7-CVE-Ansicht auch beim Einzelimport den Standort und meldenden Sensor zeigt. Schema-Metadaten, README, Validierungsbericht, Manifestspaltenzahl und SHA-256 wurden konsistent aktualisiert. Eine strukturgleiche synthetische Importregression prueft alle fuenf sichtbaren CVE-Kontexte auch nach kanonischer Persistenzprojektion. Alle 19 lokalen Mock-CSVs bestehen Zeilen-/Spalten-/Schema-/Hashpruefung; 1.245 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und beide Builds erfolgreich. Reine synthetische Fixture-Korrektur; keine Parser-, Profil-, Persistenz-, Schema- oder Realdatenaenderung. |
| 2026-09-11 | Phase 6/13A: Defender-Import auf das synthetische `Mock-Defender`-CSV-Schema erweitert. `cves.csv` wird eigenstaendig erkannt und in aggregierte CVE-Findings ueberfuehrt; `device_inventory.csv` liefert per `Device ID` die Device-, Site- und Sensoranreicherung. Mock-spezifische CVSS-, Component- und Statuswerte bleiben Evidenz, fehlende Werte werden nicht erfunden. Source Extractor 1.1.0, Defender-Profil und Processing-Manifest 1.2.0 append-only registriert; Profil 1.1.0 wird als Reimportempfehlung sichtbar. Die Regression importiert die tatsaechliche synthetische Mock-CVE-Datei und prueft fuenf Findings sowie den Inventarjoin. 1.245 Tests bestanden, acht optionale PostgreSQL-Tests uebersprungen; Local-only-/Designpruefung und Client-/Serverbuild erfolgreich. Keine Realdaten-, Datenbank- oder Migrationsaenderung; Phasenstatus unveraendert. |
| 2026-09-11 | Phasen 2/6/7/11 um den eigenen Schwachstellen-Ergebnistyp erweitert: `vulnerability_finding` ist eine additive ASSESSOR-Erweiterung des unveraenderten externen Result Contract 1.0.0. Das Defender-Profil 1.1.0 setzt fuer statuslose CVE-Findings deklarativ `outcome=warning`; Work und Unified Result projizieren dadurch `result_type=vulnerability_finding`, `is_ioe=false` und `deviation=true`. Die bereits angewandte Migration 022 bleibt checksum-identisch und erweitert nur den geschlossenen PostgreSQL-Constraint; die nachtraeglich benoetigte Aktualisierung bestehender abgeleiteter Result-Type-/Outcome-Spalten liegt append-only in Migration 023. Originalfelder, Occurrence-JSON und Profilprovenienz bleiben unveraendert. Profil 1.0.0 bleibt wegen der geaenderten kanonischen Semantik inkompatibel und benoetigt fuer einen vollstaendig aktuellen Datensatz Reimport/Reprocessing. 787 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-, Designpruefung und Client-/Server-Produktionsbuild erfolgreich. Phasen 2, 6, 7 und 11 bleiben abgeschlossen. | | 2026-09-11 | Phasen 2/6/7/11 um den eigenen Schwachstellen-Ergebnistyp erweitert: `vulnerability_finding` ist eine additive ASSESSOR-Erweiterung des unveraenderten externen Result Contract 1.0.0. Das Defender-Profil 1.1.0 setzt fuer statuslose CVE-Findings deklarativ `outcome=warning`; Work und Unified Result projizieren dadurch `result_type=vulnerability_finding`, `is_ioe=false` und `deviation=true`. Die bereits angewandte Migration 022 bleibt checksum-identisch und erweitert nur den geschlossenen PostgreSQL-Constraint; die nachtraeglich benoetigte Aktualisierung bestehender abgeleiteter Result-Type-/Outcome-Spalten liegt append-only in Migration 023. Originalfelder, Occurrence-JSON und Profilprovenienz bleiben unveraendert. Profil 1.0.0 bleibt wegen der geaenderten kanonischen Semantik inkompatibel und benoetigt fuer einen vollstaendig aktuellen Datensatz Reimport/Reprocessing. 787 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-, Designpruefung und Client-/Server-Produktionsbuild erfolgreich. Phasen 2, 6, 7 und 11 bleiben abgeschlossen. |
| 2026-09-11 | Phase-7-Abweichungsfilter zwischen Work und Result DB vereinheitlicht: Work prueft `deviation` nicht mehr gegen beliebige profilbezogene Rohwertmuster wie Defender-Severity `Critical`, sondern rekonstruiert aus Werkzeug und Statusfeld dieselben kanonischen Achsen Outcome, Result Type, Exposure State und `is_ioe` und verwendet anschliessend das gemeinsame versionierte Abweichungspraedikat. Defender-CVEs ohne Status bleiben damit wie in der Result DB `outcome=unknown`, `is_ioe=false`, `deviation=false`; Fail-/Warning-Ergebnisse und festgestellte Expositionen bleiben Abweichungen. Keine Daten-, API-, Schema-, Persistenz- oder Profilversionsaenderung. 781 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-, Designpruefung und Client-/Server-Produktionsbuild erfolgreich. Phase 7 bleibt abgeschlossen. | | 2026-09-11 | Phase-7-Abweichungsfilter zwischen Work und Result DB vereinheitlicht: Work prueft `deviation` nicht mehr gegen beliebige profilbezogene Rohwertmuster wie Defender-Severity `Critical`, sondern rekonstruiert aus Werkzeug und Statusfeld dieselben kanonischen Achsen Outcome, Result Type, Exposure State und `is_ioe` und verwendet anschliessend das gemeinsame versionierte Abweichungspraedikat. Defender-CVEs ohne Status bleiben damit wie in der Result DB `outcome=unknown`, `is_ioe=false`, `deviation=false`; Fail-/Warning-Ergebnisse und festgestellte Expositionen bleiben Abweichungen. Keine Daten-, API-, Schema-, Persistenz- oder Profilversionsaenderung. 781 Tests bestanden, 8 optionale PostgreSQL-Tests uebersprungen; Local-only-, Designpruefung und Client-/Server-Produktionsbuild erfolgreich. Phase 7 bleibt abgeschlossen. |
| 2026-09-10 | Result-DB-Beschreibungsdeduplizierung fuer einzelne Defender-CVEs nachgehaertet: Scanner- und NVD-Beschreibung werden fuer den reinen Anzeigevergleich jetzt Unicode-NFKC-normalisiert, von unsichtbaren Formatzeichen bereinigt, case-insensitiv behandelt und ueber ihre Wort-/Zahlfolge verglichen. Dadurch werden visuell identische Saetze auch bei Soft-Hyphen-, Zeichensetzungs- oder Grossschreibungsabweichungen nicht doppelt gerendert; inhaltlich unterschiedliche Wortfolgen bleiben sichtbar. Gespeicherte Quelltexte bleiben unveraendert. Drei fokussierte Regressionstests und der Client-Produktionsbuild sind erfolgreich. | | 2026-09-10 | Result-DB-Beschreibungsdeduplizierung fuer einzelne Defender-CVEs nachgehaertet: Scanner- und NVD-Beschreibung werden fuer den reinen Anzeigevergleich jetzt Unicode-NFKC-normalisiert, von unsichtbaren Formatzeichen bereinigt, case-insensitiv behandelt und ueber ihre Wort-/Zahlfolge verglichen. Dadurch werden visuell identische Saetze auch bei Soft-Hyphen-, Zeichensetzungs- oder Grossschreibungsabweichungen nicht doppelt gerendert; inhaltlich unterschiedliche Wortfolgen bleiben sichtbar. Gespeicherte Quelltexte bleiben unveraendert. Drei fokussierte Regressionstests und der Client-Produktionsbuild sind erfolgreich. |

View File

@ -32,7 +32,7 @@ Das Schema steht in `src/domain/tool-profile-definition.ts`. Die gemeinsame Lauf
| PingCastle | 1.0.0 | Healthcheck, Risk Scores und Regelkennungen | | PingCastle | 1.0.0 | Healthcheck, Risk Scores und Regelkennungen |
| CIS-CAT Pro | 1.0.0 | Assessment Results, Recommendation und Pass/Fail | | CIS-CAT Pro | 1.0.0 | Assessment Results, Recommendation und Pass/Fail |
| Maester | 1.0.0 | flache TestResults-Exporte, MT-IDs und ResultDetail | | Maester | 1.0.0 | flache TestResults-Exporte, MT-IDs und ResultDetail |
| Microsoft Defender for IoT | 1.0.0 | Asset-Summary, CVE-Details, Device-Identitaet und Sensorprovenienz | | Microsoft Defender for IoT | 1.2.0 | Asset-Summary, Mock-CVE-CSV, Device-Inventar, Device-ID-Verknuepfung und Sensorprovenienz |
Die Definitionen liegen jeweils im Verzeichnis des Werkzeugs. `profile-definition-catalog.ts` stellt sie der Erkennungsengine bereit; `profile-catalog.ts` stellt die daraus erzeugten Laufzeitprofile für den Import und die Oberfläche bereit. Die Definitionen liegen jeweils im Verzeichnis des Werkzeugs. `profile-definition-catalog.ts` stellt sie der Erkennungsengine bereit; `profile-catalog.ts` stellt die daraus erzeugten Laufzeitprofile für den Import und die Oberfläche bereit.
@ -65,7 +65,7 @@ Ein neues Werkzeug benötigt:
Formatparser, Erkennungsalgorithmus und React-Komponenten müssen dafür nicht geändert werden. Formatparser, Erkennungsalgorithmus und React-Komponenten müssen dafür nicht geändert werden.
Microsoft Defender for IoT verwendet weiterhin den generischen Excel-/CSV-Formatadapter. Ein kleiner Source Extractor verbindet Summary- und Detailtabellen, erzeugt genau ein Finding-Vorkommen je normalisiertem Device und CVE und bewahrt wiederholte Sensorzeilen als Evidenz. Summary-Werte ohne Detail-CVE bleiben separate Aggregate und werden nicht in Findings umgewandelt. Die fachlichen CVE-, Device- und Site-Projektionen liegen ausserhalb der React-Komponente in einem deterministischen Domain-Modell. Microsoft Defender for IoT verwendet weiterhin den generischen Excel-/CSV-Formatadapter. Ein kleiner Source Extractor verarbeitet sowohl das bisherige Summary-/Detailtabellenpaar als auch die synthetischen `Mock-Defender`-CSV-Dateien `cves.csv` und `device_inventory.csv`. Die Mock-CVE-Datei wiederholt Site und Sensor als ausdruecklich simulierte Fixture-Felder, damit ein Einzelimport die CVE-Ansicht vollstaendig speist; bei einem Mehrdateiimport verbindet das Domain-Modell CVE- und Inventarzeilen weiterhin ausschliesslich ueber die explizite `Device ID`. Fehlende Kontext- oder Beschreibungswerte werden nicht erfunden. Der Extractor erzeugt genau ein Finding-Vorkommen je Device und CVE, aggregiert diese je CVE und bewahrt CVSS Score, Component, Status sowie wiederholte Sensorzeilen als Evidenz. Summary-Werte ohne Detail-CVE bleiben separate Aggregate und werden nicht in Findings umgewandelt. Die fachlichen CVE-, Device- und Site-Projektionen liegen ausserhalb der React-Komponente in einem deterministischen Domain-Modell.
## Darstellung von Textfeldern ## Darstellung von Textfeldern

View File

@ -4,7 +4,8 @@ export const DEFENDER_FOR_IOT_PROFILE_ID = 'defender-for-iot-vulnerabilities';
export type DefenderSeverity = 'critical' | 'high' | 'medium' | 'low'; export type DefenderSeverity = 'critical' | 'high' | 'medium' | 'low';
export type DefenderDetailState = 'complete' | 'aggregate-only' | 'detail-only' | 'mismatch' | 'zero' | 'unavailable'; export type DefenderDetailState = 'complete' | 'aggregate-only' | 'detail-only' | 'mismatch' | 'zero' | 'unavailable';
export type DefenderIdentityMethod = 'mac' | 'ip' | 'name' | 'unresolved'; export type DefenderIdentityMethod = 'device-id' | 'mac' | 'ip' | 'name' | 'unresolved';
export type DefenderTableRole = 'summary' | 'detail' | 'inventory';
export interface DefenderSourceRow { export interface DefenderSourceRow {
tableIndex: number; tableIndex: number;
@ -108,7 +109,9 @@ export interface DefenderVulnerabilityModel {
available: boolean; available: boolean;
summaryTableCount: number; summaryTableCount: number;
detailTableCount: number; detailTableCount: number;
inventoryTableCount: number;
summaryRowCount: number; summaryRowCount: number;
inventoryRowCount: number;
devices: DefenderDevice[]; devices: DefenderDevice[];
cves: DefenderCve[]; cves: DefenderCve[];
occurrences: DefenderOccurrence[]; occurrences: DefenderOccurrence[];
@ -121,6 +124,19 @@ export interface DefenderVulnerabilityModel {
const severities: DefenderSeverity[] = ['critical', 'high', 'medium', 'low']; const severities: DefenderSeverity[] = ['critical', 'high', 'medium', 'low'];
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', 'beschreibung']; const detailHeaders = ['name', 'ip', 'site', 'sensor', 'cve', 'severity', 'beschreibung'];
const mockDetailHeaders = ['deviceid', 'devicename', 'ipaddress', 'cve', 'cvssscore', 'severity', 'component', 'status'];
const inventoryHeaders = ['deviceid', 'name', 'ipv4address', 'macaddress'];
const canonicalObservationHeaders = new Set(['name', 'devicename', 'ip', 'ipaddress', 'ipv4address', 'site', 'sensor', 'cve', 'severity', 'beschreibung', 'description']);
const headerAliases: Record<string, string[]> = {
name: ['name', 'devicename'],
ip: ['ip', 'ipaddress', 'ipv4address'],
deviceid: ['deviceid'],
site: ['site'],
sensor: ['sensor'],
cve: ['cve'],
severity: ['severity'],
beschreibung: ['beschreibung', 'description']
};
function text(value: JsonValue | undefined): string { function text(value: JsonValue | undefined): string {
if (value === null || value === undefined) return ''; if (value === null || value === undefined) return '';
@ -139,7 +155,8 @@ function normalizedText(value: string): string {
function value(row: DefenderSourceRow, header: string): JsonValue | undefined { function value(row: DefenderSourceRow, header: string): JsonValue | undefined {
const target = normalizedHeader(header); const target = normalizedHeader(header);
const match = Object.keys(row.fields).find(key => normalizedHeader(key) === target); const aliases = headerAliases[target] ?? [target];
const match = Object.keys(row.fields).find(key => aliases.includes(normalizedHeader(key)));
return match === undefined ? undefined : row.fields[match]; return match === undefined ? undefined : row.fields[match];
} }
@ -169,10 +186,12 @@ function severity(value: string): DefenderSeverity | 'unknown' {
return severities.find(item => item === normalized) ?? 'unknown'; return severities.find(item => item === normalized) ?? 'unknown';
} }
export function defenderTableRole(headersInput: string[]): 'summary' | 'detail' | undefined { export function defenderTableRole(headersInput: string[]): DefenderTableRole | undefined {
const headers = new Set(headersInput.map(normalizedHeader)); const headers = new Set(headersInput.map(normalizedHeader));
if (detailHeaders.every(header => headers.has(header))) return 'detail'; if (detailHeaders.every(header => headers.has(header))) return 'detail';
if (mockDetailHeaders.every(header => headers.has(header))) return 'detail';
if (summaryHeaders.every(header => headers.has(header))) return 'summary'; if (summaryHeaders.every(header => headers.has(header))) return 'summary';
if (inventoryHeaders.every(header => headers.has(header)) && !headers.has('cve')) return 'inventory';
return undefined; return undefined;
} }
@ -190,6 +209,8 @@ function identitySeed(row: DefenderSourceRow): IdentitySeed {
const displayName = text(value(row, 'Name')); const displayName = text(value(row, 'Name'));
const ip = text(value(row, 'IP')); const ip = text(value(row, 'IP'));
const site = text(value(row, 'Site')); const site = text(value(row, 'Site'));
const deviceId = text(value(row, 'Device ID'));
if (deviceId) return { method: 'device-id', base: normalizedText(deviceId), site, displayName, ip, provenance: 'source-native Device ID', confidence: 'high' };
const macValue = [displayName, ip].find(isMac); const macValue = [displayName, ip].find(isMac);
if (macValue) return { method: 'mac', base: normalizedText(macValue).replace(/[:-]/g, ''), site, displayName, ip, provenance: macValue === displayName ? 'valid MAC from Name' : 'valid MAC from IP', confidence: 'high' }; if (macValue) return { method: 'mac', base: normalizedText(macValue).replace(/[:-]/g, ''), site, displayName, ip, provenance: macValue === displayName ? 'valid MAC from Name' : 'valid MAC from IP', confidence: 'high' };
const ipValue = [ip, displayName].find(item => isIpv4(item) || isIpv6(item)); const ipValue = [ip, displayName].find(item => isIpv4(item) || isIpv6(item));
@ -247,9 +268,11 @@ export function buildDefenderVulnerabilityModel(tables: DefenderSourceTable[]):
const diagnostics: DefenderDiagnostic[] = []; const diagnostics: DefenderDiagnostic[] = [];
const summaryTables = tables.filter(table => defenderTableRole(table.headers) === 'summary'); const summaryTables = tables.filter(table => defenderTableRole(table.headers) === 'summary');
const detailTables = tables.filter(table => defenderTableRole(table.headers) === 'detail'); const detailTables = tables.filter(table => defenderTableRole(table.headers) === 'detail');
const inventoryTables = tables.filter(table => defenderTableRole(table.headers) === 'inventory');
const summaryRows = summaryTables.flatMap(table => table.rows); const summaryRows = summaryTables.flatMap(table => table.rows);
const detailRows = detailTables.flatMap(table => table.rows); const detailRows = detailTables.flatMap(table => table.rows);
const allRows = [...summaryRows, ...detailRows]; const inventoryRows = inventoryTables.flatMap(table => table.rows);
const allRows = [...summaryRows, ...detailRows, ...inventoryRows];
const identities = identityKeys(allRows, diagnostics); const identities = identityKeys(allRows, diagnostics);
for (const row of summaryRows) { for (const row of summaryRows) {
@ -302,7 +325,9 @@ export function buildDefenderVulnerabilityModel(tables: DefenderSourceTable[]):
const severitySource = text(value(row, 'Severity')); const severitySource = text(value(row, 'Severity'));
const additionalSourceFields: Record<string, JsonValue> = {}; const additionalSourceFields: Record<string, JsonValue> = {};
for (const [label, fieldValue] of Object.entries(row.fields)) { for (const [label, fieldValue] of Object.entries(row.fields)) {
if (normalizedHeader(label) === 'null' && text(fieldValue)) additionalSourceFields[label] = fieldValue; if (!canonicalObservationHeaders.has(normalizedHeader(label)) && (normalizedHeader(label) !== 'null' || text(fieldValue))) {
additionalSourceFields[label] = fieldValue;
}
} }
return [{ return [{
id: `observation-${row.tableIndex}-${row.rowIndex}`, id: `observation-${row.tableIndex}-${row.rowIndex}`,
@ -386,27 +411,35 @@ export function buildDefenderVulnerabilityModel(tables: DefenderSourceTable[]):
const occurrenceByDevice = new Map<string, DefenderOccurrence[]>(); const occurrenceByDevice = new Map<string, DefenderOccurrence[]>();
for (const occurrence of occurrences) occurrenceByDevice.set(occurrence.deviceId, [...(occurrenceByDevice.get(occurrence.deviceId) ?? []), occurrence]); for (const occurrence of occurrences) occurrenceByDevice.set(occurrence.deviceId, [...(occurrenceByDevice.get(occurrence.deviceId) ?? []), occurrence]);
const inventoryByDevice = new Map<string, DefenderSourceRow[]>();
for (const row of inventoryRows) {
const id = identities.get(row)!.id;
inventoryByDevice.set(id, [...(inventoryByDevice.get(id) ?? []), row]);
}
const severitiesWithDetail = new Set(occurrences.map(occurrence => occurrence.severity).filter((item): item is DefenderSeverity => item !== 'unknown')); const severitiesWithDetail = new Set(occurrences.map(occurrence => occurrence.severity).filter((item): item is DefenderSeverity => item !== 'unknown'));
const deviceIds = new Set([...summaryByDevice.keys(), ...occurrenceByDevice.keys()]); const deviceIds = new Set([...summaryByDevice.keys(), ...occurrenceByDevice.keys(), ...inventoryByDevice.keys()]);
const devices: DefenderDevice[] = [...deviceIds].map(id => { const devices: DefenderDevice[] = [...deviceIds].map(id => {
const summaryGroup = summaryByDevice.get(id) ?? []; const summaryGroup = summaryByDevice.get(id) ?? [];
const detailGroup = occurrenceByDevice.get(id) ?? []; const detailGroup = occurrenceByDevice.get(id) ?? [];
const representativeRow = summaryGroup[0] ?? detailRows.find(row => identities.get(row)?.id === id)!; const inventoryGroup = inventoryByDevice.get(id) ?? [];
const deviceDetailRows = detailRows.filter(row => identities.get(row)?.id === id);
const representativeRow = summaryGroup[0] ?? inventoryGroup[0] ?? deviceDetailRows[0];
const identity = identities.get(representativeRow)!; const identity = identities.get(representativeRow)!;
const summaryRow = summaryGroup[0]; const summaryRow = summaryGroup[0];
const detailCounts = emptyCounts(); const detailCounts = emptyCounts();
for (const occurrence of detailGroup) if (occurrence.severity !== 'unknown') detailCounts[occurrence.severity] += 1; for (const occurrence of detailGroup) if (occurrence.severity !== 'unknown') detailCounts[occurrence.severity] += 1;
const summaryCounts = Object.fromEntries(severities.map(item => [item, summaryRow ? numeric(value(summaryRow, item)) : undefined])) as Record<DefenderSeverity, number | undefined>; const summaryCounts = Object.fromEntries(severities.map(item => [item, summaryRow ? numeric(value(summaryRow, item)) : undefined])) as Record<DefenderSeverity, number | undefined>;
const measures = Object.fromEntries(severities.map(item => [item, detailState(summaryCounts[item], detailCounts[item], severitiesWithDetail.has(item))])) as Record<DefenderSeverity, DefenderSeverityMeasure>; const measures = Object.fromEntries(severities.map(item => [item, detailState(summaryCounts[item], detailCounts[item], severitiesWithDetail.has(item))])) as Record<DefenderSeverity, DefenderSeverityMeasure>;
const sourceRows = [...summaryGroup, ...detailRows.filter(row => identities.get(row)?.id === id)]; const sourceRows = [...summaryGroup, ...inventoryGroup, ...deviceDetailRows];
const firstValue = (header: string) => sourceRows.map(row => text(value(row, header))).find(Boolean) ?? '';
return { return {
id, id,
identityMethod: identity.method, identityMethod: identity.method,
identityConfidence: identity.confidence, identityConfidence: identity.confidence,
identityProvenance: identity.provenance, identityProvenance: identity.provenance,
displayName: text(value(representativeRow, 'Name')), displayName: firstValue('Name'),
ip: text(value(representativeRow, 'IP')), ip: firstValue('IP'),
site: text(value(representativeRow, 'Site')), site: firstValue('Site'),
sensors: [...new Set(sourceRows.map(row => text(value(row, 'Sensor'))).filter(Boolean))].sort(), sensors: [...new Set(sourceRows.map(row => text(value(row, 'Sensor'))).filter(Boolean))].sort(),
totalCves: summaryRow ? numeric(value(summaryRow, 'TotalCVEs')) : undefined, totalCves: summaryRow ? numeric(value(summaryRow, 'TotalCVEs')) : undefined,
severity: measures, severity: measures,
@ -454,10 +487,12 @@ export function buildDefenderVulnerabilityModel(tables: DefenderSourceTable[]):
}).sort((left, right) => left.name.localeCompare(right.name)); }).sort((left, right) => left.name.localeCompare(right.name));
return { return {
available: summaryTables.length > 0 || detailTables.length > 0, available: summaryTables.length > 0 || detailTables.length > 0 || inventoryTables.length > 0,
summaryTableCount: summaryTables.length, summaryTableCount: summaryTables.length,
detailTableCount: detailTables.length, detailTableCount: detailTables.length,
inventoryTableCount: inventoryTables.length,
summaryRowCount: summaryRows.length, summaryRowCount: summaryRows.length,
inventoryRowCount: inventoryRows.length,
devices, devices,
cves, cves,
occurrences, occurrences,

View File

@ -25,7 +25,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: false }, { 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: '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: false },
{ profileId: 'defender-for-iot-vulnerabilities', profileVersion: '1.2.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: ['recognition', 'extraction', 'finding-identity', 'evidence'], 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 },

View File

@ -67,7 +67,7 @@ export function DefenderVulnerabilityPanel({ dataset, navigation }: { dataset: I
<article><span>Raw sensor observations</span><strong><Count value={model.observations.length} /></strong></article> <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> <article><span>Aggregate-only vulnerabilities</span><strong><Count value={model.aggregateOnlyCount} /></strong></article>
</section> </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> <p className="defender-context"><span>{(model.summaryRowCount + model.inventoryRowCount).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"> <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>)} {([['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> </nav>

View File

@ -101,13 +101,14 @@ function cveCandidate(
export const defenderForIotSourceExtractor: SourceExtractor = { export const defenderForIotSourceExtractor: SourceExtractor = {
id: 'defender-for-iot-vulnerability-source-extractor', id: 'defender-for-iot-vulnerability-source-extractor',
version: '1.0.0', version: '1.1.0',
detect(document) { detect(document) {
const roles = document.tables.map(table => defenderTableRole(headerFor(table))).filter(Boolean); const roles = document.tables.map(table => defenderTableRole(headerFor(table))).filter(Boolean);
if (!roles.length) return { score: 0, evidence: [] }; if (!roles.length) return { score: 0, evidence: [] };
const both = roles.includes('summary') && roles.includes('detail'); const both = roles.includes('summary') && roles.includes('detail');
return { return {
score: both ? 1 : 0.92, score: both ? 1 : 0.92,
toolId: 'defender-for-iot-vulnerabilities',
evidence: [both evidence: [both
? 'A Defender for IoT asset summary and detailed vulnerability table were found.' ? 'A Defender for IoT asset summary and detailed vulnerability table were found.'
: `A Defender for IoT ${roles[0]} table was found.`] : `A Defender for IoT ${roles[0]} table was found.`]

View File

@ -10,18 +10,21 @@ export const defenderForIotDefinition: ToolProfileDefinition = {
targets: [], resultPopulation: 'findings-only' targets: [], resultPopulation: 'findings-only'
}, },
name: 'Microsoft Defender for IoT', name: 'Microsoft Defender for IoT',
version: '1.1.0', version: '1.2.0',
processing: { processing: {
mappingVersion: '1.0.0', normalizationVersion: '1.1.0', presentationVersion: '1.0.0', mappingVersion: '1.0.0', normalizationVersion: '1.1.0', presentationVersion: '1.0.0',
compatibilityKey: 'defender-for-iot-vulnerabilities-v2', changeClass: 'semantic-breaking', compatibilityKey: 'defender-for-iot-vulnerabilities-v2', changeClass: 'semantic-breaking',
reimportRequired: true, affectedDimensions: ['outcome', 'resultType', 'deviation'] reimportRequired: true, affectedDimensions: ['recognition', 'extraction', 'finding-identity', 'evidence']
}, },
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.' },
{ scope: 'header', pattern: /^CVE$/i, contribution: 0.38, message: 'The detail table exposes CVE identifiers.' }, { scope: 'header', pattern: /^CVE$/i, contribution: 0.38, message: 'The detail table exposes CVE identifiers.' },
{ scope: 'header', pattern: /^Sensor$/i, contribution: 0.14, message: 'The source records a reporting sensor.' }, { scope: 'header', pattern: /^Sensor$/i, contribution: 0.14, message: 'The source records a reporting sensor.' },
{ scope: 'header', pattern: /^Beschreibung$/i, contribution: 0.18, message: 'The detail table exposes the Defender CVE description field.' }, { scope: 'header', pattern: /^Beschreibung$/i, contribution: 0.18, message: 'The detail table exposes the Defender CVE description field.' },
{ scope: 'header', pattern: /^Critical$/i, contribution: 0.1, message: 'The asset summary exposes Critical counts.' } { scope: 'header', pattern: /^Critical$/i, contribution: 0.1, message: 'The asset summary exposes Critical counts.' },
{ scope: 'header', pattern: /^Device ID$/i, contribution: 0.24, message: 'The source exposes the Defender device identifier.' },
{ scope: 'header', pattern: /^CVSS Score$/i, contribution: 0.2, message: 'The CVE export exposes a CVSS score.' },
{ scope: 'header', pattern: /^Component$/i, contribution: 0.1, message: 'The CVE export identifies the affected component.' }
], ],
tableRules: { tableRules: {
preferredNames: [ preferredNames: [

View File

@ -43,12 +43,14 @@ 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 legacy = file('defender-old', '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');
const current = file('defender-current', 'defender-for-iot-vulnerabilities', '1.1.0', '1.0.0'); const previous = file('defender-previous', 'defender-for-iot-vulnerabilities', '1.1.0', '1.0.0');
const current = file('defender-current', 'defender-for-iot-vulnerabilities', '1.2.0', '1.0.0');
current.processedWith.normalizationVersion = '1.1.0'; current.processedWith.normalizationVersion = '1.1.0';
current.processedWith.presentationVersion = '1.0.0'; current.processedWith.presentationVersion = '1.0.0';
current.processedWith.compatibilityKey = 'defender-for-iot-vulnerabilities-v2'; current.processedWith.compatibilityKey = 'defender-for-iot-vulnerabilities-v2';
expect(processingReadiness('microsoft-defender-for-iot', legacy.processedWith)).toBe('incompatible'); expect(processingReadiness('microsoft-defender-for-iot', legacy.processedWith)).toBe('incompatible');
expect(processingReadiness('microsoft-defender-for-iot', previous.processedWith)).toBe('reimport-recommended');
expect(processingReadiness('microsoft-defender-for-iot', current.processedWith)).toBe('current'); expect(processingReadiness('microsoft-defender-for-iot', current.processedWith)).toBe('current');
expect(resolvedCompatibilityKey('microsoft-defender-for-iot', current.processedWith)) expect(resolvedCompatibilityKey('microsoft-defender-for-iot', current.processedWith))
.toBe('defender-for-iot-vulnerabilities-v2'); .toBe('defender-for-iot-vulnerabilities-v2');

View File

@ -68,7 +68,7 @@ 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.1.0', 'defender-for-iot-vulnerabilities-v2'), file('run-defender', 'defender-for-iot-vulnerabilities', '1.2.0', 'defender-for-iot-vulnerabilities-v2'),
'microsoft-defender-for-iot' 'microsoft-defender-for-iot'
); );
defender.processedWith.mappingVersion = '1.0.0'; defender.processedWith.mappingVersion = '1.0.0';

View File

@ -8,6 +8,7 @@ import { createCanonicalImportBatch } from '../../src/importers/canonical-import
import { importProfiles } from '../../src/profiles/profile-catalog'; 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 { defenderModelForDataset } from '../../src/features/defender-for-iot/defender-dataset-model';
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'; import { createScannerUnifiedResult } from '../../src/domain/unified-result';
@ -115,6 +116,71 @@ describe('Microsoft Defender for IoT vulnerability model', () => {
expect(detailOnly.devices[0].severity.low.state).toBe('unavailable'); expect(detailOnly.devices[0].severity.low.state).toBe('unavailable');
}); });
it('joins the mock CVE and inventory schemas through the source-native Device ID', () => {
const model = buildDefenderVulnerabilityModel([
table(0, ['Device ID', 'Device Name', 'IP Address', 'CVE', 'CVSS Score', 'Severity', 'Component', 'Status'], [{
'Device ID': 'DEV-OT-001', 'Device Name': 'PLC-LINE1-01', 'IP Address': '192.0.2.21',
CVE: 'CVE-2099-1001', 'CVSS Score': 9.1, Severity: 'Critical', Component: 'Synthetic component', Status: 'Open'
}]),
table(1, ['Name', 'IPv4 Address', 'MAC Address', 'Site', 'Sensor', 'Device ID'], [{
Name: 'PLC-LINE1-01', 'IPv4 Address': '192.0.2.21', 'MAC Address': '02:00:00:00:00:21',
Site: 'Synthetic site', Sensor: 'SENS-01', 'Device ID': 'DEV-OT-001'
}])
]);
expect(model).toMatchObject({ available: true, detailTableCount: 1, inventoryTableCount: 1, inventoryRowCount: 1 });
expect(model.devices).toHaveLength(1);
expect(model.devices[0]).toMatchObject({
id: 'device-id:dev-ot-001', identityMethod: 'device-id', displayName: 'PLC-LINE1-01',
ip: '192.0.2.21', site: 'Synthetic site', sensors: ['SENS-01']
});
expect(model.cves[0]).toMatchObject({ id: 'CVE-2099-1001', deviceIds: ['device-id:dev-ot-001'] });
expect(model.observations[0].additionalSourceFields).toMatchObject({
'Device ID': 'DEV-OT-001', 'CVSS Score': 9.1, Component: 'Synthetic component', Status: 'Open'
});
});
it('imports the synthetic Mock-Defender CVE and inventory CSVs with the Defender profile', async () => {
const bytes = new TextEncoder().encode([
'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',
'DEV-OT-005,RTU-WATER-01,192.0.2.61,Riverbend Plant,SENS-RB-01,CVE-2099-1006,6.4,Medium,Synthetic component,Accepted risk',
'DEV-OT-008,FIREWALL-OT-01,192.0.2.1,Riverbend Plant,SENS-RB-01,CVE-2099-1007,7.8,High,Synthetic component,Patch scheduled'
].join('\n'));
const imported = await importFile(new File([bytes], 'cves.csv', { type: 'text/csv' }), { profiles: importProfiles });
const inventoryBytes = new TextEncoder().encode([
'Name,IPv4 Address,MAC Address,Site,Sensor,Device Id',
'PLC-LINE1-01,192.0.2.21,02:00:00:00:00:21,Riverbend Plant,SENS-RB-01,DEV-OT-001'
].join('\n'));
const inventory = await importFile(new File([inventoryBytes], 'device_inventory.csv', { type: 'text/csv' }), { profiles: importProfiles });
expect(imported.profileId).toBe('defender-for-iot-vulnerabilities');
expect(imported.profileVersion).toBe('1.2.0');
expect(imported.extraction?.extractor).toMatchObject({ id: 'defender-for-iot-vulnerability-source-extractor', version: '1.1.0' });
expect(imported.extraction?.candidates).toHaveLength(5);
const observations = imported.extraction?.candidates[0].fields.find(field => field.label === 'Source Observations')?.originalValue;
expect(observations).toEqual(expect.arrayContaining([expect.objectContaining({
site: 'Riverbend Plant', sensor: 'SENS-RB-01',
additionalSourceFields: expect.objectContaining({ 'Device ID': expect.stringMatching(/^DEV-OT-/), 'CVSS Score': expect.any(String), Component: expect.any(String), Status: expect.any(String) })
})]));
const entry = confirmEntryMappings(previewEntryFromDataset(imported, {
sha256: 'f'.repeat(64), fileName: imported.fileName, sizeBytes: imported.fileSize, lastModified: imported.lastModified
}), importProfiles);
const canonical = createCanonicalImportBatch('defender-mock-cve-context', [entry], '2026-09-14T06:00:00.000Z').model;
const projected = canonicalModelToDataset(canonical);
expect(projected).not.toBeNull();
const model = defenderModelForDataset(projected!);
expect(model.cves).toHaveLength(5);
expect(model.cves.every(cve => cve.sites.join() === 'Riverbend Plant')).toBe(true);
expect(model.cves.every(cve => cve.sensors.join() === 'SENS-RB-01')).toBe(true);
expect(inventory.profileId).toBe('defender-for-iot-vulnerabilities');
expect(inventory.profileVersion).toBe('1.2.0');
expect(inventory.extraction?.candidates).toHaveLength(0);
expect(inventory.sheets[0].analysis.records).toHaveLength(1);
});
it('selects the versioned profile and emits one candidate per CVE with devices aggregated', async () => { it('selects the versioned profile and emits one candidate per CVE with devices aggregated', async () => {
const workbook = XLSX.utils.book_new(); const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet([ XLSX.utils.book_append_sheet(workbook, XLSX.utils.json_to_sheet([
@ -130,8 +196,8 @@ 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.1.0'); expect(imported.profileVersion).toBe('1.2.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.1.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);
expect(imported.extraction?.candidates[0].fields.find(field => field.label === 'Observation Count')?.originalValue).toBe(3); expect(imported.extraction?.candidates[0].fields.find(field => field.label === 'Observation Count')?.originalValue).toBe(3);
@ -152,7 +218,7 @@ 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.1.0', profileVersion: '1.2.0',
normalizationVersion: '1.1.0', normalizationVersion: '1.1.0',
compatibilityKey: 'defender-for-iot-vulnerabilities-v2' compatibilityKey: 'defender-for-iot-vulnerabilities-v2'
} }