-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathindex.js
186 lines (176 loc) · 6.27 KB
/
index.js
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
const fs = require('fs')
const { Octokit } = require('@octokit/rest')
const { retry } = require('@octokit/plugin-retry')
const { throttling } = require('@octokit/plugin-throttling')
const { program } = require('commander')
program.version('1.0.0')
program.name('vulnerability-auditor')
const _Octokit = Octokit.plugin(retry, throttling)
async function createClient (token, url) {
return new _Octokit({
auth: token,
request: {
retries: 100
},
baseUrl: url,
throttle: {
onRateLimit: (retryAfter, options, octokit) => {
octokit.log.warn(`Request quota exhausted for request ${options.method} ${options.url}`)
if (options.request.retryCount === 0) {
octokit.log.info(`Retrying after ${retryAfter} seconds!`)
return true
}
},
onAbuseLimit: (retryAfter, options, octokit) => {
if (options.request.retryCount === 0) {
octokit.log.warn(`Abuse detected for request ${options.method} ${options.url}`)
octokit.log.info(`Retrying after ${retryAfter} seconds!`)
return true
}
}
}
})
}
// This query fetches the first 100 vulnerabilityAlerts and the vulnerabilityAlerts page information as the
// vulnerabilityAlerts pagination is shadowed by the repo pagination.
//
// If vulnerabilityAlerts.pageInfo.hasNextPage is true, then we pass the endCursor of vulnerabilityAlerts.pageInfo to
// the repoQuery defined later in this file, which paginates all the vulnerabilityAlerts for the repo.
const query = `query($org: String!, $cursor: String){
organization(login: $org) {
repositories(first: 100, after: $cursor) {
nodes {
name
url
vulnerabilityAlerts(first: 100) {
nodes {
createdAt
dismissReason
dismissedAt
dismisser {
login
}
securityAdvisory {
ghsaId
}
vulnerableManifestPath
vulnerableManifestFilename
}
pageInfo {
endCursor
hasNextPage
}
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
}`
async function retrieveAlerts (token, org, threshold, ghsaIDs, url) {
const baseURL = url === '' ? 'https://api.github.com' : url
const client = await createClient(token, baseURL)
let cursor = null
let pageIndex = 1
let hasNextPage = true
const vulnerableRepos = []
const vulnerabilities = ghsaIDs.split(',').map(id => id.toLowerCase())
while (hasNextPage) {
console.log(`Fetching repos page: ${pageIndex++}`)
const response = await client.graphql(query, {
org: org,
cursor: cursor
})
for (const repo of response.organization.repositories.nodes) {
const alerts = repo.vulnerabilityAlerts.nodes.map(vuln => vuln.securityAdvisory.ghsaId.toLowerCase())
for (const vulnerability of vulnerabilities) {
if (alerts.includes(vulnerability)) {
console.log(`Found vulnerability in ${repo.name}: ${vulnerability}`)
vulnerableRepos.push({ name: repo.name, vulnerability: vulnerability })
}
}
if (repo.vulnerabilityAlerts.pageInfo.hasNextPage) {
console.log(`Found > 100 vulnerabilities, fetching all vulnerabilities: ${repo.name}`)
const repoAlerts = await retrieveAllAlerts(client, org, repo.name, repo.vulnerabilityAlerts.pageInfo.endCursor)
for (const vulnerability of vulnerabilities) {
if (repoAlerts.includes(vulnerability)) {
console.log(`Found vulnerability in ${repo.name}: ${vulnerability}`)
vulnerableRepos.push({ name: repo.name, vulnerability: vulnerability })
}
}
}
}
cursor = response.organization.repositories.pageInfo.endCursor
hasNextPage = response.organization.repositories.pageInfo.hasNextPage
}
const date = new Date()
date.setMonth(date.getMonth() - threshold)
const results = []
for (const repo of vulnerableRepos) {
console.log(`Retrieving last commit date: ${repo.name}`)
const { data: commit } = await client.repos.listCommits({
owner: org,
repo: repo.name,
per_page: 1
})
const lastCommitDate = commit[0].commit.author.date > commit[0].commit.committer.date ? commit[0].commit.author.date : commit[0].commit.committer.date
results.push([
repo.name,
repo.vulnerability,
lastCommitDate,
new Date(lastCommitDate) <= date
])
}
await fs.writeFileSync('vulnerable-repos.json', JSON.stringify(results, null, 2))
const headers = ['repoName', 'vulnerabilityID', 'lastCommitDate', 'exceedsThreshold']
await fs.writeFileSync('vulnerable-repos.csv', `${headers.join(',')}\n`)
for (const row of results) {
await fs.appendFileSync('vulnerable-repos.csv', `${row.join(',')}\n`)
}
}
const repoQuery = `query fetchRepoAlerts ($org: String!, $repo:String!, $cursor: String!) {
repository(owner: $org, name: $repo) {
vulnerabilityAlerts(first: 100, after: $cursor) {
nodes {
securityAdvisory {
ghsaId
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}`
async function retrieveAllAlerts (client, org, repo, endCursor) {
let hasNextPage = true
let cursor = endCursor
const alerts = []
let pageIndex = 1
while (hasNextPage) {
console.log(`Scanning repo page: ${pageIndex++}`)
const response = await client.graphql(repoQuery, {
org: org,
repo: repo,
cursor: cursor
})
alerts.push(...response.repository.vulnerabilityAlerts.nodes.map(alert => alert.securityAdvisory.ghsaId.toLowerCase()))
cursor = response.repository.vulnerabilityAlerts.pageInfo.endCursor
hasNextPage = response.repository.vulnerabilityAlerts.pageInfo.hasNextPage
}
return alerts
}
(async function main () {
program
.requiredOption('-t, --token <string>', 'GitHub personal access token')
.requiredOption('-o, --org <string>', 'GitHub organization')
.requiredOption('-h, --threshold <number>', 'Months since last commit')
.requiredOption('-i, --ids <items>', 'Comma-separated list of GHSA IDs')
.option('-u, --url <string>', 'GitHub Enterprise URL')
.parse(process.argv)
const options = program.opts()
await retrieveAlerts(options.token, options.org, options.threshold, options.ids, options.url)
})()