Assessor_2.0/docs/PROJECT_OVERVIEW.md

375 lines
21 KiB
Markdown

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