375 lines
21 KiB
Markdown
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
|
|
`026_*`). 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) |
|