Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions backend/src/api/public/v1/akrites-external/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { SCOPES } from '@/security/scopes'

import { getAkritesExternalAdvisoryDetail } from '../packages/getAkritesExternalAdvisoryDetail'
import { getAkritesExternalAdvisoryDetailBatch } from '../packages/getAkritesExternalAdvisoryDetailBatch'
import { getAkritesExternalBlastRadiusJob } from '../packages/getAkritesExternalBlastRadiusPoll'
import { submitAkritesExternalBlastRadiusJob } from '../packages/getAkritesExternalBlastRadiusSubmit'
import { getAkritesExternalContactDetail } from '../packages/getAkritesExternalContactDetail'
import { getAkritesExternalContactDetailBatch } from '../packages/getAkritesExternalContactDetailBatch'
import { getAkritesExternalPackageDetail } from '../packages/getAkritesExternalPackageDetail'
Expand Down Expand Up @@ -38,6 +40,16 @@ export function akritesExternalRouter(): Router {
advisoriesSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalAdvisoryDetailBatch))
router.use('/advisories', advisoriesSubRouter)

// Same read:advisories scope as Advisories above (see the contract's Blast Radius
// tag). The reachability pipeline isn't built yet, so every request 501s — see
// getAkritesExternalBlastRadiusSubmit/Poll — but the route/scope wiring is real.
Comment on lines +43 to +45
const blastRadiusSubRouter = Router()
blastRadiusSubRouter.use(rateLimiter)
blastRadiusSubRouter.use(requireScopes([SCOPES.READ_PACKAGES]))
blastRadiusSubRouter.post('/jobs', safeWrap(submitAkritesExternalBlastRadiusJob))
blastRadiusSubRouter.get('/jobs/:analysisId', safeWrap(getAkritesExternalBlastRadiusJob))
router.use('/blast-radius', blastRadiusSubRouter)

// Security contacts expose contact PII (e.g. reporter emails), so the contract gates
// them behind a dedicated cdp:maintainers:read scope and explicitly forbids reaching
// them via the packages scope. That scope isn't issued by Auth0 yet, so reuse the
Expand Down
113 changes: 112 additions & 1 deletion backend/src/api/public/v1/akrites-external/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ info:


Packages, Advisories and Contacts endpoints are implemented. Blast Radius
is specced separately and not yet built.
routes exist and are wired up (auth, scopes, request validation), but the
reachability pipeline itself is not built yet — both endpoints always
respond 501 NOT_IMPLEMENTED.


TODO: scopes below (read:packages, read:stewardships) are the existing
Expand Down Expand Up @@ -45,6 +47,12 @@ tags:
it, the implementation requires read:maintainer-roles (NOT read:packages).
The response shape is still under discussion upstream (reportingMethods /
reportingGuidelines / integrationHints are reserved and always null today).
- name: Blast Radius
description: >
Advisory reachability analysis jobs (submit + poll). Requires
read:packages, reused for now (see the Advisories tag TODO). The
reachability pipeline is not built yet, so both endpoints always
respond 501 NOT_IMPLEMENTED once past auth/validation.

components:
securitySchemes:
Expand Down Expand Up @@ -574,6 +582,29 @@ components:
allOf:
- $ref: '#/components/schemas/ContactDetail'

BlastRadiusJobRequest:
type: object
required: [advisoryId]
description: >
Not yet actionable — see the Blast Radius tag. Shape mirrors the draft
contract so the route can validate input ahead of the pipeline landing.
properties:
advisoryId:
type: string
description: GHSA or CVE identifier.
example: GHSA-jf85-cpcp-j695
ecosystem:
type: string
nullable: true
example: npm
package:
type: string
nullable: true
description: Full purl (e.g. pkg:npm/lodash) or bare package name. Omit for an advisory-wide analysis.
force:
type: boolean
default: false

paths:
/akrites-external/packages/detail:
get:
Expand Down Expand Up @@ -882,3 +913,83 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/Error'

/akrites-external/blast-radius/jobs:
post:
operationId: submitBlastRadiusJob
summary: Submit a blast-radius analysis job
description: >
Not implemented — always responds 501 once the request passes
auth/validation. See the Blast Radius tag.
tags: [Blast Radius]
security:
- M2MBearer:
- read:packages
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BlastRadiusJobRequest'
responses:
'501':
description: Not implemented yet.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'400':
description: Validation error.
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: Token missing read:packages scope.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'

/akrites-external/blast-radius/jobs/{analysisId}:
get:
operationId: getBlastRadiusJob
summary: Poll a blast-radius analysis job
description: >
Not implemented — always responds 501, regardless of analysisId. See
the Blast Radius tag.
Comment on lines +965 to +966
tags: [Blast Radius]
security:
- M2MBearer:
- read:packages
parameters:
- name: analysisId
in: path
required: true
schema:
type: string
responses:
'501':
description: Not implemented yet.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Comment on lines +977 to +983
'401':
description: Missing or invalid bearer token.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: Token missing read:packages scope.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
14 changes: 14 additions & 0 deletions backend/src/api/public/v1/packages/blastRadius.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { z } from 'zod'

// Advisory-wide when package is omitted; narrowed to one package otherwise.
// Mirrors BlastRadiusJobRequest in the akrites-external draft contract.
export const blastRadiusJobRequestSchema = z.object({
advisoryId: z.string().trim().min(1),
ecosystem: z.string().trim().min(1).nullable().optional(),
package: z.string().trim().min(1).nullable().optional(),
force: z.boolean().optional().default(false),
})

export const analysisIdParamSchema = z.object({
analysisId: z.string().trim().min(1),
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Request, Response } from 'express'

import { NotImplementedError } from '@crowd/common'

import { validateOrThrow } from '@/utils/validation'

import { analysisIdParamSchema } from './blastRadius'

// No jobs are ever created (see submitAkritesExternalBlastRadiusJob), so there is
// nothing to poll yet — always 501, regardless of analysisId.
Comment on lines +9 to +10
export async function getAkritesExternalBlastRadiusJob(
req: Request,
_res: Response,
): Promise<void> {
validateOrThrow(analysisIdParamSchema, req.params)

throw new NotImplementedError('Blast-radius analysis is not implemented yet')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Request, Response } from 'express'

import { NotImplementedError } from '@crowd/common'

import { validateOrThrow } from '@/utils/validation'

import { blastRadiusJobRequestSchema } from './blastRadius'

// The reachability pipeline isn't built yet — see the Blast Radius section of
// the akrites-external draft contract. Still validate the request shape so
// callers get a real 400 for malformed input rather than a blanket 501.
export async function submitAkritesExternalBlastRadiusJob(
req: Request,
_res: Response,
): Promise<void> {
validateOrThrow(blastRadiusJobRequestSchema, req.body)

throw new NotImplementedError('Blast-radius analysis is not implemented yet')
}
9 changes: 9 additions & 0 deletions services/libs/common/src/errors/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ export class ConflictError extends HttpError {
}
}

export class NotImplementedError extends HttpError {
readonly code = 'NOT_IMPLEMENTED'
readonly status = 501

constructor(message = 'Not implemented') {
super(message)
}
}

export class RateLimitError extends HttpError {
readonly code = 'RATE_LIMITED'
readonly status = 429
Expand Down
2 changes: 2 additions & 0 deletions services/libs/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
InsufficientScopeError,
InternalError,
NotFoundError,
NotImplementedError,
RateLimitError,
UnauthorizedError,
} from './errors/http'
Expand Down Expand Up @@ -65,6 +66,7 @@ export {
ConflictError,
RateLimitError,
InternalError,
NotImplementedError,

// Application errors
ApplicationError,
Expand Down
Loading