Module: StillActive::EcosystemLens
- Extended by:
- EcosystemLens
- Included in:
- EcosystemLens
- Defined in:
- lib/still_active/ecosystem_lens.rb
Overview
The cross-ecosystem maintenance lens. Given one SBOM-derived dependency (name, version), it assembles the same maintenance signals the native Ruby path produces -- latest release date, archived, advisories, scorecard -- into a gem_data hash StatusHelper.gem_status can read directly. The breadth counterpart to the depth Bundler path gives Ruby.
Security: the SBOM supplies only ecosystem/name/version. The repository is discovered from deps.dev (a public source still_active opts into), never from a lockfile-derived URL, so a hostile SBOM can't redirect a lookup. Anything unresolvable (a private package deps.dev doesn't index) degrades to nil signals -> :unknown, never a fabricated "ok".
Constant Summary collapse
- ECOSYSTEM_REGISTRIES =
still_active ecosystem symbol -> ecosyste.ms registry name for the declared dependency (poison-pill) lookup. Only the ecosystems where the ecosyste.ms registry name and the deps.dev package name align cleanly are wired; maven (group:artifact) and go (module paths) need name-format handling and are left out for now, so they simply carry no constraint signal rather than a wrong one.
{ rubygems: "rubygems.org", npm: "npmjs.org", pypi: "pypi.org", cargo: "crates.io", }.freeze
- FLAT_RESOLUTION_ECOSYSTEMS =
A transitive cap only holds the WHOLE tree hostage where the ecosystem forces one version per package. Ruby/Bundler and pip do (flat resolution); npm nests multiple versions and cargo coexists majors, so a below-latest cap there pins a duplicate copy in one subtree -- not a tree-wide block. The one npm case that DOES block (peerDependencies / de-facto singletons) isn't visible in ecosyste.ms data (its parser drops peerDependencies), so emitting poison for npm/cargo would over-claim on the non-blocking cases and miss the blocking ones. Suppress it there until peer requirements are sourced from deps.dev; silence beats a false "holds your tree hostage".
[:rubygems, :pypi].freeze
Instance Method Summary collapse
Instance Method Details
#assess(ecosystem:, name:, version:, constraint_cache: {}, python_range: nil) ⇒ Object
55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/still_active/ecosystem_lens.rb', line 55 def assess(ecosystem:, name:, version:, constraint_cache: {}, python_range: nil) info = DepsDevClient.version_info(gem_name: name, version: version, system: ecosystem) default = DepsDevClient.default_version_info(name: name, system: ecosystem) # Retry the version lookup once when it came back empty but the package DID # resolve: HttpHelper collapses a genuine 404 and a transient network blip to # the same nil, and a healthy version must not be mis-flagged as unresolved # (below) on a one-off miss. A real 404 stays nil; a blip recovers the data. info ||= DepsDevClient.version_info(gem_name: name, version: version, system: ecosystem) if default # Recover the repo from the default version when the exact locked version # isn't indexed (yanked/normalization mismatch): otherwise its project link # vanishes and a still-fresh package date would read a false :ok with # archived/scorecard silently dropped. The native Bundler path resolves the # repo independently of deps.dev's per-version record; this gives the lens # the same resilience. # The pinned version isn't indexed by deps.dev (info nil) while the package IS # (default present, so the feed is up): the version is yanked/nonexistent, not a # transient miss. Flag it so status reads :unknown rather than letting the still- # fresh PACKAGE date report a nonexistent version as :ok. version_unresolved = info.nil? && !default.nil? project_id = info&.dig(:project_id) || project_id_from(name, ecosystem, default) vulnerabilities = vulnerabilities_for(info) # Enrich with OSV: a real GHSA severity label (deps.dev can't score a CVSS-4-only # advisory) and the fixed-version ranges the "capped below the fix" signal needs. OsvClient.enrich(vulnerabilities, ecosystem: ecosystem, name: name) scorecard = DepsDevClient.project_scorecard(project_id: project_id) repo = repo_signals(project_id) used_release_date = info&.dig(:published_at) latest_release_date = default&.dig(:published_at) latest_version = default&.dig(:version) gem_data = { ecosystem: ecosystem, name: name, version_used: version, version_used_release_date: used_release_date, latest_version_release_date: latest_release_date, # libyear parity with the native path: how far behind latest the locked # version is, in release-years. Both dates come from deps.dev responses # already fetched above (no extra call); nil when either date is missing. libyear: LibyearHelper.gem_libyear( version_used_release_date: parse_time(used_release_date), latest_version_release_date: parse_time(latest_release_date), ), # Whether the pinned version is at or ahead of deps.dev's latest stable, so a # prerelease or an ahead-of-stable pin reads current (true), not "behind" -- # parity with the native path's `>=` comparison. nil when latest is unknown. up_to_date: version_current?(version, latest_version), repository_url: project_id && "https://#{project_id}", last_commit_date: repo[:last_commit_date], archived: repo[:archived], scorecard_score: scorecard&.dig(:score), scorecard_maintained: scorecard&.dig(:maintained), vulnerability_count: vulnerabilities.length, vulnerabilities: vulnerabilities, } gem_data[:version_unresolved] = true if version_unresolved attach_constraints(gem_data, ecosystem: ecosystem, name: name, version: version, cache: constraint_cache) attach_language_ceiling(gem_data, ecosystem: ecosystem, name: name, version: version, latest_version: default&.dig(:version), python_range: python_range) gem_data end |