-
Notifications
You must be signed in to change notification settings - Fork 731
fix: security contacts ingestion improvements [CM-1297] #4295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
26facec
dc78313
f4405d7
647207b
829220c
1633ec2
04aad47
33d8376
8e4a6ea
b746e41
36ecfbd
8f3669f
a958c14
5c35e59
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| ALTER TABLE security_contacts ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; | ||
|
|
||
| CREATE INDEX IF NOT EXISTS security_contacts_repo_active_idx | ||
| ON security_contacts (repo_id) | ||
| WHERE deleted_at IS NULL; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import { ExtractorDeps } from '../types' | ||
|
|
||
| /** null means unresolved (empty repo, truncated, fetch failure) — callers must fall back to probing. */ | ||
| export interface RepoTree { | ||
| paths: Set<string> | null | ||
| } | ||
|
|
||
| interface GitTreeEntry { | ||
| path?: unknown | ||
| type?: unknown | ||
| } | ||
|
|
||
| interface GitTreeResponse { | ||
| tree?: GitTreeEntry[] | ||
| truncated?: boolean | ||
| } | ||
|
|
||
| // `HEAD` resolves to the default branch on the git data API. | ||
| export async function fetchRepoTree( | ||
| owner: string, | ||
| name: string, | ||
| githubGet: ExtractorDeps['githubGet'], | ||
| ): Promise<RepoTree> { | ||
| try { | ||
| const { text } = await githubGet(`/repos/${owner}/${name}/git/trees/HEAD?recursive=1`) | ||
| if (!text) return { paths: null } | ||
|
|
||
| const doc = JSON.parse(text) as GitTreeResponse | ||
| if (doc.truncated || !Array.isArray(doc.tree)) return { paths: null } | ||
|
|
||
| const paths = new Set<string>() | ||
| for (const entry of doc.tree) { | ||
| if (entry.type === 'blob' && typeof entry.path === 'string') paths.add(entry.path) | ||
| } | ||
| return { paths } | ||
| } catch { | ||
| return { paths: null } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,17 +15,13 @@ const log = getServiceChildLogger('security-contacts:github-token') | |
|
|
||
| const GITHUB_API = 'https://api.github.com' | ||
|
|
||
| // Genuinely-absent / not-determinable → null body (see http.ts for the same set). 422 covers the | ||
| // PVR endpoint returning "can't determine" per-repo; it must read as unknown, not a hard failure. | ||
| const ABSENT_STATUSES = new Set([404, 410, 422]) | ||
| // Genuinely-absent / not-determinable → null body, not a failure (see http.ts for the same set). | ||
| // 422 covers GitHub's PVR endpoint returning "can't determine" per-repo; 451 is permanent removal. | ||
| const ABSENT_STATUSES = new Set([404, 410, 422, 451]) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 451 responses wipe stored contactsHigh Severity HTTP 451 was added to the absent-status set for GitHub and registry fetches, so those calls return empty bodies instead of failing. When every extractor therefore finishes successfully with no contacts, Additional Locations (1)Reviewed by Cursor Bugbot for commit b746e41. Configure here.
mbani01 marked this conversation as resolved.
|
||
|
|
||
| // App-wide ceiling on concurrent GitHub requests. GitHub's secondary limit rejects bursts of | ||
| // >100 concurrent requests from one app; staying under that (across all installations) is the | ||
| // single most effective guard against secondary-limit 429s at high repo concurrency. | ||
| // App-wide ceiling on concurrent requests — GitHub's secondary limit rejects bursts >100/app. | ||
| const MAX_CONCURRENT_GITHUB_REQUESTS = 50 | ||
|
|
||
| // Bound the park/switch/backoff retry loop so a persistently-limited request eventually surfaces | ||
| // as a failure (which the pipeline treats as transient and preserves existing data). | ||
| const MAX_RATE_LIMIT_RETRIES = 6 | ||
|
|
||
| /** Minimal async semaphore with fair FIFO hand-off, used to cap concurrent GitHub requests. */ | ||
|
|
@@ -126,13 +122,9 @@ async function fetchOnce( | |
| } | ||
|
|
||
| /** | ||
| * Rate-limit-safe GitHub API GET. Selects an installation from the pool, sleeps if all are parked, | ||
| * feeds response budget headers back so exhausted installations get parked before they 403, and on | ||
| * a rate-limit response parks (primary) or waits out Retry-After (secondary, app-wide) then retries | ||
| * on another installation. Falls back to a single unauthenticated request when no App is configured. | ||
| * | ||
| * Returns text on 200; null body for absent resources (404/410/422); throws on other non-200s and | ||
| * once the retry budget is exhausted (callers treat throws as transient and preserve existing data). | ||
| * Rate-limit-safe GitHub API GET. Rotates across installations in the pool, parking exhausted | ||
| * or rate-limited ones and retrying, up to MAX_RATE_LIMIT_RETRIES. Falls back to a single | ||
| * unauthenticated request when no App is configured. | ||
| */ | ||
| export async function githubApiGet( | ||
| path: string, | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.