From e4a431e506c6f162566a10bb86b9706f233371f2 Mon Sep 17 00:00:00 2001 From: Umberto Sgueglia Date: Tue, 9 Jun 2026 17:01:44 +0200 Subject: [PATCH 1/2] feat: add packages router and mock Signed-off-by: Umberto Sgueglia --- backend/src/api/public/v1/index.ts | 13 + .../public/v1/packages/batchGetStewardship.ts | 40 + .../src/api/public/v1/packages/getPackage.ts | 213 ++++ backend/src/api/public/v1/packages/index.ts | 24 + .../src/api/public/v1/packages/openapi.yaml | 937 ++++++++++++++++++ backend/src/security/scopes.ts | 2 + 6 files changed, 1229 insertions(+) create mode 100644 backend/src/api/public/v1/packages/batchGetStewardship.ts create mode 100644 backend/src/api/public/v1/packages/getPackage.ts create mode 100644 backend/src/api/public/v1/packages/index.ts create mode 100644 backend/src/api/public/v1/packages/openapi.yaml diff --git a/backend/src/api/public/v1/index.ts b/backend/src/api/public/v1/index.ts index 90f8571a24..e3d2f75975 100644 --- a/backend/src/api/public/v1/index.ts +++ b/backend/src/api/public/v1/index.ts @@ -3,12 +3,17 @@ import { Router } from 'express' import { NotFoundError } from '@crowd/common' import { AUTH0_CONFIG } from '../../../conf' +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' import { memberOrganizationAffiliationsRouter } from './affiliations' import { membersRouter } from './members' import { organizationsRouter } from './organizations' +import { packagesRouter } from './packages' +import { batchGetStewardship } from './packages/batchGetStewardship' export function v1Router(): Router { const router = Router() @@ -17,6 +22,14 @@ export function v1Router(): Router { router.use('/organizations', oauth2Middleware(AUTH0_CONFIG), organizationsRouter()) router.use('/affiliations', staticApiKeyMiddleware(), memberOrganizationAffiliationsRouter()) + router.post( + '/packages\\:batch-stewardship', + oauth2Middleware(AUTH0_CONFIG), + requireScopes([SCOPES.READ_STEWARDSHIPS]), + safeWrap(batchGetStewardship), + ) + router.use('/packages', oauth2Middleware(AUTH0_CONFIG), packagesRouter()) + router.use(() => { throw new NotFoundError() }) diff --git a/backend/src/api/public/v1/packages/batchGetStewardship.ts b/backend/src/api/public/v1/packages/batchGetStewardship.ts new file mode 100644 index 0000000000..cc431ca67d --- /dev/null +++ b/backend/src/api/public/v1/packages/batchGetStewardship.ts @@ -0,0 +1,40 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const MAX_PURLS = 100 + +const bodySchema = z.object({ + purls: z + .array(z.string().trim().min(1)) + .min(1) + .max(MAX_PURLS, `Maximum ${MAX_PURLS} purls per request`), +}) + +// TODO: replace with real DB queries once stewardship tables land +export async function batchGetStewardship(req: Request, res: Response): Promise { + const { purls } = validateOrThrow(bodySchema, req.body) + + const packages: Record = {} + 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, + } + } + + ok(res, { packages }) +} diff --git a/backend/src/api/public/v1/packages/getPackage.ts b/backend/src/api/public/v1/packages/getPackage.ts new file mode 100644 index 0000000000..9bdcb5a24a --- /dev/null +++ b/backend/src/api/public/v1/packages/getPackage.ts @@ -0,0 +1,213 @@ +import type { Request, Response } from 'express' +import { z } from 'zod' + +import { NotFoundError } from '@crowd/common' + +import { ok } from '@/utils/api' +import { validateOrThrow } from '@/utils/validation' + +const paramsSchema = z.object({ + purl: z.string().trim().min(1), +}) + +// TODO: replace with real DB queries once stewardship tables land +export async function getPackage(req: Request, res: Response): Promise { + const { purl: rawPurl } = validateOrThrow(paramsSchema, req.params) + const purl = decodeURIComponent(rawPurl) + + if (!purl.startsWith('pkg:')) { + throw new NotFoundError() + } + + ok(res, mockPackage(purl)) +} + +function mockPackage(purl: string) { + const name = purl.split('/').pop()?.split('@')[0] ?? purl + const ecosystem = purl.startsWith('pkg:npm') ? 'npm' : purl.startsWith('pkg:maven') ? 'maven' : 'unknown' + + return { + purl, + name, + ecosystem, + latestVersion: '4.17.21', + latestReleaseAt: '2021-02-20T00:00:00Z', + downloads: ecosystem === 'maven' ? null : 52142891, + dependentPackagesCount: 142312, + dependentReposCount: 39104, + lifecycle: 'declining', + health: 18, + healthBreakdown: { + maintainerHealth: 4, + securityAndSupplyChain: 8, + developmentActivity: 6, + }, + impact: 71, + transitiveReach: 'Top 0.4%', + busFactor: 1, + openVulns: { count: 1, severity: 'high' }, + repository: { + url: `https://github.com/example/${name}`, + lastCommitAt: '2024-09-14T00:00:00Z', + scorecardScore: 5.2, + isArchived: false, + }, + maintainers: [ + { + username: 'jdalton', + displayName: 'John-David Dalton', + emailHash: null, + url: 'https://github.com/jdalton', + }, + ], + securityContact: null, + advisories: [ + { + id: 'CVE-2021-44906', + severity: 'high', + summary: 'Prototype pollution via constructor', + status: 'open', + cvss: 9.8, + publishedAt: '2022-03-17T00:00:00Z', + affectedVersionRange: { + introduced: '0.0.1', + fixed: null, + lastAffected: '4.17.21', + }, + }, + ], + scorecardChecks: [ + { checkName: 'Branch-Protection', score: 3.0, reason: 'Branch protection not enabled for default branch' }, + { checkName: 'Security-Policy', score: 0.0, reason: 'security policy file not detected' }, + { checkName: 'Maintained', score: 5.0, reason: 'repo was created 9 years ago, has 337 open issues' }, + ], + disclosureReadiness: { + pvrEnabled: null, + securityMdPresent: null, + tier0StewardName: null, + hasCriticalAdvisory: true, + }, + provenanceMappings: [ + { + repoUrl: `https://github.com/example/${name}`, + confidence: 0.98, + source: 'declared', + verified: true, + }, + ], + supplyChainIntegrity: { + buildProvenance: null, + signedReleases: null, + }, + stewardship: { + status: 'escalated', + origin: 'auto_imported', + stewards: [ + { userId: 'jdoe', name: 'Jonathan R.', role: 'lead', assignedAt: '2025-05-15T00:00:00Z' }, + ], + lastActivityAt: '2025-06-08T00:00:00Z', + lastActivityDescription: 'Escalated — recommending consortium fork', + assessment: { + posture: 'Critical', + summary: 'Single maintainer, bus factor 1, no security process in place.', + draft: false, + reviewed: false, + flagged: false, + flagNote: null, + monitoringPlan: 'Watch for new maintainer activity\nWatch registry for ownership transfer', + completedAt: '2025-05-18T00:00:00Z', + completedBy: 'jdoe', + }, + findings: [ + { + id: 1, + dimension: 'maintainer_health', + severity: 'critical', + finding: 'Single maintainer, no response to issue filed 6 weeks ago. Bus factor 1.', + evidence: null, + }, + { + id: 2, + dimension: 'release_health', + severity: 'high', + finding: 'Last release 5 years ago. 180+ open issues. Active community forks exist.', + evidence: null, + }, + { + id: 3, + dimension: 'security_posture', + severity: 'high', + finding: 'No SECURITY.md, no disclosure process, releases unsigned.', + evidence: null, + }, + ], + remediationActions: [ + { + id: 1, + findingId: 3, + action: 'Filed issue requesting SECURITY.md + disclosure process (no response)', + status: 'done', + url: null, + notes: null, + completedAt: '2025-05-20T00:00:00Z', + }, + { + id: 2, + findingId: null, + action: 'Opened PR re-enabling branch protection', + status: 'done', + url: null, + notes: null, + completedAt: '2025-05-22T00:00:00Z', + }, + { + id: 3, + findingId: null, + action: 'Recommended escalation — vendor LTS or consortium fork', + status: 'pending', + url: null, + notes: null, + completedAt: null, + }, + ], + activity: [ + { + id: 4, + actorUserId: 'jdoe', + actorType: 'user', + activityType: 'escalation', + content: 'Escalated — recommending consortium fork', + metadata: { from: 'active', to: 'escalated' }, + createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 3, + actorUserId: 'jdoe', + actorType: 'user', + activityType: 'remediation_logged', + content: 'Jonathan R. logged: maintainer unresponsive after 3 attempts', + metadata: null, + createdAt: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 2, + actorUserId: 'jdoe', + actorType: 'user', + activityType: 'assessment_completed', + content: 'Assessment completed — posture Critical', + metadata: { posture: 'Critical' }, + createdAt: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000).toISOString(), + }, + { + id: 1, + actorUserId: null, + actorType: 'system', + activityType: 'steward_added', + content: 'Assigned to Jonathan R.', + metadata: { userId: 'jdoe', role: 'lead' }, + createdAt: new Date(Date.now() - 24 * 24 * 60 * 60 * 1000).toISOString(), + }, + ], + }, + } +} diff --git a/backend/src/api/public/v1/packages/index.ts b/backend/src/api/public/v1/packages/index.ts new file mode 100644 index 0000000000..7271547258 --- /dev/null +++ b/backend/src/api/public/v1/packages/index.ts @@ -0,0 +1,24 @@ +import { Router } from 'express' + +import { createRateLimiter } from '@/api/apiRateLimiter' +import { requireScopes } from '@/api/public/middlewares/requireScopes' +import { safeWrap } from '@/middlewares/errorMiddleware' +import { SCOPES } from '@/security/scopes' + +import { getPackage } from './getPackage' + +const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 }) + +export function packagesRouter(): Router { + const router = Router() + + router.use(rateLimiter) + + router.get( + '/:purl', + requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS]), + safeWrap(getPackage), + ) + + return router +} diff --git a/backend/src/api/public/v1/packages/openapi.yaml b/backend/src/api/public/v1/packages/openapi.yaml new file mode 100644 index 0000000000..95c1d491ee --- /dev/null +++ b/backend/src/api/public/v1/packages/openapi.yaml @@ -0,0 +1,937 @@ +openapi: 3.1.0 +info: + title: CDP Public API — Packages & Stewardship + version: 1.0.0 + description: > + Read-only endpoints for the OSSPREY Self Serve program. + + + **Data split:** CDP serves package identity, repo signals, advisories, + maintainers, provenance, and stewardship state. Lifecycle, Health, Impact, + and health breakdown are served by the Insights API — Self Serve fetches + those separately and merges client-side. + + + **Authentication:** OAuth 2.0 M2M bearer token (audience-only in v1). + Required scopes: `read:packages`, `read:stewardships`. + + + **V1 constraints:** All stewardship rows are `unassigned` / `auto_imported`. + Stewards, activity, findings, and remediation arrays are always empty. + Write endpoints and state transitions are deferred to v2. + +servers: + - url: https://cm.lfx.dev/api/v1 + description: Production + - url: https://lf-staging.crowd.dev/api/v1 + description: Staging + +tags: + - name: Packages + description: Package detail for the drawer (Overview, Security, Provenance tabs). + - name: Stewardship + description: Stewardship state — individual and batch. + +components: + securitySchemes: + M2MBearer: + type: http + scheme: bearer + bearerFormat: JWT + + schemas: + + # ── Stewardship ───────────────────────────────────────────────────────────── + + StewardshipStatus: + type: string + enum: + - unassigned + - open + - assessing + - active + - needs_attention + - escalated + - blocked + - inactive + + Steward: + type: object + required: [userId, name, role, assignedAt] + properties: + userId: + type: string + description: LFID. + example: jdoe + name: + type: string + example: Jonathan R. + role: + type: string + enum: [lead, co_steward] + assignedAt: + type: string + format: date-time + + OpenVulns: + type: object + nullable: true + description: > + Aggregate open vulnerability data from advisory_packages + advisories. + Null if no open advisories exist for this package. + properties: + count: + type: integer + description: Number of open advisories affecting this package. + example: 2 + severity: + type: string + enum: [critical, high, medium, low] + description: Highest severity among open advisories. + example: high + + # Lean shape — used in batch response and embedded in PackageDetail. + StewardshipSummary: + type: object + required: [status, origin, stewards, lastActivityAt, lastActivityDescription] + properties: + # ── Package identity ─────────────────────────────────────────────────── + name: + type: string + example: lodash + ecosystem: + type: string + example: npm + # ── Package signals (from packages table, nullable until columns land) ── + lifecycle: + type: string + enum: [active, stable, declining, abandoned] + nullable: true + description: Lifecycle state. Null until the column is added to packages table. + health: + type: integer + nullable: true + description: Health score (0–100). Null until the column is added to packages table. + example: 52 + impact: + type: integer + nullable: true + description: Impact score (0–100). Null until the column is added to packages table. + example: 94 + openVulns: + $ref: '#/components/schemas/OpenVulns' + # ── Stewardship state ────────────────────────────────────────────────── + status: + $ref: '#/components/schemas/StewardshipStatus' + origin: + type: string + enum: [auto_imported, self_claimed, assigned, opened_for_claim] + stewards: + type: array + items: + $ref: '#/components/schemas/Steward' + description: Empty in v1. + lastActivityAt: + type: string + format: date-time + nullable: true + description: Null in v1. + lastActivityDescription: + type: string + nullable: true + description: Human-readable last activity label. Null in v1. + example: Escalated for intervention + + StewardshipActivity: + type: object + required: [id, actorType, activityType, createdAt] + properties: + id: + type: integer + format: int64 + actorUserId: + type: string + nullable: true + description: LFID. Null for system events. + actorType: + type: string + enum: [user, system] + activityType: + type: string + enum: + - state_changed + - assessment_completed + - assessment_flagged + - remediation_logged + - status_update + - escalation + - escalation_resolved + - blocker_added + - blocker_resolved + - steward_added + - steward_removed + content: + type: string + nullable: true + metadata: + type: object + nullable: true + additionalProperties: true + description: > + JSON context. Shape depends on activityType. + E.g. `{"from":"active","to":"needs_attention"}` for state_changed. + createdAt: + type: string + format: date-time + + StewardshipFinding: + type: object + required: [id, dimension, severity, finding] + description: Per-dimension finding from stewardship_findings. Empty in v1. + properties: + id: + type: integer + format: int64 + dimension: + type: string + enum: + - maintainer_health + - security_posture + - vulnerability_exposure + - dependency_risk + - supply_chain_integrity + - release_health + severity: + type: string + enum: [critical, high, medium, low, informational] + finding: + type: string + evidence: + type: string + nullable: true + + RemediationAction: + type: object + required: [id, action, status] + description: Remediation action from stewardship_remediation_actions. Empty in v1. + properties: + id: + type: integer + format: int64 + findingId: + type: integer + format: int64 + nullable: true + action: + type: string + status: + type: string + enum: [pending, in_progress, done, blocked, abandoned] + url: + type: string + format: uri + nullable: true + notes: + type: string + nullable: true + completedAt: + type: string + format: date-time + nullable: true + + StewardshipAssessment: + type: object + nullable: true + description: From stewardship_assessments. Null in v1 (assessment flow deferred to v2). + properties: + posture: + type: string + enum: [Critical, High, Medium, Low] + nullable: true + summary: + type: string + nullable: true + draft: + type: boolean + description: True while assessment is in progress. + reviewed: + type: boolean + description: Admin spot-check done. + flagged: + type: boolean + flagNote: + type: string + nullable: true + monitoringPlan: + type: string + nullable: true + description: Free text. Frontend splits by newline to render as list. + completedAt: + type: string + format: date-time + nullable: true + completedBy: + type: string + nullable: true + description: LFID of assessor. + + # Full stewardship block for the drawer. + StewardshipDetail: + allOf: + - $ref: '#/components/schemas/StewardshipSummary' + - type: object + required: [activity, findings, remediationActions] + properties: + assessment: + $ref: '#/components/schemas/StewardshipAssessment' + findings: + type: array + items: + $ref: '#/components/schemas/StewardshipFinding' + description: Per-dimension findings. Empty in v1. + remediationActions: + type: array + items: + $ref: '#/components/schemas/RemediationAction' + description: Remediation actions. Empty in v1. + activity: + type: array + items: + $ref: '#/components/schemas/StewardshipActivity' + description: Append-only log, newest first. Empty in v1. + + # ── Package ────────────────────────────────────────────────────────────────── + + Advisory: + type: object + required: [id, severity, status] + description: Advisory from advisories joined via advisory_affected_ranges. + properties: + id: + type: string + description: GHSA or CVE identifier. + example: CVE-2021-44906 + severity: + type: string + enum: [critical, high, medium, low] + summary: + type: string + nullable: true + status: + type: string + enum: [open, patched] + description: Whether the advisory has been patched in a released version. + cvss: + type: number + format: float + nullable: true + description: CVSS score (0–10). + publishedAt: + type: string + format: date-time + nullable: true + affectedVersionRange: + type: object + nullable: true + description: Introduced and fixed version from advisory_affected_ranges. + properties: + introduced: + type: string + nullable: true + example: "1.0.0" + fixed: + type: string + nullable: true + example: "4.17.21" + lastAffected: + type: string + nullable: true + + ScorecardCheck: + type: object + required: [checkName, score] + description: Individual check from repo_scorecard_checks. + properties: + checkName: + type: string + example: Branch-Protection + score: + type: number + format: float + minimum: 0 + maximum: 10 + nullable: true + reason: + type: string + nullable: true + + Maintainer: + type: object + description: From maintainers + package_maintainers. + properties: + username: + type: string + nullable: true + displayName: + type: string + nullable: true + emailHash: + type: string + nullable: true + description: Hashed email (raw email is not stored). + url: + type: string + nullable: true + + ProvenanceMapping: + type: object + required: [repoUrl, confidence, source] + description: One package→repo mapping from package_repos. + properties: + repoUrl: + type: string + format: uri + example: https://github.com/lodash/lodash + confidence: + type: number + format: float + minimum: 0 + maximum: 1 + description: Mapping confidence score (0.00–1.00). + example: 0.98 + source: + type: string + enum: [declared, deps_dev, heuristic, manual] + description: How the mapping was determined. + verified: + type: boolean + nullable: true + + Repository: + type: object + description: From repos table. + properties: + url: + type: string + nullable: true + example: https://github.com/lodash/lodash + lastCommitAt: + type: string + format: date-time + nullable: true + scorecardScore: + type: number + format: float + minimum: 0 + maximum: 10 + nullable: true + description: Aggregate OpenSSF Scorecard score. + isArchived: + type: boolean + nullable: true + + PackageDetail: + type: object + required: [purl, name, ecosystem, maintainers, advisories, stewardship] + 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: + + # ── Identity (packages table) ────────────────────────────────────────── + purl: + type: string + example: pkg:npm/lodash + name: + type: string + example: lodash + ecosystem: + type: string + example: npm + latestVersion: + type: string + nullable: true + example: "4.17.21" + latestReleaseAt: + type: string + format: date-time + nullable: true + downloads: + type: integer + nullable: true + description: Downloads last month. Null for Maven (Sonatype data not yet ingested). + example: 52142891 + dependentPackagesCount: + type: integer + nullable: true + example: 142312 + dependentReposCount: + type: integer + nullable: true + example: 39104 + lifecycle: + type: string + enum: [active, stable, declining, abandoned] + nullable: true + description: Null until the column is added to packages table. + health: + type: integer + nullable: true + description: Total health score (0–100). Null until the column is added to packages table. + example: 18 + healthBreakdown: + type: object + nullable: true + description: Sub-scores breakdown. Source TBD (Insights or packages table). + properties: + maintainerHealth: + type: integer + nullable: true + description: Maintainer health sub-score (max 40). + example: 4 + securityAndSupplyChain: + type: integer + nullable: true + description: Security & supply chain sub-score (max 35). + example: 8 + developmentActivity: + type: integer + nullable: true + description: Development activity sub-score (max 25). + example: 6 + impact: + type: integer + nullable: true + description: Impact score (0–100). Null until the column is added to packages table. + example: 71 + transitiveReach: + type: string + nullable: true + description: Transitive reach percentile label. Source TBD (Insights or packages table). + example: "Top 0.4%" + busFactor: + type: integer + nullable: true + description: Number of distinct maintainers. Computed from COUNT(package_maintainers). + example: 1 + openVulns: + $ref: '#/components/schemas/OpenVulns' + + # ── Repo signals (repos table) ───────────────────────────────────────── + repository: + allOf: + - $ref: '#/components/schemas/Repository' + nullable: true + + # ── Maintainers (maintainers + package_maintainers) ─────────────────── + maintainers: + type: array + items: + $ref: '#/components/schemas/Maintainer' + + # ── Security (advisories + advisory_affected_ranges + repo_scorecard_checks) ── + securityContact: + type: string + nullable: true + description: > + Security contact for this package. + From stewardship_assessments.security_contact. Null in v1 (assessment flow deferred to v2). + advisories: + type: array + items: + $ref: '#/components/schemas/Advisory' + description: Advisories from advisories joined via advisory_packages. + scorecardChecks: + type: array + items: + $ref: '#/components/schemas/ScorecardCheck' + description: Individual Scorecard checks from repo_scorecard_checks. + disclosureReadiness: + type: object + description: Disclosure readiness signals for the Security tab. + properties: + pvrEnabled: + type: boolean + nullable: true + description: > + Private Vulnerability Reporting enabled. + From stewardship_assessments.disclosure_preference. Null in v1. + securityMdPresent: + type: boolean + nullable: true + description: SECURITY.md present in repo. Null until enricher captures this signal. + tier0StewardName: + type: string + nullable: true + description: > + Name of the Tier 0 steward if assigned. + From stewardship_stewards. Null in v1 (no claiming flow yet). + hasCriticalAdvisory: + type: boolean + nullable: true + description: > + Whether any open advisory has cvss >= 7.0 (advisories.is_critical generated column). + Available in v1. + + # ── Provenance (package_repos) ───────────────────────────────────────── + provenanceMappings: + type: array + items: + $ref: '#/components/schemas/ProvenanceMapping' + description: > + Package→repo mappings. Multiple entries possible for monorepos or + conflicting sources. Empty if no mapping has been resolved yet. + supplyChainIntegrity: + type: object + description: Supply chain signals. All fields null in v1 — separate ingestion workstream required. + properties: + buildProvenance: + type: string + nullable: true + description: SLSA provenance attestation status. Null until ingested. + signedReleases: + type: string + nullable: true + description: Whether releases are signed. Null until ingested. + + # ── Stewardship (stewardships + stewardship_activity) ───────────────── + stewardship: + $ref: '#/components/schemas/StewardshipDetail' + + # ── Errors ─────────────────────────────────────────────────────────────────── + + Error: + type: object + required: [error, message] + properties: + error: + type: string + example: NOT_FOUND + message: + type: string + example: Package not found. + +# ────────────────────────────────────────────────────────────────────────────── +# Paths +# ────────────────────────────────────────────────────────────────────────────── +paths: + + /packages/{purl}: + get: + operationId: getPackage + summary: Get full package detail + description: > + Returns data for the drawer's Overview, Security, and Provenance tabs. + The `purl` parameter must be URL-encoded. + Example: `pkg:npm/lodash` → `pkg%3Anpm%2Flodash`. + + + In v1 `stewardship.activity` is always an empty array. + tags: + - Packages + security: + - M2MBearer: + - read:packages + - read:stewardships + parameters: + - name: purl + in: path + required: true + schema: + type: string + example: pkg%3Anpm%2Flodash + responses: + '200': + description: Package found. + content: + application/json: + schema: + $ref: '#/components/schemas/PackageDetail' + example: + purl: pkg:npm/lodash + name: lodash + ecosystem: npm + latestVersion: "4.17.21" + latestReleaseAt: "2021-02-20T00:00:00Z" + downloads: 52142891 + dependentPackagesCount: 142312 + dependentReposCount: 39104 + lifecycle: declining + health: 18 + healthBreakdown: + maintainerHealth: 4 + securityAndSupplyChain: 8 + developmentActivity: 6 + impact: 71 + transitiveReach: "Top 0.4%" + busFactor: 1 + openVulns: + count: 1 + severity: high + repository: + url: https://github.com/lodash/lodash + lastCommitAt: "2024-09-14T00:00:00Z" + scorecardScore: 5.2 + isArchived: false + maintainers: + - username: jdalton + displayName: John-David Dalton + emailHash: null + url: https://github.com/jdalton + advisories: + - id: CVE-2021-44906 + severity: high + summary: Prototype pollution via constructor + status: open + cvss: 9.8 + publishedAt: "2022-03-17T00:00:00Z" + affectedVersionRange: + introduced: "0.0.1" + fixed: null + lastAffected: "4.17.21" + securityContact: null + scorecardChecks: + - checkName: Branch-Protection + score: 3.0 + reason: Branch protection not enabled for default branch + - checkName: Security-Policy + score: 0.0 + reason: security policy file not detected + disclosureReadiness: + pvrEnabled: null + securityMdPresent: false + tier0StewardName: null + hasCriticalAdvisory: true + provenanceMappings: + - repoUrl: https://github.com/lodash/lodash + confidence: 0.98 + source: declared + verified: true + supplyChainIntegrity: + buildProvenance: null + signedReleases: null + stewardship: + status: escalated + origin: auto_imported + stewards: + - userId: jdoe + name: Jonathan R. + role: lead + assignedAt: "2025-05-15T00:00:00Z" + lastActivityAt: "2025-06-08T00:00:00Z" + lastActivityDescription: Escalated — recommending consortium fork + assessment: + posture: Critical + summary: Single maintainer, bus factor 1, no security process in place. + draft: false + reviewed: false + flagged: false + flagNote: null + monitoringPlan: "Watch for new maintainer activity\nWatch registry for ownership transfer" + completedAt: "2025-05-18T00:00:00Z" + completedBy: jdoe + findings: + - id: 1 + dimension: maintainer_health + severity: critical + finding: Single maintainer, no response to issue filed 6 weeks ago. Bus factor 1. + evidence: null + - id: 2 + dimension: release_health + severity: high + finding: Last release 5 years ago. 180+ open issues. Active community forks exist. + evidence: null + - id: 3 + dimension: security_posture + severity: high + finding: No SECURITY.md, no disclosure process, releases unsigned. + evidence: null + remediationActions: + - id: 1 + findingId: 3 + action: Filed issue requesting SECURITY.md + disclosure process (no response) + status: done + url: null + notes: null + completedAt: "2025-05-20T00:00:00Z" + - id: 2 + findingId: null + action: Opened PR re-enabling branch protection + status: done + url: null + notes: null + completedAt: "2025-05-22T00:00:00Z" + - id: 3 + findingId: null + action: Recommended escalation — vendor LTS or consortium fork + status: pending + url: null + notes: null + completedAt: null + activity: + - id: 4 + actorUserId: jdoe + actorType: user + activityType: escalation + content: Escalated — recommending consortium fork + metadata: + from: active + to: escalated + createdAt: "2025-06-08T00:00:00Z" + - id: 3 + actorUserId: jdoe + actorType: user + activityType: remediation_logged + content: "Jonathan R. logged: maintainer unresponsive after 3 attempts" + metadata: null + createdAt: "2025-06-01T00:00:00Z" + - id: 2 + actorUserId: jdoe + actorType: user + activityType: assessment_completed + content: Assessment completed — posture Critical + metadata: + posture: Critical + createdAt: "2025-05-18T00:00:00Z" + - id: 1 + actorUserId: null + actorType: system + activityType: steward_added + content: Assigned to Jonathan R. + metadata: + userId: jdoe + role: lead + createdAt: "2025-05-15T00:00:00Z" + '404': + description: Package not found. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + /packages:batch-stewardship: + post: + operationId: batchGetStewardship + summary: Batch stewardship state for a list of purls + description: > + Returns lean stewardship state for up to 100 purls in one request. + + + **Intended flow (admin queue list view):** + + 1. Self Serve calls Insights list endpoint → page of N packages with + composites (lifecycle, health, impact, open vulns). + + 2. Self Serve calls this endpoint with those purls → live stewardship state. + + 3. Self Serve merges and renders. + + + Purls not found in CDP return `null`. Always returns HTTP 200. + tags: + - Stewardship + security: + - M2MBearer: + - read:stewardships + requestBody: + required: true + content: + application/json: + schema: + type: object + required: [purls] + properties: + purls: + type: array + items: + type: string + minItems: 1 + maxItems: 100 + example: + - pkg:npm/lodash + - pkg:npm/express + - pkg:pypi/requests + responses: + '200': + description: > + Stewardship state keyed by purl. + Unknown purls return `null`. + content: + application/json: + schema: + type: object + required: [packages] + properties: + packages: + type: object + additionalProperties: + oneOf: + - $ref: '#/components/schemas/StewardshipSummary' + - type: 'null' + example: + packages: + pkg:npm/lodash: + name: lodash + ecosystem: npm + lifecycle: declining + health: 52 + impact: 94 + openVulns: + count: 1 + severity: high + status: unassigned + origin: auto_imported + stewards: [] + lastActivityAt: null + lastActivityDescription: null + pkg:npm/express: + name: express + ecosystem: npm + lifecycle: active + health: 78 + impact: 88 + openVulns: null + status: escalated + origin: auto_imported + stewards: + - userId: jdoe + name: Jonathan R. + role: lead + assignedAt: "2025-01-15T10:00:00Z" + lastActivityAt: "2025-06-08T14:23:00Z" + lastActivityDescription: Escalated for intervention + pkg:pypi/requests: null + '400': + description: Validation error (e.g. more than 100 purls). + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + error: VALIDATION_ERROR + message: purls must contain between 1 and 100 items. + '401': + description: Missing or invalid bearer token. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '403': + description: Insufficient scopes. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' diff --git a/backend/src/security/scopes.ts b/backend/src/security/scopes.ts index 6254d524b2..c60542d2ee 100644 --- a/backend/src/security/scopes.ts +++ b/backend/src/security/scopes.ts @@ -10,6 +10,8 @@ export const SCOPES = { READ_PROJECT_AFFILIATIONS: 'read:project-affiliations', WRITE_PROJECT_AFFILIATIONS: 'write:project-affiliations', READ_AFFILIATIONS: 'read:affiliations', + READ_PACKAGES: 'read:packages', + READ_STEWARDSHIPS: 'read:stewardships', } as const export type Scope = (typeof SCOPES)[keyof typeof SCOPES] From c79f906e1a521b4695521399aaec4bf52ce65e48 Mon Sep 17 00:00:00 2001 From: Umberto Sgueglia Date: Tue, 9 Jun 2026 17:08:30 +0200 Subject: [PATCH 2/2] fix: preflight Signed-off-by: Umberto Sgueglia --- backend/src/api/public/v1/index.ts | 3 +- .../public/v1/packages/batchGetStewardship.ts | 4 +- .../src/api/public/v1/packages/getPackage.ts | 16 ++++-- .../src/api/public/v1/packages/openapi.yaml | 49 +++++++++---------- 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/backend/src/api/public/v1/index.ts b/backend/src/api/public/v1/index.ts index e3d2f75975..adc52cafa9 100644 --- a/backend/src/api/public/v1/index.ts +++ b/backend/src/api/public/v1/index.ts @@ -2,9 +2,10 @@ import { Router } from 'express' import { NotFoundError } from '@crowd/common' -import { AUTH0_CONFIG } from '../../../conf' import { safeWrap } from '@/middlewares/errorMiddleware' import { SCOPES } from '@/security/scopes' + +import { AUTH0_CONFIG } from '../../../conf' import { oauth2Middleware } from '../middlewares/oauth2Middleware' import { requireScopes } from '../middlewares/requireScopes' import { staticApiKeyMiddleware } from '../middlewares/staticApiKeyMiddleware' diff --git a/backend/src/api/public/v1/packages/batchGetStewardship.ts b/backend/src/api/public/v1/packages/batchGetStewardship.ts index cc431ca67d..69b137ad36 100644 --- a/backend/src/api/public/v1/packages/batchGetStewardship.ts +++ b/backend/src/api/public/v1/packages/batchGetStewardship.ts @@ -20,7 +20,9 @@ export async function batchGetStewardship(req: Request, res: Response): Promise< const packages: Record = {} 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' + let ecosystem = 'unknown' + if (purl.startsWith('pkg:npm')) ecosystem = 'npm' + else if (purl.startsWith('pkg:maven')) ecosystem = 'maven' packages[purl] = { name, ecosystem, diff --git a/backend/src/api/public/v1/packages/getPackage.ts b/backend/src/api/public/v1/packages/getPackage.ts index 9bdcb5a24a..31ef8002c2 100644 --- a/backend/src/api/public/v1/packages/getPackage.ts +++ b/backend/src/api/public/v1/packages/getPackage.ts @@ -24,7 +24,9 @@ export async function getPackage(req: Request, res: Response): Promise { function mockPackage(purl: string) { const name = purl.split('/').pop()?.split('@')[0] ?? purl - const ecosystem = purl.startsWith('pkg:npm') ? 'npm' : purl.startsWith('pkg:maven') ? 'maven' : 'unknown' + let ecosystem = 'unknown' + if (purl.startsWith('pkg:npm')) ecosystem = 'npm' + else if (purl.startsWith('pkg:maven')) ecosystem = 'maven' return { purl, @@ -77,9 +79,17 @@ function mockPackage(purl: string) { }, ], scorecardChecks: [ - { checkName: 'Branch-Protection', score: 3.0, reason: 'Branch protection not enabled for default branch' }, + { + checkName: 'Branch-Protection', + score: 3.0, + reason: 'Branch protection not enabled for default branch', + }, { checkName: 'Security-Policy', score: 0.0, reason: 'security policy file not detected' }, - { checkName: 'Maintained', score: 5.0, reason: 'repo was created 9 years ago, has 337 open issues' }, + { + checkName: 'Maintained', + score: 5.0, + reason: 'repo was created 9 years ago, has 337 open issues', + }, ], disclosureReadiness: { pvrEnabled: null, diff --git a/backend/src/api/public/v1/packages/openapi.yaml b/backend/src/api/public/v1/packages/openapi.yaml index 95c1d491ee..1ac8608349 100644 --- a/backend/src/api/public/v1/packages/openapi.yaml +++ b/backend/src/api/public/v1/packages/openapi.yaml @@ -40,7 +40,6 @@ components: bearerFormat: JWT schemas: - # ── Stewardship ───────────────────────────────────────────────────────────── StewardshipStatus: @@ -338,11 +337,11 @@ components: introduced: type: string nullable: true - example: "1.0.0" + example: '1.0.0' fixed: type: string nullable: true - example: "4.17.21" + example: '4.17.21' lastAffected: type: string nullable: true @@ -438,7 +437,6 @@ components: Lifecycle, Health, Impact, and health breakdown are NOT included here — Self Serve fetches those from Insights and merges client-side. properties: - # ── Identity (packages table) ────────────────────────────────────────── purl: type: string @@ -452,7 +450,7 @@ components: latestVersion: type: string nullable: true - example: "4.17.21" + example: '4.17.21' latestReleaseAt: type: string format: date-time @@ -509,7 +507,7 @@ components: type: string nullable: true description: Transitive reach percentile label. Source TBD (Insights or packages table). - example: "Top 0.4%" + example: 'Top 0.4%' busFactor: type: integer nullable: true @@ -616,7 +614,6 @@ components: # Paths # ────────────────────────────────────────────────────────────────────────────── paths: - /packages/{purl}: get: operationId: getPackage @@ -652,8 +649,8 @@ paths: purl: pkg:npm/lodash name: lodash ecosystem: npm - latestVersion: "4.17.21" - latestReleaseAt: "2021-02-20T00:00:00Z" + latestVersion: '4.17.21' + latestReleaseAt: '2021-02-20T00:00:00Z' downloads: 52142891 dependentPackagesCount: 142312 dependentReposCount: 39104 @@ -664,14 +661,14 @@ paths: securityAndSupplyChain: 8 developmentActivity: 6 impact: 71 - transitiveReach: "Top 0.4%" + transitiveReach: 'Top 0.4%' busFactor: 1 openVulns: count: 1 severity: high repository: url: https://github.com/lodash/lodash - lastCommitAt: "2024-09-14T00:00:00Z" + lastCommitAt: '2024-09-14T00:00:00Z' scorecardScore: 5.2 isArchived: false maintainers: @@ -685,11 +682,11 @@ paths: summary: Prototype pollution via constructor status: open cvss: 9.8 - publishedAt: "2022-03-17T00:00:00Z" + publishedAt: '2022-03-17T00:00:00Z' affectedVersionRange: - introduced: "0.0.1" + introduced: '0.0.1' fixed: null - lastAffected: "4.17.21" + lastAffected: '4.17.21' securityContact: null scorecardChecks: - checkName: Branch-Protection @@ -718,8 +715,8 @@ paths: - userId: jdoe name: Jonathan R. role: lead - assignedAt: "2025-05-15T00:00:00Z" - lastActivityAt: "2025-06-08T00:00:00Z" + assignedAt: '2025-05-15T00:00:00Z' + lastActivityAt: '2025-06-08T00:00:00Z' lastActivityDescription: Escalated — recommending consortium fork assessment: posture: Critical @@ -729,7 +726,7 @@ paths: flagged: false flagNote: null monitoringPlan: "Watch for new maintainer activity\nWatch registry for ownership transfer" - completedAt: "2025-05-18T00:00:00Z" + completedAt: '2025-05-18T00:00:00Z' completedBy: jdoe findings: - id: 1 @@ -754,14 +751,14 @@ paths: status: done url: null notes: null - completedAt: "2025-05-20T00:00:00Z" + completedAt: '2025-05-20T00:00:00Z' - id: 2 findingId: null action: Opened PR re-enabling branch protection status: done url: null notes: null - completedAt: "2025-05-22T00:00:00Z" + completedAt: '2025-05-22T00:00:00Z' - id: 3 findingId: null action: Recommended escalation — vendor LTS or consortium fork @@ -778,14 +775,14 @@ paths: metadata: from: active to: escalated - createdAt: "2025-06-08T00:00:00Z" + createdAt: '2025-06-08T00:00:00Z' - id: 3 actorUserId: jdoe actorType: user activityType: remediation_logged - content: "Jonathan R. logged: maintainer unresponsive after 3 attempts" + content: 'Jonathan R. logged: maintainer unresponsive after 3 attempts' metadata: null - createdAt: "2025-06-01T00:00:00Z" + createdAt: '2025-06-01T00:00:00Z' - id: 2 actorUserId: jdoe actorType: user @@ -793,7 +790,7 @@ paths: content: Assessment completed — posture Critical metadata: posture: Critical - createdAt: "2025-05-18T00:00:00Z" + createdAt: '2025-05-18T00:00:00Z' - id: 1 actorUserId: null actorType: system @@ -802,7 +799,7 @@ paths: metadata: userId: jdoe role: lead - createdAt: "2025-05-15T00:00:00Z" + createdAt: '2025-05-15T00:00:00Z' '404': description: Package not found. content: @@ -910,8 +907,8 @@ paths: - userId: jdoe name: Jonathan R. role: lead - assignedAt: "2025-01-15T10:00:00Z" - lastActivityAt: "2025-06-08T14:23:00Z" + assignedAt: '2025-01-15T10:00:00Z' + lastActivityAt: '2025-06-08T14:23:00Z' lastActivityDescription: Escalated for intervention pkg:pypi/requests: null '400':