> **Hinweis:** Dies ist der unveränderte externe Vertragstext „CANONICAL_MODEL > 1.0.0", geliefert zusammen mit [`RESULT_CONTRACT.md`](RESULT_CONTRACT.md) und > validiert gegen ein eigenständiges PostgreSQL-16-DDL. Er beschreibt ein > vollständiges eigenständiges Datenmodell (u. a. eine eigene `canonical_result`- > Haupttabelle, typspezifische Blöcke, Asset-/Kantenschicht) und ist **nicht** > identisch mit dem tatsächlich implementierten ASSESSOR-Modell. > > Der fachlich verbindliche, implementierte Stand ist > [`docs/CANONICAL_MODEL.md`](CANONICAL_MODEL.md) (Schema-Version 1, > `src/domain/canonical-model.ts`). Die Integrationsentscheidung vom 2026-07-23 > (Pfad A, additiv) bildet die Neuerungen dieses Vertrags — `result_type`, > `is_ioe`, die Asset-/Kantenschicht — auf das bestehende Modell ab, statt es zu > ersetzen. Abweichungen und Begründungen stehen in > [`RESULT_CONTRACT.md`](RESULT_CONTRACT.md#abweichungen-der-assessor-realisierung). > > Diese Datei wird nicht mit `docs/CANONICAL_MODEL.md` zusammengeführt und dient > ausschließlich als unveränderte Referenz auf den gelieferten Vertragsstand. --- # CANONICAL_MODEL ``` model_version : 1.0.0 realisiert : RESULT_CONTRACT 1.0.0 status : vollstaendiges logisches Datenmodell, implementierungsreif zielort : docs/CANONICAL_MODEL.md gilt_fuer : Purple Knight, PingCastle, Maester, CIS-CAT Pro, GPO-/Baseline-Analyse konsumenten : ASSESSOR UI, lokales LLM, Exporte, Graph, weitere Tools ``` Dies ist die eine kanonische, filterbare Datenbasis. Sie ist so geschnitten, dass alle toolübergreifenden Filter ohne Join in typspezifische Blöcke oder JSONB funktionieren, und dass Graph- und GPO-Schicht ohne Vertragsänderung darauf aufsetzen können. Das Modell trennt drei Verantwortungen: normalisierte Filterschicht (Haupttabelle), toolspezifische Wahrheit (typspezifische Blöcke plus `original_fields`), und Graph-Grundlage (Asset- und Kantenschicht). Fehlende Information bleibt NULL beziehungsweise `unknown`; sie wird nie erfunden oder aus ungeeigneten Feldern abgeleitet. ## Überblick ``` PROJECT └─ ASSESSMENT └─ SCAN_RUN ├─ IMPORT_ARTIFACT (Quelldatei, SHA-256, Tool, Version) └─ CANONICAL_RESULT (Haupttabelle, normalisierte Filterschicht) ├─ RESULT_EXPOSURE (1:1, nur bei result_type = exposure_finding) ├─ RESULT_COMPLIANCE (1:1, nur bei result_type = compliance_check) ├─ RESULT_DEVIATION (1:1, nur bei result_type = policy_deviation) ├─ RESULT_CATEGORY (1:N) ├─ RESULT_ASSET (1:N, verknuepft mit CANONICAL_ASSET) ├─ RESULT_EVIDENCE (1:N) ├─ RESULT_REMEDIATION_STEP (1:N) ├─ RESULT_REFERENCE (1:N) └─ RESULT_REVIEW (1:1, aktueller Stand) Graph-Schicht (assessmentweit, referenziert Results): CANONICAL_ASSET (deduplizierte Knoten) ├─ ASSET_PROPERTY (finding-abgeleitete Knoten-Eigenschaften) ASSET_EDGE (gerichtete Kanten zwischen Knoten) EDGE_COVERAGE (welche Kantentypen erhoben wurden) ``` Notation in den Feldlisten: `PK` Primärschlüssel, `FK` Fremdschlüssel, `NN` NOT NULL, `IX` indiziert (Filter- oder Join-relevant), `enum X` verweist auf den Enum-Katalog am Ende. ## Scope-Tabellen ``` project ──────────────────────────────── id uuid PK key text NN fachlicher Kundenschluessel name text created_at timestamptz NN assessment ──────────────────────────────── id uuid PK project_id uuid FK NN IX name text period text created_at timestamptz NN scan_run ──────────────────────────────── id uuid PK assessment_id uuid FK NN IX label text run_kind text NN import | evaluation started_at timestamptz completed_at timestamptz created_at timestamptz NN ``` ## Import-Provenienz ``` import_artifact ──────────────────────────────── id uuid PK scan_run_id uuid FK NN IX source_tool enum source_tool NN IX source_tool_version text source_format enum source_format NN html | xml | json | xccdf | arf | gpo_backup | unknown file_name text file_sha256 text NN IX Dublettenerkennung byte_size bigint extractor_id text extractor_version text profile_id text profile_version text imported_at timestamptz NN parser_warnings jsonb Importdiagnostik original_fields jsonb ``` `file_sha256` trägt die Dublettenerkennung. Ein bewusster Reimport erzeugt einen neuen `scan_run`-Kontext; bestehende Results werden nicht überschrieben. ## Haupttabelle: CANONICAL_RESULT Die normalisierte Filterschicht. Jede toolübergreifend vergleichbare Achse ist hier eine reale, indizierte Spalte. Kein Konsument muss in einen typspezifischen Block oder nach JSONB greifen, um zu filtern. ``` canonical_result ────────────────────────────────────────────────────────── Identität id uuid PK model_version text NN project_id uuid FK NN IX assessment_id uuid FK NN IX scan_run_id uuid FK NN IX import_artifact_id uuid FK IX finding_definition_id text IX stabile Regelidentitaet (z. B. RiskId) finding_occurrence_id text IX konkretes Auftreten parent_result_id uuid FK IX optionale Hierarchie Herkunft source_tool enum source_tool NN IX source_tool_version text source_rule_id text IX RiskId / PK-ID / XCCDF rule-id / setting_key source_rule_name text source_locator text HTML-Selektor / XML-Pfad / Dateipfad source_original_status text Nativwert, unveraendert source_original_severity text Nativwert, unveraendert source_original_category text Nativwert, unveraendert source_original_score text Nativwert, unveraendert Klassifikation (Achse 1: Art der Aussage) result_type enum result_type NN IX is_ioe boolean NN IX abgeleitet, materialisiert ioe_rule_id text ioe_rule_version text ioe_derivation_method text Bewertung (Achse 2: Ausgang der Aussage) result_state enum result_state NN IX severity_normalized enum severity NN IX default unknown severity_rank smallint IX fuer Sortierung innerhalb einer Severity-Skala severity_original text severity_mapping_rule_id text severity_mapping_version text score_original numeric score_normalized numeric NICHT tooluebergreifend vergleichbar score_max numeric score_unit text Inhalt title text NN summary text description text rationale text technical_details text impact text risk_statement text remediation_summary text remediation_details text Aggregierte Anzeigehilfen (denormalisiert fuer schnelle Listen) affected_asset_summary text evidence_count integer NN default 0 IX has_detailed_evidence boolean NN default false primary_category_label text IX gespiegelt aus RESULT_CATEGORY (is_primary) primary_category_namespace text IX Quality / Provenance recognition_confidence numeric mapping_confidence numeric classification_confidence numeric severity_confidence numeric identity_confidence numeric evidence_completeness numeric mapping_method text mapping_rule_id text mapping_rule_version text extractor_id text extractor_version text Lifecycle lifecycle_state enum lifecycle_state NN IX default new first_seen_at timestamptz NN last_seen_at timestamptz NN resolved_at timestamptz reoccurred_at timestamptz previous_result_id uuid FK comparison_id uuid Zeit / Erweiterung source_observed_at timestamptz imported_at timestamptz evaluated_at timestamptz created_at timestamptz NN extensions jsonb bekannte, noch nicht kanonisierte Zusatzwerte original_fields jsonb vollstaendige unveraenderte Quellfelder ``` Regel für die denormalisierten Anzeigehilfen (`primary_category_label`, `affected_asset_summary`, `evidence_count`): Sie sind gespiegelte Bequemlichkeitsfelder für schnelle Listenansichten. Die Wahrheit liegt in den Kindtabellen. Die Spiegelung erfolgt deterministisch beim Aufbau des Results, nicht in der UI. ## Typspezifische Blöcke Genau ein Block ist pro Result vorhanden, abhängig von `result_type`. Die Blöcke tragen die native Wahrheit der Quelle; die Haupttabelle trägt die normalisierte Projektion daraus. ``` result_exposure (1:1, nur bei result_type = exposure_finding) ──────────────────────────────── result_id uuid PK FK exposure_state enum exposure_state NN IX detected | not_detected | not_evaluated | execution_error | unknown native_severity text points_original numeric PingCastle weight_original numeric Purple Knight test_result_native text Maester ``` ``` result_compliance (1:1, nur bei result_type = compliance_check) ──────────────────────────────── result_id uuid PK FK xccdf_result enum xccdf_result NN IX pass | fail | error | unknown | notapplicable | notchecked | notselected | informational | fixed benchmark_id text IX benchmark_version text rule_id text IX profile_id text weight numeric check_content_ref text ``` ``` result_deviation (1:1, nur bei result_type = policy_deviation) ──────────────────────────────── result_id uuid PK FK setting_key text NN IX kanonischer Policy-Schluessel, nie Anzeigetext setting_namespace text NN registry | privilege_right | system_access | audit | gptmpl | ... baseline_value text subject_value text verdict enum verdict NN IX match | deviation | missing_on_subject | extra_on_subject | not_comparable baseline_ref text IX Referenz auf das versionierte Baseline-Objekt baseline_version text IX gpo_asset_id uuid FK betroffener GPO-Knoten, falls zutreffend ``` Der `result_deviation`-Block ist strukturell vollständig definiert, aber seine Vergleichs- und Verdikt-Logik (Setting-Identität pro Namespace, Behandlung von „nicht konfiguriert" versus „abwesend", Baseline-Registry) ist Gegenstand des noch offenen GPO-Submodells. Bis dahin bleibt der Block ungenutzt; das Modell ist bereits dafür ausgelegt. ## Kindtabellen ``` result_category (1:N) ──────────────────────────────── id uuid PK result_id uuid FK NN IX namespace text NN IX pingcastle | purple-knight | maester | cis | anssi | mitre | ... label text NN IX path text optionaler Hierarchiepfad is_primary boolean NN default false ``` Kategorien sind mehrwertig und namespaced. Eine Kategorie darf nie in eine Severity umgedeutet werden. `is_primary` markiert die Leitkategorie, die in `canonical_result.primary_category_label` gespiegelt wird. ``` result_asset (1:N) Verknuepfung Result zu Knoten ──────────────────────────────── id uuid PK result_id uuid FK NN IX canonical_asset_id uuid FK NN IX dedupliziert, siehe Graph-Schicht role text affected | subject | related is_primary boolean NN default false source_locator text ``` Betroffene Objekte werden nie als kommaseparierter String gespeichert, sondern als Verknüpfung auf deduplizierte Knoten. Das ist zugleich die Brücke zur Graph-Schicht. ``` result_evidence (1:N) ──────────────────────────────── id uuid PK result_id uuid FK NN IX evidence_type enum evidence_type NN table | table_row | object | property | text | configuration | command_output | policy_observation | reference | screenshot_reference | unknown title text summary text structured_data jsonb strukturierte Detailzeile, nicht Freitext text_value text source_view_id text source_locator text NN sequence integer NN Reihenfolge bleibt erhalten ``` Detailzeilen dürfen nicht in eine große Textspalte gepresst werden. Jede Zeile behält ihren `source_locator` und ihre `sequence`. ``` result_remediation_step (1:N) ──────────────────────────────── id uuid PK result_id uuid FK NN IX sequence integer NN title text instruction text command text validation text ``` Liefert ein Tool nur Remediation-Freitext, landet dieser in `canonical_result.remediation_details`; strukturierte Schritte bleiben leer und können später ergänzt werden, ohne den Originaltext zu überschreiben. ``` result_reference (1:N) Compliance und externe Referenzen ──────────────────────────────── id uuid PK result_id uuid FK NN IX reference_type text NN framework_control | mitre_attack | cve | cwe | vendor_doc | anssi | ... namespace text IX CIS | enterprise-attack | ... reference_value text NN IX z. B. T1078, 5.2.3 title text url text ``` MITRE-ATT&CK- und ANSSI-Zuordnungen sind hier als Annotation abgelegt. Sie sind eine Taxonomie der Findings, keine Angriffskette. ``` result_review (1:1, aktueller Stand) ──────────────────────────────── result_id uuid PK FK review_status enum review_status NN default open IX review_disposition text review_priority text review_owner text review_due_date date review_comment text accepted_risk_reason text compensating_control text updated_at timestamptz ``` Reviewdaten sind strikt von Importdaten getrennt und verändern keine importierten Werte. ## Graph-Schicht ### CANONICAL_ASSET Deduplizierter Knotenregister. Jede reale Entität erscheint genau einmal. Identität ausschließlich über autoritativen Schlüssel; Anzeigename führt nie zu einem Merge. ``` canonical_asset ──────────────────────────────── id uuid PK assessment_id uuid FK NN IX asset_key text NN IX deterministischer Dedup-Schluessel, namespaced asset_key_kind enum asset_key_kind NN sid | object_guid | gpo_guid | domain_sid | trust_key | synthetic asset_type enum asset_type NN IX user | group | computer | domain_controller | domain | ou | gpo | trust | tenant | service_principal | unknown primary_name text nicht autoritativ domain text IX key_confidence numeric NN 1.0 bei autoritativem Schluessel, < 1.0 bei synthetic resolution_method text source_tools text[] first_seen_at timestamptz NN last_seen_at timestamptz NN original_fields jsonb extensions jsonb UNIQUE (assessment_id, asset_key) ``` Merge-Regel (verbindlich in 1.0.0): Zusammenführung nur bei identischem autoritativem Schlüssel. `synthetic`-Knoten (nur-Name-Meldung ohne SID/GUID) verschmelzen nie automatisch mit autoritativen Knoten, auch nicht bei exaktem Namens- und Domänen-Match. Eine Bindung erfolgt ausschließlich über eine explizite, versionierte, protokollierte Rekonziliationsregel. Sichtbare Dubletten sind einer stillen Fehlverschmelzung vorzuziehen. Kein erfundener SID. ``` asset_property (1:N) finding-abgeleitete Knoten-Eigenschaften ──────────────────────────────── id uuid PK canonical_asset_id uuid FK NN IX property_key text NN IX z. B. kerberoastable, asrep_roastable, unconstrained_delegation property_value text source_result_id uuid FK Finding, aus dem die Eigenschaft stammt mapping_rule_version text ``` Fähigkeiten eines Knotens (Kerberoastable, AS-REP-Roastable, Unconstrained Delegation) sind Eigenschaften, keine Kanten. Die Zuordnung Finding zu Eigenschaft läuft über eine versionierte Regel. ### ASSET_EDGE Kanten sind erste Klasse, getrennt von Findings. Gerichtet, mit Pflicht-Provenienz. ``` asset_edge ──────────────────────────────── id uuid PK assessment_id uuid FK NN IX source_asset_id uuid FK NN IX target_asset_id uuid FK NN IX edge_type enum edge_type NN IX direction text NN gerichtet: source -> target edge_source text NN Tool + Import-Artefakt collection_method text NN collector_version text NN source_locator text mapping_method text mapping_rule_version text confidence numeric first_seen_at timestamptz NN last_seen_at timestamptz NN properties jsonb original_fields jsonb ``` `edge_source`, `collection_method`, `collector_version` sind verpflichtend. Sie ermöglichen die ehrliche Abdeckungsdeklaration. Kantentyp-Katalog (Stufe A aus dem Bestand erhebbar, Stufe B noch nicht in den Quellen): ``` Stufe A MemberOf, Trusts, AllowedToDelegate, AllowedToActOnBehalfOf, GpLink, Contains, GPO-DACL Stufe B HasSession, AdminTo, CanRDP, CanPSRemote, ExecuteDCOM, Objekt-DACL ``` ### EDGE_COVERAGE Macht ehrlich sichtbar, welche Kantentypen erhoben wurden. Grundlage jeder korrekten Blast-Radius-Aussage. ``` edge_coverage ──────────────────────────────── id uuid PK assessment_id uuid FK NN IX scan_run_id uuid FK IX edge_type enum edge_type NN edge_source text collected boolean NN collector_version text ``` Jede Erreichbarkeits- oder Blast-Radius-Berechnung läuft über einen deklarierten Kantentyp-Filter und liefert `computed_over_edge_types` und `uncollected_edge_types` (Katalog minus erhoben) mit. Ein Blast Radius über eine unvollständige Kantenmenge unterschätzt die reale Erreichbarkeit und darf nie als vollständig dargestellt werden. ## Enum-Katalog ``` result_type exposure_finding | compliance_check | policy_deviation | informational | unknown result_state fail | pass | warning | error | not_applicable | not_evaluated | informational | unknown severity critical | high | medium | low | info | unknown exposure_state detected | not_detected | not_evaluated | execution_error | unknown xccdf_result pass | fail | error | unknown | notapplicable | notchecked | notselected | informational | fixed verdict match | deviation | missing_on_subject | extra_on_subject | not_comparable lifecycle_state new | persistent | resolved | reoccurred | changed | not_comparable | unknown review_status open | in_review | accepted | remediated | risk_accepted | false_positive | unknown asset_type user | group | computer | domain_controller | domain | ou | gpo | trust | tenant | service_principal | unknown asset_key_kind sid | object_guid | gpo_guid | domain_sid | trust_key | synthetic edge_type MemberOf | Trusts | AllowedToDelegate | AllowedToActOnBehalfOf | GpLink | Contains | GPO-DACL | HasSession | AdminTo | CanRDP | CanPSRemote | ExecuteDCOM | ObjectDACL evidence_type table | table_row | object | property | text | configuration | command_output | policy_observation | reference | screenshot_reference | unknown source_tool purple-knight | pingcastle | maester | cis-cat | gpo-analysis | unknown source_format html | xml | json | xccdf | arf | gpo_backup | unknown ``` Enums sind geschlossen und versioniert. Keine Quelle führt Ad-hoc-Werte ein; unklassifizierbares fällt auf `unknown` zurück. ## Filteroberfläche Die folgenden Spalten sind indiziert und bilden die vollständige toolübergreifende Filterebene. Kein Filter erfordert einen Join in einen typspezifischen Block oder JSONB. ``` canonical_result.result_type canonical_result.result_state canonical_result.is_ioe canonical_result.severity_normalized canonical_result.source_tool canonical_result.lifecycle_state canonical_result.primary_category_namespace canonical_result.primary_category_label canonical_result.evidence_count result_category.namespace / label (fuer Mehrfachkategorien) result_review.review_status ``` Typspezifische Verfeinerung über einen bewussten Join: ``` result_exposure.exposure_state nur exposure_finding result_compliance.xccdf_result nur compliance_check result_deviation.verdict nur policy_deviation ``` Beispiel-Abfragen: ``` IOE, tooluebergreifend WHERE is_ioe = true High-IOEs, tooluebergreifend WHERE is_ioe = true AND severity_normalized = 'high' Alle CIS-Haertungsluecken WHERE result_type = 'compliance_check' AND result_state = 'fail' Alle Policy-Abweichungen WHERE result_type = 'policy_deviation' AND verdict = 'deviation' Alle Fails, tooluebergreifend, unabhaengig vom Typ WHERE result_state = 'fail' Nur PingCastle-IOEs WHERE is_ioe = true AND source_tool = 'pingcastle' Findings mit strukturierter Evidenz WHERE evidence_count > 0 ``` ## Determinismus- und Provenance-Regeln ``` 1. result_type ist geschlossen und versioniert. Keine Ad-hoc-Typen. 2. result_state wird abgeleitet, nie roh aus Quell-String oder Spaltenname uebernommen. 3. is_ioe wird per expliziter, versionierter Regel abgeleitet und materialisiert; nie aus der Quelle uebernommen; nie fuer compliance_check oder policy_deviation per Default true. 4. Nativwerte bleiben parallel vollstaendig erhalten (typspezifischer Block plus original_fields). 5. Jede Ableitung traegt mapping_method und mapping_rule_version. 6. Severity nur aus nativem Wert oder versionierter, freigegebener Mappingregel. Kategorie wird nie in Severity umgedeutet. Sonst severity_normalized = unknown. 7. score ist tooluebergreifend nicht vergleichbar und darf typuebergreifend nicht sortiert oder schwellwertgefiltert werden. 8. Fehlend ist NULL bzw. unknown, nie erfunden. 9. Mehrwertige Daten (Kategorien, Assets, Evidenz, Referenzen, Schritte) nie als kommaseparierter String; immer relationale Kindtabelle. 10. Knotenidentitaet nur ueber autoritativen Schluessel; Name fuehrt nie zu Merge. 11. synthetic-Knoten verschmelzen nie automatisch mit autoritativen Knoten. 12. Kanten sind erste Klasse; jede Kante traegt edge_source, collection_method, collector_version. 13. Erreichbarkeit nur ueber deklarierten Kantentyp-Filter; Ergebnis nennt uncollected_edge_types. 14. Finding zu Graph-Rolle (Eigenschaft, Kante, Overlay) nur ueber versionierte Regel. 15. Filterrelevante Werte liegen als indizierte Spalten vor, nie ausschliesslich in JSONB. ``` ## Konsumierbare API-Projektion Konsumenten (UI, LLM, Export) erhalten nicht das rohe Schema, sondern eine versionierte Projektion. Damit bleibt das interne Schema erweiterbar, ohne Konsumenten zu brechen. ``` GET /api/v1/results GET /api/v1/results/{id} GET /api/v1/results/{id}/evidence GET /api/v1/results/schema GET /api/v1/assets GET /api/v1/edges ``` Die UI rendert ausschließlich gelieferte Facetten und Resultwerte. Keine toolabhängige Sonderlogik im Client. ## Bewusst deferiert ``` - GPO-/Baseline-Vergleichs-Submodell: Setting-Identitaet pro Namespace, Verdikt-Logik, "nicht konfiguriert" vs. "abwesend", Baseline-Registry. Der result_deviation-Block ist strukturell bereits vorhanden. - CIS-CAT-Adapter: XCCDF/ARF vs. JSON nach Verifikation der Lizenzausgabe. - Elevationsregeln, die einzelne Compliance-/Policy-Befunde auf is_ioe = true heben. - Rekonziliationsregeln fuer synthetic-Knoten. - Stufe-B-Kanten (Session, AdminTo, DACL) aus BloodHound-/SharpHound-Klasse-Quellen. - Mandanten-/Isolationsmodell ueber project_id hinaus. - Physisches DDL: aus diesem Modell mechanisch ableitbar (Typen, Indizes, Constraints sind bereits annotiert). ``` ## Changelog ``` 1.0.0 Erstfassung des konsolidierten Modells. Vereint das kanonische Result-Modell (Haupttabelle, Kindtabellen, Provenance, Lifecycle) mit RESULT_CONTRACT 1.0.0 (result_type-Achsen, is_ioe als abgeleitete Facette, typspezifische Bloecke) und der Asset-/Edge-Graphschicht. Filteroberfläche als indizierte Spaltenmenge definiert. Implementierungsreif bis auf das deferierte GPO-Submodell. ```