Add onboarding SVG diagrams for data layers, AI workflows, and user journey
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
| 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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
**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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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`.
|
||||
|
|
@ -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 |
|
||||
|------|-----------|
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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&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: <area>".</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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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&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 |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "assessor",
|
||||
"version": "2.19.0",
|
||||
"version": "2.19.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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') },
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.' },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'");
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||