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.
Ainda não lançado
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,CEPandHEALTH_DATA(ICD-10 after "CID"); each type maps to an LGPD category (IDENTIFICATION, CONTACT, LOCATION, FINANCIAL, HEALTH, COMPANY, CREDENTIALS), served atGET /public/dlp-types. Decisions addlgpdCategories; sensitive personal data (art. 11) adds 10 risk points; policy conditionsdlpTypesandlgpdCategories. Content is still never stored. See docs/dlp-lgpd.md. - Structured logs --
LOG_FORMAT=json(default in production): one JSON object per line withrequestIdandorganizationId; 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 carriesX-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_LEVELfilters. - User preferences --
GET /me/preferencesandPATCH /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 /meincludespreferences. - Agent framework adapters -- Node SDK:
guardOpenAIAgentsTool(OpenAI Agents SDK tool configs and function tools) andguardAiSdkTools(Vercel AI SDK tools); Python SDKvulnify.adapters:guard_crewai_toolandcrewai_before_tool_call(CrewAI),langgraph_tool_guard/alanggraph_tool_guard(LangGraphToolNodewrap_tool_call). No framework dependency; a blocked call answers the reason to the model instead of running the tool. Examples inpackages/sdk/examplesandpackages/sdk-python/examples; see docs/sdk-adapters.md. - Docker images --
docker/api.Dockerfile(multi-stage, Node 22 slim, production dependencies only, runs asnode, health check on/health/live;--target migrateapplies migrations and the RLS app role as a release step) anddocker/web.Dockerfile(static build on unprivileged nginx, port 8080, pre-rendered pages + SPA fallback, same-origin/api/proxy toAPI_UPSTREAM). See docs/docker.md. - Load test --
npm run load:events: throughput and latency percentiles forPOST /v1/events, in-process against the test database by default (1000 requests, 10 in flight), or against a running API withLOAD_TARGET_URL;LOAD_MAX_P99_MSturns 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 auditUSAGE_ALERT(the marker that prevents repeats across instances). Checked off the decision path (quota refresh, agent creation,GET /usage); never blocks.GET /usageaddsalerts: [{ metric, threshold, period, sentAt }]. - Delete and edit inventory --
DELETE /permissions/:id(unlinks it from every agent, effective on the next decision; auditedPERMISSION_DELETEDwith the agents unlinked),PATCH /resources/:id(name, description, classification, integrationId; auditedRESOURCE_UPDATEDwith before/after) andDELETE /resources/:id(auditedRESOURCE_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 carryagentName,resourceName,policyNamesnapshots. - Policy groups and simulation —
condition.allOf/condition.anyOf;POST /policies/dry-runruns the real decision path;POST /policies/impactandPOST /policies/:id/impactreplay past events. - Approvers —
Policy.approverRolesenforced; approving a HIGH or CRITICAL risk review requires a fresh TOTP code from MFA users (mfaCode,MFA_STEP_UP_REQUIRED;approval.stepUpin the queue). Resolved reviews includereviewer{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 logGET /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-riskcomputed in SQL per calendar day in the organization timezone (zero-filled,from/toup to 366 days);/dashboard/risk-over-timeaccepts the same range. - Translatable messages -- anomalies carry
messageCode+messageParams; compliance controls carrytitleCode,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,/policiesaccept?q=(case-insensitive search),?sort=field|-field(allowlisted,400 INVALID_SORTotherwise, stableidtiebreaker) and per-list filters (agent/integrationstatus, resourceclassification/integrationId, policyenabled/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, sourcesession). - 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/toorperiod). - Operations —
GET /health/live(liveness),GET /health/readyandGET /health(readiness, 503 when the database is down), parsed releases inGET /changelog. - Credential vault -- integrations hold real credentials:
API_KEY(header or query),BEARER,BASIC,OAUTH2_CLIENT_CREDENTIALS.POST /integrationstakesconfig(allowedHosts,healthUrl,requireLinkedAgent) and an optionalcredential;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 storinglastCheckedAt,lastErrorand 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'sallowedHosts(route create/retarget andallowedHostsedits are checked). OAuth2 tokens are cached in memory until expiry with one refresh + retry on 401. Revoked credentials answer409 CREDENTIAL_REVOKED;requireLinkedAgentBLOCKs agents not linked to the integration ("agent not linked to integration"); injected values echoed by the upstream are redacted;GatewayCall.credentialIdis recorded (also inGET /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 aWEBHOOK_URLcredential;PATCH /organization { slackWebhookUrl }andslackConfiguredkeep working (legacy values migrate automatically;npm run db:backfill-slack). Alert togglesconfig.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/testsends 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/statusshows which app settings are configured;POST /integrations/slack/:id/disconnectrevokes the token. - GitHub issues and Jira tickets on BLOCK/REVIEW --
kind: GITHUB(owner/repo, fine-grained PAT as aBEARERcredential) andkind: JIRA(https site URL, project key, issue type, Atlassian email + API token asBASIC) 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 inGET /integrations/:id/tickets. Ticket text carries decision metadata and a link, never the scanned content.POST /integrations/:id/disconnectrevokes 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; credentialrotatedAt/expiresAt/fingerprint unchanged; legacyenc:webhook secrets moved tosecretEnc).
Changed
- OpenAPI -- every route in
/docs-api-jsonhas a tag, a summary, its security (public, session cookie or Bearer, API key) and required roles; responses listX-Request-Id,x-api-versionandx-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 scriptnpm run org:set-planor the dev-onlyALLOW_SELF_PLAN_CHANGEflag, both audited asPLAN_CHANGED. - Slack OAuth state (security) -- the install
stateis 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/installand the callback keep their contract ({ url, expiresInSec: 600 }; failures redirect withreason=SLACK_STATE_INVALID). - Invitations for existing accounts (security) --
POST /auth/accept-inviteno 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 itspassword, plusmfaCodewhen 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.slackWebhookUrlis no longer written. - Integration errors --
WEBHOOK_URLcredentials cannot be used by gateway routes (400 CREDENTIAL_NOT_ROUTABLE); database guard violations answer400 CREDENTIAL_NOT_ROUTABLE,409 CREDENTIAL_IN_USE_BY_ROUTESor409 SLACK_WEBHOOK_EXISTSinstead of 500. Review approval audit entries carryvia(weborslack). - Encryption at rest -- integration credentials use a key derived from
ENCRYPTION_KEY(HKDF) and av1.format with associated data (organization, row, field); the older format (seed data) still opens.ENCRYPTION_KEYis now required in production (32+ characters, different fromJWT_SECRET). - Integration status -- derived by the server once an integration holds a credential;
statuson create now only applies to inventory-only integrations. GET /export/siemis restricted to OWNER/ADMIN.- Gateway call log stored in the
GatewayCalltable, written in the decision's transaction:GET /gateway/callsitems now carryid(call id) andsecurityEventId(wasid= event id, plusauditLogId, removed). Forwarded calls no longer append agateway-upstreamaudit entry. A failed forward reportsupstream.status: nullwitherror(was0) in the gateway response and the log. The retention report addsgatewayCalls. GET /dashboard/summaryblockedTodaycounts 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,0disables; API writes invalidate immediately), the ingest path loads agent and resource once, and API keylastUsedAtis 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 /usagereturnsdemo: true).
Removed
POST /demo/loadandPOST /demo/reset(wiped the current organization;POST /demo/sandboxwith{ "reset": true }reloads the personal demo organization) and theDEMO_ORG_REQUIREDerror code.POST /auth/switch-organization(duplicate ofPOST /me/organizations/switch, which the web app uses).POST /analytics/trackand the unauthenticatedPOST /analytics/track-public.requireSeparateApproverorganization 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/impactwithoutdraftPolicyanswers 400 instead of 500;GET /integrations?status=accepts ERROR and PENDING.
0.2.0 — 2026-09-25
Added
- Gateway —
POST /v1/gateway/httpandPOST /v1/gateway/mcpevaluate 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-runsimulates 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/entryHashon AuditLog (local immutability beyond append-only trigger). - Policy approvers —
approverRoleson Policy; self-approval of REVIEW always blocked. - Pagination — agents, integrations, resources, policies return
{ items, total }. - Local RLS —
vulnify_approle + 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

