-
Notifications
You must be signed in to change notification settings - Fork 731
Expand file tree
/
Copy pathsecurityMd.ts
More file actions
120 lines (104 loc) · 3.89 KB
/
Copy pathsecurityMd.ts
File metadata and controls
120 lines (104 loc) · 3.89 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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import { parseGithubUrl } from '../../enricher/fetchLightRepo'
import {
ContactChannel,
Extractor,
ExtractorResult,
ProvenanceEntry,
RawContact,
RepoPolicies,
} from '../types'
import { githubHandleFromUrl } from './http'
const SOURCE = 'security.md'
const PATHS = ['SECURITY.md', '.github/SECURITY.md', 'docs/SECURITY.md']
const KEYWORD_RE =
/\b(report|security|vulnerabilit(?:y|ies)|disclosure|contact|advisor(?:y|ies))\b/i
const NEGATIVE_SECTION_RE = /acknowledg|hall of fame|thanks|credits|honou?r|researchers/i
const EMAIL_RE = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g
const URL_RE = /https?:\/\/[^\s)<>\]"']+/g
const HEADING_RE = /^#{1,6}\s+(.*)$/
const PVR_RE = /private vulnerability reporting|\/security\/advisories\/new/i
function cleanUrl(url: string): string {
return url.replace(/[.,;:]+$/, '')
}
export function parseSecurityMd(
text: string,
owner: string,
name: string,
provPath: string,
fetchedAt: string,
): ExtractorResult {
const policies: Partial<RepoPolicies> = {
securityPolicyUrl: `https://github.com/${owner}/${name}/blob/HEAD/${provPath}`,
}
const prov = (): ProvenanceEntry[] => [
{ source: SOURCE, sourceTier: 'B', path: provPath, fetchedAt },
]
const seen = new Set<string>()
const contacts: RawContact[] = []
const add = (channel: ContactChannel, value: string): boolean => {
const key = `${channel}:${value.toLowerCase()}`
if (seen.has(key)) return false
seen.add(key)
contacts.push({ channel, value, role: 'security-team', tier: 'B', provenance: prov() })
return true
}
// Verbose policies list many references/people; bound how many B1 promotes.
const MAX_PER_CHANNEL = 3
let emailCount = 0
let urlCount = 0
const lines = text.split('\n')
let heading = ''
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const headingMatch = HEADING_RE.exec(line.trim())
if (headingMatch) {
heading = headingMatch[1]
continue
}
// Skip researcher credits / acknowledgements sections — those are not contacts.
if (NEGATIVE_SECTION_RE.test(heading)) continue
// Paragraph-local proximity: a keyword must appear within ±2 lines of the candidate.
const windowText = lines.slice(Math.max(0, i - 2), i + 3).join('\n')
if (!KEYWORD_RE.test(windowText)) continue
if (emailCount < MAX_PER_CHANNEL) {
for (const email of line.match(EMAIL_RE) ?? []) {
if (emailCount >= MAX_PER_CHANNEL) break
if (add('email', email)) emailCount++
}
}
if (urlCount < MAX_PER_CHANNEL) {
for (const rawUrl of line.match(URL_RE) ?? []) {
if (urlCount >= MAX_PER_CHANNEL) break
const url = cleanUrl(rawUrl)
// The canonical PVR advisory URL is emitted as a github-pvr contact below.
if (/\/security\/advisories/i.test(url)) continue
// Bare github.com/<handle> profile links are people listings, not reporting channels.
if (githubHandleFromUrl(url)) continue
if (add('url', url)) urlCount++
}
}
}
// PVR redirect language corroborates A2. Emitted unconditionally; processBatch vetoes it
// when A2 authoritatively reports PVR disabled.
if (PVR_RE.test(text)) {
add('github-pvr', `https://github.com/${owner}/${name}/security/advisories/new`)
}
return { contacts, policies }
}
export const extractSecurityMd: Extractor = async (target, deps) => {
let owner: string
let name: string
try {
;({ owner, name } = parseGithubUrl(target.url))
} catch {
return { contacts: [], policies: {} }
}
const fetchedAt = new Date().toISOString()
const { paths: treePaths } = deps.repoTree
for (const path of PATHS) {
if (treePaths && !treePaths.has(path)) continue
const { text } = await deps.githubGet(`/repos/${owner}/${name}/contents/${path}`, { raw: true })
if (text) return parseSecurityMd(text, owner, name, path, fetchedAt)
}
return { contacts: [], policies: {} }
}