Skip to main content

Product updates

Changelog

Every notable change to Vulnify, newest first.

All notable changes to Vulnify are documented here. Format: Keep a Changelog, versions follow SemVer. The machine API contract is versioned in the URL (/v1); every response carries x-api-version and x-vulnify-version.

Unreleased

Added

  • Plans -- GET /public/plans (public): { plans: [{ name, agents, eventsPerMonth, retentionDays }] } (null = unlimited), the single source of plan limits; /public/config.planChangeEnabled.
  • Brazilian DLP and LGPD categories -- the content scanner also finds RG, CNH (check digits), PIX_KEY, PHONE_BR, CEP and HEALTH_DATA (ICD-10 after "CID"); each type maps to an LGPD category (IDENTIFICATION, CONTACT, LOCATION, FINANCIAL, HEALTH, COMPANY, CREDENTIALS), served at GET /public/dlp-types. Decisions add lgpdCategories; sensitive personal data (art. 11) adds 10 risk points; policy conditions dlpTypes and lgpdCategories. Content is still never stored. See docs/dlp-lgpd.md.
  • Structured logs -- LOG_FORMAT=json (default in production): one JSON object per line with requestId and organizationId; secrets (tokens, API keys, passwords, cookies, JWTs, Slack URLs, URL secrets) replaced and personal data (emails, phones, CPF, IPs) masked in messages, fields and stacks. Every response carries X-Request-Id (exposed to the browser; a caller's plain-token id is kept). One access line per request (LOG_REQUESTS): route template, status, duration, ids; never query strings, bodies or IPs. LOG_LEVEL filters.
  • User preferences -- GET /me/preferences and PATCH /me/preferences: { theme: LIGHT | DARK | SYSTEM, locale: "en" | "pt" | null, welcomeSeenAt }, stored on the user (follows them across devices and organizations). PATCH changes only the keys sent; locale: null = not chosen (the web app follows the browser); welcomeSeenAt: true = now, an ISO date (not in the future) or null; unknown keys or invalid values answer 400. GET /me includes preferences.
  • Agent framework adapters -- Node SDK: guardOpenAIAgentsTool (OpenAI Agents SDK tool configs and function tools) and guardAiSdkTools (Vercel AI SDK tools); Python SDK vulnify.adapters: guard_crewai_tool and crewai_before_tool_call (CrewAI), langgraph_tool_guard / alanggraph_tool_guard (LangGraph ToolNode wrap_tool_call). No framework dependency; a blocked call answers the reason to the model instead of running the tool. Examples in packages/sdk/examples and packages/sdk-python/examples; see docs/sdk-adapters.md.
  • Docker images -- docker/api.Dockerfile (multi-stage, Node 22 slim, production dependencies only, runs as node, health check on /health/live; --target migrate applies migrations and the RLS app role as a release step) and docker/web.Dockerfile (static build on unprivileged nginx, port 8080, pre-rendered pages + SPA fallback, same-origin /api/ proxy to API_UPSTREAM). See docs/docker.md.
  • Load test -- npm run load:events: throughput and latency percentiles for POST /v1/events, in-process against the test database by default (1000 requests, 10 in flight), or against a running API with LOAD_TARGET_URL; LOAD_MAX_P99_MS turns it into a gate. See docs/load-testing.md.
  • Usage alerts -- when events this month or agents reach 80% and 100% of the plan limit, owners and admins get an email (in their language) and Slack integrations get a message (new toggle config.alerts.usage, on by default); once per metric, threshold and UTC month, recorded as audit USAGE_ALERT (the marker that prevents repeats across instances). Checked off the decision path (quota refresh, agent creation, GET /usage); never blocks. GET /usage adds alerts: [{ metric, threshold, period, sentAt }].
  • Delete and edit inventory -- DELETE /permissions/:id (unlinks it from every agent, effective on the next decision; audited PERMISSION_DELETED with the agents unlinked), PATCH /resources/:id (name, description, classification, integrationId; audited RESOURCE_UPDATED with before/after) and DELETE /resources/:id (audited RESOURCE_DELETED; answers { deleted, eventsKept }). OWNER/ADMIN only; the decision cache is invalidated. Past events are kept: deleting an agent, resource or policy sets the link to null and events/anomalies carry agentName, resourceName, policyName snapshots.
  • Policy groups and simulation — condition.allOf / condition.anyOf; POST /policies/dry-run runs the real decision path; POST /policies/impact and POST /policies/:id/impact replay past events.
  • Approvers — Policy.approverRoles enforced; approving a HIGH or CRITICAL risk review requires a fresh TOTP code from MFA users (mfaCode, MFA_STEP_UP_REQUIRED; approval.stepUp in the queue). Resolved reviews include reviewer {id, name, email, formerMember}.
  • Anomalies — per-agent baselines (GET /agents/:id/baseline), scheduled and debounced scans, filters, POST /anomalies/:id/acknowledge.
  • Webhook deliveries — durable retries with exponential backoff; GET /webhooks/:id/deliveries, POST /webhooks/:id/deliveries/:deliveryId/redeliver.
  • Compliance evidence — GET /compliance/export (admins): sealed JSON bundle with inventory, policies, decisions, resolved reviews and audit-chain check.
  • Demo sandbox — POST /demo/sandbox: every user gets a personal demo organization (created once, reused, filled with the demo data, session switched into it); { "reset": true } reloads it. Idle sandboxes are purged after 30 days.
  • Gateway -- PATCH /gateway/routes/:id (rename, retarget with SSRF re-validation, default agent, enable/disable); call log GET /gateway/calls (route, method/path or tool, decision, upstream status and latency; filters routeId, protocol, decision, agentId, sandbox, from, to); /audit-logs?source=; OpenAPI parity for the machine endpoints.
  • Dashboard aggregates -- GET /dashboard/decisions-per-day, /dashboard/top-resources, /dashboard/agent-risk computed in SQL per calendar day in the organization timezone (zero-filled, from/to up to 366 days); /dashboard/risk-over-time accepts the same range.
  • Translatable messages -- anomalies carry messageCode + messageParams; compliance controls carry titleCode, evidenceCode, evidenceParams (English text kept as fallback). See docs/api-message-codes.md.
  • Evidence CSV -- GET /compliance/export?format=csv: the evidence bundle as spreadsheet tables (meta, summary, controls, decisions, agents, policies, approvals), tied to the sealed JSON digest; formula-injection safe. PDF not offered (would need a heavy renderer).
  • List conventions -- /agents, /integrations, /resources, /policies accept ?q= (case-insensitive search), ?sort=field|-field (allowlisted, 400 INVALID_SORT otherwise, stable id tiebreaker) and per-list filters (agent/integration status, resource classification/integrationId, policy enabled/decision/mode).
  • Profile and tenant switcher -- GET/PATCH /me (display name; email read-only), GET /me/organizations ({ currentOrganizationId, items: [{ organizationId, name, role, isDemo, isSandbox, current, joinedAt }] }), POST /me/organizations/switch; organization switches are recorded in the audit trail (ORGANIZATION_SWITCH, source session).
  • Dashboard insights -- GET /dashboard/trends, /dashboard/risk-matrix, /dashboard/blocks-by-hour, /dashboard/review-aging, /dashboard/rising-risk: SQL aggregates in the organization timezone, compared with the previous equal window (from/to or period).
  • Operations — GET /health/live (liveness), GET /health/ready and GET /health (readiness, 503 when the database is down), parsed releases in GET /changelog.
  • Credential vault -- integrations hold real credentials: API_KEY (header or query), BEARER, BASIC, OAUTH2_CLIENT_CREDENTIALS. POST /integrations takes config (allowedHosts, healthUrl, requireLinkedAgent) and an optional credential; GET/PATCH /integrations/:id; POST /integrations/:id/credentials, .../credentials/:credentialId/rotate, .../credentials/:credentialId/revoke (permanent, secret wiped by the database); POST /integrations/:id/test (health check storing lastCheckedAt, lastError and a server-derived status). Secrets are write-only (fingerprint + last4), stored only through the database's vault functions, sealed with a versioned format bound to organization and row; every write is audited without secrets; OWNER/ADMIN only.
  • Gateway credential injection -- HTTP routes take credentialId; the credential is applied only after ALLOW and only to hosts in the integration's allowedHosts (route create/retarget and allowedHosts edits are checked). OAuth2 tokens are cached in memory until expiry with one refresh + retry on 401. Revoked credentials answer 409 CREDENTIAL_REVOKED; requireLinkedAgent BLOCKs agents not linked to the integration ("agent not linked to integration"); injected values echoed by the upstream are redacted; GatewayCall.credentialId is recorded (also in GET /gateway/calls).
  • Slack alerts (incoming webhook) -- the organization Slack webhook is now a SLACK integration (config.mode: incoming_webhook) with the URL sealed in the vault as a WEBHOOK_URL credential; PATCH /organization { slackWebhookUrl } and slackConfigured keep working (legacy values migrate automatically; npm run db:backfill-slack). Alert toggles config.alerts {review, block, critical, anomaly}, Block Kit messages with links to the web app, durable retries (GET /integrations/:id/deliveries, POST .../deliveries/:deliveryId/redeliver), POST /integrations/:id/test sends a test message. Sandbox events never alert.
  • Slack app: approve/deny from Slack -- connect a workspace with the OAuth v2 install (GET /integrations/slack/install + callback) or a pasted bot token (POST /integrations/slack/bot); REVIEW alerts carry Approve/Deny buttons, handled over a signed HTTP request URL (POST /integrations/slack/interactions) or Socket Mode, with the same approver rules as the web app (member matched by verified email; HIGH/CRITICAL approvals by MFA users go to the web app). Messages are updated when a review is resolved anywhere. GET /integrations/slack/status shows which app settings are configured; POST /integrations/slack/:id/disconnect revokes the token.
  • GitHub issues and Jira tickets on BLOCK/REVIEW -- kind: GITHUB (owner/repo, fine-grained PAT as a BEARER credential) and kind: JIRA (https site URL, project key, issue type, Atlassian email + API token as BASIC) integrations with rules (onBlock, onReview, minRiskLevel) and health tests. One ticket per event and integration, created after the decision commits with retries; resolving the review closes the GitHub issue or applies an optional Jira transition, and comments. Tickets show on events and reviews (tickets[]) and in GET /integrations/:id/tickets. Ticket text carries decision metadata and a link, never the scanned content. POST /integrations/:id/disconnect revokes an integration's credentials and keeps its history. Reviews that expire also update their tickets (GitHub closed as not planned) and Slack messages.
  • ENCRYPTION_KEY rotation -- ENCRYPTION_KEY_PREVIOUS (comma-separated) keeps old keys for decryption only; new payloads carry a key id (e2.<keyId>. for MFA seeds and webhook URL/secret, v2.<keyId>. for integration credentials) and older formats still open. npm run db:reencrypt [-- --apply] [--org <id>] re-encrypts every secret at rest under the current key (dry run by default, per-field report, idempotent, compare-and-swap writes; credential rotatedAt/expiresAt/fingerprint unchanged; legacy enc: webhook secrets moved to secretEnc).

Changed

  • OpenAPI -- every route in /docs-api-json has a tag, a summary, its security (public, session cookie or Bearer, API key) and required roles; responses list X-Request-Id, x-api-version and x-vulnify-version; operation ids are unique; the document version is the API build (apps/api/package.json). New endpoints (plans, DLP types, permission/resource delete and edit, usage alerts) describe their response contract. Checked by test/openapi.spec.ts.
  • Plan changes (security) -- an owner can no longer change the organization plan with PATCH /organization (403 PLAN_CHANGE_DISABLED). Plans change only through the operator script npm run org:set-plan or the dev-only ALLOW_SELF_PLAN_CHANGE flag, both audited as PLAN_CHANGED.
  • Slack OAuth state (security) -- the install state is now 32 random bytes whose SHA-256 is stored in the database (OAuthState) and consumed atomically, so it is single-use across API instances and survives restarts (was HMAC-signed and single-use per process). GET /integrations/slack/install and the callback keep their contract ({ url, expiresInSec: 600 }; failures redirect with reason=SLACK_STATE_INVALID).
  • Invitations for existing accounts (security) -- POST /auth/accept-invite no longer signs in an existing account from the token alone. The caller must be signed in as that account (the membership is added and the session switches to the invited organization) or send its password, plus mfaCode when the account has MFA. Errors: 401 INVITE_REQUIRES_LOGIN, INVITE_PASSWORD_INVALID, INVITE_MFA_REQUIRED, INVITE_MFA_INVALID, 403 INVITE_EMAIL_MISMATCH; failures do not consume the invitation and share the login rate limit. The invitation is consumed atomically with the membership; acceptance is audited (INVITATION_ACCEPTED, via). New accounts are unchanged.
  • Slack review alerts -- sent by the Slack integrations with retries instead of a one-shot post from the review email path; Organization.slackWebhookUrl is no longer written.
  • Integration errors -- WEBHOOK_URL credentials cannot be used by gateway routes (400 CREDENTIAL_NOT_ROUTABLE); database guard violations answer 400 CREDENTIAL_NOT_ROUTABLE, 409 CREDENTIAL_IN_USE_BY_ROUTES or 409 SLACK_WEBHOOK_EXISTS instead of 500. Review approval audit entries carry via (web or slack).
  • Encryption at rest -- integration credentials use a key derived from ENCRYPTION_KEY (HKDF) and a v1. format with associated data (organization, row, field); the older format (seed data) still opens. ENCRYPTION_KEY is now required in production (32+ characters, different from JWT_SECRET).
  • Integration status -- derived by the server once an integration holds a credential; status on create now only applies to inventory-only integrations.
  • GET /export/siem is restricted to OWNER/ADMIN.
  • Gateway call log stored in the GatewayCall table, written in the decision's transaction: GET /gateway/calls items now carry id (call id) and securityEventId (was id = event id, plus auditLogId, removed). Forwarded calls no longer append a gateway-upstream audit entry. A failed forward reports upstream.status: null with error (was 0) in the gateway response and the log. The retention report adds gatewayCalls.
  • GET /dashboard/summary blockedToday counts from midnight in the organization timezone (was the server's).
  • Faster decisions: enabled policies and organization decision settings are cached per organization (DECISION_CACHE_TTL_MS, default 10000, 0 disables; API writes invalidate immediately), the ingest path loads agent and resource once, and API key lastUsedAt is written at most once a minute per key.
  • Activation analytics are emitted by the server; demo load/reset only work on demo organizations (403 DEMO_ORG_REQUIRED); demo organizations are exempt from plan limits and quotas (GET /usage returns demo: true).

Removed

  • POST /demo/load and POST /demo/reset (wiped the current organization; POST /demo/sandbox with { "reset": true } reloads the personal demo organization) and the DEMO_ORG_REQUIRED error code.
  • POST /auth/switch-organization (duplicate of POST /me/organizations/switch, which the web app uses).
  • POST /analytics/track and the unauthenticated POST /analytics/track-public.
  • requireSeparateApprover organization setting (self-approval is always blocked).

Fixed

  • Error audit -- every endpoint the web app calls is exercised as empty-org owner, demo owner, member and anonymous with malformed queries, bad/foreign ids and malformed bodies (test/web-endpoints-audit.spec.ts). NUL characters in any body, query or path param now answer 400 instead of 500 (PostgreSQL cannot store them; ?q= search broke on agents, resources, integrations and policies); POST /policies/impact without draftPolicy answers 400 instead of 500; GET /integrations?status= accepts ERROR and PENDING.

0.2.0 — 2026-09-25

Added

  • Gateway — POST /v1/gateway/http and POST /v1/gateway/mcp evaluate agent actions through the Risk + Policy engines (optional HTTP proxy on ALLOW). Dashboard routes under /gateway.
  • Expressive policies — conditions: maxRecords, agentIds, minRiskScore, outsideBusinessHours, destinationContains. POST /policies/dry-run simulates without persistence.
  • Anomalies — baseline scan per agent (POST /anomalies/scan) for volume spikes, new actions, new external destinations.
  • Webhooks + SIEM — CRUD /webhooks; delivery on BLOCK/REVIEW/CRITICAL/ANOMALY; GET /export/siem?format=json|cef.
  • Compliance reports — GET /compliance/report?framework=LGPD|ISO42001|NIST_AI_RMF|EU_AI_ACT.
  • Activation analytics — LGPD-friendly event names only (POST /analytics/track, GET /analytics/activation).
  • Health / status / changelog — GET /health, GET /status, GET /changelog.
  • Audit hash chain — prevHash / entryHash on AuditLog (local immutability beyond append-only trigger).
  • Policy approvers — approverRoles on Policy; self-approval of REVIEW always blocked.
  • Pagination — agents, integrations, resources, policies return { items, total }.
  • Local RLS — vulnify_app role + tenant policies (optional; Prisma owner still bypasses RLS in local/dev).

Changed

  • Enterprise UI palette moved off purple-AI toward slate/teal.
  • API version surface documented as 0.2.0.

0.1.0 — initial product slice

  • Auth, multi-tenant orgs, agents, integrations, resources, permissions
  • Risk Engine + Policy Engine + ingest API + Attack Simulator
  • Reviews, DLP scan, demo environment, SDK packages