Assessor_2.0/docs/RESULT_CONTRACT.md

561 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# RESULT_CONTRACT
```
schema_version : 1.0.0
status : verbindlicher Datenvertrag
zielort : docs/RESULT_CONTRACT.md
gilt_fuer : Purple Knight, PingCastle, Maester, CIS-CAT Pro, GPO-/Baseline-Analyse
konsumenten : ASSESSOR UI, lokales LLM, Exporte, weitere Tools
```
Dieser Vertrag definiert die oberste Klassifikationsschicht und die Graph-Grundlage des Canonical Result Store. Er ist ein versionierter Integrationsvertrag, kein reiner UI-Index. Alle Konsumenten greifen auf dieselbe Semantik zu und müssen nicht wissen, welche native Spalte ein Tool verwendet hat.
Der Vertrag ersetzt IOE als Organisationsprinzip durch `result_type`. IOE bleibt vollständig erhalten, wird aber zu einer abgeleiteten, indizierten Facette. Alle Zusagen des PingCastle-Arbeitspakets (Dokument 2) für Purple Knight, PingCastle und Maester bleiben unverändert gültig.
> **Realisierungshinweis:** Dieser Vertrag wurde additiv auf das bereits bestehende
> ASSESSOR-Datenmodell abgebildet (Pfad A, Integrationsentscheidung 2026-07-23).
> Siehe Abschnitt „Abweichungen der ASSESSOR-Realisierung" am Ende dieses Dokuments
> für die Stellen, an denen die konkrete Umsetzung von diesem Vertrag abweicht, und
> warum. Der fachlich verbindliche Umsetzungsstand ist
> [`docs/CANONICAL_MODEL.md`](CANONICAL_MODEL.md); dieses Dokument bleibt der
> unveränderte externe Vertragstext.
## Grundprinzip: zwei orthogonale Achsen
Der bisherige Konflikt entsteht, weil IOE zwei verschiedene Fragen vermischt hat. Der Vertrag trennt sie strikt.
`result_type` beschreibt die Art der Aussage, also die Quellsemantik. Geschlossenes, versioniertes Enum. Stabil.
`result_state` beschreibt den Ausgang der Aussage, normalisiert und typübergreifend. Dies ist die eine Achse, auf der toolübergreifend sicher gefiltert wird.
Jede weitere Dimension (Severity, IOE, Score) hängt an diesen beiden Achsen, ersetzt sie aber nicht.
## result_type (geschlossenes Enum)
```
exposure_finding Exposure-Scanner. Aussage: Exposure vorhanden oder nicht.
Quellen: Purple Knight, PingCastle RiskRules, Maester Security Tests.
Nur dieser Typ kann per Default zu IOE werden.
compliance_check Benchmark-Pruefung gegen eine Empfehlung. Aussage: konform oder nicht.
Quelle: CIS-CAT (XCCDF).
policy_deviation Setting-Vergleich gegen eine Baseline. Aussage: Sollwert vs. Istwert plus Verdikt.
Quelle: GPO-Backup vs. MS Security Baseline.
informational Nicht-assertive Kontext- oder Inventardaten ohne Pass/Fail-Semantik.
unknown Rueckfallwert, wenn die Quellsemantik nicht klassifizierbar ist.
```
Bewusst kein eigener Typ: `technical_error` ist ein Zustand (`result_state = error`), keine Art von Aussage. Eine CIS-Prüfung, die nicht ausgeführt werden konnte, bleibt `result_type = compliance_check` mit `result_state = error`. Das hält `result_type` stabil.
## result_state (normalisiert, typübergreifend)
```
fail negativer Ausgang (Exposure erkannt / Check nicht bestanden / Setting weicht ab)
pass positiver Ausgang
warning
error Auswertung oder Ausfuehrung fehlgeschlagen
not_applicable
not_evaluated
informational
unknown
```
Dies ist die einzige Achse, auf der ein typübergreifender „alle Fails"-Filter zuverlässig arbeitet. Nativwerte bleiben parallel im typspezifischen Block erhalten und werden nie durch die Normalisierung überschrieben.
## is_ioe (abgeleitete Facette)
`is_ioe` wird nicht aus der Quelle übernommen, sondern per expliziter, versionierter Regel abgeleitet und als materialisierte, indizierte boolesche Spalte gespeichert. Nicht in JSONB.
Ableitungsregeln:
```
exposure_finding + exposure_state = detected -> is_ioe = true (Default)
compliance_check -> is_ioe = false (Default)
policy_deviation -> is_ioe = false (Default)
```
Nur eine explizite, versionierte Elevationsregel darf einen einzelnen Compliance-Check oder eine Policy-Abweichung auf IOE heben. Niemals per Default.
Jede Ableitung trägt `ioe_rule_id`, `ioe_rule_version`, `ioe_derivation_method`.
Äquivalenz zum bisherigen Filter aus Dokument 2: `findingType = ioe AND exposureState = detected` entspricht jetzt `is_ioe = true`. Der Feinfilter über `not_detected`, `not_evaluated`, `execution_error` läuft weiterhin über `exposure_state`, das nun im `exposure`-Block sitzt und nur für `exposure_finding` existiert.
## Quellen-Mapping
| Quelle | Nativwert | result_type | Zustandsfeld (Block) | result_state | is_ioe |
|---|---|---|---|---|---|
| Purple Knight | IOEs found | exposure_finding | exposure_state = detected | fail | true |
| Purple Knight | No IOE found | exposure_finding | exposure_state = not_detected | pass | false |
| PingCastle | RiskRule Points > 0 | exposure_finding | exposure_state = detected | fail | true |
| PingCastle | RiskRule Points = 0 | exposure_finding | exposure_state = not_detected | pass | false |
| PingCastle | nicht auswertbar | exposure_finding | exposure_state = not_evaluated | not_evaluated | false |
| PingCastle | Ausfuehrungsfehler | exposure_finding | exposure_state = execution_error | error | false |
| Maester | Security Test Failed | exposure_finding | exposure_state = detected | fail | true |
| Maester | Passed | exposure_finding | exposure_state = not_detected | pass | false |
| Maester | Diagnose / nicht ausgefuehrt | informational | – | not_evaluated | false |
| CIS-CAT | XCCDF fail | compliance_check | xccdf_result = fail | fail | false |
| CIS-CAT | XCCDF pass | compliance_check | xccdf_result = pass | pass | false |
| CIS-CAT | XCCDF error | compliance_check | xccdf_result = error | error | false |
| CIS-CAT | XCCDF notapplicable | compliance_check | xccdf_result = notapplicable | not_applicable | false |
| CIS-CAT | XCCDF notchecked / notselected | compliance_check | xccdf_result = notchecked | not_evaluated | false |
| CIS-CAT | XCCDF informational | compliance_check | xccdf_result = informational | informational | false |
| GPO vs. Baseline | Abweichung | policy_deviation | verdict = deviation | fail | false |
| GPO vs. Baseline | Uebereinstimmung | policy_deviation | verdict = match | pass | false |
| GPO vs. Baseline | Setting fehlt beim Subjekt | policy_deviation | verdict = missing_on_subject | informational | false |
| GPO vs. Baseline | Setting nur beim Subjekt | policy_deviation | verdict = extra_on_subject | warning | false |
| GPO vs. Baseline | nicht vergleichbar | policy_deviation | verdict = not_comparable | unknown | false |
Vorbehalte: Welche Maester-Tests als Exposure zählen, entscheidet das versionierte Profil (Dokument 2, Abschnitt 8). Ob ein fehlendes Setting als `fail` oder `not_applicable` gilt, ist settingabhängig und wird im GPO-Submodell festgelegt, nicht hier. Die XCCDF-Zustände `notchecked`, `notapplicable` und `error` dürfen nicht kollabiert werden.
## Typspezifische Detailblöcke
Der Nativreichtum jeder Quelle bleibt in einem typspezifischen Block erhalten. Die normalisierten Top-Level-Felder sind der Vertrag; die Blöcke sind die Wahrheit der Quelle.
```
exposure exposure_state, score_original, score_unit, native_severity
compliance xccdf_result, benchmark_id, rule_id, profile_id, remediation
deviation setting_key (kanonisch), baseline_value, subject_value, verdict,
baseline_ref, baseline_version
```
Der `deviation`-Block ist hier nur in seiner Rolle definiert. Sein vollständiges Vergleichs-Submodell inklusive Setting-Identität und Verdikt-Logik ist noch offen (siehe Abschnitt Offene Punkte).
## Kanonische Asset-Schicht: CANONICAL_ASSET
Ein Knoten wird ausschließlich über einen autoritativen, stabilen Schlüssel identifiziert. Anzeigenamen führen niemals zu einer Knotenzusammenführung. Dies ist die „keine Textähnlichkeit"-Regel aus Dokument 2, angewandt auf die Knotenidentität. Ohne diese Regel zerfällt der Graph in Dubletten oder verschmilzt falsch. Beides zerstört jede Blast-Radius-Aussage.
```
CANONICAL_ASSET
────────────────────────────────
asset_id internes Surrogat-PK
asset_key deterministischer Dedup-Schluessel, namespaced
asset_key_kind sid | object_guid | gpo_guid | domain_sid | trust_key | synthetic
asset_type user | group | computer | domain_controller | domain
| ou | gpo | trust | tenant | service_principal | unknown
primary_name Anzeigename, nicht autoritativ
domain
key_confidence 1.0 bei autoritativem Schluessel, < 1.0 bei synthetic
resolution_method wie der Schluessel gewonnen wurde
source_tools welche Tools diese Entitaet gemeldet haben
first_seen_at
last_seen_at
original_fields jsonb
extensions jsonb
```
Schlüsselpriorität, absteigend autoritativ:
```
objectSID Security Principals (user, group, computer, DC) autoritativ
objectGUID alle AD-Objekte, stabil ueber Umbenennung autoritativ
gpo_guid GPOs (der {GUID}-Ordner im Backup, gpreport) autoritativ
domain_sid Domaenenknoten autoritativ
trust_key Trust = (Quell-Domaenen-SID, Ziel-Domaenen-SID) autoritativ
synthetic nur-Name-Meldung ohne SID/GUID NICHT autoritativ
```
Merge-Regel (hart): Zwei Zeilen werden nur zusammengeführt, wenn sie denselben autoritativen Schlüssel teilen. Namensgleichheit löst niemals einen Merge aus.
Konservative Wahl für `synthetic` (in Version 1.0.0 verbindlich): Ein synthetischer Knoten verschmilzt nie automatisch mit einem autoritativen Knoten, auch nicht bei exaktem Namens- und Domänen-Match. Eine Bindung erfolgt ausschließlich über eine explizite, versionierte, protokollierte Rekonziliationsregel. Das erzeugt sichtbare Dubletten, bis eine solche Regel greift; diese Dubletten sind auditierbar und werden einer stillen Fehlverschmelzung vorgezogen. Kein erfundener SID.
## Kantenschicht: ASSET_EDGE
Kanten sind eine eigenständige Entität, getrennt von Findings. Ein Finding ist ein Knoten-Attribut; eine Kante ist eine Beziehung zwischen zwei Knoten. Diese Trennung ist die Voraussetzung dafür, dass ein Attack Path überhaupt berechenbar wird.
```
ASSET_EDGE
────────────────────────────────
edge_id
source_asset_id FK CANONICAL_ASSET
target_asset_id FK CANONICAL_ASSET
edge_type siehe Katalog
direction gerichtet: source -> target
edge_source Tool + Import-Artefakt, das die Kante erzeugt hat
collection_method z. B. pingcastle-html-trust-table, gpo-backup-dacl
collector_version
source_locator
mapping_method
mapping_rule_version
confidence
first_seen_at
last_seen_at
properties jsonb kantentyp-spezifische Attribute
original_fields jsonb
```
`edge_source`, `collection_method` und `collector_version` sind verpflichtend. Sie sind die Grundlage der ehrlichen Blast-Radius-Deklaration: Nur wenn jede Kante ihre Herkunft trägt, ist ableitbar, welche Kantentypen erhoben wurden und welche fehlen.
## Kantentyp-Katalog
Der Katalog ist selbst versioniert und Teil des Schemas. „Nicht erhoben" ist damit wohldefiniert: Katalog minus erhobene Typen.
| edge_type | Quelle im Bestand | Stufe | Status |
|---|---|---|---|
| MemberOf | PingCastle / PK Detailtabellen | A | extrahierbar |
| Trusts | PingCastle Trust-Bericht | A | extrahierbar |
| AllowedToDelegate | PingCastle (Constrained Delegation) | A | extrahierbar |
| AllowedToActOnBehalfOf | PingCastle (RBCD) | A | extrahierbar |
| GpLink | GPO-Backup / gpreport | A | extrahierbar |
| Contains (OU -> Objekt) | GPO-Backup / gpreport | A | teilweise |
| GPO-DACL (WriteDacl, WriteOwner, GenericAll, GenericWrite) | GPO-Backup ACL | A | extrahierbar |
| HasSession | – | B | nicht in den Quellen |
| AdminTo (lokaler Admin) | – | B | nicht in den Quellen |
| CanRDP / CanPSRemote / ExecuteDCOM | – | B | nicht in den Quellen |
| Objekt-DACL (alle Objekttypen) | – | B | nicht in den Quellen |
Die Stufe-B-Zeilen sind die BloodHound-/SharpHound-Kanten, aus denen die meisten realen Pfade entstehen. Sie sind der Grund, warum Stufe A eine Angriffsflächen-Topologie liefert, aber noch keinen vollständigen Attack Path. Ihre Ingestion ist eine eigenständige, noch nicht eingeplante Datenerfassung.
## Finding zu Graph-Rolle
Nicht jedes Finding ist eine Kante. Die Abbildung erfolgt über eine versionierte Regel-Tabelle in genau drei Rollen und wird nie per Import erfunden.
Knoten-Eigenschaft: AS-REP-Roastable (`DONT_REQ_PREAUTH`), Kerberoastable (SPN gesetzt), Unconstrained Delegation, „Passwort läuft nie ab". Das sind Fähigkeiten eines Knotens, keine Beziehungen. Sie landen als Property am `CANONICAL_ASSET`, referenziert auf das erzeugende Finding.
Kante: RBCD erzeugt eine `AllowedToActOnBehalfOf`-Kante, Constrained Delegation eine `AllowedToDelegate`-Kante, eine GPO-ACL-Berechtigung eine `GPO-DACL`-Kante. Diese Findings haben strukturelle Bedeutung im Graphen.
Reines Overlay: Ein CIS-Härtungs-Fail oder eine Policy-Abweichung sitzt auf einem Knoten (etwa einem DC oder GPO), erzeugt aber weder Kante noch angriffsrelevante Eigenschaft. Kontext auf dem Knoten.
Die Zuordnung Finding-Typ zu Graph-Rolle trägt `mapping_rule_version` und bleibt damit prüfbar und austauschbar, analog zur Score-to-Severity-Sperre.
## EDGE_COVERAGE und ehrlicher Blast Radius
Ein Blast Radius über eine unvollständige Kantenmenge unterschätzt die reale Erreichbarkeit. Ihn als vollständig darzustellen wäre ein Prüffehler. Deshalb wird die Kantenabdeckung explizit modelliert.
```
EDGE_COVERAGE
────────────────────────────────
assessment_id / scan_run_id
edge_type
edge_source
collected true | false
collector_version
```
Jede Erreichbarkeits- oder Blast-Radius-Berechnung läuft über einen deklarierten Kantentyp-Filter und liefert das Ergebnis mit zwei Pflichtangaben: `computed_over_edge_types` und `uncollected_edge_types` (Katalog minus erhoben). Jede Graph-Aussage trägt damit sichtbar, worauf sie beruht und was ihr fehlt.
## Deterministische Graph-Operationen
Diese Operationen sind reine Graph-Algorithmen, damit assertionssicher und ohne Kennzeichnungspflicht.
```
Erreichbarkeit erreichbare Knotenmenge von X unter Kantenfilter E
Kuerzester Pfad zwischen Knoten A und B unter E
Transitive Huelle Blast Radius = erreichbare Menge von X unter E
Finding-Overlay welches Finding sitzt auf welchem Knoten
```
Nicht Teil dieser Schicht, weil heuristisch und kennzeichnungspflichtig: Pfad-Ranking, Risikobewertung eines Pfades, Blast Radius als Impact-Score statt roher Erreichbarkeitsmenge, das Verketten von ATT&CK-Techniken zu einer Angriffserzählung, sowie jede Cross-Tool-Knotenidentität, die auf etwas anderem als SID oder GUID beruht.
## Enrichment: ATT&CK und ANSSI
MITRE ATT&CK und ANSSI-Härtungsstufen sind eine Taxonomie der Findings, kein Graph der Umgebung. Als Knoten- und Finding-Annotation sowie als Filter- und Gruppierungsachse sind sie deterministisch und zulässig. Als Pfadgenerator sind sie unzulässig, da das Verketten von Techniken eine generierte Angriffserzählung wäre.
## Was nicht Knoten ist
Registry-Werte, einzelne Policy-Settings und CIS-Regeln sind keine Graph-Knoten. Sie bleiben Inhalt von Findings beziehungsweise des `deviation`-Blocks. Ein Policy-Deviation-Finding kann einen GPO referenzieren; der GPO ist ein Knoten, das Setting darin nicht.
## Invarianten
```
1. result_type ist geschlossen und versioniert. Keine Quelle fuehrt Ad-hoc-Typen ein.
2. result_state wird abgeleitet, nie roh aus einem Quell-String oder Spaltennamen uebernommen.
3. is_ioe wird per expliziter, versionierter Regel abgeleitet; 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. Fehlend ist NULL bzw. unknown, nie erfunden.
7. Tooluebergreifend vergleichbar sind nur result_type, result_state, severity_normalized, is_ioe.
score ist tooluebergreifend nicht vergleichbar und darf typuebergreifend nicht sortiert
oder schwellwertgefiltert werden.
8. Knotenidentitaet nur ueber autoritativen Schluessel; Name fuehrt nie zu Merge.
9. synthetic-Knoten verschmelzen nie automatisch mit autoritativen Knoten.
10. Kanten sind erste Klasse, getrennt von Findings.
11. Jede Kante traegt edge_source, collection_method, collector_version.
12. Erreichbarkeit nur ueber deklarierten Kantentyp-Filter; Ergebnis nennt uncollected_edge_types.
13. Kanten sind gerichtet; Richtung ist semantisch.
14. Finding zu Graph-Rolle nur ueber versionierte Regel, nie per Import erfunden.
```
## Filter-Semantik
```
[ IOE ] primaer: is_ioe = true. Semantisch unveraendert gegenueber Dokument 2.
[ Abweichung ] ASSESSOR-Ergaenzung, nicht Teil des Vertrags: is_ioe OR outcome = warning.
[ result_type ] oberste Facette: Exposure / Compliance / Policy / Informational.
[ result_state ] typuebergreifend: Fail / Pass / Warning / Error / N/A / Not evaluated.
[ Severity ] orthogonal, kategorial, typuebergreifend.
```
Typspezifische Facetten: `exposure_state` nur bei Exposure, `xccdf_result` nur bei Compliance, `verdict` nur bei Policy. Alle Facetten sind kombinierbar.
Beispiele:
```
is_ioe = true
is_ioe = true AND source_tool = pingcastle
is_ioe = true AND severity_normalized = high
result_type = compliance_check AND result_state = fail
result_type = policy_deviation AND verdict = deviation
result_state = fail
```
## Verhältnis zu bestehenden Dokumenten
Additiv. `result_type`, `result_state` und `is_ioe` ergänzen die dokumentierte Trennung von Originalwert und normalisiertem Wert (CANONICAL_MODEL.md). `RESULT_ASSET` referenziert künftig `canonical_asset_id` statt Namen inline zu speichern; dies ist der einzige Eingriff in die bestehende Struktur und zugleich der Punkt, an dem Deduplizierung entsteht. Der gesamte PingCastle-Extraktions- und Verknüpfungspfad aus Dokument 2 bleibt unverändert und speist Knoten und Stufe-A-Kanten aus den bereits extrahierten Detailtabellen.
## Offene Punkte (nicht Teil von 1.0.0)
```
- GPO-/Baseline-Vergleichs-Submodell: Setting-Identitaet, Verdikt-Logik,
Behandlung von "nicht konfiguriert" vs. "abwesend", baseline_ref-Versionierung.
- CIS-CAT-Adapter: Festlegung XCCDF/ARF vs. JSON nach Verifikation der Lizenzausgabe.
- Elevationsregeln, die einzelne Compliance-/Policy-Befunde auf is_ioe = true heben.
- Rekonziliationsregeln fuer synthetic-Knoten (Bindung an autoritative Knoten).
- Stufe B: Ingestion von BloodHound-/SharpHound-Klasse-Kanten (Session, AdminTo, DACL).
- Mandanten-/Isolationsmodell ueber project_id hinaus (Row-Level Security o. ae.).
```
## Changelog
```
1.0.0 Erstfassung. result_type als oberster Diskriminator, result_state normalisiert,
is_ioe als abgeleitete indizierte Facette. Typspezifische Bloecke (exposure,
compliance, deviation). Asset-Schicht (CANONICAL_ASSET) mit autoritativer
Schluesselprioritaet und konservativer Merge-Regel. Kantenschicht (ASSET_EDGE),
Kantentyp-Katalog, EDGE_COVERAGE, deterministische Graph-Operationen.
```
## Abweichungen der ASSESSOR-Realisierung
Dieser Abschnitt ist kein Teil des externen Vertrags, sondern dokumentiert, wo und
warum die konkrete Umsetzung in ASSESSOR von diesem Text abweicht. Grundlage ist
die Integrationsentscheidung „Pfad A" vom 2026-07-23: additive Abbildung auf das
bestehende ASSESSOR-Datenmodell statt einer zweiten, parallelen Resulttabelle.
### Abweichung: eigenes Praedikat neben is_ioe, nicht statt is_ioe
`is_ioe` bleibt exakt wie im Vertrag definiert: `exposure_finding` mit
`exposure_state = detected`, ohne Elevationsregel. Die Arbeitsansichten brauchen
darueber hinaus eine breitere Frage — „verlangt dieses Ergebnis Aufmerksamkeit?" —,
weil ein Werkzeug ein Ergebnis melden kann, das weder bestanden noch fehlgeschlagen
ist. Maester meldet dafuer `Investigate`, ASSESSOR normalisiert das auf
`outcome_normalized = warning`.
ASSESSOR fuehrt dafuer ein eigenes versioniertes Praedikat
(`src/domain/deviation-predicate.ts`, Version 1.0.0):
```
is_deviation := is_ioe OR outcome_normalized = 'warning'
```
Es ist nicht persistiert, sondern wird bei der Projektion abgeleitet; die
PostgreSQL-Vorfilterung bildet dieselbe Formel ab. Es liest ausschliesslich bereits
normalisierte Achsen und gilt damit ohne Aenderung fuer jedes weitere Werkzeug.
Ausdruecklich nicht enthalten sind `pass`, `not_run`, `not_applicable` und
`execution_error`; ein Ausfuehrungsfehler ist ein technisches Problem des Laufs und
hat mit `technical_execution_state` eine eigene Achse.
Offen bleibt, dass `deriveExposureState` fuer `outcome = warning` weiterhin
`unknown` liefert. Ein eigener Zustand (etwa `indeterminate`) wuerde den
Vertragsenum, die Check-Constraint aus Migration 007 und damit eine Migration samt
Versionssprung beruehren; er ist bewusst noch nicht umgesetzt.
### result_state statt eigener Spalte: abgeleitete Projektion
ASSESSOR trennt seit Migration `007_unified_result_read_model.sql` bewusst zwei
Dimensionen, die dieser Vertrag in einer Spalte `result_state` zusammenführt:
- `outcome_normalized` — das fachliche Ergebnis (`fail`, `pass`, `warning`,
`informational`, `execution_error`, `not_run`, `not_applicable`,
`not_evaluated`, `unknown`)
- `technical_execution_state` — ob die Prüfung technisch überhaupt durchlief
(`completed`, `failed`, `not-run`, `unknown`)
Begründung im Umsetzungsplan: „Ein technischer Parser-/Toolfehler ist kein
fachlich fehlgeschlagener Check." Ein `result_state = error` würde diese
Unterscheidung wieder aufheben. ASSESSOR führt deshalb **keine physische
`result_state`-Spalte**. Wo ein Konsument den Vertragswert braucht, wird er per
versionierter Regel aus beiden Bestandsachsen projiziert (`projectResultState` in
`src/domain/result-contract-mapping.ts`, `RESULT_CONTRACT_MAPPING_VERSION`).
Die Bestandsachsen bleiben die persistierte Wahrheit.
### source_tool bleibt offen statt geschlossenes Enum
`tool_id` ist in ASSESSOR ein freier Wert, kein geschlossenes Enum. Grund ist der
generische Fallback (Leitprinzip „unbekannte Quellen müssen importierbar
bleiben"): ein geschlossenes `source_tool`-Enum würde jede noch nicht
katalogisierte Quelle auf `unknown` zwingen und die tatsächliche Tool-Kennung
verlieren. `source_tool` im Sinne dieses Vertrags ist daher eine Projektion über
`tool_id`, kein Speicherformat.
### review_status bleibt zweidimensional
ASSESSOR trennt `reviewStatus` (Workflow-Zustand) und `reviewDisposition`
(Entscheidung bei Abschluss) mit eigener `ReviewWorkflowStatus`-Achse (7 Werte,
inkl. optimistischer Revision je Review). Der einwertige `review_status` dieses
Vertrags wird bei Bedarf daraus abgeleitet, nicht umgekehrt gespeichert.
### Keine eigene canonical_result-Tabelle
`result_type`, `is_ioe`, `ioe_rule_id`, `ioe_rule_version`,
`ioe_derivation_method` sowie `exposure_state` sind additive Spalten auf der
bestehenden Occurrence-Tabelle (`assessor_finding_occurrences`,
Migration `008_result_contract_facets.sql`), nicht Spalten einer neuen
Haupttabelle. `result_compliance` und `result_deviation` wurden nicht als neue
Haupttabellen angelegt. GPO-/Baseline-Vergleiche stammen aus den versionierten
Policy-Evaluation-Entitäten und werden nur im Read-Modell vereinheitlicht.
### Datenschutzgrenze der Result DB
Die Result DB ist eine fail-closed, LLM-faehige Offenlegungsprojektion und kein
Rohdaten- oder Kundenkontextspeicher. Seit Disclosure Policy 2.1.0 duerfen
regulaere kanonische Findings und GPO-Vergleiche keinen personen- oder
kundenidentifizierenden Inhalt transportieren. Bei den explizit bekannten
Scannerprofilen CIS-CAT, Maester, PingCastle und Purple Knight bleiben jedoch
regelbezogene Titel, Beschreibungen und Empfehlungen erhalten, damit die
Result DB fachlich und fuer ein LLM nutzbar bleibt. Vor der Ausgabe ersetzt die
Projektion exakte private Begleitwerte und feste Identifierklassen durch
typisierte Platzhalter und wandelt HTML-Regelprosa in Klartext um. Assets,
Personen-/Reviewfelder, Tenant-/Host-/GPO-/Backupidentitaeten, Locators,
Quellzeitpunkte und interne Artefaktprovenienz bleiben ausschliesslich im
internen Analysepfad. Unbekannte Werkzeuge und generische Quellen fallen
weiterhin auf kontrollierte Bezeichnungen ohne Quellprosa zurueck.
Disclosure Policy 2.5.0 erweitert die freigegebenen PingCastle-Rule-IDs auf den
oeffentlich dokumentierten Regelkatalog. Die Validierung ist
gross-/kleinschreibungsunabhaengig, die Ausgabe behaelt die native Schreibweise;
unbekannte IDs bleiben aus der LLM-faehigen Result DB ausgeschlossen.
Disclosure Policy 2.5.0 zieht fuer GPO-Vergleiche die verbindliche Grenze
zwischen technischer Abweichung und Quellenidentitaet. Titel, technische
Setting-ID, Policy-Pfad, Value Name, Operation, Registry-Typ, konfigurierter
Wert, erwarteter Wert, Comparator-Relation, Status, oeffentliche
Baselinebezeichnung/-version, oeffentliche Beschreibung/Empfehlung, belegte
Diagnosen, Verarbeitungsstaende und die Aussagegrenze des Backupvergleichs
duerfen vollstaendig erscheinen. Kunden-/Projektname, Domainlabel,
kundenspezifischer GPO-Name, GPO-/Backup-GUID, Datei-/Sharepfad, interne
Projekt-/Assessment-/Run-/Import-/Artefaktidentitaet, Source Locator,
Zeitpunkte und Review-/Risikoakzeptanzdaten bleiben ausgeschlossen.
`policyIdentityKey` und interne Baseline-IDs werden nicht ausgegeben; die
technische Setting-ID steht ausschliesslich als kontrollierte `Source Rule ID`.
Automatisch erzeugte `gpo-policy-identity-*`-Hashes sind niemals oeffentliche
Regelidentitaeten.
Wohlbekannte oeffentliche Principals wie `S-1-5-18`, `S-1-5-32-544` und
`BUILTIN\\Administrators` bleiben technisch lesbar. Domain-SIDs verlieren ihre
eindeutigen Domainkomponenten (`S-1-5-21-[DOMAIN]-<RID>`), bekannte
Domainrollen werden als `[DOMAIN]\\<Rolle>` dargestellt und sonstige konkrete
Benutzer/Gruppen erhalten stabile, nur innerhalb des einzelnen Resultats
gueltige Platzhalter `[DOMAIN_PRINCIPAL_n]`. Dieselbe Reduktion gilt rekursiv
fuer Baselinevarianten. Die Originalzuordnung bleibt ausschliesslich in Work
und GPO Analysis erhalten.
Die fachlichen Bewertungsachsen bleiben erhalten: normalisierte Severity und
Outcome, Result-Typ, Abweichung/IOE, kontrollierte GPO-Klassifikation,
Comparator-Relation sowie nicht identifizierende Aggregate und technische
Versionsmetadaten. Damit bleibt die Sicherheitsbewertung fuer ein LLM nutzbar,
ohne Kunden- oder konkrete GPO-Quellenidentitaet weiterzugeben. Die Aussage bezieht sich auf
Identifizierbarkeit; ein Finding beschreibt weiterhin den anonymisierten
Sicherheitszustand des aktiv ausgewaehlten Scopes.
Ergaenzende Scannerbeobachtungen durchlaufen vor der Unified-Result-Projektion
die versionierte, fail-closed Disclosure Policy
`result-db-supplemental-disclosure` 1.0.0. Eine Beobachtung wird nur ausgegeben,
wenn Werkzeug, Property und ein enger Datentyp-/Wertebereich ausdruecklich
freigegeben sind. Titel, Subject-Klasse und Property werden aus der kontrollierten
Regel neu aufgebaut. Subject-Identitaet, Originalwert, Attribute, betroffener
Assetname, Abschnitt und Source Locator werden niemals aus der Beobachtung in
die Result DB uebernommen. Unbekannte Werkzeuge und neue Zusatzfelder bleiben
vollstaendig in der internen Persistenz, sind aber bis zu einer bewussten
Klassifikation nicht Teil der Result DB. Das betrifft nicht ihre regulaeren
kanonischen Findings.
Rohe GPO-Policy-Beobachtungen sind ebenfalls kein Result-DB-Inhalt. Sie bleiben
im expliziten `internal-analysis`-Lesemodus fuer Work und GPO Analysis erhalten;
versioniert evaluierte Soll-/Ist-Vergleiche erreichen die normale Result DB als
`policy_deviation`. Ohne freigegebene Risikoregel bleibt die Severity `unknown`;
die Finding Factory darf nur belegte Ergebnisse anreichern und erzeugt keine
doppelte Result-Zeile. Der JSONL-Export verwendet Schema 2.0.0 und Disclosure
Policy 2.5.0. Er wendet die allgemeine Projektion nochmals an, verlangt fuer
jede Environment-Zeile weiterhin eine Disclosure-Klassifikation und entfernt
zusaetzlich interne Result-, Projekt-, Assessment-, Run-, Snapshot-, Evaluation-
und Artefaktidentitaeten sowie Zeitpunkte. Manifest-Scope und Filter werden nur
als Boolesche beziehungsweise Mengenangaben ausgegeben; Suchtext, Assetfilter
und konkrete Scope-IDs werden nie serialisiert.
Disclosure Policy 2.6.0 (2026-09-28) schliesst eine Luecke zur obigen Grenze:
`reviewStatus`, `reviewDisposition` (einschliesslich akzeptierter Risiken) und
`reviewPriority` gelangten zuvor in Result DB API und JSONL. Die Projektion gibt
jetzt nur noch Felder aus, die in `RESULT_DB_FIELD_DISCLOSURE` ausdruecklich als
`disclose` klassifiziert sind; ein neues Projektionsfeld verhindert die
Kompilierung, bis es klassifiziert ist, und zur Laufzeit hinzugefuegte Felder
fallen weg. Review-Status und -Disposition sind deshalb keine Result-DB-Filter
mehr; Work und die interne Analyse behalten den Review unveraendert. JSONL-Schema
2.1.0 ergaenzt im Manifest `handling` mit der Klassifikation
`confidential-security-posture`: Die Result DB ist anonymisiert, beschreibt aber
weiterhin ausnutzbare Schwaechen der bewerteten Umgebung und ist bei Weitergabe
an externe LLM-Anbieter oder Dritte entsprechend zu behandeln.
Disclosure Policy 3.0.0 und JSONL-Schema 3.0.0 (2026-09-28, verbindliche
Nutzeranforderung, ersetzt die Entscheidung vom 2026-09-25 zum vollstaendigen
Ist-/Sollwert): Die Result DB ist datenschutzkonform und keine Angriffsanleitung.
Sie enthaelt keine Werte, die die bewertete Umgebung wiedererkennbar machen oder
den Angriffsaufwand kalibrieren. Der GPO-Istwert samt Principals, die Anzahl der
GPO-Quellen und konkrete Baseline-Releases entfallen; es bleiben technische
Setting-ID, Sollwert, Comparator-Relation und die Baseline-Familie. Identische
Baseline-Erwartungen werden zusammengefasst. In Scannerprosa ausser CIS-CAT
werden exakte Anzahlen, Datums-, Zeit- und Dauerangaben zu `[count]`, `[date]`,
`[time]` und `[duration]`; oeffentliche technische Kennungen (Versionen, CVE/KB,
Regel- und ATT&CK-IDs, Produktnamen, Ports, Event-IDs, oeffentliche SIDs) bleiben.
CIS-CAT-Prosa ist Benchmarktext und behaelt ihre oeffentlichen Empfehlungswerte.
Die Zusatzbeobachtungs-Policy 2.0.0 laesst nur noch binaere Konfigurationszustaende
zu; Anzahlen, Scores, Funktionsebenen und Versionen bleiben intern. Das JSONL-Manifest
enthaelt statt `generatedAt` nur `generatedOn` (Kalendertag), der Dateiname nur das
Datum. Work und interne Analyse behalten alle Originalwerte.
Disclosure Policy 3.1.0 (2026-09-28, Nutzerkorrektur nach Pruefung des Dummy-Exports)
begrenzt die Schwaerzung auf echte Umgebungswerte: Nummerierte Schritte bleiben; nur das
Empfehlungsfeld behaelt Hersteller-Schwellenwerte (`at least 8 characters`, `more than 180 days`,
`15 or more`), Titel und Beschreibung bleiben vollstaendig geschwaerzt. Gruppenrichtlinien- und
Registry-Pfade sowie dokumentierte Windows-Autoritaeten (`NT SERVICE`, `RESTRICTED SERVICES` u. a.)
gelten nicht als Konten. `[ip]` betrifft nur alleinstehende IPv4-Adressen, nie CIS-Regelnummern,
`Rule`-Verweise oder dokumentierte Bereichsgrenzen. Der Handling-Hinweis nennt jetzt ausdruecklich,
dass oeffentliche Baseline-Erwartungen, Herstellerhinweise und Schwellenwerte erhalten bleiben.
Disclosure Policy 3.2.0 (2026-09-28, Nutzerkorrektur nach Pruefung des gefilterten Dummy-Exports)
erhaelt Inline-Aufzaehlungen (Folge ab 1 mit mindestens zwei Punkten, eine wiederholte oder
uebersprungene Quellnummer toleriert), dokumentierte Attributflags (`adminCount=1`) und im
Empfehlungsfeld Zielwerte (`set to 1`). Links und Hostnamen von Hersteller- und Standard-
dokumentation bleiben, solange sie weder `@`, Tenant-Merkmale (`onmicrosoft`, `sharepoint`),
private Literale noch weitere nicht oeffentliche Hosts enthalten. Tenant-Hosts und alle
uebrigen Hosts bleiben geschwaerzt. Dateinamen, ScubaGear-Kennungen, Graph-Berechtigungen,
Code-Namespaces und -Bezeichner sowie Satzverbindungen ohne Leerzeichen gelten nicht als Domain.
Disclosure Policy 3.5.0 (2026-10-02, Nutzerentscheidung) nimmt Microsoft Defender
for IoT als bekanntes Werkzeug auf. Freigegeben wird nur eine formal gueltige
oeffentliche CVE-ID (`CVE-JJJJ-NNNN…`) als Rule ID und zugleich als Titel; Geraete-,
Site-, Sensor- und Zaehlwerte bleiben wie bisher ausgeschlossen. Das Profil
Defender for IoT Alerts bleibt ein generisches Werkzeug ohne freigegebene Rule ID.
Gespeicherte Result DB (2026-09-28, verbindliche Nutzeranforderung, Migration 024): Die Zeilen werden beim
Erzeugen offengelegt, geprueft und in `assessor_result_db_rows` gespeichert, mit Statussatz
`assessor_result_db_state` (Revision, Policy- und Waechterversion, Zeilen, Ersetzungen, gesperrte Zeilen).
Der Aufbau laeuft nach dem Start und nach jeder gespeicherten Aenderung; Lesezugriffe warten auf einen
aktuellen Stand. Jede Zeile traegt `disclosureTrace` (Feld, Regel, Anzahl, zurueckgehaltene Felder) ohne
Originalwerte. Der Identitaetswaechter 1.3.0 entfernt die eigenen Namen des Clients aus allen Zeilen und
sperrt ein Feld, wenn ein Name in anderer Schreibweise ueberlebt. `GET /api/v1/results/status` liefert den
Pruefstand; die Oberflaeche zeigt ihn und im Zeilendetail das Protokoll mit `Open original in Work`.
### Asset-/Kantenschicht: Struktur ohne Producer
`CANONICAL_ASSET`, `ASSET_EDGE`, `ASSET_PROPERTY` und `EDGE_COVERAGE` wurden
strukturgetreu als `assessor_canonical_assets`, `assessor_asset_edges`,
`assessor_asset_properties` und `assessor_edge_coverage` übernommen
(Migration `009_asset_graph.sql`), mit `text`-Primärschlüsseln statt `uuid`
gemäß Bestandskonvention. In diesem Integrationsschritt existiert noch kein
Producer, kein Merge- und kein Rekonziliationscode — die Tabellen sind bewusst
leer nutzbar angelegt, nicht befüllt.