Skip to content

Commit efba8cd

Browse files
committed
fix: code review fixes
Signed-off-by: Mouad BANI <mouad-mb@outlook.com>
1 parent 8cb71f6 commit efba8cd

5 files changed

Lines changed: 38 additions & 11 deletions

File tree

services/apps/packages_worker/src/security-contacts/extractors/registry/npm.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const SOURCE = 'npm-registry'
1010
function npmPackagePath(parsed: ParsedPurl): string {
1111
const full = parsed.namespace ? `${parsed.namespace}/${parsed.name}` : parsed.name
1212
// Scoped packages must percent-encode the slash for the registry path.
13-
return full.startsWith('@') ? full.replace('/', '%2F') : full
13+
return full.startsWith('@') ? full.replaceAll('/', '%2F') : full
1414
}
1515

1616
export function mapNpm(doc: unknown, sourceUrl: string, fetchedAt: string): RawContact[] {

services/apps/packages_worker/src/security-contacts/extractors/securityInsights.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ function classifyValue(raw: string): { channel: ContactChannel; value: string }
4545
return { channel: 'github-handle', value: v }
4646
}
4747

48+
// project-si-source may only point at trusted raw-content hosts (SSRF guard).
49+
const REDIRECT_ALLOWED_HOSTS = new Set([
50+
'raw.githubusercontent.com',
51+
'gist.githubusercontent.com',
52+
'gitlab.com',
53+
])
54+
55+
function isAllowedRedirect(url: string): boolean {
56+
try {
57+
const u = new URL(url)
58+
return u.protocol === 'https:' && REDIRECT_ALLOWED_HOSTS.has(u.hostname.toLowerCase())
59+
} catch {
60+
return false
61+
}
62+
}
63+
4864
// MAINTAINERS.md / OWNERS.md links are documents, not contacts.
4965
function isDocumentUrl(value: string): boolean {
5066
const v = value.toLowerCase()
@@ -211,7 +227,9 @@ export const extractSecurityInsights: Extractor = async (target, deps) => {
211227
}
212228

213229
const redirect = (doc as any)?.header?.['project-si-source']
214-
if (typeof redirect === 'string' && /^https?:\/\//i.test(redirect)) {
230+
// Only follow redirects to trusted raw-content hosts — a repo-controlled URL must not
231+
// be able to point the worker at internal/metadata endpoints (SSRF).
232+
if (typeof redirect === 'string' && isAllowedRedirect(redirect)) {
215233
const redirected = await fetchText(redirect, deps.fetchTimeoutMs)
216234
if (redirected.text) return parseSecurityInsights(redirected.text, redirect, fetchedAt)
217235
}

services/apps/packages_worker/src/security-contacts/extractors/securityMd.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ export function parseSecurityMd(
9090
}
9191
}
9292

93-
// PVR redirect language corroborates A2. Emitted unconditionally here; the pipeline
94-
// vetoes it when A2 reports PVR disabled. TODO(CM-1243): apply the A2 veto in Step 6.
93+
// PVR redirect language corroborates A2. Emitted unconditionally; processBatch vetoes it
94+
// when A2 authoritatively reports PVR disabled.
9595
if (PVR_RE.test(text)) {
9696
add('github-pvr', `https://github.com/${owner}/${name}/security/advisories/new`)
9797
}

services/apps/packages_worker/src/security-contacts/processBatch.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async function fetchBatch(qx: QueryExecutor, config: Config): Promise<SweepRow[]
5656
FROM repos r
5757
JOIN package_repos pr ON pr.repo_id = r.id
5858
JOIN packages p ON p.id = pr.package_id AND p.is_critical
59-
WHERE r.host IN ('github', 'github.com')
59+
WHERE r.host = 'github'
6060
AND (
6161
r.contacts_last_refreshed IS NULL
6262
OR r.contacts_last_refreshed < NOW() - INTERVAL '$(updateIntervalHours) hours'
@@ -97,6 +97,13 @@ async function processRepo(
9797
// One failing extractor must not sink the repo.
9898
const results = await Promise.allSettled(EXTRACTORS.map((extract) => extract(target, deps)))
9999

100+
// If every extractor failed (e.g. a transient network outage), skip the write entirely so we
101+
// don't delete previously good contacts/policies; leaving contacts_last_refreshed retries next sweep.
102+
if (results.every((r) => r.status === 'rejected')) {
103+
log.warn({ repoId: target.repoId }, 'All extractors failed — skipping write to preserve data')
104+
return
105+
}
106+
100107
let contacts: RawContact[] = []
101108
const policies: Partial<RepoPolicies> = {}
102109
for (const r of results) {

services/apps/packages_worker/src/security-contacts/writeContacts.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { RepoPolicies, ScoredContact } from './types'
44

55
/**
66
* Idempotent per-repo recompute: replace the repo's security_contacts rows and refresh
7-
* the policy columns in one transaction. pvr_enabled uses COALESCE so an "unknown" result
8-
* (A2 endpoint failure) never clears a previously known value.
7+
* the policy columns in one transaction. Policy columns use COALESCE so a run that doesn't
8+
* rediscover a field (partial/failed extractor pass) never clears a previously known value.
99
*/
1010
export async function writeContacts(
1111
qx: QueryExecutor,
@@ -36,11 +36,13 @@ export async function writeContacts(
3636
}
3737

3838
await tx.result(
39+
// COALESCE preserves previously stored values when a run doesn't (re)discover a field —
40+
// a partial/failed extractor pass must not wipe still-valid policy URLs or the PVR flag.
3941
`UPDATE repos SET
40-
security_policy_url = $(securityPolicyUrl),
41-
vulnerability_reporting_url = $(vulnerabilityReportingUrl),
42-
bug_bounty_url = $(bugBountyUrl),
43-
security_txt_url = $(securityTxtUrl),
42+
security_policy_url = COALESCE($(securityPolicyUrl), security_policy_url),
43+
vulnerability_reporting_url = COALESCE($(vulnerabilityReportingUrl), vulnerability_reporting_url),
44+
bug_bounty_url = COALESCE($(bugBountyUrl), bug_bounty_url),
45+
security_txt_url = COALESCE($(securityTxtUrl), security_txt_url),
4446
pvr_enabled = COALESCE($(pvrEnabled), pvr_enabled),
4547
contacts_last_refreshed = NOW()
4648
WHERE id = $(repoId)`,

0 commit comments

Comments
 (0)