Module: StillActive::DepsDevClient
- Extended by:
- DepsDevClient
- Included in:
- DepsDevClient
- Defined in:
- lib/still_active/deps_dev_client.rb
Constant Summary collapse
- BASE_URI =
URI("https://api.deps.dev/")
- ADVISORY_CANARY =
A known-vulnerable package used to canary the
advisoryKeysschema. deps.dev is an explicitly ALPHA API (v3alpha), and every cross-ecosystem vulnerability count flows through theadvisoryKeysfield with a field-level degrade to[]-- so a rename or drop of that field would silently turn every count to 0 and read a known-vulnerable package as clean (exit 0). django 3.0.0 carries 30+ permanent advisories; if the canary returns none, the schema drifted. { system: "pypi", name: "django", version: "3.0.0" }.freeze
Instance Method Summary collapse
- #advisory_detail(advisory_id:) ⇒ Object
-
#advisory_schema_ok? ⇒ Boolean
Is deps.dev still returning advisories in the shape we parse? False when the canary comes back empty (schema drift) or unreachable (can't confirm).
-
#default_version_info(name:, system: :rubygems) ⇒ Object
The package's default version and its release date: { version:, published_at: }, or nil.
-
#latest_release_date(name:, system: :rubygems) ⇒ Object
The package's most recent release date (ISO8601 string), or nil.
-
#latest_stable_version(versions) ⇒ Object
The newest non-prerelease version by version number (not publishedAt: a backported patch on an old line can post-date the latest major).
- #project_scorecard(project_id:) ⇒ Object
-
#target_frameworks(name:, version:) ⇒ Object
The NuGet target framework monikers a version declares (net6.0, net45, netstandard2.0, ...), or [] when unknown.
-
#version_info(gem_name:, version:, system: :rubygems) ⇒ Object
systemis the deps.dev package system, lowercased: rubygems, npm, pypi, cargo, go, maven, nuget.
Instance Method Details
#advisory_detail(advisory_id:) ⇒ Object
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/still_active/deps_dev_client.rb', line 139 def advisory_detail(advisory_id:) return if advisory_id.nil? path = "/v3alpha/advisories/#{encode(advisory_id)}" body = HttpHelper.get_json(BASE_URI, path) return if body.nil? { id: body.dig("advisoryKey", "id"), url: body["url"], title: body["title"], # deps.dev's v3alpha returns aliases as bare id strings (["CVE-..."]); # tolerate the legacy object shape ({"id":...}) too since it's an alpha API. aliases: Array(body["aliases"]).filter_map { |a| normalize_alias(a) }, cvss3_score: body["cvss3Score"], cvss3_vector: body["cvss3Vector"], cvss2_score: body["cvss2Score"], source: "deps.dev", } end |
#advisory_schema_ok? ⇒ Boolean
Is deps.dev still returning advisories in the shape we parse? False when the canary comes back empty (schema drift) or unreachable (can't confirm). The caller warns loudly rather than presenting a possibly-understated "all clear".
23 24 25 26 |
# File 'lib/still_active/deps_dev_client.rb', line 23 def advisory_schema_ok? info = version_info(gem_name: ADVISORY_CANARY[:name], version: ADVISORY_CANARY[:version], system: ADVISORY_CANARY[:system]) !(info.nil? || info[:advisory_keys].empty?) end |
#default_version_info(name:, system: :rubygems) ⇒ Object
The package's default version and its release date: { version:, published_at: }, or nil. deps.dev's default version is the latest stable; when none is flagged (all pre-release), the newest publishedAt stands in so a still-active package isn't mis-read as dormant. The version is exposed (not just the date) so a caller can recover the package's repo link from it when an exact locked version isn't indexed (yanked/normalization mismatch).
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'lib/still_active/deps_dev_client.rb', line 55 def default_version_info(name:, system: :rubygems) return if name.nil? path = "/v3alpha/systems/#{encode(system)}/packages/#{encode(name)}" body = HttpHelper.get_json(BASE_URI, path) return if body.nil? versions = body["versions"] return unless versions.is_a?(Array) && !versions.empty? # deps.dev's `isDefault` is NOT reliably the latest release: cargo/wasi flags # 0.7.0 (2019) while 0.14.7 (2025) ships, and pypi/httpx flags a 1.0.0.dev3 # prerelease over the 0.28.1 stable. Trusting it reads an active package as # years-stale (a false SA002 "abandoned") and paints a downgrade as an # upgrade. So rank by version and take the newest STABLE release; fall back to # isDefault, then newest-by-date, only when no stable version parses (a # genuinely prerelease-only package still reads active, not dormant). entry = latest_stable_version(versions) || versions.find { |v| v.is_a?(Hash) && v["isDefault"] } || newest_version(versions) return if entry.nil? { version: entry.dig("versionKey", "version"), published_at: entry["publishedAt"] } end |
#latest_release_date(name:, system: :rubygems) ⇒ Object
The package's most recent release date (ISO8601 string), or nil. This is the cross-ecosystem freshness signal, and deps.dev's publishedAt is more reliable than ecosyste.ms's latest_release_published_at, which can lag badly (mpmath: 2023 vs 2026).
99 100 101 |
# File 'lib/still_active/deps_dev_client.rb', line 99 def latest_release_date(name:, system: :rubygems) default_version_info(name: name, system: system)&.dig(:published_at) end |
#latest_stable_version(versions) ⇒ Object
The newest non-prerelease version by version number (not publishedAt: a backported patch on an old line can post-date the latest major). nil when no version parses as a stable release.
83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/still_active/deps_dev_client.rb', line 83 def latest_stable_version(versions) versions .filter_map do |v| next unless v.is_a?(Hash) && !v["isDeprecated"] gem_version = VersionHelper.comparable(v.dig("versionKey", "version")) [gem_version, v] if gem_version && !gem_version.prerelease? end .max_by(&:first) &.last end |
#project_scorecard(project_id:) ⇒ Object
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/still_active/deps_dev_client.rb', line 103 def project_scorecard(project_id:) return if project_id.nil? path = "/v3alpha/projects/#{encode(project_id)}" body = HttpHelper.get_json(BASE_URI, path) return if body.nil? scorecard = body["scorecard"] return if scorecard.nil? { score: scorecard["overallScore"], date: scorecard["date"], # The "Maintained" sub-check (0-10) scores recent commit and issue # activity directly -- still_active's core question -- so we surface it # alongside the aggregate. nil when the check is absent (never 0, which # would read as "unmaintained" rather than "not measured"). maintained: maintained_check_score(scorecard), } end |
#target_frameworks(name:, version:) ⇒ Object
The NuGet target framework monikers a version declares (net6.0, net45,
netstandard2.0, ...), or [] when unknown. On the NuGet-specific
:requirements endpoint, not the version endpoint. The language-runtime
ceiling (SA009) reads these to detect a package that targets only EOL .NET
runtimes. Degrades to [] on 404/failure, never raising.
129 130 131 132 133 134 135 136 137 |
# File 'lib/still_active/deps_dev_client.rb', line 129 def target_frameworks(name:, version:) return [] if name.nil? || version.nil? path = "/v3alpha/systems/nuget/packages/#{encode(name)}/versions/#{encode(version)}:requirements" body = HttpHelper.get_json(BASE_URI, path) return [] if body.nil? Array(body.dig("nuget", "targetFrameworks")).grep(String) end |
#version_info(gem_name:, version:, system: :rubygems) ⇒ Object
system is the deps.dev package system, lowercased: rubygems, npm, pypi,
cargo, go, maven, nuget. It matches the ecosystem symbol SbomReader emits,
so a cross-ecosystem caller threads it straight through. An unknown system
404s and degrades to nil (HttpHelper swallows 404), never raising.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/still_active/deps_dev_client.rb', line 32 def version_info(gem_name:, version:, system: :rubygems) return if gem_name.nil? || version.nil? path = "/v3alpha/systems/#{encode(system)}/packages/#{encode(gem_name)}/versions/#{encode(version)}" body = HttpHelper.get_json(BASE_URI, path) return if body.nil? { advisory_keys: body.dig("advisoryKeys")&.map { |a| a["id"] } || [], project_id: extract_project_id(body), # The locked version's release date -- the cross-ecosystem libyear input # (paired with the package's latest-release date). Already in this response, # so no extra fetch; nil when the feed omits it. published_at: body["publishedAt"], } end |