What's new
Every release, newest first. Before upgrading, take a backup and read the Upgrading notes of each release you skip. How to upgrade: docker compose pull && docker compose up -d, or bump the Helm chart (see the installation guide).
0.13.8
Added
- Oracle Linux vulnerabilities. Oracle Linux hosts and images are no longer an evaluation gap: their packages are matched against Oracle's security advisories (ELSA), which OSV does not carry. A new feed,
elsa:Oracle Linux, created when Oracle Linux is in inventory, reads Oracle's OVAL file for each release in inventory (and no other) fromlinux.oracle.comevery 6 hours (STACKURITY_ORACLE_LINUX_URLfor a mirror; 1 to 6 MB each), streaming it. Oracle Linux 7, 8, 9 and 10 are covered, and 6 while Oracle publishes its file. Each ELSA is stored per release (OL9-ELSA-2023-12850) and aliased to its CVEs, so KEV, EPSS, search and the second check apply as for Red Hat advisories. Versions are compared with rpm rules, epoch included, so Oracle's rebuilds (.0.1,.0.3) of Red Hat packages compare correctly; the Unbreakable Enterprise Kernel is matched per line (R6, R7, R8), so a UEK R7 kernel is never measured against an R8 fix; Ksplice and FIPS builds, which carry another epoch, are matched against their own advisories only; module streams named by version (nodejs:18) are kept apart; and where one advisory fixes a package on several version lines (Oracle Cloud Native Environment'skubeadm1.22, 1.23 and 1.24), a host is compared with its own line's fix, and one on no listed line is reported as needing validation rather than as affected. Advisory ids with Oracle's revision suffix (ELSA-2026-61581-0) are read too. A release file that cannot be read, or that lost more than half of its advisories, is refused and the release keeps what was last read, without holding up the other releases. Other module streams, signing keys and Ksplice live kernel patches are not checked. Oracle Linux 7 hosts are toldyum updateinstead ofdnfin the action plan (and so are RHEL and CentOS 7). See Oracle Linux. - VEX import and export.
stackurity vex import, Settings → VEX andPOST /vex/importread OpenVEX (v0.2, and v0.0.1) and CycloneDX VEX, standalone or embedded in a CycloneDX SBOM (an SBOM uploaded withsbom importhas its VEX imported too), plain or as a signed in-toto attestation (DSSE envelope or Sigstore bundle). Statements (not_affectedwith its justification,affected,fixed,under_investigation) are matched to component versions by image digest, package URL with a version, application package URL or BOM serial number (also BOM-Links), or CPE; a product that could be more than one thing (no version, two versions in one image, a CPE that fits two packages, a tag instead of a digest, version ranges) is not applied and is listed as not matched with the reason. An importednot_affectedstatement never hides anything by itself: it waits under Approvals (marked VEX, with the file, its SHA-256, the source and the justification) for an organisation-wide security or admin user, unless it comes from a source an admin trusts to apply on its own, which must be pinned to a public key its attestation verifies with or to the uploading API token (a document's author is never trusted); even then it applies on its own only on the assets the signed attestation is about (the rest of a bare package match waits), and never from a document older than one already imported from the source. An approved statement becomes ignore rules (false_positiveormitigated) pinned to the component version and image digest it matched; they end by themselves after an upgrade or a new image, or when a newer statement says affected.affected,fixedandunder_investigationare shown on the finding with a VEX badge and change nothing. Imports, decisions (file, statement, approver) and source changes are in the activity log. See docs/vex.md. - VEX export.
stackurity vex export, Export VEX on an asset's page and in Settings → VEX, andGET /vex/exportwrite the decisions about open issues (false positives, mitigations, and imported statements) as OpenVEX or CycloneDX VEX, for one asset, one project or every asset you can see; people's ignore notes and mitigations are included only on request by a security or admin user. Findings carryvex, ignore requests and rulesvex_statement_id, and the AI assistants'list_findingsandget_findingreturn the VEX statement. - Stackurity Cloud: weekly vulnerability news. A digest for one ISO week, sent only to people subscribed to Vulnerability news: the CVEs CISA added to its Known Exploited Vulnerabilities catalogue that week (vendor and product, CISA's description, the date added and CISA's due date, known ransomware use, and the FIRST EPSS score when there is one), release cycles whose security support ends about eight weeks later (from endoflife.date, for products Stackurity matches packages against), the week's Stackurity blog posts, and one line pointing to the dashboard, with the source of every fact linked. It carries no organisation's data. A week with no KEV additions and no posts sends nothing. It can be previewed, tested on an instance administrator and, once switched on, sent; it is sent once per week. A weekly schedule exists but is off unless configured.
- The KEV feed (
kev:cisa) now keeps CISA's short description of each vulnerability (migration0059_kev_description).
Upgrading
- Migration
0061_vexcreatesvex_sources,vex_documents,vex_statementsandvex_matchesand adds nullable or constant-default columns tofinding_exceptions(vex_statement_id,version,digest) andignore_requests(vex_statement_id,vex_assets), and replaces theignore_requestsrow-level security policy so that members with project access only do not see VEX requests; it rewrites nothing. - Migration
0059_kev_descriptionadds an emptyshort_descriptioncolumn toknown_exploited; the nextkev:cisasync (daily) fills it. It rewrites nothing.
0.13.7
Added
- Exploit prediction (FIRST EPSS) in priorities. A daily feed,
epss:first, downloads FIRST's EPSS scores for every CVE (one bulk file fromepss.empiricalsecurity.com, about 3 MB;STACKURITY_EPSS_URLfor a mirror) and stores each CVE's probability of exploitation in the next 30 days, its percentile, the model version and the score date (migration0056_epss). The risk policy gets an EPSS factor:epsspoints (1.5 by default) while a finding's CVE is at or aboveepss_threshold(10% by default), not for CVEs already in CISA KEV, and never more than the known-exploited points. So that daily movement does not churn priorities and alerts, the factor is all or nothing, its text names the threshold rather than the day's score, and a finding that crossed the threshold keeps it until its EPSS falls below 80% of it. Priorities read the scores directly every minute; nothing is re-evaluated when the file changes. Measured on 130,000 open findings and 400,000 scores, the prioritisation pass that runs every minute took about 4.7 s with or without EPSS (reading and scoring 0.8 s against 0.6 s), and three days of small score changes rewrote only findings that crossed the threshold (TestPrioritizeEPSSPerformance). See Predicted exploitation (EPSS). - Findings carry
epss,epss_percentile,epss_dateandepss_model_version;GET /findingsfilters byepss_min(0 to 1). All issues has an EPSS column and an Exploit prediction (EPSS) filter, Settings → Risk policy the EPSS points and threshold, and Why this priority on the finding page,GET /findings/{id}/explain(risk.epss,why.epss) and the AI assistants'explain_findingsay what EPSS predicts for the CVE and why it counts or not.list_findingstakesepss_minand returnsepss. - Windows missing updates. Windows 10, 11 and Server 2016, 2019, 2022 and 2025 hosts are now checked for operating system vulnerabilities, not only end of life: the build and update revision the agent already reports (
10.0.20348.4171) are compared with the fixed builds Microsoft publishes in its Security Update Guide, with no agent change. Each missing CVE is a finding naming the update that fixes it (KB, fixed build, link), with KEV and EPSS applied as for any finding; the action plan has one fix per host, Install Windows update KB…, the newest cumulative update among its findings, which fixes all of them. Fixes that come only with paid Extended Security Updates after a release's end of life are labelled and go to the release upgrade. The new feedmsrc:Windowsreads the last 24 months of monthly documents (STACKURITY_MSRC_MONTHS) fromapi.msrc.microsoft.com(STACKURITY_MSRC_URLfor a mirror), checks for changes every 6 hours so a Patch Tuesday release is read the same evening, and rereads everything every 30 days. Hotpatched hosts (Azure Edition, hotpatch builds), Windows Server 2012 R2 and older (paid Extended Security Updates), .NET Framework, Office and servicing stack updates are not evaluated, and say so in the host's coverage and under Settings → Coverage gaps. Migration0058_windows_updates. See Windows missing updates. - Email preferences for bulk email (hosted installations; Stackurity Cloud turns them on). Product updates (Marketing) and Vulnerability news are the only email people can unsubscribe from: Your account → Email preferences has a switch for each and lists what is always sent (account and security, billing, the organisation's own notifications) and why. Every such email carries one-click unsubscribe headers (
List-Unsubscribe,List-Unsubscribe-Post, RFC 8058), an unsubscribe link and an Email preferences link that work without signing in (signed for one person; an unsubscribe link only turns its own category off), and the sender's postal address. They are sent through one path that checks each recipient's choice just before their email goes, waits between emails, and records each recipient before sending, so a retry never sends twice.stackurity admin email-suppressunsubscribes an address on its owner's request. New endpointsGET/PUT /api/v1/auth/email-preferences,POST /api/v1/email/preferences,POST /api/v1/email/preferences/updateandPOST /email/preferences/one-click(migration0057_email_preferences). See Email preferences. - Stackurity Cloud: product emails. The sign-up form says that product updates and vulnerability news will follow and that people can unsubscribe at any time, and which wording was shown is recorded with each sign-up. A "what changed" email per release, built from this changelog, can be previewed, tested on an instance administrator and, once switched on, sent to subscribers. Nothing is sent to customers until the operator switches it on.
- Stackurity Cloud's sign-up page shows a refused email address (a disposable mail service, or a domain that takes no email) at the email field, with a link to contact us.
Changed
- Prioritisation writes in chunks. Every pass, including one that rewrites every open finding (a changed risk policy, such as the first pass after this upgrade), writes 1,000 findings per transaction instead of all of them in one, so it no longer holds a row lock on every open finding until it ends: evaluations, workflow actions and API writes wait for one chunk at most. A pass interrupted part way keeps the chunks it wrote, and the next pass, a minute later, finishes the rest without rewriting them. A chunk writes a finding only if it is still open and unchanged since the pass read it, so a finding resolved during a long pass keeps the priority it closed with, and an evaluation's fresher priority is not overwritten with one scored from older evidence; a finding changed meanwhile is scored by the next pass. Measured on 130,000 open findings (
TestPrioritizeRewritePerformance, on a loaded machine): the rewrite was one transaction of 42 to 50 s, during which a workflow action on a finding waited up to 48 s; it is now 130 transactions of at most 0.63 s, and the longest wait was 0.25 s. - An email the mail server accepted is no longer reported as failed (and sent again) when the connection drops while saying goodbye (
QUIT) afterwards.
Upgrading
- Migration
0056_epsscreates theepss_scorestable and allows the new feed kindprediction; it rewrites nothing. The risk policy gainsepssandepss_thresholdwith their defaults (a saved policy whoseknown_exploitedis below 1.5 gets that value forepss), so its version changes once and the first prioritisation after the upgrade records the new version on every open finding. That pass writes in chunks of 1,000 findings, each its own short transaction, not in one transaction over every open finding (about 65,000 on a large installation). EPSS starts counting after the firstepss:firstsync, within minutes of the start; setepssto 0 in the risk policy to leave it out. - Migration
0057_email_preferencesonly creates four new tables (email preferences, suppressions, campaigns and their recipients); it changes no existing rows. - Windows hosts now get vulnerability findings and a new feed,
msrc:Windows, which downloads about 140 MB fromapi.msrc.microsoft.comon its first sync (see network.md); disable it under Intelligence, or mirror it for air-gapped servers.
0.13.6
Security
- A botnet cannot lock an account out with failed sign-ins. Addresses an account signed in from with its password in the last 30 days are exempt from the per-account cap of 100 failures in 15 minutes; they keep their own per-address backoff.
- The dashboard's Content-Security-Policy no longer allows inline styles (
style-src 'self', without'unsafe-inline'); every page was checked in a browser without a violation. - Sessions end after 30 minutes unused (
STACKURITY_SESSION_IDLE_TIMEOUT,offto keep only the 12-hour limit), and an expired session and signing out are recorded in the activity log (auth.session_expiredwithidleorabsolute,auth.logout). Signing in always ends the session the browser already had, and turning on two-factor sign-in or changing your password gives the current session a new ID. Changing your password also disconnects AI assistants unless you choose otherwise, and can revoke API tokens; the answer says how many tokens still work. Migration0055_session_idle. - Breached passwords are refused. A new password (setup, change, reset, invitation, an admin's, and sign-up on Stackurity Cloud) is checked against Have I Been Pwned's Pwned Passwords with k-anonymity: only the first five hex characters of its SHA-1 are sent. On by default;
STACKURITY_BREACHED_PASSWORD_CHECK=offturns it off,STACKURITY_BREACHED_PASSWORD_URLuses a mirror, and an unreachable service is logged and does not block anyone. Passwords stay 12 to 256 characters. See network.md. - Password reset by email. Forgot your password? emails a single-use link that works for 15 minutes (only its hash is stored), with the same answer whether or not the account exists, limited per address and per email. A new password ends every session, disconnects AI assistants and, if chosen, revokes API tokens; two-factor sign-in still applies. It works on Stackurity Cloud and on servers with the new
STACKURITY_SMTP_*settings andSTACKURITY_BASE_URL; elsewhere the sign-in page says to ask an administrator, and a password an admin sets for someone else is now temporary: it must be changed at the next sign-in. Your account has a Password card. Migration0054_password_reset. See Forgotten passwords.
Added
- Stackurity Cloud: each organisation's own single sign-on (on plans whose
featuresincloud-plans.jsonincludesso; the pricing page lists it under Enterprise). Admins connect a SAML identity provider under Settings → Single sign-on (metadata URL or file, attribute and role mapping, the addresses to give the provider), prove their email domains with a DNS TXT record (a domain can be verified by one organisation only), test the connection (required before switching on, and again after any change), and can require single sign-on for members, with the owner keeping password access. People find their organisation's sign-in from their email on the sign-in page. Everything is scoped to the organisation under row-level security, and a session started by an organisation's single sign-on works only in that organisation (migration0049_sso_sessions). Existing accounts are linked only when the organisation allows it, and only its own members that are not instance administrators. - End-of-life detection for products endoflife.date tracks without the package names their installs use: Stackurity's own lifecycle overlay, a versioned data file checked against each vendor's package index, maps MongoDB's
mongodb-org-server, Docker'sdocker-ce,containerd.io, Electron on npm,redis-server,postgresql-<major>and PGDG'spostgresql<major>-server, NodeSource'snodejs, Elastic, Grafana, InfluxData, MySQL, MariaDB, HashiCorp, Kubernetes (kubelet), GitLab, Jenkins, SQL Server, Temurin, Corretto, Go, Tomcat (including Spring Boot's embedded Tomcat), Symfony, Kotlin, Quarkus, Kafka and more. RHEL, Rocky Linux, AlmaLinux and Oracle Linux packages now use endoflife.date's Red Hat identifiers, and Debian packages its Ubuntu ones. endoflife.date's own mappings always win. See Lifecycle mappings. - Settings → Coverage gaps for instance administrators (
GET /api/v1/admin/coverage-gaps): across every workspace, files nothing reads (by kind), components with no vulnerability source (by type and reason) and products with no lifecycle match, counted by assets and workspaces. It names no workspace, asset, path or package. - Scans report more files they find but cannot read as unsupported coverage:
Package.swift,Package.resolved,Podfile.lock,conanfile.txt,conanfile.py,conan.lock,vcpkg.json,uv.lock,pdm.lock,rebar.lock,packages.config,deno.lock,bun.lockandbuild.sbt. - Three official stack packs (Settings → Stack packs; Home names them when a repository holds their files). As with the Java and Rust packs, they ship inside the server and read nothing until an instance administrator installs them. See Stack packs.
swift: Swift Package Manager'sPackage.resolved(formats 1, 2 and 3, Xcode projects and package registries) and CocoaPods'Podfile.lock. Swift packages are checked against OSV's SwiftURL advisories (GitHub's Swift advisories; a few dozen packages, mostly SwiftNIO, Vapor and their servers). OSV has no CocoaPods advisories, so pods are inventoried and shown as having no vulnerability source.hex: Elixir and Erlangmix.lock(every format since Mix 1.0), checked against OSV's Hex advisories (GitHub and the Erlang Ecosystem Foundation). Latest releases come from hex.pm (releases:hex, at most 100 lookups a minute as hex.pm asks;STACKURITY_HEX_URLfor a mirror), with retired releases never offered as the latest.cpp: Conanconan.lock(Conan 1 and 2), vcpkgvcpkg.json(exact only where pinned underoverrides) and thevcpkg.spdx.jsonvcpkg writes for each installed port. No public vulnerability database covers ConanCenter or vcpkg ports, so C and C++ libraries are inventory only.
- Files these packs read (
Package.resolved,Podfile.lock,conan.lock,vcpkg.json,vcpkg.spdx.json) are reported as unsupported coverage without the pack, asmix.lockalready was, instead of being ignored. - Two-factor sign-in with an authenticator app (TOTP, RFC 6238) under Your account: a QR code, a code to confirm, and ten one-time recovery codes stored as hashes. The secret is encrypted with
STACKURITY_SECRET_KEY. A code is accepted once, wrong codes are limited (five per sign-in, ten per account in 15 minutes), and API tokens and AI assistants are not affected (the page lists them). Admins can Require two-factor sign-in for all members (members who sign in with a password are sent to set it up; single sign-on is not affected) and Reset two-factor for a member, recorded in the activity log. Home recommends it to admins. Migration0053_mfaadds the tables. See Two-factor sign-in.
Changed
- Packages built by a vendor's repository (MongoDB, Docker, Elastic, Grafana, PGDG, NodeSource, Redis, nginx.org, MariaDB, MySQL, Ondřej Surý's PHP, Remi and others) are no longer matched against the distribution's advisories, which describe the distribution's own builds and showed them as clean. Their coverage is now
unsupported("built by , not by the distribution"), so assets running them show an evaluation gap; findings that matching produced on them are resolved. Their end of life uses upstream dates, and the inventory note names the repository. See Packages from vendor repositories. - Repository scans skip
.build, Swift Package Manager's build directory, which holds a checkout of every dependency.
Fixed
- The
consulandnomadpackages Debian (10, 11) and Ubuntu (18.04 to 23.10) built themselves are no longer taken for HashiCorp's builds: the lifecycle overlay now recognises HashiCorp's repository by its version format (1.20.1-1), so distribution builds (1.8.7+dfsg1-3) are still matched against the distribution's advisories (overlay2026-10-29.1). - The coverage-gap report (
GET /api/v1/admin/coverage-gaps) no longer scans all inventory on every request: each server reuses a report for up to 5 minutes (checked_atsays when it was built). - A Debian or RPM epoch no longer stops the release line from being read (
5:7.0.15is Redis 7.0,1:10.11.6MariaDB 10.11). - An upstream pre-release version such as
2.0~betais no longer taken for a build named after a release codename.
Upgrading
POST /auth/passwordanswers 200 with a JSON body (what was revoked, andtokens_kept) instead of 204, and disconnects AI assistants by default; send"revoke_assistants": falseto keep them.- Browser sessions now end after 30 minutes without use. Set
STACKURITY_SESSION_IDLE_TIMEOUT(for example2h, oroff) to change it. - To email password reset links from a self-hosted server, set
STACKURITY_SMTP_HOST,STACKURITY_SMTP_FROM(and username, password, port, TLS as needed) andSTACKURITY_BASE_URL. - New passwords are checked against known breaches by default (an outbound request to
api.pwnedpasswords.com, see network.md); setSTACKURITY_BREACHED_PASSWORD_CHECK=offfor none. - Two-factor sign-in needs
STACKURITY_SECRET_KEY.
0.13.5
Security
- The audit log is append-only. The application's database role could change and delete audit events in its workspace (row-level security had UPDATE and DELETE policies); it can now only record and read them, and a database trigger refuses changes and removals by any role, the table owner included. Audit events are still removed with their workspace when a whole workspace is deleted (Stackurity Cloud, 30 days after a trial or plan ended), and an operator can remove events on purpose as described under Audit log.
- Browser features the dashboard does not use are turned off. The
Permissions-Policyheader denies them (camera, microphone, geolocation, USB, serial, HID, Bluetooth, MIDI, screen capture, sensors and others) to every page and any frame in it. Payment, passkeys and security keys (WebAuthn) keep the browser's default; the payment page of Stackurity Cloud lets Razorpay Checkout use what it needs. - Single sign-on (OpenID Connect, licensed build) with
STACKURITY_OIDC_LINK_EXISTING=trueno longer links a disabled account (enabling it again later would have handed it to the identity) or an account that also belongs to another workspace (it could then switch there through the provider).
Added
- I've fixed it — check now on fix cards (Home and Fixes) and on a fix's page. It asks every place the fix covers for a new scan (the server's agent reports within a minute; a connected repository is scanned again), shows that it is waiting ("Checking DBC… usually within a minute"), then what the scan found: "Fixed, confirmed by a new scan at 16:05", or "Still found on DBC: jquery 3.3.1 at /var/www/html/phpmyadmin/yarn.lock". Places that cannot report on demand say what to do instead: re-run the CI scan for a repository, upload the new SBOM, or restart an agent older than 0.7. Nothing is closed without evidence; the fix's page points to Snooze and Accept for now for a fix you are not making.
GET /api/v1/checks/options: for fix groups or assets, whether Check again gets a fresh report within minutes (on_demand) and, if not, why (removed,agent,upload), with the latest check.- Applications that are gone are marked Removed. The agent sends the list of application directories it reports with its server's batch (
apps, to servers listing theapp_listfeature), and an application missing from it (a deleted directory, an old release next to the current one, a directory no longer configured) is resolved: its issues close as removed, the asset shows "No longer present", and it no longer turns stale. A configured--app-dirthat does not exist is reported as removed instead of as a failed scan. - Asset pickers group assets by kind (Servers, Applications, Repositories…) and tell same-named assets apart: "commmerceblog (server)" and "commmerceblog (repository)", or an application's path. MCP
list_assetsreturns the same aslabel. - The audit log is tamper-evident. Each workspace's events, and the platform's own, form a hash chain written by the database as each event is recorded.
stackurity admin verify-auditchecks the chains and reports the first break of each (a changed, removed or inserted event). See Verifying the audit log. - SAML single sign-on (licensed build, Pro and Enterprise), next to OpenID Connect: SP-initiated sign-in from the sign-in page, optional sign-in started from the identity provider (
STACKURITY_SAML_IDP_INITIATED, off by default), this server's metadata at/saml/metadata, the provider's metadata by URL (refreshed daily) or file, signed assertions required, encrypted assertions accepted with an optional key, email, name and group attributes with the same domain allowlist and group-to-role mapping as OpenID Connect, each assertion used once, SHA-1 signatures refused, and the same account rules as OpenID Connect (an existing account is linked only withSTACKURITY_SAML_LINK_EXISTING=true, and never an instance administrator);STACKURITY_SAML_ALLOWED_DOMAINSis required. Setup for Okta, Microsoft Entra ID and Google Workspace is in install.md. The sign-in page shows a button for each configured provider. Migration0048_samladds the tables for sign-ins in progress and used assertions.
Changed
- Application discovery follows Ghost-CLI installs like Capistrano, Deployer and Envoyer ones: with
versions/(orreleases/) and acurrentlink, only the current release is an application. Old versions kept for rollback (/var/www/ghost/versions/6.22.0next tocurrent -> 6.52.0) are no longer scanned as live applications, and ones reported before are resolved as removed on the next collection. A directory named with--app-diris scanned as given. - Copies of one application on a server always get distinct names ("ghost (/6.22.0) on commmerceblog"), also when both existed before 0.13; migration 0050 renames those already alike. A name a person set is never changed.
- "I applied this" and Check again on a fix are one button, I've fixed it — check now. The asset page keeps Check again for the whole asset.
- Asset status reads as words: Reporting, Last scan incomplete, Stopped reporting, Removed. Hosts are called servers in asset kinds.
- Smaller consistency fixes: on Home, Show the places of a fix needed in several places is a plain button like Show issues, not a primary one; Home's Assets needing attention and a fix's Where say which kind a same-named asset is; on a fix's issues, Reopen reads "Reopen snoozed or ignored" (it only changes those).
Fixed
- An upgraded application kept its old version's libraries, and their issues, for good when one of its directories could not be read. phpMyAdmin's
tmp/twigcache belongs to the web server user, so every scan of phpMyAdmin was incomplete, and incomplete scans removed nothing: after an upgrade from 4.8.0 to 5.2.3, twig 1.35.3 and jquery 3.3.1 stayed listed next to twig 3.11.3 and jquery 3.7.1. A scan now removes what it no longer lists outside the paths it could not read; the next collection clears what earlier ones left. Servers' OS packages were not affected. - A check on an asset whose scans are incomplete waited for a week, and its agent was asked to report again every minute meanwhile. Any report now completes it; one that could not read everything ends as Scan incomplete instead of claiming the issues are still there.
- A check on an application its agent no longer reports (a deleted directory) was never picked up; the server's agent now picks it up.
- Windows Server hosts now get end-of-life findings; the docs said they did but no finding was raised. The release comes from the build number (14393 is Windows Server 2016, 17763 is 2019 or 1809 by product name, 20348 is 2022), with the product name and edition as a cross-check, and Windows 10 and 11 hosts are mapped by build and edition. A build that does not name one release is a coverage gap with the reason, never supported. The Windows agent now also reports
EditionIDon the operating system row. See Windows end of life. - .NET installed from distribution packages gets end-of-life findings:
dotnet-runtime-X.Y(deb or rpm) is .NET X.Y. Red Hat's and Microsoft's packages take Microsoft's date, since neither supports .NET past it; Ubuntu's own archive packages follow the Ubuntu release. See .NET end of life.
Upgrading
- Migration
0052adds the chain columns and triggers without rewriting existing audit events, which stay as they were and are reported as "earlier events not chained"; each chain starts with the first event recorded after the upgrade. It builds one index onaudit_events(seconds for a few hundred thousand events).
0.13.4
Security
- The client address can no longer be forged behind a proxy. With
STACKURITY_TRUST_PROXY=truethe server usedTrue-Client-IP,X-Real-IPor the leftmostX-Forwarded-For, which a client can set, so per-address rate limits (sign-in, sign-up, assistant sign-in, enrollment) could be bypassed and any address recorded in the activity log. Now one header is read,STACKURITY_TRUSTED_PROXY_HEADER(defaultCF-Connecting-IP), and only from a peer inSTACKURITY_TRUSTED_PROXIES(default loopback and private ranges);X-Forwarded-Formode takes the rightmost untrusted hop. Cloudflare Tunnel and same-network reverse proxies need no change. - Sign-in lockout can no longer be used against someone else. The per-account limit counted successful sign-ins and applied to every address, so anyone could lock out a known email for 15 minutes. Failures are now counted per account and address: five free, then a delay from 30 seconds doubling to 15 minutes, cleared by a successful sign-in, with a looser cap per account across all addresses. Delays and the cap are recorded in the activity log.
- Single sign-on (OpenID Connect, licensed build) no longer takes over existing accounts. Before, a first sign-in linked any account with the same email, including an instance administrator or a member of another workspace, and with a role map could demote the only admin. Now an existing account is linked only with
STACKURITY_OIDC_LINK_EXISTING=true, and then only a member of the single sign-on workspace that is not an instance administrator; the last admin is never demoted; and ID tokens withoutemail_verified: trueare refused (STACKURITY_OIDC_REQUIRE_EMAIL_VERIFIED=falsefor Microsoft Entra ID, which does not send it). The server warns at start whenSTACKURITY_OIDC_ALLOWED_DOMAINSis empty. - The image's Syft (container image SBOMs from registries) is built from source with Go 1.27.3, go-jose 4.1.6 and golang.org/x/net 0.62.0. The Syft 1.54.1 release binary in 0.13.3 was built with Go 1.26.8 and carried fixable vulnerabilities: CVE-2026-116587 (go-jose, high), CVE-2026-78669 (x/net, high) and Go standard library CVE-2026-111943, CVE-2026-78667 and CVE-2026-97031 (high), plus medium ones. Syft is now 1.55.0.
- The image is based on Alpine 3.24 (was 3.22), and the example
deploy/docker-compose.ymluses PostgreSQL 17.11 (was 17.6, whose image has OpenSSL, zlib, musl and util-linux vulnerabilities with fixes, including critical CVE-2026-31789). PostgreSQL 17.11 is a minor update: replace the image and restart, no dump or restore. - Releases are gated: a tag publishes nothing while govulncheck reports a reachable vulnerability, npm audit reports a high or critical advisory, or the built image has a fixable high or critical vulnerability. Every pull request and a daily run on
maincheck the same. Exceptions need a reason and an expiry (.trivyignore.yaml,web/audit-allow.json).make vulncheckandmake scan-image IMAGE=...run the checks locally. - GitHub Actions used by the build and release workflows are pinned to commit SHAs.
Upgrading
- Behind Cloudflare (including Cloudflare Tunnel), nothing changes: keep
STACKURITY_TRUST_PROXY=true. The tunnel connector reaches Stackurity from loopback or a private (Docker network) address, which are trusted by default. - Behind another reverse proxy (Caddy, nginx, Traefik, an ingress controller), set
STACKURITY_TRUSTED_PROXY_HEADER=X-Forwarded-For(or the header your proxy overwrites, such asX-Real-IP). If the proxy reaches Stackurity from a public address, list it inSTACKURITY_TRUSTED_PROXIES. The Helm chart setsX-Forwarded-Forby default (config.trustedProxyHeader). A warning is logged once when the header arrives from a peer that is not trusted. - Single sign-on (OpenID Connect): accounts already linked keep working. People whose password account was never linked are refused at their first single sign-on until they use their password, or until the operator sets
STACKURITY_OIDC_LINK_EXISTING=true. With Microsoft Entra ID setSTACKURITY_OIDC_REQUIRE_EMAIL_VERIFIED=falseandSTACKURITY_OIDC_ALLOWED_DOMAINS.
0.13.3
Added
- Adding Stackurity to Slack from Slack's side.
/integrations/slack/installis the Slack app's Direct install URL: signed in, it asks which organisation (only those where you are an admin) and goes to Slack; signed out, it sends you to Slack's consent page as Slack requires. Installs started from Slack (its Install App page, the shareable link, the Marketplace) come back to Finish adding Stackurity to Slack, which starts a proper install instead of exchanging Slack's code: a link from someone else can never add their Slack channel to your organisation. See Installs started from Slack. - Your account in the account menu: your organisations and linked Slack accounts, with Unlink. A Slack integration added with the Stackurity app shows Your Slack account (linked as @name, or when you will be asked) on its page.
Changed
- The dashboard is built with React 19, react-router 8, Vite 8 and TypeScript 7. Pages look and work as before. Building it from source needs Node.js 22.22 or later; the image's build stage uses Node.js 22.23.4.
- Integration pages have one Save changes for the connection and what it sends, sticky at the bottom while something is unsaved, with Discard and a warning before leaving with unsaved changes. The kind is shown next to the name instead of as a read-only field.
- Sending one event to another Slack channel is a small + Add another channel next to that event, instead of an Add to Slack button among the triggers; back from Slack, the new channel is already picked for that event.
- Quiet hours are one setting, Off or On from … to …, instead of two time fields that looked set when they were not. Inputs and selects in forms have one height; empty Environments and Owner teams filters are hidden; a single rule's Enabled box is gone (the page's Disable does that).
- Linked Slack accounts moved from Settings to Your account;
/settings/slackopens it. - The Integrations list shows Slack integrations as "Slack (Stackurity app)" or "Slack (webhook)", and drops the settings button that repeated the name's link.
Fixed
- Cancelling on Slack's page, or coming back with a used, expired or someone else's request, explains what happened and offers to start again.
- The secret reference field of email, webhook and Teams integrations suggested a Slack name.
- The licensed build compiles again:
go.modkeeps the modules onlystackurity-proimports (tools/eedeps), whichgo mod tidyin a Dependabot update had removed. A test now fails when one of them is dropped.
0.13.2
Fixed
- Stackurity Cloud: upgrading a plan paid by an Indian card works. Razorpay refuses to change such a plan even at the end of the period ("Only offers can be updated for subscriptions when payment mode is domestic card."), so the upgrade from 0.13.1 was refused with the plan unchanged. It now answers like a plan paid by UPI: Plan and billing offers Cancel and subscribe again, the current plan keeps the period it paid for, and the new one starts at once when authorised on the payment page with the difference for the rest of the period, billed in full from the end of the period. A replacement left unpaid and replaced again has its payment link (the add-on invoice) cancelled too.
0.13.1
Added
- The worker logs a warning naming a job or periodic task, while it runs, when the heap has grown by more than 256 MiB since it started (
STACKURITY_HEAP_WARN_MIBchanges the threshold or turns it off), so a memory spike names its cause even when the process is then killed. - With
STACKURITY_METRICS_ADDRset, the metrics listener also serves Go heap and goroutine profiles at/debug/pprof/heapand/debug/pprof/goroutine; never on the API port. See Memory.
Fixed
- A vulnerability feed sync no longer runs the server out of memory when many large records change at once. An incremental OSV sync fetched every changed record (up to 5,000) and held them all before storing any; when Ubuntu republished 4,815 records (2.1 GB of JSON, kernel records of up to 3 MB each), every attempt was killed for running out of memory, the server restarted about every 35 minutes, and each restart queued the sync again. Records are now fetched eight at a time and stored in batches of at most 500 records and 16 MiB of JSON, in incremental and full syncs alike. Releases since incremental sync was added were affected; 0.12.0 and 0.13.0 both were.
- An evaluation uses less memory. It reads a package's advisories only for the release being evaluated (an Ubuntu kernel package has some 70,000 entries across releases and channels, a few hundred megabytes once read, of which one release's apply), and drops them once it has evaluated that package's rows, instead of keeping every package's advisories until it ends. A full evaluation of a workspace with 8,800 installed rows peaked at 765 MiB of heap before. Results are unchanged.
- Stackurity Cloud: upgrading a plan paid by an Indian card no longer fails. Razorpay changes a subscription on a card mandate only at the end of the period, and the upgrade was refused ("the payment provider refused the change"). The bigger plan now applies at once; the plan change is scheduled in Razorpay for the end of the period, and the difference for the rest of the period, pro-rated by the second with tax, is added to the next invoice, so that invoice is the new plan's price plus that difference and nothing is charged again for the old plan. A plan paid by UPI or e-mandate, which Razorpay cannot change, offers Cancel and subscribe again: the current plan keeps the period it paid for and the new one starts at its end, with the difference charged on the payment page.
POST /billing/subscribetakesreplaceand answersmessage;GET /billinghasupgrade.
Upgrading
- No action needed. Stackurity Cloud builds apply the additive migration
cloud/0003_upgrades. A vulnerability feed you turned off because its sync ran out of memory can be turned on again; it resumes from where it stopped.
0.13.0
Security
- go-jose is upgraded to 4.1.6 for GHSA-mgcc-cww5-fp2q (high). Stackurity uses it to check single sign-on tokens and, new in this release, assistants' signed client assertions.
Added
- Settings → AI assistants: connect an assistant step by step. The page shows the server's MCP address with a Copy button and a card per assistant (Claude, Claude Code, ChatGPT, Cursor, Visual Studio Code, Windsurf, Gemini CLI, Replit, other) that opens short numbered steps checked against each vendor's documentation, with one-click install links for Cursor, VS Code and Replit, and questions to try. Connected assistants, with Allow changes and Disconnect, are listed below. Without
STACKURITY_BASE_URLit explains how to turn assistants on. It is linked from the account menu (Connect an AI assistant) and from Help.GET /assistantsnow returnsmcp_url. The old address,/settings/assistants, opens it. - Create a team where you need one. Giving a team access to a project offers Create a team in place, and selects the new team. Team dropdowns with no teams (asset owner, assigning issues, remediation policies) say where teams are made. Community's one-team limit still applies.
- Home's first-run checklist also asks to connect an AI assistant, and each item opens the page that does it: add a server, connect an AI assistant, turn on notifications, invite your team.
GET /onboardinghas a newsteps.assistant. - ChatGPT can sign in. Clients whose metadata document declares
private_key_jwt(ChatGPT's, https://chatgpt.com/oauth/client.json, does) authenticate at the token endpoint with a signed client assertion (RFC 7523), checked against the keys at theirjwks_uri: issuer, subject, audience, expiry, and ajtithat cannot be used twice. Migration 0043 adds the method, the client's keys and the used assertions; it is additive. GET /.well-known/openai-apps-challengeservesSTACKURITY_OPENAI_APPS_CHALLENGE(or_FILE) as text for OpenAI's domain check, and 404 when it is unset (it used to return the dashboard).- Data handling in docs/ai-assistants.md: what an assistant can read and change, how your role caps it, how to disconnect, and that Stackurity never receives or stores prompts.
- Settings → Integrations: one place for everything Stackurity connects to. Active integrations lists notification channels and sources with their status (last delivery or run, errors) and a gear for their settings; Add an integration has cards in three groups: Notifications (email, Slack, Microsoft Teams, Jira, webhook), Sources (Linux and Windows servers through the agent, Git repositories, CI pipelines, container images and registries, Kubernetes, AWS, SBOM upload, other plugins; CI and SBOM with copyable steps) and AI assistants. Servers with the agent are listed with their count and last report. Settings → Notifications links lead there; the delivery log is a tab.
- Triggers instead of modes. Each notification integration has checkboxes for what it sends: new urgent issues, all new issues (with its own minimum priority,
new_min_priority), issues that came back after a fix, fix overdue (Pro), new asset connected, collection, feed or platform problems, a daily summary and a weekly summary (both may be ticked). On Slack each trigger can go to another Slack channel (event_channels). Messages stay grouped: at most one per check per integration and destination, plus summaries, never one per issue. Routes haveevents;modeis still accepted and returned (customwhen no mode matches), and the newurgent_weeklymode is urgent right away with a weekly summary. Changingmodegives the route that mode's events;eventsthat contradict a changed mode are a 400; fields left out keep their values. Deleting a channel some events go to sends them to the route's own channel again. - Two questions in the first-run flow: "Send right away" (urgent issues, all new issues, or nothing) and "Summary" (daily at 9:00, weekly on Mondays at 9:00, or none), in the browser's time zone, shown on the form. Nothing with no summary cannot be saved.
- Add to Slack. With a Slack app configured on the server (
STACKURITY_SLACK_CLIENT_ID,STACKURITY_SLACK_CLIENT_SECRET,STACKURITY_SLACK_SIGNING_SECRET, each secret also as_FILE), Slack is connected with Slack's OAuth v2 consent page: pick a channel, Allow, done. Scopeincoming-webhookonly. Assign to me and Snooze act from Slack, verified with the app's signing secret and only from the Slack workspace the app was installed into; uninstalling the app in Slack (or revoking its tokens) switches its integrations off everywhere and deletes their webhooks and tokens. Webhooks and bot tokens are stored encrypted withSTACKURITY_SECRET_KEY. The OAuth state is single use, 10 minutes, bound to the person and organisation. Adding it to the same Slack channel again refreshes that integration; each integration's webhook is its own secret; events are accepted once. A manifest to create the app is indocs/slack-app-manifest.yaml, the steps indocs/slack-app.md. Without the app, Slack works with a webhook as before ("Advanced: use a webhook" is always there).POST /api/v1/integrations/slack/install,POST /api/v1/integrations/slack/oauth,POST /api/v1/integrations/slack/events. - Email sent by Stackurity (
stackurity_emailchannels) on installations that provide a platform mail server (Stackurity Cloud): pick members (once they have signed in or accepted an invitation), or other addresses, which receive nothing until they confirm a link emailed to them (7 days, once; fixed subject, at most one pending and 3 a week per address across organisations, and a "Never email me again" link that blocks the address everywhere). Each recipient gets their own copy with a "Stop these emails" link and one-click unsubscribe headers (RFC 8058). Per-organisation limits per minute, hour and day (in emails): a message over a limit is held and counted in the next summary or hourly digest, never dropped, and a summary over the limit waits for it; the delivery log (held_by) and Platform health (email_quota, problemnotifications/email-quota) show it.GET /api/v1/notification-options,GET /api/v1/email-recipients,POST /api/v1/email-recipients/resend,POST /api/v1/email-recipients/confirm,POST /api/v1/email-recipients/block,POST /api/v1/email/unsubscribe,POST /email/unsubscribe/one-click,POST /email/block/one-click. Installations can refuse direct account creation (InviteOnly, set on Stackurity Cloud):POST /usersanswers403 invite_only. - Provider presets for your own mail server: Amazon SES (with a region picker), SendGrid, Postmark, Mailgun, ZeptoMail and Gmail / Google Workspace fill the host, port and encryption, with a note on where to get the credentials.
Changed
- Summaries count only real fixes as fixed: issues whose installed version changed or whose component was removed. Issues closed because their advisory was withdrawn or no longer lists the installed version are listed apart under Fixed ("2 issues closed without a fix", count
no_longer_affected). - Jira issues name the version to move to as the dashboard does: the action plan's target version, with "fixes from" the minimum fixed version when they differ.
- Every MCP tool has a title and sets
readOnlyHint,destructiveHintandopenWorldHintexplicitly, as assistant directories require. Renaming a project, removing assets from one, assigning, snoozing and ignoring are marked destructive; inviting someone is marked open-world (it emails any address). - OAuth limits suit hosted assistants, which reach the server from their vendor's shared addresses: token requests are limited per code or refresh token (10 a minute), per client (600), in all (6,000) and per address only as an outer bound (1,200, was 60). Registration allows 200 an hour per address (was 20) and 2,000 in all;
/mcpallows 300 failed tokens a minute per address (was 30). - Upgrade advice leads with the recommended version. A package upgrade's title and action name the newest stable release in the minimum fix's major line when release data has one: "Upgrade ip-address 10.1.0 → 10.7.5", with "fixes from 10.7.4" beside it, where it read "→ 10.7.4". Home, Fixes, a fix's page, Slack and email messages that list fixes, and the MCP
action_planandremediation_plantools show it. Fix groups have a newtarget_version;to_versionstays the minimum fixed version. A major upgrade, a withdrawn release, or below 1.0 another minor line (0.21 to 0.34) never heads the title. - Install commands look for websites and apps by default. Also check websites and apps on this server is ticked under Add server and in the first-run flow, so the agent finds applications under
/srv,/var/www,/opt,/appand home directories. The MCPcreate_enrollmenttool does the same unlesscheck_appsis false.stackurity agentitself is unchanged: without--discover-appsit does not look. - Application discovery is bounded: at most 20,000 directories read and 200 applications reported per run. The installer's service now runs at a low CPU and disk priority (
Nice=10, best-effort I/O priority 7). - Fix cards count and act like one fix. With fixes grouped by fix (the default since 0.12.0), the counts on Home and Fixes ("4 fixes need you", "8 fixes in all") count a fix needed in several places once, as the list shows it;
GET /fix-groups/summarytakesgroup_bylikeGET /fix-groups. Home's focus list shows such a fix as one card listing its places, and a fix card can Assign, snooze or ignore in all N places at once, reporting partial results. The MCPaction_plantool lists the places of each fix. - Two copies of an application on one host are told apart. The second copy of "phpmyadmin on DBC" is named by its path ("phpmyadmin (/demo/phpmyadmin) on DBC"), and the first is renamed to match while it still has its generated name. A name a person set is never changed (migration 0044 records which names those are). Application paths show beside the name on Assets, Home and in the MCP
list_assetstool. - Approvals moved to Activity (
/approvals, with the Activity tabs);/settings/approvalsredirects there, and the count of requests waiting shows on Activity. - Remediation numbers count only remediation. A finding now records why it closed, as a machine-readable
resolution_kindnext to the sentence inresolution, with the facts inresolution_detail:remediated(upgraded, or fixed by a verified remediation plan),removed(the component is no longer installed),superseded(upgraded to a version that is still affected; the issue continues asresolution_detail.successor),advisory_withdrawn(every advisory behind it was withdrawn by its source: "advisory withdrawn by Ubuntu on 2026-10-27"),advisory_changed(the advisory or lifecycle data no longer lists the installed version, which did not change: "advisory updated: UBUNTU-CVE-2026-2 no longer lists mysql-8.0 8.0.42"),not_evaluated(the row could no longer be evaluated) andother. Before, every closure the intelligence caused read "no longer affected (re-evaluated …)" and was counted as a fix. - Outcomes (
GET /reports/outcomes) counts only remediated and removed findings inresolved, time to remedy and SLA, with the newremediatedandremovedtotals; the others are inclosed_not_remediation(total,by_kind) and shown apart ("50 closed because the advisory was withdrawn or changed (not counted as remediation)") with a link to the list.causesgainsresolution_kindandcounts_as_remediation; the causereevaluatedis replaced byadvisory_withdrawn,advisory_changed,supersededandnot_evaluated. - The evidence report (licensed build) does the same:
resolved(withremediatedandremoved), the deadline share and the median days count remediation only, andclosed_not_remediationlists the rest. GET /findings/verifyand the MCP toolverify_sincegive each resolved finding itsresolution_kindandcounts_as_remediation, and the totalsremediated,closed_not_remediationandresolved_by_kind.- The finding page and its history say why it closed ("Closed: Ubuntu withdrew this advisory on 27 Oct"), and that such a closure is not counted as remediation.
GET /findingstakesresolution_kind(comma-separated). - Durations in the dashboard are pluralised ("1 hour", "1 day", not "1 hours").
- docs/api.md is reorganised by area (one short entry per route instead of one wide table), and every route the server serves is documented, including health checks, OAuth and
/mcp, the licensed and Stackurity Cloud routes;internal/api/openapi.jsongains the query parameters it was missing (GET /findingsfilters,limitandoffseton paged lists).
Fixed
- The project switcher in the header shows a project created in the first-run flow at once, and choosing it at the end of the flow works, instead of needing a page reload.
- Incidents load in well under a second with a project chosen. With a project selected in the switcher (or for a member who sees only some assets), Activity → Incidents took 8 to 27 seconds and could sit on "Loading…": deciding which incidents were visible read every finding of every organisation, open and resolved, on each request, and the list then looked up each incident's findings one by one. Visibility now starts from the findings of the visible assets, read from a new index, and each incident's visible priority, SOS and escalation are worked out in one pass. The page and its total come from one query. What a narrowed caller sees is unchanged. On a copy of a production database (240,000 findings, 2 vCPUs) the list went from 8.5–15.5 s to about 0.2–0.3 s; the organisation-wide list was already fast and stays so.
- The findings summary (the tiles above Findings) took 1 to 2 seconds after a large rescan resolved many findings, because "resolved this week" read each of them from the table. It now reads an index only (about 0.15 s on the same copy).
Upgrading
- Migration
0046builds the indexfindings_asset_incident, rebuildsfindings_resolved_atwith two more columns, and replaces the row-level security policy onincidentswith an equivalent one. Migrations run in a transaction, so the indexes are built withoutCONCURRENTLY: writes to findings (uploads, evaluation) wait while they are built (reading is blocked only for the moment before the commit), 1.5 to 11 seconds (depending on load and cache) on a copy of a 1.2 GB production database with 240,000 findings (2 CPUs, 768 MB memory limit). Upgrading takes that much longer to start. - Migration
0045addsfindings.resolution_kindandfindings.resolution_detailand classifies findings resolved before it: "fixed by remediation plan …" and "upgraded to …" becomeremediated(orsupersededwhen the same issue was open on the new version), "component no longer installed"removed, and "no longer affected (re-evaluated …)", written while the same inventory row (one version) was installed,advisory_withdrawnwhen every advisory behind the finding is withdrawn andadvisory_changedotherwise, with the specific sentence. Anything else becomesother. The lastresolvedhistory event of each finding gets the kind in its detail. Outcomes and evidence numbers for past periods drop the findings that were never remediated. - Migration
0047_notify_setupgives every route the events of its mode, addsdeliveries.held_byand createsemail_recipients,email_sends,email_blocks,platform_keys,slack_installationsandslack_oauth_states. Routes keep exactly what they sent. API clients that send onlymodekeep working, and so do clients that read a route and send it back with onlymodechanged.
0.12.0
Added
- One card per fix, wherever it is needed. When the same upgrade is needed in several places (jquery 3.3.1 → 3.5.0 in two phpMyAdmin copies, or lodash on ten servers), Fixes and Home show one card that lists every place: the asset, the application's directory, its environment and its issue count, each linking to that place's own fix. Issues cleared add up across places; distinct vulnerabilities are counted once. Show issues lists the findings of every place. Checks, "applied", notifications and project access still work per place, so a project member sees only the places they can see. Settings → Fix grouping switches a workspace back to one card per asset.
GET /fix-groupstakesgroup_by=fix|asset(default: the workspace setting) and returnsplaces,fix_keyandapp_dir;GET/PUT /settings/fix-grouping;GET /findings?fix_group=accepts several groups, comma-separated.
Changed
- Fix cards for a deployed application show its directory, so two applications with the same name on one server can be told apart.
Fixed
- A library in an application inside another application was reported twice. With a webroot and an application under it both scanned (a CodeIgniter site in
/var/www/htmlwith phpMyAdmin in/var/www/html/phpmyadmin), the outer scan also read the inner application's libraries and lockfiles, so each of its vulnerabilities appeared under both. The agent now learns every application directory first and each library belongs only to the innermost application. Upgrade the agent to clear the duplicates.
0.11.0
Added
- Connect AI assistants with no install: the remote MCP endpoint. Paste
https://<your server>/mcpinto Claude (claude.ai, Claude Desktop, Claude Code), ChatGPT or any assistant that supports remote MCP servers, and sign in: a Stackurity page asks which organisation, optionally which single project, and which role (never above your own) the assistant gets. It is the MCP Streamable HTTP transport, for both the 2026-07-28 revision and theinitializehandshake of earlier ones, with the same tools asstackurity mcp. Clients that cannot sign in can send a personal API token instead. Seedocs/ai-assistants.md. - Sign-in for assistants with OAuth 2.1, as the MCP authorization specification asks, with Stackurity as its own authorization server: protected resource and authorization server metadata, dynamic client registration and client ID metadata documents (fetched through the outbound address guard), the authorization code flow with PKCE (S256 only) and exact redirect URIs, one-hour access tokens, rotating refresh tokens (reusing an old one ends the connection), revocation, and Settings → Connected assistants to see and disconnect them. Only hashes of codes and tokens are stored. Access tokens work only at
/mcp, never on the REST API; a connection acts with the lower of its role and the person's current role, and one limited to a project sees and changes only that project. Every connection, disconnection and change is in the activity log. Registration, token and MCP requests are rate limited.GET /.well-known/oauth-protected-resource,GET /.well-known/oauth-authorization-server,POST /oauth/register,POST /oauth/token,POST /oauth/revoke,POST /api/v1/oauth/authorize/preview,POST /api/v1/oauth/authorize,GET /api/v1/assistants,DELETE /api/v1/assistants/{id}. - OpenAPI 3.1 description of the REST API, served at
GET /api/v1/openapi.json(public) and kept in the repository (internal/api/openapi.json): every operation with its summary, role, parameters and authentication, ready for Swagger UI, Postman or client generators. A test fails when a route is added without an entry. - Assistants can make changes when you allow it. "Allow changes" is a separate box on the consent page, off by default, and can be changed under Settings → Connected assistants; without it a connection only reads. New MCP tools
create_project,rename_project,add_assets_to_project,remove_assets_from_project,create_enrollment(returns the install command for N servers),assign,snooze,ignore(files a request for approval when the role needs one),check_againandinvite_to_project. Each first answers with a preview of exactly what it would change and a confirm code and changes nothing until called again with that code, which guards against a model acting on a guess; each is limited by the caller's role on what it touches and carries MCP annotations (not read-only; destructive where it removes access). Install codes made by an assistant are for at most 50 servers and 24 hours; invitation links never go to the assistant. - Project limits, read-only organisations and trial reports, generic per-organisation settings like the monitored-asset limit, none of them set on a self-hosted install unless an instance administrator sets them (
PATCH /admin/workspaces/{id}withproject_limit,read_onlyandread_only_reason,report_mode). At a project limit a new project (or one taken out of the archive) answers402 project_limit; existing projects are never archived or deleted. A read-only organisation can sign in and see everything, but uploads, enrollment, integrations, checks and notifications answer402 read_onlywith the reason, and background evaluation and alerts skip it; a banner tells its members why. In trial report mode the evidence and outcomes reports and the SBOM export keep the first 10 rows of each list, say how many more there are, and carry a watermark; report pages show it as a banner. Settings → Usage shows projects against the limit, andGET /usageandGET /auth/mereturn the state. Seedocs/asset-limits.md.
Changed
stackurity mcpstays read-only by default.--allow-writesoffers the same tools that change something as/mcp, within the token's role.
Upgrading
- Migration
0041addsoauth_clients,oauth_grants,oauth_codesandoauth_access_tokensfor connected assistants, each with row-level security. Nothing existing changes./mcpand the OAuth endpoints are on only whenSTACKURITY_BASE_URLis set to the server's publichttps://address; without it they answer 404 and the server logs why at startup.
0.10.0
Added
- Monitored-asset limits per organisation. An instance administrator can limit how many assets each workspace monitors (Settings → Workspaces, or
PATCH /admin/workspaces/{id}withasset_limit;-1removes it). Self-hosted installs have no limit unless one is set. A monitored asset is a host, application, repository, image, cluster, managed service or SBOM that is not merged into another; Kubernetes workloads are not counted. At the limit, new assets are refused wherever they would be created (agent and collector uploads, CI scans, SBOM imports, image scans, integrations, enrollment codes) with402 asset_limitand a message that says so, while every asset already monitored keeps being collected, evaluated and alerted on. A banner shows when the organisation is at its limit, and Settings → Usage shows usage against the limit by kind and the assets that were refused.GET /usage. Seedocs/asset-limits.md. - Act from Slack: Assign to me and Snooze from inside Slack. A Slack channel set up with a Slack app's signing secret (a new, optional Slack app signing secret reference, stored like every other secret) gets interactive Assign to me and Snooze 1 week buttons; Open still opens the dashboard. The message is updated with what was done ("Assigned to Nirmal (3 issues)", "Snoozed until 3 Nov by Nirmal"), or the person is told, only them, why it was refused. Without a signing secret, messages are exactly as in 0.9. The app needs no OAuth scopes beyond its incoming webhook; its Interactivity Request URL is
POST /api/v1/integrations/slack/actions. Setup steps are indocs/install.mdand Help. - Linked Slack accounts. The first time someone clicks, Slack shows them a link that works once, for 15 minutes; they sign in and confirm which Slack account acts as them. After that, clicks act as that person with their role and projects, exactly as in the dashboard (a viewer cannot assign, snoozing needs the security role, people with access to some projects only cannot touch other projects' fixes), and are audited like dashboard actions with source
slack. People see and unlink their Slack accounts under the account menu and Settings, Linked Slack accounts.GET /slack/links,DELETE /slack/links/{id},POST /slack/links/preview,POST /slack/links/confirm. - Actions in webhook JSON carry
fix, the fix group id. - How clicks are trusted: the Slack interactivity endpoint is public and acts only on requests signed with the signing secret of the channel the button belongs to (Slack's HMAC-SHA256 signature, compared in constant time), at most 5 minutes old and accepted once, from that channel's Slack workspace, and it answers only to Slack's own response URLs. A Slack account is never matched by name or email: it must be linked by its owner through a single-use link, and a Slack account linked to someone else cannot be taken over. See
docs/security-audit.md, "Acting from Slack", anddocs/network.mdfor the inbound request. - "Urgent only" notifications. A route can send urgent messages right away (new urgent issues, issues that came back after a fix, new assets and, with overdue reminders, overdue messages) with no daily or weekly summary:
"mode": "urgent_only", Urgent only, no summary in Settings ▸ Notifications and in the first-run notifications step. Urgent messages over the route's hourly limit, which a summary would otherwise take in, are sent together once an hour. - Jira routes limited to a project. A route to Jira can now name one project, for example to keep a client's own Jira. It keeps its own issue per incident, describing only that project's part: its assets, findings, counts, priority, escalation comments and links (opened in the project), never another project's. Everything the issue says is read with a connection narrowed to the project, so the database's row-level security keeps other projects' assets out. The issue resolves when none of the project's findings remain open, even while the incident stays open elsewhere, and is told if they open again. Routes to Jira for every asset keep their whole-incident issues exactly as before, also on the same Jira project (the labels differ). Organisation-wide roles see both kinds of issue on the incident (
issues[].project_id); members with project access see none, as before. Two or more projects on a Jira route is still 400: add a route for each. - The CLI has a project option.
stackurity scan,agent,sbom import,sbom export,mcpanddiagnosticstake--projectwith a project's id or name (orSTACKURITY_PROJECTin CI). Reads are limited to that project (X-Stackurity-Project), and uploads add the assets they create to it. The CLI refuses to upload with--projectto a server that would ignore it. POST /ingesttakesproject_id, as SBOM uploads already did: an admin's upload files the asset in the project, and a collector token made for a project may name that project only (403 for any other) and never moves an asset that already exists. Uploads answer withproject_idandin_project.stackurity enroll --project <id>checks that the enrollment code is for that project, and refuses without using the code if it is not.POST /enrollreturns the code'sproject_id, andGET /metalistsfeatures(upload_project).
Changed
-
A collector token made for a project may now pass that project's
project_idtoPOST /sbom/import; other callers who are not admins still get 403. -
Periodic tasks run independently, so a slow one never delays alerts. Each background task (alert dispatching, digests, feed and integration scheduling, housekeeping and the others) now has its own schedule instead of waiting its turn behind the others, so a slow or stuck task no longer holds up notifications. At most 4 run at the same time in one server, which keeps them to at most 8 of the 20 database connections, and a task never runs twice at once.
ensure-feedsnow also queues the feeds that are due, so a new feed still syncs at once. On shutdown a running task has the 30-second shutdown deadline to finish before it is cancelled and handed to another replica. Seedocs/reliability.md, "Periodic tasks". -
Feed syncs write only what changed. Each advisory now keeps a hash of what is stored for it. A sync skips a record whose hash is unchanged and, for a changed one, updates the advisory and deletes or inserts only the package rows that differ. Before, every sync deleted and re-inserted all package rows of every record it read, so a full re-read of Ubuntu rewrote millions of rows and left them as dead space. On a copy of the Debian, npm and PyPI archives and a sixteenth of Ubuntu's (666,000 package rows), a full re-read of unchanged data went from deleting and re-inserting 664,480 rows in 4 minutes 32 seconds to writing nothing in 36 seconds (laptop, PostgreSQL 17 in Docker), and re-evaluates no components (it re-evaluated every component of those ecosystems).
-
Advisory package data takes about an eighth of the space. An affected entry keeps only its ranges and listed versions, which is all the matcher and the second check read (the package is in the row's own columns; severities and other feed fields were already read when the record was synced). Identical entries are stored once in
affected_values: 666,000 rows share 68,000 distinct entries. On the same data, the table with its TOAST and indexes went from 644 MB to 121 MB includingaffected_values, afterVACUUM FULL(1,008 to 191 bytes per row); after a full re-read with the old code and beforeVACUUM FULLit was 1,118 MB. -
A full archive read re-evaluates only components whose advisories changed, not every component of the ecosystem. Everything is still re-evaluated when the feed's coverage changes (a release is added or dropped), when it was stale or had never synced, and daily as before. Packages a sync changed are recorded with the advisories, so a sync that is interrupted after storing records still has them re-evaluated by the next one.
-
A withdrawn advisory keeps no package rows (nothing matched them); its advisory row records the withdrawal.
-
Intelligence ▸ Changed last sync counts the advisories a sync changed, not the records it read.
-
Findings are the same as before: on the data above, the same 25,470 findings with the same status, severity, fixed version, sources, verification and priority. The rows of a package are read in stored order, so the deciding record and the order of
matched_rangesandosv_ecosystemsin a finding's evidence no longer move when a feed is synced again (before, a re-read of unchanged data reordered them in 444 of those findings). -
A KEV catalogue sync updates only the advisories whose known-exploited flag changes; it rewrote every advisory each time.
-
Feed syncs that store the same advisories at the same time (one advisory is in the npm and the PyPI archive) wait for each other instead of failing with a deadlock.
Fixed
-
Evaluations and the priority pass no longer deadlock. An evaluation changed findings in the order the matcher listed them, while the priority pass changed them in no fixed order, so the two could each hold a finding the other wanted; Postgres then aborted one of them (nothing was lost: the job failed an attempt and succeeded on its retry). Writers of many findings now lock them in id order before changing any: an evaluation, the priority pass, resolving findings whose component is gone, ignoring an issue (one finding or an approved request), ending an ignore rule, snooze expiry, escalation, and merging assets. The rest of the minute-by-minute upkeep of findings (ignore rules, fix plans, incidents) no longer runs while an evaluation of the workspace does: it waits up to 5 seconds, then delivers what is queued and leaves the upkeep to the next minute (after three such minutes in a row it waits up to 30 seconds). Recording second checks, and an evaluation's own priority pass, run under the evaluation lock too.
-
Add an asset in a project's settings no longer offers assets already in the project: they show as "already in this project" and cannot be picked.
-
The asset page header has a separator before the project chips (it read "first seen 15 min agoIn Shop").
-
A fix that is urgent because it matches an SOS rule, not because it is P1, says Urgent: SOS rule, so the label does not look wrong next to a P2 badge.
-
The first summary after upgrading to 0.6 or later no longer lists every existing asset as newly connected ("Daily summary: 51 fixed, 11 assets connected"). Migration
0030marked existing assets as announced at the time of the upgrade, and summaries treat an asset announced in their period as new. Migration0035sets those assets back to when they were first seen; assets connected since keep their time. Installations that already sent that summary see nothing change; a weekly summary due after the upgrade will no longer repeat it. -
The second check understands kernel source packages. On Ubuntu it called 295 of 302 kernel findings a "package identity mismatch": a running kernel is several binary packages named after the kernel ABI (linux-image-5.4.0-216-generic, linux-modules-, linux-modules-extra-, linux-image-unsigned-), the image built by a signed wrapper source (linux-signed, linux-signed-aws-5.15, linux-signed-hwe-5.15, Debian's linux-signed-amd64), and advisories name the kernel source (linux, linux-aws-5.15, linux-hwe-5.15, linux-azure, linux-gcp, linux-oracle, linux-lowlatency). It now maps the wrapper to the kernel source it wraps, from the upstream qualifier or the source package, with its own rules, and compares the binary's version (the kernel source's version,
~20.04.1backports included) with that stream's records only. A kernel that is not fixed still agrees as affected, one at or above the fix still disagrees, and a record for another kernel stream (linux for an hwe or aws kernel), or for a package that is not a kernel, still disagrees. When neither upstream nor source package is reported it says it could not check, because the binary name alone does not tell linux-aws from linux-aws-5.15. Metapackages (linux-meta-) are not mapped: their versions use another scheme. RHEL, Rocky, Alma and Amazon Linux kernels were already checked correctly; SUSE stays could not check. Each result now records the version of the second check's rules, and results made under older rules are redone on the next runs, so the false disagreements clear themselves within minutes of upgrading (priorities and alerts were never affected). -
A Jira escalation that creates the incident's issue now adds its "Escalated: ..." comment. When an escalation was the delivery that created a new incident's issue (the incident's earlier deliveries had failed, or with several replicas it won the race to create the issue), the issue never said the incident had been escalated.
Upgrading
- Migration
0039addsworkspaces.asset_limit(empty: no limit) and therefused_assetstable. Nothing changes until a limit is set. - Migration
0037widens the check onroutes.modeto allowurgent_onlyand createsroute_issues(row-level security per workspace, hidden from connections narrowed to projects). It rewrites no rows. - Migration
0035updatesassets.announced_atfor the assets that existed when0030ran (one row per asset, no lock on findings). - Migration
0038addsslack_links,slack_link_requestsandslack_seen_requests, each with row-level security per workspace. Nothing existing changes; Slack channels keep their link buttons until an administrator adds a signing secret. - Migration
0036addsadvisories.content_hash,advisory_packages.affected_hash, and the tablesaffected_valuesandpending_packages, and makesadvisory_packages.affectednullable. Nothing is rewritten and it waits at most 10 seconds for each lock. It also clears the cursor of every OSV feed, so the next sync of each reads its full archive once: that sync stores every advisory's hash and rewrites its package rows in the new form, which takes about as long as a full archive read did before (Ubuntu is the largest) and re-evaluates the components of that ecosystem. Rows not yet rewritten are read as before in the meantime. - Reclaim the space afterwards. The old rows, and the dead rows earlier syncs left behind, stay in the table file until it is rewritten. Once every OSV feed shows a sync after the upgrade on Intelligence, run
VACUUM FULL advisory_packages; VACUUM FULL advisories; VACUUM FULL affected_values;in a quiet moment.VACUUM FULLholds an exclusive lock on each table while it copies the live rows: evaluations, feed syncs, the finding page and the second check wait for it and then continue; ingest and sign-in are not affected. It takes about one read of the old file plus one write of the new, much smaller one: 16 seconds for the 1.1 GB, 670,000-row table on the test data above, a few minutes for 3.5 million rows on a server with an SSD.pg_repackavoids the long lock but needs a unique key onadvisory_packages, which has none: see the operations guide for the extra steps. See docs/operations.md.
0.9.0
Security
Proof: what Stackurity checked, why it believes a finding, and limits on scan jobs.
- Scan jobs outside the packs sandbox have size, count and time limits, and their output is cleaned before it is stored. A repository clone may use at most 1 GiB on disk and an image scan at most 4 GiB of layers, measured while git or Syft runs; past that, or at the deadline, the whole process group is killed (killing only git or Syft left their helpers running). Lockfile walks stop at 500,000 entries and 64 levels and with the scan's deadline (a cancelled repository scan kept walking), hold at most 512 MiB for stack packs, and cap coverage at 1,000 items. Host collections stop after 30 minutes. Every batch has NUL and control characters removed from its text (one NUL failed the whole ingest), control characters refused in purls and versions, environment, provider and region limited to 256 bytes, and link values with a
javascript:,data:,vbscript:orfile:scheme dropped. Plugin metadata is bounded and its homepage must be an http or https address. Seedocs/security-audit.md, "Scan jobs outside the packs sandbox".
Added
- Set up Stackurity: the first ten minutes. A new organisation's administrators start in a short flow: create a project; add a Linux server (one install command, first results in about 2 minutes), a code repository (a CI token with a GitHub Actions step or
stackurity scan, results on the next CI run) or an SBOM (under a minute), and what is added lands in the project; a waiting screen that counts what arrives; the first result in plain words ("We found 1,676 packages on commmerceblog. 3 fixes matter now."); notifications for the project by Slack or email, urgent right away or a weekly summary; and invitations. Every step can be skipped, and Home keeps a checklist until they are done. Organisations that already have assets never see it.GET/PUT /onboarding,GET /onboarding/results. - Invite people by email, with a role on a project. An invitation is a link that works once, for 7 days, can be withdrawn, and creates the account (or adds an existing one, signed in as itself) with only that role in only that organisation. It is emailed through the organisation's email channel, or shown once to copy. Tokens are stored as SHA-256, following links is rate limited per address and sending per organisation, and every step is audited (
invitation.create,invitation.revoke,invitation.accept). Also on each project's page under Settings ▸ Projects and access.POST/GET /invitations,DELETE /invitations/{id},POST /invitations/preview,POST /invitations/accept. - Assign to me, Snooze and Open on every fix in Slack and email. URL buttons in Slack (they work with a plain incoming webhook; no Slack app), links in email and Teams,
actionsin webhook JSON. They open the fix with the action ready, and the person confirms with one tap after signing in; following a link changes nothing. Acting from inside Slack (interactivity) is for later: it needs a public Slack app endpoint and a mapping of Slack users. - Links open in the route's project. Every link in a message of a route limited to projects carries
?project=, so the dashboard opens scoped to it. - Collector tokens and enrollment codes take
project_id, and an admin's SBOM upload?project_id=: the assets they create are added to that project.
Proof:
- Why it matched, in one sentence, on every finding. For example: "Installed 6.10.0 (from web/package-lock.json, seen 2 h ago) is in the affected range < 7.0.7 of GHSA-xxxx (osv:npm, refreshed 3 h ago), compared with SemVer rules; fixed in 7.0.7." The matcher stores the rule that matched when it makes the finding (
evidence.match: the rule, the deciding record, a SEMVER range, a distro backport, a normalised version, why it is ambiguous), so the sentence is the rule that really matched. It leads the finding page and iswhyinGET /findings/{id},GET /findings/{id}/explainand theexplain_findingtool, with the version evidence, the source and its last refresh, the evidence state, Missing information (what exactly would resolve a finding that is not confirmed) and typed detail for package advisories, OS packages, container images and end of life. - An independent second check of high-impact findings. P1, SOS, known exploited, regressed, unconfirmed and ambiguous findings are re-derived from the raw stored advisory record and the raw reported version by separate, deliberately simple code (its own package URL parser, release rules, version comparators and range reading). The result is recorded apart from detection: agreed, disagreed (with what differs) or could not check (with why). A disagreement is never silently accepted and never hides an issue: the finding and lists say "Needs validation: the second check disagreed (what differs)" and Home counts it for review, while its priority, SOS and urgent alerts stay as they are (the second check is deliberately simple, so a mistake in it fails toward alerting). It is counted on Coverage and in
stackurity_second_check_findingsandstackurity_second_check_disagreements_total, and logged with its inputs for new matching tests.GET /second-checks,GET /findings?second_check=disagreed. It runs in the background, 500 findings per workspace per minute; over the matching corpus it agrees with every finding it can read (0 disagreements on 156 findings). - Help: "How Stackurity knows" explains the sentence, the evidence states and the second check.
docs/verification.mdnow holds the one definition of confirmed, needs validation, stale and rejected.
Changed
- A quiet first scan is quiet everywhere. A new asset's backlog (what it reported before its "connected" message) no longer creates Jira issues, is never escalated, and is not counted in overdue reminders or in summaries' overdue and not-acknowledged counts; a late part of it, and an issue that comes back before the asset is announced, raise no alert either. Issues found after the first scan alert, escalate and become overdue as before.
- Connected messages and summaries say how much an asset's first scan found: "commmerceblog connected: 1,676 packages checked, 412 issues found, 3 fixes need you", and "1 asset connected, 412 issues found" rather than new issues.
countsgainsconnected_issues(summary) andissues(connected).
Proof:
- Wording: a confirmed finding is described as a confirmed advisory match, not proof that the issue can be exploited on the asset, and "known exploited" as exploited somewhere (CISA's catalogue), not on this asset.
- Findings carry
second_checkandsecond_check_reason, andGET /findings/summarycountssecond_check_disagreed. A second-check disagreement never changesverificationor lowers priority. - Linux and Windows host batches leave out an entry the server would refuse (and say so) instead of failing the whole batch; an SBOM's over-long supplier or reference is clipped instead of refusing the document.
Fixed
- The release check no longer holds up alerts and other background tasks. Periodic tasks run one after another, and the check waited a random 0 to 30 seconds and then up to 30 seconds for GitHub inside its turn, so after each start, and every 12 hours, notifications and the other periodic tasks could pause for up to a minute. The wait and the request now run in the background, at most one at a time, and stop when the server shuts down.
- Evaluations no longer resolve and reopen the same findings when they overlap. Two evaluations of one organisation could run at once; one that read the intelligence feeds before a sync finished could write after a newer one and resolve the findings it had just opened, which the next evaluation then reopened: a "fixed" that never happened in summaries, a duplicate incident, and possibly a wrong Check again result. Evaluations of an organisation now run one at a time (a database lock per organisation), so each reads what the previous one wrote. An evaluation that finds another running goes back to the queue for 5 to 15 seconds, leaving its worker free for other jobs; this is not counted as a failed attempt.
Upgrading
- Migration
0033addsinvitations(with row-level security per workspace),workspaces.onboarding_stateandonboarding_project_id,project_idonenrollmentsandapi_tokens, andassets.quiet_backlog. All are new tables, nullable columns or constant defaults, so no table is rewritten. Workspaces that already have assets are marked as onboarded. Existing assets keepquiet_backlogfalse: their findings keep escalating as before; every asset created after the upgrade gets a quiet first scan. - Storing a Slack webhook URL or SMTP password from the first-run flow needs
STACKURITY_SECRET_KEY; without it the flow says so, and channels can still be set up withenv:references.
Proof:
- Migration
0034createssecond_checksandsecond_check_disagreements(row-level security per workspace, narrowed to the caller's projects) and one partial index on open high-impact findings (findings_second_check). It adds no column tofindingsand rewrites no rows. Findings get their stored match rule (evidence.match) when they are next evaluated (the daily evaluation does all of them); until then their explanation says the rule was not recorded. The first second-check pass over a large workspace takes about an hour in the background (about 70 runs for 34,000 covered findings).
0.8.0
Security
- Access can be limited to projects, and the database enforces it. A person or team given a role on a project sees only that project's assets and what hangs off them, in every page, API route, search, report, export, audit view and assistant (MCP) tool. Row-level security narrows every asset-bound table for them (migration
0032), as it already isolates workspaces, so a query that forgets a filter still cannot return another project's rows. Organisation routes (users, teams, integrations, notifications, jobs, secrets, remediation plans and policies, ingest) refuse them, and every route must declare its project scope or it is refused too. Their tokens act as them. Checks, ignore rules and ignore requests follow their assets; ignoring an issue on every asset, approving such a request or ending such a rule needs an organisation-wide security or admin role. Seedocs/design/projects.md. - What members of one project learn about other projects is limited further. They never see who else was given access (grants are audited as
access_grant, which only organisation-wide roles read), tracker (Jira) links of incidents (an issue describes the whole incident), or stored incident values that span projects: priority, SOS, escalation, filters and sorting come from their own findings. Someone who assigns through a project role can assign only to the people and teams of their projects, and gets only those people in the directory, even with an organisation-wide viewer role. - Messages to a notification route limited to projects are checked before they are queued: one that links to an asset outside the projects is withheld and logged.
Added
- Check again, and know the result. A Check again button on fix pages and asset pages asks for fresh evidence: the agent on a host reports within about a minute, a Git repository connected under Integrations is scanned again on the server, and a repository scanned in CI shows its
stackurity scancommand. The fix page then says Passed (all its issues are gone), Partly fixed (how many are left, linked) or Still open, with the date and who asked; a whole-asset check also counts new issues. Each check is recorded (requested by, requested at, asset, fix or whole asset, the report that answered it, completed at, outcome and counts), listed under Activity ▸ Checks, and expires after 7 days without a report.POST /fix-groups/{id}/check,POST /assets/{id}/check,GET /checks,GET /checks/{id}. A fix whose issues are all gone shows its last check instead of "not found". - Agents pick up Check again within a minute. Between collections, 0.7 agents ask
GET /agent/checkevery 45 seconds plus jitter (--check-interval, 0 turns it off) with their own token and the assets they report, and report at once when asked. The answer comes from a small index; the server records the poll on the asset at most every 5 minutes (agent_polled_at). Agents older than 0.7 keep reporting on their interval: the dashboard says "Waiting for the agent. Agents older than 0.7 report on their interval; restart the agent to report now." with the restart command. An agent talking to an older server stops asking after the first 404. - "I applied this" on a fix sets its open issues to in progress with "applied, waiting for a check" and asks for a check. It never resolves anything: only a fresh report closes an issue (
POST /fix-groups/{id}/applied). - Regressed issues. An issue fixed by an upgrade or removal that comes back on the same asset (same component and advisory, on any version) is shown as Regressed, with its own chip, a
regressedevent in its history, a tile on Home and a preset in All issues (GET /findings?state=regressed,regressedinGET /findings/summary). Work in progress on it goes back to new. An upgrade that leaves the component vulnerable, or an issue closed only because the intelligence changed, is not a regression. - Snooze until a fix ships. Besides a date, an issue (or all of a fix's issues) can be snoozed until a free fixed version exists; it returns as new on its own, with a
fix_availableevent, when the intelligence shows one (a fix only in Ubuntu Pro or SUSE LTSS does not count). The risk policy's exception limit stays as a backstop.suppresstakes"until_fix": true. - Ignore with a reason, a scope and an expiry. Ignore replaces Accept risk in the dashboard: the reason is accepted risk, false positive, mitigated or no fix; the scope is this asset or every asset in the workspace with the same component and advisory, including assets and findings found later; the expiry is a date, until a fix ships, or never (false positives only). A false positive sets the finding's verification to
rejected, which the matcher keeps until it is reopened. Ignores are stored as rules (GET /ignore-rules,DELETE /ignore-rules/{id}ends one and returns its issues to new); expired ones return their issues to new with anexpiredevent. New actionignoreonPOST /findings/{id}/actions,/findings/actionsand/incidents/{id}/actions. - Approval inbox. Engineers and viewers, who may not snooze or ignore, can Ask to ignore an issue, a selection or a fix (
POST /ignore-requests). Security and admin users approve or decline in Settings ▸ Approvals (also under Activity), with a count on Settings; the request records who asked, why, the scope and the expiry, and decisions are in the audit log (ignore_request.create,ignore_request.approve,ignore_request.decline). - Seven states. Issues show one of New, To do, In progress, Fixed, Regressed, Snoozed and Accepted (
display_stateon every finding). The storedstatusandstatevalues are unchanged; seedocs/api.md, "The seven states". - Urgent messages and summaries include issues that came back. They are urgent whatever their priority: the next urgent message says "web-1: 3 issues came back after a fix", once, and summaries have a Came back after a fix section (
counts.regressed). There is still no message per incident. - Projects. An organisation groups its assets into projects, such as a site, a client or a product; one asset can be in several, such as a database two sites share. Settings ▸ Projects and access (admins) creates, renames and archives projects, adds and removes assets, and gives people (by email; someone new joins with access to that project only) or teams a role on a project. A role on one asset covers the rare server one contractor looks after. Every change is in the audit log (
project.*,access.grant,access.change,access.revoke). - Roles per project. The organisation-wide roles still exist; a role on a project means the same for that project's assets only, and acts per asset: an organisation viewer with engineer on one project can acknowledge and assign that project's issues and only view the rest. A team's grant applies to everyone in the team, including people who join later. Settings ▸ Users can make someone projects only (
role: none). - A project switcher next to the workspace: All projects, then each project. The choice scopes Home, Fixes, Assets and Activity, is kept in the address (
?project=) and remembered per person (PUT /auth/default-project). On phones it takes the workspace's place in the top bar. API clients sendproject_idorX-Stackurity-Project. - Assets in no project are seen by organisation-wide roles only; Home says how many there are and links to them (
GET /assets?unfiled=true), and the asset list shows each asset's projects. - Suggested groupings on the Projects page, from what Stackurity knows about how assets relate (an application runs on a host, is built from a repository): add related assets to their neighbours' project, or create a project for a related group. Nothing changes until an admin accepts.
- Notifications per project. A route can be limited to projects (
project_ids): its urgent alerts, summaries, overdue alerts and "connected" messages count and name only those projects' assets, so a client's channel hears about their project only. Not available for Jira routes yet. - The evidence report and exports per project: they follow the switcher (
GET /reports/evidence?project_id=).
Changed
- Accept risk is called Ignore in the dashboard, and accepted issues show their reason.
accept_riskstill works and records the reasonaccepted_risk. - The fix and asset pages' "how to check again" text is replaced by the Check again block.
- Incidents that span projects show each member with project access only the part they can see: counts, asset and component names, priority and title come from their findings, and an incident action changes only their findings.
GET /auth/mereturnsaccess(organisationorprojects),project_role,projectsanddefault_project_id; for a member with project access onlyroleis empty. Users and memberships carryaccess.- Resolving an identity conflict (
POST /conflicts/{id}/resolve) answers 200 withprojects_joined,grants_movedand anoticewhen the kept asset joins the merged assets' projects or takes their access grants, and recordsasset.merge_accessin the audit log.
Upgrading
- Migration
0031adds nullable or constant-default columns tofindings(regressed_at,regressed_notified_at,until_fix,ignore_reason,exception_id,verification_before) andassets(agent_polled_at), so the table is not rewritten, and two indexes onfindings(findings_identity,findings_open_regressed): on a workspace with 130,000 findings they take a few seconds and lock writes to findings meanwhile. It createsfinding_exceptions,ignore_requestsandchecks, each with row-level security per workspace. - Existing snoozes and risk acceptances keep their dates and behave as before; acceptances made before 0.7 show no reason. Issues fixed before the upgrade can be flagged regressed when they come back.
- Upgrade agents to 0.7 for Check again within a minute. Older agents keep working and report on their interval.
- Migration
0032addsprojects,project_assetsandaccess_grants, makesworkspace_members.rolenullable, and addsworkspace_members.default_project_idandroutes.project_ids. It replaces the row-level security policies of the asset-bound tables; it rewrites no rows and takes seconds even with 130,000 findings. Nothing changes for existing members until projects and grants are created: everyone keeps their organisation-wide role. - Measured on 130,000 open findings, the findings list takes the same time for organisation-wide roles as in 0.6 (58 ms against 51 ms on the same data) and 76 to 77 ms for members of a project (88 ms with a project chosen) (see
docs/design/projects.md, "Performance"). - Do not roll back to an earlier release after giving someone project access only: earlier releases cannot read a membership without a role. Remove such members, or restore the pre-upgrade backup.
- Single sign-on role mapping does not change members with project access only.
- A notification route limited to projects is stored switched off in the column releases before 0.8 read (its own switch is in
routes.project_enabled), so an older release started against the database by mistake never sends every project's alerts to a project's channel; such a release also refuses a database migrated by a newer one. - Migration
0032replaces the row-level security policies of about twenty tables and waits for open transactions on them. It gives up after 10 seconds per lock (lock_timeout) instead of blocking the application; if it does, run the upgrade again in a quieter moment. Nothing is half applied.
0.6.0
Added
- Send feedback from the dashboard. Send feedback in the account menu (and under More on phones) opens a dialog to report a problem, suggest an improvement or ask a question. The server forwards it to the Stackurity team (
POST /api/v1/feedback, at most 10 an hour per user) with only the message, the email address if the user leaves it in, the page path, the version and the edition; never findings, asset, host or workspace names, or the installation's URL. Each send is recorded in the audit log asfeedback.send.STACKURITY_FEEDBACK_URL(defaulthttps://stackurity.dev/feedback) sends it elsewhere, and an empty value turns it off and hides the menu item (GET /auth/mereportsfeedback_enabled). Seedocs/network.md, "Feedback". - Home shows what needs you. It opens with "N fixes need you": the fixes with a fix available that match an SOS rule or have a P1 issue, on production assets or ones with no environment, most urgent first, each with where, how many issues it clears, the kind of change and How to fix, Assign and Snooze buttons. A line says how many issues it does not show and why (no free fix yet, kernels that are not running, assets that are not production, lower priority, snoozed, accepted), tiles count urgent and known-exploited issues, issues fixed in the last 7 days and end-of-life software, and a short list shows the assets needing attention with when each was last checked. Every number is a link to the list it counts.
GET /fix-groups?bucket=andfocusinGET /fix-groups/summaryprovide it. - Fix pages say what, where, how, and how to check again. A fix leads with one sentence on what it clears and why it matters, then where (asset, files, packages, when the asset was last checked and the command to check again: restart the agent on a server,
stackurity scanfor a repository), then how (the command with Copy, the kind of change, a warning for major upgrades, the recommended version), the issues it clears, and Assign, Snooze and Accept risk for all its issues at once (up to 2,000). - All issues has one-tap presets (Open, Urgent, Assigned to me, New this week, Known exploited), a Filters panel instead of the wall of filters (a sheet from the bottom on phones), Group by asset, and cards instead of a table on phones.
GET /findingstakesnew_within,resolved_withinandsort=asset;GET /findings/summaryreturnsresolved_7d. - Settings has an index page with every setting grouped by what it is for.
Changed
-
Five places instead of twenty-five. The menu is Home, Fixes, Assets, Activity and Settings. Fixes holds the action plan (now "Fixes"), All issues (the findings list) and Automatic fixes (remediation plans, Pro, with an empty state that explains how to turn them on); Assets holds Components and Coverage; Activity holds Incidents, Outcomes, the evidence report and the audit log; Settings also holds the automatic fix policies and Intelligence. Old addresses (
/action-plan,/findings,/remediation,/remediation?tab=policies) redirect with their filters, and notifications and Jira issues link to/fixes. -
One header. The workspace and edition are on the left (a menu when you belong to several workspaces), search in the middle, and one account menu on the right with your email and role, Help, Send feedback and Log out.
-
Phones get a bottom tab bar (Home, Fixes, Assets, More) instead of the icon rail, 44-pixel touch targets, and no sideways scrolling at 360 and 390 pixels on Home, Fixes, All issues, fix pages and asset pages.
-
Asset pages lead with health: how many fixes need you, when the asset was last checked and how to check again. Attributes and identifiers are under a collapsed Details, and Delete asset moved to a danger zone at the bottom. The Action plan tab is now Fixes, Findings is Issues and Remediation is Automatic fixes.
-
Suppress is called Snooze in the dashboard. The API, the stored state (
suppressed) and the audit log are unchanged. -
The Overview's inventory statistics moved to Assets ▸ Inventory at a glance, and the priority and severity tiles to All issues ▸ Counts by priority and severity.
-
Notifications are a few messages that bring people to the dashboard, not one per incident. In a production workspace with 11,219 open incidents, one Slack channel got 3,528 messages in a week, 2,869 escalations on one day and 1,521 in one hour, which Slack cut off. Per route there is now at most:
- one urgent message per dispatcher run (every minute), only when it found new issues at the route's urgent priority (P1 by default for new routes), SOS or known exploited, summarised per asset with up to 3 fixes from Fixes: "commmerceblog: 6 new urgent issues. Top fix: Install security updates and reboot (clears 2,913 issues)". Messages over
max_per_hourare counted in the next summary, never sent one by one; - one daily or weekly summary at a time of day in the route's time zone: new issues by asset, issues fixed and the fix that closed them ("pos-api: 1,240 closed after Install security updates and reboot"), the top 3 fixes still open, overdue counts and open problems; skipped when nothing changed;
- one overdue message a day (Pro, routes with overdue reminders) when urgent issues pass their fix deadline: "37 urgent issues past their deadline across 3 fixes". Escalation levels are still recorded on incidents and findings; there is no message per incident any more;
- one connected message for a new asset's first scan ("commmerceblog connected: 1,676 packages checked, 3 fixes need you") instead of urgent alerts for its backlog.
Resolved incidents are counted in summaries, never announced one by one, and the "more than 3 incidents" digest and the hourly rate-limit digest are gone. Routes have a mode: Urgent right away + daily summary (default), Daily summary only or Weekly summary only. Slack messages use Block Kit at the top level (not an attachment Slack folds away), email is plain text and HTML, and every item links to the dashboard (an asset's issues in All issues, a fix page in Fixes, the overdue list). Webhook JSON keeps its fields and adds
route,sections,counts,periodandsettings_link; the events areurgent,summary,overdue,connected(andtest). Seedocs/api.md, "Notification messages". - one urgent message per dispatcher run (every minute), only when it found new issues at the route's urgent priority (P1 by default for new routes), SOS or known exploited, summarised per asset with up to 3 fixes from Fixes: "commmerceblog: 6 new urgent issues. Top fix: Install security updates and reboot (clears 2,913 issues)". Messages over
-
Jira channels are unchanged: one issue per incident, with escalations as comments.
Fixed
- The findings list loads in well under a second on large workspaces. On a copy of a workspace with 130,861 open findings,
GET /findingstook 2.3 to 4.5 seconds andGET /findings/summary1.2 to 1.5 seconds; they now take about 0.04 and 0.07 seconds. The risk pass rewrote every open finding every minute even when nothing changed, which bloated the table (about 4 KB a row) and kept the database from answering counts from indexes; it now writes only findings whose priority, factors, SOS or deadlines changed. Migration0029adds indexes for the list's order and for the counts, and the summary is one pass.
Upgrading
- Migration
0029adds five indexes onfindings; on a workspace with 130,000 findings it takes under a minute and locks writes to findings meanwhile. Bookmarks keep working: the old addresses redirect. - The old risk pass left the
findingstable several times larger than its data (553 MB for 130,000 findings, 157 MB after compaction). Searches and other lists that read every finding are faster once it is compacted: in a quiet period runVACUUM (FULL, ANALYZE) findings;(it locks the table, about 10 seconds per 130,000 findings) or usepg_repack. - Migration
0030addsroutes.mode,summary_time,summary_dayandlast_summary_at,assets.announced_atandfindings.resolved_fix_groupandresolved_fix_title. Every existing route becomes Urgent right away + daily summary at 09:00 in its time zone, keeping its filters, quiet hours and hourly limit; its minimum priority becomes the urgent threshold. A route marked escalation used to receive escalations only: it now sends urgent messages and summaries too, with the overdue section and the daily overdue message. If it sends to the same channel as another route, that channel now hears from both; delete one or narrow it.send_resolvednow decides whether summaries list fixed issues. Every existing asset counts as announced. Per-incident messages still queued for people (not Jira) are held and counted in each route's first summary. - Webhook receivers that act on
opened,priority_raised,escalatedorresolvedevents for email, Slack, Teams or webhook channels receiveurgent,summary,overdueandconnectedinstead.
0.5.1
Added
- Release images are also published to Docker Hub as
stackurity/stackurity(same signed image as GHCR), and themirror-dockerhubworkflow copies an existing release there.
Changed
- Kernel findings are reported per kernel version. The packages with kernel code of one installed kernel version (image, modules, extra modules, tools; on RPM systems
kernel,kernel-core,kernel-modules*and their flavours, SUSEkernel-default*) share one finding per CVE, on the package that boots, with the others listed in its evidence. Kernels that are installed but not running keep that one finding, marked not running (P4, under Remove old kernels). On a pilot Ubuntu 20.04 host with three kernels, 119,719 of 129,732 open findings were kernel packages repeating each CVE; a fixture built like it goes from 510 findings and 100 P1 to 150 and 20, with each exploited CVE P1 once, on the running kernel. Seedocs/compatibility.md, "Kernels". - Ubuntu Pro and SUSE LTSS are no longer presented as the fix. They are paid subscriptions: the action plan's primary action is the free path ("Fixed in Ubuntu Pro (paid subscription); without it, upgrade to a supported release" on an end-of-life release; install the update when the distribution publishes a free fix on a supported one). The "Issues fixed only in Ubuntu Pro (ESM)" group is now "Issues with no free fix yet", the finding badge "fix needs Ubuntu Pro" is "no free fix", the group badge "needs Ubuntu Pro" is "Ubuntu Pro optional (paid)", and upgrade recommendations, notifications and the MCP
remediation_plantool (which now returnspaid_channeland each group'snoteinstead ofneeds) say the same. - Findings whose only fix is in SUSE LTSS no longer get the risk score's "fix available" bonus, as Ubuntu Pro-only fixes already did not.
Fixed
-
Kernel headers no longer produce vulnerability findings.
linux-headers-*,linux-libc-dev,linux-tools-common,linux-source*, and on RPM systemskernel-headers,kernel-develand the other-develpackages hold no running code; they are recorded as checked with the reason "kernel headers or build files, not running code". Their old findings are resolved as "no longer affected (re-evaluated)" on the first evaluation after the upgrade. -
No upstream end-of-life finding on a distribution's own package while the distribution release is supported. nginx 1.18 and PHP 8.1 on Ubuntu 22.04, nginx 1.24 on Ubuntu 24.04 or PHP 8.0 on RHEL 9 are patched by the distribution; the component and asset pages show the upstream status and the distribution's support date instead ("nginx 1.18 is end of life upstream since 2021-04-20; this is Ubuntu 22.04's own package, and Ubuntu 22.04 provides security updates until 2027-06-30"). On a release that is itself end of life the operating system's end-of-life finding stands alone and the release upgrade lists the packages. Runtimes installed outside the package manager, builds recognised as coming from another repository, and releases without lifecycle data keep the upstream finding. See
docs/compatibility.md, "Lifecycle of distribution packages". -
stackurity scan --fail-on-priorityno longer gates on unscored findings. The collection result saidevaluatedas soon as matching finished, while the findings still had the default priority (P4, score 0) until the periodic risk pass scored them: 447 findings across 5 repositories printed as P4 and were P2 and P3 minutes later, so a P2 gate passed. The evaluate job now prioritises the asset's findings before marking it evaluated, the result reportsscoringin between, and the CLI also waits until every finding carries a policy version (for older servers). When--waitruns out first the command exits with status 4, "result not ready", instead of passing. -
stackurity scan DIRno longer counts other checkouts' lockfiles. It walked into.claude/worktrees/*(coding-agent worktrees) and other nested repositories, clones and worktrees, reporting the same lockfiles once per checkout (1658 observations instead of 340 on one repository). It now skips.claude/and any directory below the root with its own.gitdirectory or worktree.gitfile (submodules are still scanned), lists each skipped checkout once in coverage as "skipped: nested checkout", and, when the directory is in a git work tree andgitis installed, skips paths git ignores (git ls-files --others --ignored --exclude-standard, with the repository'score.fsmonitorturned off). These skips do not mark the scan incomplete.
Upgrading
- Migration
0028addsinventory.lifecycle_note. The first evaluation after the upgrade resolves the duplicate kernel findings and the distribution-package end-of-life findings as re-evaluated, so the outcomes report shows a one-time drop attributed to intelligence.
0.5.0
Security
- The quick start no longer turns off the
Secureflag on the session cookie. Chrome, Edge and Firefox keep aSecurecookie onhttp://localhost, so a trial on your own machine runs with the same cookie settings as production; the README separates "Try it on your own machine" from "Production install" (HTTPS through a reverse proxy). Safari does not sendSecurecookies tohttp://localhost; the documented workaround is labelled local evaluation only and keeps the port on loopback. - When
STACKURITY_COOKIE_SECURE=falseand the deployment is not declared local (STACKURITY_BASE_URLanhttp://loopback address, or no base URL and a loopbackSTACKURITY_LISTEN), the server logs a warning at startup and Platform health shows "Session cookies are not marked Secure". Anhttps://base URL with insecure cookies always warns. Seedocs/install.md, "TLS".
Added
- Mixed versions are refused. A server,
stackurity migrateorstackurity admincommand refuses a database that a newer version has migrated ("this database was upgraded by a newer Stackurity; run that version or restore the backup") and changes nothing in it. A running server checks every minute: once a newer version has migrated its database it stops its background work,/readyzreturns 503schema_newer, it logs an error and Platform health shows a critical problem, so orchestrators stop routing to replicas left on the old version. The documented rollback (restore the pre-upgrade backup, start the previous image) is unaffected. - An upgrade checklist for single and multi-replica deployments in
docs/install.md("Upgrading"), stating that mixed-version operation is unsupported and why, and that the Helm chart'sRecreatestrategy must not be replaced byRollingUpdatefor upgrades. - CI checks that the Helm chart renders
strategy: type: Recreateby default, and the upgrade release check now starts the previous version against the database the new one migrated and expects it to refuse (and a previous-version replica left running to fail/readyz). This applies to upgrades from releases after 0.4.0, which have the guard. - Recommended Prometheus alerts (
deploy/prometheus/stackurity-alerts.yml): 25 alerts with thresholds, a severity label, a summary and a runbook link, covering servers down or restarting, mixed versions, the database down, pool saturation, API and ingest errors, critical platform health, stale and failing feeds, periodic tasks that stop succeeding (stuck leases), failed jobs, job queue age, failed notifications, failing channels, a notification backlog and assets that stop reporting. They are unit tested withpromtool test rulesin CI. The Helm chart renders the same rules as a PrometheusRule withmetrics.prometheusRule.enabled=true(when the cluster servesmonitoring.coreos.com/v1). - Operations runbook (
docs/operations.md): what to watch, what each alert means and what to do, and drills with exact steps and expected observations for a database interruption, disk exhaustion, a prolonged upstream outage, a notification provider failure and an unreachable agent. Each drill except disk exhaustion is also a scenario ininternal/crashtest, run in CI. - Notification metrics and health:
stackurity_notification_deliveries{kind,state}(pending, retrying, held, failed in the last 24 hours),stackurity_notification_oldest_due_age_secondsandstackurity_notification_channels_failing{kind}. Platform health warns about failed notifications, failing channels and notifications waiting over 15 minutes, andGET /api/v1/admin/healthreturns the counts. - Edition matrix (
docs/editions.md): capabilities of Community, Pro and Enterprise with their limits and prerequisites, and which need the licensed build rather than only a licence. - Tenant-isolation incident process in
SECURITY.mdanddocs/security-audit.md: reporting, triage with the logs, audit events and row-level security checks to run, containment, notification and disclosure, and the security-sensitive areas reviewers should know. - The workspace isolation suite (
tools/release-checks/isolation.sh) runs as its own Workspace isolation check on pull requests and before every release; any failure, skip or missing test blocks the release. - Risk policy preview.
POST /api/v1/risk-policy/preview(admins) validates a proposed policy like a save and scores the workspace's open findings under the current and the proposed policy in memory, inside a read-only transaction: counts per priority and SOS before and after, how many findings are raised or lowered, and up to 50 changed findings with their asset and the factors whose points differ. Settings > Risk policy requires a preview of the exact changes before saving, and shows who last changed the policy and when (last_changeinGET /risk-policy, from the audit log). The preview and prioritisation share one query, so the preview is what a save produces. - Remediation policy preview (Pro).
POST /api/v1/remediation/policies/previewshows which fixable packages and assets a new or edited policy would authorise or stop authorising, using the planner's candidates and first-matching-policy rule, without saving. The policy editor requires it before saving and shows who last changed the policy. - Health centre. Every platform health problem now has when it last worked, the latest error, what it affects (a summary, counts across workspaces, up to ten named objects with links) and a suggested next step with a documentation link. New problems: integrations whose latest run failed (and those failing for two scheduled runs or more), notification channels with failed or retrying deliveries, assets whose collector stopped reporting (with the production count), production assets with failed or partial collection, production assets running components no source could evaluate, and job kinds retried three or more times. Objects are named, and their errors shown, only for the instance administrator's active workspace; these problems never become platform incidents.
- Finding provenance. The finding page shows how each verdict was reached: every matched advisory (linked) with its feed, ecosystem and the affected range that matched, the installed version, the version rules used (Debian version rules, RPM epoch:version-release, SemVer, ...) and the evidence state (confirmed, needs validation, stale, confirmed with stale advisory data, unknown) with a one-line reason. The findings list shows the same state.
- Reports count open findings by evidence state:
open.by_evidencein the outcomes report and in the evidence report (Pro), whose exceptions also carry theirverification. - The audit log page can be opened filtered by object type (
/audit?object_type=settings). - Documentation and the Helm chart can no longer name another version than the release being published: a test fails on any image, chart or
helm --versionreference in the README,docs/install.mdordocs/kubernetes.md, or aChart.yamlversion, that differs from the newest release in this changelog, and the release workflow also checks the tag. The README's status line no longer names a version, and the supported upgrade sources are listed only inrelease-checks.yml.
Changed
- Saving a risk policy or a remediation policy in the dashboard now requires previewing the exact change first, and "Reset to default" loads the defaults into the form to preview and save. The API is unchanged.
deploy/docker-compose.ymlpassesSTACKURITY_BASE_URLto the server; before, setting it indeploy/.envhad no effect.
Fixed
- While the database was unreachable, requests with valid API tokens or sessions were answered
401, so agents and CI dropped their submissions instead of retrying and the dashboard signed people out. They now get503, which clients retry. Found by the new database-outage crash scenario. - During a database outage the scheduler logged one error per periodic task on every tick; it now logs one line per tick.
Upgrading
- Migration 0027 adds
deliveries.failed_at. Deliveries that failed before the upgrade count as failed when they were queued. - The mixed-version guard protects upgrades from 0.5.0 onwards; a 0.4.x server does not have it, so stop every 0.4.x replica before starting 0.5.0.
- Platform health reports notification failures per channel (with names and next steps) instead of as totals; the totals stay in the metrics.
0.4.0
Security
Found by turning the security documentation into tests (see "Verification" in docs/security-audit.md); each fix has its own commit.
- Workspace admins could disable shared intelligence feeds for every workspace. Enabling or disabling a feed now needs an instance administrator.
- A workspace admin could set the password, name or status of an instance administrator whose only workspace was theirs. That now needs an instance administrator.
- Zoned IPv6 link-local addresses (
fe80::1%eth0) passed the private-network check for webhooks, Jira, SMTP, Git and Kubernetes destinations. IPv4-compatible IPv6, local-use NAT64 and site-local ranges are now blocked too. - A workspace could point its routes, assets, findings and teams at another workspace's channels, teams and users by id, and probe which ids exist. Migration 0025 adds database triggers that keep references inside the workspace; team members must be members of the workspace.
- The output caps on plugins, git, stack packs and runtime detection were never applied. They now are.
- Image scans for workspaces without private network access run Syft through a guarded proxy, so DNS rebinding and redirects to internal addresses are refused.
- Versions longer than 256 characters are no longer compared (they could keep the shared evaluator busy for hours); such components are reported as not evaluated.
- Git repository URLs with IPv4 in decimal, octal or hexadecimal notation, or with a query or fragment, are refused; clone errors redact the Basic credential.
- One malformed or over-long entry in a lockfile or an installed package no longer makes the whole scan invalid; it is skipped and reported as partial coverage.
- A release
currentlink must resolve inside the application, through every link. - Delivery errors redact Slack and Teams URLs in every form; plugin logs and errors redact the plugin's credentials; failed sign-ins no longer record what was typed as the email unless it is an existing account; malformed Git and Kubernetes URLs are not echoed in errors.
Added
- Update notices for administrators. Every 12 hours one server replica reads the public list of Stackurity releases from GitHub (an anonymous request with a generic
User-Agent; no version, id or other data about the installation is sent) and compares it with the running version locally. Instance administrators see a banner when a newer release exists (dismissable per version) or a stronger one when a newer release has a Security section (it can only be snoozed for 24 hours), and Settings → What's new (/updates, also linked from the version in the sidebar) shows the notes of every newer release with upgrade steps.GET /api/v1/updatesreturns the same for instance administrators.STACKURITY_UPDATE_CHECK=offturns the check off entirely, andSTACKURITY_UPDATE_URLpoints it at a mirror; see docs/network.md. - Coverage states. Every asset says how far its findings can be trusted: Healthy and recently evaluated, Stale intelligence, Failed collection, Partially evaluated, Unsupported or Awaiting verification, each with its reason (
GET /api/v1/assets/{id}/coverage-summary, the asset page, the Coverage page and the overview). Inventory rows say whether they were checked, and the Inventory and component pages mark each component that was not ("not checked: "). Seedocs/compatibility.md. - A matching corpus of real advisories for every supported ecosystem (
internal/intel/testdata/corpus), run through the whole pipeline in the tests. - How priorities are decided (
docs/risk.md): every input and factor, how missing evidence is scored and why, a table of 36 validated scenarios and the invariants the scoring keeps. A scenario suite and property tests ininternal/riskcheck them on every build. - Each finding records the risk policy version that produced its priority (
priority_policy_version;policy_versionin the explanation is now that version, withcurrent_policy_versionnext to it). - Risk policy changes are audited in the same transaction, with the full policy before and after, both versions and the changed fields.
PUT /risk-policyreturnsversion,previous_versionandchanged. - Explanations name the evidence behind exposure (set on the asset, reported by the collector, or a workload running the image) and say when a severity is missing.
- Release checks in CI (
.github/workflows/release-checks.yml, on pull requests to main and before every release). For each earlier release still in use (0.2.0, 0.3.0 and 0.3.1, listed in the workflow), a job installs it from GHCR with demo data, a second workspace and findings, starts the current build on the same database and checks that migrations apply,/readyzis 200, the API answers and nothing was lost; restores apg_dumpof the result into a new PostgreSQL server (which has nostackurity_approle) and checks again; and runs the documented rollback. A tag publishes nothing unless these pass. Run them locally withtools/release-checks/upgrade.sh. docs/install.mddescribes how to roll back an upgrade (recreate the database, restore the pre-upgrade backup, start the previous image), anddocs/remediation.mdhas a "Failure handling" section on what automatic fixing does when a change, an agent or the network fails.- Crash recovery tests (
internal/crashtest, run in CI): the real server is killed during a feed refresh, an inventory upload, a notification delivery, a periodic task and a job, and restarted.docs/reliability.mdlists what is guaranteed (previous data kept, work resumed or failed visibly, findings never duplicated, notifications at least once with a delivery id to deduplicate) and how each is tested. - The Helm chart is published and signed with each release:
helm install stackurity oci://ghcr.io/stackurity/charts/stackurity --version 0.4.0. Also new outside this repository: the GitHub Actionstackurity/scan-action@v1and the Homebrew formulabrew install stackurity/tap/stackurity.
Changed
- An asset with no environment adds
environment_unknown(default +0.5) instead of scoring like a known non-production asset, and SOS rules that require production match it, as unknown exposure already matches rules that require exposure. A confirmed known-exploited finding on an asset with no environment is now SOS; set environments to avoid it. - The risk policy editor rejects weights that point the wrong way (for example negative points for known exploitation, or unknown exposure weighted above exposed). Stored policies keep working until next saved.
PUT /risk-policyfills fields it leaves out with their defaults instead of 0, as reading a stored policy already did.- Automatic fixing (Pro): an approval by a person lapses if the rollout has not started within 14 days; the plan is cancelled and proposed again for a fresh decision. A host whose package changed since the plan was made is dropped from the plan before release, instead of failing the rollout when its agent refuses the stale change.
- The agent retries a remediation result report through brief network or server outages (four attempts). The server accepts a repeated report without recording it twice, and answers 409, not 404, to a report that contradicts what is recorded.
- GitHub release notes are now this changelog's section for the version, instead of a generated list of pull requests, and the release workflow fails when the section is missing. Tags with a prerelease suffix (
v0.4.0-rc.1) are published as prereleases.
Fixed
These change findings after upgrading: expect new findings on Rocky Linux and RHEL 10 hosts and for Drupal modules, and fewer on Ubuntu ESM and FIPS hosts.
- Rocky Linux binary packages such as
openssl-libswere never matched: Rocky's advisories name source packages. - RHEL 10 hosts matched no advisory: Red Hat publishes RHEL 10 per minor release.
- Ubuntu Pro FIPS channels were applied to regular builds (patched ESM hosts were reported affected, with a FIPS build as the fix) and regular fixes to FIPS builds. For FIPS builds, FIPS-updates is preferred over the frozen certified channel.
- Composer advisories published for another repository (Drupal contrib modules) were never matched.
- Fixes published only for SLES for SAP or HPC are labelled as needing a subscription on plain SLES.
- A release the feed has no advisories for (too new, or no longer covered) is reported as not evaluated instead of clean; so are unreadable RubyGems versions, unresolved Maven properties and, when a record matched without a comparison, any unparseable version.
- The Go standard library reported as
go1.22.1and PyPI names with dots are matched. - Evaluation gaps are recorded per inventory row: evaluating another asset or workspace no longer clears a gap. Reasons say exactly why a component could not be checked.
- A saved SOS rule picked up the conditions of the default rule at the same position that it did not set itself (for example
production: true), so the rule applied was narrower than the one shown. - An image run by one internal workload and one whose exposure could not be decided scored as internal; it is now unknown.
- Lower confidence (
needs_validation,stale) no longer raises a score that is already below zero. - Automatic fixing (Pro): a change already on a host when its plan was revoked, its policy disabled or its rollout halted was never verified or timed out. It stayed in flight for ever and blocked every later plan for that package on that host.
- Automatic fixing (Pro): when an agent applied a change but its report was lost, the change timed out after two hours and, with
auto_rollback, was rolled back although it had worked. Fresh inventory now settles such changes, and timeouts never queue a rollback, since nothing is known to have changed. - Automatic fixing (Pro): a change released shortly before the maintenance window closed, and not fetched in time, failed the whole plan at its dispatch deadline. It now waits for the next window.
- Automatic fixing (Pro): two rollout loops running at once (replicas whose worker leases overlapped) could each release up to
max_concurrentchanges. - Automatic fixing (Pro): a plan whose changes were all cancelled was marked
succeeded. A halted plan now says which host failed and why. - A background job whose server was killed on every attempt (for example a feed sync that runs the server out of memory) was retried for ever: interrupted attempts did not count towards
max_attempts. It now fails after the limit, with the interruption as its error, and shows on Platform health and in metrics. - An interrupted feed sync showed no error until its retry finished; it now shows as failing ("the sync started ... was interrupted") as soon as the next attempt starts.
- A crash between storing new advisories and queueing their evaluation could delay new findings until the daily evaluation; evaluation is now queued before the feed's cursor moves.
Upgrading
- Update agents along with the server: they retry remediation reports, which the server now expects.
- Migrations 0023 to 0025 run at startup. 0023 rewrites every inventory row once and replaces
evaluation_gapswith a view; 0025 adds triggers that refuse references to another workspace's channels, teams and users (existing rows are kept). Each feed's first sync after the upgrade scans its stored advisories once to learn which releases it covers; for Ubuntu this sync is slow. - Expect findings to change: new ones on Rocky Linux and RHEL 10 hosts and for Drupal modules, fewer on Ubuntu ESM and FIPS hosts. A confirmed known-exploited finding on an asset with no environment is now SOS.
- Approvals by a person now lapse after 14 days if the rollout has not started.
- The update check contacts
api.github.comevery 12 hours; add it to an egress allowlist or setSTACKURITY_UPDATE_CHECK=off.
0.3.1
Fixed
- Full vulnerability feed downloads (for example the Ubuntu archive, about 750 MB) were cancelled after the 20-minute job timeout on small servers and retried from the start, so the one-off re-sync after upgrading to 0.3.0 could never finish. Feed syncs may now run for 3 hours, and long jobs renew their lock so a second worker does not start the same job.
- The sidebar showed the version as "vv0.3.0".
- A finding's own fix is labelled "Fixes this issue", so it no longer reads as contradicting the package's upgrade recommendation.
0.3.0
Added
- Kubernetes. A read-only
kubernetesplugin, shipped in the image likeaws-managed, reports each cluster (control plane, kubelet, container runtime and node OS versions, for lifecycle), its workloads, the images they run (by digest) and whether each workload is reachable from outside the cluster (LoadBalancer or NodePort Services, Ingresses, host networking). Running images are queued for scanning, most exposed first. Namespaces it cannot read and images it cannot pull are reported as coverage gaps. Least-privilege RBAC:deploy/kubernetes/stackurity-reader.yaml; seedocs/kubernetes.md. - Jira. A
jiranotification channel (Jira Cloud and Data Center) opens one issue per incident, comments when findings join or leave or the priority changes, and comments and transitions it when the incident resolves. Issues are never duplicated, across retries and replicas. Issue links show on incidents. - Upgrade recommendations for every finding and installed component: minimum fixed version, recommended target (newest stable release in the installed major line that fixes every open finding), change type, withdrawn and end-of-life flags, and a rationale built only from stored data.
- Remediation outcomes report (Issues → Remediation outcomes,
GET /api/v1/reports/outcomes): time to remedy by priority, how findings were resolved, SLA compliance, reopened findings, and remediation success, failures and rollbacks. - AI assistant tools (MCP, read-only):
explain_finding,upgrade_recommendation,remediation_planandverify_since. - CVSS v4.0 scoring, ported from the FIRST reference calculator, including threat and environmental metrics. The higher of the v3 and v4 ratings is used.
- Automatic fixing (Pro) with zypper and yum: SUSE (SLES, SLED, openSUSE Leap) and Amazon Linux 2 hosts, plus RHEL, CentOS and Oracle Linux 7 through yum. Like the other managers, a change runs only when its dry run shows exactly the planned change; removals, obsoletes, vendor or architecture changes and changes in the wrong direction are refused.
- Search finds workspace members (for engineers and above) and open findings by title.
STACKURITY_PLATFORM_WORKSPACEchooses the workspace that receives platform-health incidents.- Helm: optional
metrics.serviceMonitorfor the Prometheus Operator.
Changed
- Sign-in, setup, password change, enrollment and single sign-on rate limits are shared across replicas (stored in the database) and fail closed.
- Hosts whose OS has no supported package manager (for example Amazon Linux 1, Tumbleweed, or an unknown OS) no longer get automatic-fix plans.
- Image scan failures keep Syft's error (for example
DENIEDfrom a registry) as the coverage reason. - Plugin requests carry the workspace's network policy, and
pkg/pluginhas aDialerthat enforces it.
Fixed
- Automatic fixing (Pro) on dnf hosts (RHEL, Rocky, AlmaLinux, Fedora, Amazon Linux 2023) did not check what the dry run would do: it ran with
-q, which hides the transaction, so a change was approved on dnf's exit status alone. The dry run is now read, and the change is refused if it would remove or obsolete a package, change a package's vendor or architecture, pull in weak dependencies, or move anything in the wrong direction. - Outbound webhook, Slack and Teams calls shared one connection pool across workspaces, so a workspace not allowed to reach private addresses could reuse a connection another workspace had opened to an internal host. Workspaces with different network policies now use separate pools.
Upgrading
- Update agents before enabling automatic fixing on SUSE, Amazon Linux 2 or EL7 hosts: older agents refuse yum and zypper actions (safely).
- Migrations 0017 to 0021 run automatically at startup. Migration 0021 makes each OSV feed download its full archive once on its next sync, so stored advisories are rated with their CVSS v4 vectors; expect one sync as heavy as a fresh install.
- Reopened findings are counted from this upgrade on.
0.2.0
Added
- Vulnerability matching for RHEL, SUSE, openSUSE Leap and Amazon Linux. Red Hat, SUSE and openSUSE advisories come from OSV; Amazon Linux 2 and 2023 advisories (ALAS) are read from Amazon's own repositories. Packages on these systems were inventoried before but listed as evaluation gaps. SUSE LTSS fixes are labelled as needing a subscription, like Ubuntu Pro. Container images scanned with Syft (for example Red Hat UBI and Amazon Linux base images) match as well as agent inventory.
- Latest-release data for PyPI, Go modules, crates.io, RubyGems, NuGet, Maven Central and pub.dev, alongside npm and Packagist, including deprecated packages and yanked, retracted or unlisted versions.
- Global search (
GET /api/v1/search, the search box in the top bar, and an MCPsearchtool): CVE and advisory ids, packages with or without a version, hosts and any asset identifier, image digests, repositories and teams. - Several server replicas. Periodic tasks are claimed in the database with a lease, so each runs once per interval across replicas; notification deliveries are claimed before sending; the setup token, licence and stack packs are shared.
- Prometheus metrics on an optional separate listener (
STACKURITY_METRICS_ADDR). - Platform health for instance administrators (
GET /api/v1/admin/health, Settings → Platform health, and a banner): failing or stale feeds, background tasks that stopped succeeding, failed jobs and a job backlog.
Changed
- NuGet versions are compared with NuGet's own rules (four numeric parts, case-insensitive prerelease labels). A NuGet version that cannot be parsed is now an evaluation gap instead of a loose comparison.
- Fix plans use
yumon Amazon Linux 2 andzypperon SUSE. - Automatic fixing (Pro) skips SUSE and Amazon Linux 2 hosts, which have no
dnf. - Release images are cross-compiled; building the arm64 image no longer needs emulation.
Fixed
- After a backup was restored into a new PostgreSQL server, the database role used for workspace isolation was missing: a server with several workspaces refused to start, and one with a single workspace ran without row-level security. The role and its grants are now created on every start.
Upgrading
Migrations 0014 to 0016 run automatically at startup. Keep the Helm update strategy at Recreate (the default) for this upgrade: a rolling upgrade would briefly run the old in-memory scheduler next to the new one.
0.1.0
First public release. The v0.1.0 tag exists, but no release artifacts or images were published for it; 0.2.0 is the first published release.
Also on GitHub releases, with signed binaries and checksums. Follow new releases with the RSS feed.