feat: add rubygems (CM-1358) - #4446
Conversation
05f0eac to
4b6b8bc
Compare
There was a problem hiding this comment.
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.
b0cd494 to
93efad8
Compare
There was a problem hiding this comment.
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
fetchcan indicate a timeout, DNS failure, TLS error, or connection reset—not that the gem does not exist. Converting every rejection toRubyGemsSourceNotFoundErrorrecords 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 throughversionsInRangesand stores duplicate values invulnerable_versions. Deduplicate the numbers before range filtering.
allVersions = versionsResult.map((v) => v.number)
There was a problem hiding this comment.
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::Requirementaccepts one-component and prerelease pessimistic bounds (for example,~> 1has ceiling2). This numeric-only, minimum-two-segment check classifies valid requirements as unparseable, so dependents that are actually outside the range bypass exclusion. MirrorGem::Version#bumpsemantics, 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
~> 1is a valid RubyGems requirement, not malformed: it matches versions from1up 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
fetchVersionsthrows for those cases; it only returns a typed error for 404/429. Catch registry exceptions and usegetVersionNumberswhendbPackageIdexists, 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
RubyGemsSourceNotFoundErrorreports a false no-source condition; reserve that error for HTTP 404 and preserve transport failures as download errors.
} catch {
throw new RubyGemsSourceNotFoundError(packageName, version)
}
PR SummaryMedium Risk Overview The RubyGems pipeline mirrors NuGet-style manifest ecosystems: dependents come from reverse Supporting changes: Reviewed by Cursor Bugbot for commit e2a8ec7. Bugbot is set up for automated code reviews on this repo. Configure here. |
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
93efad8 to
e2a8ec7
Compare
There was a problem hiding this comment.
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
numberloses precision aboveNumber.MAX_SAFE_INTEGER. For example, bumping9007199254740992produces 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 withBigInttoo.
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
fetchVersionsthrows on network and 5xx failures, so this helper can abortprepareSourcebefore the gem download is attempted. Treat those lookup failures like the existing structured errors and returnnull, 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
~> 1is 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:
fetchVersionsrethrows network, timeout, and 5xx errors, so execution never reaches thedbPackageIdbranch. Catch transient fetch failures here and usegetVersionNumberswhen 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)
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
rubygemsinSUPPORTED_ECOSYSTEMSand the backend'sSUPPORTED_BLAST_RADIUS_ECOSYSTEMS, and wires the new stage implementations intostages/ecosystems.ts'sECOSYSTEMSrecord.toBareGemNametopackageIdentifier.ts, stripping thepkg: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.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).clients/rubygemsSource.tsto download and extract the real published.gemsource 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.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 concreteto_version).agent/rubygemsPrompts.tswith Ruby-specific intel/reachability prompts (import styles:require,require_relative,autoload,gem_dependency; excludesspec/,test/,features/,vendor/).tar-built fixtures, not mocks), identifier helper, and dispatch routing; extendedecosystemVersions.test.tsfor 4-segment version ordering.Type of change
JIRA ticket
CM-1358