# ASSESSOR — Project Overview ASSESSOR imports security-scanner reports (PingCastle, Purple Knight, CIS-CAT, Microsoft Defender for IoT, Maester, Group Policy backups, and generic CSV/JSON/XML/HTML/Excel exports), normalizes them into one canonical model, scores the assessed environment, lets an analyst review and compare findings, and presents an executive security posture. It also offers optional AI overview generation and regulatory (ISO 27001/27002) mapping through a retrieval index. **This is the entry point for the whole project.** It explains what the software is, how it is put together, how many containers it runs, how to develop it locally, and how it is deployed. Detailed topic docs live alongside this file in [`docs/`](.); this document links to the right one for each topic. > Status: the application runs today as a local browser app served by a Node.js > backend, and is being deployed to an internal server behind an nginx reverse > proxy. The full phased plan and its history are in > [`docs/IMPLEMENTATION_PLAN.md`](IMPLEMENTATION_PLAN.md); binding rules for > anyone (human or AI) changing the code are in [`AGENTS.md`](../AGENTS.md). --- ## 1. What runs where — the short answer There are **three long-running services**, defined in [`docker-compose.deploy.yml`](../docker-compose.deploy.yml): | Service | Image | Role | Published on host? | |------------|----------------------|------------------------------------------------------------------|--------------------| | `app` | built from `Dockerfile` | Node.js server: serves the React app **and** the `/api/v1` API from one origin | Yes — the only exposed port | | `postgres` | `postgres:16-alpine` | System of record (canonical findings, reviews, comparisons, jobs) | No | | `qdrant` | `qdrant/qdrant:v1.19.1` | Rebuildable vector index for regulatory RAG retrieval | No | So **one deployed environment = 3 containers**. Only `app` is reachable from the host; PostgreSQL and Qdrant are private on the internal Docker network. The server runs **two isolated environments in parallel** (see [`docs/DEPLOYMENT.md`](DEPLOYMENT.md)), each its own set of 3 containers with its own database, artifact volume, and Qdrant index: | Branch | Compose project | VPN address | Public host name (via nginx) | |----------------|-------------------------|--------------------------|------------------------------------| | `main` | `assessor_prod` | `http://10.0.0.154:4173` | `https://assessor.soc-ops.de` | | `experimental` | `assessor_experimental` | `http://10.0.0.154:4174` | `https://staging.assessor.soc-ops.de` | That is **6 containers total** for the managed stacks. A separate, older HTTPS installation exists on the same host and is intentionally *not* managed by this workflow until an approved cutover. For **local development** you do not containerize the app. You run only the two backing services in Docker (`postgres` + `qdrant`, project `assessor-2_0` — the single canonical local datastore) and run the Node backend + Vite client as host processes. See [§4](#4-local-development). --- ## 2. Architecture ### 2.1 Layered design ASSESSOR keeps a strict separation between format parsing, structural analysis, semantic mapping, normalization, comparison, persistence, and presentation. The core engine is **platform-neutral**: it never touches the DOM, `localStorage`, browser `File` objects, SQL, or the network directly — those live behind adapters. This is what lets the same domain logic run in the browser today and in the server backend now / a future shell later. ``` ┌─────────────────────────────────────────────────────────────┐ │ React UI (src/features, src/components) │ presentation only │ no source-specific parsing, no direct DB access │ └───────────────┬───────────────────────────────────────────────┘ │ HTTP (same origin) ┌───────────────▼───────────────────────────────────────────────┐ │ Node backend (server/) — /api/v1 + static React app │ │ import workers · GPO analysis · CVE enrichment · AI · RAG │ └───────────────┬───────────────────────────────────────────────┘ │ AssessorRepository contract (no SQL leaks upward) ┌───────────────▼───────────────────────────────────────────────┐ │ Persistence adapter │ │ • PostgresAssessorRepository (durable, production) │ │ • LocalAssessorRepository (in-memory, dev only) │ └───────────────┬───────────────────────────────────────────────┘ │ ┌───────────────▼──────────┐ ┌──────────────────────────┐ │ PostgreSQL 16 │ │ Qdrant (regulatory RAG) │ │ system of record │ │ rebuildable index │ └──────────────────────────┘ └──────────────────────────┘ ``` The **platform-neutral import engine** (`src/importers`, `src/domain`) turns raw bytes into an `ImportSource`, parses it through injected runtime adapters, picks a scanner profile via the shared recognition engine, and produces the canonical model. Known tool profiles *extend* this generic pipeline rather than bypass it. Key boundary docs: - [`docs/PLATFORM_NEUTRAL_ENGINE.md`](PLATFORM_NEUTRAL_ENGINE.md) — engine/adapter layering - [`docs/PERSISTENCE_BOUNDARY.md`](PERSISTENCE_BOUNDARY.md) — the `AssessorRepository` contract and import transaction - [`docs/CANONICAL_MODEL.md`](CANONICAL_MODEL.md) — the normalized data model - [`docs/RECOGNITION_ENGINE.md`](RECOGNITION_ENGINE.md) — profile selection - [`docs/UNIFIED_RESULT_MODEL.md`](UNIFIED_RESULT_MODEL.md) / [`docs/RESULT_CONTRACT.md`](RESULT_CONTRACT.md) — the read model behind the UI - [`docs/SCORING_MODEL.md`](SCORING_MODEL.md) — native-score-first scoring engine ### 2.2 The app service in detail The single `app` container (or local `node dist-server/server.mjs`) does three jobs from one origin (port `4173`): 1. Serves the built React SPA from `dist/`. 2. Serves the versioned JSON API under `/api/v1`. 3. Runs background work: isolated import parser workers, GPO inventory/analysis workers, CVE enrichment, AI overview jobs, and regulatory retrieval. Same-origin is a deliberate security property: the server sets **no CORS**, and every write requires the `X-Assessor-Request: 1` header with a matching `Origin`. See [`docs/LOCAL_BACKEND.md`](LOCAL_BACKEND.md) and [`SECURITY.md`](../SECURITY.md). ### 2.3 API surface All endpoints live under `/api/v1`. Top-level resource groups: `health` · `audit-events` · `db-structure` · `reference-data` · `projects` (with nested `policy-analyses`, `environment-profiles`, `policy-snapshot-aggregations`, `policy-evaluations`, `policy-finding-runs`, `gpo-backups`, `scan-runs`) · `assessments` · `findings` · `results` · `reviews` · `profiles` · `comparisons` · `cves` · `import-sessions`. Imports are **atomic sessions** (stage → commit → rollback). Errors always carry `code`, `message`, and `requestId`. The contract is in [`src/api/assessor-api-contract.ts`](../src/api/assessor-api-contract.ts); the server in [`server/assessor-http-server.ts`](../server/assessor-http-server.ts). --- ## 3. Repository layout ``` server/ Node backend assessor-http-server.ts HTTP router (/api/v1 + static app) index.ts process entry, wiring, env parsing postgres/ repository adapter + migrations/ gpo/ Group Policy backup ingestion & analysis cve/ NVD CVE enrichment (opt-in, backend-only) ai/ LiteLLM client + AI overview service regulatory/ ISO corpus, chunking, Qdrant index, RAG runtime, mapping artifacts/ filesystem artifact store src/ Browser app + platform-neutral core importers/ domain/ platform-neutral engine & canonical model profiles/ scanner profiles (see §5) recognition/ analysis/ comparison/ scoring/ evaluation/ persistence/ repository contract + LocalAssessorRepository platform/browser/ browser adapters (File, DOM, storage, download) api/ typed API client & contract features/ components/ styles/ React UI docs/ all detailed documentation (see §9) tests/ vitest suites + local-only & design-language checks deploy/nginx/ reverse-proxy vhosts for prod + staging .gitea/workflows/ CI/CD (deploy.yml) Dockerfile multi-stage build for the app image docker-compose.deploy.yml 3-service deployed stack (app+postgres+qdrant) docker-compose.postgres.yml local backing services (postgres+qdrant) ``` --- ## 4. Local development ### Prerequisites - Node.js 22 (matches the runtime image) and npm - Docker + Docker Compose (for PostgreSQL + Qdrant) ### First-time setup ```bash cp .env.postgres.example .env.postgres # edit .env.postgres: set long, local-only ASSESSOR_POSTGRES_PASSWORD and ASSESSOR_QDRANT_API_KEY npm ci npm run db:up # starts the canonical postgres + qdrant (project assessor-2_0) npm run dev # builds/starts backend on 127.0.0.1:4173, then Vite client on 127.0.0.1:5173 ``` `npm run dev` checks `/api/v1/health` first, brings up a missing backend, then opens the Vite client (which proxies `/api` to `4173`). The backend binds to `127.0.0.1` only — it is **not** exposed on the network in local mode. > The canonical local datastore is fixed: Compose project `assessor-2_0`, > database `assessor`, volume `assessor-2_0_assessor-postgres-data`, on > `127.0.0.1:5432`. Never create an alternative DB/volume/port for this > workspace (see [`AGENTS.md`](../AGENTS.md) → "Canonical local PostgreSQL"). If > port 5432 is taken by a different container, stop and report the conflict. ### Useful scripts | Command | What it does | |------------------------|-----------------------------------------------------------------| | `npm run dev` | Full local stack (backend + Vite client) | | `npm run dev:client` | Vite client only (backend managed separately) | | `npm run build` | Build client (`dist/`) and server (`dist-server/`) | | `npm run start:local` | Run the built server on 4173 using `.env.postgres` | | `npm run serve:local` | `build` then `start:local` | | `npm run db:up` / `db:down` / `db:logs` | Manage local postgres+qdrant | | `npm run rag:prepare-iso` / `rag:ingest` / `rag:evaluate` | Build/populate/evaluate the regulatory index | | `npm test` | vitest + local-only check + design-language check | Without `ASSESSOR_DATABASE_URL` the backend runs in an explicitly ephemeral in-memory mode. If the URL is set but the DB is unreachable, startup **fails** (no silent fallback), so a misconfiguration can never produce imports that look saved but are not. Details: [`docs/POSTGRESQL.md`](POSTGRESQL.md). --- ## 5. Supported scanner profiles Profiles live in [`src/profiles/`](../src/profiles/): `pingcastle`, `purple-knight`, `cis-cat`, `defender-for-iot`, `maester`, `group-policy`, `generic`, and `public-search` (regulatory). Each profile must declare its scoring semantics (`securityScoring`) and extend the generic import pipeline — see [`docs/TOOL_PROFILES.md`](TOOL_PROFILES.md) and the profile rules in [`AGENTS.md`](../AGENTS.md). Any change to recognition, parsing, mapping, identity, scoring inputs, or persisted evidence must follow [`docs/REIMPORT_POLICY.md`](REIMPORT_POLICY.md) (add a processing-manifest version and mark affected historical imports for reimport). --- ## 6. Deployment Deployment is fully driven by the Gitea workflow [`.gitea/workflows/deploy.yml`](../.gitea/workflows/deploy.yml). **You do not run `docker compose` by hand on the server for a normal release** — a push does it. ### How a release happens 1. Push to `main` (→ stable) or `experimental` (→ experimental). 2. The workflow loads that environment's complete env-file secret (`PROD_ENV_FILE` / `STAGING_ENV_FILE`) and validates required keys are present. 3. Runs `npm ci`, the full `npm test`, and `npm run build`. 4. Acquires a per-environment Docker lock (Gitea ignores the `concurrency` key), validates the resolved Compose config, and checks port/project isolation (only `app`/`postgres`/`qdrant` may exist; the target port must not be published by another project). 5. **Backs up** the existing environment (custom `pg_dump`, artifact tarball, `SHA256SUMS`) into the external volume `assessor_deployment_backups`. 6. `docker compose ... up -d --build --wait` for that environment only. 7. Verifies health via `/api/v1/health` inside the container, then releases the lock. The two environments never share PostgreSQL, artifacts, or the Qdrant index — Compose scopes all named volumes by project. `restart: unless-stopped` keeps both alive across daemon/host restarts. Feature promotion is a `experimental → main` merge; **databases and artifacts are never copied between environments.** ### Reverse proxy / TLS nginx terminates TLS and proxies to the VPN ports; vhosts are in [`deploy/nginx/assessor.conf`](../deploy/nginx/assessor.conf). `proxy_set_header Host $host` is mandatory — ASSESSOR rejects writes (403 `cross_origin_rejected`) if the forwarded host does not match the browser origin. `client_max_body_size` is 200m to allow ~100 MiB uploads (base64 overhead). ### Backups & restore The workflow writes a backup before every update and **never deletes backups**. Restore tests must use an isolated temporary Compose project and synthetic data, verify `SHA256SUMS`, and `pg_restore --exit-on-error` into a fresh DB. Never run `docker compose down --volumes` on a permanent environment. Full procedure: [`docs/DEPLOYMENT.md`](DEPLOYMENT.md) and [`docs/POSTGRESQL.md`](POSTGRESQL.md). ### Manual deploy (reference only) ```bash docker compose --env-file .env -p assessor_prod -f docker-compose.deploy.yml \ up -d --build --wait ``` --- ## 7. Configuration Real secret values live only in the git-ignored `.env.postgres` (workstation) or the deployment env-file secrets — never in the repo or in client-side config. [`.env.postgres.example`](../.env.postgres.example) is the annotated template. | Variable | Purpose | |----------------------------------|----------------------------------------------------------------| | `ASSESSOR_HOST` / `ASSESSOR_PORT`| Bind address / port (default `127.0.0.1:4173`) | | `ASSESSOR_DATABASE_URL` | PostgreSQL DSN; absent = ephemeral in-memory dev mode | | `ASSESSOR_DATABASE_SSL` | `disable` only for local loopback; TLS required for remote DBs | | `ASSESSOR_POSTGRES_*` | DB name/user/password (compose builds the DSN in deploy) | | `ASSESSOR_ARTIFACT_ROOT` | Artifact store path (must be outside the public web root) | | `ASSESSOR_QDRANT_URL` / `_API_KEY` | Regulatory index endpoint & key | | `ASSESSOR_RAG_EMBEDDING_*` | Approved embedding model for regulatory ingestion/retrieval | | `ASSESSOR_AI_MODE` (+ `ASSESSOR_AI_*`) | Enables the LiteLLM AI overview; unset = outbound AI off | | `NVD_API_KEY` / `ASSESSOR_NVD_TIMEOUT_MS` | Optional CVE enrichment against the fixed NVD endpoint | | `ASSESSOR_PARSER_*` / `ASSESSOR_GPO_*` | Import/GPO worker limits (timeout, memory, concurrency) | AI and CVE calls are the only outbound network paths, they are backend-only, opt-in, and off by default. Trust boundaries: [`docs/AI_OVERVIEW_SECURITY.md`](AI_OVERVIEW_SECURITY.md), [`docs/REGULATORY_RAG.md`](REGULATORY_RAG.md), [`docs/IMPORT_SECURITY_LIMITS.md`](IMPORT_SECURITY_LIMITS.md). --- ## 8. Data & persistence - **PostgreSQL 16** is the system of record: relational keys/foreign keys for identity plus the full versioned canonical object as JSONB (so new scanner fields are captured losslessly). Full-text `tsvector` + GIN indexes back search. - **Migrations** are versioned SQL files in [`server/postgres/migrations/`](../server/postgres/migrations/) (currently through `027_*`). On start the server takes an advisory lock, verifies each migration's version + SHA-256, applies new ones in their own transaction, and refuses to start if an already-applied migration was edited. Never edit an applied migration — add a new numbered file. - **Qdrant** holds the regulatory retrieval index. It is authenticated and **rebuildable** from the versioned corpus — its volume speeds restarts but is not a system-of-record backup. - **Artifacts** (e.g. GPO raw backups) go to a filesystem store on a dedicated volume, outside the public web root. Details: [`docs/POSTGRESQL.md`](POSTGRESQL.md), [`docs/REGULATORY_RAG.md`](REGULATORY_RAG.md), [`MIGRATION.md`](../MIGRATION.md). --- ## 9. Testing & verification ```bash npm test # vitest + local-only network check + design-language check npm run build # type-check + build client and server (CI gate) npm run test:postgres # integration tests against a real PostgreSQL 16 (ASSESSOR_TEST_DATABASE_URL) ``` Baseline verification for any relevant change is `npm test` + `npm run build` (both run in CI before deploy). **Testing is privacy-safe:** use synthetic fixtures; never read, log, screenshot, or quote real report content, file names, finding titles, or tenant data. Against a running app, verify only content-free signals (HTTP status, health, anonymized counts, boolean DOM state). See the "Privacy-safe testing" section of [`AGENTS.md`](../AGENTS.md) and [`docs/TEST_BASELINE.md`](TEST_BASELINE.md). --- ## 10. Security model (summary) - Local/same-origin by default: no CORS, write requests need `X-Assessor-Request` with a matching `Origin`; the backend binds loopback locally. - Imported HTML/XML/Excel/JSON/CSV are **untrusted input** — active content is never executed; HTML is inertized before table extraction; Excel formulas/VBA stay disabled. Parsers run in isolated workers with time/memory limits. - No external uploads, telemetry, or CDN assets. The only outbound paths are the opt-in backend AI (LiteLLM) and CVE enrichment (NVD). - Embedded report data is parsed with a bounded static grammar, never evaluated. Full model: [`SECURITY.md`](../SECURITY.md), [`docs/IMPORT_SECURITY_LIMITS.md`](IMPORT_SECURITY_LIMITS.md), [`docs/architecture/gpo-threat-model.md`](architecture/gpo-threat-model.md). --- ## 11. Documentation map **Start here → this overview.** Then, by topic: | Area | Documents | |------|-----------| | Rules for contributors | [`AGENTS.md`](../AGENTS.md) · [`CLAUDE.md`](../CLAUDE.md) | | Plan & history | [`docs/IMPLEMENTATION_PLAN.md`](IMPLEMENTATION_PLAN.md) · [`MIGRATION.md`](../MIGRATION.md) | | Architecture | [`docs/PLATFORM_NEUTRAL_ENGINE.md`](PLATFORM_NEUTRAL_ENGINE.md) · [`docs/PERSISTENCE_BOUNDARY.md`](PERSISTENCE_BOUNDARY.md) · [`docs/architecture/`](architecture/) | | Data model | [`docs/CANONICAL_MODEL.md`](CANONICAL_MODEL.md) · [`docs/UNIFIED_RESULT_MODEL.md`](UNIFIED_RESULT_MODEL.md) · [`docs/RESULT_CONTRACT.md`](RESULT_CONTRACT.md) · [`docs/SCORING_MODEL.md`](SCORING_MODEL.md) | | Import | [`docs/IMPORT_WORKFLOW.md`](IMPORT_WORKFLOW.md) · [`docs/IMPORT_INTERMEDIATE_MODEL.md`](IMPORT_INTERMEDIATE_MODEL.md) · [`docs/RECOGNITION_ENGINE.md`](RECOGNITION_ENGINE.md) · [`docs/TOOL_PROFILES.md`](TOOL_PROFILES.md) · [`docs/REIMPORT_POLICY.md`](REIMPORT_POLICY.md) | | Compare & filter | [`docs/COMPARISON_ENGINE.md`](COMPARISON_ENGINE.md) · [`docs/DYNAMIC_FILTERS.md`](DYNAMIC_FILTERS.md) | | Backend & persistence | [`docs/LOCAL_BACKEND.md`](LOCAL_BACKEND.md) · [`docs/POSTGRESQL.md`](POSTGRESQL.md) | | Deployment | [`docs/DEPLOYMENT.md`](DEPLOYMENT.md) · [`deploy/nginx/assessor.conf`](../deploy/nginx/assessor.conf) | | AI & regulatory | [`docs/AI_OVERVIEW_SECURITY.md`](AI_OVERVIEW_SECURITY.md) · [`docs/LLM_DASHBOARD_PROMPT.md`](LLM_DASHBOARD_PROMPT.md) · [`docs/REGULATORY_RAG.md`](REGULATORY_RAG.md) | | Security & testing | [`SECURITY.md`](../SECURITY.md) · [`docs/IMPORT_SECURITY_LIMITS.md`](IMPORT_SECURITY_LIMITS.md) · [`docs/TEST_BASELINE.md`](TEST_BASELINE.md) · [`docs/USER_FLOW_CHECKS.md`](USER_FLOW_CHECKS.md) |