Skip to content

feat: add rubygems (CM-1358) - #4446

Merged
ulemons merged 2 commits into
mainfrom
feat/add-rubygems-ecosystem-to-blast
Aug 6, 2026
Merged

feat: add rubygems (CM-1358)#4446
ulemons merged 2 commits into
mainfrom
feat/add-rubygems-ecosystem-to-blast

Conversation

@ulemons

@ulemons ulemons commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds RubyGems as a supported blast-radius ecosystem, following the same intel → dependents → reachability → report pipeline already in place for npm, go, maven, cargo, and nuget.

Changes

  • Registers rubygems in SUPPORTED_ECOSYSTEMS and the backend's SUPPORTED_BLAST_RADIUS_ECOSYSTEMS, and wires the new stage implementations into stages/ecosystems.ts's ECOSYSTEMS record.
  • Adds toBareGemName to packageIdentifier.ts, stripping the pkg:gem/ purl prefix without lowercasing — gem names aren't universally lowercase (RedCloth, Ascii85), and both deps.dev and rubygems.org store the canonical published spelling.
  • Implements rubygemsConstraintMayInclude (stages/rubygems/rubygemsConstraint.ts), a native comparator-based parser for the Gem::Requirement grammar (comma-separated <op> <version> clauses ANDed together), since real RubyGems versions are 4-segment and node-semver can't parse them. Correctly expands the pessimistic operator per RubyGems' documented semantics: ~> 1.2>= 1.2, < 2.0, ~> 1.2.3>= 1.2.3, < 1.3.0 (drop the last segment, bump the new last segment).
  • Adds clients/rubygemsSource.ts to download and extract the real published .gem source directly from rubygems.org, instead of guessing at a GitHub tarball as NuGet has to — gems ship uncompressed source, so this avoids the monorepo/heartbeat problems hit on the NuGet source-download path.
  • Adds the RubyGems stage trio (intelRubyGems.ts, dependentsRubyGems.ts/dependentsScanRubyGems.ts, reachabilityConfig.ts), modeled directly on the NuGet implementation since both are manifest-style ecosystems (deps.dev never resolves a concrete to_version).
  • Adds agent/rubygemsPrompts.ts with Ruby-specific intel/reachability prompts (import styles: require, require_relative, autoload, gem_dependency; excludes spec/, test/, features/, vendor/).
  • Full unit test coverage for the new constraint parser, source extraction (using real tar-built fixtures, not mocks), identifier helper, and dispatch routing; extended ecosystemVersions.test.ts for 4-segment version ordering.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Performance improvement
  • Chore / dependency update
  • Documentation

JIRA ticket

CM-1358

@ulemons ulemons self-assigned this Aug 6, 2026
Copilot AI balanced review requested due to automatic review settings August 6, 2026 09:34
@ulemons ulemons added the Feature Created by Linear-GitHub Sync label Aug 6, 2026
@ulemons
ulemons force-pushed the feat/add-rubygems-ecosystem-to-blast branch from 05f0eac to 4b6b8bc Compare August 6, 2026 09:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds RubyGems support to the blast-radius analysis pipeline.

Changes:

  • Registers RubyGems across API and worker dispatch.
  • Adds constraint evaluation, source extraction, and Ruby-specific analysis stages.
  • Adds focused tests for routing, versions, identifiers, constraints, and extraction.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
backend/src/api/public/v1/packages/blastRadius.ts Allows RubyGems API requests.
services/apps/packages_worker/src/blast-radius/ecosystemSupport.ts Registers the ecosystem.
services/apps/packages_worker/src/blast-radius/packageIdentifier.ts Normalizes gem identifiers.
services/apps/packages_worker/src/blast-radius/agent/rubygemsPrompts.ts Defines Ruby analysis prompts.
services/apps/packages_worker/src/blast-radius/clients/rubygemsSource.ts Downloads and extracts gems.
services/apps/packages_worker/src/blast-radius/stages/ecosystems.ts Wires stage dispatch.
services/apps/packages_worker/src/blast-radius/stages/rubygems/intelRubyGems.ts Implements vulnerability intelligence.
services/apps/packages_worker/src/blast-radius/stages/rubygems/dependentsRubyGems.ts Persists dependent candidates.
services/apps/packages_worker/src/blast-radius/stages/rubygems/dependentsScanRubyGems.ts Discovers and filters dependents.
services/apps/packages_worker/src/blast-radius/stages/rubygems/reachabilityConfig.ts Configures source analysis.
services/apps/packages_worker/src/blast-radius/stages/rubygems/rubygemsConstraint.ts Evaluates gem requirements.
services/apps/packages_worker/src/blast-radius/__tests__/ecosystemSupport.test.ts Tests ecosystem registration.
services/apps/packages_worker/src/blast-radius/__tests__/packageIdentifier.test.ts Tests gem normalization.
services/apps/packages_worker/src/blast-radius/clients/__tests__/rubygemsSource.test.ts Tests gem extraction.
services/apps/packages_worker/src/blast-radius/stages/__tests__/dispatch.test.ts Tests RubyGems routing.
services/apps/packages_worker/src/blast-radius/stages/__tests__/ecosystemVersions.test.ts Tests RubyGems ordering.
services/apps/packages_worker/src/blast-radius/stages/rubygems/__tests__/rubygemsConstraint.test.ts Tests requirement evaluation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread services/apps/packages_worker/src/blast-radius/clients/rubygemsSource.ts Outdated
Comment thread services/apps/packages_worker/src/blast-radius/clients/rubygemsSource.ts Outdated
Copilot AI review requested due to automatic review settings August 6, 2026 10:23
@ulemons
ulemons force-pushed the feat/add-rubygems-ecosystem-to-blast branch from b0cd494 to 93efad8 Compare August 6, 2026 10:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (2)

services/apps/packages_worker/src/blast-radius/clients/rubygemsSource.ts:54

  • A rejected fetch can indicate a timeout, DNS failure, TLS error, or connection reset—not that the gem does not exist. Converting every rejection to RubyGemsSourceNotFoundError records the misleading “No downloadable gem” reason and hides transient registry failures. Only the explicit 404 below should map to not-found; let transport errors propagate.
    } catch {
      throw new RubyGemsSourceNotFoundError(packageName, version)

services/apps/packages_worker/src/blast-radius/stages/rubygems/intelRubyGems.ts:91

  • The versions endpoint returns one entry per version/platform, so a version published for ruby, java, and native platforms appears multiple times. Mapping directly here preserves those duplicates through versionsInRanges and stores duplicate values in vulnerable_versions. Deduplicate the numbers before range filtering.
      allVersions = versionsResult.map((v) => v.number)

Copilot AI review requested due to automatic review settings August 6, 2026 10:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (4)

services/apps/packages_worker/src/blast-radius/stages/rubygems/rubygemsConstraint.ts:40

  • Gem::Requirement accepts one-component and prerelease pessimistic bounds (for example, ~> 1 has ceiling 2). This numeric-only, minimum-two-segment check classifies valid requirements as unparseable, so dependents that are actually outside the range bypass exclusion. Mirror Gem::Version#bump semantics, including arbitrary-precision increments.
  const segments = version.split('.')
  if (segments.length < 2 || segments.some((s) => !/^\d+$/.test(s))) return null

services/apps/packages_worker/src/blast-radius/stages/rubygems/tests/rubygemsConstraint.test.ts:66

  • ~> 1 is a valid RubyGems requirement, not malformed: it matches versions from 1 up to (but excluding) 2. This assertion locks in the parser bug instead of testing the valid one-component form.
  it('conservatively includes a malformed "~>" version', () => {
    expect(rubygemsConstraintMayInclude('~> abc', ['1.5.0'])).toBe('unparseable-included')
    expect(rubygemsConstraintMayInclude('~> 1', ['1.5.0'])).toBe('unparseable-included')

services/apps/packages_worker/src/blast-radius/stages/rubygems/intelRubyGems.ts:88

  • The stated DB fallback does not run for registry timeouts, network failures, or 5xx responses because fetchVersions throws for those cases; it only returns a typed error for 404/429. Catch registry exceptions and use getVersionNumbers when dbPackageId exists, while preserving the original failure when no fallback is available.
    // rubygems.org is the authoritative version list; fall back to our own ingested
    // `versions` rows if the registry is unreachable/rate-limited and we know the package.
    const versionsResult = await fetchVersions(gemName)

services/apps/packages_worker/src/blast-radius/clients/rubygemsSource.ts:54

  • A timeout, DNS failure, or connection reset does not mean the artifact is absent. Converting every rejected fetch into RubyGemsSourceNotFoundError reports a false no-source condition; reserve that error for HTTP 404 and preserve transport failures as download errors.
    } catch {
      throw new RubyGemsSourceNotFoundError(packageName, version)
    }

@ulemons
ulemons marked this pull request as ready for review August 6, 2026 12:28
@cursor

cursor Bot commented Aug 6, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
New ecosystem path touches vulnerability analysis, external gem downloads, and LLM reachability verdicts; constraint parsing is intentionally over-inclusive but bounded by existing pipeline patterns and tests.

Overview
Adds RubyGems as a sixth blast-radius ecosystem end-to-end: the public API and worker both accept rubygems, and intel → dependents → reachability dispatch through new RubyGems stage implementations registered in ECOSYSTEMS.

The RubyGems pipeline mirrors NuGet-style manifest ecosystems: dependents come from reverse package_dependencies edges and are filtered with a new rubygemsConstraintMayInclude parser for Gem::Requirement (including ~> pessimistic expansion and four-segment versions), then reachability runs against agent prompts tailored to Ruby (require, Gemfile/.gemspec deps, excluding spec//test/). Intel and reachability download real .gem artifacts from rubygems.org (nested data.tar.gz extraction, platform-aware URLs, size limits) instead of guessing GitHub tarballs.

Supporting changes: toBareGemName for pkg:gem/ purls without lowercasing; pickPlatform / resolveGemPlatform when choosing universal vs native gem builds; versionConstraint typed as nullable on reverse-dependent rows; and decodeURIComponent wrapped in try/catch for Go/Cargo/NuGet/gem identifier normalization.

Reviewed by Cursor Bugbot for commit e2a8ec7. Bugbot is set up for automated code reviews on this repo. Configure here.

ulemons added 2 commits August 6, 2026 14:28
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Copilot AI review requested due to automatic review settings August 6, 2026 12:28
@ulemons
ulemons force-pushed the feat/add-rubygems-ecosystem-to-blast branch from 93efad8 to e2a8ec7 Compare August 6, 2026 12:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (5)

services/apps/packages_worker/src/blast-radius/stages/rubygems/rubygemsConstraint.ts:40

  • Converting the segment to number loses precision above Number.MAX_SAFE_INTEGER. For example, bumping 9007199254740992 produces the same value, making the upper bound equal to the floor and incorrectly excluding every matching vulnerable version. The comparator already supports arbitrary-size RubyGems segments, so bump with BigInt too.
  const last = Number(bumped[bumped.length - 1])
  bumped[bumped.length - 1] = String(last + 1)

services/apps/packages_worker/src/blast-radius/stages/rubygems/rubygemsPlatform.ts:17

  • fetchVersions throws on network and 5xx failures, so this helper can abort prepareSource before the gem download is attempted. Treat those lookup failures like the existing structured errors and return null, allowing the default Ruby artifact URL to be tried.
export async function resolveGemPlatform(name: string, version: string): Promise<string | null> {
  const versionsResult = await fetchVersions(name)
  if (isRubyGemsFetchError(versionsResult)) return null
  return pickPlatform(versionsResult, version)

services/apps/packages_worker/src/blast-radius/stages/rubygems/rubygemsConstraint.ts:36

  • ~> 1 is a valid Gem::Requirement and should expand to >= 1, < 2; rejecting every one-segment version marks it unparseable and can let out-of-range dependents consume the top-N scan slots. Please preserve the sole segment before bumping it and update the test that currently labels this valid requirement malformed.

This issue also appears on line 39 of the same file.

  if (segments.length < 2 || segments.some((s) => !/^\d+$/.test(s))) return null

services/apps/packages_worker/src/blast-radius/stages/rubygems/intelRubyGems.ts:88

  • The documented DB fallback is bypassed for the main “registry unreachable” case: fetchVersions rethrows network, timeout, and 5xx errors, so execution never reaches the dbPackageId branch. Catch transient fetch failures here and use getVersionNumbers when a DB package is available; otherwise RubyGems intel fails despite having local version data.
    const versionsResult = await fetchVersions(gemName)

services/apps/packages_worker/src/blast-radius/clients/rubygemsSource.ts:54

  • Every rejected fetch—including DNS failures and the two-minute timeout—is converted into “No downloadable gem,” discarding the real transient error and producing a misleading verdict. Only the explicit 404 below proves absence; let fetch failures retain their original cause.
    try {
      res = await fetch(url, { signal: controller.signal })
    } catch {
      throw new RubyGemsSourceNotFoundError(packageName, version)

@ulemons
ulemons merged commit 6ad8440 into main Aug 6, 2026
15 checks passed
@ulemons
ulemons deleted the feat/add-rubygems-ecosystem-to-blast branch August 6, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Feature Created by Linear-GitHub Sync

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants