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
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/still_active/deps_dev_client.rb', line 154 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).
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/still_active/deps_dev_client.rb', line 70 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).
114 115 116 |
# File 'lib/still_active/deps_dev_client.rb', line 114 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.
98 99 100 101 102 103 104 105 106 107 108 |
# File 'lib/still_active/deps_dev_client.rb', line 98 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
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/still_active/deps_dev_client.rb', line 118 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.
144 145 146 147 148 149 150 151 152 |
# File 'lib/still_active/deps_dev_client.rb', line 144 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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# 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"], # The pinned version's SPDX licences, the same array shape RubyGems serves # (verified across npm, pypi, cargo, go and maven). Also already in this # response, so licence parity with the native path costs no extra request. # Always an array, so a caller can't mistake "no licence declared" for a # failed lookup. licenses: body["licenses"] || [], # The maintainer's own deprecation declaration, and the message they left # with it (often naming the replacement: left-pad's reads "use # String.prototype.padStart()"). This is a declared fact from the registry, # not a heuristic inferred from dates, and it rides in the same response as # the advisories and the release date, so it costs no extra request. # Populated for npm today; other ecosystems mostly lack a deprecation # mechanism, and any deps.dev later fills in lights up with no code change. deprecated: body["isDeprecated"] == true, deprecation_reason: presence(body["deprecatedReason"]) } end |