Skip to content

Commit 5a50a56

Browse files
authored
feat: add blast radius methodlogy (CM-1328) (#4377)
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 678b486 commit 5a50a56

31 files changed

Lines changed: 3897 additions & 54 deletions

backend/.env.dist.composed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,4 @@ CROWD_PACKAGES_PACKAGIST_MAILTO=
4141

4242
# security-contacts-worker
4343
SECURITY_CONTACTS_USER_AGENT="lfx-security-contacts-worker"
44+
CROWD_PACKAGES_TEMPORAL_SERVER_URL=temporal:7233

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ import { getAkritesExternalContactDetail } from '../packages/getAkritesExternalC
1111
import { getAkritesExternalContactDetailBatch } from '../packages/getAkritesExternalContactDetailBatch'
1212
import { getAkritesExternalPackageDetail } from '../packages/getAkritesExternalPackageDetail'
1313
import { getAkritesExternalPackageDetailBatch } from '../packages/getAkritesExternalPackageDetailBatch'
14+
import { getBlastRadiusJob } from '../packages/getBlastRadiusJob'
1415
import { submitBlastRadiusJob } from '../packages/submitBlastRadiusJob'
1516

1617
const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 })
1718

1819
// Blast-radius jobs kick off a Temporal workflow per request, so they get their own,
19-
// much stricter limiter — configurable via env so it can be tuned without a redeploy
20-
// while the pipeline is still a no-op stub. Defaults to 5 requests/hour.
20+
// much stricter limiter — configurable via env so it can be tuned without a redeploy.
21+
// Defaults to 5 requests/hour.
2122
const blastRadiusRateLimitMax = Number(process.env.AKRITES_BLAST_RADIUS_RATE_LIMIT_MAX)
2223
const blastRadiusRateLimitWindowMs = Number(process.env.AKRITES_BLAST_RADIUS_RATE_LIMIT_WINDOW_MS)
2324

@@ -72,9 +73,9 @@ export function akritesExternalRouter(): Router {
7273
// (same as advisories above — see the scope-naming note in the akrites-external
7374
// OpenAPI). Not issued by Auth0 yet, so reuse READ_PACKAGES for now.
7475
const blastRadiusSubRouter = Router()
75-
blastRadiusSubRouter.use(blastRadiusRateLimiter)
7676
blastRadiusSubRouter.use(requireScopes([SCOPES.READ_PACKAGES]))
77-
blastRadiusSubRouter.post('/jobs', safeWrap(submitBlastRadiusJob))
77+
blastRadiusSubRouter.post('/jobs', blastRadiusRateLimiter, safeWrap(submitBlastRadiusJob))
78+
blastRadiusSubRouter.get('/jobs/:analysisId', rateLimiter, safeWrap(getBlastRadiusJob))
7879
router.use('/blast-radius', blastRadiusSubRouter)
7980

8081
return router

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

Lines changed: 185 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ info:
1010
1111
1212
Packages, Advisories and Contacts endpoints are implemented. Blast Radius
13-
submit (2a) is implemented but the reachability pipeline is not built yet —
14-
every submitted job currently fails with an ECOSYSTEM_NOT_SUPPORTED error
15-
once the underlying workflow runs. Poll (2b), the 7-day result cache, and
16-
the actual analysis are specced separately and not yet built.
13+
submit (2a) and poll (2b) are both implemented, backed by a 4-stage
14+
Temporal pipeline (intel, dependents, reachability, report) for npm
15+
packages; other ecosystems fail fast with ECOSYSTEM_NOT_SUPPORTED. The
16+
7-day result cache is specced separately and not yet built.
1717
1818
1919
TODO: scopes below (read:packages, read:stewardships) are the existing
@@ -50,11 +50,12 @@ tags:
5050
reportingGuidelines / integrationHints are reserved and always null today).
5151
- name: Blast Radius
5252
description: >
53-
Advisory reachability analysis — submit (2a) is implemented; poll (2b) is
54-
not built yet. Submitting kicks off a Temporal workflow which currently
55-
fails every job with ECOSYSTEM_NOT_SUPPORTED, since the reachability
56-
pipeline itself hasn't shipped. Rate-limited independently of the other
57-
akrites-external endpoints (default 5 requests/hour, configurable via
53+
Advisory reachability analysis — submit (2a) and poll (2b) are both
54+
implemented. Submitting kicks off a Temporal workflow that runs the
55+
npm reachability pipeline (other ecosystems fail fast with
56+
ECOSYSTEM_NOT_SUPPORTED); poll returns job status and, once done,
57+
results. Rate-limited independently of the other akrites-external
58+
endpoints (default 5 requests/hour, configurable via
5859
AKRITES_BLAST_RADIUS_RATE_LIMIT_MAX / _WINDOW_MS). Same interim scope
5960
note as Advisories applies (read:packages, pending a dedicated
6061
read:advisories scope).
@@ -412,7 +413,7 @@ components:
412413
properties:
413414
analysisId:
414415
type: string
415-
description: Opaque identifier for polling via 2b (not yet implemented).
416+
description: Opaque identifier for polling via 2b (GET .../jobs/{analysisId}).
416417
advisoryId:
417418
type: string
418419
package:
@@ -426,10 +427,118 @@ components:
426427
status:
427428
type: string
428429
enum: [pending, running, done, failed]
429-
description: >
430-
Always pending today — every job starts a Temporal workflow that
431-
currently fails with ECOSYSTEM_NOT_SUPPORTED once it runs, since
432-
the reachability pipeline isn't built yet.
430+
description: Always pending — the response is returned before the Temporal workflow runs.
431+
432+
BlastRadiusResultConfidence:
433+
type: string
434+
enum: [high, medium, low]
435+
description: Crosswalk of the 0-1 reachability confidence score into bands (>=0.8 high, >=0.4 medium, else low).
436+
437+
BlastRadiusResultItem:
438+
type: object
439+
required: [dependent, affected, verdict, confidence, evidence, downloadsLast30Days]
440+
properties:
441+
dependent:
442+
type: string
443+
description: Purl of the analyzed dependent package.
444+
affected:
445+
type: boolean
446+
description: True only when verdict is 'affected'. Kept for backwards compatibility — prefer verdict, since this field can't distinguish 'not_affected' from 'unclear'.
447+
verdict:
448+
type: string
449+
enum: [affected, not_affected, unclear]
450+
description: Raw reachability verdict. 'unclear' covers both a genuine ambiguous read and a persistent agent failure.
451+
confidence:
452+
$ref: '#/components/schemas/BlastRadiusResultConfidence'
453+
evidence:
454+
type: string
455+
nullable: true
456+
description: Flattened file:line — snippet evidence lines from the reachability agent, one per line.
457+
downloadsLast30Days:
458+
type: string
459+
nullable: true
460+
461+
BlastRadiusAnalysisSummary:
462+
type: object
463+
required:
464+
- totalDependentsInRange
465+
- dependentsExcludedUpfront
466+
- dependentsAnalyzed
467+
- dependentsAffected
468+
- affectedPercentage
469+
- affectedDependents
470+
properties:
471+
totalDependentsInRange:
472+
type: integer
473+
description: Sum of dependentsAnalyzed and dependentsExcludedUpfront; the count of dependents that fell within the vulnerable version range (before top-N cutoff was applied).
474+
dependentsExcludedUpfront:
475+
type: integer
476+
description: totalDependentsInRange minus dependentsAnalyzed.
477+
dependentsAnalyzed:
478+
type: integer
479+
description: Number of dependents that received a reachability verdict.
480+
dependentsAffected:
481+
type: integer
482+
affectedPercentage:
483+
type: number
484+
nullable: true
485+
description: Rounded to 1 decimal. Null when dependentsAnalyzed is 0, to avoid a misleading 0%.
486+
affectedDependents:
487+
type: array
488+
items:
489+
type: string
490+
491+
BlastRadiusAnalysis:
492+
type: object
493+
required:
494+
- analysisId
495+
- status
496+
- advisoryId
497+
- package
498+
- ecosystem
499+
- submittedAt
500+
- completedAt
501+
- errorMessage
502+
- summary
503+
- results
504+
description: Response body of 2b — poll for job status and, once done, results.
505+
properties:
506+
analysisId:
507+
type: string
508+
status:
509+
type: string
510+
enum: [pending, running, done, failed]
511+
advisoryId:
512+
type: string
513+
package:
514+
type: string
515+
nullable: true
516+
ecosystem:
517+
type: string
518+
enum: [npm]
519+
submittedAt:
520+
type: string
521+
nullable: true
522+
format: date-time
523+
completedAt:
524+
type: string
525+
nullable: true
526+
format: date-time
527+
errorMessage:
528+
type: string
529+
nullable: true
530+
summary:
531+
type: object
532+
nullable: true
533+
allOf:
534+
- $ref: '#/components/schemas/BlastRadiusAnalysisSummary'
535+
description: Null until status is 'done'.
536+
results:
537+
type: array
538+
nullable: true
539+
description: Null until status is 'done'.
540+
items:
541+
$ref: '#/components/schemas/BlastRadiusResultItem'
433542

434543
ContactConfidenceBand:
435544
type: string
@@ -883,12 +992,13 @@ paths:
883992
description: >
884993
Always exactly one job per request — no bulk submit. Omit package for
885994
an advisory-wide analysis; provide it to narrow to a single package.
995+
Starts a Temporal workflow running the 4-stage reachability pipeline
996+
(intel, dependents, reachability, report) for npm; other ecosystems
997+
fail fast with ECOSYSTEM_NOT_SUPPORTED. Poll status/results via
998+
GET /jobs/{analysisId}.
886999
8871000
888-
Not yet implemented: the 7-day result cache, force-bypass semantics,
889-
and the actual reachability pipeline. Today every submission starts a
890-
fresh Temporal workflow that fails with ECOSYSTEM_NOT_SUPPORTED — the
891-
job is accepted and echoed back as pending, but never completes.
1001+
Not yet implemented: the 7-day result cache and force-bypass semantics.
8921002
tags: [Blast Radius]
8931003
security:
8941004
- M2MBearer:
@@ -935,6 +1045,63 @@ paths:
9351045
schema:
9361046
$ref: '#/components/schemas/Error'
9371047

1048+
/akrites-external/blast-radius/jobs/{analysisId}:
1049+
get:
1050+
operationId: getBlastRadiusJob
1051+
summary: 2b — Poll a blast-radius analysis job
1052+
description: >
1053+
Returns job status and, once status is 'done', the summary and
1054+
per-dependent results. summary and results are null for any other
1055+
status.
1056+
tags: [Blast Radius]
1057+
security:
1058+
- M2MBearer:
1059+
- read:packages
1060+
parameters:
1061+
- name: analysisId
1062+
in: path
1063+
required: true
1064+
schema:
1065+
type: string
1066+
format: uuid
1067+
responses:
1068+
'200':
1069+
description: Job status (and results, once done).
1070+
content:
1071+
application/json:
1072+
schema:
1073+
$ref: '#/components/schemas/BlastRadiusAnalysis'
1074+
'400':
1075+
description: Malformed analysisId (not a UUID).
1076+
content:
1077+
application/json:
1078+
schema:
1079+
$ref: '#/components/schemas/Error'
1080+
'401':
1081+
description: Missing or invalid bearer token.
1082+
content:
1083+
application/json:
1084+
schema:
1085+
$ref: '#/components/schemas/Error'
1086+
'403':
1087+
description: Token missing read:packages scope.
1088+
content:
1089+
application/json:
1090+
schema:
1091+
$ref: '#/components/schemas/Error'
1092+
'404':
1093+
description: No job with this analysisId.
1094+
content:
1095+
application/json:
1096+
schema:
1097+
$ref: '#/components/schemas/Error'
1098+
'429':
1099+
description: Too many requests — rate-limited independently of the other endpoints.
1100+
content:
1101+
application/json:
1102+
schema:
1103+
$ref: '#/components/schemas/Error'
1104+
9381105
/akrites-external/contacts/detail:
9391106
get:
9401107
operationId: getContactDetail

0 commit comments

Comments
 (0)