Module: StillActive::OsvClient
Overview
OSV (api.osv.dev) enrichment for advisories deps.dev has already discovered.
deps.dev is the discovery source -- it lists a package version's advisory ids --
but it stores only CVSS 3.x (a CVSS-4-only advisory comes back with cvss3Score
0, which reads as unscored) and carries no fixed-version ranges at all. OSV
supplies all three gaps: a GHSA severity LABEL (database_specific.severity)
that reads correctly even for a v4-only advisory, the CVSS v4 VECTOR (turned into
a real score, so the finding carries a security-severity number), and the
per-package fixed versions the "capped below the fix" signal compares against a
poison cap's ceiling.
Enrichment is best-effort: any failure (missing record, transport error, odd shape) leaves the advisory exactly as deps.dev produced it. It must never drop an advisory the audit already found, so it only ever ADDS fields.
Constant Summary collapse
- BASE_URI =
URI("https://api.osv.dev/")
- ECOSYSTEM_NAMES =
Every ecosystem SbomReader/deps.dev resolve, mapped to OSV's package-ecosystem casing (verified against live OSV records). One advisory can name the same package in several ecosystems, so
affectedis filtered to the one being audited; the native Bundler path carries no ecosystem and is always rubygems. Anything unmapped falls back to name-only fix filtering rather than dropping fixes. { rubygems: "RubyGems", pypi: "PyPI", npm: "npm", cargo: "crates.io", maven: "Maven", go: "Go", nuget: "NuGet", }.freeze
- CVSS_PRIORITY =
Prefer the newest CVSS version a record carries (v4 is the whole point: it's the one deps.dev can't score).
severity[].scoreis the vector STRING, oddly named. { "CVSS_V4" => 3, "CVSS_V3" => 2, "CVSS_V2" => 1 }.freeze
- TYPE_VERSIONS =
A v2 vector has no
CVSS:X.Yprefix, so the version can't be read from the string; fall back to the entry type so the CycloneDX rating method labels it v2. { "CVSS_V4" => "4.0", "CVSS_V3" => "3.1", "CVSS_V2" => "2.0" }.freeze
Instance Method Summary collapse
-
#detail(advisory_id:) ⇒ Object
Fetch and parse one OSV record by advisory id (GHSA/CVE).
-
#enrich(advisories, ecosystem:, name:) ⇒ Object
Enrich each advisory in place with the OSV severity label and the fixed versions for the audited package.
Instance Method Details
#detail(advisory_id:) ⇒ Object
Fetch and parse one OSV record by advisory id (GHSA/CVE). Returns
{ severity_label:, cvss_score:, cvss_version:, cvss_vector:, affected: [...] }
or nil when the id is absent or OSV has no usable record for it. A non-object
body (a CDN/error envelope that parses to an array or scalar) yields nil rather
than raising on dig.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/still_active/osv_client.rb', line 74 def detail(advisory_id:) return if advisory_id.nil? body = HttpHelper.get_json(BASE_URI, "/v1/vulns/#{encode(advisory_id)}") return unless body.is_a?(Hash) cvss = best_cvss(body) { severity_label: body.dig("database_specific", "severity"), cvss_score: cvss[:score], cvss_version: cvss[:version], cvss_vector: cvss[:vector], affected: Array(body["affected"]).filter_map { |entry| parse_affected(entry) }, } end |
#enrich(advisories, ecosystem:, name:) ⇒ Object
Enrich each advisory in place with the OSV severity label and the fixed versions for the audited package. A missing/failed lookup is a no-op on that advisory (it keeps whatever deps.dev gave it), never a raise.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/still_active/osv_client.rb', line 50 def enrich(advisories, ecosystem:, name:) advisories.each do |advisory| record = detail(advisory_id: advisory[:id]) next if record.nil? advisory[:osv_severity] = record[:severity_label] advisory[:osv_cvss_score] = record[:cvss_score] advisory[:cvss_version] = record[:cvss_version] advisory[:cvss_vector] = record[:cvss_vector] advisory[:fixed_versions] = fixed_versions(record, ecosystem: ecosystem, name: name) rescue StandardError => e # Enrichment is additive and best-effort. An unexpected OSV shape must never # raise out through the workflow's per-gem rescue, which would DROP the whole # gem and read a known-vulnerable dependency as clean. Leave the advisory # exactly as deps.dev produced it. $stderr.puts("warning: OSV enrichment for #{advisory[:id]} failed: #{e.class} (#{e.}); leaving advisory unchanged") end end |