21 KiB
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; binding rules for anyone (human or AI) changing the code are inAGENTS.md.
1. What runs where — the short answer
There are three long-running services, defined in
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), 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.
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— engine/adapter layeringdocs/PERSISTENCE_BOUNDARY.md— theAssessorRepositorycontract and import transactiondocs/CANONICAL_MODEL.md— the normalized data modeldocs/RECOGNITION_ENGINE.md— profile selectiondocs/UNIFIED_RESULT_MODEL.md/docs/RESULT_CONTRACT.md— the read model behind the UIdocs/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):
- Serves the built React SPA from
dist/. - Serves the versioned JSON API under
/api/v1. - 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 and 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; the server
in 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
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, databaseassessor, volumeassessor-2_0_assessor-postgres-data, on127.0.0.1:5432. Never create an alternative DB/volume/port for this workspace (seeAGENTS.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.
5. Supported scanner profiles
Profiles live in 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 and the profile rules in
AGENTS.md.
Any change to recognition, parsing, mapping, identity, scoring inputs, or
persisted evidence must follow docs/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. You do not run
docker compose by hand on the server for a normal release — a push does it.
How a release happens
- Push to
main(→ stable) orexperimental(→ experimental). - The workflow loads that environment's complete env-file secret
(
PROD_ENV_FILE/STAGING_ENV_FILE) and validates required keys are present. - Runs
npm ci, the fullnpm test, andnpm run build. - Acquires a per-environment Docker lock (Gitea ignores the
concurrencykey), validates the resolved Compose config, and checks port/project isolation (onlyapp/postgres/qdrantmay exist; the target port must not be published by another project). - Backs up the existing environment (custom
pg_dump, artifact tarball,SHA256SUMS) into the external volumeassessor_deployment_backups. docker compose ... up -d --build --waitfor that environment only.- Verifies health via
/api/v1/healthinside 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. 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 and
docs/POSTGRESQL.md.
Manual deploy (reference only)
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 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,
docs/REGULATORY_RAG.md,
docs/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/(currently through027_*). 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,
docs/REGULATORY_RAG.md, MIGRATION.md.
9. Testing & verification
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 and
docs/TEST_BASELINE.md.
10. Security model (summary)
- Local/same-origin by default: no CORS, write requests need
X-Assessor-Requestwith a matchingOrigin; 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, docs/IMPORT_SECURITY_LIMITS.md,
docs/architecture/gpo-threat-model.md.
11. Documentation map
Start here → this overview. New to ASSESSOR? Read the illustrated
docs/ONBOARDING.md first. Then, by topic:
| Area | Documents |
|---|---|
| Rules for contributors | AGENTS.md · CLAUDE.md |
| Plan & history | docs/IMPLEMENTATION_PLAN.md · MIGRATION.md |
| Architecture | docs/PLATFORM_NEUTRAL_ENGINE.md · docs/PERSISTENCE_BOUNDARY.md · docs/architecture/ |
| Data model | docs/CANONICAL_MODEL.md · docs/UNIFIED_RESULT_MODEL.md · docs/RESULT_CONTRACT.md · docs/SCORING_MODEL.md |
| Import | docs/IMPORT_WORKFLOW.md · docs/IMPORT_INTERMEDIATE_MODEL.md · docs/RECOGNITION_ENGINE.md · docs/TOOL_PROFILES.md · docs/REIMPORT_POLICY.md |
| Compare & filter | docs/COMPARISON_ENGINE.md · docs/DYNAMIC_FILTERS.md |
| Backend & persistence | docs/LOCAL_BACKEND.md · docs/POSTGRESQL.md |
| Deployment | docs/DEPLOYMENT.md · deploy/nginx/assessor.conf |
| AI & regulatory | docs/AI_OVERVIEW_SECURITY.md · docs/LLM_DASHBOARD_PROMPT.md · docs/REGULATORY_RAG.md |
| Security & testing | SECURITY.md · docs/IMPORT_SECURITY_LIMITS.md · docs/TEST_BASELINE.md · docs/USER_FLOW_CHECKS.md |