Repository navigation
v0.11.1
A patch release:
- Complete monthly reports. Whole-catalog reports no longer lose NVD advisories to NVD's rate limit. Requests are paced, retried, and can use
NVD_API_KEY. A source that still fails is named in the report as "unknown, not absent" instead of reading as "nothing found". The November 2026 report run, and the 2026-10-30 early edition before it, each silently lost a different set of projects' advisories. - Five untrusted-text hardening fixes for the MCP tools (track T, T-F3).
No contract or verdict changes.
Security
- Lifecycle cycle strings and engine reasons are checked at the agent boundary (track T, T-F3 fix B, v0.12.0 item U1). Release-cycle names come from endoflife.date and become event scope versions, which the engine interpolates into
reasonstrings returned to MCP agents. A bridged lifecycle event whose scope isn't version-shaped is now withheld from the answer. It is counted undersources(status: withheld) and innot_established, never dropped silently.verdict.reasonandcauses[].reasonalso go throughstrip_unsafe. This is checked incore/agent_toolson purpose:analyzers/lifecycle_events.pyis imported by the frozen evaluation's replay, so the engine, the CLI and the evaluation are unchanged. Every real cycle in the bundled fixtures (24) is version-shaped. - Error text carries no control, bidi or zero-width characters (track T, T-F3 fix C, v0.12.0 item U1).
collectors.errors.redact, and with itsafe_message, every collector error record, the GitHub reader's skip reasons and webhook delivery errors, now turns C0/C1 control characters into a space and removes bidi overrides and isolates, directional marks and zero-width characters (strip_unsafe). This error text reaches terminals, webhooks and MCP agents, where those characters could reorder or hide what is shown. Stripping runs before redaction, so a hidden character can't split a secret past it. - Repository metadata is validated field by field (track T, T-F3 fix E, v0.12.0 item U1). The MCP answer's
repositoryblock passed GitHub'sfull_name,url,licenseandpushed_atthrough as given. Each now must have its shape (the GitHub owner/name grammar, an SPDX-style id, an ISO timestamp) or it isnull. The URL is built from the validated name, never taken from the API. The GitHub repository grammar also usedre.matchwith$, which accepted a trailing newline ("owner "). It now usesfullmatch. - Advisory ids are validated before they are listed or put in a URL (track T, T-F3 fix D, v0.12.0 item U1). The MCP answer's
advisorieslisted every advisory id and builthttps://osv.dev/vulnerability/<id>from it. These ids are upstream strings. Only an id with the OSV<DB>-<ENTRYID>shape (CVE, GHSA, PYSEC, RUSTSEC, GO, MAL, RHSA…) is now listed, percent-encoded in its URL. Anything else is withheld and counted in the newids_withheldfield. The verdict andaffecting_this_version_countare unchanged. - Agent inputs must be identifiers, and are never echoed (track T, T-F3 fix A, v0.12.0 item U1). The MCP tools took package names, versions, ecosystems and image refs as free text and repeated them in
dependency,verdict.reasonandcauses[].reason. So an agent, or text injected into it, could make OpenPulse return arbitrary instructions inside fields the agent trusts.- What's accepted: these values must now match a general identifier shape (letters, digits and
@ / . _ : + ~ -for names; similar sets for versions, ecosystems and image refs). Every ecosystem's real names fit, including legacy npm names with capitals. - Tool arguments: a failing argument is refused with a reason that doesn't repeat it.
- File entries:
check_dependency_fileskips a failing entry like an unpinned one ("entry N: package is not an identifier - skipped, value not shown"). The agent writes the files it asks about. - Unchanged: every dependency in the bundled fixtures passes the shape.
- What's accepted: these values must now match a general identifier shape (letters, digits and
Fixed
- NVD requests stay inside NVD's rate limit, are retried, and can use an API key (
NVD_API_KEY). A whole-catalogopenpulse reportmakes one NVD request per project, unpaced and keyless. NVD allows 5 requests per 30 s without a key and answers the excess with 403, so each run silently lost a different set of projects' advisories: vitess in November 2026, timescaledb in the early edition.- Pacing: requests are now paced process-wide, 6.5 s apart without a key and 0.7 s with one.
- Retry: a 403, 429 or 503 is retried up to 3 times, honouring
Retry-Afteror waiting out NVD's 30 s window. - Key: read from
NVD_API_KEYwhen none is passed, and never logged. - Cost: a keyless whole-catalog report takes about 12 minutes longer; a key (free from NVD) removes most of that.
- The monthly report names sources that did not answer, instead of reading them as "nothing found". The November 2026 run lost vitess's NVD advisories to NVD's rate limit, and the report showed vitess as having no security changes. Each project's failed sources (name, error category and HTTP status, never the error text) are now listed in the executive summary and in "Data gaps and limitations", as "unknown, not absent", and counted in the metadata sidecar under
coverage.source_errors. A report without failures is unchanged.