-
Notifications
You must be signed in to change notification settings - Fork 731
Expand file tree
/
Copy pathsecurityContactsFile.ts
More file actions
92 lines (76 loc) · 2.8 KB
/
Copy pathsecurityContactsFile.ts
File metadata and controls
92 lines (76 loc) · 2.8 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
import { getServiceChildLogger } from '@crowd/logging'
import { parseGithubUrl } from '../../enricher/fetchLightRepo'
import { Extractor, ExtractorDeps, ProvenanceEntry, RawContact } from '../types'
import { isEmail } from './http'
const log = getServiceChildLogger('security-contacts:security_contacts-file')
const SOURCE = 'security_contacts'
const PATH = 'SECURITY_CONTACTS'
const HANDLE_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/
export interface SecurityContactEntry {
handle: string
email?: string
}
export function parseSecurityContacts(text: string): SecurityContactEntry[] {
const entries: SecurityContactEntry[] = []
for (const rawLine of text.split('\n')) {
const line = rawLine.trim()
if (!line || line.startsWith('#')) continue
const tokens = line.replace(/^-\s*/, '').split(/\s+/)
const handle = tokens[0].replace(/^@/, '')
if (!HANDLE_RE.test(handle)) continue
const email = tokens.slice(1).find((t) => isEmail(t))
entries.push(email ? { handle, email } : { handle })
}
return entries
}
async function resolvePublicEmail(
login: string,
githubGet: ExtractorDeps['githubGet'],
): Promise<string | null> {
try {
const { text } = await githubGet(`/users/${login}`)
const email = (text ? (JSON.parse(text) as { email?: unknown }) : null)?.email
return typeof email === 'string' && isEmail(email) ? email : null
} catch (err) {
log.warn({ login, errMsg: (err as Error).message }, 'Handle email resolution failed')
return null
}
}
export const extractSecurityContactsFile: Extractor = async (target, deps) => {
let owner: string
let name: string
try {
;({ owner, name } = parseGithubUrl(target.url))
} catch {
return { contacts: [], policies: {} }
}
if (deps.repoTree.paths && !deps.repoTree.paths.has(PATH)) return { contacts: [], policies: {} }
const { text } = await deps.githubGet(`/repos/${owner}/${name}/contents/${PATH}`, { raw: true })
if (!text) return { contacts: [], policies: {} }
const fetchedAt = new Date().toISOString()
const prov = (): ProvenanceEntry[] => [{ source: SOURCE, sourceTier: 'A', path: PATH, fetchedAt }]
const contacts: RawContact[] = []
for (const entry of parseSecurityContacts(text)) {
const email = entry.email ?? (await resolvePublicEmail(entry.handle, deps.githubGet))
if (email) {
// handle = the username this email was resolved from (used for identity-linking).
contacts.push({
channel: 'email',
value: email,
handle: entry.handle,
role: 'security-team',
tier: 'A',
provenance: prov(),
})
} else {
contacts.push({
channel: 'github-handle',
value: entry.handle,
role: 'security-team',
tier: 'A',
provenance: prov(),
})
}
}
return { contacts, policies: {} }
}