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



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/still_active/deps_dev_client.rb', line 99

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)


22
23
24
25
# File 'lib/still_active/deps_dev_client.rb', line 22

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



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/still_active/deps_dev_client.rb', line 54

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?

  entry = 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).



74
75
76
# File 'lib/still_active/deps_dev_client.rb', line 74

def latest_release_date(name:, system: :rubygems)
  default_version_info(name: name, system: system)&.dig(:published_at)
end

#project_scorecard(project_id:) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/still_active/deps_dev_client.rb', line 78

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

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



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/still_active/deps_dev_client.rb', line 31

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