Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 59 additions & 21 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Multi-stage Dockerfile for no-package-malware
# Supports both microservice and monolith modes via SERVICE_MODE
# Builds artifacts once, then produces three focused, rootless runtime images:
# - api-runtime: unified API (services/api)
# - app-runtime: unified SPA served by nginx-unprivileged (services/app)
# - registry-runtime: Verdaccio registry (strict/lenient)

# --------- Builder stage ---------
FROM node:24-bullseye-slim AS builder
Expand All @@ -15,44 +18,79 @@ COPY secure-registry/tsconfig.base.json secure-registry/tsconfig.base.json

# Copy workspace package manifests (without full src yet) to resolve deps
COPY secure-registry/plugins ./secure-registry/plugins
COPY secure-registry/services ./secure-registry/services
COPY apps ./apps
COPY services ./services

# Install dependencies for all workspaces
RUN pnpm install --frozen-lockfile

# Copy the rest of the repo (src, configs, docs)
COPY . .

# Build all workspaces (plugins, services, apps)
# Build all workspaces (plugins, services, app)
RUN pnpm -r build

# --------- Runtime stage ---------
FROM node:24-bullseye-slim AS runtime

# --------- Unified API runtime (Node, rootless) ---------
FROM node:24-bullseye-slim AS api-runtime

WORKDIR /app

# Install a tiny init system to handle signals properly
RUN apt-get update && apt-get install -y --no-install-recommends tini \
&& rm -rf /var/lib/apt/lists/*
ENV NODE_ENV=production \
PNPM_HOME=/usr/local/share/pnpm \
PATH="/app/node_modules/.bin:$PNPM_HOME:$PATH"

# Copy built repo and dependencies from builder
COPY --from=builder /app /app

# Create unprivileged user with numeric UID for Kubernetes policies
RUN useradd -u 1000 -r -s /usr/sbin/nologin appuser && \
chown -R 1000:1000 /app

USER 1000

WORKDIR /app/services/api

EXPOSE 4000

CMD ["node", "dist/index.js"]


# --------- Unified SPA runtime (nginx-unprivileged, rootless) ---------
FROM nginxinc/nginx-unprivileged:stable-alpine AS app-runtime

# Ensure numeric USER is declared explicitly for policy checks
USER 101

# Copy built SPA assets
COPY --from=builder /app/services/app/dist /usr/share/nginx/html

# Custom nginx config with SPA routing
COPY services/app/nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80


# --------- Verdaccio registry runtime (strict/lenient, rootless) ---------
FROM node:24-bullseye-slim AS registry-runtime

WORKDIR /app

ENV NODE_ENV=production \
PNPM_HOME=/usr/local/share/pnpm \
PATH="/app/node_modules/.bin:$PNPM_HOME:$PATH"

# Copy node_modules and built artifacts
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json /app/pnpm-workspace.yaml /app/pnpm-lock.yaml ./
COPY --from=builder /app/secure-registry ./secure-registry
COPY --from=builder /app/apps ./apps
# Copy built repo and dependencies from builder (includes Verdaccio plugins)
COPY --from=builder /app /app

# Create unprivileged user with numeric UID for Kubernetes policies
RUN useradd -u 1001 -r -s /usr/sbin/nologin registry && \
chown -R 1001:1001 /app

# Copy Procfile-based runtime supervisor config (will be added in repo)
COPY Procfile.runtime ./Procfile.runtime
USER 1001

# Copy entrypoint script
COPY docker-entrypoint.sh ./docker-entrypoint.sh
RUN chmod +x ./docker-entrypoint.sh
EXPOSE 4873

EXPOSE 4000 4100 4873 4874 4875 4173 4174
# REGISTRY_MODE determines which Verdaccio config to use: "strict" or "lenient"
ENV REGISTRY_MODE=strict

ENTRYPOINT ["/usr/bin/tini", "--", "./docker-entrypoint.sh"]
CMD ["sh", "-c", "if [ \"$REGISTRY_MODE\" = \"lenient\" ]; then CONFIG=secure-registry/verdaccio/lenient/config.docker.yaml; else CONFIG=secure-registry/verdaccio/strict/config.docker.yaml; fi; npx verdaccio --config \"$CONFIG\" --listen 0.0.0.0:4873"]
8 changes: 2 additions & 6 deletions Procfile.dev
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
api: cd secure-registry/services/security-decision-api && pnpm dev
workers: cd secure-registry/services/security-workers && TARBALL_REGISTRY_URL=http://localhost:4875 pnpm dev
api: cd services/api && pnpm dev
app: cd services/app && pnpm dev
verdaccio-strict: pnpm dev:verdaccio:strict
verdaccio-lenient: pnpm dev:verdaccio:lenient
verdaccio-malware: pnpm dev:verdaccio:malware
access-admin-api: cd secure-registry/services/access-admin-api && pnpm dev
security-dashboard: cd apps/security-dashboard && pnpm dev
access-admin-dashboard: cd apps/access-admin-dashboard && pnpm dev
request-access-portal: cd apps/request-access-portal && pnpm dev
6 changes: 0 additions & 6 deletions Procfile.runtime

This file was deleted.

142 changes: 94 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,61 @@ This repository implements a secure npm registry built on Verdaccio, acting as a
- Token auth plugin: [`index.ts`](secure-registry/plugins/verdaccio-security-token-auth/src/index.ts:1)
- HTTP rate limiting middleware: [`index.ts`](secure-registry/plugins/verdaccio-rate-limit-middleware/src/index.ts:1)
- **Services**
- Security decision API: [`index.ts`](secure-registry/services/security-decision-api/src/index.ts:1)
- Security workers: [`index.ts`](secure-registry/services/security-workers/src/index.ts:1)
- Unified API service: [`index.ts`](services/api/src/index.ts:1)
- Background workers (integrated into unified API): [`workers.ts`](services/api/src/workers.ts:1)
- **Infra**
- MongoDB and Redis via [`docker-compose.yml`](secure-registry/infra/docker-compose.yml:1)

## HTTP API endpoints

All HTTP APIs are now served by the **unified API service** in `services/api`.

- **Base URL (dev & Docker):** `http://localhost:4000`

### Security decision API

- `POST /api/security/bulk-report/:name`
- Given a package name and list of versions, returns a map of versions to security reports (status, riskScore, riskLevel, summary, lastScanAt).
- `GET /api/security/report/:name/:version`
- Return a single security report for one package version.
- `POST /api/security/scan-download/:name/:version`
- Trigger (or wait on) a security scan for the given version, enforcing scan budgets and audit rate limits.
- `GET /api/security/reports`
- Paginated list endpoint used by the security dashboard.

### Token validation API

- `POST /api/auth/token/validate`
- Validate a raw npm auth token issued by the access-admin API and map it to an organization.

### Access request & admin APIs

- `POST /api/access-requests`
- Public endpoint for creating an access request (email + optional orgName).
- `GET /request-access`
- HTTP redirect to the request-access portal SPA (`ACCESS_REQUEST_PORTAL_URL`).
- `POST /api/admin/login`
- Admin login (default dev credentials: `admin / admin`). Returns a short-lived admin bearer token.
- `GET /api/admin/access-requests`
- List access requests with filters (`status`, `page`, `pageSize`) and token usage stats.
- `POST /api/admin/access-requests/:id/approve`
- Approve an access request, creating an org + token and returning the raw token.
- `POST /api/admin/access-requests/:id/reject`
- Reject an access request.
- `POST /api/admin/tokens/:id/audit-rate-limit`
- Configure per-token rate limits for new package audits.

### Health endpoints

- `GET /health/live` – process liveness for the unified API.
- `GET /health/ready` – readiness, aggregating MongoDB, Redis, access-admin API, and workers health.
- `GET /health` – backwards-compatible readiness alias.

Workers also expose their own health server on `http://localhost:4101` (inside the cluster or docker-compose network):

- `GET /health/live`
- `GET /health/ready`

## High level flow

1. Client (npm/yarn/pnpm) installs from Verdaccio strict or lenient.
Expand Down Expand Up @@ -79,7 +129,7 @@ The lenient instance has the same structure but uses `policyId: "lenient"` and c

## Tokens, orgs and policies

The decision API models organizations and their policies in Mongo via [`db.ts`](secure-registry/services/security-decision-api/src/db.ts:1):
The unified API models organizations and their policies in Mongo via [`db.ts`](services/api/src/db.ts:1):

- `OrgDoc` – organizations.
- `TokenDoc` – API tokens (stored as SHA‑256 hashes), each linked to an org.
Expand All @@ -91,7 +141,7 @@ The decision API models organizations and their policies in Mongo via [`db.ts`](

### Token validation API

Token validation is implemented in [`index.ts`](secure-registry/services/security-decision-api/src/index.ts:310) as `POST /api/auth/token/validate`.
Token validation is implemented in [`index.ts`](services/api/src/index.ts:1) as `POST /api/auth/token/validate`.

- Request:

Expand All @@ -110,7 +160,7 @@ The Verdaccio auth plugin [`index.ts`](secure-registry/plugins/verdaccio-securit

### Policy resolution

For every request, the decision API resolves a `PolicyContext` in [`index.ts`](secure-registry/services/security-decision-api/src/index.ts:121):
For every request, the unified API resolves a `PolicyContext` in [`index.ts`](services/api/src/index.ts:1):

- Reads `X-Security-Policy` (`strict` or `lenient`).
- Optionally reads `X-Security-Token`.
Expand All @@ -122,7 +172,7 @@ For every request, the decision API resolves a `PolicyContext` in [`index.ts`](s

### Risk to status mapping

`mapRiskToStatusForPolicy` in [`index.ts`](secure-registry/services/security-decision-api/src/index.ts:157) takes:
`mapRiskToStatusForPolicy` in [`index.ts`](services/api/src/index.ts:1) takes:

- Stored `risk_score` and `status` from `security_reports`.
- The active `EffectivePolicy`.
Expand All @@ -138,7 +188,7 @@ It returns one of:

Per org scan budgets are enforced when the decision API sees new versions in `POST /api/security/bulk-report/:name`:

- Budget logic is implemented in [`budget.ts`](secure-registry/services/security-decision-api/src/budget.ts:1).
- Budget logic is implemented in [`budget.ts`](services/api/src/budget.ts:1).
- If `max_scans_per_day` on the selected `SecurityPolicyDoc` is set, a daily counter `budget:scan:<orgId>:<yyyymmdd>` in Redis is incremented.
- When the count exceeds the limit:
- The new version is given an `ERROR` status with summary `Scan budget exceeded for this org today.`
Expand All @@ -156,11 +206,11 @@ The same budget module provides `checkAndConsumeGptBudget`, intended to be used
- Workers skip the GPT call.
- They rely solely on heuristics and record in `details` that GPT was skipped due to budget exhaustion.

The decision API already enqueues scan jobs with `orgId` and `policyId` in the job payload via [`queue.ts`](secure-registry/services/security-decision-api/src/queue.ts:4), so workers can attribute GPT usage to an org and policy.
The unified API already enqueues scan jobs with `orgId` and `policyId` in the job payload via [`queue.ts`](services/api/src/queue.ts:1), so workers can attribute GPT usage to an org and policy.

## Security workers

Workers are implemented in [`index.ts`](secure-registry/services/security-workers/src/index.ts:1) and GPT logic in [`llm.ts`](secure-registry/services/security-workers/src/llm.ts:1).
Workers are implemented in [`workers.ts`](services/api/src/workers.ts:1) and GPT logic in [`llm.ts`](services/api/src/llm.ts:1).

Responsibilities:

Expand Down Expand Up @@ -193,56 +243,51 @@ The rate limiting middleware plugin is implemented in [`index.ts`](secure-regist

Strict and lenient instances can use different `maxRequests` values to enforce different HTTP traffic limits.

## Docker deployment (microservices vs monolith)
## Docker deployment (unified services)

You can build a single Docker image and run it either as separate microservices or as a monolith (all app services in one container), while MongoDB and Redis remain separate containers.
The deployment model is now:

### Build the image
- **API** – unified decision + access-admin + workers (`services/api`), one container.
- **APP** – unified SPA (`services/app`) built with Vite and served by nginx-unprivileged.
- **Registry** – two Verdaccio instances (`strict` and `lenient`) backed by the unified API and Redis.

From the repo root:
MongoDB and Redis remain separate containers.

```bash
# Build the multi-stage image used by both modes
docker build -t no-package-malware-app .
```
### Build the images

### Microservices mode (multiple app containers)

This mode runs each service in its own container (decision API, workers, access-admin-api, Verdaccio strict/lenient/malware), plus MongoDB and Redis.
From the repo root, use the multi-stage `Dockerfile` to build three images:

```bash
cd secure-registry/infra
# Start Mongo, Redis, decision-api, workers, access-admin-api, Verdaccio strict/lenient/malware
docker compose -f docker-compose.app.yml up
```
# Unified API (Node, rootless USER 1000)
docker build -t no-package-malware-api --target api-runtime .

Available endpoints:
# Unified SPA (nginx-unprivileged, rootless USER 101)
docker build -t no-package-malware-app --target app-runtime .

- Decision API: `http://localhost:4000`
- Access admin API: `http://localhost:4100`
- Verdaccio strict gate: `http://localhost:4873`
- Verdaccio lenient gate: `http://localhost:4874`
- Verdaccio malware test registry: `http://localhost:4875`
# Verdaccio registry (strict/lenient, rootless USER 1001)
docker build -t no-package-malware-registry --target registry-runtime .
```

### Monolith mode (single app container)
### Run the stack with docker-compose

This mode runs all Node/JS app services (decision-api, workers, access-admin-api, Verdaccio strict/lenient/malware) in **one container**, with Mongo and Redis as separate containers.
A single compose file defines the full stack (Mongo, Redis, API, APP, strict & lenient registries):

```bash
cd secure-registry/infra
# Start Mongo, Redis and a single app container running all services
docker compose -f docker-compose.monolith.yml up
# Start Mongo, Redis, unified API, unified SPA and strict/lenient registries
docker compose -f docker-compose.app.yml up
```

Endpoints are the same as above:
Available endpoints:

- Decision API: `http://localhost:4000`
- Access admin API: `http://localhost:4100`
- Unified API: `http://localhost:4000`
- Unified SPA: `http://localhost:4173`
- Verdaccio strict gate: `http://localhost:4873`
- Verdaccio lenient gate: `http://localhost:4874`
- Verdaccio malware test registry: `http://localhost:4875`

You can still use the existing local dev workflow described below.
All application containers are rootless and declare an explicit numeric `USER` so Kubernetes and platform policies can enforce non-root execution.

You can still use the local dev workflow described below for iterative development.

## Running locally (high level)

Expand All @@ -264,15 +309,16 @@ pnpm dev
This will:

- Start MongoDB and Redis via [`docker-compose.yml`](secure-registry/infra/docker-compose.yml:1).
- Run the security decision API in dev mode on `http://localhost:4000` using [`index.ts`](secure-registry/services/security-decision-api/src/index.ts:1).
- Run the security workers consuming the `security-scan` queue using [`index.ts`](secure-registry/services/security-workers/src/index.ts:1).
- Run the **unified API** (security decision + access-admin + workers) in dev mode on `http://localhost:4000` using [`services/api`](services/api/src/index.ts).
- Start two Verdaccio instances:
- Strict gate at `http://localhost:${VERDACCIO_STRICT_PORT:-4873}` using [`config.yaml`](secure-registry/verdaccio/strict/config.yaml:1).
- Lenient gate at `http://localhost:${VERDACCIO_LENIENT_PORT:-4874}` using [`config.yaml`](secure-registry/verdaccio/lenient/config.yaml:1).
- Start the web dashboards:
- Security reports dashboard at `http://localhost:5174`.
- Access admin dashboard at `http://localhost:5175`.
- Request access portal at `http://localhost:5173` (public form for creating access requests).
- Start the **unified web app** in [`services/app`](services/app) with Vite dev server on `http://localhost:5174`.

The same SPA will expose:
- `http://localhost:5174/dashboard/security` – security reports dashboard
- `http://localhost:5174/dashboard/access-admin` – access admin dashboard (once routing is fully wired)
- `http://localhost:5174/request-access` – public request access portal

The access admin API also exposes `/request-access`, which now redirects to the request access portal. The target URL is configured via the `ACCESS_REQUEST_PORTAL_URL` environment variable (see `.env.example`).

Expand Down Expand Up @@ -316,7 +362,7 @@ To seed demo data (org, token, strict/lenient policies) you can run:
task seed
```

which delegates to the decision API seeding script defined in [`package.json`](secure-registry/services/security-decision-api/package.json:7).
which delegates to the unified API seeding script defined in [`package.json`](services/api/package.json:1).

You can still use the raw `pnpm dev` and `pnpm --filter ... seed` commands directly if you prefer.

Expand Down Expand Up @@ -412,8 +458,8 @@ You can point the consumer app to the lenient registry instead by adjusting `.np
## Further reading

- Functional and architectural guide: [`guide_implementation_registry_securisee_verdaccio_gpt_5.md`](docs/guide_implementation_registry_securisee_verdaccio_gpt_5.md:1)
- Security decision API implementation: [`index.ts`](secure-registry/services/security-decision-api/src/index.ts:1)
- Workers and GPT integration: [`index.ts`](secure-registry/services/security-workers/src/index.ts:1), [`llm.ts`](secure-registry/services/security-workers/src/llm.ts:1)
- Unified API implementation: [`index.ts`](services/api/src/index.ts:1)
- Workers and GPT integration: [`workers.ts`](services/api/src/workers.ts:1), [`llm.ts`](services/api/src/llm.ts:1)
- Verdaccio filter plugin: [`index.ts`](secure-registry/plugins/verdaccio-security-gate-filter/src/index.ts:1)
- Auth plugin: [`index.ts`](secure-registry/plugins/verdaccio-security-token-auth/src/index.ts:1)
- HTTP rate limiting middleware: [`index.ts`](secure-registry/plugins/verdaccio-rate-limit-middleware/src/index.ts:1)
12 changes: 12 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
- We want to remove all that monolithic/microservices things, we'll keep only a deployment way with 2 separated services app and api, and a npm registry service (with both registry lenient and strict)

- We want to serve all spa using nginx unprivileged images

- We should ensure that all docker images are rootless and have a USER instruction followed by number (so kubernetes policies can detect that is rootless and let is pass)

- Deploy and test deployed (no-package-malware.fabrique.social.gouv.fr)
- kaizo

- change initial limit to 1000 new audit per 24h

- test on domifa
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ tasks:
seed:
desc: Seed demo org, token and strict/lenient policies
cmds:
- pnpm --filter @secure-registry/security-decision-api seed
- pnpm --filter @secure-registry/api seed
Loading