Skip to content

feat: add packages router and mock (CM-1218)#4185

Open
ulemons wants to merge 2 commits into
mainfrom
feat/packages-api
Open

feat: add packages router and mock (CM-1218)#4185
ulemons wants to merge 2 commits into
mainfrom
feat/packages-api

Conversation

@ulemons

@ulemons ulemons commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

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, disclosureReadiness sub-fields, healthBreakdown scores) return null as 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 return status: unassigned.
  • Two new OAuth 2.0 scopes: read:packages, read:stewardships.
  • Batch route registered at v1 router level (before packages sub-router) to handle the AIP-136 colon-method notation without Express route-param conflicts.
  • Self-contained OpenAPI 3.1.0 spec at v1/packages/openapi.yaml covering both endpoints with full schema and realistic lodash/minimist examples.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Performance improvement
  • Chore / dependency update
  • Documentation

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/:purl returns a large mock PackageDetail (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-stewardship is 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 all unassigned / auto_imported with null signals—TODO until DB lands).

Introduces OAuth scopes read:packages and read: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.

Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
@ulemons ulemons self-assigned this Jun 9, 2026
Copilot AI review requested due to automatic review settings June 9, 2026 15:07
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c79f906. Configure here.

lastActivityAt: null,
lastActivityDescription: null,
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

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(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c79f906. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:packages and read:stewardships.
  • Introduced mocked endpoints: GET /v1/packages/:purl and POST /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.

Comment on lines +15 to +20
const { purl: rawPurl } = validateOrThrow(paramsSchema, req.params)
const purl = decodeURIComponent(rawPurl)

if (!purl.startsWith('pkg:')) {
throw new NotFoundError()
}
Comment on lines +6 to 10
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'
Comment on lines +20 to +36
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,
}
Comment on lines +102 to +106
stewardship: {
status: 'escalated',
origin: 'auto_imported',
stewards: [
{ userId: 'jdoe', name: 'Jonathan R.', role: 'lead', assignedAt: '2025-05-15T00:00:00Z' },
Comment on lines +604 to +614
Error:
type: object
required: [error, message]
properties:
error:
type: string
example: NOT_FOUND
message:
type: string
example: Package not found.

Comment on lines +436 to +440
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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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: [],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's call this stewardship instead of status

impact: null,
openVulns: null,
status: 'unassigned',
origin: 'auto_imported',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No need to return this in the response payload

ok(res, mockPackage(purl))
}

function mockPackage(purl: string) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants