Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ProvenanceEntry, RawContact } from '../../types'

// Registry usernames are only *guessed* to be GitHub logins — callers emit them as candidates
// that must pass verifyHandleCandidates corroboration before becoming contacts.
export function toHandleCandidates(
handles: unknown[],
source: string,
sourceUrl: string,
fetchedAt: string,
): RawContact[] {
const prov = (): ProvenanceEntry[] => [{ source, sourceTier: 'B', path: sourceUrl, fetchedAt }]
const candidates: RawContact[] = []
const seen = new Set<string>()
for (const handle of handles) {
if (typeof handle !== 'string' || handle.length === 0) continue
const key = handle.toLowerCase()
if (seen.has(key)) continue
seen.add(key)
candidates.push({
channel: 'github-handle',
value: handle,
role: 'maintainer',
tier: 'B',
provenance: prov(),
})
}
return candidates
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ import { XMLParser } from 'fast-xml-parser'
import { ExtractorResult, ProvenanceEntry, RawContact } from '../../types'
import { extractEmails, fetchJson, fetchText, registryHeaders } from '../http'

import { toHandleCandidates } from './handles'
import { ParsedPurl } from './purl'

const SOURCE = 'nuget-nuspec'
const SEARCH_SOURCE = 'nuget-search'
const BASE = 'https://api.nuget.org/v3-flatcontainer'
// Owner usernames are only exposed by the search service, not the flatcontainer API.
const SEARCH_BASE = 'https://azuresearch-usnc.nuget.org/query'
Comment thread
mbani01 marked this conversation as resolved.
Outdated
const parser = new XMLParser({ ignoreAttributes: true })

/* eslint-disable @typescript-eslint/no-explicit-any */
Expand Down Expand Up @@ -45,6 +49,22 @@ export function mapNuspec(xml: string, sourceUrl: string, fetchedAt: string): Ra
return contacts
}

// NuGet.org accounts are independent of GitHub, so an owner username matching a GitHub login is
// only a guess — emitted as candidates that must pass repo-contributor corroboration before use.
export function mapNugetOwnerHandles(
doc: unknown,
packageId: string,
sourceUrl: string,
fetchedAt: string,
): RawContact[] {
const data = (doc as any)?.data
if (!Array.isArray(data) || data.length === 0) return []
const entry = data[0] as Record<string, unknown>
if (typeof entry.id !== 'string' || entry.id.toLowerCase() !== packageId.toLowerCase()) return []
if (!Array.isArray(entry.owners)) return []
return toHandleCandidates(entry.owners, SEARCH_SOURCE, sourceUrl, fetchedAt)
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
mbani01 marked this conversation as resolved.
Outdated
}

async function latestStableVersion(
id: string,
timeoutMs: number,
Expand All @@ -67,11 +87,23 @@ export async function fetchNuget(
userAgent: string,
): Promise<ExtractorResult> {
const id = parsed.name.toLowerCase()
const version = await latestStableVersion(id, timeoutMs, userAgent)
if (!version) return { contacts: [], policies: {} }
const searchUrl = `${SEARCH_BASE}?q=packageid:${encodeURIComponent(id)}&prerelease=true&semVerLevel=2.0.0`
const fetchedAt = new Date().toISOString()

const [version, searchResult] = await Promise.all([
latestStableVersion(id, timeoutMs, userAgent),
fetchJson(searchUrl, timeoutMs, registryHeaders(userAgent)).catch(() => ({
status: 0,
json: null,
})),
])

const handleCandidates = mapNugetOwnerHandles(searchResult.json, id, searchUrl, fetchedAt)

if (!version) return { contacts: [], policies: {}, handleCandidates }

const url = `${BASE}/${id}/${version}/${id}.nuspec`
const { text } = await fetchText(url, timeoutMs, registryHeaders(userAgent))
if (!text) return { contacts: [], policies: {} }
return { contacts: mapNuspec(text, url, new Date().toISOString()), policies: {} }
if (!text) return { contacts: [], policies: {}, handleCandidates }
return { contacts: mapNuspec(text, url, fetchedAt), policies: {}, handleCandidates }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Candidates lost on fetch errors

Medium Severity

fetchNuget loads owner handleCandidates in parallel with the version index, but a thrown error from latestStableVersion (via Promise.all) or from the later nuspec fetchText rejects the whole extractor. extractManifest then skips the package, discarding handle candidates that were already fetched successfully.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bf5172c. Configure here.

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ExtractorResult, ProvenanceEntry, RawContact } from '../../types'
import { extractEmails, fetchJson, isEmail, registryHeaders } from '../http'

import { toHandleCandidates } from './handles'
import { ParsedPurl } from './purl'

const SOURCE = 'rubygems'
Expand Down Expand Up @@ -76,24 +77,11 @@ export function mapRubygemsOwnerHandles(
): RawContact[] {
if (!Array.isArray(owners)) return []

const candidates: RawContact[] = []
const seen = new Set<string>()
for (const owner of owners) {
const o = (owner ?? {}) as Record<string, unknown>
if (typeof o.email === 'string' && isEmail(o.email)) continue
if (typeof o.handle !== 'string' || o.handle.length === 0) continue
const key = o.handle.toLowerCase()
if (seen.has(key)) continue
seen.add(key)
candidates.push({
channel: 'github-handle',
value: o.handle,
role: 'maintainer',
tier: 'B',
provenance: [{ source: SOURCE, sourceTier: 'B', path: sourceUrl, fetchedAt }],
})
}
return candidates
const handles = owners
.map((owner) => (owner ?? {}) as Record<string, unknown>)
.filter((o) => !(typeof o.email === 'string' && isEmail(o.email)))
.map((o) => o.handle)
return toHandleCandidates(handles, SOURCE, sourceUrl, fetchedAt)
}

export async function fetchRubygems(
Expand Down
Loading