Add onboarding SVG diagrams for data layers, AI workflows, and user journey

This commit is contained in:
Binh 2026-10-07 14:28:16 +02:00
parent 925ce6c276
commit f35b1795a1
27 changed files with 1377 additions and 29 deletions

View File

@ -3404,6 +3404,12 @@ Festgelegtes Betriebsrisiko vom 2026-09-06: Verzeichnisabhaengige Compose-Projek
## 12. Änderungsprotokoll
2026-10-07 - Ingest / PingCastle-HTML-Gesamtstruktur aus Configuration-DN (Nutzerentscheidung Option 2; Core-Freigabe Ingest, Normalization, Core Tests; Produktversion 2.19.1): Der PingCastle-HTML-Bericht enthaelt kein Feld fuer den Gesamtstrukturnamen, nur die Gesamtstruktur-Funktionsebene. Vollstaendige DNs `CN=Configuration,DC=...` stehen nur in PingCastles eingebetteten Graphdaten (`<script type="application/json" data-pingcastle-selector=...>`); im sichtbaren Text ist der DC-Teil abgeschnitten. Extraktor 2.4.0 liest diese Bloecke als inerte Daten (hoechstens 64 Bloecke, je 5 Mio. Zeichen, nur `JSON.parse`, nur Zeichenketten `nodes[].name`), nimmt jeden DN der Form `[CN=...,]CN=Configuration,DC=...` und leitet daraus die Gesamtstruktur-Stammdomaene ab, weil der Configuration-Namenskontext stets unter ihr liegt. `forest.name` entsteht nur, wenn alle Fundstellen dieselbe Stammdomaene nennen; es traegt `derivation: configuration-partition-dn`, die Bezeichnung `Derived from the configuration partition path`, die Fundstellenzahl und den Quellpfad der ersten Fundstelle. Widersprechende Stammdomaenen ergeben die Warnung `pingcastle-forest-root-conflict` und keinen Wert; fehlende, abgeschnittene, nicht-JSON- oder unlesbare Daten ergeben keinen Wert. Nie aus dem Domaenennamen geraten. Profil PingCastle 1.7.0 mit append-only Manifest (`reimportRequired`, `environment-context`). Nachtrag mit Core-Freigabe Presentation: Client Info (Domain Information und Directory) waehlt fuer `Forest` zuerst eine native Beobachtung ohne Ableitung (PingCastle-XML, Purple Knight) und erst sonst die abgeleitete; eine abgeleitete zeigt nach dem Wert gedaempft `(derived from configuration path)` ueber das neue optionale Feld `note` von `DashboardFact` und die Klasse `.client-fact-note` (vorhandene Tokens `--muted`, `--fw-regular`). Synthetische Renderpruefung: nur abgeleitet mit Hinweis, nativ plus abgeleitet nur nativ, ohne Beobachtung Gedankenstrich. Nachtrag mit Core-Freigabe Core Tests: dauerhafter Verhaltenstest in `tests/features/dashboard-layout.test.ts` (nur abgeleitet mit Hinweis im gerenderten `dd`, nativ vor abgeleitet in beiden Reihenfolgen, nicht beobachtete native Werte ignoriert, ohne Beobachtung Gedankenstrich, nativ ohne Hinweisklasse); `npm test` mit 2.131 bestandenen und 18 uebersprungenen Tests. Offen: der Management Report (`src/reporting/client-report-projection.ts`, Report Engine) liest `forest.name` weiter ohne Vorrang und Hinweis; beide Themes nicht live geprueft, weil die laufende Anwendung reale Daten enthaelt. Verifikation: synthetischer Test fuer Uebereinstimmung, Konflikt und Nichtableitung; Dummy-Bericht ergibt `sevenkingdoms.local` aus vier Fundstellen ohne Konflikt; `npm test` 2.129 bestanden, ein lastbedingter Zeitfehler in `datahub-metrics-prefetch` besteht einzeln; beide Produktionsbuilds erfolgreich. Kein Phasenstatus geaendert.
2026-10-07 - Ingest / PingCastle-XML-Domaenenfakten (Core-Freigabe Ingest, Core Tests; danach Ingest, Normalization, Platform; Produktversion 2.19.1): Der XML-Import legte jedes `HealthcheckData`-Element nur unter einer pfadbasierten Eigenschaft `pingcastle.xml.*` ab; Client Info, Data Hub und Report fanden deshalb weder Domaene noch die uebrigen Domaenenfakten (nur das Scoring las `DomainFQDN` ueber einen Leseadapter). Extraktor 2.3.0 ordnet jetzt ausschliesslich die exakten Top-Level-Blaetter `DomainFQDN`, `NetBIOSName`, `ForestFQDN`, `DomainCreation`, `DomainFunctionalLevel`, `ForestFunctionalLevel`, `SchemaVersion`, `IsRecycleBinEnabled`, `NumberOfDC`, `GenerationDate` und `EngineVersion` denselben Eigenschaften wie der HTML-Import zu (`domain.fqdn`, `domain.netbios_name`, `forest.name`, ...). Originalwert und XML-Pfad bleiben erhalten; Funktionsebenen bleiben die numerischen XML-Codes und werden nicht uebersetzt; `DomainSid` und verschachtelte gleichnamige Elemente erhalten keine semantische Eigenschaft. Profil PingCastle 1.6.0 mit neuem, append-only Manifest (`reimportRequired`, Dimension `environment-context`); da Manifeste je Profil gelten, empfiehlt Data Hub den Reimport auch fuer HTML-Importe, die dadurch nichts gewinnen. Synthetischer Test fuer Zuordnung, ausgenommene SID und verschachtelte Elemente; Pruefung gegen den freigegebenen Dummy-Export. Nachtrag mit Core-Freigabe Core Tests: Die Tests, die PingCastle 1.5.0 als aktuelles Profil pinnten, sind auf 1.6.0 angeglichen; 1.5.0 ist als reimportpflichtig abgesichert. `npm test` mit 2.129 bestandenen und 18 uebersprungenen Tests sowie beide Produktionsbuilds erfolgreich. Versionsentscheidung: rueckwaertskompatible Korrektur, faellt in den noch nicht festgeschriebenen Patch 2.19.1. Kein Phasenstatus geaendert.
2026-10-07 - Phase 7 / Engine-Diagramm: Context -> Report als implementiert (Core-Freigabe Presentation, Core Tests, Platform; Produktversion 2.19.1): In `Engines & relationships` war die Beziehung Context Engine -> Report Engine noch als geplant gestrichelt. Sie ist seit 2.13.0 implementiert: Das Core-Backend liefert die versionierte Context-Publikation (`publishContextReport`) ueber `readContext` an den Management Report, ohne dass eine Engine die andere importiert. Die Beziehung ist jetzt durchgezogen mit der Beschriftung `Published` und entsprechender Detailbeschreibung; keine Beziehung ist mehr als geplant markiert. Der bestehende Diagrammtest prueft das. Ergaenzend neues Onboarding-Dokument `docs/ONBOARDING.md` mit statischen Diagrammen unter `docs/images/onboarding/`. Versionsentscheidung: rueckwaertskompatible Darstellungskorrektur, Patch 2.19.0 -> 2.19.1. Kein Phasenstatus geaendert.
2026-10-05 - Phase 13F / AI-Integration in die Compliance Engine verlegt (Nutzerentscheidung; Core-Freigabe Platform, Presentation, Disclosure, Store, Normalization, Core Tests, Governance; per Test verifiziert; in Produktversion 2.19.0 enthalten, keine weitere Versionserhoehung): Die AI-Uebersicht (Dienst `server/compliance/ai-overview-service.ts`, Modell `src/compliance/ai-overview.ts`, Panel und CSS unter `src/compliance/ui/`) und der Privacy-Safe-Export (`src/compliance/privacy-safe-export.ts`, vorher `src/exports/result-db-copy.ts`) gehoeren jetzt zur Compliance Engine. Der Dienst liest ueber `ComplianceReadPort` und speichert ueber `ComplianceStore` (Postgres und lokal); die Tabelle aus Migration 025 behaelt Namen und Daten und gehoert der Engine, ohne Migration oder Schemaaenderung. Das Core-Repository verliert die AI-Methoden, das Feld im Core-Projektexport und die Loeschkaskade; die Engine liefert die erfolgreichen Laeufe neben dem Core-Export und entfernt Laeufe geloeschter Timeslots beim Start und beim Lesen selbst. `server/ai/litellm-client.ts` bleibt Core und hat einen eigenen Tokenzaehler-Typ. Neue Erweiterungspunkte: Client-Info-Tab `AI Overview` und Result-DB-Aktion `Export JSON`, beide nur bei eingeschalteter Engine. `Export JSON` (Schema 2) enthaelt 1:1, was die KI erhaelt: `aiOverviewInput` aus derselben Funktion, die die AI-Uebersicht hasht und sendet (deterministische Reihenfolge), und `isoMappingQueries` mit jeder verschiedenen ISO-Anfrage und ihren Result-DB-Zeilen; die abgerufenen Kandidatenkontrollen haengen vom Index ab und sind nicht enthalten. Tests verschoben nach `tests/compliance/`; neu: 1:1-Export, Engine-Schalter fuer AI-Tab und Export, Bereinigung geloeschter Timeslots, Core-Export ohne AI-Laeufe. Gesamtsuite gruen bis auf einen lastbedingten Timeout von `sheet-selection-cost` (einzeln 0,7 s gruen); beide Builds, Local-only- und Designpruefung bestanden.
2026-10-05 - Phase 13F / GPO-Einstellungen in den Work-ISO-Facetten (Engine-Arbeit, Nutzerentscheidung Option 1 mit Core-Freigabe Governance, per Test verifiziert): Die Engine liest ueber `ComplianceReadPort` zusaetzlich die internen `policy-evaluation`-Zeilen desselben Projekts und Timeslots, ausschliesslich um die von der Result DB zurueckgehaltene technische GPO-Einstellungs-ID zu erhalten. Damit zaehlt eine GPO-Einstellung exakt wie ihr Work-Datensatz (auch bei gleichem Titel verschiedener Einstellungen), und die Work-Facetten `ISO control`/`ISO theme` finden GPO-Einstellungen. Die ID wird nie angezeigt oder exportiert; Mapping und Entscheidungen bleiben je offengelegtem Titel. Die zugehoerige AGENTS.md-Regel wurde am selben Tag mit erneuter Governance-Freigabe nachgetragen.

470
docs/ONBOARDING.md Normal file
View File

@ -0,0 +1,470 @@
# ASSESSOR onboarding guide
A guided tour for new colleagues: what ASSESSOR does, how a scan report travels
through it, how the Core and the engines fit together, and which data you may
use for which kind of feature, including AI workflows.
This guide explains concepts. For setup commands, deployment and configuration
read [`PROJECT_OVERVIEW.md`](PROJECT_OVERVIEW.md). For binding rules read
[`AGENTS.md`](../AGENTS.md). Where this guide and a binding document disagree,
the binding document wins.
**Contents**
1. [What ASSESSOR is](#1-what-assessor-is)
2. [Vocabulary you will hear every day](#2-vocabulary-you-will-hear-every-day)
3. [A typical assessment in the application](#3-a-typical-assessment-in-the-application)
4. [From scan report to stored finding](#4-from-scan-report-to-stored-finding)
5. [GPO backups: a path of their own](#5-gpo-backups-a-path-of-their-own)
6. [Evaluation: verdicts, scores, security domains](#6-evaluation-verdicts-scores-security-domains)
7. [The Core Engine and the optional engines](#7-the-core-engine-and-the-optional-engines)
8. [Which data can be used where](#8-which-data-can-be-used-where)
9. [AI workflows](#9-ai-workflows)
10. [Building a new feature: a decision checklist](#10-building-a-new-feature-a-decision-checklist)
11. [Your first day in the code](#11-your-first-day-in-the-code)
12. [Where to read next](#12-where-to-read-next)
---
## 1. What ASSESSOR is
![ASSESSOR at a glance](images/onboarding/01-big-picture.svg)
Security consultants run several scanners against a client environment, such as
PingCastle, Purple Knight, CIS-CAT, Maester and Defender for IoT, and collect
Group Policy (GPO) backups. Each tool writes its own report format, uses its own
words for "failed" or "critical", and scores differently.
ASSESSOR takes those reports and:
- **imports** them safely, whatever the format (HTML, XML, JSON, CSV, Excel, GPO
backup folders),
- **normalizes** every result into one canonical finding model and keeps the
original values next to the normalized ones,
- **evaluates** them: verdicts, GPO baseline comparison, scores and security
domains,
- **stores** everything in PostgreSQL with full provenance (which file, which
table, which row),
- **presents** the result to the analyst (Work, Finding Reader, Pathfinder) and
to management (Client Info, reports),
- **discloses** a privacy-safe version, the **Result DB**, that may leave the
client context.
Optional **engines** add capabilities on top: context clusters, reports and
regulatory (ISO) mapping with AI. They read the Core's results and never change
them.
What ASSESSOR is **not**: it does not scan anything itself, it does not change
a verdict a scanner reported, and it never claims effective device state from a
GPO backup.
---
## 2. Vocabulary you will hear every day
| Term | Meaning |
|---|---|
| **Client** (code: *project*) | One customer. Created in Data Hub. |
| **Timeslot** (code: *assessment*) | One assessment period of a client. Holds the scan runs of that period. |
| **Scan run** | One confirmed import of one or more report files from one tool. |
| **Finding definition / occurrence** | The definition is the stable identity of a check (tool + rule). An occurrence is that check showing up in one scan run. |
| **Original vs. normalized value** | Every mapped value keeps the source value (`originalValue`) beside ASSESSOR's normalized value. Originals are never overwritten. |
| **Provenance** | Every value knows its import file, source table, row/column or object path. |
| **Scanner profile** | Tool-specific knowledge (field mapping, status/severity vocabulary, native score, first-read fields) in `src/profiles/<tool>/`. |
| **Deviation** | A failed check, shown with the `Deviation` status in Work. The default Work filter. |
| **Verdict** | ASSESSOR's authoritative evaluation of a GPO setting (see section 5). |
| **Review / risk acceptance** | Client-owned work state. Stored separately and kept across reimports. |
| **Result DB** | The stored, privacy-checked projection of all results. The only dataset that may be shared or fed to AI. |
| **Core / Core area** | Everything ASSESSOR does today. It is split into areas such as Ingest or Evaluation. Changing an area needs explicit consent (section 7). |
| **Engine** | An optional capability that reads the Core and never writes to it: Context, Report, Compliance. |
| **Extension point** | A place in a Core view where an engine shows its content, for example the Finding Reader `Context` section. |
---
## 3. A typical assessment in the application
![A typical assessment in the application](images/onboarding/07-user-journey.svg)
| View | What you do there |
|---|---|
| **Data Hub** | Create and select a client, import scan reports and GPO backups, manage timeslots, update ATT&CK/D3FEND catalog references, upload the ISO corpus. Its six donuts summarize the selected client's results. |
| **Data Flow** | The live database schema (tables, columns, relationships) and the interactive engine diagram `Engines & relationships`. Available without a selected client. |
| **Work** | The analyst workbench: the list of all findings with facet filters (status, source, category, security domain, context cluster, ISO control and theme), the **Finding Reader** (first a `Problem` and a `Next step`, then background and evidence), reviews, risk acceptance and CSV/MD/HTML exports of the current filtered set. |
| **Client Info** | The executive picture: overall score, security domains, environment facts, context heatmap, the compliance tile, reports and the AI overview. Almost every tile opens Work with the matching filter. |
| **Pathfinder** | A graph of findings, targets, ATT&CK techniques/tactics, D3FEND defenses and context clusters. `Extract Path` isolates one node and its neighbours. |
| **Result DB** | The privacy-safe, stored dataset of all results: filter, inspect the redaction trace of a row, export JSONL, start ISO mapping batches, `Export JSON` of the AI input. |
The blue **Engines** button in the toolbar switches the optional engines on or
off for you. The Core is always on.
---
## 4. From scan report to stored finding
![From scan report to stored finding](images/onboarding/02-import-pipeline.svg)
An import always has two stages. Nothing is saved until the analyst confirms.
**Preview (in memory).**
1. **Upload.** In Data Hub, inside a client, `Import Data`. Up to 50 files per
batch, 100 MiB per file.
2. **Isolated worker.** Every file is parsed in a separate worker with time and
memory limits. Imported files are untrusted: scripts never run, Excel
formulas stay off, embedded report data is read with a small static grammar
and never evaluated. Limits: [`IMPORT_SECURITY_LIMITS.md`](IMPORT_SECURITY_LIMITS.md).
3. **Format parser.** Turns HTML, XML, JSON, CSV or Excel into one
format-neutral `ImportDocument`: tables, fields and source positions
([`IMPORT_INTERMEDIATE_MODEL.md`](IMPORT_INTERMEDIATE_MODEL.md)).
4. **Recognition.** Decides which tool produced the file, which tables are
relevant and what each column means, with a traceable confidence score. Low
confidence requires the analyst to confirm
([`RECOGNITION_ENGINE.md`](RECOGNITION_ENGINE.md)).
5. **Scanner profile.** Applies the tool's knowledge. Profiles **extend** the
generic pipeline. They never bypass it.
**Commit (one transaction).**
6. **Preview review.** The analyst sees tool, confidence, warnings, unmapped
fields and SHA-256 duplicates, and selects files.
7. **Canonical model.** Project, assessment, scan run, import files, source
tables, finding definitions and occurrences, original fields, mappings,
evidence and diagnostics ([`CANONICAL_MODEL.md`](CANONICAL_MODEL.md)).
8. **Atomic commit.** One database transaction and one repository revision. An
error leaves nothing half-imported.
9. **PostgreSQL.** The system of record: relational keys plus the full
versioned canonical object as JSONB.
10. **Read models.** The unified result projection, the Work sheets and the
materialized Result DB rows are rebuilt and checked.
### Supported sources
| Profile | Tool | Native overall score |
|---|---|---|
| `pingcastle` | PingCastle AD health check | `100 - native risk` |
| `purple-knight` | Purple Knight for AD and for Entra ID (separate scopes) | native posture 0-100 |
| `cis-cat` | CIS-CAT benchmark results | native benchmark score |
| `maester` | Maester (Entra / M365 tests) | none, so the pass rate is used |
| `defender-for-iot`, `defender-for-iot-alerts` | Microsoft Defender for IoT inventory and alerts | declared in the profile |
| `group-policy` | GPO backups | pass rate of configured, comparable settings |
| `generic` | any other CSV/JSON/XML/HTML/Excel table | no native score is inferred |
### Reimport
When a parser or profile changes in a way that affects stored data, the change
gets a new processing-manifest version and affected imports are marked for
reimport ([`REIMPORT_POLICY.md`](REIMPORT_POLICY.md)). A reimport must use
byte-identical files (same SHA-256 set). It replaces scanner-derived records
only. Reviews and accepted risks move to the new records through a unique
scanner/rule/asset identity and are never guessed by title.
---
## 5. GPO backups: a path of their own
![How GPO backups become verdicts](images/onboarding/03-gpo-evaluation.svg)
GPO backups are not scanner reports, so they get their own pipeline in
`server/gpo/` and `src/gpo/`:
1. A backup folder (uploaded as `.zip`) contains `Backup.xml`, `GPReport.xml`,
`Registry.pol`, `GptTmpl.inf` and Group Policy Preferences.
2. An isolated **inventory worker** classifies and parses every artifact.
3. Each instruction is resolved to its **technical setting ID** (registry
value, security template setting, user right, and so on).
4. All backups of the timeslot are collected **per setting and scope**. The
source of every value stays attributed.
5. The settings are compared with every applicable **Microsoft Security
Baseline** release.
The result is exactly **one rolled-up verdict per technical setting**:
- **Deviation (Fail).** At least one comparable, in-scope configured value
fails. This counts in the score.
- **Passed.** Every applicable member value matches. This counts in the score.
- **Review required.** Applicability or precedence is unresolved. ASSESSOR does
not invent a fail.
- **Not configured.** Recommended by the baseline but absent from the analyzed
backups. Score-neutral, and never a remediation obligation by default.
- **No baseline rule.** Configured, but no baseline recommends it.
Score-neutral.
- **Not comparable.** A Group Policy Preference. Preferences have no policy
identity and are never compared with a baseline.
**Conflicts** and **Duplicates** are diagnostic flags shown beside the verdict.
They never replace it. **Not linked** means the backup's own report lists no
enabled link. All of this describes the **backup configuration**, never the
effective state of a device. Details: [`GPO_BASELINE_EVALUATION.md`](GPO_BASELINE_EVALUATION.md).
---
## 6. Evaluation: verdicts, scores, security domains
- **Scanner outcomes are authoritative.** If a scanner says `fail` or
`warning`, ASSESSOR shows a finding and does not question it. Normalization
maps the tool's words to shared statuses and severities and keeps the
original next to them.
- **Scoring is native-score first** ([`SCORING_MODEL.md`](SCORING_MODEL.md)).
If the exact file and scan run carry a valid native overall score, that score
is used. Otherwise the pass rate `passed / (passed + deviations)` is used,
where the result population supports one. The two are never mixed for one
scan. Not run, error and unknown results do not count as passes.
- **Security domains.** Every check gets exactly one of six domains: Identity &
Authentication, Privileged Access & Delegation, AD Infrastructure & PKI,
System Hardening, Cloud & Collaboration, Monitoring & Recovery. One
classifier (`src/evaluation/security-domains.ts`) with per-profile mapping
tables serves Work, Client Info and the reports. It reads structured fields
only, never the title, prose or outcome.
- **Comparison between timeslots** works on the canonical model and does not
change findings ([`COMPARISON_ENGINE.md`](COMPARISON_ENGINE.md),
[`TIMESLOT_COMPARISON_PLAN.md`](TIMESLOT_COMPARISON_PLAN.md)). Progress is
proven by a rescan, never by a self-reported status.
---
## 7. The Core Engine and the optional engines
![Core Engine and optional engines](images/onboarding/04-core-and-engines.svg)
**The Core Engine** is ASSESSOR with all its existing functions. It is
authoritative, owns all Core data and works unchanged with every engine
switched off. It is divided into **Core areas**. The exact path map is in
[`.claude/hooks/core-areas.mjs`](../.claude/hooks/core-areas.mjs):
| Area | Typical paths | Responsibility |
|---|---|---|
| Ingest | `src/importers`, `src/recognition`, `src/profiles`, `src/gpo`, `server/gpo`, import workers | getting data in |
| Normalization | `src/domain` | canonical model, finding identity, status/severity |
| Evaluation | `src/evaluation`, `src/comparison`, `src/scoring` | verdicts, comparison, scores, security domains |
| Store | `src/persistence`, `server/postgres` | repositories, PostgreSQL, migrations |
| Client Records | reviews, risk acceptances, review transfer on reimport | client-owned state |
| Disclosure | `src/domain/result-db-disclosure.ts`, `src/exports` | Result DB policy, exports |
| Reference Catalogs | `src/references`, `server/reference-data` | ATT&CK, D3FEND and other catalogs |
| Shared Kernel | `src/shared` | shared helpers |
| Presentation | `src/app`, `src/features`, `src/components`, `src/styles` | all views |
| Platform | `server/`, `src/api`, build, deploy, `package.json` | backend, API, product version |
| Core Tests | `tests/`, `fixtures/` | acceptance tests |
| Governance | `AGENTS.md`, `CLAUDE.md`, `.claude/hooks` | the rules themselves |
**Core consent.** A change to a Core area needs explicit consent in the chat
message, for example `Core-Freigabe: Evaluation, Store`. It applies to that one
message only. `Core-Freigabe: alle` releases every area except Governance. This
binds every coding agent and every person. For Claude Code a hook enforces it.
Documentation under `docs/` is not gated.
### The engines
| Engine | Code | What it does | Storage |
|---|---|---|---|
| **Context Engine** | `src/assessment-context/`, `src/features/context-layer/` | Gives every deviation a context cluster, in a fixed order of tiers: *catalogued* (catalogue rule, CIS section, declared ATT&CK technique), *recognized* (distinctive terms, technical anchors) or *estimated* (local TF-IDF similarity, category fallback). Deterministic, no AI. Shown as badges, the `Context Cluster` facet, the Finding Reader section and the Client Info heatmap. A context statement says "favours", never "exploitable". | none (in memory) |
| **Report Engine** | `src/reporting/`, `server/reporting/` | Builds the Management Report (German, offline, no LLM, requires published Context results) and the Technical Report from one shared projection. | own file archive, separate read-only DB pool |
| **Compliance Engine** | `src/compliance/`, `server/compliance/` | Maps findings to regulatory controls; ISO/IEC 27001/27002 is the first framework. Owns **all AI integration**: ISO mapping and the AI overview. | own tables in PostgreSQL, Qdrant index |
**Engine rules**:
- Engines read the Core only through a **declared read-only port**. A port is a
`Pick<AssessorRepository, ...>` that contains read methods only, for example
`ComplianceReadPort` in `src/compliance/compliance-store.ts`. They never
import Core internals or touch Core tables.
- Dependencies point one way: an engine depends on the Core, never the reverse.
Engines do not import each other. They meet only through published results,
for example the Context publication that the Management Report consumes.
- An engine never writes Core data and cannot create findings. New scanners,
parsers and importers are always Core changes.
- An engine that needs storage gets its **own** tables after separate approval,
with backup and migration. It never adds columns to Core tables.
- Every engine has a permanent **on/off invariance test**. Work, Client Info,
scores and the Result DB must be identical with the engine on and off.
- `tests/architecture/core-engine-boundary.test.ts` enforces the import
direction.
---
## 8. Which data can be used where
![Which data can be used where](images/onboarding/05-data-layers.svg)
This is the most important section for anyone building a feature. ASSESSOR
holds client security data. Each layer below is derived from the one above it
and carries less:
| Layer | Contains | May be used by | Must never |
|---|---|---|---|
| **1. Original evidence** | Complete scanner originals: source tables, raw fields, host and account names, GPO customer values (*Istwert*), file names | Work, Finding Reader, Results window, internal analysis | leave ASSESSOR, enter the Result DB, reach an AI service |
| **2. Canonical model** | Normalized and original values, provenance, scores, reviews, risk acceptances | Core evaluation; Context and Report Engine through their read-only ports | be written by an engine or sent to an external service |
| **3. Result DB** | Materialized, disclosure-checked rows: public rule ID, tool, outcome/severity, Sollwert, comparison relation, baseline *family*, public scanner guidance | Result DB view, API, JSONL export, Compliance Engine | contain an unclassified field, a client identity or an original value |
| **4. AI input** | The privacy-safe JSON of Result DB rows (`aiOverviewInputJson`) and rule-level check text for ISO queries (`buildRegulatoryFindingQuery`) | the corporate LiteLLM gateway and the embedding service, from the backend only | include assets, per-object values or anything from layers 1-2 |
What the Result DB disclosure policy removes (summary; the binding text is in
`AGENTS.md` under "UI Result DB filter consistency"):
- GPO customer values and every principal in them, GPO source counts and
concrete baseline releases (only the family remains);
- client-owned review, priority and risk-acceptance state;
- per-finding scanner scores, versions and functional levels;
- in scanner prose: exact counts, dates, times and durations become `[count]`,
`[date]`, `[time]` and `[duration]`; standalone IPv4 addresses, non-public
hosts and GUIDs that are not in the public catalog are redacted as well;
- ATT&CK/D3FEND references and Pathfinder relations;
- the client's own names, removed by a per-client identity guard.
Every projection field is classified in `RESULT_DB_FIELD_DISCLOSURE`.
**Unclassified fields never cross.** Each row carries a content-free trace of
what was withheld. The Result DB view shows that trace and never the original.
**Privacy-safe testing.** During development, never read, log, quote or
screenshot real client content: file names, finding titles, field values,
report text. Use synthetic fixtures. Against a real instance, verify only
content-free signals such as HTTP status, counts and the presence of an element.
---
## 9. AI workflows
![AI workflows](images/onboarding/06-ai-workflows.svg)
All AI belongs to the **Compliance Engine**. Both features are opt-in. They run
only in the backend and are **advisory**: they never change a finding, verdict,
score, review or risk acceptance.
### A. AI Overview (Client Info, `AI Overview` tab)
1. All Result DB rows of the selected timeslot are read through
`ComplianceReadPort`.
2. `aiOverviewInputJson` (`src/compliance/privacy-safe-export.ts`) builds the
privacy-safe input in a deterministic order.
3. The backend sends it to the corporate **LiteLLM gateway**: HTTPS only, an
approved model alias, the key only in the server environment, redirects
rejected and no automatic retries.
4. The answer is parsed as inert JSON and validated against the output schema
and length limits. It may reference only rule IDs and facts that are in the
input.
5. The result is rendered as plain text. It is marked stale when its input,
model or prompt changes.
Security boundary: [`AI_OVERVIEW_SECURITY.md`](AI_OVERVIEW_SECURITY.md).
### B. ISO control mapping (Finding Reader `Map ISO controls`, Result DB `Map all ISO controls`)
1. One Result DB row, or a filtered batch.
2. `buildRegulatoryFindingQuery` builds the query from the **rule-level check
text** only: title, description, technical explanation and recommendation,
passed through the privacy boundary. Assets, per-object values and the GPO
customer value are never read.
3. The query is embedded with the approved embedding model.
4. **Qdrant** retrieves candidates from the uploaded ISO 27001/27002 corpus
(top 32 chunks, grouped into 8 controls). The index can always be rebuilt
from the versioned corpus.
5. The **LiteLLM mapper** proposes controls, grounded only in the retrieved
candidates.
6. **Accept / Reject** per control in a window like `Accepted Risk`. As a
provisional rule, the first two proposals are accepted automatically, and an
existing decision is never overridden.
7. Accepted controls appear in the Finding Reader `ISO controls` section, the
Client Info `Compliance` tile and the Work `ISO control` / `ISO theme`
facets. A proposal indicates relevance. It is not proof of compliance.
Details: [`REGULATORY_RAG.md`](REGULATORY_RAG.md).
### Check exactly what the AI sees
Result DB, then **`Export JSON`**, produces a 1:1 copy of what the AI features
receive: the AI overview input built by the same function, plus every distinct
ISO mapping query with the rows that produce it. The retrieved corpus excerpts
are not included because they depend on the index. Use this export whenever
you design or review an AI feature.
### Building a new AI workflow
- It belongs in the **Compliance Engine**, or in a new engine with separate
approval. Never put it in the Core. The shared model client
`server/ai/litellm-client.ts` is Core platform code you may reuse.
- Its input starts from **Result DB rows (layer 3)** and goes through the
privacy-safe export functions. If you need a field that is not there, the
answer is a disclosure-policy discussion with the user, not a shortcut to
layer 1 or 2.
- Its output is advisory, validated as inert data, stored only in engine-owned
tables and rendered as text.
- Its tests use synthetic records and a mocked LiteLLM.
---
## 10. Building a new feature: a decision checklist
1. **Which plan phase?** Read [`IMPLEMENTATION_PLAN.md`](IMPLEMENTATION_PLAN.md)
and name the phase.
2. **Core or engine?** Does the feature change how data is imported, normalized,
evaluated, stored or displayed? Then it is a Core change and needs
`Core-Freigabe: <area>`. Does it add a capability that only reads results?
Then it is engine work through a read-only port.
3. **Which data layer?** Use the lowest layer that is sufficient. Anything that
leaves ASSESSOR or reaches an AI service starts at the Result DB.
4. **Scanner profile work?** Declare `securityScoring`, keep original values and
provenance, add a processing-manifest version and mark affected imports for
reimport.
5. **UI?** Read `.claude/skills/assessor-ui/SKILL.md`. Data Hub, Work and
Pathfinder are the visual references. Text sizes are 14px for information
and 16px for values and prose. Yellow marks selection, blue marks keyboard
focus. No new palettes.
6. **Data flow changed?** Remind the user to update the engine diagram in the
Data Flow view (`src/features/db-structure/EngineDataFlow.tsx`), and update the
diagrams in this guide.
7. **Verify.** Run `npm test` and `npm run build`. Add a synthetic behavioral
test for the exact interaction. Bump the product version in `package.json`
only for a verified release batch.
---
## 11. Your first day in the code
```bash
npm ci
```
```bash
npm run db:up
```
```bash
npm run dev
```
Then open `http://127.0.0.1:5173`. The full setup is in
[`PROJECT_OVERVIEW.md` section 4](PROJECT_OVERVIEW.md#4-local-development).
There is one canonical local database. Never create a second one.
Good files to read first, in this order:
| File | Why |
|---|---|
| `src/domain/` (canonical model types) | the shape of every finding |
| `src/profiles/pingcastle/pingcastle-definition.ts` | what a scanner profile declares |
| `src/recognition/recognition-engine.ts` | how a tool is recognized |
| `src/persistence/assessor-repository.ts` | the persistence contract every read port picks from |
| `src/domain/result-db-disclosure.ts` | what may leave ASSESSOR |
| `src/compliance/compliance-store.ts` | a read-only port example |
| `src/compliance/privacy-safe-export.ts` | exactly what AI receives |
| `src/assessment-context/context-engine.ts` | a deterministic engine |
| `.claude/hooks/core-areas.mjs` | which path belongs to which Core area |
---
## 12. Where to read next
| Topic | Document |
|---|---|
| Setup, deployment, configuration | [`PROJECT_OVERVIEW.md`](PROJECT_OVERVIEW.md), [`DEPLOYMENT.md`](DEPLOYMENT.md), [`POSTGRESQL.md`](POSTGRESQL.md) |
| Binding rules | [`AGENTS.md`](../AGENTS.md), [`SECURITY.md`](../SECURITY.md) |
| Plan and decisions | [`IMPLEMENTATION_PLAN.md`](IMPLEMENTATION_PLAN.md) |
| Import | [`IMPORT_WORKFLOW.md`](IMPORT_WORKFLOW.md), [`TOOL_PROFILES.md`](TOOL_PROFILES.md), [`REIMPORT_POLICY.md`](REIMPORT_POLICY.md) |
| Data model | [`CANONICAL_MODEL.md`](CANONICAL_MODEL.md), [`UNIFIED_RESULT_MODEL.md`](UNIFIED_RESULT_MODEL.md), [`RESULT_CONTRACT.md`](RESULT_CONTRACT.md) |
| Evaluation | [`SCORING_MODEL.md`](SCORING_MODEL.md), [`GPO_BASELINE_EVALUATION.md`](GPO_BASELINE_EVALUATION.md) |
| Reports | [`REPORT_STRATEGY.md`](REPORT_STRATEGY.md) |
| AI and ISO | [`AI_OVERVIEW_SECURITY.md`](AI_OVERVIEW_SECURITY.md), [`REGULATORY_RAG.md`](REGULATORY_RAG.md) |
---
*About the diagrams:* the SVGs in `docs/images/onboarding/` are static
documentation images, current as of product version 2.19.1 (2026-10-07). The
live engine diagram in the application is in the Data Flow view, under
`Engines & relationships`.

View File

@ -358,7 +358,8 @@ Full model: [`SECURITY.md`](../SECURITY.md), [`docs/IMPORT_SECURITY_LIMITS.md`](
## 11. Documentation map
**Start here → this overview.** Then, by topic:
**Start here → this overview.** New to ASSESSOR? Read the illustrated
[`docs/ONBOARDING.md`](ONBOARDING.md) first. Then, by topic:
| Area | Documents |
|------|-----------|

View File

@ -0,0 +1,117 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" font-family="Inter, 'Segoe UI', Arial, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
<marker id="arrow-start" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
</defs>
<rect x="0" y="0" width="1200" height="640" fill="#FAFAFB"/>
<text x="40" y="46" font-size="22" font-weight="600" fill="#1D1D1F" text-anchor="start">ASSESSOR at a glance</text>
<text x="40" y="72" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Scanner reports in, one normalized and evaluated security picture out. Optional engines add capabilities without changing the Core.</text>
<text x="40" y="112" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">1 · Inputs</text>
<rect x="40" y="126" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="54" y="152" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">PingCastle</text>
<text x="54" y="173" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">AD health check (HTML/XML)</text>
<rect x="40" y="192" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="54" y="218" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Purple Knight</text>
<text x="54" y="239" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">AD and Entra ID</text>
<rect x="40" y="258" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="54" y="284" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">CIS-CAT</text>
<text x="54" y="305" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Benchmark results</text>
<rect x="40" y="324" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="54" y="350" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Maester</text>
<text x="54" y="371" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Entra / M365 tests</text>
<rect x="40" y="390" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="54" y="416" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Defender for IoT</text>
<text x="54" y="437" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Inventory and alerts</text>
<rect x="40" y="456" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="54" y="482" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">GPO backups</text>
<text x="54" y="503" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Backup folders as .zip</text>
<rect x="40" y="522" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="54" y="548" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Generic exports</text>
<text x="54" y="569" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">CSV, JSON, XML, HTML, Excel</text>
<path d="M280 154 H300" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M280 220 H300" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M280 286 H300" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M280 352 H300" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M280 418 H300" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M280 484 H300" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M280 550 H300" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M300 154 V550" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M300 300 H358" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="300.48" y="275" width="57.040000000000006" height="20" fill="#FAFAFB"/>
<text x="329" y="290" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="middle">upload</text>
<text x="360" y="112" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">2 · ASSESSOR</text>
<rect x="360" y="126" width="480" height="330" rx="0" fill="#FFFFFF" stroke="#D99A00" stroke-width="2"/>
<text x="380" y="156" font-size="18" font-weight="600" fill="#1D1D1F" text-anchor="start">Core Engine</text>
<text x="820" y="156" font-size="13" font-weight="400" fill="#5F6068" text-anchor="end">always on · authoritative</text>
<rect x="380" y="172" width="440" height="37" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="394" y="196" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Ingest</text>
<text x="520" y="196" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">recognize the tool, parse safely</text>
<rect x="380" y="217" width="440" height="37" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="394" y="241" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Normalize</text>
<text x="520" y="241" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">one canonical finding model</text>
<rect x="380" y="262" width="440" height="37" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="394" y="286" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Evaluate</text>
<text x="520" y="286" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">verdicts, GPO baselines, scores</text>
<rect x="380" y="307" width="440" height="37" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="394" y="331" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Client records</text>
<text x="520" y="331" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">reviews, accepted risks</text>
<rect x="380" y="352" width="440" height="37" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="394" y="376" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Store</text>
<text x="520" y="376" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">PostgreSQL, system of record</text>
<rect x="380" y="397" width="440" height="37" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="394" y="421" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Disclose</text>
<text x="520" y="421" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">privacy-safe Result DB</text>
<path d="M600 456 V494" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="605" y="465" width="119.76" height="20" fill="#FAFAFB"/>
<text x="610" y="480" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="start">read-only port</text>
<rect x="360" y="496" width="150" height="116" rx="0" fill="#FFFFFF" stroke="#2F6FEB" stroke-width="2"/>
<text x="374" y="522" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Context Engine</text>
<text x="374" y="543" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Groups deviations</text>
<text x="374" y="561" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">into</text>
<text x="374" y="579" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">attack-favouring</text>
<text x="374" y="597" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">clusters</text>
<rect x="525" y="496" width="150" height="116" rx="0" fill="#FFFFFF" stroke="#2F6FEB" stroke-width="2"/>
<text x="539" y="522" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Report Engine</text>
<text x="539" y="543" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Management and</text>
<text x="539" y="561" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">technical reports</text>
<rect x="690" y="496" width="150" height="116" rx="0" fill="#FFFFFF" stroke="#2F6FEB" stroke-width="2"/>
<text x="704" y="522" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Compliance</text>
<text x="704" y="541" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Engine</text>
<text x="704" y="562" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">ISO 27001/27002</text>
<text x="704" y="580" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">mapping and AI</text>
<text x="704" y="598" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">overview</text>
<text x="920" y="112" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">3 · Outputs</text>
<rect x="920" y="126" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="934" y="152" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Data Hub</text>
<text x="934" y="173" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">clients, imports, timeslots</text>
<rect x="920" y="192" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="934" y="218" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Work + Finding Reader</text>
<text x="934" y="239" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">filter, read, review</text>
<rect x="920" y="258" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="934" y="284" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Client Info</text>
<text x="934" y="305" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">executive posture</text>
<rect x="920" y="324" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="934" y="350" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Pathfinder</text>
<text x="934" y="371" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">relationship graph</text>
<rect x="920" y="390" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="934" y="416" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Result DB</text>
<text x="934" y="437" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">privacy-safe dataset</text>
<rect x="920" y="456" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="934" y="482" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Reports</text>
<text x="934" y="503" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">management · technical</text>
<rect x="920" y="522" width="240" height="56" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="934" y="548" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Exports</text>
<text x="934" y="569" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">CSV · MD · HTML · JSONL</text>
<path d="M900 154 H918" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M900 220 H918" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M900 286 H918" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M900 352 H918" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M900 418 H918" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M900 484 H918" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M900 550 H918" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M900 154 V550" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M840 290 H900" fill="none" stroke="#4A4A50" stroke-width="2"/>
<text x="870" y="280" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="middle">results</text>
<path d="M840 554 H900" fill="none" stroke="#4A4A50" stroke-width="2"/>
<text x="870" y="544" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="middle">add-ons</text>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,101 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="600" viewBox="0 0 1200 600" font-family="Inter, 'Segoe UI', Arial, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
<marker id="arrow-start" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
</defs>
<rect x="0" y="0" width="1200" height="600" fill="#FAFAFB"/>
<text x="40" y="46" font-size="22" font-weight="600" fill="#1D1D1F" text-anchor="start">From scan report to stored finding</text>
<text x="40" y="72" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Every import is a two-stage transaction: preview first, save only after the analyst confirms.</text>
<text x="40" y="116" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Preview: parsed in memory, nothing saved yet</text>
<rect x="40" y="132" width="200" height="150" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="65" cy="153" r="11" fill="#1D1D1F"/>
<text x="65" y="158" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">1</text>
<text x="84" y="158" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Upload</text>
<text x="54" y="182" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Data Hub › client ›</text>
<text x="54" y="201" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Import Data. Up to 50</text>
<text x="54" y="220" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">files, 100 MiB each.</text>
<rect x="270" y="132" width="200" height="150" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="295" cy="153" r="11" fill="#1D1D1F"/>
<text x="295" y="158" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">2</text>
<text x="314" y="158" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Isolated worker</text>
<text x="284" y="182" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Parser runs with time</text>
<text x="284" y="201" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">and memory limits.</text>
<text x="284" y="220" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Active content never</text>
<text x="284" y="239" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">runs.</text>
<rect x="500" y="132" width="200" height="150" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="525" cy="153" r="11" fill="#1D1D1F"/>
<text x="525" y="158" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">3</text>
<text x="544" y="158" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Format parser</text>
<text x="514" y="182" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">HTML, XML, JSON, CSV,</text>
<text x="514" y="201" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Excel to one</text>
<text x="514" y="220" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">ImportDocument with</text>
<text x="514" y="239" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">source positions.</text>
<rect x="730" y="132" width="200" height="150" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="755" cy="153" r="11" fill="#1D1D1F"/>
<text x="755" y="158" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">4</text>
<text x="774" y="158" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Recognition</text>
<text x="744" y="182" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Detects the tool,</text>
<text x="744" y="201" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">relevant tables and</text>
<text x="744" y="220" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">field roles, with a</text>
<text x="744" y="239" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">confidence score.</text>
<rect x="960" y="132" width="200" height="150" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="985" cy="153" r="11" fill="#1D1D1F"/>
<text x="985" y="158" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">5</text>
<text x="1004" y="158" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Scanner profile</text>
<text x="974" y="182" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Maps fields, status,</text>
<text x="974" y="201" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">severity, first-read</text>
<text x="974" y="220" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">and native score of</text>
<text x="974" y="239" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">that tool.</text>
<path d="M240 207 H268" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M470 207 H498" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M700 207 H728" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M930 207 H958" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M960 282 V342 H140 V378" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="427.4" y="332" width="245.20000000000002" height="20" fill="#FAFAFB"/>
<text x="550" y="347" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="middle">analyst confirms the selection</text>
<text x="40" y="318" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Commit: one atomic repository transaction</text>
<rect x="40" y="380" width="200" height="150" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="65" cy="401" r="11" fill="#1D1D1F"/>
<text x="65" y="406" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">6</text>
<text x="84" y="406" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Preview review</text>
<text x="54" y="430" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Tool, confidence,</text>
<text x="54" y="449" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">warnings, unmapped</text>
<text x="54" y="468" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">fields, SHA-256</text>
<text x="54" y="487" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">duplicates.</text>
<rect x="270" y="380" width="200" height="150" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="295" cy="401" r="11" fill="#1D1D1F"/>
<text x="295" y="406" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">7</text>
<text x="314" y="406" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Canonical model</text>
<text x="284" y="430" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Definitions and</text>
<text x="284" y="449" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">occurrences. Original</text>
<text x="284" y="468" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">and normalized values</text>
<text x="284" y="487" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">side by side.</text>
<rect x="500" y="380" width="200" height="150" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="525" cy="401" r="11" fill="#1D1D1F"/>
<text x="525" y="406" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">8</text>
<text x="544" y="406" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Atomic commit</text>
<text x="514" y="430" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">One DB transaction,</text>
<text x="514" y="449" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">one revision. No</text>
<text x="514" y="468" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">partial imports on</text>
<text x="514" y="487" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">error.</text>
<rect x="730" y="380" width="200" height="150" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="755" cy="401" r="11" fill="#1D1D1F"/>
<text x="755" y="406" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">9</text>
<text x="774" y="406" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">PostgreSQL</text>
<text x="744" y="430" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">System of record. Full</text>
<text x="744" y="449" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">canonical object plus</text>
<text x="744" y="468" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">relational keys.</text>
<rect x="960" y="380" width="200" height="150" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="985" cy="401" r="11" fill="#1D1D1F"/>
<text x="985" y="406" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">10</text>
<text x="1004" y="406" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Read models</text>
<text x="974" y="430" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Unified results, Work</text>
<text x="974" y="449" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">sheets, Result DB rows</text>
<text x="974" y="468" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">rebuilt and checked.</text>
<path d="M240 455 H268" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M470 455 H498" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M700 455 H728" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M930 455 H958" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<text x="40" y="566" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">GPO backups take a parallel path through the GPO inventory worker and the baseline comparison (next diagram). A reimport must be</text>
<text x="40" y="585" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">byte-identical to the original files (SHA-256) and keeps all reviews and accepted risks.</text>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,125 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" font-family="Inter, 'Segoe UI', Arial, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
<marker id="arrow-start" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
</defs>
<rect x="0" y="0" width="1200" height="640" fill="#FAFAFB"/>
<text x="40" y="46" font-size="22" font-weight="600" fill="#1D1D1F" text-anchor="start">How GPO backups become verdicts</text>
<text x="40" y="72" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Configured customer settings are compared with the Microsoft Security Baselines. Every setting gets exactly one verdict.</text>
<rect x="40" y="100" width="200" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="65" cy="121" r="11" fill="#1D1D1F"/>
<text x="65" y="126" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">1</text>
<text x="84" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">GPO backup</text>
<text x="54" y="150" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Backup.xml,</text>
<text x="54" y="169" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">GPReport.xml,</text>
<text x="54" y="188" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Registry.pol,</text>
<text x="54" y="207" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">GptTmpl.inf,</text>
<text x="54" y="226" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Preferences</text>
<rect x="270" y="100" width="200" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="295" cy="121" r="11" fill="#1D1D1F"/>
<text x="295" y="126" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">2</text>
<text x="314" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Inventory worker</text>
<text x="284" y="150" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Classifies and parses</text>
<text x="284" y="169" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">every artifact in an</text>
<text x="284" y="188" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">isolated worker.</text>
<rect x="500" y="100" width="200" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="525" cy="121" r="11" fill="#1D1D1F"/>
<text x="525" y="126" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">3</text>
<text x="544" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Policy identity</text>
<text x="514" y="150" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Resolves each</text>
<text x="514" y="169" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">instruction to its</text>
<text x="514" y="188" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">technical setting ID.</text>
<rect x="730" y="100" width="200" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="755" cy="121" r="11" fill="#1D1D1F"/>
<text x="755" y="126" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">4</text>
<text x="774" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Snapshot per</text>
<text x="774" y="148" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">scope</text>
<text x="744" y="172" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Collects all backups</text>
<text x="744" y="191" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">of the timeslot per</text>
<text x="744" y="210" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">setting, with source.</text>
<rect x="960" y="100" width="200" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="985" cy="121" r="11" fill="#1D1D1F"/>
<text x="985" y="126" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">5</text>
<text x="1004" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Baseline</text>
<text x="1004" y="148" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">comparison</text>
<text x="974" y="172" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Compares with every</text>
<text x="974" y="191" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">applicable Microsoft</text>
<text x="974" y="210" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">baseline release.</text>
<path d="M240 170 H268" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M470 170 H498" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M700 170 H728" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M930 170 H958" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M1040 240 V285" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M127.5 285 H1072.5" fill="none" stroke="#4A4A50" stroke-width="2"/>
<path d="M127.5 285 V328" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M316.5 285 V328" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M505.5 285 V328" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M694.5 285 V328" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M883.5 285 V328" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M1072.5 285 V328" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<text x="590" y="278" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="middle">one rolled-up verdict per technical setting</text>
<rect x="40" y="330" width="175" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="40" y="330" width="4" height="140" fill="#D64545"/>
<text x="56" y="356" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Deviation</text>
<text x="56" y="380" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">A configured value</text>
<text x="56" y="399" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">fails the</text>
<text x="56" y="418" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">baseline. Counts</text>
<text x="56" y="437" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">in the score.</text>
<rect x="229" y="330" width="175" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="229" y="330" width="4" height="140" fill="#2E9E5B"/>
<text x="245" y="356" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Passed</text>
<text x="245" y="380" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Every configured</text>
<text x="245" y="399" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">member value</text>
<text x="245" y="418" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">matches. Counts in</text>
<text x="245" y="437" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">the score.</text>
<rect x="418" y="330" width="175" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="418" y="330" width="4" height="140" fill="#8E8E96"/>
<text x="434" y="356" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Review required</text>
<text x="434" y="380" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Applicability or</text>
<text x="434" y="399" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">precedence</text>
<text x="434" y="418" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">unresolved. Never</text>
<text x="434" y="437" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">guessed as a fail.</text>
<rect x="607" y="330" width="175" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="607" y="330" width="4" height="140" fill="#8E8E96"/>
<text x="623" y="356" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Not configured</text>
<text x="623" y="380" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Recommended, but</text>
<text x="623" y="399" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">absent from the</text>
<text x="623" y="418" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">backups.</text>
<text x="623" y="437" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Score-neutral.</text>
<rect x="796" y="330" width="175" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="796" y="330" width="4" height="140" fill="#8E8E96"/>
<text x="812" y="356" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">No baseline rule</text>
<text x="812" y="380" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Configured, but no</text>
<text x="812" y="399" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">baseline</text>
<text x="812" y="418" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">recommends it.</text>
<text x="812" y="437" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Score-neutral.</text>
<rect x="985" y="330" width="175" height="140" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="985" y="330" width="4" height="140" fill="#8E8E96"/>
<text x="1001" y="356" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Not comparable</text>
<text x="1001" y="380" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Group Policy</text>
<text x="1001" y="399" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Preference, no</text>
<text x="1001" y="418" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">policy identity.</text>
<text x="1001" y="437" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Score-neutral.</text>
<text x="40" y="500" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Diagnostic flags (shown next to the verdict, never replacing it)</text>
<rect x="40" y="516" width="260" height="96" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="40" y="516" width="4" height="96" fill="#E07A1F"/>
<text x="56" y="542" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Conflicts</text>
<text x="56" y="566" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Different backups set the</text>
<text x="56" y="585" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">same setting differently.</text>
<rect x="320" y="516" width="260" height="96" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="320" y="516" width="4" height="96" fill="#E07A1F"/>
<text x="336" y="542" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Duplicates</text>
<text x="336" y="566" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Several backups set the</text>
<text x="336" y="585" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">identical value.</text>
<rect x="600" y="516" width="260" height="96" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="600" y="516" width="4" height="96" fill="#8E8E96"/>
<text x="616" y="542" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Not linked</text>
<text x="616" y="566" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">The backup report lists no</text>
<text x="616" y="585" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">enabled link. No effect.</text>
<rect x="880" y="516" width="280" height="96" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="880" y="516" width="4" height="96" fill="#D99A00"/>
<text x="896" y="542" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">GPO score</text>
<text x="896" y="566" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Passed / (Passed + Deviation) of</text>
<text x="896" y="585" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">configured, comparable in-scope</text>
<text x="896" y="604" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">settings.</text>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,86 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="700" viewBox="0 0 1200 700" font-family="Inter, 'Segoe UI', Arial, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
<marker id="arrow-start" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
</defs>
<rect x="0" y="0" width="1200" height="700" fill="#FAFAFB"/>
<text x="40" y="46" font-size="22" font-weight="600" fill="#1D1D1F" text-anchor="start">Core Engine and optional engines</text>
<text x="40" y="72" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">The Core owns all data and works with every engine switched off. Engines read through a read-only port and never write Core data.</text>
<rect x="40" y="100" width="560" height="540" rx="0" fill="#FFFFFF" stroke="#D99A00" stroke-width="2"/>
<text x="60" y="132" font-size="18" font-weight="600" fill="#1D1D1F" text-anchor="start">Core Engine: the Core areas</text>
<rect x="60" y="150" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="74" y="175" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Ingest</text>
<text x="74" y="197" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">recognition, parsers, profiles, GPO</text>
<rect x="325" y="150" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="339" y="175" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Normalization</text>
<text x="339" y="197" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">canonical model, identity</text>
<rect x="60" y="222" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="74" y="247" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Evaluation</text>
<text x="74" y="269" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">verdicts, comparison, scoring</text>
<rect x="325" y="222" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="339" y="247" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Store</text>
<text x="339" y="269" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">repositories, PostgreSQL</text>
<rect x="60" y="294" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="74" y="319" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Client Records</text>
<text x="74" y="341" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">reviews, risk acceptances</text>
<rect x="325" y="294" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="339" y="319" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Disclosure</text>
<text x="339" y="341" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Result DB policy, exports</text>
<rect x="60" y="366" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="74" y="391" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Reference Catalogs</text>
<text x="74" y="413" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">ATT&amp;CK, D3FEND, baselines</text>
<rect x="325" y="366" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="339" y="391" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Shared Kernel</text>
<text x="339" y="413" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">src/shared helpers</text>
<rect x="60" y="438" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="74" y="463" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Presentation</text>
<text x="74" y="485" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">all views and components</text>
<rect x="325" y="438" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="339" y="463" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Platform</text>
<text x="339" y="485" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">backend, API, build, deploy</text>
<rect x="60" y="510" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="74" y="535" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Core Tests</text>
<text x="74" y="557" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">acceptance tests, fixtures</text>
<rect x="325" y="510" width="255" height="62" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="339" y="535" font-size="15" font-weight="600" fill="#1D1D1F" text-anchor="start">Governance</text>
<text x="339" y="557" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">AGENTS.md, Core guard</text>
<text x="60" y="600" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Changing a Core area needs explicit consent in chat:</text>
<text x="60" y="619" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">"Core-Freigabe: &lt;area&gt;".</text>
<rect x="650" y="300" width="150" height="120" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="664" y="326" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Read-only</text>
<text x="664" y="348" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">port</text>
<text x="664" y="372" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Only read</text>
<text x="664" y="391" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">methods of the</text>
<text x="664" y="410" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">repository</text>
<path d="M600 360 H648" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="603.32" y="335" width="41.36" height="20" fill="#FAFAFB"/>
<text x="624" y="350" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="middle">read</text>
<path d="M800 360 H825 V170 H858" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="734.44" y="265" width="80.56" height="20" fill="#FAFAFB"/>
<text x="815" y="280" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="end">read only</text>
<path d="M800 360 H858" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M825 360 V555 H858" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="860" y="100" width="300" height="140" rx="0" fill="#FFFFFF" stroke="#2F6FEB" stroke-width="2"/>
<text x="874" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Context Engine</text>
<text x="874" y="150" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Assigns every deviation to a</text>
<text x="874" y="169" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">context cluster (catalogued,</text>
<text x="874" y="188" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">recognized or estimated).</text>
<text x="874" y="207" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">In memory, no own storage.</text>
<rect x="860" y="290" width="300" height="140" rx="0" fill="#FFFFFF" stroke="#2F6FEB" stroke-width="2"/>
<text x="874" y="316" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Report Engine</text>
<text x="874" y="340" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Management and technical reports</text>
<text x="874" y="359" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">from one shared projection.</text>
<text x="874" y="378" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Own file archive, separate</text>
<text x="874" y="397" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">read-only DB pool.</text>
<rect x="860" y="480" width="300" height="160" rx="0" fill="#FFFFFF" stroke="#2F6FEB" stroke-width="2"/>
<text x="874" y="506" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Compliance Engine</text>
<text x="874" y="530" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">ISO 27001/27002 control mapping</text>
<text x="874" y="549" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">and AI overview.</text>
<text x="874" y="568" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Own tables (mappings, decisions,</text>
<text x="874" y="587" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">AI runs), Qdrant index, LiteLLM.</text>
<path d="M1010 240 V288" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="1017" y="253" width="143.28" height="20" fill="#FAFAFB"/>
<text x="1022" y="268" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="start">published context</text>
<text x="40" y="672" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Dependencies point one way: an engine depends on the Core, never the reverse. Engines meet only through published results. Each engine has a</text>
<text x="40" y="691" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">permanent on/off invariance test.</text>
</svg>

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,61 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="720" viewBox="0 0 1200 720" font-family="Inter, 'Segoe UI', Arial, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
<marker id="arrow-start" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
</defs>
<rect x="0" y="0" width="1200" height="720" fill="#FAFAFB"/>
<text x="40" y="46" font-size="22" font-weight="600" fill="#1D1D1F" text-anchor="start">Which data can be used where</text>
<text x="40" y="72" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Each layer is derived from the one above and carries less. Use the lowest layer that is sufficient for a feature.</text>
<rect x="40" y="100" width="700" height="112" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<rect x="40" y="100" width="4" height="112" fill="#D64545"/>
<text x="56" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Layer 1 · Original evidence</text>
<text x="56" y="150" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Complete scanner originals: source tables, raw fields, host and account names, GPO</text>
<text x="56" y="169" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">customer values (Istwert), file names.</text>
<rect x="780" y="100" width="380" height="112" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="794" y="126" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Used by</text>
<text x="794" y="147" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Work, Finding Reader, Results window,</text>
<text x="794" y="166" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">internal analysis.</text>
<text x="794" y="185" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Never leaves ASSESSOR. No AI, no Result DB.</text>
<rect x="70" y="252" width="640" height="112" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<rect x="70" y="252" width="4" height="112" fill="#E07A1F"/>
<text x="86" y="278" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Layer 2 · Canonical model</text>
<text x="86" y="302" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Normalized and original values side by side, provenance, scores, reviews and</text>
<text x="86" y="321" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">accepted risks.</text>
<rect x="780" y="252" width="380" height="112" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="794" y="278" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Used by</text>
<text x="794" y="299" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Core evaluation, Context Engine, Report</text>
<text x="794" y="318" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Engine (reports for the client).</text>
<text x="794" y="337" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Read-only for engines. No external services.</text>
<rect x="100" y="404" width="580" height="112" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<rect x="100" y="404" width="4" height="112" fill="#2F6FEB"/>
<text x="116" y="430" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Layer 3 · Result DB</text>
<text x="116" y="454" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Stored, disclosure-checked rows: rule ID, tool, outcome, Sollwert,</text>
<text x="116" y="473" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">public guidance. Counts, dates, identities redacted.</text>
<rect x="780" y="404" width="380" height="112" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="794" y="430" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Used by</text>
<text x="794" y="451" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Result DB view, API, JSONL export, Compliance</text>
<text x="794" y="470" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Engine.</text>
<text x="794" y="489" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Every field classified in</text>
<text x="794" y="508" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">RESULT_DB_FIELD_DISCLOSURE.</text>
<rect x="130" y="556" width="520" height="112" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<rect x="130" y="556" width="4" height="112" fill="#2E9E5B"/>
<text x="146" y="582" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Layer 4 · AI input</text>
<text x="146" y="606" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Privacy-safe JSON of Result DB rows and rule-level check text</text>
<text x="146" y="625" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">for ISO queries.</text>
<rect x="780" y="556" width="380" height="112" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<text x="794" y="582" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Used by</text>
<text x="794" y="603" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Corporate LiteLLM gateway and the embedding</text>
<text x="794" y="622" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">service, backend only.</text>
<text x="794" y="641" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Opt-in. Check it with Result DB › Export</text>
<text x="794" y="660" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">JSON.</text>
<path d="M390 212 V250" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="397" y="221" width="206.00000000000003" height="20" fill="#FAFAFB"/>
<text x="402" y="236" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="start">normalize, keep originals</text>
<path d="M390 364 V402" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="397" y="373" width="292.24" height="20" fill="#FAFAFB"/>
<text x="402" y="388" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="start">disclosure policy and identity guard</text>
<path d="M390 516 V554" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="397" y="525" width="158.96" height="20" fill="#FAFAFB"/>
<text x="402" y="540" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="start">privacy-safe export</text>
<text x="40" y="696" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Testing rule: real client content is never read, logged or screenshotted during development. Use synthetic fixtures.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -0,0 +1,97 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="640" viewBox="0 0 1200 640" font-family="Inter, 'Segoe UI', Arial, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
<marker id="arrow-start" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
</defs>
<rect x="0" y="0" width="1200" height="640" fill="#FAFAFB"/>
<text x="40" y="46" font-size="22" font-weight="600" fill="#1D1D1F" text-anchor="start">AI workflows (Compliance Engine)</text>
<text x="40" y="72" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Both AI features run in the backend only, start from Result DB rows and are advisory: they never change findings, verdicts or scores.</text>
<text x="40" y="116" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">A · AI Overview (Client Info › AI Overview)</text>
<rect x="40" y="132" width="200" height="120" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="54" y="158" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Result DB rows</text>
<text x="54" y="182" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">All rows of the</text>
<text x="54" y="201" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">selected timeslot.</text>
<rect x="270" y="132" width="200" height="120" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="284" y="158" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Privacy-safe input</text>
<text x="284" y="182" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">aiOverviewInputJson in</text>
<text x="284" y="201" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">deterministic order.</text>
<rect x="500" y="132" width="200" height="120" rx="0" fill="#FFFFFF" stroke="#2F6FEB" stroke-width="2"/>
<text x="514" y="158" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">LiteLLM gateway</text>
<text x="514" y="182" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">HTTPS, approved model</text>
<text x="514" y="201" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">alias, no automatic</text>
<text x="514" y="220" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">retries.</text>
<rect x="730" y="132" width="200" height="120" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="744" y="158" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Validate output</text>
<text x="744" y="182" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">JSON schema, known</text>
<text x="744" y="201" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">rule IDs only, length</text>
<text x="744" y="220" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">limits.</text>
<rect x="960" y="132" width="200" height="120" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="974" y="158" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">AI Overview tab</text>
<text x="974" y="182" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Rendered as plain</text>
<text x="974" y="201" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">text. Marked stale</text>
<text x="974" y="220" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">when input changes.</text>
<path d="M240 192 H268" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M470 192 H498" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M700 192 H728" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M930 192 H958" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<text x="40" y="300" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">B · ISO control mapping (Finding Reader › Map ISO controls, Result DB › Map all)</text>
<rect x="40" y="316" width="140" height="170" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="54" y="342" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Result DB row</text>
<text x="54" y="363" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">One finding or</text>
<text x="54" y="381" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">a filtered</text>
<text x="54" y="399" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">batch.</text>
<rect x="202" y="316" width="140" height="170" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="216" y="342" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Query builder</text>
<text x="216" y="363" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Rule-level</text>
<text x="216" y="381" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">check text. No</text>
<text x="216" y="399" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">assets or</text>
<text x="216" y="417" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">values.</text>
<rect x="364" y="316" width="140" height="170" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="378" y="342" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Embedding</text>
<text x="378" y="363" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Approved</text>
<text x="378" y="381" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">embedding</text>
<text x="378" y="399" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">model.</text>
<rect x="526" y="316" width="140" height="170" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="540" y="342" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Qdrant index</text>
<text x="540" y="363" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">ISO 27001/27002</text>
<text x="540" y="381" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">corpus. Top 32</text>
<text x="540" y="399" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">chunks to 8</text>
<text x="540" y="417" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">controls.</text>
<rect x="688" y="316" width="140" height="170" rx="0" fill="#FFFFFF" stroke="#2F6FEB" stroke-width="2"/>
<text x="702" y="342" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">LiteLLM mapper</text>
<text x="702" y="363" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Grounded</text>
<text x="702" y="381" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">proposals from</text>
<text x="702" y="399" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">retrieved</text>
<text x="702" y="417" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">controls only.</text>
<rect x="850" y="316" width="140" height="170" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="864" y="342" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Accept /</text>
<text x="864" y="361" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Reject</text>
<text x="864" y="382" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Human decision</text>
<text x="864" y="400" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">per control.</text>
<text x="864" y="418" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">First two</text>
<text x="864" y="436" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">proposals</text>
<text x="864" y="454" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">auto-accepted.</text>
<rect x="1012" y="316" width="140" height="170" rx="0" fill="#FFFFFF" stroke="#DCDDE3" stroke-width="1"/>
<text x="1026" y="342" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">Compliance</text>
<text x="1026" y="361" font-size="14" font-weight="600" fill="#1D1D1F" text-anchor="start">tables</text>
<text x="1026" y="382" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Finding Reader,</text>
<text x="1026" y="400" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">Client Info</text>
<text x="1026" y="418" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">tile, Work ISO</text>
<text x="1026" y="436" font-size="13" font-weight="400" fill="#5F6068" text-anchor="start">facets.</text>
<path d="M180 400 H200" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M342 400 H362" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M504 400 H524" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M666 400 H686" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M828 400 H848" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M990 400 H1010" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="40" y="520" width="540" height="92" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="40" y="520" width="4" height="92" fill="#2E9E5B"/>
<text x="56" y="546" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Sent to AI services</text>
<text x="56" y="570" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Result DB rows through the privacy-safe export; rule-level check</text>
<text x="56" y="589" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">text; retrieved ISO corpus excerpts.</text>
<rect x="620" y="520" width="540" height="92" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<rect x="620" y="520" width="4" height="92" fill="#D64545"/>
<text x="636" y="546" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Never sent</text>
<text x="636" y="570" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Original files, GPO customer values, host, account and tenant</text>
<text x="636" y="589" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">names, reviews, evidence, file paths.</text>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,66 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="470" viewBox="0 0 1200 470" font-family="Inter, 'Segoe UI', Arial, sans-serif">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
<marker id="arrow-start" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="#4A4A50"/></marker>
</defs>
<rect x="0" y="0" width="1200" height="470" fill="#FAFAFB"/>
<text x="40" y="46" font-size="22" font-weight="600" fill="#1D1D1F" text-anchor="start">A typical assessment in the application</text>
<text x="40" y="72" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">The views a consultant works through, left to right. The Engines button in the toolbar switches optional engines on or off.</text>
<rect x="40" y="100" width="200" height="220" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="65" cy="121" r="11" fill="#1D1D1F"/>
<text x="65" y="126" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">1</text>
<text x="84" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Data Hub</text>
<text x="54" y="150" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Create a client,</text>
<text x="54" y="169" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">import scan reports</text>
<text x="54" y="188" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">and GPO backups,</text>
<text x="54" y="207" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">manage timeslots,</text>
<text x="54" y="226" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">update catalog</text>
<text x="54" y="245" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">references.</text>
<rect x="270" y="100" width="200" height="220" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="295" cy="121" r="11" fill="#1D1D1F"/>
<text x="295" y="126" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">2</text>
<text x="314" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Work</text>
<text x="284" y="150" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Filter all findings</text>
<text x="284" y="169" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">(status, source,</text>
<text x="284" y="188" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">domain, context, ISO).</text>
<text x="284" y="207" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Read them in the</text>
<text x="284" y="226" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Finding Reader. Review</text>
<text x="284" y="245" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">and accept risks.</text>
<rect x="500" y="100" width="200" height="220" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="525" cy="121" r="11" fill="#1D1D1F"/>
<text x="525" y="126" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">3</text>
<text x="544" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Client Info</text>
<text x="514" y="150" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Overall score,</text>
<text x="514" y="169" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">security domains,</text>
<text x="514" y="188" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">environment facts,</text>
<text x="514" y="207" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">context heatmap,</text>
<text x="514" y="226" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">compliance tile,</text>
<text x="514" y="245" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">reports, AI overview.</text>
<rect x="730" y="100" width="200" height="220" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="755" cy="121" r="11" fill="#1D1D1F"/>
<text x="755" y="126" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">4</text>
<text x="774" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Pathfinder</text>
<text x="744" y="150" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Graph of findings,</text>
<text x="744" y="169" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">targets, ATT&amp;CK</text>
<text x="744" y="188" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">techniques and D3FEND</text>
<text x="744" y="207" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">defenses. Extract the</text>
<text x="744" y="226" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">path around one node.</text>
<rect x="960" y="100" width="200" height="220" rx="0" fill="#F5F5F7" stroke="#DCDDE3" stroke-width="1"/>
<circle cx="985" cy="121" r="11" fill="#1D1D1F"/>
<text x="985" y="126" font-size="12" font-weight="600" fill="#FFFFFF" text-anchor="middle">5</text>
<text x="1004" y="126" font-size="16" font-weight="600" fill="#1D1D1F" text-anchor="start">Result DB</text>
<text x="974" y="150" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">Privacy-safe, stored</text>
<text x="974" y="169" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">dataset of all</text>
<text x="974" y="188" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">results. Filter,</text>
<text x="974" y="207" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">export JSONL, run ISO</text>
<text x="974" y="226" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">batches.</text>
<path d="M240 210 H268" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M470 210 H498" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M700 210 H728" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M930 210 H958" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<path d="M600 320 V350 H370 V322" fill="none" stroke="#4A4A50" stroke-width="2" marker-end="url(#arrow)"/>
<rect x="323.2" y="355" width="323.6" height="20" fill="#FAFAFB"/>
<text x="485" y="370" font-size="14" font-weight="400" fill="#1D1D1F" text-anchor="middle">tiles open Work with the matching filter</text>
<text x="40" y="420" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">The Data Flow view shows the live database schema and the engine diagram. Work exports (CSV, MD, HTML) contain the complete current filtered</text>
<text x="40" y="439" font-size="14" font-weight="400" fill="#5F6068" text-anchor="start">record set.</text>
</svg>

After

Width:  |  Height:  |  Size: 6.8 KiB

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "assessor",
"version": "2.19.0",
"version": "2.19.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "assessor",
"version": "2.19.0",
"version": "2.19.1",
"dependencies": {
"@fontsource-variable/inter": "^5.3.0",
"jsdom": "^29.1.1",

View File

@ -1,6 +1,6 @@
{
"name": "assessor",
"version": "2.19.0",
"version": "2.19.1",
"private": true,
"type": "module",
"scripts": {

View File

@ -44,7 +44,9 @@ export const PROCESSING_VERSION_MANIFESTS: readonly ProcessingVersionManifest[]
{ profileId: 'pingcastle', profileVersion: '1.2.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'presentation-only', reimportRequired: true, affectedDimensions: ['evidence', 'presentation'], current: false },
{ profileId: 'pingcastle', profileVersion: '1.3.0', mappingVersion: '1.1.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context', 'evidence', 'presentation'], current: false },
{ profileId: 'pingcastle', profileVersion: '1.4.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: false },
{ profileId: 'pingcastle', profileVersion: '1.5.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context', 'evidence', 'import-diagnostics'], current: true },
{ profileId: 'pingcastle', profileVersion: '1.5.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context', 'evidence', 'import-diagnostics'], current: false },
{ profileId: 'pingcastle', profileVersion: '1.6.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: false },
{ profileId: 'pingcastle', profileVersion: '1.7.0', mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0', compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: true },
{ profileId: 'cis-cat', profileVersion: '1.2.0', mappingVersion: '1.2.0', normalizationVersion: '1.2.0', presentationVersion: '1.2.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['evidence'], current: false },
{ profileId: 'cis-cat', profileVersion: '1.3.0', mappingVersion: '1.3.0', normalizationVersion: '1.3.0', presentationVersion: '1.3.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: false, affectedDimensions: ['evidence', 'presentation'], current: false },
{ profileId: 'cis-cat', profileVersion: '1.4.0', mappingVersion: '1.3.0', normalizationVersion: '1.3.0', presentationVersion: '1.3.0', compatibilityKey: 'cis-cat-canonical-v1', changeClass: 'compatible', reimportRequired: true, affectedDimensions: ['environment-context'], current: true },

View File

@ -3,13 +3,15 @@
export interface DashboardFact {
label: string;
value: string;
/** Muted qualifier after the value, e.g. how a value was derived. */
note?: string;
}
export type DashboardGlyphName = 'domain' | 'cloud' | 'user' | 'computer' | 'server' | 'policy' | 'shield' | 'lock' | 'code' | 'chart' | 'clipboard' | 'folder' | 'scan';
export function DashboardFactList({ items }: { items: DashboardFact[] }) {
return <dl className="client-env-facts">
{items.map(item => <div className={/^\S{17,}$/.test(item.value) ? 'is-wide' : undefined} key={item.label}><dt>{item.label}</dt><dd>{item.value}</dd></div>)}
{items.map(item => <div className={/^\S{17,}$/.test(item.value) ? 'is-wide' : undefined} key={item.label}><dt>{item.label}</dt><dd>{item.value}{item.note && <span className="client-fact-note"> ({item.note})</span>}</dd></div>)}
</dl>;
}

View File

@ -583,6 +583,21 @@ function factText(value: string | undefined): string {
return cleaned || UNKNOWN_FACT;
}
/**
* A native forest name (PingCastle XML, Purple Knight) takes priority over one
* the PingCastle HTML import derived from the configuration partition path; a
* derived value is shown with its derivation note.
*/
function forestFact(snapshot: EnvironmentSnapshot | undefined): DashboardFact {
const observed = (snapshot?.observations ?? []).filter(item => item.property === 'forest.name' && item.state === 'observed');
const observation = observed.find(item => !item.attributes.derivation) ?? observed[0];
if (!observation) return { label: 'Forest', value: UNKNOWN_FACT };
const value = factText(displayValue(observation));
return observation.attributes.derivation === 'configuration-partition-dn' && value !== UNKNOWN_FACT
? { label: 'Forest', value, note: 'derived from configuration path' }
: { label: 'Forest', value };
}
function observationFact(snapshot: EnvironmentSnapshot | undefined, property: string, label?: string): string {
const observation = observationFor(snapshot, property, label);
return observation ? factText(displayValue(observation)) : UNKNOWN_FACT;
@ -650,7 +665,7 @@ export function dashboardEnvironmentDetail(snapshot?: EnvironmentSnapshot): Dash
directory: [
{ label: 'Domain', value: observationFact(snapshot, 'domain.fqdn', 'Domain') },
{ label: 'NetBIOS name', value: observationFact(snapshot, 'domain.netbios_name', 'Netbios Name') },
{ label: 'Forest', value: observationFact(snapshot, 'forest.name') },
forestFact(snapshot),
{ label: 'Domains', value: numberFact(dashboardEnvironmentDomainCount(snapshot)) },
{ label: 'Domain functional level', value: observationFact(snapshot, 'domain.functional_level', 'Domain Functional Level') },
{ label: 'Forest functional level', value: observationFact(snapshot, 'forest.functional_level', 'Forest Functional Level') },
@ -731,7 +746,7 @@ export function dashboardDomainInformation(snapshot?: EnvironmentSnapshot): Dash
return [
{ label: 'Domain', value: observationFact(snapshot, 'domain.fqdn', 'Domain') },
{ label: 'NetBIOS name', value: observationFact(snapshot, 'domain.netbios_name', 'Netbios Name') },
{ label: 'Forest', value: observationFact(snapshot, 'forest.name') },
forestFact(snapshot),
{ label: 'Domains', value: numberFact(dashboardEnvironmentDomainCount(snapshot)) },
{ label: 'Domain controllers', value: observationFact(snapshot, 'domain.controller_count', 'DC count') },
{ label: 'Domain functional level', value: observationFact(snapshot, 'domain.functional_level', 'Domain Functional Level') },

View File

@ -28,7 +28,7 @@ const relationships = [
{ from: 'core', to: 'ui', label: 'Core results', detail: 'Authoritative findings, verdicts, scores and disclosure rules', path: 'M420 290 V160' },
{ from: 'context', to: 'context-ui', label: 'Context', detail: 'Published context results through existing UI extension points', path: 'M710 290 V160' },
{ from: 'report', to: 'archive', label: 'Reports', detail: 'Generated reports; separate from Core database storage', path: 'M1000 520 V390' },
{ from: 'context', to: 'report', label: 'Planned', detail: 'Published context results; documented target, not yet connected in report generation', path: 'M820 340 H855 V490 H940 V520', planned: true },
{ from: 'context', to: 'report', label: 'Published', detail: 'Versioned Context publication for the Management Report, composed by the Core backend; neither engine imports the other', path: 'M820 340 H855 V490 H940 V520', planned: false },
{ from: 'port', to: 'compliance', label: 'Read only', detail: 'Single Result DB row by client, filtered Result DB pages, client, assessment and scan-run lists', path: 'M710 620 V750' },
{ from: 'compliance', to: 'compliance-tables', label: 'Own data', detail: 'Mapping runs, decisions and batches; never Core tables', path: 'M600 800 H530', bidirectional: true },
{ from: 'compliance', to: 'retrieval', label: 'Retrieval', detail: 'Privacy-safe query, retrieved control evidence and model proposals', path: 'M820 800 H890', bidirectional: true },

View File

@ -544,6 +544,61 @@ function preserveSubSectionMarkers(rawText: string): string {
);
}
const maximumEmbeddedDataBlocks = 64;
const maximumEmbeddedDataCharacters = 5_000_000;
// The HTML report has no forest name field. PingCastle's embedded graph data
// (inert `application/json` blocks) names directory objects by their full DN;
// the Configuration naming context always lies below the forest root domain,
// so `CN=Configuration,DC=a,DC=b` states the forest root `a.b`. Only the
// `nodes[].name` strings of those blocks are read, never evaluated. The forest
// is retained only when every such DN names the same root; conflicting roots
// yield a diagnostic and no forest.
function configurationPartitionForest(document: DecodedSourceDocument): {
observations: EnvironmentObservationCandidate[];
diagnostics: ImportDiagnostic[];
} {
const roots = new Map<string, { count: number; source: SourceLocator }>();
const pattern = /<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi;
let match: RegExpExecArray | null;
let blocks = 0;
while ((match = pattern.exec(document.rawText)) && blocks < maximumEmbeddedDataBlocks) {
const attributes = match[1] ?? '';
const selector = attributeValue(attributes, 'data-pingcastle-selector');
if (attributeValue(attributes, 'type').toLocaleLowerCase('en-US') !== 'application/json' || !selector) continue;
blocks += 1;
const body = match[2] ?? '';
if (body.length > maximumEmbeddedDataCharacters || !body.includes('CN=Configuration,')) continue;
let data: unknown;
try { data = JSON.parse(body); } catch { continue; }
const nodes = data && typeof data === 'object' && Array.isArray((data as { nodes?: unknown }).nodes)
? (data as { nodes: unknown[] }).nodes : [];
nodes.forEach((node, index) => {
const name = node && typeof node === 'object' ? (node as { name?: unknown }).name : undefined;
if (typeof name !== 'string') return;
const components = /^(?:CN=[^,=]+,)*CN=Configuration,((?:DC=[A-Za-z0-9-]+,)*DC=[A-Za-z0-9-]+)$/i.exec(name.trim())?.[1];
if (!components) return;
const root = components.split(',').map(part => part.slice(3)).join('.').toLocaleLowerCase('en-US');
const entry = roots.get(root);
if (entry) entry.count += 1;
else roots.set(root, { count: 1, source: { fileName: document.file.name, objectPath: `html/embedded-data/${selector}/nodes/${index}/name` } });
});
}
if (roots.size > 1) return { observations: [], diagnostics: [{
severity: 'warning', code: 'pingcastle-forest-root-conflict', stage: 'extraction',
message: 'Die Configuration-DNs des PingCastle-Berichts nennen verschiedene Gesamtstruktur-Stammdomänen; es wird keine Gesamtstruktur abgeleitet.',
source: { fileName: document.file.name, objectPath: 'html/embedded-data' }, details: { rootCount: roots.size }
}] };
const [root, entry] = [...roots.entries()][0] ?? [];
if (!root || !entry) return { observations: [], diagnostics: [] };
return { diagnostics: [], observations: [{
version: 1, subject: { type: 'domain-information', identity: 'html/embedded-data/configuration-partition' },
property: 'forest.name', label: 'Forest', originalValue: root, normalizedValue: root, valueType: 'string', state: 'observed',
source: entry.source,
attributes: { derivation: 'configuration-partition-dn', derivationLabel: 'Derived from the configuration partition path', occurrenceCount: entry.count }
}] };
}
function htmlCandidates(document: DecodedSourceDocument): {
candidates: FindingCandidate[];
diagnostics: ImportDiagnostic[];
@ -644,11 +699,15 @@ function htmlCandidates(document: DecodedSourceDocument): {
const coverage = pingCastleHtmlRuleCoverage(document.file.name, markup, candidates);
// Coverage is source metadata, not another set of findings or environment assets.
if (coverage.section) sections.push({ ...coverage.section, ordinal: sections.length });
const forest = configurationPartitionForest(document);
return {
candidates,
diagnostics: [...diagnostics, ...ruleDiagnostics, ...coverage.diagnostics],
diagnostics: [...diagnostics, ...ruleDiagnostics, ...coverage.diagnostics, ...forest.diagnostics],
reportSections: sections,
environmentObservations: contextObservations(document, sections.filter(section => section.sectionKey !== 'pingcastle-rule-coverage'), reportScores)
environmentObservations: [
...contextObservations(document, sections.filter(section => section.sectionKey !== 'pingcastle-rule-coverage'), reportScores),
...forest.observations
]
};
}
@ -1193,6 +1252,29 @@ function detailValues(nodes: DecodedHierarchyNode[]): string[] {
return [...plain, ...extra];
}
// Exact top-level HealthcheckData leaves that carry the same facts the HTML
// Domain Information table provides. Only these names are mapped; every other
// node keeps its path-based property. Values stay as PingCastle wrote them
// (functional levels are numeric codes in XML and are not translated).
const xmlDomainProperties: Record<string, [string, string]> = {
DomainFQDN: ['domain.fqdn', 'Domain'],
NetBIOSName: ['domain.netbios_name', 'Netbios Name'],
ForestFQDN: ['forest.name', 'Forest'],
DomainCreation: ['domain.created_at', 'Creation date'],
DomainFunctionalLevel: ['domain.functional_level', 'Domain Functional Level'],
ForestFunctionalLevel: ['forest.functional_level', 'Forest Functional Level'],
SchemaVersion: ['forest.schema_version', 'Schema version'],
IsRecycleBinEnabled: ['domain.recycle_bin_enabled', 'Recycle Bin enabled'],
NumberOfDC: ['domain.controller_count', 'DC count'],
GenerationDate: ['report.generated_at', 'Report generation date'],
EngineVersion: ['report.engine_version', 'PingCastle engine version']
};
function xmlDomainProperty(path: string): [string, string] | undefined {
const leaf = /^\/?HealthcheckData(?:\[\d+\])?\/([A-Za-z]+)\[\d+\]$/.exec(path)?.[1];
return leaf && Object.hasOwn(xmlDomainProperties, leaf) ? xmlDomainProperties[leaf] : undefined;
}
function xmlCandidates(document: DecodedSourceDocument): {
candidates: FindingCandidate[];
diagnostics: ImportDiagnostic[];
@ -1271,7 +1353,15 @@ function xmlCandidates(document: DecodedSourceDocument): {
label: node.name, sectionKey: 'HealthcheckData', source: node.source,
attributes: { xmlPath: node.path }
};
if (value || (!parentPaths.has(node.path) && !attributeEntries.length && !node.attributes.serialized)) {
const domainProperty = value ? xmlDomainProperty(node.path) : undefined;
if (domainProperty) {
const [property, label] = domainProperty;
const subject = property.startsWith('report.')
? { type: 'scanner-report', identity: document.file.name }
: { type: 'domain-information', identity: node.path };
environmentObservations.push({ ...base, subject, property, label, originalValue: value,
...typedValue(value), state: 'observed' });
} else if (value || (!parentPaths.has(node.path) && !attributeEntries.length && !node.attributes.serialized)) {
environmentObservations.push({ ...base, originalValue: value || null,
...(value ? typedValue(value) : { valueType: 'object' as const }), state: value ? 'observed' : 'empty' });
}
@ -1313,7 +1403,7 @@ function isHealthcheckXml(document: DecodedSourceDocument): boolean {
export const pingCastleSourceExtractor: SourceExtractor = {
id: 'pingcastle-healthcheck-source-extractor',
version: '2.2.1',
version: '2.4.0',
detect(document) {
if (isHealthcheckXml(document)) {
const riskRuleCount = xmlRuleGroups(document).length;

View File

@ -47,11 +47,11 @@ export const pingCastleDefinition: ToolProfileDefinition = {
diagnosticPolicy: 'fail-closed-by-impact'
},
name: 'PingCastle',
version: '1.5.0',
version: '1.7.0',
processing: {
mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0',
compatibilityKey: 'pingcastle-canonical-v1.1', changeClass: 'compatible',
reimportRequired: true, affectedDimensions: ['environment-context', 'evidence', 'import-diagnostics']
reimportRequired: true, affectedDimensions: ['environment-context']
},
sourceSignals: [
{ scope: 'file-name', pattern: /ping[\s_.-]*castle|^ad[_-]hc[_-]/i, contribution: 0.5, message: 'Der Dateiname entspricht einem PingCastle-Healthcheck.' },

View File

@ -384,6 +384,7 @@ button.client-info-metric[aria-pressed="true"] { border-color: var(--tile-select
.client-environment-grid > section > .client-env-facts > div { min-height: 0; padding: var(--space-1) var(--space-2); border: var(--border-hair) solid var(--client-info-line); background: var(--tile-surface); }
.client-environment-grid > section > .client-env-facts > div.is-wide { grid-column: 1 / -1; }
.client-environment-grid > section > .client-env-facts dt { font-weight: var(--fw-regular); }
.client-fact-note { color: var(--muted); font-weight: var(--fw-regular); }
.client-environment-grid .client-env-group-title,
.client-command-center .client-environment-grid > .client-os-inventory > header h3 { font-size: var(--type-h4-size); font-weight: var(--fw-semibold); }
/* Compliance Engine row (Client Info extension point): framework tiles with the exact environment tile

View File

@ -9,7 +9,7 @@ const components = readFileSync(new URL('../../src/styles/components.css', impor
describe('ASSESSOR version in the brand header', () => {
it('shows the centrally maintained product version and opens About from the brand', () => {
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.19.0');
expect(ASSESSOR_PRODUCT_VERSION).toBe('2.19.1');
expect(app).toContain("'db-structure': 'Data Flow'");
expect(app).toContain("['db-structure', 'Data Flow']");
expect(app).not.toContain("'DB Structure'");

View File

@ -29,7 +29,9 @@ describe('processing compatibility contracts', () => {
const old = file('old', 'pingcastle', '1.1.0', '1.1.0');
const contextless = file('contextless', 'pingcastle', '1.2.0', '1.1.0');
const previous = file('previous', 'pingcastle', '1.4.0', '1.2.0');
const current = file('current', 'pingcastle', '1.5.0', '1.2.0');
const xmlWithoutDomainFacts = file('xml-without-domain-facts', 'pingcastle', '1.5.0', '1.2.0');
const htmlWithoutForest = file('html-without-forest', 'pingcastle', '1.6.0', '1.2.0');
const current = file('current', 'pingcastle', '1.7.0', '1.2.0');
expect(resolvedCompatibilityKey('pingcastle', old.processedWith)).toBe('pingcastle-canonical-v1.1');
expect(resolvedCompatibilityKey('pingcastle', contextless.processedWith)).toBe('pingcastle-canonical-v1.1');
@ -37,6 +39,8 @@ describe('processing compatibility contracts', () => {
expect(processingReadiness('pingcastle', old.processedWith)).toBe('reimport-recommended');
expect(processingReadiness('pingcastle', contextless.processedWith)).toBe('reimport-recommended');
expect(processingReadiness('pingcastle', previous.processedWith)).toBe('reimport-recommended');
expect(processingReadiness('pingcastle', xmlWithoutDomainFacts.processedWith)).toBe('reimport-recommended');
expect(processingReadiness('pingcastle', htmlWithoutForest.processedWith)).toBe('reimport-recommended');
expect(processingReadiness('pingcastle', current.processedWith)).toBe('current');
expect(processingScopeIssues([old, contextless, current])).toEqual([]);
});

View File

@ -4,6 +4,8 @@ import type { FindingRecord } from '../../src/domain/finding';
import type { EnvironmentSnapshot } from '../../src/domain/environment-context';
import { dashboardCisCatSeverityRemainder, dashboardDomainInformation, dashboardEnvironmentSummary, dashboardExplicitIoeCount, dashboardExplicitIoeRecordIds, dashboardOverallScore, dashboardPingCastleRiskCategories, dashboardRiskTools, dashboardScanSourceLabel } from '../../src/features/dashboard/DashboardPanel';
import { securityDomainCatalog } from '../../src/features/dashboard/security-domains';
import { renderToStaticMarkup } from 'react-dom/server';
import { DashboardFactList } from '../../src/features/dashboard/ClientTileParts';
const component = readFileSync(new URL('../../src/features/dashboard/DashboardPanel.tsx', import.meta.url), 'utf8');
const styles = readFileSync(new URL('../../src/styles/datahub.css', import.meta.url), 'utf8');
@ -212,6 +214,31 @@ describe('dashboard reference layout', () => {
.find(item => item.label === 'DC OS')?.value).toBe('Windows 2019, Windows Server 2025');
});
it('prefers a native forest over one derived from the configuration path and marks the derived value', () => {
const forest = (id: string, value: string, attributes: Record<string, string>, state: 'observed' | 'unknown' = 'observed') => ({
version: 1 as const, id, scanRunId: 'run-1', importFileId: 'file-1',
tool: { id: 'pingcastle', name: 'PingCastle', version: '1' }, subject: { type: 'domain-information' },
property: 'forest.name', label: 'Forest', originalValue: value, normalizedValue: value, valueType: 'string' as const,
state, source: { objectPath: 'synthetic' }, mappingVersion: '1', attributes
});
const base = { version: 1 as const, projectId: 'project-1', scanRunIds: ['run-1'], generatedAt: '2026-10-07T00:00:00.000Z',
coverage: { observed: 0, explicitlyAbsent: 0, empty: 0, restricted: 0, incomplete: 0 }, sections: [] };
const derived = forest('derived', 'derived.synthetic.example', { derivation: 'configuration-partition-dn' });
const native = forest('native', 'native.synthetic.example', {});
const fact = (observations: Array<ReturnType<typeof forest>>) =>
dashboardDomainInformation({ ...base, observations }).find(item => item.label === 'Forest');
expect(fact([derived])).toEqual({ label: 'Forest', value: 'derived.synthetic.example', note: 'derived from configuration path' });
expect(fact([derived, native])).toEqual({ label: 'Forest', value: 'native.synthetic.example' });
expect(fact([native, derived])).toEqual({ label: 'Forest', value: 'native.synthetic.example' });
expect(fact([forest('stale', 'unknown.synthetic.example', {}, 'unknown'), derived])?.value).toBe('derived.synthetic.example');
expect(fact([])).toEqual({ label: 'Forest', value: '—' });
const markup = renderToStaticMarkup(DashboardFactList({ items: [fact([derived])!] }));
expect(markup).toContain('<dd>derived.synthetic.example<span class="client-fact-note"> (derived from configuration path)</span></dd>');
expect(renderToStaticMarkup(DashboardFactList({ items: [fact([native])!] }))).not.toContain('client-fact-note');
});
it('removes only a trailing scan timestamp from source labels', () => {
expect(dashboardScanSourceLabel('PurpleKnight · 2026-08-22T08:15:30.000Z')).toBe('PurpleKnight');
expect(dashboardScanSourceLabel('CIS-CAT Pro · 2026-08-22')).toBe('CIS-CAT Pro');

View File

@ -86,7 +86,7 @@ describe('Datahub client processing readiness', () => {
// A display tool label cannot turn a generic processor into a native one.
expect(summarizeScanProcessingReadiness([{ ...companion, detectedTool: { id: 'purple-knight', name: 'PurpleKnight' } }]).state).toBe('review');
expect(summarizeScanProcessingReadiness([currentPurpleKnight('run'), companion,
file('run', 'pingcastle', '1.5.0', 'pingcastle-canonical-v1.1')]).state).toBe('review');
file('run', 'pingcastle', '1.7.0', 'pingcastle-canonical-v1.1')]).state).toBe('review');
});
it('marks GPO failure and recovery red, including clients with healthy scanner reports', () => {
@ -144,7 +144,7 @@ describe('Datahub client processing readiness', () => {
file('run-old', 'pingcastle', '1.1.0', 'pingcastle-canonical-v1.1')
])).toMatchObject({
state: 'update',
explanation: 'ASSESSOR profile update: 1.1.0 → 1.5.0. Reimporting applies the newer ASSESSOR processing profile; the pingcastle report itself has not changed. The newer ASSESSOR profile captures additional environment context for the assessment.'
explanation: 'ASSESSOR profile update: 1.1.0 → 1.7.0. Reimporting applies the newer ASSESSOR processing profile; the pingcastle report itself has not changed. The newer ASSESSOR profile captures additional environment context for the assessment.'
});
});
@ -153,7 +153,7 @@ describe('Datahub client processing readiness', () => {
file('run-contextless', 'pingcastle', '1.2.0', 'pingcastle-canonical-v1.1')
])).toMatchObject({
state: 'update',
explanation: 'ASSESSOR profile update: 1.2.0 → 1.5.0. Reimporting applies the newer ASSESSOR processing profile; the pingcastle report itself has not changed. The newer ASSESSOR profile captures additional environment context for the assessment.'
explanation: 'ASSESSOR profile update: 1.2.0 → 1.7.0. Reimporting applies the newer ASSESSOR processing profile; the pingcastle report itself has not changed. The newer ASSESSOR profile captures additional environment context for the assessment.'
});
});

View File

@ -55,7 +55,7 @@ describe('DB Structure data flow', () => {
} finally { await act(async () => root.unmount()); }
});
it('selects engine modules, highlights only their directed connections and discloses planned flow', async () => {
it('selects engine modules, highlights only their directed connections and shows the implemented Context publication', async () => {
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
const container = document.createElement('div');
const root = createRoot(container);
@ -71,8 +71,10 @@ describe('DB Structure data flow', () => {
expect(context.getAttribute('aria-pressed')).toBe('true');
expect(container.querySelector('.db-engine-flow__inspector h4')?.textContent).toBe('Context Engine');
expect(container.querySelectorAll('.db-engine-flow__edges > path.is-related')).toHaveLength(3);
expect(container.querySelector('.db-engine-flow__edges > path.is-planned.is-related')).not.toBeNull();
expect(container.querySelector('.db-engine-flow__inspector')?.textContent).toContain('not yet connected in report generation · Planned');
// The Management Report consumes the published Context results, so no relationship is drawn as planned.
expect(container.querySelector('.db-engine-flow__edges > path.is-planned')).toBeNull();
expect(container.querySelector('.db-engine-flow__inspector')?.textContent).toContain('Versioned Context publication for the Management Report');
expect(container.querySelector('.db-engine-flow__inspector')?.textContent).not.toContain('Planned');
await act(async () => context.click());
expect(container.querySelector('.db-engine-flow__inspector')).toBeNull();
expect(container.querySelectorAll('.db-engine-flow__edges > path.is-related')).toHaveLength(0);
@ -202,8 +204,8 @@ describe('DB Structure data flow', () => {
const relations = container.querySelector('table[aria-label="Directed engine relationships"]');
expect(relations?.querySelectorAll('tbody tr')).toHaveLength(13);
expect(relations?.textContent).toContain('Core read-only port');
expect(relations?.textContent).toContain('Planned');
expect(relations?.textContent).toContain('not yet connected in report generation');
expect(relations?.textContent).not.toContain('Planned');
expect(relations?.textContent).toContain('Versioned Context publication for the Management Report');
await act(async () => {
container.querySelector<HTMLButtonElement>('.db-structure-intro-actions button')!.click();
await new Promise(resolve => setTimeout(resolve, 0));

View File

@ -152,7 +152,7 @@ describe('PingCastle native finding extraction', () => {
attributes: expect.objectContaining({ subsectionLabel: 'Account analysis', sourceTableIndex: expect.any(Number) }) })
]));
expect(model.importFiles[0].processedWith).toMatchObject({
profileId: 'pingcastle', profileVersion: '1.5.0', mappingVersion: '1.2.0',
profileId: 'pingcastle', profileVersion: '1.7.0', mappingVersion: '1.2.0',
compatibilityKey: 'pingcastle-canonical-v1.1'
});
expect(model.scanRuns[0].tool.version).toBe('3.4.0');
@ -197,6 +197,81 @@ describe('PingCastle native finding extraction', () => {
expect([...subjectTypes].some(type => /rule-details/.test(type))).toBe(false);
});
it('derives the HTML forest only from agreeing configuration partition DNs in the embedded graph data', async () => {
const graph = (selector: string, names: string[]) => `<script type="application/json" data-pingcastle-selector="${selector}">`
+ JSON.stringify({ nodes: names.map((name, id) => ({ id, name, type: 'container' })) }) + '</script>';
const withGraphs = (...blocks: string[]) => pingCastleHtml.replace('</main>', blocks.join('') + '</main>');
const forests = async (html: string) => {
const dataset = await importPingCastle(html, 'ad_hc_synthetic.example.html', 'text/html');
return {
observations: (dataset.extraction?.environmentObservations ?? []).filter(item => item.property === 'forest.name'),
diagnostics: (dataset.extraction?.diagnostics ?? []).filter(item => item.code === 'pingcastle-forest-root-conflict')
};
};
const agreeing = await forests(withGraphs(
graph('Data_Synthetic_A', ['CN=Services,CN=Configuration,DC=Forest,DC=Synthetic,DC=Example', 'CN=Domain Admins,CN=Users,DC=child,DC=forest,DC=synthetic,DC=example']),
graph('Data_Synthetic_B', ['CN=Configuration,DC=forest,DC=synthetic,DC=example'])
));
expect(agreeing.diagnostics).toEqual([]);
expect(agreeing.observations).toEqual([expect.objectContaining({
label: 'Forest', originalValue: 'forest.synthetic.example', state: 'observed',
source: expect.objectContaining({ objectPath: 'html/embedded-data/Data_Synthetic_A/nodes/0/name' }),
attributes: expect.objectContaining({ derivation: 'configuration-partition-dn', occurrenceCount: 2 })
})]);
const conflicting = await forests(withGraphs(graph('Data_Synthetic_A', [
'CN=Configuration,DC=one,DC=example', 'CN=Services,CN=Configuration,DC=two,DC=example'
])));
expect(conflicting.observations).toEqual([]);
expect(conflicting.diagnostics).toEqual([expect.objectContaining({ severity: 'warning', details: { rootCount: 2 } })]);
// No full DN, a truncated visible DN, a non-JSON script, unparsable JSON or a domain DN never yield a forest.
const absent = await forests(withGraphs(
'<p>CN=Templates,CN=Configuration,DC=visible,DC=example</p>',
'<script>var x = {"nodes":[{"name":"CN=Configuration,DC=script,DC=example"}]};</script>',
'<script type="application/json" data-pingcastle-selector="Data_Broken">{"nodes": [CN=Configuration,DC=broken,DC=example</script>',
graph('Data_Synthetic_C', ['CN=Configuration', 'CN=Domain Admins,CN=Users,DC=domain,DC=example'])
));
expect(absent).toEqual({ observations: [], diagnostics: [] });
});
it('maps the top-level XML domain facts to the same environment properties the HTML import uses', async () => {
const xml = pingCastleXml.replace('<DomainFQDN>synthetic.example</DomainFQDN>', [
'<EngineVersion>3.5.1.33</EngineVersion>', '<GenerationDate>2026-07-22T10:18:58+02:00</GenerationDate>',
'<DomainFQDN>synthetic.example</DomainFQDN>', '<NetBIOSName>SYNTHETIC</NetBIOSName>',
'<ForestFQDN>forest.synthetic.example</ForestFQDN>', '<DomainCreation>2026-05-27T10:24:53</DomainCreation>',
'<DomainSid>S-1-5-21-1-2-3</DomainSid>', '<DomainFunctionalLevel>7</DomainFunctionalLevel>',
'<ForestFunctionalLevel>7</ForestFunctionalLevel>', '<SchemaVersion>88</SchemaVersion>',
'<IsRecycleBinEnabled>false</IsRecycleBinEnabled>', '<NumberOfDC>2</NumberOfDC>'
].join(''));
const dataset = await importPingCastle(xml, 'ad_hc_synthetic.example.xml', 'application/xml');
const observations = dataset.extraction?.environmentObservations ?? [];
const fact = (property: string) => observations.filter(item => item.property === property);
expect(fact('domain.fqdn')).toEqual([expect.objectContaining({
label: 'Domain', originalValue: 'synthetic.example', normalizedValue: 'synthetic.example', state: 'observed',
subject: expect.objectContaining({ type: 'domain-information' }),
attributes: expect.objectContaining({ xmlPath: expect.stringMatching(/DomainFQDN\[\d+\]$/) })
})]);
expect(Object.fromEntries([
'domain.netbios_name', 'forest.name', 'domain.created_at', 'domain.functional_level', 'forest.functional_level',
'forest.schema_version', 'domain.recycle_bin_enabled', 'domain.controller_count', 'report.generated_at', 'report.engine_version'
].map(property => [property, fact(property).map(item => item.originalValue)]))).toEqual({
'domain.netbios_name': ['SYNTHETIC'], 'forest.name': ['forest.synthetic.example'],
'domain.created_at': ['2026-05-27T10:24:53'], 'domain.functional_level': ['7'], 'forest.functional_level': ['7'],
'forest.schema_version': ['88'], 'domain.recycle_bin_enabled': ['false'], 'domain.controller_count': ['2'],
'report.generated_at': ['2026-07-22T10:18:58+02:00'], 'report.engine_version': ['3.5.1.33']
});
// Unmapped leaves keep their exact path-based property; the domain SID gets no semantic name.
expect(observations.some(item => item.property.startsWith('pingcastle.xml.') && item.originalValue === 'S-1-5-21-1-2-3')).toBe(true);
expect(observations.some(item => item.property.startsWith('pingcastle.xml.') && item.originalValue === 'synthetic.example')).toBe(false);
// Nested leaves with a mapped name are not top-level facts.
const nested = await importPingCastle(pingCastleXml.replace('<RiskRules>', '<Trusts><Trust><NetBIOSName>OTHER</NetBIOSName></Trust></Trusts><RiskRules>'),
'ad_hc_synthetic.example.xml', 'application/xml');
expect(nested.extraction?.environmentObservations?.filter(item => item.property === 'domain.netbios_name')).toEqual([]);
});
it('maps the same matched rule identity from HTML and XML while preserving format-native provenance', async () => {
const datasets = await Promise.all([
importPingCastle(pingCastleHtml, 'ad_hc_synthetic.example.html', 'text/html'),

View File

@ -354,7 +354,7 @@ describe('Unified result read model', () => {
format: 'HTML',
fileName: 'synthetic-pingcastle.html',
processedWith: {
parserId: 'html-report', parserVersion: '1.1.0', profileId: 'pingcastle', profileVersion: '1.5.0',
parserId: 'html-report', parserVersion: '1.1.0', profileId: 'pingcastle', profileVersion: '1.7.0',
mappingVersion: '1.2.0', normalizationVersion: '1.1.0', presentationVersion: '1.2.0',
compatibilityKey: 'pingcastle-canonical-v1.1'
}
@ -402,7 +402,7 @@ describe('Unified result read model', () => {
resultProjectionId: 'environment-observation:environment-recycle-bin',
sourceKind: 'environment-observation',
sourceTool: 'pingcastle',
sourceProfileVersion: '1.5.0',
sourceProfileVersion: '1.7.0',
mappingRuleVersion: '1.2.0+context-1.0.0',
processingStatus: 'current',
environmentProperty: 'domain.recycle_bin_enabled',

View File

@ -59,7 +59,7 @@ describe('local ASSESSOR HTTP server', () => {
const health = await fetch(`${baseUrl}/api/v1/health`);
expect(await health.json()).toEqual({
status: 'ok', application: 'ASSESSOR', productVersion: '2.19.0', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
status: 'ok', application: 'ASSESSOR', productVersion: '2.19.1', apiVersion: 1, repositoryRevision: 0, persistence: 'memory'
});
expect(health.headers.get('access-control-allow-origin')).toBeNull();
});