Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
81139ce
chore: update pvtr plugin to v0.24.0+2fixes
joanagmaia Jul 7, 2026
6789e92
Merge branch 'main' into chore/update-pvtr-version
joanagmaia Jul 8, 2026
f41380e
Merge branch 'main' into chore/update-pvtr-version
gaspergrom Jul 9, 2026
01742f6
Merge branch 'main' into chore/update-pvtr-version
joanagmaia Jul 10, 2026
338d99b
fix: issues with runs
joanagmaia Jul 13, 2026
8ea9d01
Merge remote-tracking branch 'origin/main' into chore/update-pvtr-ver…
joanagmaia Jul 13, 2026
da949b3
fix: lint
joanagmaia Jul 13, 2026
c2d9ba0
fix: handle '401 Bad credentials' as invalid token in privateer auth …
joanagmaia Jul 13, 2026
7da9a3e
fix: address rate limiting, token rotation, and timeout issues in sec…
joanagmaia Jul 13, 2026
b884d89
fix: lint
joanagmaia Jul 13, 2026
2228dc6
Merge branch 'main' into chore/update-pvtr-version
joanagmaia Jul 13, 2026
b7e036d
fix: harden token state handling across batches and concurrent tasks
joanagmaia Jul 13, 2026
5d9fa19
fix: prevent stuck tokens and repo blacklist across continueAsNew bat…
joanagmaia Jul 13, 2026
f25e23d
fix: use workflow startTime for determinism and avoid continueAsNew h…
joanagmaia Jul 13, 2026
d2c415c
fix: distinguish 403 rate-limit from permission failures in token cla…
joanagmaia Jul 13, 2026
fd4dd19
Merge branch 'main' into chore/update-pvtr-version
joanagmaia Jul 13, 2026
aeccd3e
fix: accurate rate-limit timestamps, dynamic token attempts, env-var …
joanagmaia Jul 14, 2026
4f6f192
Merge branch 'main' into chore/update-pvtr-version
joanagmaia Jul 14, 2026
5bfa6ab
fix: refresh wall-clock time in loop, requeue rate-limited pool
joanagmaia Jul 14, 2026
24d0730
fix: classify 429 as rate limit and scope 403 permission failures per…
joanagmaia Jul 14, 2026
93a94da
fix: monotonic release timestamp for LRU rotation, clarify isInvalid TTL
joanagmaia Jul 14, 2026
2be8763
fix: fail repos when all tokens invalid, widen lastUsed to Date | string
joanagmaia Jul 14, 2026
cdc45de
fix: harden token error handling and workflow determinism
joanagmaia Jul 14, 2026
34a9c0d
Merge branch 'main' into chore/update-pvtr-version
joanagmaia Jul 14, 2026
ea84fef
fix: requeue repo on rate-limit exhaustion instead of failing
joanagmaia Jul 14, 2026
c5c8646
docs: correct stale references to workflowInfo().startTime in comments
joanagmaia Jul 14, 2026
351c47c
fix: track per-repo attempted tokens and match HTTP status by word bo…
joanagmaia Jul 14, 2026
0b8b786
fix: lint
joanagmaia Jul 14, 2026
0636470
Merge branch 'main' into chore/update-pvtr-version
joanagmaia Jul 14, 2026
7ed7432
fix: persist per-repo attempted tokens across requeues
joanagmaia Jul 14, 2026
95a17e9
fix: fail fast on empty token pool and refresh clock per-attempt
joanagmaia Jul 14, 2026
1daefa3
fix: skip first-attempt clock refresh to preserve sync token claim
joanagmaia Jul 14, 2026
6a552da
refactor(security-worker): remove TokenRepoAccessError and simplify t…
joanagmaia Jul 14, 2026
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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG PVTR_VERSION=v0.23.2
ARG PVTR_VERSION=v0.24.0
Comment thread
joanagmaia marked this conversation as resolved.
Comment thread
joanagmaia marked this conversation as resolved.
Comment thread
joanagmaia marked this conversation as resolved.

FROM alpine:3.21 AS core
RUN apk add --no-cache wget tar unzip
Expand All @@ -10,13 +10,13 @@ ARG PLATFORM=Linux_x86_64
RUN wget https://github.com/privateerproj/privateer/releases/download/v${VERSION}/privateer_${PLATFORM}.tar.gz
RUN tar -xzf privateer_${PLATFORM}.tar.gz

FROM golang:1.26.3-alpine3.23 AS plugin
FROM golang:1.26.4-alpine3.22 AS plugin
RUN apk add --no-cache make git
WORKDIR /plugin
ARG PVTR_COMMIT=c7bd9538d64f7eaab94a05c9b5fd05458a387b1c
ARG PVTR_COMMIT=c1095c95a1b399ec63e4f4e2b7880b0ef55e604f
Comment thread
joanagmaia marked this conversation as resolved.
Comment thread
joanagmaia marked this conversation as resolved.
ARG PVTR_VERSION
# To run the latest version of the plugin, we need to use the latest commit of the pvtr-github-repo-scanner repository.
# Currently using v0.23.2: https://github.com/ossf/pvtr-github-repo-scanner/commit/c7bd9538d64f7eaab94a05c9b5fd05458a387b1c
# Currently using v0.24.0+2fixes: https://github.com/ossf/pvtr-github-repo-scanner/commit/c1095c95a1b399ec63e4f4e2b7880b0ef55e604f
RUN git clone https://github.com/ossf/pvtr-github-repo-scanner.git && cd pvtr-github-repo-scanner && git checkout ${PVTR_COMMIT}
RUN cd pvtr-github-repo-scanner && make binary && cp github-repo ../github-repo

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ services:
- Maturity Level 1

vars:
owner: $REPO_OWNER
repo: $REPO_NAME
token: $GITHUB_TOKEN
owner: '$REPO_OWNER'
repo: '$REPO_NAME'
token: '$GITHUB_TOKEN'
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
findObsoleteRepos,
getCurrentTimeMs,
getOSPSBaselineInsights,
initializeTokenInfos,
saveOSPSBaselineInsightsToDB,
Expand All @@ -12,4 +13,5 @@ export {
findObsoleteRepos,
initializeTokenInfos,
updateTokenInfos,
getCurrentTimeMs,
}
106 changes: 76 additions & 30 deletions services/apps/security_best_practices_worker/src/activities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,25 +44,12 @@ export async function getOSPSBaselineInsights(repoUrl: string, token: string): P

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

if (combinedOutput.includes('403')) {
svc.log.warn('Detected 403 error in privateer output!')
throw ApplicationFailure.create({
message: 'GitHub token rate-limited',
type: 'Token403Error',
})
}
classifyTokenError(combinedOutput, 'privateer output')
} catch (err) {
svc.log.error(`Privateer run failed: ${err.message}`)

// check for 403 in captured output if available
const output = `${err.stdout || ''}\n${err.stderr || ''}`
if (output.includes('403')) {
svc.log.warn('Detected 403 error in failed privateer output!')
throw ApplicationFailure.create({
message: 'GitHub token rate-limited',
type: 'Token403Error',
})
}
classifyTokenError(output, 'failed privateer output')
throw err
}

Expand Down Expand Up @@ -196,6 +183,40 @@ export async function saveOSPSBaselineInsightsToRedis(
await redisCache.set(key, JSON.stringify(insights), 60 * 60 * 24) // 1 day
}

function classifyTokenError(output: string, source: string): void {
const failedAtMs = Date.now()

if (output.includes('401 Unauthorized') || output.includes('401 Bad credentials')) {
Comment thread
joanagmaia marked this conversation as resolved.
svc.log.warn(`Detected 401 error in ${source} - token invalid or expired!`)
throw ApplicationFailure.create({
message: 'GitHub token invalid or expired',
type: 'TokenAuthError',
nonRetryable: true,
details: [failedAtMs],
})
}

// Word-boundary match so unrelated numbers don't get misclassified as HTTP 429/403.
const has403 = /\b403\b/.test(output)
const has429 = /\b429\b/.test(output)
if (!has403 && !has429) return

// 429 is always rate-limit. 403 with rate-limit body is rate-limit. 403 without is a
// permission problem (SAML, missing scopes) — log it but don't throw; the child workflow's
// retry policy will exhaust retries and the repo will be deferred to the next scheduled run.
const isRateLimit = has429 || /rate limit|rate_limit|secondary rate/i.test(output)
if (isRateLimit) {
svc.log.warn(`Detected rate-limit in ${source} - token rate-limited!`)
throw ApplicationFailure.create({
message: 'GitHub token rate-limited',
type: 'Token403Error',
nonRetryable: true,
details: [failedAtMs],
})
}
svc.log.warn(`Detected 403 permission error in ${source} - token may lack access to this repo`)
Comment thread
joanagmaia marked this conversation as resolved.
}
Comment thread
joanagmaia marked this conversation as resolved.

function computeRunDuration(start: string | undefined, end: string | undefined): string {
if (!start || !end) return ''
const startMs = new Date(start).getTime()
Expand Down Expand Up @@ -258,13 +279,13 @@ async function runBinary(
resolve({ stdout, stderr })
} else {
const truncated = (s: string) => (s.length > 500 ? s.slice(0, 500) + '…' : s)
const truncStdout = truncated(stdout)
const truncStderr = truncated(stderr)
// Attach full stdout/stderr so classifyTokenError sees rate-limit markers that may
// appear after the truncation cut. Message is still truncated for log readability.
const err = Object.assign(
new Error(
`Binary exited with code ${code}\nStderr:\n${truncStderr}\nStdout:\n${truncStdout}`,
`Binary exited with code ${code}\nStderr:\n${truncated(stderr)}\nStdout:\n${truncated(stdout)}`,
),
{ stdout: truncStdout, stderr: truncStderr },
{ stdout, stderr },
)
reject(err)
}
Expand All @@ -276,20 +297,45 @@ export async function initializeTokenInfos(): Promise<ITokenInfo[]> {
const redisCache = new RedisCache(`osps-baseline-insights`, svc.redis, svc.log)

const tokenInfosInRedis = await redisCache.get('tokenInfos')

if (tokenInfosInRedis) {
return JSON.parse(tokenInfosInRedis)
}

return process.env['CROWD_GITHUB_PERSONAL_ACCESS_TOKENS'].split(',').map((token) => ({
token,
inUse: false,
lastUsed: new Date(),
isRateLimited: false,
}))
Comment thread
cursor[bot] marked this conversation as resolved.
const cached: ITokenInfo[] = tokenInfosInRedis ? JSON.parse(tokenInfosInRedis) : []
const cachedByToken = new Map(cached.map((t) => [t.token, t]))

// Env var is authoritative for pool membership so PAT rotation is picked up on next run:
// a new token in CROWD_GITHUB_PERSONAL_ACCESS_TOKENS joins the pool fresh, and a removed
// token drops out even if its cached entry is still in Redis.
const envTokens = process.env['CROWD_GITHUB_PERSONAL_ACCESS_TOKENS'].split(',')

return envTokens.map((token) => {
Comment thread
joanagmaia marked this conversation as resolved.
const t = cachedByToken.get(token)
if (t) {
return {
...t,
// Reset inUse — workflow processes may have crashed leaving stale in-use flags.
inUse: false,
// Backward compat: legacy entries have isRateLimited=true with no rateLimitedAt timestamp.
// Without a timestamp the 1-hour expiry can't apply and the token would be stuck forever.
// Clear stale rate-limits that lack a timestamp so they can be retried on this run.
isRateLimited: t.isRateLimited && !!t.rateLimitedAt,
rateLimitedAt: t.isRateLimited && t.rateLimitedAt ? t.rateLimitedAt : undefined,
}
}
return {
token,
inUse: false,
lastUsed: new Date(),
isRateLimited: false,
}
})
}

export async function updateTokenInfos(tokenInfos: ITokenInfo[]): Promise<void> {
const redisCache = new RedisCache(`osps-baseline-insights`, svc.redis, svc.log)
await redisCache.set('tokenInfos', JSON.stringify(tokenInfos), 60 * 60 * 24) // 1 day
}

// Wall-clock time via activity — workflow code can't call Date.now() (determinism rule),
// but rate-limit cooldowns need real elapsed time to expire mid-run rather than only at
// the next continueAsNew batch boundary.
export async function getCurrentTimeMs(): Promise<number> {
return Date.now()
}
6 changes: 5 additions & 1 deletion services/apps/security_best_practices_worker/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export interface ITriggerSecurityInsightsCheckForReposParams {
export interface ITokenInfo {
token: string
inUse: boolean
lastUsed: Date
// Date at initialization time; becomes an ISO string after JSON round-trip through Redis
// and Temporal payloads, so callers must wrap in `new Date()` before comparing.
lastUsed: Date | string
isRateLimited: boolean
Comment thread
joanagmaia marked this conversation as resolved.
rateLimitedAt?: string // ISO timestamp; used to auto-reset after 1 hour
isInvalid?: boolean // 401 auth failure; recover by rotating the PAT out of CROWD_GITHUB_PERSONAL_ACCESS_TOKENS
}
Loading
Loading