Skip to content

Commit a580ec2

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix/osv-advisory-dedup-CM-1258
2 parents a9b7a98 + 8a7f246 commit a580ec2

111 files changed

Lines changed: 11049 additions & 336 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/.env.dist.composed

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,9 @@ CROWD_PACKAGES_DB_USERNAME=postgres
3636
CROWD_PACKAGES_DB_PASSWORD=example
3737
CROWD_PACKAGES_DB_DATABASE=packages-db
3838

39+
# Packagist registry crawler contact email
40+
CROWD_PACKAGES_PACKAGIST_MAILTO=
41+
3942
# security-contacts-worker
4043
SECURITY_CONTACTS_USER_AGENT="lfx-security-contacts-worker"
44+
CROWD_PACKAGES_TEMPORAL_SERVER_URL=temporal:7233

backend/.env.dist.local

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ CROWD_PACKAGES_DB_USERNAME=postgres
180180
CROWD_PACKAGES_DB_PASSWORD=example
181181
CROWD_PACKAGES_DB_DATABASE=packages-db
182182

183+
# Packagist registry crawler contact email
184+
CROWD_PACKAGES_PACKAGIST_MAILTO=
185+
183186
# github-repos-enricher
184187
ENRICHER_GITHUB_TOKENS=
185188
ENRICHER_BATCH_SIZE=100

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

Lines changed: 57 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,40 @@ 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'
15+
import { ingestAkritesExternalContactDetail } from '../packages/ingestAkritesExternalContactDetail'
1416
import { submitBlastRadiusJob } from '../packages/submitBlastRadiusJob'
1517

1618
const rateLimiter = createRateLimiter({ max: 60, windowMs: 60 * 1000 })
1719

18-
// 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.
21-
const blastRadiusRateLimitMax = Number(process.env.AKRITES_BLAST_RADIUS_RATE_LIMIT_MAX)
22-
const blastRadiusRateLimitWindowMs = Number(process.env.AKRITES_BLAST_RADIUS_RATE_LIMIT_WINDOW_MS)
20+
// Shared by every endpoint below that kicks off a Temporal workflow per request — those
21+
// get their own, much stricter limiter than plain reads, configurable via env so it can
22+
// be tuned without a redeploy.
23+
function envTunableRateLimiter(envPrefix: string, defaultMax: number, defaultWindowMs: number) {
24+
const max = Number(process.env[`${envPrefix}_MAX`])
25+
const windowMs = Number(process.env[`${envPrefix}_WINDOW_MS`])
26+
return createRateLimiter({
27+
max: Number.isSafeInteger(max) && max > 0 ? max : defaultMax,
28+
windowMs: Number.isSafeInteger(windowMs) && windowMs > 0 ? windowMs : defaultWindowMs,
29+
})
30+
}
31+
32+
// Blast-radius jobs default to 5 requests/hour.
33+
const blastRadiusRateLimiter = envTunableRateLimiter(
34+
'AKRITES_BLAST_RADIUS_RATE_LIMIT',
35+
5,
36+
60 * 60 * 1000,
37+
)
2338

24-
const blastRadiusRateLimiter = createRateLimiter({
25-
max:
26-
Number.isSafeInteger(blastRadiusRateLimitMax) && blastRadiusRateLimitMax > 0
27-
? blastRadiusRateLimitMax
28-
: 5,
29-
windowMs:
30-
Number.isSafeInteger(blastRadiusRateLimitWindowMs) && blastRadiusRateLimitWindowMs > 0
31-
? blastRadiusRateLimitWindowMs
32-
: 60 * 60 * 1000,
33-
})
39+
// /contacts/ingest starts a Temporal workflow and blocks for it (worst case ~95s per
40+
// attempt cycle, plus unbounded time waiting for a free worker slot — see
41+
// security-contacts/workflows.ts's singleActs config), vs. the read-only /contacts/detail
42+
// endpoints, so it gets its own limiter. Defaults to 20 requests/hour.
43+
const contactIngestRateLimiter = envTunableRateLimiter(
44+
'AKRITES_CONTACT_INGEST_RATE_LIMIT',
45+
20,
46+
60 * 60 * 1000,
47+
)
3448

3549
export function akritesExternalRouter(): Router {
3650
const router = Router()
@@ -61,20 +75,42 @@ export function akritesExternalRouter(): Router {
6175
// them via the packages scope. That scope isn't issued by Auth0 yet, so reuse the
6276
// closest issued one — READ_MAINTAINER_ROLES (maintainer data) — NOT READ_PACKAGES.
6377
// TODO: swap for cdp:maintainers:read once issued.
78+
// requireScopes is applied per-route (not router-level) so each route can put its own
79+
// rate limiter *before* the scope check — failed-auth requests still count against that
80+
// route's quota — without forcing every route in this subrouter onto the same limiter
81+
// instance. /ingest gets its own dedicated contactIngestRateLimiter instead of sharing
82+
// the read endpoints' quota, matching the blast-radius jobs endpoint below.
83+
const contactsScopes = [SCOPES.READ_MAINTAINER_ROLES]
6484
const contactsSubRouter = Router()
65-
contactsSubRouter.use(rateLimiter)
66-
contactsSubRouter.use(requireScopes([SCOPES.READ_MAINTAINER_ROLES]))
67-
contactsSubRouter.get('/detail', safeWrap(getAkritesExternalContactDetail))
68-
contactsSubRouter.post(/^\/detail:batch\/?$/, safeWrap(getAkritesExternalContactDetailBatch))
85+
contactsSubRouter.get(
86+
'/detail',
87+
rateLimiter,
88+
requireScopes(contactsScopes),
89+
safeWrap(getAkritesExternalContactDetail),
90+
)
91+
contactsSubRouter.post(
92+
/^\/detail:batch\/?$/,
93+
rateLimiter,
94+
requireScopes(contactsScopes),
95+
safeWrap(getAkritesExternalContactDetailBatch),
96+
)
97+
// Sync, single-purl on-demand ingest — starts a Temporal workflow and blocks a while,
98+
// so it gets the dedicated contactIngestRateLimiter, not the shared rateLimiter above.
99+
contactsSubRouter.post(
100+
'/ingest',
101+
contactIngestRateLimiter,
102+
requireScopes(contactsScopes),
103+
safeWrap(ingestAkritesExternalContactDetail),
104+
)
69105
router.use('/contacts', contactsSubRouter)
70106

71107
// TODO: the contract gates blast-radius behind a dedicated read:advisories scope
72108
// (same as advisories above — see the scope-naming note in the akrites-external
73109
// OpenAPI). Not issued by Auth0 yet, so reuse READ_PACKAGES for now.
74110
const blastRadiusSubRouter = Router()
75-
blastRadiusSubRouter.use(blastRadiusRateLimiter)
76111
blastRadiusSubRouter.use(requireScopes([SCOPES.READ_PACKAGES]))
77-
blastRadiusSubRouter.post('/jobs', safeWrap(submitBlastRadiusJob))
112+
blastRadiusSubRouter.post('/jobs', blastRadiusRateLimiter, safeWrap(submitBlastRadiusJob))
113+
blastRadiusSubRouter.get('/jobs/:analysisId', rateLimiter, safeWrap(getBlastRadiusJob))
78114
router.use('/blast-radius', blastRadiusSubRouter)
79115

80116
return router

0 commit comments

Comments
 (0)