Skip to content

Commit aab51a3

Browse files
authored
feat: add security contacts api (CM-1336) (#4354)
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 50de99b commit aab51a3

13 files changed

Lines changed: 894 additions & 32 deletions

backend/src/api/public/v1/akrites-external/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { SCOPES } from '@/security/scopes'
77

88
import { getAkritesExternalAdvisoryDetail } from '../packages/getAkritesExternalAdvisoryDetail'
99
import { getAkritesExternalAdvisoryDetailBatch } from '../packages/getAkritesExternalAdvisoryDetailBatch'
10+
import { getAkritesExternalContactDetail } from '../packages/getAkritesExternalContactDetail'
11+
import { getAkritesExternalContactDetailBatch } from '../packages/getAkritesExternalContactDetailBatch'
1012
import { getAkritesExternalPackageDetail } from '../packages/getAkritesExternalPackageDetail'
1113
import { getAkritesExternalPackageDetailBatch } from '../packages/getAkritesExternalPackageDetailBatch'
1214

@@ -36,5 +38,17 @@ export function akritesExternalRouter(): Router {
3638
advisoriesSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalAdvisoryDetailBatch))
3739
router.use('/advisories', advisoriesSubRouter)
3840

41+
// Security contacts expose contact PII (e.g. reporter emails), so the contract gates
42+
// them behind a dedicated cdp:maintainers:read scope and explicitly forbids reaching
43+
// them via the packages scope. That scope isn't issued by Auth0 yet, so reuse the
44+
// closest issued one — READ_MAINTAINER_ROLES (maintainer data) — NOT READ_PACKAGES.
45+
// TODO: swap for cdp:maintainers:read once issued.
46+
const contactsSubRouter = Router()
47+
contactsSubRouter.use(rateLimiter)
48+
contactsSubRouter.use(requireScopes([SCOPES.READ_MAINTAINER_ROLES]))
49+
contactsSubRouter.get('/detail', safeWrap(getAkritesExternalContactDetail))
50+
contactsSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalContactDetailBatch))
51+
router.use('/contacts', contactsSubRouter)
52+
3953
return router
4054
}

backend/src/api/public/v1/akrites-external/openapi.yaml

Lines changed: 324 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ info:
99
entirely between Akrites and Auth0.
1010
1111
12-
Packages and Advisories endpoints are implemented. Blast Radius and
13-
Contacts are specced separately and not yet built.
12+
Packages, Advisories and Contacts endpoints are implemented. Blast Radius
13+
is specced separately and not yet built.
1414
1515
1616
TODO: scopes below (read:packages, read:stewardships) are the existing
@@ -37,6 +37,14 @@ tags:
3737
issues it, the implementation reuses read:packages (advisories need no
3838
stewardship read). Confirm the final scope name (read:advisories vs
3939
cdp:advisories:read) with Akrites/product.
40+
- name: Contacts
41+
description: >
42+
Security contacts for a package — includes contact PII (e.g. reporter
43+
emails). The contract gates these behind a dedicated cdp:maintainers:read
44+
scope and forbids reaching them via the packages scope; until Auth0 issues
45+
it, the implementation requires read:maintainer-roles (NOT read:packages).
46+
The response shape is still under discussion upstream (reportingMethods /
47+
reportingGuidelines / integrationHints are reserved and always null today).
4048
4149
components:
4250
securitySchemes:
@@ -55,6 +63,7 @@ components:
5563
scopes:
5664
read:packages: Read package detail
5765
read:stewardships: Read package stewardship data
66+
read:maintainer-roles: Read security contacts (interim scope for Contacts; see the Contacts tag)
5867

5968
schemas:
6069
Error:
@@ -356,6 +365,215 @@ components:
356365
allOf:
357366
- $ref: '#/components/schemas/AdvisoryDetail'
358367

368+
ContactConfidenceBand:
369+
type: string
370+
enum: [PRIMARY, SECONDARY, FALLBACK, NONE]
371+
description: Per-contact confidence — mirrors the internal SecurityContact confidence enum.
372+
373+
OverallConfidenceBand:
374+
type: string
375+
enum: [HIGH, MEDIUM, LOW, NONE]
376+
description: >
377+
Aggregate confidence, derived from the highest-scoring contact.
378+
UNCONFIRMED CROSSWALK: the internal aggregate band is
379+
PRIMARY/SECONDARY/FALLBACK/NONE; this HIGH/MEDIUM/LOW/NONE scale is a
380+
4→4 tier rename (PRIMARY→HIGH, …) pending confirmation with Akrites —
381+
see OVERALL_CONFIDENCE_CROSSWALK in akritesExternalContactDetail.ts.
382+
383+
SecurityContact:
384+
type: object
385+
required: [channel, value, role, confidenceBand, confidenceScore]
386+
properties:
387+
channel:
388+
type: string
389+
enum: [email, github-pvr, github-handle, url, web-form]
390+
description: Mirrors the internal SecurityContact.channel enum; passed through verbatim.
391+
value:
392+
type: string
393+
example: security@example.org
394+
role:
395+
type: string
396+
enum: [security-team, maintainer, admin, committer, org-owner]
397+
description: Mirrors the internal SecurityContact.role enum; passed through verbatim.
398+
confidenceBand:
399+
$ref: '#/components/schemas/ContactConfidenceBand'
400+
confidenceScore:
401+
type: number
402+
format: float
403+
minimum: 0
404+
maximum: 1
405+
406+
ReportingMethod:
407+
type: object
408+
nullable: true
409+
description: Reserved shape — always null until SECURITY.md parsing ships.
410+
required: [methodId, type, status, description, actionRequired]
411+
properties:
412+
methodId:
413+
type: integer
414+
type:
415+
type: string
416+
status:
417+
type: string
418+
enum: [preferred, fallback]
419+
recipient:
420+
type: string
421+
nullable: true
422+
description:
423+
type: string
424+
actionRequired:
425+
type: string
426+
427+
ReportingGuidelines:
428+
type: object
429+
nullable: true
430+
description: Reserved shape — always null until SECURITY.md parsing ships.
431+
required: [generalPrinciples, qualityRequirements]
432+
properties:
433+
generalPrinciples:
434+
type: array
435+
items:
436+
type: string
437+
qualityRequirements:
438+
type: object
439+
required: [avoid, recommend]
440+
properties:
441+
avoid:
442+
type: array
443+
items:
444+
type: string
445+
recommend:
446+
type: array
447+
items:
448+
type: object
449+
required: [scenario, action]
450+
properties:
451+
scenario:
452+
type: string
453+
action:
454+
type: string
455+
456+
IntegrationHints:
457+
type: object
458+
nullable: true
459+
description: Reserved shape — always null until computed.
460+
required: [primaryEndpoint, fallbackEndpoint, validationHints]
461+
properties:
462+
primaryEndpoint:
463+
type: object
464+
nullable: true
465+
required: [type, target]
466+
properties:
467+
type:
468+
type: string
469+
target:
470+
type: string
471+
fallbackEndpoint:
472+
type: object
473+
nullable: true
474+
required: [type, target]
475+
properties:
476+
type:
477+
type: string
478+
target:
479+
type: string
480+
validationHints:
481+
type: object
482+
nullable: true
483+
required: [falsePositiveRules, requirePocForSeverity, debounce]
484+
properties:
485+
falsePositiveRules:
486+
type: array
487+
items:
488+
type: string
489+
requirePocForSeverity:
490+
type: array
491+
items:
492+
type: string
493+
debounce:
494+
type: string
495+
nullable: true
496+
497+
ContactDetail:
498+
type: object
499+
description: >
500+
NOTE: shape not final — still under discussion upstream, in particular
501+
how reportingMethods / reportingGuidelines / integrationHints will be
502+
derived from SECURITY.md parsing. Those three, plus targetOrganizationName
503+
and bugBountyProgramFlag, are always null today (not stored). Expect this
504+
schema to change before the contract is frozen.
505+
required:
506+
- purl
507+
- name
508+
- ecosystem
509+
- overallConfidenceBand
510+
- contacts
511+
- securityPolicyUrl
512+
- vulnerabilityReportingUrl
513+
- bugBountyUrl
514+
- pvrEnabled
515+
- targetOrganizationName
516+
- bugBountyProgramFlag
517+
- reportingMethods
518+
- reportingGuidelines
519+
- integrationHints
520+
properties:
521+
purl:
522+
type: string
523+
name:
524+
type: string
525+
ecosystem:
526+
$ref: '#/components/schemas/Ecosystem'
527+
overallConfidenceBand:
528+
$ref: '#/components/schemas/OverallConfidenceBand'
529+
contacts:
530+
type: array
531+
items:
532+
$ref: '#/components/schemas/SecurityContact'
533+
securityPolicyUrl:
534+
type: string
535+
nullable: true
536+
vulnerabilityReportingUrl:
537+
type: string
538+
nullable: true
539+
bugBountyUrl:
540+
type: string
541+
nullable: true
542+
pvrEnabled:
543+
type: boolean
544+
nullable: true
545+
targetOrganizationName:
546+
type: string
547+
nullable: true
548+
description: Reserved — not stored today, always null.
549+
bugBountyProgramFlag:
550+
type: boolean
551+
nullable: true
552+
description: Reserved — not stored today, always null.
553+
reportingMethods:
554+
type: array
555+
nullable: true
556+
items:
557+
$ref: '#/components/schemas/ReportingMethod'
558+
reportingGuidelines:
559+
$ref: '#/components/schemas/ReportingGuidelines'
560+
integrationHints:
561+
$ref: '#/components/schemas/IntegrationHints'
562+
563+
ContactDetailBulkEntry:
564+
type: object
565+
required: [requestedPurl, found, contact]
566+
properties:
567+
requestedPurl:
568+
type: string
569+
found:
570+
type: boolean
571+
contact:
572+
type: object
573+
nullable: true
574+
allOf:
575+
- $ref: '#/components/schemas/ContactDetail'
576+
359577
paths:
360578
/akrites-external/packages/detail:
361579
get:
@@ -560,3 +778,107 @@ paths:
560778
application/json:
561779
schema:
562780
$ref: '#/components/schemas/Error'
781+
782+
/akrites-external/contacts/detail:
783+
get:
784+
operationId: getContactDetail
785+
summary: Get security contact detail by PURL
786+
description: >
787+
Requires read:maintainer-roles (interim for the contract's cdp:maintainers:read).
788+
Never reachable with the packages scope.
789+
tags: [Contacts]
790+
security:
791+
- M2MBearer:
792+
- read:maintainer-roles
793+
parameters:
794+
- name: purl
795+
in: query
796+
required: true
797+
schema:
798+
type: string
799+
example: pkg:npm/%40angular/core
800+
responses:
801+
'200':
802+
description: Security contact detail (empty contacts + NONE band when the package has none).
803+
content:
804+
application/json:
805+
schema:
806+
$ref: '#/components/schemas/ContactDetail'
807+
'400':
808+
description: Malformed purl.
809+
content:
810+
application/json:
811+
schema:
812+
$ref: '#/components/schemas/Error'
813+
'401':
814+
description: Missing or invalid bearer token.
815+
content:
816+
application/json:
817+
schema:
818+
$ref: '#/components/schemas/Error'
819+
'403':
820+
description: Token missing read:maintainer-roles scope.
821+
content:
822+
application/json:
823+
schema:
824+
$ref: '#/components/schemas/Error'
825+
'404':
826+
description: Package not found.
827+
content:
828+
application/json:
829+
schema:
830+
$ref: '#/components/schemas/Error'
831+
832+
/akrites-external/contacts/detail:batch:
833+
post:
834+
operationId: getContactDetailBatch
835+
summary: Bulk security contact lookup
836+
tags: [Contacts]
837+
security:
838+
- M2MBearer:
839+
- read:maintainer-roles
840+
requestBody:
841+
required: true
842+
content:
843+
application/json:
844+
schema:
845+
type: object
846+
required: [purls]
847+
properties:
848+
purls:
849+
type: array
850+
minItems: 1
851+
maxItems: 100
852+
items:
853+
type: string
854+
responses:
855+
'200':
856+
description: Results in the same order as the request.
857+
content:
858+
application/json:
859+
schema:
860+
type: object
861+
required: [results]
862+
properties:
863+
results:
864+
type: array
865+
items:
866+
$ref: '#/components/schemas/ContactDetailBulkEntry'
867+
'400':
868+
description: Validation error (empty array, >100 items, malformed purl).
869+
content:
870+
application/json:
871+
schema:
872+
$ref: '#/components/schemas/Error'
873+
'401':
874+
description: Missing or invalid bearer token.
875+
content:
876+
application/json:
877+
schema:
878+
$ref: '#/components/schemas/Error'
879+
'403':
880+
description: Token missing read:maintainer-roles scope.
881+
content:
882+
application/json:
883+
schema:
884+
$ref: '#/components/schemas/Error'

0 commit comments

Comments
 (0)