feat: add packages router and mock (CM-1218)#4185
Conversation
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c79f906. Configure here.
| origin: 'auto_imported', | ||
| stewards: [], | ||
| lastActivityAt: null, | ||
| lastActivityDescription: null, |
There was a problem hiding this comment.
Batch and detail stewardship mismatch
Medium Severity
For the same purl, POST /packages:batch-stewardship always returns status: unassigned with empty stewards, while GET /packages/:purl returns stewardship.status: escalated with stewards and activity. List-plus-drawer flows that merge batch summaries with detail will show conflicting stewardship state for one package.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit c79f906. Configure here.
| lastActivityAt: null, | ||
| lastActivityDescription: null, | ||
| } | ||
| } |
There was a problem hiding this comment.
Batch never returns null entries
Medium Severity
The batch handler builds a stewardship object for every requested purl, but the OpenAPI for this endpoint specifies unknown CDP packages map to null (e.g. pkg:pypi/requests). Clients implementing merge logic against the spec cannot exercise missing-package handling with this mock.
Reviewed by Cursor Bugbot for commit c79f906. Configure here.
| activityType: 'escalation', | ||
| content: 'Escalated — recommending consortium fork', | ||
| metadata: { from: 'active', to: 'escalated' }, | ||
| createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), |
There was a problem hiding this comment.
lastActivityAt disagrees with activity
Low Severity
stewardship.lastActivityAt is fixed at 2025-06-08T00:00:00Z, while the newest stewardship.activity entry uses createdAt from Date.now() minus one day. Those fields usually represent the same event but drift on every request and no longer match each other.
Reviewed by Cursor Bugbot for commit c79f906. Configure here.
There was a problem hiding this comment.
Pull request overview
Adds a new v1 public API surface for package stewardship to unblock frontend work ahead of the DB-backed implementation, including new OAuth scopes, a packages sub-router, a batch “colon-method” route, and a self-contained OpenAPI spec.
Changes:
- Added OAuth scopes
read:packagesandread:stewardships. - Introduced mocked endpoints:
GET /v1/packages/:purlandPOST /v1/packages:batch-stewardship. - Added OpenAPI 3.1 spec for the new endpoints under
backend/src/api/public/v1/packages/openapi.yaml.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/src/security/scopes.ts | Adds new public API scopes for packages/stewardship. |
| backend/src/api/public/v1/index.ts | Registers the batch colon-method route and mounts the packages router. |
| backend/src/api/public/v1/packages/index.ts | Adds packages sub-router with per-router rate limiting and scope enforcement. |
| backend/src/api/public/v1/packages/getPackage.ts | Implements mocked package-detail handler (purl path param). |
| backend/src/api/public/v1/packages/batchGetStewardship.ts | Implements mocked batch stewardship handler (up to 100 purls). |
| backend/src/api/public/v1/packages/openapi.yaml | Documents both endpoints and their schemas/examples. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { purl: rawPurl } = validateOrThrow(paramsSchema, req.params) | ||
| const purl = decodeURIComponent(rawPurl) | ||
|
|
||
| if (!purl.startsWith('pkg:')) { | ||
| throw new NotFoundError() | ||
| } |
| import { safeWrap } from '@/middlewares/errorMiddleware' | ||
| import { SCOPES } from '@/security/scopes' | ||
| import { oauth2Middleware } from '../middlewares/oauth2Middleware' | ||
| import { requireScopes } from '../middlewares/requireScopes' | ||
| import { staticApiKeyMiddleware } from '../middlewares/staticApiKeyMiddleware' |
| const packages: Record<string, object> = {} | ||
| for (const purl of purls) { | ||
| const name = purl.split('/').pop()?.split('@')[0] ?? purl | ||
| const ecosystem = purl.startsWith('pkg:npm') ? 'npm' : purl.startsWith('pkg:maven') ? 'maven' : 'unknown' | ||
| packages[purl] = { | ||
| name, | ||
| ecosystem, | ||
| lifecycle: null, | ||
| health: null, | ||
| impact: null, | ||
| openVulns: null, | ||
| status: 'unassigned', | ||
| origin: 'auto_imported', | ||
| stewards: [], | ||
| lastActivityAt: null, | ||
| lastActivityDescription: null, | ||
| } |
| stewardship: { | ||
| status: 'escalated', | ||
| origin: 'auto_imported', | ||
| stewards: [ | ||
| { userId: 'jdoe', name: 'Jonathan R.', role: 'lead', assignedAt: '2025-05-15T00:00:00Z' }, |
| Error: | ||
| type: object | ||
| required: [error, message] | ||
| properties: | ||
| error: | ||
| type: string | ||
| example: NOT_FOUND | ||
| message: | ||
| type: string | ||
| example: Package not found. | ||
|
|
| description: > | ||
| Full package data for the drawer (Overview, Security, Provenance tabs). | ||
| Lifecycle, Health, Impact, and health breakdown are NOT included here — | ||
| Self Serve fetches those from Insights and merges client-side. | ||
| properties: |
| lifecycle: null, | ||
| health: null, | ||
| impact: null, | ||
| openVulns: null, |
There was a problem hiding this comment.
openVulns can be fetched from the advisories table:
I'm imagining something like this for openVulns:
{
low: lowCount,
medium: mediumCount,
high: highCount,
critical: criticalCount
}
- advisories — one row per OSV advisory, holds the severity label and CVSS score.
- advisory_packages — joins advisories to packages (one advisory can affect many packages).
- advisory_affected_ranges — version ranges where the vulnerability applies. You need this if you want to restrict to packages whose current version is still vulnerable (i.e.
fixed_version IS NULL = unpatched). Otherwise, skip it for a raw count. -> Let's have all open vulns for now skipping version.
E.g. To check data
SELECT
ap.package_id,
COUNT(*) FILTER (WHERE a.severity = 'LOW') AS low,
COUNT(*) FILTER (WHERE a.severity = 'MEDIUM') AS medium,
COUNT(*) FILTER (WHERE a.severity = 'HIGH') AS high,
COUNT(*) FILTER (WHERE a.severity = 'CRITICAL') AS critical
FROM advisory_packages ap
JOIN advisories a ON a.id = ap.advisory_id
WHERE ap.package_id IS NOT NULL
GROUP BY ap.package_id
| openVulns: null, | ||
| status: 'unassigned', | ||
| origin: 'auto_imported', | ||
| stewards: [], |
There was a problem hiding this comment.
I think it will only be one value. So I would say to keep it as null for now.
| health: null, | ||
| impact: null, | ||
| openVulns: null, | ||
| status: 'unassigned', |
There was a problem hiding this comment.
Let's call this stewardship instead of status
| impact: null, | ||
| openVulns: null, | ||
| status: 'unassigned', | ||
| origin: 'auto_imported', |
There was a problem hiding this comment.
No need to return this in the response payload
| ok(res, mockPackage(purl)) | ||
| } | ||
|
|
||
| function mockPackage(purl: string) { |
There was a problem hiding this comment.
We can discuss with Gasper but I would suggest to have a simpler endpoints/response payload. We would have a filter/query param in this endpoint by tab.
So by overview, assessment, security, provenance and history.
We would only return the needed data for each.
| if (purl.startsWith('pkg:npm')) ecosystem = 'npm' | ||
| else if (purl.startsWith('pkg:maven')) ecosystem = 'maven' | ||
|
|
||
| return { |
There was a problem hiding this comment.
I think there are some inconsistencies with the design.
Latest version: https://linuxfoundation.slack.com/archives/C0B6TK0QJ00/p1780999390218909
Suggestion on how to return data:
{
purl,
name,
ecosystem,
general: {
healthScore: {
maintainerHealth (e.g. 4),
securitySupplyChain (e.g. 8),
developmentActivity (e.g. 6),
total (e.g. 18),
},
impact: {
impactScore,
downloadsLastMonth,
dependentPackages,
dependentRepos,
transitiveReach,
},
riskSignals: {
lifecycle,
maintainerBusFactor,
lastRelease,
hasSecurityFile,
openSSFScorecard,
},
},
assessment: {},
security: {
securityContacts: [
{
email,
name,
}
],
advisories: [
osvId,
severity,
resolution, (TBD)
],
cvd: {
isPvrEnabled,
hasSecurityPolicyEnabled,
tier0Steward,
criticalVulnerabilityFlag
}
},
provenance: {
repositoryMapping: {
declaredRepo,
mappingConfidence,
lastCommitAt
},
supplyChainIntegrity: {
buildProvenance,
signedReleases,
}
},
history: {}
}
If we split the response by tab, then we would return only the data points for each one of the objects
|
|
||
| const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 }) | ||
|
|
||
| export function packagesRouter(): Router { |
There was a problem hiding this comment.
Let's add 2 extra endpoints:
- One for the list page (not filtered by ids/purls). Ability to have filters, sorters and pagination
- One for the overview metrics on top of the list page:
- For now should return: totalPackages and criticalPackages count.


Summary
Scaffolds the two mocked REST API endpoints for the OSSPREY Self Serve admin package stewardship view (Workstream 3). The endpoints return realistic mock data so the frontend can be built and integrated before the DB tables land.
Changes
GET /v1/packages/:purl— returns full package detail: identity, lifecycle, health breakdown, vulnerabilities, repository, maintainers, OpenSSF Scorecard checks, disclosure readiness, provenance mappings, supply chain integrity, and full stewardship record (assessment, findings, remediation actions, activity log). Fields not yet ingested (supplyChainIntegrity,disclosureReadinesssub-fields,healthBreakdownscores) returnnullas documented.POST /v1/packages:batch-stewardship— accepts up to 100 purls, returns a per-purl stewardship summary (status, stewards, lifecycle, health, impact, openVulns). In v1 all rows returnstatus: unassigned.read:packages,read:stewardships.v1/packages/openapi.yamlcovering both endpoints with full schema and realistic lodash/minimist examples.Type of change
JIRA ticket
ticket
Note
Low Risk
New read-only mocked routes behind existing OAuth/scope checks; no persistence or writes. Main risk is contract drift between mock detail vs batch responses and OpenAPI until real queries land.
Overview
Adds read-only public v1 APIs for OSSPREY Self Serve package stewardship, wired like other v1 routes with OAuth 2.0,
requireScopes,safeWrap, and a 60/min rate limit on the packages sub-router.GET /v1/packages/:purlreturns a large mockPackageDetail(identity, security/advisories, provenance, stewardship assessment/findings/activity, etc.) after validating the path param and rejecting non-pkg:PURLs with 404.POST /v1/packages:batch-stewardshipis registered on the v1 router (escaped colon path) so it does not clash with/:purl; it accepts 1–100 purls and returns per-purl lean stewardship summaries (currently allunassigned/auto_importedwith null signals—TODO until DB lands).Introduces OAuth scopes
read:packagesandread:stewardships, and ships an OpenAPI 3.1 spec for both endpoints. Handlers are explicitly mock/stub implementations pending stewardship tables.Note for reviewers: the detail mock and OpenAPI examples show rich escalated stewardship, while the batch handler and spec’s v1 narrative emphasize empty/unassigned stewardship—frontends should not assume both endpoints behave the same until real data is wired.
Reviewed by Cursor Bugbot for commit c79f906. Bugbot is set up for automated code reviews on this repo. Configure here.