diff --git a/backend/src/api/public/v1/akrites-external/openapi.yaml b/backend/src/api/public/v1/akrites-external/openapi.yaml index 08327892fa..03a89875fd 100644 --- a/backend/src/api/public/v1/akrites-external/openapi.yaml +++ b/backend/src/api/public/v1/akrites-external/openapi.yaml @@ -686,6 +686,9 @@ components: - vulnerabilityReportingUrl - bugBountyUrl - pvrEnabled + - declaredRepositoryUrl + - resolvedRepositoryUrl + - repoMappingConfidence - targetOrganizationName - bugBountyProgramFlag - reportingMethods @@ -716,6 +719,28 @@ components: pvrEnabled: type: boolean nullable: true + declaredRepositoryUrl: + type: string + nullable: true + description: Raw repository URL as declared in the package's own metadata. + resolvedRepositoryUrl: + type: string + nullable: true + description: > + Best available repository URL, resolved via the confidence-ranked + package_repos → repos join — the repo CDP has actually resolved and + enriched (scorecard, branch protection, etc.). Null when the + package has no package_repos link yet. + repoMappingConfidence: + type: number + format: float + minimum: 0 + maximum: 1 + nullable: true + description: > + Confidence score of the package_repos mapping used to resolve + resolvedRepositoryUrl. Null when the package has no package_repos + link yet. targetOrganizationName: type: string nullable: true diff --git a/backend/src/api/public/v1/packages/akritesExternalContactDetail.test.ts b/backend/src/api/public/v1/packages/akritesExternalContactDetail.test.ts index 5ecb43d69e..286bf71cfe 100644 --- a/backend/src/api/public/v1/packages/akritesExternalContactDetail.test.ts +++ b/backend/src/api/public/v1/packages/akritesExternalContactDetail.test.ts @@ -15,6 +15,9 @@ function baseRow( vulnerabilityReportingUrl: null, bugBountyUrl: null, pvrEnabled: true, + declaredRepositoryUrl: 'https://github.com/lodash/lodash.git', + resolvedRepositoryUrl: 'https://github.com/lodash/lodash', + repoMappingConfidence: 0.9, securityContacts: [ { channel: 'email', @@ -107,4 +110,31 @@ describe('toAkritesExternalContactDetail', () => { expect(result.vulnerabilityReportingUrl).toBe('https://example.org/report') expect(result.pvrEnabled).toBe(false) }) + + it('passes through the repo provenance fields when present', () => { + const result = toAkritesExternalContactDetail(baseRow()) + expect(result.declaredRepositoryUrl).toBe('https://github.com/lodash/lodash.git') + expect(result.resolvedRepositoryUrl).toBe('https://github.com/lodash/lodash') + expect(result.repoMappingConfidence).toBe(0.9) + }) + + it('casts repoMappingConfidence from a numeric string (pg-promise numeric type)', () => { + const result = toAkritesExternalContactDetail( + baseRow({ repoMappingConfidence: '0.9' as unknown as number }), + ) + expect(result.repoMappingConfidence).toBe(0.9) + }) + + it('returns resolvedRepositoryUrl and repoMappingConfidence as null when there is no repo link', () => { + const result = toAkritesExternalContactDetail( + baseRow({ resolvedRepositoryUrl: null, repoMappingConfidence: null }), + ) + expect(result.resolvedRepositoryUrl).toBeNull() + expect(result.repoMappingConfidence).toBeNull() + }) + + it('returns declaredRepositoryUrl as null when the package has no declared repository', () => { + const result = toAkritesExternalContactDetail(baseRow({ declaredRepositoryUrl: null })) + expect(result.declaredRepositoryUrl).toBeNull() + }) }) diff --git a/backend/src/api/public/v1/packages/akritesExternalContactDetail.ts b/backend/src/api/public/v1/packages/akritesExternalContactDetail.ts index 7b344b330f..aadb0827c7 100644 --- a/backend/src/api/public/v1/packages/akritesExternalContactDetail.ts +++ b/backend/src/api/public/v1/packages/akritesExternalContactDetail.ts @@ -4,6 +4,8 @@ import { securityContactConfidenceBand, } from '@crowd/data-access-layer' +import { toNullableNumber } from './mappers' + // Both the per-contact band and the aggregate band mirror the internal // SecurityContactConfidence enum verbatim (PRIMARY/SECONDARY/FALLBACK/NONE) — // no external crosswalk (confirmed with product). @@ -28,6 +30,9 @@ export interface AkritesExternalContactDetail { vulnerabilityReportingUrl: string | null bugBountyUrl: string | null pvrEnabled: boolean | null + declaredRepositoryUrl: string | null + resolvedRepositoryUrl: string | null + repoMappingConfidence: number | null // Reserved by the contract but not stored today — always null (see the akrites-external // OpenAPI notes). Typed as null so a future non-null shape is an explicit, reviewed change. targetOrganizationName: null @@ -71,6 +76,9 @@ export function toAkritesExternalContactDetail( vulnerabilityReportingUrl: row.vulnerabilityReportingUrl ?? null, bugBountyUrl: row.bugBountyUrl ?? null, pvrEnabled: row.pvrEnabled ?? null, + declaredRepositoryUrl: row.declaredRepositoryUrl ?? null, + resolvedRepositoryUrl: row.resolvedRepositoryUrl ?? null, + repoMappingConfidence: toNullableNumber(row.repoMappingConfidence), targetOrganizationName: null, bugBountyProgramFlag: null, reportingMethods: null, diff --git a/backend/src/api/public/v1/packages/akritesExternalPackageDetail.ts b/backend/src/api/public/v1/packages/akritesExternalPackageDetail.ts index 1f8003b930..88b53940f4 100644 --- a/backend/src/api/public/v1/packages/akritesExternalPackageDetail.ts +++ b/backend/src/api/public/v1/packages/akritesExternalPackageDetail.ts @@ -4,7 +4,7 @@ import { computeHealthBand, } from '@crowd/data-access-layer' -import { repoMappingLabel, snakeToCamelKeys } from './mappers' +import { repoMappingLabel, snakeToCamelKeys, toNullableNumber } from './mappers' import { HEALTH_BAND_SET, LIFECYCLE_VALUES, type Lifecycle } from './types' const LIFECYCLE_SET = new Set(LIFECYCLE_VALUES) @@ -102,8 +102,7 @@ export function toAkritesExternalPackageDetail( row: AkritesExternalPackageDetailRow, ): AkritesExternalPackageDetail { const scorecardScore = row.scorecardScore != null ? Number(row.scorecardScore) : null - const mappingConfidence = - row.repoMappingConfidence != null ? Number(row.repoMappingConfidence) : null + const mappingConfidence = toNullableNumber(row.repoMappingConfidence) return { purl: row.purl, diff --git a/backend/src/api/public/v1/packages/getPackage.ts b/backend/src/api/public/v1/packages/getPackage.ts index 5125fedea9..ddc0323461 100644 --- a/backend/src/api/public/v1/packages/getPackage.ts +++ b/backend/src/api/public/v1/packages/getPackage.ts @@ -13,7 +13,7 @@ import { getPackagesQx } from '@/db/packagesDb' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' -import { repoMappingLabel, snakeToCamelKeys } from './mappers' +import { repoMappingLabel, snakeToCamelKeys, toNullableNumber } from './mappers' import { purlQuerySchema } from './purl' import { HEALTH_BAND_SET, LIFECYCLE_VALUES, type StewardshipStatus } from './types' @@ -35,8 +35,7 @@ export async function getPackage(req: Request, res: Response): Promise { ]) const scorecardScore = pkg.scorecardScore != null ? Number(pkg.scorecardScore) : null - const mappingConfidence = - pkg.repoMappingConfidence != null ? Number(pkg.repoMappingConfidence) : null + const mappingConfidence = toNullableNumber(pkg.repoMappingConfidence) const securityContacts = pkg.contactsLastRefreshed == null diff --git a/backend/src/api/public/v1/packages/mappers.ts b/backend/src/api/public/v1/packages/mappers.ts index f630f5691d..e8d0e38e3a 100644 --- a/backend/src/api/public/v1/packages/mappers.ts +++ b/backend/src/api/public/v1/packages/mappers.ts @@ -7,6 +7,11 @@ export function snakeToCamelKeys( ) } +// pg-promise returns numeric columns as strings; this coerces without turning null into 0. +export function toNullableNumber(value: number | string | null): number | null { + return value != null ? Number(value) : null +} + export function repoMappingLabel(confidence: number | null): 'High' | 'Medium' | 'Low' | null { if (confidence === null) return null if (confidence >= 0.8) return 'High' diff --git a/services/libs/data-access-layer/src/osspckgs/api.ts b/services/libs/data-access-layer/src/osspckgs/api.ts index 8b95019514..20261c432a 100644 --- a/services/libs/data-access-layer/src/osspckgs/api.ts +++ b/services/libs/data-access-layer/src/osspckgs/api.ts @@ -875,12 +875,16 @@ export async function getPackageDetailsByPurls( // live on the repo, not the package). A missing purl yields no row → "not found"; a found // package with no contacts yields a row with securityContacts null → resolves to []. export interface AkritesExternalContactDetailRow - extends Pick, + extends Pick, Pick< RepoDbRow, 'securityPolicyUrl' | 'vulnerabilityReportingUrl' | 'bugBountyUrl' | 'pvrEnabled' > { securityContacts: SecurityContactRow[] | null + // --- joined from repos via package_repos, same BEST_REPO_LINK_JOIN as + // AkritesExternalPackageDetailRow — not part of the packages or repos row. + resolvedRepositoryUrl: string | null + repoMappingConfidence: number | null } export async function getContactDetailsByPurls( @@ -894,6 +898,9 @@ export async function getContactDetailsByPurls( p.purl, p.name, p.ecosystem, + p.declared_repository_url AS "declaredRepositoryUrl", + r.url AS "resolvedRepositoryUrl", + pr.confidence AS "repoMappingConfidence", r.security_policy_url AS "securityPolicyUrl", r.vulnerability_reporting_url AS "vulnerabilityReportingUrl", r.bug_bounty_url AS "bugBountyUrl",