Skip to content

Commit d2c415c

Browse files
committed
fix: distinguish 403 rate-limit from permission failures in token classifier
Signed-off-by: Joana Maia <jmaia@contractor.linuxfoundation.org>
1 parent f25e23d commit d2c415c

1 file changed

Lines changed: 36 additions & 36 deletions

File tree

  • services/apps/security_best_practices_worker/src/activities

services/apps/security_best_practices_worker/src/activities/index.ts

Lines changed: 36 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -44,46 +44,12 @@ export async function getOSPSBaselineInsights(repoUrl: string, token: string): P
4444

4545
const combinedOutput = `${stdout}\n${stderr}`
4646

47-
if (
48-
combinedOutput.includes('401 Unauthorized') ||
49-
combinedOutput.includes('401 Bad credentials')
50-
) {
51-
svc.log.warn('Detected 401 error in privateer output - token invalid or expired!')
52-
throw ApplicationFailure.create({
53-
message: 'GitHub token invalid or expired',
54-
type: 'TokenAuthError',
55-
nonRetryable: true,
56-
})
57-
}
58-
if (combinedOutput.includes('403')) {
59-
svc.log.warn('Detected 403 error in privateer output - token rate-limited!')
60-
throw ApplicationFailure.create({
61-
message: 'GitHub token rate-limited',
62-
type: 'Token403Error',
63-
nonRetryable: true,
64-
})
65-
}
47+
classifyTokenError(combinedOutput, 'privateer output')
6648
} catch (err) {
6749
svc.log.error(`Privateer run failed: ${err.message}`)
6850

69-
// check for auth errors in captured output if available
7051
const output = `${err.stdout || ''}\n${err.stderr || ''}`
71-
if (output.includes('401 Unauthorized') || output.includes('401 Bad credentials')) {
72-
svc.log.warn('Detected 401 error in failed privateer output - token invalid or expired!')
73-
throw ApplicationFailure.create({
74-
message: 'GitHub token invalid or expired',
75-
type: 'TokenAuthError',
76-
nonRetryable: true,
77-
})
78-
}
79-
if (output.includes('403')) {
80-
svc.log.warn('Detected 403 error in failed privateer output - token rate-limited!')
81-
throw ApplicationFailure.create({
82-
message: 'GitHub token rate-limited',
83-
type: 'Token403Error',
84-
nonRetryable: true,
85-
})
86-
}
52+
classifyTokenError(output, 'failed privateer output')
8753
throw err
8854
}
8955

@@ -217,6 +183,40 @@ export async function saveOSPSBaselineInsightsToRedis(
217183
await redisCache.set(key, JSON.stringify(insights), 60 * 60 * 24) // 1 day
218184
}
219185

186+
// GitHub returns 403 for both rate-limit exhaustion AND persistent permission/SAML failures.
187+
// Only rate-limit responses should trigger the 1h token-rotation cooldown; permission failures
188+
// must be marked as invalid so the token isn't retried in a loop.
189+
function classifyTokenError(output: string, source: string): void {
190+
if (output.includes('401 Unauthorized') || output.includes('401 Bad credentials')) {
191+
svc.log.warn(`Detected 401 error in ${source} - token invalid or expired!`)
192+
throw ApplicationFailure.create({
193+
message: 'GitHub token invalid or expired',
194+
type: 'TokenAuthError',
195+
nonRetryable: true,
196+
})
197+
}
198+
if (!output.includes('403')) return
199+
200+
// Rate-limit 403s from GitHub carry a distinctive body: "API rate limit exceeded" or
201+
// "secondary rate limit". Anything else (SAML enforcement, missing scopes, resource
202+
// not accessible) is a permission problem and the token won't recover.
203+
const isRateLimit = /rate limit|rate_limit|secondary rate/i.test(output)
204+
if (isRateLimit) {
205+
svc.log.warn(`Detected 403 rate-limit in ${source} - token rate-limited!`)
206+
throw ApplicationFailure.create({
207+
message: 'GitHub token rate-limited',
208+
type: 'Token403Error',
209+
nonRetryable: true,
210+
})
211+
}
212+
svc.log.warn(`Detected 403 permission error in ${source} - token lacks required access!`)
213+
throw ApplicationFailure.create({
214+
message: 'GitHub token lacks required permissions',
215+
type: 'TokenAuthError',
216+
nonRetryable: true,
217+
})
218+
}
219+
220220
function computeRunDuration(start: string | undefined, end: string | undefined): string {
221221
if (!start || !end) return ''
222222
const startMs = new Date(start).getTime()

0 commit comments

Comments
 (0)