-
Notifications
You must be signed in to change notification settings - Fork 731
feat: resolve nuget owner handles via github contributor verification [CM-1341] #4364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
|---|---|---|
|
|
@@ -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' | ||
| const parser = new XMLParser({ ignoreAttributes: true }) | ||
|
|
||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
|
|
@@ -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) | ||
|
cursor[bot] marked this conversation as resolved.
mbani01 marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| async function latestStableVersion( | ||
| id: string, | ||
| timeoutMs: number, | ||
|
|
@@ -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 } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Candidates lost on fetch errorsMedium Severity
Reviewed by Cursor Bugbot for commit bf5172c. Configure here. |
||
| } | ||


Uh oh!
There was an error while loading. Please reload this page.