The rebrand renamed many Tailwind color classes vulncheck-blue → truevuln-blue
but left the theme token as --color-vulncheck-blue, so those 115 classes
referenced an undefined color → transparent backgrounds / unstyled text
(tester: the "Save Template" button was invisible until hover; also affected
buttons, links, focus rings, sort arrows app-wide).
Unified everything on truevuln-blue: renamed the @theme token to
--color-truevuln-blue and the remaining 156 vulncheck-blue class usages to
truevuln-blue. Now all 271 usages resolve to one defined token; 0 vulncheck-blue
left.
Renames the product name in every user-visible surface and internal
self-reference: page title, nav/shell, login/MFA pages, email templates and
subject prefixes ([VULNCHECK] → [TRUEVULN]), TOTP issuer label, report/PDF
headers, notification previews, outbound User-Agent/HTTP-Referer headers we
set ourselves, docs (README, ARCHITECTURE, PROJECT_OVERVIEW, DATABASE_SCHEMA,
README.DEV, TROUBLESHOOTING is untouched — see below), and .env.example
placeholder config (LDAP/OIDC/SAML example domains and paths).
Also renamed the on-disk cache file paths (/tmp/vulncheck-*.zip|csv|json →
/tmp/truevuln-*), kept consistent across the two files that share the
cvelistV5 ZIP cache path — first run after deploy re-downloads that ~557 MB
cache once (harmless, disposable).
Deliberately LEFT UNCHANGED (not branding — real external references or
infra identifiers; renaming the text without renaming the underlying thing
would just break/mislead):
- The actual Gitea repo URL/path (gitea.isuit.ch/vulncheck/vulncheck) and the
README lines derived from it (git clone target dir, tree listing) — a real
repo rename is a manual Gitea-side step (Settings → repository name) the
user would need to do themselves, and existing clones would need
`git remote set-url` after.
- The real support mailbox (support-vulncheck.sq9vd@passmail.net, in both
README and TROUBLESHOOTING) and the Buy Me A Coffee link — both point to
accounts that still exist under the old name; renaming the text alone
wouldn't create new ones.
- GitNexus MCP resource URIs in CLAUDE.md/AGENTS.md (gitnexus://repo/
vulncheck/...) — tied to GitNexus's own index name for this repo, not our
branding; those files are untracked in this repo anyway.
- docker-compose.yml container/network/Postgres user+db names
(vulnmanager-*) — explicit user decision: infra naming carries real
deploy/data risk on an already-running instance and isn't part of the
product-branding ask.
- The Tailwind color token class `vulncheck-blue` (frontend/app/globals.css)
— invisible internal CSS variable name, renaming it would touch ~270
className occurrences for zero user-visible benefit.
Verified: backend py_compile clean on every touched .py file; frontend tsc
clean (two pre-existing, unrelated errors remain: assets/page.tsx SVG title
prop, mfa-setup missing qrcode.react types). All diffs are exact-string
renames — no other changes riding along.
The /auth/[...path]/route.ts catch-all in the Next.js frontend
proxies *every* /auth/* request to the backend, including the
forced-mfa-setup page route. Backend has no GET endpoint for that
path → 404 → page never serves → user sees blank or bounces back
to /login.
Move the page to /mfa-setup (outside /auth/) and update redirects:
- login page.tsx redirect
- OIDC + SAML callback redirects
Settings → Security card lets admins require TOTP for selected roles
(admin/editor by default). Affected local users hit a dedicated
forced-enrolment page at next login with no session issued until they
scan the QR and submit a valid code. SSO/LDAP users exempt by default;
opt-in via mfa_enforce_sso_users=true redirects them to the same page
after the IdP callback.
Backend
- app/auth/mfa_policy.py: reads mfa_enforced / mfa_enforced_roles /
mfa_enforce_sso_users from the settings KV. Fail-open on any DB
hiccup to avoid lockouts.
- AuthResult gains mfa_setup_required (mutually exclusive with
mfa_required). Orchestrator sets it in both credential and SSO paths.
- /auth/login short-circuits to a setup-token (10 min TTL) before any
session cookies are issued.
- New endpoints POST /auth/mfa/forced-setup/{start,activate}.
Activate issues a full session — the user has just proven a fresh
TOTP code and authenticated with their password seconds earlier.
- OIDC + SAML callbacks redirect to /auth/forced-mfa-setup?token=...
when the enforced-SSO toggle is on.
Frontend
- Login page handles mfa_setup_required by redirecting.
- New page /auth/forced-mfa-setup shows QR + secret + code form.
- Settings → Security card with three toggles (enforced / roles /
enforce-sso). Saves to the existing settings KV — no new endpoint.
Three new settings keys, no schema migration. Existing deploys stay
unchanged until an admin flips mfa_enforced=true.
Phase 2-4 of multi-provider authentication. All three providers slot
into the AuthOrchestrator from phase 1; no further changes to
/auth/login are needed.
LDAPS (app/auth/strategies/ldap_strategy.py):
- ldap3 with strict TLS cert validation (CERT_REQUIRED + CA bundle)
- Service-account search-then-bind flow (DN never exposed to caller)
- Filter chars escaped via ldap3.utils.conv.escape_filter_chars
- AD-flavored defaults: sAMAccountName / memberOf / objectGUID
- Refuses if filter returns >1 entry (anti-impersonation safety)
- Bind password encrypted at rest (Fernet), bootstrapped from env on
first start then stored in settings table
OIDC (app/auth/strategies/oidc_strategy.py + app/routers/auth_oidc.py):
- Authorization Code + PKCE (S256)
- Strict ID-Token validation via Authlib: signature (JWKS w/ auto-refresh
on rotation), iss (essential), aud (essential, must == client_id),
exp (essential), nonce (replay protection)
- State/PKCE-verifier/nonce stored in signed itsdangerous cookie (no
server-side session store needed)
- Discovery + JWKS cached in-process; JWKS auto-refetched on key miss
- Groups merged from both id_token claims and userinfo endpoint
- Hardened: prompt=select_account to defeat silent IdP reuse
SAML 2.0 (app/auth/strategies/saml_strategy.py + app/routers/auth_saml.py):
- python3-saml (OneLogin) with strict=true; xmlsec1 handles signature
validation. XSW attacks mitigated via strict assertion/response
signature position checks plus wantAssertionsSigned=true
- SP-initiated (/auth/saml/login) + IdP-initiated (POST /auth/saml/acs)
- /auth/saml/metadata serves signed SP descriptor
- RelayState same-origin check to prevent open redirect
- IdP metadata loaded from URL or file at startup
Wiring:
- app/main.py imports auth_oidc/auth_saml routers behind try/except so
the app still starts when authlib or python3-saml aren't installed
- Frontend login page fetches /auth/providers and renders matching
redirect buttons (Sign in with Entra ID / SAML SSO) plus the local
credentials form. Adds MFA second-step screen with 6-digit OTP input
when /auth/login returns mfa_required=true.
.env.example: full provider config blocks with worked examples for
Entra ID, Okta, Keycloak, Google. Each block is commented with the
exact format the corresponding admin needs.
router.push('/') performs client-side navigation, which causes AppShell
to mount and fire /auth/me before the browser has committed the
Set-Cookie from the login response. The result was a 401 on the first
/auth/me, bouncing the user back to /login as if the credentials were
wrong; only after a couple of retries (and an eventual hard reload)
would the cookie be picked up and the dashboard load.
Switch to window.location.href = '/' so the browser performs a full
navigation. The cookie jar is guaranteed to be committed before the new
page loads, so the very first /auth/me succeeds.