Skip to content

v0.11.1

Choose a tag to compare

@github-actions github-actions released this 01 Nov 20:15
· 134 commits to main since this release
be5d0f8

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 reason strings returned to MCP agents. A bridged lifecycle event whose scope isn't version-shaped is now withheld from the answer. It is counted under sources (status: withheld) and in not_established, never dropped silently. verdict.reason and causes[].reason also go through strip_unsafe. This is checked in core/agent_tools on purpose: analyzers/lifecycle_events.py is 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 it safe_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 repository block passed GitHub's full_name, url, license and pushed_at through as given. Each now must have its shape (the GitHub owner/name grammar, an SPDX-style id, an ISO timestamp) or it is null. The URL is built from the validated name, never taken from the API. The GitHub repository grammar also used re.match with $, which accepted a trailing newline ("owner "). It now uses fullmatch.
  • 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 advisories listed every advisory id and built https://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 new ids_withheld field. The verdict and affecting_this_version_count are 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.reason and causes[].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_file skips 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.

Fixed

  • NVD requests stay inside NVD's rate limit, are retried, and can use an API key (NVD_API_KEY). A whole-catalog openpulse report makes 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-After or waiting out NVD's 30 s window.
    • Key: read from NVD_API_KEY when 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.