Skip to content

Commit a4d2422

Browse files
authored
feat: get rubygems owners as security-contacts [CM-1241] (#4328)
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
1 parent 7735a8d commit a4d2422

2 files changed

Lines changed: 56 additions & 7 deletions

File tree

services/apps/packages_worker/src/rubygems/workflows.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { continueAsNew, log, proxyActivities } from '@temporalio/workflow'
33
import type * as activities from './activities'
44

55
const acts = proxyActivities<typeof activities>({
6-
startToCloseTimeout: '30 minutes',
6+
startToCloseTimeout: '60 minutes',
77
retry: { initialInterval: '30 seconds', backoffCoefficient: 2, maximumAttempts: 5 },
88
})
99

services/apps/packages_worker/src/security-contacts/extractors/registry/rubygems.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { ExtractorResult, ProvenanceEntry, RawContact } from '../../types'
2-
import { extractEmails, fetchJson, registryHeaders } from '../http'
2+
import { extractEmails, fetchJson, isEmail, registryHeaders } from '../http'
33

44
import { ParsedPurl } from './purl'
55

66
const SOURCE = 'rubygems'
77

88
/* eslint-disable @typescript-eslint/no-explicit-any */
99

10-
// RubyGems hides author emails; we can only surface emails when an author string embeds one.
1110
// bug_tracker_uri is an issue tracker, not a security contact, so it is intentionally skipped.
1211
export function mapRubygems(doc: unknown, sourceUrl: string, fetchedAt: string): RawContact[] {
1312
if (!doc || typeof doc !== 'object') return []
@@ -34,13 +33,63 @@ export function mapRubygems(doc: unknown, sourceUrl: string, fetchedAt: string):
3433
return contacts
3534
}
3635

36+
export function mapRubygemsOwners(
37+
owners: unknown,
38+
sourceUrl: string,
39+
fetchedAt: string,
40+
): RawContact[] {
41+
if (!Array.isArray(owners)) return []
42+
43+
const prov = (): ProvenanceEntry[] => [
44+
{ source: SOURCE, sourceTier: 'B', path: sourceUrl, fetchedAt },
45+
]
46+
const contacts: RawContact[] = []
47+
const seen = new Set<string>()
48+
for (const owner of owners) {
49+
const o = (owner ?? {}) as Record<string, unknown>
50+
const email = o.email
51+
if (typeof email !== 'string' || !isEmail(email)) continue
52+
const key = email.toLowerCase()
53+
if (seen.has(key)) continue
54+
seen.add(key)
55+
// RubyGems handles are not GitHub logins — stored as `name`, not `handle`, so
56+
// identityLinkMerge (reconcile.ts) never cross-links them to a github-handle contact.
57+
const name = typeof o.handle === 'string' ? o.handle : undefined
58+
contacts.push({
59+
channel: 'email',
60+
value: email,
61+
name,
62+
role: 'maintainer',
63+
tier: 'B',
64+
provenance: prov(),
65+
})
66+
}
67+
return contacts
68+
}
69+
3770
export async function fetchRubygems(
3871
parsed: ParsedPurl,
3972
timeoutMs: number,
4073
userAgent: string,
4174
): Promise<ExtractorResult> {
42-
const url = `https://rubygems.org/api/v1/gems/${encodeURIComponent(parsed.name)}.json`
43-
const { json } = await fetchJson(url, timeoutMs, registryHeaders(userAgent))
44-
if (!json) return { contacts: [], policies: {} }
45-
return { contacts: mapRubygems(json, url, new Date().toISOString()), policies: {} }
75+
const gemUrl = `https://rubygems.org/api/v1/gems/${encodeURIComponent(parsed.name)}.json`
76+
const ownersUrl = `https://rubygems.org/api/v1/gems/${encodeURIComponent(parsed.name)}/owners.json`
77+
const fetchedAt = new Date().toISOString()
78+
79+
const [gemResult, ownersResult] = await Promise.all([
80+
fetchJson(gemUrl, timeoutMs, registryHeaders(userAgent)),
81+
fetchJson(ownersUrl, timeoutMs, registryHeaders(userAgent)).catch(() => ({
82+
status: 0,
83+
json: null,
84+
})),
85+
])
86+
87+
if (!gemResult.json) return { contacts: [], policies: {} }
88+
89+
const contacts = [
90+
...mapRubygems(gemResult.json, gemUrl, fetchedAt),
91+
...mapRubygemsOwners(ownersResult.json, ownersUrl, fetchedAt),
92+
]
93+
94+
return { contacts, policies: {} }
4695
}

0 commit comments

Comments
 (0)