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 advisoryKeys schema. deps.dev is an explicitly ALPHA API (v3alpha), and every cross-ecosystem vulnerability count flows through the advisoryKeys field 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

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".

Returns:

  • (Boolean)


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