Skip to content

Commit 04214f7

Browse files
authored
feat: add provenance fields (CM-1347) (#4392)
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 370e3f0 commit 04214f7

7 files changed

Lines changed: 80 additions & 7 deletions

File tree

backend/src/api/public/v1/akrites-external/openapi.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -686,6 +686,9 @@ components:
686686
- vulnerabilityReportingUrl
687687
- bugBountyUrl
688688
- pvrEnabled
689+
- declaredRepositoryUrl
690+
- resolvedRepositoryUrl
691+
- repoMappingConfidence
689692
- targetOrganizationName
690693
- bugBountyProgramFlag
691694
- reportingMethods
@@ -716,6 +719,28 @@ components:
716719
pvrEnabled:
717720
type: boolean
718721
nullable: true
722+
declaredRepositoryUrl:
723+
type: string
724+
nullable: true
725+
description: Raw repository URL as declared in the package's own metadata.
726+
resolvedRepositoryUrl:
727+
type: string
728+
nullable: true
729+
description: >
730+
Best available repository URL, resolved via the confidence-ranked
731+
package_repos → repos join — the repo CDP has actually resolved and
732+
enriched (scorecard, branch protection, etc.). Null when the
733+
package has no package_repos link yet.
734+
repoMappingConfidence:
735+
type: number
736+
format: float
737+
minimum: 0
738+
maximum: 1
739+
nullable: true
740+
description: >
741+
Confidence score of the package_repos mapping used to resolve
742+
resolvedRepositoryUrl. Null when the package has no package_repos
743+
link yet.
719744
targetOrganizationName:
720745
type: string
721746
nullable: true

backend/src/api/public/v1/packages/akritesExternalContactDetail.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ function baseRow(
1515
vulnerabilityReportingUrl: null,
1616
bugBountyUrl: null,
1717
pvrEnabled: true,
18+
declaredRepositoryUrl: 'https://github.com/lodash/lodash.git',
19+
resolvedRepositoryUrl: 'https://github.com/lodash/lodash',
20+
repoMappingConfidence: 0.9,
1821
securityContacts: [
1922
{
2023
channel: 'email',
@@ -107,4 +110,31 @@ describe('toAkritesExternalContactDetail', () => {
107110
expect(result.vulnerabilityReportingUrl).toBe('https://example.org/report')
108111
expect(result.pvrEnabled).toBe(false)
109112
})
113+
114+
it('passes through the repo provenance fields when present', () => {
115+
const result = toAkritesExternalContactDetail(baseRow())
116+
expect(result.declaredRepositoryUrl).toBe('https://github.com/lodash/lodash.git')
117+
expect(result.resolvedRepositoryUrl).toBe('https://github.com/lodash/lodash')
118+
expect(result.repoMappingConfidence).toBe(0.9)
119+
})
120+
121+
it('casts repoMappingConfidence from a numeric string (pg-promise numeric type)', () => {
122+
const result = toAkritesExternalContactDetail(
123+
baseRow({ repoMappingConfidence: '0.9' as unknown as number }),
124+
)
125+
expect(result.repoMappingConfidence).toBe(0.9)
126+
})
127+
128+
it('returns resolvedRepositoryUrl and repoMappingConfidence as null when there is no repo link', () => {
129+
const result = toAkritesExternalContactDetail(
130+
baseRow({ resolvedRepositoryUrl: null, repoMappingConfidence: null }),
131+
)
132+
expect(result.resolvedRepositoryUrl).toBeNull()
133+
expect(result.repoMappingConfidence).toBeNull()
134+
})
135+
136+
it('returns declaredRepositoryUrl as null when the package has no declared repository', () => {
137+
const result = toAkritesExternalContactDetail(baseRow({ declaredRepositoryUrl: null }))
138+
expect(result.declaredRepositoryUrl).toBeNull()
139+
})
110140
})

backend/src/api/public/v1/packages/akritesExternalContactDetail.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
securityContactConfidenceBand,
55
} from '@crowd/data-access-layer'
66

7+
import { toNullableNumber } from './mappers'
8+
79
// Both the per-contact band and the aggregate band mirror the internal
810
// SecurityContactConfidence enum verbatim (PRIMARY/SECONDARY/FALLBACK/NONE) —
911
// no external crosswalk (confirmed with product).
@@ -28,6 +30,9 @@ export interface AkritesExternalContactDetail {
2830
vulnerabilityReportingUrl: string | null
2931
bugBountyUrl: string | null
3032
pvrEnabled: boolean | null
33+
declaredRepositoryUrl: string | null
34+
resolvedRepositoryUrl: string | null
35+
repoMappingConfidence: number | null
3136
// Reserved by the contract but not stored today — always null (see the akrites-external
3237
// OpenAPI notes). Typed as null so a future non-null shape is an explicit, reviewed change.
3338
targetOrganizationName: null
@@ -71,6 +76,9 @@ export function toAkritesExternalContactDetail(
7176
vulnerabilityReportingUrl: row.vulnerabilityReportingUrl ?? null,
7277
bugBountyUrl: row.bugBountyUrl ?? null,
7378
pvrEnabled: row.pvrEnabled ?? null,
79+
declaredRepositoryUrl: row.declaredRepositoryUrl ?? null,
80+
resolvedRepositoryUrl: row.resolvedRepositoryUrl ?? null,
81+
repoMappingConfidence: toNullableNumber(row.repoMappingConfidence),
7482
targetOrganizationName: null,
7583
bugBountyProgramFlag: null,
7684
reportingMethods: null,

backend/src/api/public/v1/packages/akritesExternalPackageDetail.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
computeHealthBand,
55
} from '@crowd/data-access-layer'
66

7-
import { repoMappingLabel, snakeToCamelKeys } from './mappers'
7+
import { repoMappingLabel, snakeToCamelKeys, toNullableNumber } from './mappers'
88
import { HEALTH_BAND_SET, LIFECYCLE_VALUES, type Lifecycle } from './types'
99

1010
const LIFECYCLE_SET = new Set<string>(LIFECYCLE_VALUES)
@@ -102,8 +102,7 @@ export function toAkritesExternalPackageDetail(
102102
row: AkritesExternalPackageDetailRow,
103103
): AkritesExternalPackageDetail {
104104
const scorecardScore = row.scorecardScore != null ? Number(row.scorecardScore) : null
105-
const mappingConfidence =
106-
row.repoMappingConfidence != null ? Number(row.repoMappingConfidence) : null
105+
const mappingConfidence = toNullableNumber(row.repoMappingConfidence)
107106

108107
return {
109108
purl: row.purl,

backend/src/api/public/v1/packages/getPackage.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { getPackagesQx } from '@/db/packagesDb'
1313
import { ok } from '@/utils/api'
1414
import { validateOrThrow } from '@/utils/validation'
1515

16-
import { repoMappingLabel, snakeToCamelKeys } from './mappers'
16+
import { repoMappingLabel, snakeToCamelKeys, toNullableNumber } from './mappers'
1717
import { purlQuerySchema } from './purl'
1818
import { HEALTH_BAND_SET, LIFECYCLE_VALUES, type StewardshipStatus } from './types'
1919

@@ -35,8 +35,7 @@ export async function getPackage(req: Request, res: Response): Promise<void> {
3535
])
3636

3737
const scorecardScore = pkg.scorecardScore != null ? Number(pkg.scorecardScore) : null
38-
const mappingConfidence =
39-
pkg.repoMappingConfidence != null ? Number(pkg.repoMappingConfidence) : null
38+
const mappingConfidence = toNullableNumber(pkg.repoMappingConfidence)
4039

4140
const securityContacts =
4241
pkg.contactsLastRefreshed == null

backend/src/api/public/v1/packages/mappers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ export function snakeToCamelKeys(
77
)
88
}
99

10+
// pg-promise returns numeric columns as strings; this coerces without turning null into 0.
11+
export function toNullableNumber(value: number | string | null): number | null {
12+
return value != null ? Number(value) : null
13+
}
14+
1015
export function repoMappingLabel(confidence: number | null): 'High' | 'Medium' | 'Low' | null {
1116
if (confidence === null) return null
1217
if (confidence >= 0.8) return 'High'

services/libs/data-access-layer/src/osspckgs/api.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,12 +875,16 @@ export async function getPackageDetailsByPurls(
875875
// live on the repo, not the package). A missing purl yields no row → "not found"; a found
876876
// package with no contacts yields a row with securityContacts null → resolves to [].
877877
export interface AkritesExternalContactDetailRow
878-
extends Pick<PackageDbRow, 'purl' | 'name' | 'ecosystem'>,
878+
extends Pick<PackageDbRow, 'purl' | 'name' | 'ecosystem' | 'declaredRepositoryUrl'>,
879879
Pick<
880880
RepoDbRow,
881881
'securityPolicyUrl' | 'vulnerabilityReportingUrl' | 'bugBountyUrl' | 'pvrEnabled'
882882
> {
883883
securityContacts: SecurityContactRow[] | null
884+
// --- joined from repos via package_repos, same BEST_REPO_LINK_JOIN as
885+
// AkritesExternalPackageDetailRow — not part of the packages or repos row.
886+
resolvedRepositoryUrl: string | null
887+
repoMappingConfidence: number | null
884888
}
885889

886890
export async function getContactDetailsByPurls(
@@ -894,6 +898,9 @@ export async function getContactDetailsByPurls(
894898
p.purl,
895899
p.name,
896900
p.ecosystem,
901+
p.declared_repository_url AS "declaredRepositoryUrl",
902+
r.url AS "resolvedRepositoryUrl",
903+
pr.confidence AS "repoMappingConfidence",
897904
r.security_policy_url AS "securityPolicyUrl",
898905
r.vulnerability_reporting_url AS "vulnerabilityReportingUrl",
899906
r.bug_bounty_url AS "bugBountyUrl",

0 commit comments

Comments
 (0)