-
Notifications
You must be signed in to change notification settings - Fork 731
Expand file tree
/
Copy pathingestSingle.ts
More file actions
102 lines (91 loc) · 3.29 KB
/
Copy pathingestSingle.ts
File metadata and controls
102 lines (91 loc) · 3.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor'
import { getServiceChildLogger } from '@crowd/logging'
import { getSecurityContactsConfig } from '../config'
import { buildBaseDeps, processRepo } from './processBatch'
import { RepoPackage, RepoTarget } from './types'
import { markRepoAttempted } from './writeContacts'
const log = getServiceChildLogger('security-contacts-ondemand')
type Config = ReturnType<typeof getSecurityContactsConfig>
export interface IngestSingleResult {
found: boolean
repoId?: string
}
interface SingleRepoRow {
repoId: string
url: string
homepage: string | null
archived: boolean | null
packages: RepoPackage[] | null
}
// Mirrors the best-repo LATERAL selection in getPackageDetailByPurl
// (services/libs/data-access-layer/src/osspckgs/api.ts) so the repo we ingest is the
// one the read side surfaces. No `is_critical` filter — non-critical purls are exactly
// what this on-demand path exists for. `packages` aggregates every package linked to
// the repo (not just the requested purl) so extractors see every ecosystem, matching
// the shape the batch sweep builds.
//
// No host filter: processRepo already degrades gracefully for non-github repos (the
// github-specific extractors no-op, security.txt/registry-manifest extractors still run) —
// filtering here would leave non-github repos permanently NULL, re-triggering this
// on-demand path on every single request.
async function findBestRepoForPurl(qx: QueryExecutor, purl: string): Promise<SingleRepoRow | null> {
return qx.selectOneOrNone(
`
SELECT r.id::text AS "repoId",
r.url,
r.homepage,
r.archived,
(
SELECT json_agg(json_build_object('purl', p2.purl, 'ecosystem', p2.ecosystem))
FROM package_repos pr2
JOIN packages p2 ON p2.id = pr2.package_id
WHERE pr2.repo_id = r.id
) AS packages
FROM packages p
JOIN LATERAL (
SELECT pr.repo_id
FROM package_repos pr
WHERE pr.package_id = p.id
ORDER BY pr.confidence DESC, (pr.source = 'declared') DESC
LIMIT 1
) best ON true
JOIN repos r ON r.id = best.repo_id
WHERE p.purl = $(purl)
`,
{ purl },
)
}
function toTarget(row: SingleRepoRow): RepoTarget {
return {
repoId: row.repoId,
url: row.url,
homepage: row.homepage,
archived: row.archived,
packages: row.packages ?? [],
}
}
/**
* On-demand ingest for a single purl, bypassing the daily critical-only sweep.
* Used when the akrites API hits a repo that has never been evaluated
* (repos.contacts_last_refreshed IS NULL).
*/
export async function ingestSecurityContactsForPurl(
qx: QueryExecutor,
config: Config,
purl: string,
): Promise<IngestSingleResult> {
const row = await findBestRepoForPurl(qx, purl)
if (!row) {
log.info({ purl }, 'On-demand security contacts: no linked repo found — skipping')
return { found: false }
}
const target = toTarget(row)
const baseDeps = buildBaseDeps(config)
try {
await processRepo(target, baseDeps, qx)
} catch (err) {
log.error({ repoId: target.repoId, errMsg: (err as Error).message }, 'Repo processing failed')
await markRepoAttempted(qx, target.repoId).catch(() => undefined)
}
return { found: true, repoId: target.repoId }
}