From 68a00e4f71af95540ca1823b0aa78fbc954c00b7 Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Mon, 13 Jul 2026 09:23:48 +0100 Subject: [PATCH 1/4] feat: get rubygems owners as security-contacts Signed-off-by: Mouad BANI --- .../extractors/registry/rubygems.ts | 62 +++++++++++++++++-- 1 file changed, 56 insertions(+), 6 deletions(-) diff --git a/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts b/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts index 63375d36e0..bf5dc4e6e3 100644 --- a/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts +++ b/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts @@ -1,5 +1,5 @@ import { ExtractorResult, ProvenanceEntry, RawContact } from '../../types' -import { extractEmails, fetchJson, registryHeaders } from '../http' +import { extractEmails, fetchJson, isEmail, registryHeaders } from '../http' import { ParsedPurl } from './purl' @@ -7,7 +7,6 @@ const SOURCE = 'rubygems' /* eslint-disable @typescript-eslint/no-explicit-any */ -// RubyGems hides author emails; we can only surface emails when an author string embeds one. // bug_tracker_uri is an issue tracker, not a security contact, so it is intentionally skipped. export function mapRubygems(doc: unknown, sourceUrl: string, fetchedAt: string): RawContact[] { if (!doc || typeof doc !== 'object') return [] @@ -34,13 +33,64 @@ export function mapRubygems(doc: unknown, sourceUrl: string, fetchedAt: string): return contacts } +export function mapRubygemsOwners( + owners: unknown, + sourceUrl: string, + fetchedAt: string, +): RawContact[] { + if (!Array.isArray(owners)) return [] + + const prov = (): ProvenanceEntry[] => [ + { source: SOURCE, sourceTier: 'B', path: sourceUrl, fetchedAt }, + ] + const contacts: RawContact[] = [] + const seen = new Set() + for (const owner of owners) { + const email = owner && typeof owner === 'object' ? (owner as any).email : undefined + if (typeof email !== 'string' || !isEmail(email)) continue + const key = email.toLowerCase() + if (seen.has(key)) continue + seen.add(key) + const handle = typeof (owner as any).handle === 'string' ? (owner as any).handle : undefined + contacts.push({ + channel: 'email', + value: email, + handle, + role: 'maintainer', + tier: 'B', + provenance: prov(), + }) + } + return contacts +} + export async function fetchRubygems( parsed: ParsedPurl, timeoutMs: number, userAgent: string, ): Promise { - const url = `https://rubygems.org/api/v1/gems/${encodeURIComponent(parsed.name)}.json` - const { json } = await fetchJson(url, timeoutMs, registryHeaders(userAgent)) - if (!json) return { contacts: [], policies: {} } - return { contacts: mapRubygems(json, url, new Date().toISOString()), policies: {} } + const gemUrl = `https://rubygems.org/api/v1/gems/${encodeURIComponent(parsed.name)}.json` + const ownersUrl = `https://rubygems.org/api/v1/gems/${encodeURIComponent(parsed.name)}/owners.json` + const fetchedAt = new Date().toISOString() + + const [gemResult, ownersResult] = await Promise.all([ + fetchJson(gemUrl, timeoutMs, registryHeaders(userAgent)), + fetchJson(ownersUrl, timeoutMs, registryHeaders(userAgent)).catch(() => ({ + status: 0, + json: null, + })), + ]) + + if (!gemResult.json) return { contacts: [], policies: {} } + + const contacts = mapRubygems(gemResult.json, gemUrl, fetchedAt) + const seen = new Set(contacts.map((c) => c.value.toLowerCase())) + for (const ownerContact of mapRubygemsOwners(ownersResult.json, ownersUrl, fetchedAt)) { + const key = ownerContact.value.toLowerCase() + if (seen.has(key)) continue + seen.add(key) + contacts.push(ownerContact) + } + + return { contacts, policies: {} } } From dee4ffc5792875fc53a67db085809520e7eaab9a Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Mon, 13 Jul 2026 09:27:38 +0100 Subject: [PATCH 2/4] chore: increase ingestion timeout Signed-off-by: Mouad BANI --- services/apps/packages_worker/src/rubygems/workflows.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/apps/packages_worker/src/rubygems/workflows.ts b/services/apps/packages_worker/src/rubygems/workflows.ts index 6983562983..a6015f2c2a 100644 --- a/services/apps/packages_worker/src/rubygems/workflows.ts +++ b/services/apps/packages_worker/src/rubygems/workflows.ts @@ -3,7 +3,7 @@ import { continueAsNew, log, proxyActivities } from '@temporalio/workflow' import type * as activities from './activities' const acts = proxyActivities({ - startToCloseTimeout: '30 minutes', + startToCloseTimeout: '60 minutes', retry: { initialInterval: '30 seconds', backoffCoefficient: 2, maximumAttempts: 5 }, }) From 74e94b009083ddfcb78fdcc2924f8d4afed6060a Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Mon, 13 Jul 2026 09:33:44 +0100 Subject: [PATCH 3/4] fix: code revies fixes Signed-off-by: Mouad BANI --- .../extractors/registry/rubygems.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts b/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts index bf5dc4e6e3..52677652c9 100644 --- a/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts +++ b/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts @@ -46,12 +46,13 @@ export function mapRubygemsOwners( const contacts: RawContact[] = [] const seen = new Set() for (const owner of owners) { - const email = owner && typeof owner === 'object' ? (owner as any).email : undefined + const o = (owner ?? {}) as Record + const email = o.email if (typeof email !== 'string' || !isEmail(email)) continue const key = email.toLowerCase() if (seen.has(key)) continue seen.add(key) - const handle = typeof (owner as any).handle === 'string' ? (owner as any).handle : undefined + const handle = typeof o.handle === 'string' ? o.handle : undefined contacts.push({ channel: 'email', value: email, @@ -83,14 +84,10 @@ export async function fetchRubygems( if (!gemResult.json) return { contacts: [], policies: {} } - const contacts = mapRubygems(gemResult.json, gemUrl, fetchedAt) - const seen = new Set(contacts.map((c) => c.value.toLowerCase())) - for (const ownerContact of mapRubygemsOwners(ownersResult.json, ownersUrl, fetchedAt)) { - const key = ownerContact.value.toLowerCase() - if (seen.has(key)) continue - seen.add(key) - contacts.push(ownerContact) - } + const contacts = [ + ...mapRubygems(gemResult.json, gemUrl, fetchedAt), + ...mapRubygemsOwners(ownersResult.json, ownersUrl, fetchedAt), + ] return { contacts, policies: {} } } From 507747f882e411f189526e36733d51d0c97ca1b9 Mon Sep 17 00:00:00 2001 From: Mouad BANI Date: Mon, 13 Jul 2026 09:50:49 +0100 Subject: [PATCH 4/4] fix: wrong name classification as github handle Signed-off-by: Mouad BANI --- .../src/security-contacts/extractors/registry/rubygems.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts b/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts index 52677652c9..f0fa7463fd 100644 --- a/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts +++ b/services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts @@ -52,11 +52,13 @@ export function mapRubygemsOwners( const key = email.toLowerCase() if (seen.has(key)) continue seen.add(key) - const handle = typeof o.handle === 'string' ? o.handle : undefined + // RubyGems handles are not GitHub logins — stored as `name`, not `handle`, so + // identityLinkMerge (reconcile.ts) never cross-links them to a github-handle contact. + const name = typeof o.handle === 'string' ? o.handle : undefined contacts.push({ channel: 'email', value: email, - handle, + name, role: 'maintainer', tier: 'B', provenance: prov(),