Release notes¶
0.11.0 — in preparation¶
The release candidates v0.11.0-rc.1, v0.11.0-rc.2 and v0.11.0-rc.3 were tagged on 2026-10-06, 2026-10-12 and 2026-10-13; 0.11.0 itself is not released yet.
What follows is what 0.11.0 will contain, against 0.10.0.
Before you upgrade¶
Each point is written out in full below; this is what to do before the new image starts. The first list
is for every installation coming from 0.10.0; one running v0.11.0-rc.1 reads it too, then the second.
- Back up the database. Migrations V65 to V90 run at startup and have no way back: going back to 0.10.0 means restoring that backup and the previous image digest (backup and restore).
- Update every
delegatedagent with the control plane. Credentials are now sealed bound to what they carry (sealed:v2:), and an agent older than this release —v0.11.0-rc.1included — is handed none: its sealing key is refused, and its scans of repositories carrying a key or a token wait for an executor that can run them (agents). - With the composition's
with-agentorssoprofile, complete.env. DeclareVECTISPIRE_AGENT_TOKENandVECTISPIRE_AGENT_SIGNING_KEY(empty if unused) — the agent no longer starts without both lines — and setKEYCLOAK_ADMIN_PASSWORD, which no longer defaults toadmin(Agent credentials and the evaluation Keycloak, below). - A pipeline that gates a target not yet scanned now fails. So does one gating a target whose last scan failed. Scan first and wait for the scan to complete, then gate; look on the Security overview for targets marked never scanned or last scan failed before the upgrade, since each of their pipelines turns red (The gate refuses a target nobody examined, below).
- Check who writes the settings that changed hands.
notification_allow_private_url,ticket_projectandticket_userare an administrator's now,evidence_retention_daysthe platform governor's, and the statement of applicability (GET /api/v1/compliance/soa) requires governance read. Stored values are kept; a script writing these throughPUT /api/v1/settingswith a CISO's key, or reading the statement without governance read, receives 403. A GitHubticket_projectstored as anything butowner/repositoryis refused at the next ticket: correct it (Three settings a CISO could change are no longer theirs and Four authorization gaps closed, below). - Tell the teams their grades will drop. The score formula counts mediums, lows and every further
issue; nothing in their repositories changed. A check reading the badge's letter or the API's
scorewill see it drop, and one readingscoreorgradeonGET /api/v1/scorecards/globalfinds neither: the portfolio has no single grade any more (Score formula, below). - Tell the SOC that
VECTI-SEC-030now carries the late issue's severity. It was a fixed 6, under the default minimum, so no breach reached the SOC; with the default, critical and high breaches now arrive, and a rule matching them on severity 6 stops matching — match the signature (Other changes, below). - Expect every unscheduled target to be rescanned weekly. All of them move to the installation's
default interval at the upgrade, spread across the week. Size the workers or agents for it, set the
targets meant to stay manual to manual only, or set
scan_default_interval_daysto0to keep the old behaviour everywhere. A script that switched rescans off by sending an interval of0must sendscanManualOnly: true:0now means the default (Every target without a schedule is now scanned weekly, below). - On MySQL, start the server with
--max-allowed-packet=160M. At the default 64 MiB a report's export is kept up to about 32 MiB and its document about 31 MiB; the shipped composition now sets it (report plugins). - An integration creating repositories unconditionally must read 409 as "already there". A repository target is filed once (A repository target is filed once, below).
- Read the approval queue for requests a tracker's refusal put there. Until 0.11.0 a ticket closed
as Won't Fix, Declined, Rejected, Risk Accepted, Withdrawn or GitHub's not planned was
queued as
not_affectedwithinline_mitigations_already_exist, and those requests stay as they are. List the issues inpending_approvalthat a webhook queued —select id, triaged_by, triage_comment from t_issue where triage_status = 'pending_approval' and triaged_by like '%_webhook'— and turn down those that came from a refusal: decide them as Will not fix — risk accepted, with a review date, if that is what they are. Nothing converts them for you: only a person can tell a refusal from a false positive in the comment. A request queued from now on is granted as asked: approving it with another settling status, by hand or by script, answers 409triage-approval-differs(Will not fix — risk accepted, below). - Re-import the rule catalogue if your active set came from it with
generic. That set keeps the rules that duplicate gitleaks, and each leaked key its SAST twin, until the catalogue is imported again and the new set activated; the activation counts, by rule, the open issues those rules leave behind before it goes ahead (rule sets). - Clear the change-password flag of the accounts the SCIM directory created. They keep it after the
upgrade and are held on the change-password screen after a single sign-on; the audit log names them
(
USER_CREATED, "SCIM provisioned account: …"), andupdate t_user set must_change_password = false where username = '…'clears it for each. The directory no longer binds an existing privileged account to an identity-provider subject unlessVECTISPIRE_OIDC_LINK_PRIVILEGED_ACCOUNTS=true— and never one with a local second factor (single sign-on). - Tell administrators that changing their own password revokes the API keys they issued. A pipeline key one of them owns stops working at the change: issue a new one.
From v0.11.0-rc.1, in addition:
- Migrations V84 to V90 run — V83 and V85 are not used; the backup above applies. From
v0.11.0-rc.2, V89 and V90 run. - Helm chart with in-cluster agents: complete the values, or
helm upgraderefuses to render. Addportsto eachagents.networkPolicy.allowToentry —[{port: 443}]for the ingress controller; give the agents a node selector (or a required node affinity) and tolerations of their own, or setagents.acknowledgeSharedNodes; and makeagents.controlPlaneUrlhttps://(installation; An in-cluster agent's way back to the Ingress, below). - Do not rely on the
.sigin a report package a release candidate produced: it was made with the platform's key over bytes a plugin wrote (A report plugin's file is no longer signed raw, below). And the report documents produced before this release recorded no targets: they are read only by somebody who sees everything.
Changes an integration can see¶
Score formula (0.11.0)¶
Grades drop because mediums, lows and every further issue now count — not because a project got worse. Nothing in your repositories changed. The scorecard's formula is replaced, everywhere at once — the cards, the project and solution scorecards, the dashboard's ranking and the public badge — (decision 0036, how it is computed).
- The score is
100 × e^(−risk points / 55), rounded and never below 1, over risk points that weigh what is open: 25 an actively exploited issue (CISA KEV, whatever its severity, counted in that class only), 10 a critical, 4 a high, 0.5 a medium, 0.125 a low, 4 a disallowed licence. Any exploited issue caps the score at 54 (D). There is no longer a bonus for a completed scan. The bands do not change. The old formula — a hundred less 25 per KEV on top of its severity, 8 per critical, 4 per high, 5 per licence, plus 5 for a scan — reached 0 at twenty-seven highs and read fifty or five hundred mediums at 100, A+. Some figures, old → new: one critical 97 A+ → 83 B; one exploited critical 72 B → 54 D; fifty mediums 100 A+ → 63 C; one disallowed licence 100 A+ → 93 A; twenty-seven highs 0 F → 14 F. On the eleven seeded targets of the comparison, eight change grade and none rises. - A new field,
riskPoints, onGET /api/v1/scorecards/repositories/{id}and…/containers/{id}, on thescorecardofGET /api/v1/projects/{id}/complianceand…/solutions/{id}/compliance, and on each row of the maturity ranking (GET /api/v1/dashboard/posture-analytics,targetScoreboard[].riskPoints) —nullexactly when the score is. Signed-in routes only. - The public badge (
GET /api/v1/scorecards/badges/{token}.svg) keeps its look and shows the letter alone — it may change letter and colour on its next render with nobody having touched the repository, and never shows the risk points: they would tell anyone reading the README how much is open, and when that moves. - A project or a solution is graded by its weakest link: its score is the lowest score among the
scanned targets of it you see, each as its own card computes it, held at the scanned share as before,
and the scorecard names that target in a new field,
weakestTarget(kind, id, name, its own score, grade and risk points). It used to be the formula over the scope's summed backlog, which graded a scope lower the more targets it held. ItsriskPointsare the whole scope's open backlog, each issue and licence once. - A scope's
licenseViolationCountcan fall. A project or a solution filing both an image and a repository that one scan named counted that scan's disallowed licences twice; each is counted once now, as each target's own card already did. GET /api/v1/scorecards/globalchanges shape: the portfolio has no single grade. A grade over a whole estate is crushed by its size or is the worst target's grade under another name. The response is nowgrades— every grade,NO_DATAincluded, with how many of the targets you see read it —,weakestTarget(nullwhen none is scanned),riskPoints,totalTargets,observedTargetsand the open counts (openCriticalCount,openHighCount,openKevCount,overdueCount,licenseViolationCount).score,grade,targetId,targetKind,targetName,hasAttestationandrecommendationsare gone, rather than kept with a meaning they no longer have: an integration thresholding the globalscoremust move to the distribution or the weakest target. The dashboard shows the same three figures.- The ranking breaks ties on the risk points, the fewer first, where it kept the listing order: inside F every score is held at 1, and at 54 the exploited cap holds many.
GET /api/v1/dashboard/trendsgainsscore_formula_changed_on, the day (ISO, UTC) this installation's grades changed formula — the day it was upgraded, written by migration V69 when the installation already held a completed scan;nullon a fresh one. The dashboard's trend chart draws a dated line there, and says it in words under the chart.- Nothing else reads the grade. The gate, SIEM events, notifications, tickets and the exports
(SARIF, VEX, CSAF, CycloneDX) are unchanged. A check of yours that reads the badge's letter or the
API's
score— "no worse than B" — will see it drop.
Every target without a schedule is now scanned weekly (0.11.0)¶
A repository or an image with neither a scan interval nor a cron expression used to be scanned only when somebody asked — and that is what every target added through the forms got. It is now rescanned on the installation's default interval, seven days unless an administrator changes it. To keep a target as it was, set it to manual only. Targets somebody deliberately left unscheduled cannot be told apart from the ones nobody scheduled, so all of them move to the default at the upgrade.
- A new setting,
scan_default_interval_days(Settings › Scanners › Scheduling, default7,0for no default — the old behaviour, for every target at once) (how). - The rounds are spread over the week. Each target under the default has its own moment in the
interval, derived from its kind and identifier, and keeps it from week to week: a thousand targets
come a handful an hour, never all at once. Migration V70 records the upgrade as their last scheduled
round, so the first round falls in the week after the upgrade rather than in its first minute —
lastScheduledScanAtof those targets reads as the upgrade until then. A target's own interval or cron expression behaves exactly as before. - New fields on
GET /api/v1/repositoriesandGET /api/v1/containers, and on what their create and update return:scanManualOnly(boolean) andschedule, the schedule in force as the server decides it —mode(manual,cron,intervalordefault) andintervalMinutes(the interval it runs at underintervalanddefault;nullfor a cron expression, for manual only and fordefaultwhen the installation has none). Readschedulerather than working the precedence out from the other fields. scanManualOnlyonPOST/PATCH /api/v1/repositoriesand/api/v1/containers.trueclears the interval and the expression; sent beside a positive interval or an expression it is refused (400); an update naming an interval or an expression without it leaves manual only. An interval of0now means "not set" — the default — and no longer "manual only", and is stored asnull(scanIntervalMinutesreads backnull, not0). A negative interval is refused (400). A script that switched rescans off by sending0must sendscanManualOnly: true.- A scheduled round is skipped while a scan of the target is running, as it was while one was waiting. The Scan button is unchanged.
- A dependency-analysis checklist rule asking for a matching schedule counts the default: a repository with no schedule of its own, under a weekly default, now matches a seven-day maximum age; one set to manual only never does.
A repository target is filed once (0.11.0)¶
Adding a repository that is already a target — the same repository, on the same branch and sub-path —
is refused, and so is an edit that would make one target another's twin. Two URLs name the same
repository once the scheme, the user part, the port, the case, a trailing .git and trailing slashes are
set aside: git@gitlab.example.org:Team/API.git and https://gitlab.example.org/team/api are one
(the rule and its limits). Another directory of a monorepo, or
another branch, is another target and is accepted as before.
POST /api/v1/repositoriesandPATCH /api/v1/repositories/{id}answer 409 with the typeurn:vectispire:problem:target-already-registered. When the caller sees the existing target, the problem carries its id asexistingRepositoryIdand thedetailnames it; otherwise neither — the refusal says the target exists and nothing of which one. A script that registers repositories unconditionally must treat this 409 as "already there".- The database enforces it (migration V73, a unique index on a hash of the three parts): two creations racing past the check end in one target and one 409, on PostgreSQL and MySQL.
- Nothing is deleted at the upgrade. Targets already filed twice keep working, are scanned and are
editable; the oldest of each pair is the one a new filing is compared with. A new route,
GET /api/v1/repositories/duplicates(administrators), lists them, grouped, oldest first, each target in the shape of the repository list, to merge by hand. - Until the first maintenance turn after the upgrade (thirty seconds after the start, then hourly), targets registered before it are not yet compared, and a duplicate of one of them is accepted — and then listed by the route above.
The gate refuses a target nobody examined (0.11.0)¶
POST /api/v1/gate answered passed: true for a target that had never been scanned: its backlog was
empty, and an empty backlog passes every policy — while the Security overview called the same target
never scanned. It now fails, closed, as the overview always said it should
(the rule).
- A new violation rule,
observation, besidekev,severityandcoverage:passed: false, HTTP 200 as for any refusal, noissueId,identifier,severityorpackage, and areasonnaming the case — no scan of the target has completed (never scanned, or only scans still pending or running), or the last scan that finished failed. It comes first inviolations. A client switching onrulemeets a fourth value. - No policy flag and no request field relaxes it.
include_triaged,fixable_onlyand the threshold still narrow the findings considered, and this rule considers none. - The verdict rests on the newest finished scan. A scan still pending or running is not an examination yet: a target re-scanned on a schedule keeps the verdict of its previous scan while the next one runs, and a target whose first scan is still running fails.
- Recorded and signalled like any refusal: a row in the verdict register, counted in its refusal
rate, and a
SECURITY_GATE_FAILEDevent to the SIEM. - The Security overview and everything built on it agree with the endpoint. A never-scanned or
last-scan-failed target is now failing there (
passed: false, the same violation), sofailingCountcounts it — onGET /api/v1/security/overview, the dashboard's failing targets and the weekly posture report — and the posture PDF reads it as failing. Theobservationfield and the never-scanned and last-scan-failed counts are unchanged; compliance figures already left these targets out of the passing ones and do not move. - The gate script, the GitHub action and the GitLab template are unchanged: they already exit 1 on
passed: false, and print the violation's reason.
Will not fix — risk accepted (0.11.0)¶
A fifth triage status, will_not_fix: the vulnerability applies, and the team decided not to fix it.
An accepted risk had no status of its own and was recorded as not_affected, with a justification picked
from a list in which none is true of an exposed product — and signed into the VEX documents as such
(decision 0041, accepting a risk).
POST /api/v1/issues/{id}/triageandPOST /api/v1/issues/triageacceptwill_not_fix. It requiresexpires_in_days(1–3650), its review date, and refuses ajustification(400): every VEX justification says why a product is not exposed. It settles likenot_affectedandfixed— it stops failing the gate and leaves the risk figures — so it goes through four-eyes like them, and lapses back tounder_reviewat its review date.GET /api/v1/issues?triage_status=will_not_fixfilters by it. A client switching ontriageStatusmeets a new value.- It is an exception.
GET /api/v1/exceptionslists it, counts it ingranted, and it is reviewed — confirmed, extended or revoked — like a clearance. - The VEX documents say the product is exposed. CycloneDX
exploitablewith the responsewill_not_fix, OpenVEXaffectedwith the action statement "Will not fix: …", CSAFknown_affectedwith ano_fix_plannedremediation. SARIF marks it suppressed, as the other settled statuses. - Every VEX format now reads a triage through one table
(how a triage reads in VEX):
pending_approvalis under investigation everywhere, anot_affectedwithout a justification is under investigation rather than given one, a resolved issue isfixed, and CycloneDX justifications are spelled in CycloneDX's vocabulary (code_not_present,code_not_reachable,requires_environment,protected_by_mitigating_control). A consumer comparing documents before and after the upgrade sees statements change on issues nobody touched: those are the fixes below. - The ticket webhook proposes
will_not_fixfor a refusal — Jira Won't Fix, Declined, Rejected; ServiceNow Won't Fix, Risk Accepted, Withdrawn; GitHub not planned; GitLab wontfix — with no justification and a proposed review date ninety days out, which the approver replaces with theirs. A refusal was queued asnot_affectedwithinline_mitigations_already_exist, a mitigation the tracker never mentioned, which the signed documents would have carried once approved; and GitHub's not planned, its only way to close without completing, which says nothing about reachability, asnot_affectedwithvulnerable_code_not_in_execute_path. Only an explicit false positive is still proposed asnot_affected(the inbound webhook). - A request awaiting approval says what it asks for, and is granted as asked. A request kept its
justification, comment and review date and dropped its status, and the approver chose one: a tracker's
Won't Fix could be granted as
not_affected, and the signed VEX then said "not exposed". The request now keeps its status (migration V88). Every issue shape (GET /api/v1/issues,GET /api/v1/issues/{id}, a triage's answer) gainstriageRequestedStatus—not_affected,will_not_fixorfixedwhiletriageStatusispending_approval, null otherwise — andGET /api/v1/exceptionsgainsrequested_statuson each entry. Approving with another settling status answers 409 with the typeurn:vectispire:problem:triage-approval-differsand the memberrequestedStatus; an approver who disagrees turns the request down (under_revieworaffected) and decides after (granted as asked, or turned down). The triage dialog opens on the requested status and the proposed review date, which it used to empty. Requests queued before the upgrade recorded no status, and are granted as the approver chooses. - A webhook no longer moves an issue a person settled (
not_affected,will_not_fix,fixed). The tracker's word is recorded in the audit log; when it contradicts the decision, the entry isTRIAGE_CONTRADICTED_BY_TRACKER, sent to the SIEM asVECTI-SEC-037(catalogue). - The four-eyes setting's description now names the three statuses it covers:
NOT_AFFECTED,WILL_NOT_FIXorFIXED.
Other changes¶
- The ticket webhook syncs every issue carrying the ticket, and answers
issueIds. A reference on two issues — one ticket attached to several findings, or a GitLab or GitHub number stored with and without its#— madePOST /api/v1/tickets/webhook/{provider}answer500at every delivery. Every such issue is now synced, and the answer gainsissueIds, every issue synced, oldest first;issueIdstays, the first of them (inbound webhook). - SCIM answers an unsupported filter
400 invalidFilter, no longer every account.GET /scim/v2/UsersevaluatesuserName eq "…"andexternalId eq "…"; any other filter —emails[value eq "…"], anand,co, an unquoted value — returned the whole list, which a directory looking an account up before creating it read as its match. It now answers400in SCIM's error schema withscimType: invalidFilter(RFC 7644 §3.4.2.2). A directory sending only those two filters, or none, sees no difference (single sign-on). - A triage can answer 409
issue-changed-meanwhile.POST /api/v1/issues/{id}/triage,POST /api/v1/issues/triageandPOST /api/v1/exceptions/{issueId}/reviewsrefuse a decision when a scan or another decision wrote the issue between the request's read and its write — the problem'stypeisurn:vectispire:problem:issue-changed-meanwhile, and nothing is recorded. Read the issue again and repeat the decision (A triage made while a scan ran is no longer lost, below). VECTI-SEC-030(remediation deadline passed) is as severe as the late issue: CEF severity 8 for a critical issue, 7 for a high, 5 for a medium, 3 for a low — in the CEF header and in the syslog priority, and the minimum severity is compared with that. It was a fixed 6, under the default minimum (High), so with the factory configuration no breach reached the SOC, not even a critical one's. With the default, critical and high breaches now arrive; Medium lets medium ones through too. A SOC rule matching these alerts on severity 6 must adapt — matchVECTI-SEC-030on the signature, and read the severity as the issue's. An event already queued at the upgrade leaves at 6, as it was raised — SIEM export.- No request is retried behind its caller's back any more. The HTTP client every outbound call goes
through sent a GET again after a 429 or a 503, sleeping whatever
Retry-Aftersaid, and after a dropped connection; a webhook, a ticket, a model review or a catalogue download that failed once now fails once, and a caller that retries says so. - The CLI is a release asset, signed like the gate script.
vectispire-cli.shis attached to the release with its Sigstore bundle,vectispire-cli.sh.cosign.bundle, signed byrelease.ymlat the tag and verified with the samecosign verify-blobcommand as the jar; the release notes print its SHA-256. The snippets of the CI/CD dialog on Repositories and ofCI_CD_INTEGRATIONused to downloadscripts/vectispire-cli.shfrom the repository's raw URL at the tag and run it unchecked; they now download the asset, compare its SHA-256 with the one they pin, and stop the job on any other file. A pipeline copied from an earlier snippet keeps working, unchecked, until it is replaced — 0.10.0 and earlier carry no CLI asset. - Publishing a checklist template version can answer 409
checklist-template-unrenderable.POST /api/v1/checklist-templates/{slug}/versions/{ordinal}/publishfills the workbook in once, as a sign-off would, and refuses a workbook no sign-off could be written into; the problem'scellsmember names each cell at fault (cell,kind—sharedorarray— andrange). See Fixed below. Opening a project checklist, or moving one to another version, can answer 409checklist-version-unrenderable—POST /api/v1/projects/{id}/checklistsruns the same trial on the version opened on or moved to, and refuses one published earlier that fails it, with the samecellsmember; nothing is stored or recorded. The version moved from is never tried.GET /api/v1/projects/{id}/checklists/offeredstill lists such a version, with a new member,unrenderable— the trial'sdetailand the samecells— which is null on every other version. - A checklist's automatic answers arrive within a minute of a scan or a report, not at once. A completed scan, or an accepted SARIF, coverage or test report, queues them with its own results, and the scheduler's next relay gives them, retrying if answering fails. They used to be given just after the commit, and a server stopping in between left the lines unanswered until the next scan. A script that reads a checklist right after uploading a report should wait for the relay — opening, moving or reopening a revision still answers at once.
- A SIEM event an audit entry signals, and
SECURITY_GATE_FAILED, are queued with what caused them. They were queued just after, in a transaction of their own, and a stop between the two lost them. If the two cannot be written together, the entry or the verdict is still written and the event queued right after it, as before; the server logs a warning when that happens. - An agent's
AGENT_RESULT_SUBMITTEDaudit entry is written up to a minute after the result, from the outbox, instead of just after it — and is no longer lost when the server stops in between. The entry's timestamp is when it is written; the moment the result was accepted is in its description ("… at 2026-…"), with the delivery's id. - The OWASP report is in the menu's Security section, after Attack paths, no longer under Compliance & evidence: it reads the findings, as the Security screens do. Same page, same access.
- The OWASP report carries what its screen links with (
GETandPOST /api/v1/repositories/{id}/owasp-review). ACATEGORYblock gainscategory, the Top 10:2021 code its heading names (A01…A10;nullon every other block).categoryFindingsgives all ten codes, a zero included, each with the number of the repository's open issues of unsettled triage placed in the category — thetotalofGET /api/v1/issues?repository_id=…&owasp_category=…&unsettled=truefor the same caller, now, not at the reviewed scan.issueLinksmaps each identifier of a finding the model was shown to the repository's open issues carrying exactly that identifier now (issueIdwhen there is one,nullwhen there are several;count); an identifier no open issue carries is left out, and one the report's prose cites without the model having been shown it is never there. A report written before this release recorded no identifier set and readsissueLinks: null. Migration V84 addst_ai_review_result.evidence_identifiers. - A checklist rule has a sixth kind,
component_present, and a measurement a new reason,inventory_absent.kindon the rules route and inboundRule,ruleKindon a measurement, may readcomponent_present; itscomponentscarry apurlPrefixand noversions. Acomponent_versionsentry may be a Maven range ([1.17,2.0)). A client that switches overkindorreasonshould handle the new tokens; one that does not still reads a rule and no data. Every rule bound before keeps its canonical form and its content digest (Components). - A
component_versionsline no longer fails on a scan whose SBOM the retention purged. It failed with "its newest analysed scan stored no SBOM" — and Vectispire answered no — although the inventory the scan left still listed every package. It now reads that inventory, ascomponent_presentdoes, and an empty one beside a purged payload is no data,inventory_absent, which withdraws the automatic no.dependency_analysisstill requires the SBOM kept: that is what it measures. - A checklist rule has a seventh kind,
change_review, and a measurement four more reasons and a source. A rule'skindmay readchange_review, withminimumApprovals,windowDaysand an optionalbranchonChecklistRuleForm(its share isminimumRatio, as coverage's minimum is); areasonmay readforge_unlinked,forge_unreadable,review_incompleteorno_change_merged, a repository'sstatusthe same, and itssourceforge_review. Only on a line bound to the new kind; a client that switches over these vocabularies exhaustively needs the new members. - A test rule may name several suite patterns,
suitePatterns, each of which must be met — unit and functional tests on one line (the rule kinds).ChecklistRuleFormgainssuitePatterns(2 to 10 globs), stated instead ofsuitePattern, never beside it; each pattern is judged on its own suites, against the sameminimumTests. A rule with one pattern is writtensuitePatternexactly as before — its canonical form and its digest unchanged, so no bound line is marked changed. - A coverage import answers
packagesState, and a checklist measurement has three more reasons.POSTandGET /api/v1/repositories/{id}/coverage-importscarrypackagesState—kept,too_many,path_refused,inconsistent, ornullfor an import accepted before this version — and a rule'sreasonmay readpackages_unrecorded,packages_not_keptorscope_matches_nothing, only on a coverage line scoped to packages. A client that switches overreasonshould handle them; one that does not still reads no data. A rule's form (boundRule, the rules route) gains an optionalscope; a rule without one keeps its canonical form and its content digest. Schema V71 adds a column and a table, nothing to do. - The project export is schema 1.1: each
inventory.components[]gainssources—build,scanneror both, who listed the component across its targets. An optional field, by the schema's own rule; a plugin written for 1.0 reads 1.1 as it read 1.0.GET /api/v1/inventory/searchoccurrences gainsource(scanner,build,both) andscannerVersion, and the project's consolidated components (GET /api/v1/projects/{id}/components)sources. A source'skindsmay now holdsbom; a client listing them should show a kind it does not know as itself, as the screen does. - A plugin refusal has a third reason,
registry_authentication_required, and a checklist measurement a matching reason,plugin_registry_authentication_required: the plugin's image declares a signer, and its registry would not let the signature be read. A client that switches overrefusalor over a measurement'sreasonshould handle the new token; one that does not know it still reads a refusal. - An issue's triage history gains entries of origin
reopen, onGET /api/v1/issues/{id}(decisions) and in a target's history (GET /api/v1/history/repositories/{id}), with a new field,previousResolvedAt— null on every other entry. Theiractoris null, as on anexpiry. The history CSV gains a last column,decision_previous_resolved_at; a target'sdecisionscount leaves reopenings out.
New¶
- Kubernetes: a Helm chart in
deploy/helm/vectispire/(installation, decision 0038). - What it runs. The control plane by digest, against an external MySQL, with its secrets as files,
a read-only root,
Recreateupgrades and an Ingress. It runs no container: every scan goes to an agent, and report plugins answer409 report-executor-unavailableon Kubernetes for now. - Where agents run. Outside the cluster on a Docker host (recommended, nothing deployed by the
chart), or, opt-in, in the cluster as pods with a privileged
docker:dindsidecar on nodes of their own. The rootless variant renders only when acknowledged, because it does not scan. - In one namespace you were given. Nothing the chart renders is cluster-wide. Agents can stay in
the release's namespace (
agents.namespace: "",agents.createNamespace: false); their privileged pod is then admitted only if that namespace's Pod Security level allows it. - What it refuses to render.
trustedProxieswithoutnetworkPolicy.enabled, since any pod could otherwise call the pod directly and name any client address; agents without a pinned signing key, with nothing inagents.networkPolicy.excludeCidrsor with that policy turned off, without a node selector and a toleration of their own, or with acontrolPlaneUrlthat is nothttps://. Each has an explicit acknowledgement where a deployment needs it, but HTTPS. It also keeps the control plane off the node of every agent, of any release in any namespace, with a required anti-affinity, and excludes the cloud metadata addresses —169.254.0.0/16and those outside it — from the agents' egress whatever the list says. - Checked in CI for each of these shapes, and that the single-namespace ones render nothing outside their namespace.
- A project's export, signed, and its published schema — the first lot of report plugins
(decision 0035,
accepted on 2026-10-03).
GET /api/v1/projects/{id}/exportreturns a zip ofexport.json— the project's targets, newest scans, gate verdicts, unresolved issues with their triage, counts, components, compliance state and checklist statements, asvectispire-project-export1.0 — andexport.json.sig, verifiable withcosign verify-blob --keyagainst the instance's public key. For write accounts and auditors who see the whole project, images included, and integration keys with theexportscope; no source, secret value, credential or e-mail address. Over 100,000 issues or components or 64 MiB, it is refused (409project-export-too-large), never cut short. The schema is served atGET /api/v1/schemas/project-export/1. AuditedPROJECT_EXPORTED, sent to the SIEM as the newVECTI-SEC-032;VECTI-SEC-033is reserved for the plugins themselves (how). - The report plugins' registry — the second lot of decision 0035. The platform governor registers a
report plugin from its manifest — an image pinned by digest, the export major it reads, the one file it
writes and its media type from a closed list (Office Open XML, OpenDocument, PDF, CSV, plain text; no
HTML, no macro-enabled package), its output ceiling and timeout, and a signer, required with no
waiver — at
POST /api/v1/report-plugins. With four-eyes on, each manifest digest waits for a second person: an administrator, a CISO or another governor approves it (POST /api/v1/report-plugins/{id}/manifests/{digest}/approval), never the account that registered it (409report-plugin-four-eyes) — even once four-eyes is switched off, since it was registered under it — while the previously approved manifest keeps serving. Setting a plugin back to a digest approved earlier waits for a second person too. A security lead switches an approved plugin on for a project they see whole (PUT /api/v1/projects/{id}/report-plugins/{pluginId}); the governor withdraws a digest with a justification, and it never runs nor registers again. No delete. Every gesture is audited (REPORT_PLUGIN_*) and sent to the SIEM as the newVECTI-SEC-031. A private plugin image is pulled and verified with the control plane's Docker configuration: the manifest holds no credential, and Vectispire stores none (how). - Report runs — the third lot of decision 0035. A write account or an auditor who sees a whole project
asks a report of a plugin switched on for it (
POST /api/v1/projects/{id}/reports, 202): the run is queued in the database and claimed by the control plane's executor — the Docker endpoint the built-in worker uses, never an agent — two at a time by default (VECTISPIRE_REPORT_CONCURRENCY). At the claim it runs the manifest approved at that moment, builds the project's export for the requester as they see it then, verifies the image's signer with cosign before the pull (no waiver, the control plane's Docker configuration for a private registry), and runs the image without any network, the export alone and read-only, one output directory bounded by the manifest'smax_output_bytesand 16 files, the manifest's timeout, exit 0 or failed. A run ispending,running,produced,failedorrefused, with its reason (GET /api/v1/projects/{id}/reports); one of a plugin per project at a time (409report-run-in-progress); its executor renews its lease while it runs, and a run whose executor died is failedexecutor_lostonce the lease lapses — seventeen minutes without a renewal. A produced run keeps the export it was given, purged by the evidence window; nothing else is kept of a run that did not produce. On MySQL that export is bounded bymax_allowed_packet: about 32 MiB at the server's default 64 MiB packet, refusedexport_too_largebefore the plugin runs; start MySQL with--max-allowed-packet=160Mfor the whole 64 MiB, as the shipped composition now does. An installation whose built-in worker is switched off cannot run report plugins in this version: 409report-executor-unavailable; a run queued before the worker was switched off is failedexecutor_unavailableonce nothing has claimed it for seventeen minutes while no executor worked, rather than left pending for ever. AuditedREPORT_REQUESTED,PROJECT_EXPORTED(VECTI-SEC-032, the export reaching a plugin),REPORT_PRODUCED,REPORT_FAILED,REPORT_REFUSED— the last sent to the SIEM as the newVECTI-SEC-033(how). - Report documents, checked, signed and downloadable — the fourth lot of decision 0035. At the end of a
run the file the plugin wrote is checked on its bytes against its manifest's media type: an Office
Open XML package through the checklist import's zip guards, its main part of the declared type, and no
VBA project, macro sheet, macro-enabled part, ActiveX control or external relationship but a hyperlink;
an OpenDocument package with its
mimetypefirst and stored and noBasic/orScripts/; a PDF from%PDF-to%%EOF; a CSV or a text in valid UTF-8, with no NUL and nothing a browser would read as HTML. A file that is not what it declared is refused, the new reasonoutput_refused, discarded unsigned — its SHA-256 kept on the run — and sent to the SIEM asVECTI-SEC-033. A file that passes is stored as a package withprovenance.json, an in-toto statement in a DSSE envelope signed by the platform's key — the file itself is never signed raw — naming the run, the project, the requester, the plugin, its manifest, image and verified signer, the export's schema and SHA-256, the file's media type and SHA-256, the product version and the signing key.GET /api/v1/projects/{id}/reports/{runId}/documentdownloads it — always an attachment,nosniff, underContent-Security-Policy: sandbox— to anybody who sees the whole project, auditedREPORT_DOWNLOADED;cosign verify-blob-attestationverifies it against the instance's public key. The signature means provenance, not truth: it says what the installation gave which image and what came back, not that the document renders the export faithfully — the kept export is how that is checked. Documents are purged with the exports by the evidence window, and with their project. On MySQL at its defaultmax_allowed_packeta run's file is lowered to about 31 MiB, which--max-allowed-packet=160Mrestores to the manifest's ceiling. The runs gainoutputMediaType,signingKeyIdandpackageSha256(how). - The demonstration report plugin — the fifth lot of decision 0035.
vectispire-report-demorenders a project's export intosummary.xlsx— aSummarysheet (the project, who asked, the version, the export's instant, the gate per target, the counts), one row per issue onIssues, one per checklist line onChecklists— and the same export into the same bytes: no clock, fixed zip dates and order. Another schema or major exits 2, an export missing a part exits 1, each with its reason. A third image is released,ghcr.io/asmolabs/vectispire-report-demo, distroless, signed by digest with the same identity as the two others, its SBOM and provenance attested; its manifest is a release asset,vectispire-report-demo.manifest.json, filled with that digest and the tag's identity and signed with its Sigstore bundle — verify it, then register it like any report plugin. It is the contract's test as much as a sample: every build renders the exports the suite generates and checks the workbook (how). - Report plugins on screen — the sixth lot of decision 0035. Administration → Report plugins, for the roles that read governance, lists every report plugin with its approved and pending digests and the history of its manifests — who registered, approved and withdrew each, and whether four-eyes applied. The platform governor registers a plugin or gives it a new manifest by pasting the JSON, which the page checks field by field and words before sending; enables and disables it; and withdraws a digest with a justification of 20 to 500 characters. A security lead approves a waiting digest; under four-eyes the account that registered it sees the button disabled and why. A project's page gains a Reports section, for a reader who sees the whole project: the plugins switched on for it (security leads switch them on and off), Request a report for write accounts and auditors, the runs with their state and reason in words — a refusal shown apart from a failure —, followed while one is waiting or running, the download of a produced run's signed package, what it holds and the link to the commands that verify it, and Download the export. Every refusal the server names (a disabled plugin, no approved manifest, no executor, a run already under way, four-eyes) is said in the reader's language. Marking a withdrawn manifest's documents comes with the next lot (how).
- Withdrawn report documents, and asking whether a document still stands — the seventh lot of decision
- Withdrawing a manifest digest now withdraws every document it produced, which stay stored and
downloadable: their runs carry
withdrawnAt,withdrawnByandwithdrawalJustification, the download answersVectispire-Document-Status: withdrawn(upheldfor every other document), and a run in hand when the digest is withdrawn signs nothing (failedplugin_unavailable).GET /api/v1/report-documents/{sha256}, for any signed-in account, takes the SHA-256 of a package or of the file inside it and answers itsstanding—upheld,withdrawnorunknown— with the runs that produced it, their plugin, manifest and image digests, signing key and withdrawal. A document of a project the caller does not see whole answersunknown, exactly as one never produced. On a project's Reports section a withdrawn document's run is marked Withdrawn with the date, who withdrew it and why, and its download is followed by what it is worth.REPORT_PLUGIN_WITHDRAWNcounts the documents withdrawn; V79 adds three indexes to the runs (how). - Forge connections: a read-only token to a GitHub or a GitLab, probed before it is kept
(Forge connections, decision 0037, lot D1 — the discovery
and the import of repositories come next).
/api/v1/forge-connections, administrators only, on screen with lot D7 (above). The token is presented to the forge once through the outbound guard and refused when it is rejected, broader than read-only (GitLab:read_api; GitHub: a fine-grained token, or a classic one on Enterprise Server, flaggedcanWrite), or the server is older than GitLab 16 or GHES 3.12. A self-managed server on the internal network is reached when the administrator says so, behind its own CA when it is pinned for that connection — there is no switch that skips verification. The token is encrypted with its row as context, replaced in place, re-sealed when the connection is saved during anENCRYPTION_KEYrotation, and never returned. AuditedFORGE_CONNECTION_CHANGEDandFORGE_CONNECTION_REFUSED; signalledVECTI-SEC-034, andVECTI-SEC-036for a blocked address or a refused scope. - Forge discoveries: a GitLab's repositories listed in the background, kept as a snapshot and compared
run to run (Discovering repositories,
decision 0037, lots D2 and D3 — choosing and importing repositories come next, and GitHub's listing with
lot D4).
POST /api/v1/forge-connections/{id}/discoveriesanswers 202 with apendingrun that a control-plane instance takes under a lease — a restart resumes it — andGET …/discoveries/{discoveryId}reports its progress; one per connection at a time (409forge-discovery-in-progress). The groups and projects the token is a member of are listed (membership=true,min_access_level=10) with their default branch, archived, fork, visibility, last activity, language and size — a value GitLab did not give is keptnull, unknown, never zero — and a project in a user's own namespace is flaggedpersonal.…/repositories?change=new|changed|gonereads the comparison; only a completed run marks a repository gone, and nothing is deleted. Bounds: thirty minutes and 20,000 repositories per run, a rate-limit wait of up to a minute spent inside it — past any of them the run endspartial. Every request goes through the outbound guard and the connection's pinned CA; a next page on another origin is never followed — the run fails, recordedFORGE_CONNECTION_REFUSEDand signalledVECTI-SEC-036. Each discovery queued is auditedFORGE_DISCOVERY_REQUESTED(a request answered 409 records nothing), not signalled. Administrators only; on screen with lot D7 (below). - Forge discoveries: a GitHub's repositories too — github.com, Enterprise Cloud with data residency
(
<subdomain>.ghe.com) and Enterprise Server 3.12 or later at/api/v3(what is listed, decision 0037, lot D4). A GitHub discovery no longer answers 409forge-discovery-unsupported. A fine-grained token lists the connection's owner; a classic token (Enterprise Server) every organisation its user belongs to and the user's own repositories, flaggedpersonaland offered unticked. The listing carries the default branch, archived, fork, visibility (internalincluded),pushed_at, language and size, pages byLinkon the connection's own origin, and waits out GitHub's primary and secondary rate limits. An organisation the token cannot read — single sign-on not authorised, an IP allow list — no longer stops anything: the run goes on, endscompleted, and names it in a new member of the discovery,unreadableNamespaces(path,reason), and indetail; none of its repositories is marked gone by that run. Imported as GitLab's are, the organisation a solution and each repository its own project. Migration V80 (t_forge_discovery.unreadable_namespaces). - Forge imports: a discovery's repositories selected, previewed and imported as ordinary targets
(Selecting and importing, decision 0037,
lots D5 and D6 — the screen is lot D7).
GET …/discoveries/{discoveryId}/selectionis the table, filtered and paged on the server — archived and forks hidden by default, inactivity, language, visibility, namespace, a path pattern, personal and present — with each repository's presence by identity (the repository form's duplicate rule: its HTTPS or SSH clone URL against every target's, whatever the branch and sub-path) and the solution and project GitLab's layout proposes (top-level group → solution, parent subgroup → project);POSTon it applies proposed / all / none / invert / add / remove.POST …/imports/previewsays what would be created, reused, skipped or refused, the clone credential per host, the first scans and who will see the new targets;POST …/importscreates up to 1,000 targets in one transaction, through the forms' own gestures and audit entries, planned again inside it so that a replay creates nothing. The forge's default branch; no schedule of its own — the default applies, at each target's own slot rather than all at the next tick; first scans only when asked, one every 60 seconds by default (10 s to 10 min); no grant. OneFORGE_IMPORT_APPLIEDentry per import, signalledVECTI-SEC-035once when it created something. Administrators only. The connection's own token never clones. Deleting an imported target drops its provenance; deleting the connection keeps the targets. Migration V78 (t_forge_import_link). - Forge connections, discovery and import on screen (On screen, decision 0037, lot D7). Administration → Forge connections, administrators only: each connection with its token's kind and scopes, whether it can write (Not reported for a GitHub fine-grained token — unknown, not read-only), its expiry announced fourteen days ahead, the network statement, the pinned CA, the last discovery and the targets imported; adding one with a CA pasted in PEM and read for its shape in words before it is sent, the probe's refusal shown as the server words it; replacing the token in place; renaming or changing the network statement or the CA; deleting, the targets kept. Each connection's page takes three steps: discover (the run followed with a back-off until it ends, its counters, a partial or failed run's reason in words, the comparison's figures each opening its list), choose (the server's filtered table, what each filter could not judge counted, proposed / all / none / invert / by id, personal namespaces unticked at first, an id the server drops named), and place and import (the filing per namespace or repository, the clone credential per host, first scans off by default and spaced 10 s to 10 min, a preview saying who will see each target, an import that sends exactly what was previewed — at most a thousand at a time — and a result linking each target). No route changes.
- A checklist line can measure change reviews — every merge request is approved by a peer before
merge (the rule,
what the forge is asked,
decision 0037, lot G3). A
change_reviewrule states the approvals by people other than the author (minimumApprovals), the window (windowDays), the share of merged changes that must have them (minimumRatio) and, optionally, the branch. An hourly maintenance turn reads each repository's forge through the connection that imported it — or one whose discovery listed its URL —, never on a request: the settings where the forge has them (GitLab Premium and Ultimate approval rules, Prevent approval by author and the protected branch; GitHub rulesets and branch protection), which pass the line alone when they require the approvals, refuse the author's own and refuse a direct push; and the history on every edition — the GitLab Community Edition included, which has no approval rule and answers 404 for the settings: the merge requests or pull requests merged in the window and who approved each, an approval by the author counted nowhere. The line shows 47 of 47 merged merge requests approved by a peer in 30 days, or the settings in a sentence, and the signed document prints it. No data, never a pass, for a repository no connection knows, a token refused (on GitHub a fine-grained token also needs Pull requests: read; GitLab'sread_apialready covers it), more than 500 changes in the window, or nothing merged in it. The template editor offers the kind, proposing one peer, every change and thirty days. Migration V81 (t_forge_review_reading). - A checklist line answers "library X is used" and "the versions of family Y in use", and a range of
versions (Components).
A
component_presentrule passes when a package matching its prefix is in the newest analysed SBOM, whatever its version — one a Maven BOM manages, which Syft writesUNKNOWN, included: a line that had no automatic answer on a compliant project using a BOM now has one. It fails when the package is absent, and has no data without a fresh analysis, or where the newest analysis no longer holds its SBOM and its inventory lists nothing. A namespace prefix (pkg:maven/org.example.platform) lists, per repository, each of its packages with its versions, twenty at most and the rest counted — oncomponent_versionstoo. Acomponent_versionsline now takes Maven version ranges beside exact versions on apkg:maven/package —[1.17,2.0),[1.17.7],(,2.0), unions — in Maven's own order:1.17-SNAPSHOTand1.17-RC1before1.17,5.3.0.RELEASEequal to5.3.0; a range that does not read is refused in words when bound. Where a version stays unstated, the evidence says it is managed outside the SBOM and that an SBOM produced by the build resolves it. The template's rule editor offers both, in French and English. - A coverage line may measure a scope of packages
(how to write one). A
coverage_thresholdrule takes an optionalscope—includeandexcludepatterns over package paths,**/service/**,org/example/**,**for whole segments and*within one — decided in the template rather than in each build's coverage filter, where a reviewer never saw it. The figure is covered over total across the matching packages; the evidence names how many matched, and the measurement's summary — the signed document'sEvidencesheet — states the scope beside the figure. To measure it, a coverage import now keeps its counts per package beside its totals (a JaCoCo or Cobertura<package>, an lcov file's directory), up to 10,000 packages and only when they add up to the totals. A scope matching nothing, an import from before this version (upload the report again) or one whose packages were not kept is no data in those words — never 0 %, 100 % or the report's totals. Patterns that would not read as written —org.example.service,**/serv**,?— are refused when the rule is bound. Until a line is rebound with a scope, nothing changes. - A build's SBOM completes the scanner's inventory (Importing a build's SBOM,
decision 0039,
lot G10). The scanner reads a Maven tree's poms: a version a parent's BOM manages came out as
UNKNOWN, and a library pulled in transitively was not listed at all, so a checklist line asking whether it is used answered a false "no". A declared source may now deliversbom:POST /api/v1/repositories/{id}/build-sbom-importstakes the CycloneDX JSON the build wrote (cyclonedx-maven-plugin'smakeAggregateBom, the Gradle CycloneDX plugin), spec 1.4 to 1.6, up to 32 MB (VECTISPIRE_MAX_BODY_SBOM_IMPORT) and 50,000 components, fetching nothing it refers to; the CLI'sbuild-sbomsends it. Each scan's inventory is the scanner's, completed by the newest build SBOM of its branch: the union of both, matched by purl, the build's stated version winning, the scanner's kept beside it. The newest completed scan is completed as the import is answered, and every later scan as its inventory is written; older scans keep what they were given. The component search, the consolidated inventory, its CycloneDX export, the project export, the licence inventory and its tallies, and thecomponent_versionsandcomponent_presentchecklist lines all read the completed rows, each saying who listed it. Vulnerability matching is unchanged, and an SBOM opens no issue. AuditedBUILD_SBOM_IMPORTED; a refusalREPORT_IMPORT_REFUSED,VECTI-SEC-027. The SBOMs leave with the evidence window and with their repository. Migration V82 (t_build_sbom,t_build_sbom_component, five columns oft_component), nothing to do. - A weekly record of the OWASP Top 10 coverage starts now. Every six hours at most, the maintenance
turn records, for every repository and image — scanned or not — and for each of the ten categories,
the state the OWASP grid shows (findings, not measured, not covered, nothing found), the open findings
it counts, and apart from them the open findings whose triage is settled, so that accepted risks can be
shown as such — for a target never scanned, its findings as the grid counts them once a target beside it
is scanned, so that a project's or an estate's week reads what the live grid reads. The current week
(Monday 00:00 UTC) is rewritten until it closes; a closed week keeps its
last capture. Weeks before the upgrade have no recorded state — whether a category was covered or
measured then depended on settings and rules that have moved since, and the weekly view below says
"not recorded" for them rather than guess.
A deleted target's rows go with it (migration V67), and the record is purged by the evidence
window, Evidence kept for (days) (
evidence_retention_days, 400 days by default, zero keeps it for ever), as the gate's verdicts and the compliance captures are: a week is kept while any of it is inside the window, and a purged week reads as reconstructed in the weekly view below. - The OWASP Top 10, week by week:
GET /api/v1/owasp/coverage/weekly. Up to 52 ISO weeks (the last 12 by default), over the caller's estate or one project or solution, for the targets they may see (a scope they see nothing of answers 404, as an absent one does). Each week gives, per category, the state, the open and the settled findings as the weekly record captured them, and the issues opened and resolved in the week. A week before the record is reconstructed from the issues' dates and says so: its state and settled figure arenullrather than guessed, and its open figure counts every issue open at the week's end, whatever its triage — the triage of a past date is not known. An earlier resolution of a reopened issue counts — not open from that resolution to the reopening, and a resolution of its week — from the reopening entries of the triage history; a reopening before this release left none, and such an issue still counts as open between that earlier resolution and its reopening. Each week and category also givesreopened, the issues a recorded reopening brought back in the week — what makes open rise with no opened figure to match. It isnull, not zero, on a week that began before reopenings were recorded (dated by V68's application, a day's margin aside);reopenedRecordedFromnames the first week that has it. - The live OWASP grid takes a project or a solution too:
GET /api/v1/owasp/coverage?project_id=…or?solution_id=…, with the weekly view's rules — both at once answer 400, a scope that does not exist and one the caller sees nothing of answer 404 in the same words, and one seen in part is computed over the targets they see. Every figure is narrowed to the scope, whether anything was scanned included: a project nothing has scanned reads not measured, however covered the rest of the estate is. The response gainsscope(kind,id,name,partial,targetCount,nullwithout one); the category declarations are the category's, whatever the scope. Without a parameter the grid is unchanged. On screen, the OWASP page's current grid has the weekly view's scope picker, the scope kept in the address, and each open count opens the backlog of its category in the same scope. - New backlog filters for that view's figures:
owasp_category(A01…A10, placed as the grid places issues — a vulnerability isA06),open_at(open at the end of that day, UTC) andfirst_seen_from/first_seen_to/resolved_from/resolved_to—open_atand the resolved range read a reopened issue's earlier resolutions the same way — andreopened_from/reopened_to(a reopening the triage history recorded in that range).owasp_category=anylists the issues placed in any of the ten categories — a week's totals — and never a licence, quality, plugin or import finding, which no category holds. With a date and nostate,GET /api/v1/issueslists every state, since the issues open on a past day are mostly resolved since; the default staysopenotherwise. - The OWASP report screen gains a "By week" view (OWASP report → By week, or
/owasp?view=weekly): a heatmap of the ten categories over 12, 26 or 52 weeks or a chosen range, for the whole estate or one project or solution; the curves of what is open per category; the issues opened, reopened (stacked on the opened, in purple) and resolved each week; the selected week's figures and their change since the week before; and that week's grid. Reconstructed weeks are hatched and their curves dashed — their open count includes accepted risks, so no change is shown across the week the record started. Accepted risks are shown apart, in grey, never added into the open count. Every count opens the backlog it counts — open at the week's Sunday (not settled, on a recorded week), or first seen / resolved / reopened from its Monday to its Sunday, in the same scope — the totals included, which open the issues placed in any category (except a recorded week's total open count where a category was not measured: the grid counted nothing there, and the list would). A week before reopenings were recorded shows a dash for them, not a zero. The backlog says what it was asked in a banner, with the way back and a way to clear it. The figures export as CSV, one row per week and category with the reconstructed flag; Print / PDF prints the view without the application's menus (the browser's own "save as PDF" — no PDF is generated on the server). Window, scope and selected week are in the address, so a link reproduces the view. - The OWASP report links to the findings it describes. Under each category heading, View the N findings opens the repository's open, unsettled findings in that category — the same list the grid's count opens — or says there are none. The identifiers the model was shown (CVEs, rule ids, packages) link to their issue, or to a search when several issues carry one; an identifier the prose cites without the model having been shown it stays text.
- The OWASP report can be written after each repository scan
(OWASP report). With Write the OWASP
report after each repository scan (
ai_review_owasp_after_scan, off by default) and model review on, a repository scan that completes asks the model for that repository's report, built from that scan — never a failed scan, never a container image. The request is queued with the scan's results and written beside the scans, never holding one up: one report at a time per instance, and none started while any report is being written, by another instance or from the button, since a local model answers one request at a time. A repository whose report is being written is not asked again, and scans of one repository arriving while its report waits are folded into one, from the newest. A failure is recorded on the report as for a report asked by hand, and the scan stays completed. The audit log records each one asAI_REVIEW_REQUESTEDwith no user. Off, nothing changes. A request held by an instance that stops before starting it is lost; the next scan asks again. - Experimental: other scorecard weights, side by side with the production ones.
GET /api/v1/scorecards/simulation, administrators only, scores every visible target, project and solution under the card's formula and under the weights asked for, with each row's risk points; with no parameter the two agree. It served to decide the formula above, sets both ways of grading a scope — the weakest link the card uses and the rejected sum — beside each other, and is retired in the release after this one (how).
Security¶
- Kubernetes: in a pod, a destination an administrator sets is also refused ports 6443 and 10250 on every address — the API server's and the kubelets' — beside the cluster's API service: kube-proxy forwards the service to addresses no setting names. Forge connections, webhooks, the AI server and the SIEM collector are concerned. An API server a managed cluster publishes on 443 is the NetworkPolicy's to keep out, as the installation guide and the chart's README now say.
- A forge import clones only from its connection's host. A repository whose clone URL — the HTTPS one, or the SSH one when a key is chosen — is not on the connection's host is refused by the preview and the import, and no credential of that host is proposed for it: the forge's listing no longer decides where a target is cloned from. A GitLab whose SSH clones go through a separate host imports over HTTPS.
- The composition's database health check no longer puts the MySQL root password on a command line.
mysqladmin ping -p"…"ran every ten seconds with the password among its arguments, whichpsshows to every user of the container and of the host. It now reads the password from an option file only its owner can read, written by a shell builtin and removed after each ping. Applies todocker-compose.yml; the Helm chart runs no database. - Agent credentials and the evaluation Keycloak no longer sit in reach of
docker inspector a published default. The composition'swith-agentprofile passedVECTISPIRE_AGENT_TOKENandVECTISPIRE_AGENT_SIGNING_KEYas environment, which every client of the daemon — both socket proxies included — reads back; they are now files under/run/secrets/, read through the configuration tree as the Helm chart's agents read them. Keep both lines in.env, empty if unused: the agent does not start while either is undeclared. Thessoprofile's Keycloak administrator defaulted toadmin/admin; it now refuses to start withoutKEYCLOAK_ADMIN_PASSWORDin.env, and the evaluation realm's published password is temporary, changed at the first sign-in (single sign-on). - An HTTPS token sealed for an agent is bound to its host and user name, and a deployment key to
being one. The token was sealed alone while its host travelled beside it, so the envelope stayed
valid whatever host it was paired with; the agent now opens it only under the host and user name it
was sealed for, and refuses the scan otherwise. The envelope format is
sealed:v2:and an agent vouches for it in its signed sealing-key announcement; migration V86 forgets the sealing keys accepted before, which every current agent announces again at its next start. - Three settings a CISO could change are no longer theirs. Allow a private webhook URL is an
administrator's, like the SIEM's private switch: with the channels' test button quoting the sender's
error — a refused connection, a timeout, an HTTP status, and with the switch off the address a host name
resolved to — a CISO could map the internal network from it. The button now answers delivered or not
delivered, the cause in the server log. The tracker's project and Jira account are an administrator's,
like its URL and token: the token acts on any project of its host, so choosing the project chose where it
wrote; a GitHub project must be
owner/repository, refused otherwise when saved and at the send, where a path after it had steered the token's request to another resource. And Evidence kept for (days) is a platform governor's: a CISO or an administrator could set it to one day and purge, at the next turn, the gate verdicts, compliance captures and OWASP weeks that record their own controls ran (settings). Stored values are kept; an integration writing these throughPUT /api/v1/settingswith another role now receives 403. - Four authorization gaps closed. A key restricted to a repository could export the project whose
only repository it was, though such a key never sees a project whole; it is now answered as absent there,
as on its checklists. An administrator could revoke a platform governor's API key, which only a governor
may administer; it is refused. The exceptions register read every exception and filtered afterwards, and
its cursor, like the gate verdict register's, named the last row read, visible or not: a reader
restricted to one repository, paging one row at a time, read the timing of every verdict and the instant
and issue id of every risk acceptance in the estate. A restricted reader's pages are now read within its
targets. And the statement of applicability (
GET /api/v1/compliance/soa,…/soa/{framework}) was readable by every signed-in account, while its overdue reviews already required governance read; both now require it, as the reference said. - A report document, and a checklist revision, are read by who sees what they carried. A document was served to whoever saw the project whole at the download, so a repository taken out of the project after the run left its findings readable by somebody it was never shown to. A run now records the targets its export carried, and the download and the document status route also require the caller to see each of them; documents produced before this release recorded none and are read only by somebody who sees everything (decision 0042). A submission's and a sign-off's stored measurements, the signed package and the statement a project export embeds carry the evidence of every repository the project held then, and were served to whoever saw the project whole now. They now also require the caller to see each of those repositories — named by the stored evidence itself, so revisions submitted before this release are covered without a migration; anybody else is told the revision does not exist, and a project export carrying such a statement is refused rather than cut. A report run records those repositories among its export's targets (decision 0042 §5).
- The SCIM directory no longer binds an existing privileged account, or one with a local second
factor, to an identity-provider subject. It could not grant those roles, but it could bind an
account that already held one, and the next sign-on skipped that account's password and second
factor. A first binding of a privileged account is now refused unless
VECTISPIRE_OIDC_LINK_PRIVILEGED_ACCOUNTS=trueallows it — the rule a sign-in claim already follows — and of an account with a local second factor always (single sign-on). - Changing your own password revokes the API keys you issued, as an administrator's reset already
did: the other sessions closed, but a key minted by whoever knew the old password kept acting for the
account. The answer carries
revokedApiKeys, and each key revoked is its own audit entry. A pipeline key owned by an administrator who changes their password stops working: issue a new one. - The single sign-on hand-off is no longer a session. The cookie lived sixty seconds, the row behind it a session's hour, and its value worked as a bearer token until it was exchanged. It is now stored apart, exchanged only, and expires with the cookie.
- The sign-in counters can no longer be walked around. A name the database's collation equates with
an account's —
jeßicaforjessica,æforae— counted against a counter of its own, so the lockout told an existing account from an absent one; the name is now folded as MySQL compares it, checked against the server's own collation. The collation also reads past characters an account name never contains — the soft hyphen, the zero-width spaces and joiners, the word joiner, the byte-order mark, most control characters — soalicetyped with a soft hyphen inside (U+00AD) openedaliceand met its lockout while counting under a name of its own; a typed name outside the account names' grammar (letters, digits,.,_,-) is now answered as an unknown one, the right password included, and an accented spelling such asÀliceno longer signs in toaliceeither. An IPv6 caller had a fresh bucket for every address of its /64, which its host picks at will; the sign-in, bearer-token and webhook ceilings now count it by its /64, while the audit log keeps the full address. And a successful sign-in erased its address's failures: signing in to one's own account between attempts reset the per-address ceiling. Only the account's and the name's counters start again now. - The outbound guard refuses more of what no setting should reach. The cloud metadata addresses that
are not link-local — Alibaba's
100.100.100.200, AWS's IPv6fd00:ec2::254, Oracle's legacy192.0.0.192, Azure's WireServer168.63.129.16— and the unspecified address (0.0.0.0,::), which reaches the local machine, are refused under every policy, for a clone as for a request. A database or Docker daemon reserved aslocalhostis reserved on every loopback address. A forge listing's next page must stay under the connection's API path, not merely on its host (forge connections). - An in-cluster agent's way back to the Ingress opens the port it names, not every port. The Helm chart's
agents.networkPolicy.allowTobecame an egress rule withoutports, so the entry an agent needs to reach the ingress controller — whose pods lie in the excluded pod range once kube-proxy has translated the LoadBalancer address — opened every port of those pods to anything escaping a scanner, and an entry for an internal Git host every port of that host. Each entry now carries its ownports(protocoldefaults to TCP) and is a rule of its own; an entry without them is refused at render, the message naming the fix, since the chart cannot tell all ports meant from ports forgotten.agents.networkPolicy.dnsTo, empty by default, narrows DNS from port 53 anywhere to your resolvers (installation). - A report plugin's file is no longer signed raw by the platform's key.
v0.11.0-rc.1put a detached<output>.sigin the package, made with the key that signs VEX, CSAF and the project export, so a plugin image could obtain the platform's signature on any bytes it wrote. The package now holds the file andprovenance.jsononly, and the file is bound by the provenance's subject (how to verify). Packages a release candidate produced keep their.sig: do not rely on it. - The upstream rule catalogue is read without following a link. A symbolic link committed upstream — a
LICENSEpointing at a file of this host, a secret mounted under/run/secretssay — was followed by the fetch: shown on the licence preview, or stored as a rule. A linked file is now left out, a linked licence leaves the catalogue without one and the fetch is refused, and no file is opened through a link (rule sets).
Fixed¶
- A stored tracker token, webhook secret or AI API key whose state could not be read says so, with the server's reason, instead of "none stored".
- The OWASP page's model-written report refreshes on its own every five seconds while it is being written, and its repository picker offers only the repositories of the solution or project chosen above the grid — a chosen repository and its report are dropped when the scope no longer holds them.
- The attack-surface repository picker is announced by its label, not by its placeholder or the repository shown.
- Screens no longer state what they did not read. A failed read of the licence conflicts showed "No blocking conflict" over a list nobody had read; an unreadable threat-feed status showed "0 KEV CVEs" and "Never"; the remediation plan and the attack paths kept the previous scope's or repository's figures under the new selection. Each now says the read failed, with the server's reason — as do the other panels of the licences, remediation, security posture, quality, scan detail and attack-paths screens, which stayed empty.
- Days shown inside a sentence read dd/MM/yyyy like every other date: the dashboard's score-formula note and its chart axes, the weekly OWASP view (window, week, squares, figures table, reopenings note), the backlog's weekly drill-down banner, and a checklist proof's "performed on" and "valid until" dates. The interface's i18n check now refuses a date pattern not written day, month, year.
- The verdict register and the security posture name the policy that decided in the reader's language —
"Built-in default", "Global policy", "Target policy" with its version — instead of the stored value
(
BUILT_IN, "global v2"). - The tracker token's expiry and refusal dates, and the forge screens' dates, are written dd/MM/yyyy like
every other date, instead of year first (
2026-09-29): the Settings screen's tracker card, and the forge connection, discovery, selection and import screens. - The gate's verdict register no longer fails on a verdict that names no target. One such row — restored
or written by hand, since the application never records one — made the whole register and section 09 of
the evidence bundle answer 500; it is now listed with no target, to whole-estate readers only. The register
also names each target (
target_name) instead of showing "Repository 5", and only targets the reader may see. - The attestation is dated by the server's check of the audit chain (
checkedAtonGET /api/v1/audit-log/verify), no longer by the browser's clock. - The AI-written OWASP report's 300-finding sample keeps the riskiest findings — KEV first, then by severity — instead of the first finding types in alphabetical order, which could leave every vulnerability out.
- The documentation says which client address
trustedProxiesbelieves behind a load balancer or a Kubernetes node that translates the source address, and how to restore it for each kind of load balancer; the SIEM guide's note onsrcbehind a load balancer was out of date. - Without an identity provider, the compliance report counts local TOTP. The audit-logging controls were capped at 65 with "no second factor" whenever no provider was configured, even with TOTP enrolled on every account. Once every active account holding a password has enrolled, the cap is 85 — the one already applied to a provider beside an open password, since TOTP is chosen per account; otherwise it stays at 65 and the report says how many of those accounts have no second factor.
- The OWASP grid and the REST reference say how a category is really covered: by the finding's type, or,
for code analysis, by the category the rule declares — not by the type alone, and not "three categories out
of ten". The reference now lists the OWASP report routes (
/repositories/{id}/owasp-review, its PDF) and the category declaration (PUT /owasp/coverage/{category}/declaration). - A repository's Imports window says whether a coverage import kept its counts per package, and why not (too many packages, a path refused, inconsistent figures, or imported before packages were kept). The gate-policy dialog's title follows a language switch, and the issue and attack-surface filters keep an accessible name once a value is chosen.
- A screen no longer shows the answer to an earlier request. Switching the dashboard's window, searching a second CVE on the EPSS page, opening another dossier in the history or picking another package in the blast radius could leave the slower, earlier answer on screen under the new label — one CVE's AI explanation under another's numbers. The last request asked is now the one shown. The attack-surface page waits for the scans it launched to finish rather than for the first endpoint, and the blast-radius and attack-surface pages say when a list failed to load instead of staying empty.
- A severity has one colour on every screen: critical red, high orange, medium blue, low grey. High was red on the issue list and the solutions, orange on the exceptions, the attack paths and the dashboard.
- The ticket sweep no longer overwrites a reference a person attached while it opened a ticket. The sweep reads its candidates, then calls the tracker; a reference attached in between was replaced by the one just opened, and the person's ticket lost its link. The issue now keeps the person's, and the ticket opened is named in the log and in its audit entry as not recorded on the issue. Marking a ticket closed likewise touches only an issue still carrying that reference.
- The SBOM diff no longer invents licences. A component that declared none was given one guessed
from its name —
MITfor anything containing "mit" (commit-graph,summit-ui),Apache-2.0forspring-andcommons-,GPL-3.0for "gpl" — and the first declared licence only of the others, soGET /api/v1/sbom/diffand…/diff/latestreported licence changes nobody had declared. A licence is now the build SBOM's declaration, else every licence the scanner's SBOM declares (joined byOR), elseUNKNOWN— the reading the licence inventory already used. And an unreadable SBOM is no longer an empty one: the diff swallowed the parse error and compared nothing, which read as every component removed or as no change; it now answers 409 with the typeurn:vectispire:problem:sbom-unreadable, and the server logs which scan. - A triage made while a scan ran is no longer lost. A scan read the target's issues when it began
writing its results and saved them whole at the end; a decision committed in between — a triage, an
approval, a review, a ticket attached — was put back to what the scan had read, with no error and no log
line. Each issue now carries a revision (migration V89): a scan that finds an issue changed under it reads
it again and folds once more, the decision kept, and so does a SARIF import; a decision whose issue a scan
changed under it is refused with 409
issue-changed-meanwhilerather than writing the scan's changes away. - A tracker behind an internal CA is reached: its CA can be pinned, as a forge connection's is. Every
call to the tracker — opening a ticket on GitLab, GitHub, Jira or ServiceNow, closing it, Jira's
transitions lookup, ServiceNow's incident lookup — was verified against the Java runtime's trust store
alone, so a self-hosted tracker whose certificate came from the organisation's own CA failed the
handshake on every call: no ticket was ever opened, and the hourly sweep retried with a line in the
server log as the only trace. The administrator-only setting
ticket_ca_pemtakes that CA in PEM; it is checked when saved by the forge connections' rule (a CA, current, within 8 certificates and 16,384 characters) and replaces the runtime's store for the tracker alone — the public CAs are then not trusted for it, and the tracker's name is still verified. The settings screen shows its subject and expiry, read fromGET /api/v1/settings/ticket-ca(a tracker behind an internal CA). - A tracker token that expires is announced before it does, and one the tracker refuses is said to
have stopped working. A lapsed token — GitLab now forces an expiry, a year at most by default — made
the hourly sweep fail on every call with a log line as the only trace, for ever. GitLab's and GitHub's
reported expiry is read when the token is saved and daily after (through the pinned CA), a date can be
declared for Jira and ServiceNow (
ticket_token_expires_on, administrator-only), and the tracker card shows it fourteen days ahead as the forge connections' list does, Expiry unknown when nothing is known; a401or403met by the sweep (a401by the daily check) shows Stopped working — but not GitHub's rate limit, a403carryingx-ratelimit-remaining: 0orretry-after— until a call is accepted again or a new token is saved. Each change is logged once atWARN, not every hour.GETandPUT /api/v1/settings/ticket-tokenanswerexpiresAt,expirySource,refusedStatus,refusedSinceandcheckedAtbesideconfigured(when the token expires). - The catalogue import leaves out the rules that duplicate gitleaks, which found every leaked key a
second time. Choosing
genericimportedgeneric/secrets/gitleaks/, 175 rules ported from the gitleaks rules the secrets step already runs, and 27 rules ofgeneric/secrets/security/whose token formats a dedicated default rule of gitleaks already finds (AWS access key ids, GitHub, Slack, Stripe, private keys…). Each leak became a secret issue and a SAST twin, and triaging the secret left the twin open: it could still fail the gate, weighed on the score and counted its OWASP category twice. Both groups are now left out unless Include them anyway is ticked — for an installation whose scans run no secrets step — and the screen says how many. The other 23 rules ofgeneric/secrets/security/— a bcrypt hash, credentials in a URI, an AWS secret key… — are still imported. A set imported before keeps those rules until the next import, whose activation counts the issues they leave behind as any activation does. The import's audit entry now carries the licence digest before the languages, which a selection of eleven languages had pushed past the entry's 255 characters.POST /api/v1/rule-sets/cataloguetakesincludeGitleaksDuplicates,falsewhen absent, andGET /api/v1/rule-sets/catalogueanswersgitleaksDuplicates(rule sets). - An OWASP category reads clean only where every target's newest scan examined it. The grid read a category measured as soon as any target in scope had a scan: a project of images alone showed A05 and A07 nothing found, though an image's scan looks for neither secrets nor misconfiguration, and a repository whose secrets step failed showed A07 clean all the same. Each target now carries what its newest finished scan examined — the types its steps produced and the languages its code-analysis rules read — and a category is measured over a scope only when every target that can be examined for it was; one that was not makes it not measured, and the line says how many and why. A target nothing can examine for a category (an image, for secrets) is left aside, and open findings are counted whatever examined the target. Code analysis is credited per repository and per language, in the grid and in the monthly compliance record, and the weekly record reads the same rule. A category that read nothing found before the upgrade may read not measured after it: nothing had examined it.
- The VEX documents no longer state what nobody decided. The OpenVEX aggregate looked for
false_positiveandaccepted_risk, statuses that never existed: every approvednot_affectedleft it asaffected, and an issue under review or awaiting approval asaffectedtoo, where CycloneDX saidin_triageand CSAFunder_investigation. The per-target CSAF export had no case forpending_approval, so such a product appeared in no status list at all; it isunder_investigation. Anot_affectedrow without a justification — older or imported — was givenvulnerable_code_not_in_execute_pathby CycloneDX andvulnerable_code_cannot_be_controlled_by_adversaryby CSAF, guessed from the spelling of whatever was there; it is now stated as under investigation, since nobody made that claim. And CycloneDX received OpenVEX's justification labels, which its schema does not accept, and answeredwill_not_fixon everynot_affected— nothing to fix in a product that is not affected; its justifications are now in its own vocabulary (how a triage reads in VEX). - Applying the advisor's suggestion no longer fails on every finding off the KEV list. The advisor
suggests in VEX's vocabulary,
under_investigationfor those, and the dialog took it as the triage status: the status list showed nothing and the server refused the decision. It is now applied as under review. - An account the SCIM directory creates is no longer held on the change-password screen. It was
given a random password and told to change it, which its federated sessions could not do. Accounts
the directory created on 0.10.0 or
v0.11.0-rc.1keep the flag after the upgrade, and are still held there after a single sign-on: an administrator clears it for each — the audit log names them (USER_CREATED, "SCIM provisioned account: …") — withupdate t_user set must_change_password = false where username = '…', since no screen clears it for an account with no password to change. - Concurrent audit writes could break the audit chain, and raise a false tampering alarm. Two
entries written at the same moment — two requests on one server, or two servers — could both chain
onto the same predecessor; the verification then reported the chain broken, and the SIEM received
AUDIT_CHAIN_BROKEN, although nothing had been altered. Entries are now written one at a time, across servers too (migration V66). An installation that saw an unexplained break should verify again after the upgrade: entries written from then on chain correctly; a break already recorded stays where it is, since rewriting an integrity log is what it exists to reveal. - The first evidence bundle of an installation without
vectispire.signing.keyanswered 500. The signing key created on first use joined the bundle's read-only transaction, which MySQL and PostgreSQL refuse to write in. It is now created outside it. - A Docker daemon on
tcp://withDOCKER_TLS_VERIFYis spoken to over TLS. The client readDOCKER_TLS_VERIFYandDOCKER_CERT_PATHand then built its connection without them, so a daemon listening with TLS on 2376 was sent plain HTTP and every call failed. The client certificate underDOCKER_CERT_PATH(ca.pem,cert.pem,key.pem) is now used; a directory missing one of them stops the executor with a message naming the files, rather than reaching the daemon in the clear (decision 0038). - A signed plugin whose image lives in a private registry is verified instead of refused. The
signature verifier asked the registry anonymously, so a registry that serves nothing to an anonymous
pull answered "unauthorized" and the plugin was refused as
signature_unverified— a signer problem on screen for a signature nobody had read. cosign is now handed, for its run alone, the credentials the executor's own pulls use for that registry (its Docker configuration; Vectispire stores none), read-only and erased with the container. A registry that still refuses — no credentials held, or the ones held refused — gives the new refusalregistry_authentication_required, which says which. - The container guide said registry credentials were stored encrypted with
ENCRYPTION_KEY. They never were: a pull uses the Docker configuration of the executor that scans. The guide now says so, and how to give an executor credentials for a private registry. - A
git://repository its server does not serve was retried for a quarter of an hour before failing as "the clone failed".git daemonanswers a path it does not have, or does not export, with a refusal of its own, which was read as an unknown failure — transient — so the scan waited one minute, then five, and spent its three attempts on the same answer. It now fails at its first attempt, as a repository absent over HTTPS or SSH always has, withgit://… could not be found.A host that does not resolve, overgit://or SSH, is still retried — the network may come back — and now says… could not reach its host.where it saidThe clone of … failed. - A checklist template whose workbook no sign-off could fill in was published, and found at the first sign-off. A cell Vectispire writes — an answer or a comment of an item row, a header value — that holds the master of a shared formula (a helper filled down the comment column, typically) or an array formula over several cells cannot be written without breaking the cells depending on it. The sign-off refused it, rightly, but only once a project had answered every line: the revision stayed submitted, its draft could not even be exported, and the version could never be signed. Publication now runs the sign-off's own rendering once, with placeholder answers and nothing kept, and refuses such a version, naming every cell — the sign-off of a version published earlier names them all too, where it named the first. A version published before this release with such a cell still refuses its sign-offs: publish a corrected one and move the projects' checklists to it — see a formula in a cell Vectispire writes. Nor is a checklist opened on, or moved to, such a version any more: the same trial runs there, the cells are named on screen, and nothing is opened — no project starts a checklist that could never be signed. Moving a checklist away from such a version is never refused: that is the way out — when the server refuses. The templates screen and the project checklist screen both name the cells in the reader's language and say what to do. The versions a project is offered list such a version disabled, cannot be signed off, its cells named beneath — rather than offering it to be refused once chosen.
- A reopened issue left no trace in its triage history. When a scan or an import found a resolved
issue again, it reopened it and cleared a
fixeddecision with no entry: the history showedfixedas the last word on an issue standing open under review again, and the resolution it had — from when to when — was lost. The reopening is now an entry of its own: the triage it left (or kept, for a judgement that survives the return), the date the resolution it ended began, and the scan that found the issue again; nobody is named, since nobody decided (migration V68). A reopening before the upgrade stays without an entry. - The exceptions register no longer fails on an issue that names no target. One excepted issue with neither a repository nor a container answered the register with an error — the screen, the answer to every review, and the evidence bundle that seals the register. The application creates no such issue, but the schema allows one, written by hand or restored from elsewhere. It is now listed with no target to a reader who sees the whole estate, and to no restricted reader.
- The OWASP report files each finding where the OWASP grid does. The model chose the categories
itself, and put committed secrets under A02 while the grid on the same screen counts them in A07. It
is now sent each finding's category as the grid places it — by type, or by the category a static
analysis rule declares — told never to move one, and given the findings the grid places nowhere apart,
under Not placed by the scanners, rather than in a category of its choosing. It is also given the
grid's state of each category, and no longer told that a category with no finding is one no scanner
looked at: the grid's nothing found says a scanner did. The PDF's "How to read this" says the same,
and says which of the two ways a report was written: one written before this release was placed by the
model. The
inputsof a report (GET /api/v1/repositories/{id}/owasp-review) gain anowasp_categorycolumn, second, and the repository's coverage above the table. - A repository without an OWASP report says so. The card showed No findings categorized under this section, which reads as a report that found nothing; and any failure to read the report, a 500 included, was shown as no report at all. Only a 404 now means "no report yet"; anything else is an error.
- The model review's switch says what it sends.
ai_review_enabledwas labelled Review the code with a local model, with a help describing what such a review would find, and the leak warnings spoke of source code leaving. Nothing calls a code review: what leaves is the repository's open findings — identifiers, components, paths, descriptions, a description able to quote a committed line — for the OWASP report. The switch is now Let a model write the OWASP report, and its help, the provider and endpoint settings, the remote-endpoint acknowledgement, its confirmation and the banner say what is sent today, and what would be the day a code review is wired. - Refreshing a project's checklist, or opening a link to it, no longer answers 404. The server handed
the interface paths of one and two segments only;
/projects/{id}/checklistand/solutions/{id}/compliancehave three, and answered "Nothing is served at this path" unless reached by clicking. Every route of the interface is now forwarded, and the build checks it. - A template with an identifier column in front of its domain is proposed in the right columns. The column left of the domain shifted every proposed column by one (control on the identifier, contact on the domain, KPI on the objective).
- Following a link from one scan to another could show the previous scan. The scan page kept the first scan on screen until the second answered, and if the first answer came last it replaced the second: the address named scan 35 while the header, the plugins and the findings were scan 34's. The page now clears the previous scan and cancels its request when the address changes.
- Changing a password no longer ends on the dashboard. An account whose password had been set by an administrator, following a link it had been handed, signed in, was sent to change its password and then to the dashboard, the link forgotten; the same happened to a change opened from the top bar or the account page. The change now returns to the page asked for — or the one it was opened from — and only to a page of Vectispire: an address pointing elsewhere is ignored.
- A scan's count names issues, its list occurrences, and every count says what it counts. The scan detail headed with Findings a count of issues — one per fingerprint, which carries no line — above a list of findings holding one row per place a tool reported, so a scan read "28" above 56 rows. The card now says Issues with the occurrences beneath it (28 issues · 56 occurrences), the list is Occurrences in this scan, and the history and the dashboard's recent scans call the same figure issues — as does each scan's line of the exported history PDF, which read 28 findings, 3 new, 2 resolved and now reads 28 issues, … (scans). The scan pickers of the SBOM comparison say 7 issues, with the reader's digit grouping, instead of a bare number; the dashboard's failing-targets table heads its count Violations, since it counts gate-policy violations, not findings; and the scan detail heads its occurrence list's identifier column Identifier. No figure changed.
- Dates, numbers and labels read the same on every screen, in the reader's language. A few screens
wrote dates in the browser's locale or a hard-coded one (month first, a two-digit year, English month
names to a French reader); every date now follows the interface's one pattern, day/month/year on a
24-hour clock. Percentages follow each language — 50% in English, 50 % in French with a no-break
space — and decimals carry the reader's decimal mark (11,4 jours, not 11.4 jours). Severities,
statuses, compliance states and frameworks are labels where screens printed the raw value (
critical,PARTIAL,NON_COMPLIANT,ISO_27001), the EPSS screen no longer shows HTML entities as text (> 20%), and the AI review's VEX suggestionunder_investigationreads Under investigation. The last labels frozen in one language follow the language preference: the change tags of the inventory diff ("AJOUTÉ", "SUPPRIMÉ"…), a notification channel's state ("Configuré", "Inactif"), the "UNPROTECTED" and "HIGH RISK" tags of the attack surface, and the badge image's alternative text. - "1 scans", "1 target(s)": counts now agree with their noun, in English and French. Forty-six messages — scans queued, issues to be resolved, entries missing from the audit chain, targets never scanned, components listed, checklist lines answered… — were written in the plural or with "(s)" and said so whatever the number. Each now has a singular and a plural chosen by the language's own rule: in French 0 takes the singular ("0 dépôt"), in English the plural ("0 repositories"). The last ones follow: the audit chain's verdict, a team's deletion warning, the checklist's "lines 3 still need attention" and the other messages naming lines, and six counted labels (blast radius, EPSS, licences, rule files).
- The settings screen speaks French to a French reader, and its help reads as text. Twenty-one
settings showed the server's English label and help to a French reader — the tracker card's URL,
account, issue type, labels and internal-URL switch, the Slack, Discord and e-mail channels, the
notification threshold, the evidence window, target visibility, the forbidden licences, the certified
scope and the four-eyes rule among them — and the French help of the tracker's provider still listed
values that no longer exist; each now has its own. Seventeen English helps showed markdown asterisks
as text (
**in place of**); they are written without them. The provider's help now names all five values (gitlab,github,jira,servicenow,none), and the tracker account's and labels' helps say what ServiceNow does with them. The Imports window of a repository sizes its Latest coverage, Latest test report and SARIF imports headings as section titles rather than as page titles. - The screens name the OWASP Top 10:2021, and claim no more than the tool does. Vectispire classifies
against the 2021 edition only, and the menu, the coverage pages, the heatmap, the issues filter, the
rule-set catalogue, the posture PDF and the coverage API said "OWASP Top 10" without the edition; they now
say OWASP Top 10:2021. SOC 2 Type II — an auditor's report over a period, which no tool produces — is
SOC 2; audit trails called immutable are tamper-evident; Download Evidence Vault downloads the
evidence bundle. The CRA controls' titles cite Regulation (EU) 2024/2847 as adopted — Article 14(1) for
the actively exploited vulnerabilities, Annex I Part II, Article 13 — where they followed the 2022
proposal's Articles 10 and 11; their codes (
CRA-ART10-*,CRA-ART11-*) are stored with every declaration and assessment, and stay. The OpenAPI document's descriptions moved with them; no field did. - The licences of the interface now ship with it.
ng buildwrites the notices of the npm packages it bundles (Angular, Optimus UI, chart.js…, mostly MIT) beside thebrowser/folder, and only that folder reached the jar and the image; the MIT notice of the shell (Sparked, PrimeTek's Sakai ported to Optimus UI — copied code, not a package) shipped nowhere either. Both are now in the jar understatic/licenses/(3rdpartylicenses.txt,sparked/LICENSE.md),NOTICEnames the shell, and the build refuses a bundle that lost either.
Performance¶
- Two issue columns are indexed (migration V90).
t_issue.ticket_ref, which the tracker webhook looks up at every delivery and the ticket sweep reads for issues carrying none, andt_issue.triage_expires_at, which the maintenance tick reads for the review dates passed: both read the whole table before. - The dashboard's maturity ranking no longer reads the estate's SBOMs at every load. Its licence term — five points a refused licence, the same as on each target's scorecard — was counted from the whole licence inventory each time the page opened: every scan's SBOM parsed and every component row read, for a reader granted five targets as for an administrator. Measured on two hundred targets of two scans and 250 components each, a load cost about 420 ms; it now costs 30 to 50 ms. Each target's licences are counted once and counted again only when its scans change — a new scan, an SBOM removed by the retention purge, a target deleted — and a licence policy change applies at the next load. The ranking's scores are unchanged, with one precision: when two scans of a target declare the same component under different licences, the newer scan's licence is now the one kept, on the ranking and on the licence inventory alike, where it used to depend on the order the scans were read in.
- The portfolio scorecard and the licence screen no longer read the estate's history for what those tallies already count. The portfolio scorecard's licence term, the licence summary of the whole estate and the evidence bundle's licence summary are counts by licence, now taken from the same per-target tallies as the ranking; the scans attached to no target are tallied together and count, as before, for a reader who sees the whole estate only. A reader granted some targets has their licence inventory read from their targets' scans alone, where every scan, component and licence finding of the installation used to be read and then narrowed. On the same estate, each of these calls cost 340 to 840 ms, for either reader; warm, the portfolio scorecard now costs 40 to 55 ms, a summary about 20 and a restricted reader's inventory about 25. An administrator's licence inventory and licence conflicts over the whole estate still read every scan's SBOM and every component row, since they list one row per component of every scan, but only the columns they need: 220 to 350 ms instead of 380 to
- The answers are unchanged — the same entries, counts and score for an administrator and for a restricted reader — with one precision: when a component the SBOM does not declare has rows in two scans of a target with different package URLs, the older row's is now the one shown, where it depended on the order the database returned the rows in.
0.10.0 — 2026-10-01¶
Read Before you upgrade first: five of its points stop something working until an operator acts, on purpose.
Before you upgrade¶
SIEM signature identifiers are renamed from ZAN-SEC-nnn to VECTI-SEC-nnn, all at once. The
number and the meaning of every event stay the same; only the prefix changes, and no event carries
the old one any more — not even those still queued when you upgrade, since the identifier is
written when an event leaves. A correlation rule, alert or dashboard filtering on ZAN-SEC- stops
matching, without any error. Before upgrading, change each one to the new prefix, or to match both
while you roll out. The CEF signatureId, the syslog MSGID and the webhook's JSON all carry it.
See SIEM.
| Before | From this version | Event |
|---|---|---|
ZAN-SEC-002 |
VECTI-SEC-002 |
Actively exploited vulnerability (KEV) detected |
ZAN-SEC-003 |
VECTI-SEC-003 |
Security gate refused a build |
ZAN-SEC-005 |
VECTI-SEC-005 |
Finding settled by triage |
ZAN-SEC-006 |
VECTI-SEC-006 |
MFA backup code consumed |
ZAN-SEC-007 |
VECTI-SEC-007 |
Sign-in failure ceiling reached |
ZAN-SEC-008 |
VECTI-SEC-008 |
MFA failure ceiling reached |
ZAN-SEC-009 |
VECTI-SEC-009 |
Bearer token failure ceiling reached |
ZAN-SEC-010 |
VECTI-SEC-010 |
Account privileges or credentials changed |
ZAN-SEC-011 |
VECTI-SEC-011 |
Team access grant changed |
ZAN-SEC-012 |
VECTI-SEC-012 |
API key issued |
ZAN-SEC-013 |
VECTI-SEC-013 |
API key revoked |
ZAN-SEC-014 |
VECTI-SEC-014 |
Agent declared or its credentials changed |
ZAN-SEC-015 |
VECTI-SEC-015 |
Agent result refused: attestation did not verify |
ZAN-SEC-016 |
VECTI-SEC-016 |
Four-eyes triage request approved |
ZAN-SEC-017 |
VECTI-SEC-017 |
Four-eyes triage request refused |
ZAN-SEC-018 |
VECTI-SEC-018 |
Audit log integrity verification failed |
ZAN-SEC-019 |
VECTI-SEC-019 |
Security-relevant setting changed |
ZAN-SEC-020 |
VECTI-SEC-020 |
Agent sealing key refused: signature or generation did not verify |
ZAN-SEC-021 |
VECTI-SEC-021 |
Analysis plugin registered, changed or activated |
ZAN-SEC-022 |
VECTI-SEC-022 |
SARIF import source declared or changed |
ZAN-SEC-023 |
VECTI-SEC-023 |
SARIF import refused: undeclared source, scope or tool |
ZAN-SEC-024 |
VECTI-SEC-024 |
Checklist template version published or retired |
ZAN-SEC-027 |
VECTI-SEC-027 |
Report import refused: undeclared source, kind or scope |
ZAN-SEC-999 |
VECTI-SEC-999 |
SIEM connector health check |
Delegated scans stop until each delegating agent is updated and has a pinned signing key.
An agent in delegated credentials mode receives a repository's SSH key or HTTPS token sealed for
its sealing key. That key is now accepted only when the agent signs it with the Ed25519 key an
administrator pinned (decision 0031),
and no credential is ever sent in the clear, over TLS or not. Until an agent is updated and its
signing key is pinned, it claims no scan that needs a credential — those scans wait, pending and
costing no attempt, for an executor that can take them. Agents in local mode, image scans and
repositories without a credential are not affected. Before upgrading: pin each delegating agent's
signing key on the Agents screen, then update the agents. See
Agents.
Attempts counted by withheld claims are given back once, at the first start. Before this
version each poll of such an agent took a scan needing a credential — one attempt — and put it
back, so a scan nothing had tried could fail as "lease exhausted" at its first real takeover. On a
database with a delegated agent, the waiting scans of repositories carrying a credential that no
agent was ever handed (no AGENT_CREDENTIAL_SENT) have their attempts reset to 0, recorded once — by one instance, when several start together — as
SCAN_ATTEMPTS_REPAIRED. Not touched: a scan delivered at least once, running, completed or already
failed — run a failed one again by hand — image scans, and repositories without a credential.
Secrets reach the containers as files, not as environment variables. The shipped
docker-compose.yml now hands ENCRYPTION_KEY, the database passwords, the bootstrap password,
VECTISPIRE_SIGNING_KEY and the OIDC client secret over as Compose secrets under /run/secrets/:
a container's environment is readable by anything that can inspect it through the Docker daemon.
If you run your own composition or manifests, move these to files the same way
(ENCRYPTION_KEY_FILE, spring.config.import: optional:configtree:/run/secrets/). Your .env
must declare VECTISPIRE_SIGNING_KEY and VECTISPIRE_OIDC_CLIENT_SECRET, empty if unused:
Compose stops with the variable's name rather than dropping a signing key an installation was
using, because a replaced key makes every document already signed unverifiable.
The database is no longer published on the host. The 127.0.0.1:3306 port mapping is gone
and the database sits on an internal network. A tool that connected to it from the host needs
docker compose exec or a network of its own.
Every request body has a ceiling. 1 MB by default (VECTISPIRE_MAX_BODY_DEFAULT); routes
with their own keep it: VEX 16 MB, SARIF imports 32 MB, rule-set uploads 64 MB, agent results
256 MB. A client sending a larger body to an ordinary route now gets a 413.
The KEV feed is CISA's catalogue, read from the network. It was a list of ten records typed
into the code; the control plane now reads known_exploited_vulnerabilities.json every six hours
and from the Threat Intelligence settings tab, and a scan asks the stored copy instead of
downloading it. The control plane needs to reach www.cisa.gov — or set VECTISPIRE_KEV_URL to a
mirror (and VECTISPIRE_KEV_ALLOW_PRIVATE=true if it is on a private network), see
Configuration. The upgrade empties the old feed: until the
first synchronisation, the status says never synchronized and a scan marks nothing as actively
exploited. That first synchronisation also un-flags open issues whose CVE the catalogue does not
list, including those the typed-in list had flagged.
Scans need a host directory mounted at the same path — the shipped composition now makes one.
The scanners are containers the Docker daemon starts, and it resolves what it mounts into them on
its own host; the composition kept the scans' workspaces in the control plane's own /tmp, so every
scanner received an empty directory and no scan of the shipped docker-compose.yml ever
succeeded. It now mounts VECTISPIRE_WORK_DIR (default /var/lib/vectispire/work, some 3 GB for
the vulnerability database) at the same path and prepares it with a one-shot work-dir service;
docker compose up does it, nothing to do but have the disk. If you run your own composition or
manifests, mount a host directory at the same absolute path and set
JDK_JAVA_OPTIONS=-Djava.io.tmpdir=<path> — see Installation
and Configuration. Scanners now run as the workspace's owner
rather than as root, which could not read it.
Images built from the Dockerfile run as 1000:1000, like the published ones. If you built your
own and its audit mirror volume already exists, hand it over once:
docker run --rm -v vectispire_audit:/a alpine chown -R 1000:1000 /a.
SSH clones with a deploy key work through the composition — and it no longer mounts your
~/.ssh. Through the shipped docker-compose.yml no such clone had ever succeeded: the images
have no account for their user, the home was /, and the known-hosts file could not be created
("The known-hosts file could not be prepared: /.ssh", shown as "The clone of … failed.").
Behind that, the host-key check refused every host it had not met before ("Server key did not
validate") — outside the composition too, unless the host was already in the running user's
~/.ssh/known_hosts. Now a first contact is recorded and a changed key refused, the process's home
is $VECTISPIRE_WORK_DIR/home (the agent's under $VECTISPIRE_AGENT_WORK_DIR), and the
${HOME}/.ssh mount is gone, with VECTISPIRE_HOST_SSH now false in the composition. What to do:
- Nothing, if your private repositories carry a deploy key:
docker compose upcreates the home. - If you relied on the mount — only images you built from the
Dockerfileever read it — attach a deploy key to each of those repositories on the SSH keys screen. Alocalagent of thewith-agentprofile clones with$VECTISPIRE_AGENT_WORK_DIR/home/.ssh, empty unless you put a dedicated key there. - Your own composition or manifests: add
-Duser.home=<work dir>/hometoJDK_JAVA_OPTIONS. Without it, the images now fall back toHOME=/home/vectispire, which a plaindocker runcan write but which is lost with the container — hosts recorded there are met again as new. - Outside a container, a clone with a key no longer reads the running user's
~/.ssh/config: aHostalias,PortorProxyJumpthere stops applying to it. Put the real host and port in the repository URL. Hosts already in~/.ssh/known_hostsstay checked against it.
See Over SSH: the forge's host key for pinning the keys in advance.
EPSS scores come from FIRST's daily file, and scans no longer call api.first.org. Each scan
used to send the CVE it had found to FIRST's API — which told a third party what each repository was
vulnerable to — and on an estate without outbound access every score stayed unknown. The control
plane now downloads epss_scores-current.csv.gz once a day and from the Threat Intelligence
tab, stores it (some 380,000 rows, a few seconds on MySQL and PostgreSQL), refreshes the open
issues' scores from it, and scans read the stored copy. It needs to reach
epss.empiricalsecurity.com — or set VECTISPIRE_EPSS_URL to a mirror (and
VECTISPIRE_EPSS_ALLOW_PRIVATE=true on a private network), see
Configuration; an allow-list that opened api.first.org for
scans can close it. Until the first synchronisation, which the first maintenance turn runs half a
minute after the start, a new finding gets no EPSS score — unknown, not zero — and scores already
on issues stay until the file replaces them.
A permanent failure no longer retries; transient ones wait 1, then 5, then 15 minutes. A scan
that could not run — on an agent or on the built-in worker — fails at once, on its first attempt and
with the reason, when another attempt would meet the same refusal: a host key that changed, an
authentication refused, a repository, a branch or a sub-path that is not there, a credential that
will not open, a URL the clone refuses. Anything else — the network, a timeout, a daemon that did not
answer, a lapsed lease — goes back to the queue with the attempt counted and cannot be claimed again
for one minute after the first attempt, five after the second, fifteen after any later one
(VECTISPIRE_SCAN_RETRY_DELAYS, see Configuration), and fails at the
third. Before, a lone agent retook the scan it had just reported at its next poll and spent the three
attempts in seconds; the built-in worker failed a scan for good at its first error, with the raw
message. A sub-path the clone does not hold now fails the scan before any scanner runs, where each
analyser used to report it.
Signing a checklist off needs the signing key. The sign-off now renders and signs the checklist's
document in the same transaction. An installation with neither ENCRYPTION_KEY nor a configured
vectispire.signing.key answers 412 there, as every other signed export already did — and a sign-off
that cannot be signed is not recorded. See Security checklists.
A checklist line on static analysis reads "no data" until each repository is scanned again. A
line on builtin:sast, builtin:quality or a plugin now counts a repository as examined only where
the analysis read the tree's languages, judged on what the scan recorded: its census and, from V58,
the languages of the SAST rules its task carried. No earlier scan recorded the latter, so every such
line measures languages_unrecorded until the next scan of each repository, and an automatic yes
Vectispire gave there is withdrawn at the next measurement. Then, on an installation with only the
bundled rules — one Python pattern — a Java or JavaScript repository measures language_not_analysed:
install a rule set covering its languages (Rule sets, import from the catalogue) and scan again. A
line that passed before because its rules found nothing in code they could not read was not measuring
anything. See Security checklists.
Secure coding, IaC and secrets now read coverage, as vulnerabilities did — scores drop on an estate scanned in part. ISO 27001 A.8.28, A.8.9 and A.5.15, and the controls of the same categories in NIS 2, DORA, PCI DSS and SOC 2, were scored on zero findings alone: ten targets with one scanned clean read them compliant, beside an A.8.8 that read non-compliant on the same estate. They now carry A.8.8's coverage cap — a target never scanned makes the control non-compliant, one scanned outside the freshness window makes it partial at best, the score is at most the share of targets observed, and the detail says so. An estate fully scanned inside the window reads as before. On one scanned in part, these controls and their frameworks' scores fall at the first page load after the upgrade, and a declaration that such a control is implemented reads contradicted in the statement of applicability. The compliance progression rewrites the running month's point on its next capture and shows the fall as same estate, same rules, N points down: nothing in the estate changed — the old score counted targets nobody looked at as clean, and the months already captured keep that score. Scan the rest of the estate, or tell whoever reads the chart before they do. See Compliance.
A target never scanned has no security grade any more, and a portfolio, project or solution scanned
in part has its score capped — grades fall, and published badges can read no data. The scorecard
subtracts what it finds from a hundred, so a repository registered and never scanned read 100, A+, on
its card and on its README badge, and a project whose only target was never scanned read the same.
Such a card now grades NO_DATA, with no score, and the badge reads no data in grey. Where only some
of the targets are scanned — the global scorecard, a project's, a solution's — the score is capped at
the scanned share: ten targets with one scanned clean score 10, F, where they read A+. A repository or
image with a completed scan keeps its grade. Nothing is stored, so the change shows at the first read;
scan the targets the new recommendation names. See
How the scorecard grade is computed.
A plugin whose image is not signed no longer runs, unless the platform governor waives the
requirement for it. VECTISPIRE_PLUGIN_SIGNATURE_REQUIRED now defaults to true, on the control
plane's built-in worker and on every agent. Plugins did not exist in 0.9.0, so no released installation
loses anything; a development build that registered a plugin without a signature in its manifest
sees it refused from the first scan after the upgrade — the scan's Plugins card says refused —
unsigned, the scan lists the failure under plugin <id>, its issues stay as they were, and a checklist
line on it reads no data (plugin_unsigned). Before upgrading, declare the image's signer on each such
plugin, or — while it cannot be signed — have the governor record a waiver with its justification on the
plugin's page (PUT /api/v1/plugins/{id}/unsigned-waiver). Setting the variable to false still runs
every unsigned plugin on that executor, with no trace of why; the waiver is the documented way. Why the
default moved: a registry, mirror or tag compromised upstream runs code over the source of every project
the plugin is on for, and having no network does not stop it from fabricating or hiding findings. See
Plugins and
decision 0017.
Schema migrations V32 to V64 run at start, on MySQL and PostgreSQL. Back up the database first, as for any upgrade — backup and restore.
Changes an integration can see¶
- Three new SIEM events, and the export says when it stops.
VECTI-SEC-028is sent to the collector being left when the export is switched off or pointed elsewhere — the silence used to be the only signal.VECTI-SEC-029announces a new secret of high or critical severity, once per issue;VECTI-SEC-030an issue passing its remediation deadline, once per issue, from the hourly turn (forwarded from a minimum severity of Medium). A correlation rule written for the retired001and004does not match them: they took new numbers.GET/PUT /api/v1/siem/configcarrytlsCaPem,tlsCaSubjectandtlsCaNotAfter, andPOST /api/v1/siem/testacceptstlsCaPem— SIEM export. - The GitLab gate template now fails the pipeline on a red verdict.
ci/gitlab/vectispire-gate.gitlab-ci.ymlshippedallow_failure: true, so a failed gate showed as a warning and the pipeline passed; it now accepts only exit3, whichVECTISPIRE_GATE_MODE: advisoryproduces for a red verdict. A pipeline that relied on the old behaviour sets that variable. The template also needsVECTISPIRE_GATE_VERSION, the tag it was included at: it downloads that release'svectispire-gate.shand runs it only at the SHA-256 it pins — it ranci/vectispire-gate.shfrom the consumer's checkout, where there is no such file. It no longer declaresVECTISPIRE_REPOSITORY_IDorVECTISPIRE_CONTAINER_IDon its job, so a value set globally reaches the gate. See CI examples. - The release carries
vectispire-gate.shand its Sigstore bundle, signed by the same workflow identity as the jar. vectispire-cliexits2with the server'sdetailon a refusal, where it exited with curl's22and printed nothing. Exit1is now only a red verdict; a failed or timed-out scan is2.scanfollows the scan already waiting on a409;sbom --repo-idtakes the latest completed scan, where it took the latest, which could be pending;status, advertised and missing, exists.- A scorecard can grade
NO_DATA, with anullscore, and carriestotalTargetsandobservedTargets.GET /api/v1/scorecards/repositories/{id},/containers/{id},/globaland thescorecardofGET /api/v1/projects/{id}/complianceand/solutions/{id}/complianceanswer gradeNO_DATAandscore: nullwhen none of the card's targets holds a completed scan — they answered 100 andA_PLUS.scoreis no longer always present as a number: a client reading it as one must testgradefirst.observedTargetsbelowtotalTargetsmeans the score is capped at that share.SecurityGradegainsNO_DATA; a client mapping the grades it knows should treat an unknown one as no grade. - The dashboard's maturity ranking can grade
NO_DATA, with anullsecurityScore. IntargetScoreboardofGET /api/v1/dashboard/posture-analytics, a target holding no completed scan — its findings came from a SARIF import alone — ranks last asNO_DATA; it could read 100, A, at the head of the ranking. Its counts stay. A target never scanned carries no issue and is not listed, as before. - The dashboard's maturity ranking grades each target as its scorecard does: its scores and grades
change. In
targetScoreboardofGET /api/v1/dashboard/posture-analytics,securityScoreandmaturityGradeare now the target's scorecardscoreandgrade— the onesGET /api/v1/scorecards/repositories/{id}and/containers/{id}and the badge answer. The ranking had a rule of its own (100 minus 25, 10, 3 and 1 per open critical, high, medium and other; A from 90, B from 75, C from 50, D from 30), which read 0, F for ten open highs as for five hundred. The same rows read other numbers now: ten highs on a scanned target read 65, C, where they read 0, F.maturityGradetakes the scorecard's values,A_PLUS,A,B,C,D,F,NO_DATA, with the scorecard's thresholds (A+ from 95, A from 85, B from 70, C from 55, D from 40);A_PLUSis new to this field, and the document types it as that enum. A target scanned clean is now listed, at 100,A_PLUS; it was left out, having no issue. The rows keep their shape;openCriticalandopenHighare the card's counts,openMediumandopenLoware shown and not scored, andtotalResolvedandtargetMttrDaysare unchanged. - A
component_versionsline readsno_data(version_unrecorded) where the SBOM states no version for a declared package. Syft writesUNKNOWNfor a Maven dependency whose version a parent or a BOM manages and it does not resolve; that was judged "not an allowed version", the line readfail, and Vectispire's automatic answer wrote no for a module that is present. Now a package none of whose occurrences states a version makes its repository no data, the package named in the evidence, and an automatic no resting on it is withdrawn. Where some occurrences state a version, those judge and the others are named beside them; a stated disallowed version and a package absent from the SBOM still fail.NoDataReasongainsversion_unrecorded: a client mapping the reasons it knows should treat an unknown one as no data. GET /api/v1/solutionsaccepts areadAPI key, as the repository and image lists it regroups already did — it answered 403. A key restricted to one repository or image reads the projects holding it, partial, and no project through its account's grants.-
A project's figures and filters include the container images filed in it. Once an image is filed in a project,
GET /api/v1/issues?project_id=…and?solution_id=…answer its issues beside the repositories', and theopenIssuesofGET /api/v1/solutionscount them; each project node, solution node and theunfiledgroup gaincontainerCount(and projects andunfiledacontainerslist),repositoryCountstaying a count of repositories. Nothing changes until an administrator files an image: every existing image starts in no project. -
A rule-set change that would resolve open issues answers 409 unless their number is accepted.
POST /api/v1/rule-sets/{id}/activateandPOST /api/v1/rule-sets/deactivaterefuse, with the typeurn:vectispire:problem:rule-set-activation-loses-issuesand the membersaffectedIssuesandlosingIssues, a change whose rules leave behind open issues the next scan would resolve with their triage — unless the body carriesacceptLosingequal toaffectedIssues, read again at the request; a stale, lower or higher number is refused with the current one. A change that resolves nothing needs no field.GET /api/v1/rule-sets/deactivate/impactpreviews a deactivation as/{id}/impactpreviews an activation, and neither counts the bundled rules' issues any more, which never resolve. Rule sets did not exist in 0.9.0, so no integration built against that release calls these routes; one written against a development build since does, and must now send the number — Rule sets. -
Two new reasons for a measurement without data,
language_not_analysedandlanguages_unrecorded, in a measurement'sreasonand a repository'sstatusin its evidence — a script matching the reasons it knows should treat an unknown one as no data, which it is. -
A name already taken is a 409 with a type, whatever the gesture. Creating a solution, or renaming one, to a name another solution holds (case aside) answered
400; it answers409with the typeurn:vectispire:problem:solution-name-taken. Creating a project, or renaming one within its solution, to a name the solution holds answered400; it answers409with the typeurn:vectispire:problem:project-name-taken, as a move to such a solution already did. Thedetailis unchanged. Branch on thetype, not on the status — a400from these routes is now a malformed name only (empty, too long) — Solutions and projects. - A compliance verdict can be
NO_DATA. Until a target has been scanned successfully, every control that reads the estate — and every framework — readsNO_DATAwith a score of zero, which is no measurement, onGET /api/v1/compliance/summary, the framework detail, the evidence bundle's01_compliance_frameworks.jsonand the statement of applicability'smeasured; a target never scanned readsNO_DATAin the matrix, with noframeworkScores. It read compliant on zero findings nobody looked for, and ISO 27001 A.8.8 non-compliant on "1 target(s) have never been scanned" with no target registered. The SoA'sdivergencegainsUNEVIDENCED, betweenOVERSTATEDandUNDERSTATED, for a line evidenced here with nothing measured — it readCONSISTENT. No monthly capture is written for a framework with nothing measured. - A checklist answer names its author's kind. Every answer in a checklist view, a line's history
and
checklist.jsoncarriesansweredByKind—person, orsystemfor an answer Vectispire gave from the line's measurement — andwithdrawn, true only on the history row by which Vectispire withdrew its own answer. Tell an automatic answer byansweredByKind, never byansweredBy: it readsVectispire, a name an account may also hold.checklist.jsonis statementform2 for it. - Every error is an RFC 9457 problem (
application/problem+json) with adetailmeant to be shown — see API errors. Refusals that carried no sentence now do: an unknown route, 405, 406, 415, an unreadable body, the sign-in refusals, the 401 of a missing or invalid credential (which had no body) and the 403 of a role (which had the container's{timestamp, status, error, path}). The three rate limiters answered{"message": …}: the sentence is nowdetail, and a newretryAfterSecondsrepeats theRetry-Afterheader. A client readingmessagemust readdetail. A URL refused before the application — by the security firewall (//, an encoded..) or by the servlet container (a lone%), which answered its own HTML page — is a 400 problem too. - A 500 no longer quotes the failure. An error nobody wrote a message for — including one that
used to answer 400 or 404 with a library's own words ("For input string", "No value present",
"No enum constant …") — is a 500 whose
detailgives acorrelationId, logged with the error. The refusals Vectispire writes keep their status and their sentence. - Granting a repository or an image that does not exist, or that the granting administrator cannot see, is refused with a 404; a grant naming a missing project was a 400 and is a 404.
- An API key restricted to a target acts on that target only; a restriction no route would enforce is refused when the key is issued. A key restricted to a deleted target is revoked with it, and each revocation is audited under the key's id.
- Integration keys act for the account they belong to; an agent's key is confined to the agent routes (403 elsewhere).
- The threat-intelligence sync is audited as
THREAT_INTEL_SYNCED, no longerSETTING_UPDATED; a change of certified scope is audited asCERTIFIED_SCOPE_CHANGED. A filter on the old operation stops finding syncs. - Recording a gate verdict is a write: the auditor and the platform governor are refused.
GET /api/v1/threat-intel/statusand both sync routes answer the catalogue's version, its release date, the last attempt and its error;statusisNEVER_SYNCED,SYNCEDorFAILED. A sync that cannot read the catalogue answers 200 withFAILEDand the reason, keeps the catalogue in use, and is audited as a failedTHREAT_INTEL_SYNCED.GET /api/v1/epss/cve/{id}answers from the stored catalogue and EPSS file only — a 404 for a CVE neither holds.- The same routes answer the EPSS file's state under
epss: its status, model version, score date, the CVE it scores, the last attempt and its error, andinProgresswhile a synchronisation runs. Both sync routes read the catalogue and the EPSS file, and write oneTHREAT_INTEL_SYNCEDentry for each.epss_scoreandepss_percentileleavet_threat_intel_feed(V45). - An OWASP review is recorded before the model is asked:
GET …/owasp-reviewmay answerstatus: runningwhile another request waits, and readsfailedonce the model's timeout has passed with nobody left to settle it. - New finding types
pluginandimported, and new fields on issues and scans (tool,toolName,toolVersion,importSource, a scan'splugins[]). The OpenAPI document in the repository is the contract. - Reachability is not computed, and says so. An issue's
reachabilityandreachableSymbolsstay inGET /api/v1/issuesandGET /api/v1/issues/{id}, marked deprecated in the OpenAPI document: alwaysUNKNOWNand null, since nothing analyses the call graph. Removed where only the interface read them:reachableEpssCountand each ranked issue'sreachabilityfromGET /api/v1/epss/priorities,reachabilityfrom each target of the blast radius, thereachabilitykey of an attack-path node'smetadata, anddeterministic.exposurefrom the AI advisor's answer.POST /api/v1/ai-advisor/explain/cve/{id}ignores areachabilityparameter. In OpenVEX, a finding awaiting triage reads "Awaiting contextual triage." rather than "Awaiting reachability confirmation and contextual triage." - The AI advisor's
deterministic.activelyExploitedisdeterministic.kev:LISTED,NOT_LISTEDorUNKNOWN, read from the stored CISA catalogue — the boolean said false before any catalogue had been read.deterministic.packageName,currentVersion,targetVersionandremediation.suggestedVersionare null when nothing is recorded, rather than "the component", "current" or "the fixed version", and a model's advice no longer carries a VEX justification. POST /api/v1/ai-advisor/explain/cve/{id}no longer readspackageName,currentVersionorfixVersion: they were printed as the advice's facts on the caller's word alone. A client still sending them is answered as if it had not; the route accepts no integration key. A CVE no issue you can see carries is explained from the stored feeds alone, and its VEX suggestion isunder_investigationeven when CISA lists it — it saidaffected, about an estate nothing showed to carry it. An issue's suggested upgrade is one command for the ecosystem its purl names (Maven, npm, PyPI, Cargo, NuGet, Composer, Go), with a Maven<dependency>only for Maven, and none when the purl is missing or names another ecosystem — it offeredmvnandnpmtogether whatever the component. Both explain routes takelanguage(en,fr; English when absent, anything else a 400): the model answers in the screen's language, where it answered in French for everyone.- The EPSS generation a file replaces is kept until the next one is applied (V47,
epss_previous_generation), so a scan enriched during a switch no longer finds its scores gone;t_epss_scoreholds two files' rows between synchronisations, some 760,000. - The agent protocol has a seventh call,
POST /api/v1/agent/jobs/{id}/failure: a scan the agent claimed and could not run is reported at once with its reason — requeued with the attempt counted, or failed at the last — instead of waiting twenty minutes for its lease to lapse. The claim's answer carries theattemptthe report names; an older agent reads past it. Signed like a result when the agent's key is pinned, audited asAGENT_SCAN_FAILED. An agent of this version falls back to the lapse against an older control plane (404). See Agents. - The failure report takes a
kind,permanentortransient(V48 addst_scan.not_before): a permanent failure fails the scan at once, a transient one waits before its next claim. Absent — an older agent — or unknown reads as transient. The answer addspermanentandretryAt, the instant the scan can be claimed again. A scan's summary (GET /api/v1/scans,GET /api/v1/scans/{id}) addsnotBefore, set on a waiting scan whose last attempt could not run. - A scan's detail adds
examinedTypes(V49 addst_scan.examined_types): the built-in finding types whose step produced in that scan, as wire names —vulnerability,secret,iac,sast,quality,eol,license. A type left out was not examined, and its issues were left as they were.nullmeans not recorded — a scan from before this version, or one that never ran — and never examined nothing, which is[]. Plugins stay inplugins, in their three states. - A declared source states its
kinds(V50 addst_sarif_source.kinds):sarif,coverage,test_report. A declaration without them issarifalone, and every source declared before this version stays a SARIF source.toolsis required withsarifand refused without it. - Switching four-eyes on needs two accounts that can publish a checklist template — the platform governor, an administrator or a CISO — besides an account that can approve a triage: under four-eyes a template's author may not publish it. The setting is refused with a sentence saying so; a deployment where it is already on is not changed. See Four-eyes.
- Switching four-eyes on also needs two accounts that can approve — an administrator, a CISO or a security champion: under four-eyes a project's checklist is signed off by an approver who wrote none of it, and the approver who filled one in cannot sign it. Refused with a sentence saying so; a deployment where the setting is already on is not changed. See Four-eyes.
- A 409 may name its cause in the problem's
type,urn:vectispire:problem:<cause>, where a route refuses for several reasons that call for different gestures — the project checklists' routes do (checklist-changed,checklist-line-changed,checklist-four-eyes,checklist-incomplete, …), and so do the checklist templates' (checklist-template-changed,checklist-template-not-draft,checklist-template-has-draft, … andchecklist-four-eyes, which means the same there — see Checklist templates). A problem without a cause keepsabout:blank; thedetailis unchanged. Achecklist-incompleteproblem also names its lines as data, in alinesmember — each line'sitemId,positionandproblems(unanswered,evidence_required, …) — so that a client points at them in its own language rather than parsing the English sentence. - A checklist's measured lines add two 409 causes and change three routes. A submission answers
checklist-measurement-contradictedwhen a line is answered yes where its measurement fails, and a sign-offchecklist-measurement-changedwhen a line's measurement is no longer what the submission stored — each naming its lines in the problem'slinesmember (itemId,position,answer,outcome,reason, and for the sign-offsubmittedOutcome,submittedReason). A yes where a measurement has no data needs a comment and a proof at submission:checklist-incompletenames themcomment_requiredandevidence_required, the tokens it already had. The answer route takes an optionalmeasurementDigest; a checklist line's view gainsrule, and a template version's items and each measurement carry theirboundRule, structured as the rules route takes it. On a draft, a line'sproblemsand the checklist'sreadyToSubmitcount what its measurement keeps from a submission —measurement_contradictedfor a yes against a failure, the comment and proof a yes without data needs — judged as the submission judges it, so a client needs no second request to know whether it will pass. A measurement's evidence names each repository,repositoryNamebesiderepositoryId(null for one no longer in the project). The closed vocabularies of the checklist views are enumerated in the OpenAPI document, and the two problems'linesmembers have their schemas (ChecklistIncompleteProblem,ChecklistMeasurementProblem). A SARIF import's view gainstoolKeys(V53): the tool keys its runs were accepted for, a sorted list,nullfor an import accepted before. - A SARIF import's
toolsis a list of strings, onename versionper run in the report's order — it was one comma-joined string, on the upload's answer and onGET /api/v1/repositories/{id}/sarif-imports. A comma inside a tool's version is written as a semicolon, so a comma always separates two tools. SARIF imports are new in this release, so no 0.9.0 integration read the string; a script written against a development build that split it must read the list.toolKeysis a list too, sorted — it was the keys joined by commas — and staysnull, never an empty list, for an import accepted before V53. On the same lists, a plugin activation names its project —projectName,solutionId,solutionNamebesideprojectId— and a declared source its key,apiKeyNamebesideapiKeyId(nullonce the key is revoked); a scan'splugins[].stateis enumerated in the OpenAPI document:produced,not_applicable,absent,refused— the last withrefusal(unsigned,signature_unverified); a produced one carriessignature(verified,waived,not_required). A plugin carriesunsignedWaiver(nullfor none), set and withdrawn throughPUT/DELETE /api/v1/plugins/{id}/unsigned-waiver; a scan's task carriesrunsUnsignedwith each plugin. Two reasons join a checklist measurement'sno_data:plugin_unsigned,plugin_signature_unverified. Two audit operations,PLUGIN_SIGNATURE_WAIVEDandPLUGIN_SIGNATURE_WAIVER_REVOKED, both signalled asVECTI-SEC-021. - A new key scope,
report_import, never granted by default: the scope of the coverage and test-report uploads, apart fromsarif_importso that a key sending a coverage figure never deposits findings.
New¶
- A syslog-over-TLS collector can pin its own CA. Pasted in PEM on the SIEM card, it replaces the
Java runtime's trust store for that connection alone — no more
cacertsmounted over the JVM's, which made every outbound TLS connection trust the private CA too. Only a current CA certificate is accepted; hostname verification stays on — SIEM export. -
One project on its own: its read, its compliance and score, its consolidated SBOM.
GET /api/v1/projects/{id}reads one project as its node in the tree describes it, its solution named.GET /api/v1/projects/{id}/complianceandGET /api/v1/solutions/{id}/compliancerun the estate's evaluation over the scope's targets only — same controls, same caps,NO_DATAwhen none of them was scanned — with the portfolio scorecard computed the same way.GET /api/v1/projects/{id}/componentsmerges the components of the newest completed scan of each repository and image by package URL and version, naming the targets carrying each and what was read of every target (listed,empty,absent,never_scanned, andcomplete), andGET /api/v1/cyclonedx/projects/{id}/cyclonedx-vex.jsonrenders it as CycloneDX 1.5 with the project's VEX,compositionssaying whether it is complete. The first three accept areadkey, the document anexportkey. Each follows the tree's rule: a project seen in part is computed over that part and sayspartial; one seen not at all answers404like one that does not exist — Solutions and projects. -
Container images can be filed in projects (V59 adds
t_container.project_id), like repositories: at most one project each, filed, moved and taken out by an administrator throughPUT/DELETE /api/v1/projects/{id}/containers/{containerId}, audited asPROJECT_CONTAINERS_CHANGED. A grant on a project now covers its images too, resolved at each request: an image filed in the project is visible to the project's holders at once, and stops being visible through that grant the moment it leaves. The tree lists each project's images and the unfiled ones; a project's move carries them; deleting a project returns them to no project; a team granted a project is notified of its images' scans.GET /api/v1/containersnames each image's project (projectId,projectName), and Containers shows it with a link into the tree, where images are filed with the same dialogs as repositories. Checklists still measure a project's repositories only — Solutions and projects. -
The languages detected in a repository are kept. Every repository scan counts its tree's languages and keeps them;
GET /api/v1/repositoriesgives each repositorydetectedLanguagesfrom its newest completed scan (nullwhen unknown,[]when none), and each project ofGET /api/v1/solutionsthe union over the repositories the caller sees, withlanguagesUnknownFor. They are spelled as a plugin manifest declares itslanguages— Plugins. - Vectispire answers the lines it measures (V56 adds
answered_by_kindandwithdrawntot_checklist_answer). On a draft checklist, a line bound to a rule is answered yes when its measurement passes and no, with the measurement as its comment, when it fails — when a scan completes or a SARIF, coverage or test report is accepted for one of the project's repositories, and when the checklist is opened, moved or reopened; no data answers nothing, and withdraws an answer of Vectispire's that rested on data it no longer has. The author is Vectispire, no account: marked automatic on the screen, in the history, theEvidencesheet andchecklist.json, audited asCHECKLIST_ANSWEREDwith no user. A person's answer is never replaced; Vectispire replaces its own only when what it states changes — its value, or its comment carrying the figures — so a new scan measuring the same thing writes nothing. Submitting and signing off are unchanged — people, under four-eyes, and a yes on a line asking for a proof still needs it. On by default: the platform governor's settingchecklist_auto_answerreturns to answers by people only. Checklists already in draft are answered at their project's next scan, import or opening. See Security checklists. - The measured lines of a checklist answered as measured, in one act.
POST /api/v1/projects/{id}/checklists/{revision}/answers/as-measured, body{ edition, lines: [{ itemId, measurementDigest }] }, on a draft: each line named — as shown, with the digest read — that is unanswered, still on that evidence and passing is answered yes by the caller, resting on that measurement — the one click of a measured line, for every line shown at once, each answer a row of its history and aCHECKLIST_ANSWEREDentry of its own. Lines already answered, named lines whose evidence moved, passing lines not named, lines without data and failing lines — whose no needs a comment a person writes — are left alone and returned inskippedwith their reason (already_answered,measurement_changed,not_shown,no_data,needs_comment). The whole-project guard of every checklist route; anything written since the edition read refuses the act (checklist-changed). See Security checklists. - A signed-off checklist is a signed document (V55 adds
t_checklist_document).GET /api/v1/projects/{id}/checklists/{revision}/documentreturns a zip:checklist.xlsx, the organisation's own workbook with only the answer, comment and header cells written and anEvidencesheet added, andchecklist.json, the same statement machine-readable — each with a detached signature when the revision is signed off. That package is rendered and signed inside the sign-off and served as stored, so next year's download is the document that was signed; a draft or submitted revision renders on request, unsigned, and says so.@AcceptsApiKey(EXPORT), auditedCHECKLIST_EXPORTED. Verify withcosign verify-blob --key vectispire-signing-key.pub --insecure-ignore-tlog=true --signature checklist.xlsx.sig checklist.xlsx— Security checklists. - Checklist lines measured by the evidence that ran (V54 adds
t_checklist_measurement). A security lead binds a rule to a draft's line (PUT /api/v1/checklist-templates/{slug}/versions/{ordinal}/rules, on therevisionread, auditedCHECKLIST_TEMPLATE_RULES_BOUND): dependency analysis, a findings threshold over built-in steps, plugins or imported tools, coverage, a test suite passed, or component versions from an explicit list — each with its maximum age (required, 1 to 366 days) and its own parameters, part of the line's content digest and carried like its proof requirement.GET /api/v1/projects/{id}/checklists/{revision}/measurementsshows each bound line's measurement — pass, fail or no data with its reason (never_examined,step_absent,examination_unrecorded,stale,not_applicable_anywhere, …), never a pass by default — beside the answer. The submission measures again and refuses a yes against a failure; the sign-off measures again and is refused when a measurement changed since the submission, and freezes the rest with the revision. Vectispire never answers: an answer may rest on a measurement the person read, and stays theirs. A repository's scans from before V49, and a source's SARIF imports from before V53, read asexamination_unrecorded— nothing recorded whether they looked — until its next scan or upload. On the template screen a security lead binds a draft's line to its rule, the line's KPI text beside the parameters and the rule in words beside both, a secrets at zero preset included, and saves the lines changed together; the auditor and a published version read each line's rule. On a project's checklist each bound line shows its measurement — met, not met, or no data with its reason in words — as of when, its figures and each repository's evidence on demand; a met or not-met line offers to answer as measured in one click, resting on the measurement read. The page says whether the measurements are live or frozen by the sign-off, and Submit stays greyed out, with the lines named, while a measurement keeps the revision back — a yes contradicted, or a yes without data missing its comment or proof — Security checklists. - What proof a checklist line asks for is set on the template's draft
(
PUT /api/v1/checklist-templates/{slug}/versions/{ordinal}/evidence, on therevisionread): for each line,none,link_or_fileorfile, and for a proof that expires, its validity in months (1–120). Until now every imported line asked for none, so no proof was ever required at submission. The requirement is part of what the line asks: a line whose requirement moved is changed against the previous version, and a project's answer carried onto it waits for confirmation. Confirming a layout again keeps each line's requirement, and a new workbook's line takes the previous version's under the same key; deriving a version carries them. AuditedCHECKLIST_TEMPLATE_EVIDENCE_SET. The draft's screen sets it line by line, in the Proof asked column of the items read, and sends only the lines changed — Checklist templates. - Project checklists, answered by people (V52 adds
t_checklist,t_checklist_answer,t_checklist_evidence,t_checklist_file). A project's checklist is opened on a published template version (POST /api/v1/projects/{id}/checklists), answered line by line — every answer kept, with its author and instant — proven by links and files (25 MB, served back only as downloads), submitted, returned, signed off by an approver, reopened, or moved to a newer version with its answers carried: current where the line did not change, awaiting confirmation where it did. Only a caller who sees the whole project reads or writes it; anybody else is answered 404. Every write names theeditionread. AuditedCHECKLIST_*; a sign-off is sent to the SIEM asVECTI-SEC-025, a refused sign-off or a return asVECTI-SEC-026. Deleting a project deletes its checklists; the audit entries stay. The screens come with the interface's half of this lot.GET /api/v1/projects/{id}/checklists/contextnames the project and its newest revision for a page that has no checklist to show yet; the list and the offered versions stay bare arrays. - Coverage and test reports from declared sources. A pipeline sends a JaCoCo, Cobertura or lcov
coverage report (
POST /api/v1/repositories/{id}/coverage-imports?format=…) or a JUnit report — one XML file or a zip of them (…/test-report-imports) — with areport_importkey its source is declared for. The figures are kept, never the document; an empty report is refused rather than recorded as zero, and nothing opens or resolves an issue. AuditedCOVERAGE_IMPORTED,TEST_REPORT_IMPORTEDandREPORT_IMPORT_REFUSED, the refusal sent to the SIEM asVECTI-SEC-027.scripts/vectispire-cli.shgainscoverageandtest-report— Importing coverage and test reports. - The scan page shows which steps examined the tree. A What this scan examined card lists the built-in steps that produced and those that did not look — failed, or not run for that target — so a clean list of findings reads as clean only for the steps that ran. Scans from before this version say Not recorded until the target is scanned again — Scans.
- Solutions and projects: a solution holds projects, a project references repositories, and a grant may name a whole project — Solutions and projects.
- A project moves to another solution (
solutionIdonPATCH /api/v1/projects/{id}), with its repositories, grants, checklists, plugin activations and SARIF sources; nobody's access changes. A name the target solution already holds is refused with 409project-name-taken— Solutions and projects. - Analysis plugins: third-party analysers packaged as container images, registered by the
platform governor, switched on per project, run confined like the built-in scanners, only when
a language they declare is present. An image declares its signer; the signature is verified
before the pull, and a plugin that declares none is refused unless the governor waived the
requirement for it, in writing (V60) —
VECTISPIRE_PLUGIN_SIGNATURE_REQUIRED, on by default. - SARIF imports from declared internal sources (a CI job, an on-premise SonarQube), each bound
to one restricted key and one scope, with the provenance kept on every issue —
Plugins and SARIF imports. Gate policies count plugin and
imported findings only when they say so (
include_plugins). - Git over HTTPS with a managed token bound to one host, beside SSH keys;
VECTISPIRE_GIT_ALLOWED_HOSTSrestricts where anything is cloned from. - SIEM export over syslog UDP, TCP or TLS (RFC 5424, CEF), sent after commit — SIEM export.
- Parallel scans on an agent, 1 to 16 at once (
max_concurrent), counted by the database. - One vulnerability database per host, downloaded once under a lock and shared read-only by
every scan instead of once per scan (some 3 GB), in
VECTISPIRE_VULNERABILITY_DB_DIR; the matcher now runs with no network. - Resetting an agent's sealing key from the Agents screen, for a host whose clock was put back or a key suspected of having leaked.
- Single sign-on records the provider's second factor and may require it
(
VECTISPIRE_OIDC_REQUIRE_MFA); Vault Transit may hold the encryption key. - Signing in through single sign-on returns to the page asked for, filter included, instead of the dashboard — and only to a page of this application: the sign-in screen forwards a return address it would follow itself, and the control plane checks it again before keeping it.
- The screens no longer show reachability, which nothing computes: the issues list loses its
reachable / unreachable tags, the issue detail its reachability panel, the EPSS page its
"reachable and weaponised" card and its column, the blast radius its column. The scorecard
charges every critical 8 points, the EPSS ranking is CVSS × EPSS with KEV on top, and the
attack paths no longer admit or flag a node on it — no figure an installation has shown moves,
since every issue read
UNKNOWN. The AI advisor says the exposure was not assessed. - The AI advisor no longer invents exploitation figures. Explaining a CVE the estate does not carry showed an EPSS probability of 75 % for every CVE, and "actively exploited" for two identifiers typed into the code; a listed CVE without a score read 85 %. It now shows the KEV listing and the EPSS score the stored feeds hold, and says "unknown" when they hold nothing — before the first synchronisation, for instance. The impact sentences nobody had looked up ("a remote attacker may execute arbitrary code") are gone, no upgrade is proposed without a recorded fixed version, and the EPSS page no longer shows a percentile of 0 for a CVE the file does not score.
- The bundled rules no longer pile up in the work directory. Each start of the control plane
or of an agent unpacked them to a new
vectispire-bundled-rules-*directory and none was ever removed. The directory now goes when the process stops, and the first start of this version sweeps what earlier ones left: only directories of that name, owned by the process's user, older than the process and held by no running one.
Security¶
This release closes the findings of the security review of 2026-09-26 and its follow-ups —
authentication limits under concurrent attempts, single sign-on linking, SSRF and parser
differentials on clone URLs, bounded outbound reads, audit and SIEM completeness, and the
agent's sealing key. Jackson is raised above the Spring Boot BOM for GHSA-q4xh-88c3-wmh7 (High) and
two related advisories, on the control plane and the agent, whose own SBOM is now scanned in the
pipeline — it was not, and sat on an advised version unseen. The cosign verify-blob --key commands
the product and the compliance guide hand out now carry --insecure-ignore-tlog=true: without it,
cosign looked for the signature in a transparency log Vectispire never publishes to and refused every
export. Build dependencies and plugins are now verified against signatures and
checksums. The detail is in the commit history and in the
decision register.