Skip to content

Commit cebf94d

Browse files
committed
feat: add packages api
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 36b4ed4 commit cebf94d

17 files changed

Lines changed: 1131 additions & 57 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Router } from 'express'
2+
3+
import { createRateLimiter } from '@/api/apiRateLimiter'
4+
import { requireScopes } from '@/api/public/middlewares/requireScopes'
5+
import { safeWrap } from '@/middlewares/errorMiddleware'
6+
import { SCOPES } from '@/security/scopes'
7+
8+
import { getAkritesExternalPackageDetail } from '../packages/getAkritesExternalPackageDetail'
9+
import { getAkritesExternalPackageDetailBatch } from '../packages/getAkritesExternalPackageDetailBatch'
10+
11+
const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 })
12+
13+
export function akritesExternalRouter(): Router {
14+
const router = Router()
15+
16+
// TODO: swap for a dedicated cdp:packages:read scope once Akrites gets its own
17+
// Auth0 M2M scopes (per the akrites-external draft contract) — reusing the
18+
// internal CDP UI scopes for now since that's what's actually issued today.
19+
const packagesSubRouter = Router()
20+
packagesSubRouter.use(rateLimiter)
21+
packagesSubRouter.use(requireScopes([SCOPES.READ_PACKAGES, SCOPES.READ_STEWARDSHIPS], 'all'))
22+
packagesSubRouter.get('/detail', safeWrap(getAkritesExternalPackageDetail))
23+
packagesSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalPackageDetailBatch))
24+
router.use('/packages', packagesSubRouter)
25+
26+
return router
27+
}
Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
openapi: 3.0.3
2+
info:
3+
title: CDP → Akrites External API
4+
version: 0.1.0
5+
description: >
6+
Read-only external API exposing CDP package security data to the Akrites
7+
service. Authenticated via Auth0 M2M client-credentials — CDP only
8+
verifies the resulting access token; the assertion exchange happens
9+
entirely between Akrites and Auth0.
10+
11+
12+
Only the Packages endpoints are implemented so far. Advisories, Blast
13+
Radius, and Contacts are specced separately and not yet built.
14+
15+
16+
TODO: scopes below (read:packages, read:stewardships) are the existing
17+
internal CDP UI scopes, reused here for now. Swap for a dedicated
18+
cdp:packages:read scope once Akrites gets its own Auth0 M2M scopes per
19+
the akrites-external draft contract.
20+
21+
servers:
22+
- url: https://cm.lfx.dev/api/v1
23+
description: Production
24+
25+
security:
26+
- M2MBearer:
27+
- read:packages
28+
- read:stewardships
29+
30+
tags:
31+
- name: Packages
32+
description: Package detail — requires read:packages and read:stewardships (see TODO above).
33+
34+
components:
35+
securitySchemes:
36+
M2MBearer:
37+
type: http
38+
scheme: bearer
39+
bearerFormat: JWT
40+
41+
schemas:
42+
Error:
43+
type: object
44+
required: [error]
45+
properties:
46+
error:
47+
type: object
48+
required: [code, message]
49+
properties:
50+
code:
51+
type: string
52+
example: BAD_REQUEST
53+
message:
54+
type: string
55+
56+
Ecosystem:
57+
type: string
58+
example: npm
59+
60+
HealthBand:
61+
type: string
62+
enum: [critical, low, medium, high]
63+
description: >
64+
UNCONFIRMED CROSSWALK — the internal health scale is
65+
excellent/healthy/fair/concerning/critical (good→bad); there is no
66+
agreed mapping to this critical/low/medium/high scale yet. See
67+
HEALTH_BAND_CROSSWALK in akritesExternalPackageDetail.ts. Confirm
68+
with Akrites/product before relying on exact band values.
69+
70+
PackageDetail:
71+
type: object
72+
required:
73+
- purl
74+
- name
75+
- ecosystem
76+
- latestVersion
77+
- versionCount
78+
- health
79+
- impact
80+
- riskSignals
81+
- security
82+
- provenance
83+
- supplyChainIntegrity
84+
properties:
85+
purl:
86+
type: string
87+
name:
88+
type: string
89+
ecosystem:
90+
$ref: '#/components/schemas/Ecosystem'
91+
latestVersion:
92+
type: string
93+
nullable: true
94+
versionCount:
95+
type: integer
96+
nullable: true
97+
health:
98+
type: object
99+
required: [score, band, subScores, signalCoverageHealth]
100+
properties:
101+
score:
102+
type: integer
103+
nullable: true
104+
minimum: 0
105+
maximum: 100
106+
band:
107+
$ref: '#/components/schemas/HealthBand'
108+
subScores:
109+
type: object
110+
required: [maintainerHealth, securitySupplyChain, developmentActivity]
111+
properties:
112+
maintainerHealth:
113+
type: number
114+
nullable: true
115+
securitySupplyChain:
116+
type: number
117+
nullable: true
118+
developmentActivity:
119+
type: number
120+
nullable: true
121+
signalCoverageHealth:
122+
type: object
123+
nullable: true
124+
additionalProperties: true
125+
impact:
126+
type: object
127+
required: [score, downloadsLast30Days, dependentPackagesCount, dependentReposCount, transitiveReach]
128+
properties:
129+
score:
130+
type: integer
131+
nullable: true
132+
minimum: 0
133+
maximum: 100
134+
downloadsLast30Days:
135+
type: string
136+
nullable: true
137+
description: Raw registry count string, not normalized.
138+
dependentPackagesCount:
139+
type: integer
140+
nullable: true
141+
dependentReposCount:
142+
type: integer
143+
nullable: true
144+
transitiveReach:
145+
type: integer
146+
nullable: true
147+
description: Always null for now — reserved for future computation.
148+
riskSignals:
149+
type: object
150+
required:
151+
- lifecycle
152+
- maintainerBusFactor
153+
- lastReleaseAt
154+
- hasSecurityFile
155+
- hasSecurityPolicy
156+
- branchProtectionEnabled
157+
- openssfScorecardScore
158+
properties:
159+
lifecycle:
160+
type: string
161+
enum: [active, inactive, deprecated, abandoned, null]
162+
nullable: true
163+
description: >
164+
UNCONFIRMED CROSSWALK from the internal
165+
active/stable/declining/abandoned/archived scale — see
166+
LIFECYCLE_CROSSWALK in akritesExternalPackageDetail.ts.
167+
maintainerBusFactor:
168+
type: integer
169+
nullable: true
170+
lastReleaseAt:
171+
type: string
172+
format: date-time
173+
nullable: true
174+
hasSecurityFile:
175+
type: boolean
176+
nullable: true
177+
hasSecurityPolicy:
178+
type: boolean
179+
nullable: true
180+
branchProtectionEnabled:
181+
type: boolean
182+
nullable: true
183+
openssfScorecardScore:
184+
type: number
185+
format: float
186+
minimum: 0
187+
maximum: 10
188+
nullable: true
189+
security:
190+
type: object
191+
required: [securityPolicyUrl, vulnerabilityReportingUrl, bugBountyUrl, pvrEnabled, criticalVulnerabilityFlag]
192+
properties:
193+
securityPolicyUrl:
194+
type: string
195+
nullable: true
196+
vulnerabilityReportingUrl:
197+
type: string
198+
nullable: true
199+
bugBountyUrl:
200+
type: string
201+
nullable: true
202+
pvrEnabled:
203+
type: boolean
204+
nullable: true
205+
criticalVulnerabilityFlag:
206+
type: boolean
207+
nullable: true
208+
provenance:
209+
type: object
210+
required: [resolvedRepositoryUrl, declaredRepositoryUrl, mappingConfidenceScore, mappingConfidenceLabel, lastCommitAt]
211+
properties:
212+
resolvedRepositoryUrl:
213+
type: string
214+
nullable: true
215+
description: >
216+
From the confidence-ranked package_repos → repos join — the
217+
repo CDP has actually resolved and enriched (scorecard,
218+
branch protection, etc.).
219+
declaredRepositoryUrl:
220+
type: string
221+
nullable: true
222+
description: Raw repository URL as declared in the package's own metadata.
223+
mappingConfidenceScore:
224+
type: number
225+
format: float
226+
minimum: 0
227+
maximum: 1
228+
nullable: true
229+
mappingConfidenceLabel:
230+
type: string
231+
enum: [High, Medium, Low, null]
232+
nullable: true
233+
lastCommitAt:
234+
type: string
235+
format: date-time
236+
nullable: true
237+
supplyChainIntegrity:
238+
type: object
239+
required: [buildProvenance, signedReleases]
240+
properties:
241+
buildProvenance:
242+
type: string
243+
nullable: true
244+
description: Always null for now — reserved.
245+
signedReleases:
246+
type: string
247+
nullable: true
248+
description: Always null for now — reserved.
249+
250+
PackageDetailBulkEntry:
251+
type: object
252+
required: [requestedPurl, found, package]
253+
properties:
254+
requestedPurl:
255+
type: string
256+
found:
257+
type: boolean
258+
package:
259+
type: object
260+
nullable: true
261+
allOf:
262+
- $ref: '#/components/schemas/PackageDetail'
263+
264+
paths:
265+
/akrites-external/packages/detail:
266+
get:
267+
operationId: getPackageDetail
268+
summary: Get package detail by PURL
269+
tags: [Packages]
270+
security:
271+
- M2MBearer:
272+
- read:packages
273+
- read:stewardships
274+
parameters:
275+
- name: purl
276+
in: query
277+
required: true
278+
schema:
279+
type: string
280+
example: pkg:npm/%40angular/core
281+
responses:
282+
'200':
283+
description: Package detail.
284+
content:
285+
application/json:
286+
schema:
287+
$ref: '#/components/schemas/PackageDetail'
288+
'400':
289+
description: Malformed purl.
290+
content:
291+
application/json:
292+
schema:
293+
$ref: '#/components/schemas/Error'
294+
'401':
295+
description: Missing or invalid bearer token.
296+
content:
297+
application/json:
298+
schema:
299+
$ref: '#/components/schemas/Error'
300+
'403':
301+
description: Token missing read:packages or read:stewardships scope.
302+
content:
303+
application/json:
304+
schema:
305+
$ref: '#/components/schemas/Error'
306+
'404':
307+
description: Package not found.
308+
content:
309+
application/json:
310+
schema:
311+
$ref: '#/components/schemas/Error'
312+
313+
/akrites-external/packages/detail:batch:
314+
post:
315+
operationId: getPackageDetailBatch
316+
summary: Bulk package detail lookup
317+
tags: [Packages]
318+
security:
319+
- M2MBearer:
320+
- read:packages
321+
- read:stewardships
322+
requestBody:
323+
required: true
324+
content:
325+
application/json:
326+
schema:
327+
type: object
328+
required: [purls]
329+
properties:
330+
purls:
331+
type: array
332+
minItems: 1
333+
maxItems: 100
334+
items:
335+
type: string
336+
responses:
337+
'200':
338+
description: Results in the same order as the request.
339+
content:
340+
application/json:
341+
schema:
342+
type: object
343+
required: [results]
344+
properties:
345+
results:
346+
type: array
347+
items:
348+
$ref: '#/components/schemas/PackageDetailBulkEntry'
349+
'400':
350+
description: Validation error (empty array, >100 items, malformed purl).
351+
content:
352+
application/json:
353+
schema:
354+
$ref: '#/components/schemas/Error'
355+
'401':
356+
description: Missing or invalid bearer token.
357+
content:
358+
application/json:
359+
schema:
360+
$ref: '#/components/schemas/Error'
361+
'403':
362+
description: Token missing read:packages or read:stewardships scope.
363+
content:
364+
application/json:
365+
schema:
366+
$ref: '#/components/schemas/Error'

0 commit comments

Comments
 (0)