Three-service Docker Compose stack with PostgreSQL persistence.
| Service | Port | Description |
|---|---|---|
postgres |
5432 | PostgreSQL 16 — persistent storage |
backend |
4000 | Go SCA scanner + REST API + background worker |
frontend |
8085 | React + nginx |
cp .env.example .env
# Edit .env — add GITHUB_TOKEN, NVD_API_KEY at minimum
docker compose up --buildThe first docker compose build downloads the offline OSV, NVD, EPSS, KEV, PoC,
malware and Grype corpus inside the backend image. That is the one long build —
expect tens of minutes and several GB. Docker caches those layers, so later
rebuilds only recompile the Go server and the React application:
docker compose build backend frontend
docker compose up -dTo refresh the vulnerability corpus itself, invalidate its layers by rebuilding without cache:
docker compose build --no-cache backendFirst boot creates a default admin / admin account (forced password change
on first login). Set WOLFEE_ADMIN_PASSWORD to pick the initial password
instead.
- UI sessions — JWT via
POST /api/v1/auth/login, stored in an HttpOnly cookie. Browser mutations also require the CSRF cookie/header pair. TTL isWOLFEE_JWT_TTL(default 8h). Bearer tokens remain accepted for API clients. - CI / automation — team-scoped API keys (
wsca_...) created in Settings → API Keys, sent asX-Api-Key. A key inherits its team's permissions; projects it creates are auto-attached to that team. - Permissions (Dependency-Track style):
VIEW_PORTFOLIO,PORTFOLIO_MANAGEMENT,BOM_UPLOAD,VIEW_VULNERABILITY,VULNERABILITY_ANALYSIS,POLICY_MANAGEMENT,ACCESS_MANAGEMENT,SYSTEM_CONFIGURATION— granted to users and teams; effective set is the union. - Portfolio ACL — teams see only projects granted to them
(Settings → Teams → Project access). Principals with
ACCESS_MANAGEMENTbypass the ACL. - LDAP — optional second identity provider (search+bind), configured via
WOLFEE_LDAP_*env vars; users are auto-provisioned on first login with no permissions. - Deprecated: the global
API_KEYenv var still works as a superuser key so existing CI keeps running — migrate to team API keys and remove it.
PUT /api/v1/project create project
GET /api/v1/project list all projects
GET /api/v1/project/{uuid} get one project
DELETE /api/v1/project/{uuid} delete project
GET /api/v1/project/{uuid}/branches list branches
POST /api/v1/project/{uuid}/branches create branch: {"name":"feature/api"}
Every project gets a default main branch. Branch creation is API-only; the
project page exposes a branch switcher but no create action.
POST /api/v1/bom multipart: project=<uuid>, bom=<file.json>
→ 200 { token, branchUuid }
Processing happens in the background worker. Poll /api/v1/bom/token/{token} for status.
Pass branch=<name-or-uuid> to upload into a branch. upload=true is the
CI-friendly alias for autoCreate=true: if a named project and/or branch does
not exist, the upload request creates it before queueing the SBOM.
curl -X POST http://localhost:4000/api/v1/bom \
-H "X-Api-Key: $WOLFEE_API_KEY" \
-F project=my-service \
-F branch=feature/payment-refactor \
-F upload=true \
-F bom=@bom.jsonJSON clients can send the equivalent payload with a base64-encoded BOM:
{
"projectName": "my-service",
"branch": "feature/payment-refactor",
"upload": true,
"bom": "<base64 CycloneDX JSON>"
}Branch-aware reads and rescans accept ?branch=<name-or-uuid>. Without it,
the API uses the project's default branch, preserving existing integrations.
WORKER_CONCURRENCY controls how many BOM scans one backend processes in
parallel. The default is 3; the backend enforces a hard maximum of 4.
GET /api/v1/finding/project/{uuid}?branch=<name-or-uuid> findings for the branch's active BOM
GET /api/v1/vulnerability/project/{uuid}?branch=<name-or-uuid> unique vulnerabilities for the branch
GET /api/v1/component/project/{uuid}?branch=<name-or-uuid> components for the branch
GET /api/v1/metrics/project/{uuid}?branch=<name-or-uuid> severity and audit counters for the branch
GET /api/v1/quality-gate/project/{uuid}?branch=<name-or-uuid> pass/block result for the branch
The quality-gate response is evaluated from the active BOM of only the requested branch. It includes the blocking findings so CI can explain a failure without fetching the whole project:
{
"projectUuid": "...",
"branch": { "uuid": "...", "name": "feature/payment-refactor" },
"bomUuid": "...",
"gate": "block",
"pipeline": "failed",
"findingsTotal": 12,
"blockingFindings": 2,
"critical": 1,
"high": 4,
"findings": [
{
"branchUuid": "...", "branchName": "feature/payment-refactor",
"vulnerabilityId": "CVE-...", "severity": "CRITICAL", "cvss": 9.8,
"componentName": "...", "componentVersion": "...",
"gate": "block", "policyAction": "block", "policyWhy": "critical threshold exceeded"
}
]
}findings contains every finding from the branch's active BOM, ordered by severity (CRITICAL to LOW) and then by descending CVSS score.
Example CI gate (requires jq):
gate_json=$(curl -fsS --get \
-H "X-Api-Key: $WOLFEE_API_KEY" \
--data-urlencode "branch=$CI_COMMIT_REF_NAME" \
"http://localhost:4000/api/v1/quality-gate/project/$WOLFEE_PROJECT_UUID")
echo "$gate_json" | jq .
test "$(echo "$gate_json" | jq -r .gate)" = "pass"GET /api/v1/finding/project/{uuid}/export?format=sarif SARIF 2.1.0
GET /api/v1/finding/project/{uuid}/export?format=cyclonedx-vex CycloneDX VEX
GET /api/rules current rules config
PUT /api/rules replace rules config
POST /api/scan single-package scan with rules evaluation
cmd/server/main.go entry point, DI wiring
internal/
config/ env-based config
domain/ entities: Project, BOM, Component, Finding, Job, Rules
repository/ interfaces + memory.go + postgres.go + migrate.go
service/ use cases (business logic)
worker/ background job processor
http/ thin HTTP handlers
modules/ vulnerability data sources (OSV, NVD, EPSS, KEV…)
scanner/ scan pipeline
migrations/ SQL migration files (auto-applied at startup)
- With DATABASE_URL: PostgreSQL — data persists across restarts
- Without DATABASE_URL: in-memory — zero setup, data lost on restart
See .env.example for all options.
Things the platform deliberately does not do yet, so you can plan around them instead of discovering them mid-triage.
Operating-system packages are scanned through the image endpoint, not the SBOM
one. pkg:deb, pkg:rpm, pkg:apk and pkg:alpm components uploaded to
/api/v1/bom are recorded but not resolved — the dependency scanner has no
distro-aware backport logic, and reporting a backported package as vulnerable is
worse than not reporting it. Upload a container report instead; the scan summary
tells you how many system packages it saw (systemComponents).
The CLI cannot push container reports. wolfee scan --image --server …
uploads nothing; image reports reach the platform through the UI only. Language
dependencies do upload, enriched.
Findings inside an uploaded document are trusted wholesale. If any component
in the document carries vulnerabilities, the server takes the document's findings
as authoritative and does not scan any component itself (scanMode: sbom in the
summary). A partially enriched document therefore reports its unenriched
components as clean. Documents without findings are scanned normally
(scanMode: scanner).
Grype is disabled by default. WOLFEE_GRYPE_HELPER=false — the binary and
its database are baked into the image, but running it on every scan costs time,
so enabling it is an explicit choice.
Suppressions are project-wide by vulnerability ID. Triaging a CVE silences it for the whole project, not for one package.
The portfolio report covers default branches. /api/v1/quality-gate returns
one row per project, evaluated on its default branch; other branches are
available per project at /api/v1/quality-gate/project/<uuid>?branch=<name>.
See DEPLOY.md — TLS/ingress, secrets, rate limiting, BOM retention, backup/restore, upgrades, and a production checklist.
The first docker compose build bakes the offline vulnerability corpus into the
backend image and takes a long time. Later rebuilds reuse those cached layers
and only recompile the Go server and the React application:
docker compose build backend frontendRefresh the corpus with docker compose build --no-cache backend.
