Skip to content

Commit 5c4055a

Browse files
author
testcafe-build-bot
committed
🔄 synced local '.github/scripts/security-checker.mjs' with remote 'scripts/security-checker.mjs'
1 parent 54f62a7 commit 5c4055a

File tree

1 file changed

+219
-167
lines changed

1 file changed

+219
-167
lines changed
Lines changed: 219 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -1,180 +1,232 @@
11
const STATES = {
2-
open: 'open',
3-
closed: 'closed',
2+
open: 'open',
3+
closed: 'closed',
44
};
55

66
const LABELS = {
7-
dependabot: 'dependabot',
8-
codeq: 'codeql',
9-
security: 'security notification',
7+
dependabot: 'dependabot',
8+
codeq: 'codeql',
9+
security: 'security notification',
1010
};
1111

1212
const ALERT_TYPES = {
13-
dependabot: 'dependabot',
14-
codeq: 'codeql',
13+
dependabot: 'dependabot',
14+
codeq: 'codeql',
15+
}
16+
17+
const UPDATE_TYPE = {
18+
addAlertToIssue: 'addAlertToIssue',
19+
closeTask: 'closeTask'
1520
}
1621

1722
class SecurityChecker {
18-
constructor (github, context, issueRepo) {
19-
this.github = github;
20-
this.issueRepo = issueRepo;
21-
this.context = {
22-
owner: context.repo.owner,
23-
repo: context.repo.repo,
24-
};
25-
}
26-
27-
async check () {
28-
const dependabotAlerts = await this.getDependabotAlerts();
29-
const codeqlAlerts = await this.getCodeqlAlerts();
30-
const existedIssues = await this.getExistedIssues();
31-
32-
this.alertDictionary = this.createAlertDictionary(existedIssues);
33-
34-
await this.closeSpoiledIssues();
35-
await this.createDependabotlIssues(dependabotAlerts);
36-
await this.createCodeqlIssues(codeqlAlerts);
37-
}
38-
39-
async getDependabotAlerts () {
40-
const { data } = await this.github.rest.dependabot.listAlertsForRepo({ state: STATES.open, ...this.context });
41-
42-
return data;
43-
}
44-
45-
async getCodeqlAlerts () {
46-
try {
47-
const { data } = await this.github.rest.codeScanning.listAlertsForRepo({ state: STATES.open, ...this.context });
48-
49-
return data;
50-
}
51-
catch (e) {
52-
if (e.message.includes('no analysis found') || e.message.includes('Advanced Security must be enabled for this repository to use code scanning'))
53-
return [];
54-
55-
throw e;
56-
}
57-
}
58-
59-
async getExistedIssues () {
60-
const { data: existedIssues } = await this.github.rest.issues.listForRepo({
61-
owner: this.context.owner,
62-
repo: this.issueRepo,
63-
labels: [LABELS.security],
64-
state: STATES.open,
65-
});
66-
67-
return existedIssues;
68-
}
69-
70-
createAlertDictionary (existedIssues) {
71-
return existedIssues.reduce((res, issue) => {
72-
const [, repo] = issue.body.match(/Repository:\s*`(.*)`/);
73-
const [, url, type, number] = issue.body.match(/Link:\s*(https:.*\/(dependabot|code-scanning)\/(\d+))/);
74-
75-
if (!url || repo !== this.context.repo)
76-
return res;
77-
78-
res[url] = { issue, number, type };
79-
80-
return res;
81-
}, {});
82-
}
83-
84-
async closeSpoiledIssues () {
85-
for (const key in this.alertDictionary) {
86-
const alert = this.alertDictionary[key];
87-
88-
if (alert.type === ALERT_TYPES.dependabot) {
89-
const isAlertOpened = await this.isDependabotAlertOpened(alert.number);
90-
91-
if (isAlertOpened)
92-
continue;
93-
94-
await this.closeIssue(alert.issue.number);
95-
}
96-
}
97-
}
98-
99-
async isDependabotAlertOpened (alertNumber) {
100-
const alert = await this.getDependabotAlertInfo(alertNumber);
101-
102-
return alert.state === STATES.open;
103-
}
104-
105-
async getDependabotAlertInfo (alertNumber) {
106-
try {
107-
const { data } = await this.github.rest.dependabot.getAlert({ alert_number: alertNumber, ...this.context });
108-
109-
return data;
110-
}
111-
catch (e) {
112-
if (e.message.includes('No alert found for alert number'))
113-
return {};
114-
115-
throw e;
116-
}
117-
}
118-
119-
async closeIssue (issueNumber) {
120-
return this.github.rest.issues.update({
121-
owner: this.context.owner,
122-
repo: this.issueRepo,
123-
issue_number: issueNumber,
124-
state: STATES.closed,
125-
});
126-
}
127-
128-
async createDependabotlIssues (dependabotAlerts) {
129-
for (const alert of dependabotAlerts) {
130-
if (!this.needCreateIssue(alert))
131-
return;
132-
133-
await this.createIssue({
134-
labels: [LABELS.dependabot, LABELS.security, alert.dependency.scope],
135-
originRepo: this.context.repo,
136-
summary: alert.security_advisory.summary,
137-
description: alert.security_advisory.description,
138-
link: alert.html_url,
139-
issuePackage: alert.dependency.package.name,
140-
});
141-
}
142-
}
143-
144-
async createCodeqlIssues (codeqlAlerts) {
145-
for (const alert of codeqlAlerts) {
146-
if (!this.needCreateIssue(alert))
147-
return;
148-
149-
await this.createIssue({
150-
labels: [LABELS.codeql, LABELS.security],
151-
originRepo: this.context.repo,
152-
summary: alert.rule.description,
153-
description: alert.most_recent_instance.message.text,
154-
link: alert.html_url,
155-
});
156-
}
157-
}
158-
159-
needCreateIssue (alert) {
160-
return !this.alertDictionary[alert.html_url] && Date.now() - new Date(alert.created_at) <= 1000 * 60 * 60 * 24;
161-
}
162-
163-
async createIssue ({ labels, originRepo, summary, description, link, issuePackage = '' }) {
164-
const title = `[${originRepo}] ${summary}`;
165-
const body = ''
166-
+ `#### Repository: \`${originRepo}\`\n`
167-
+ (issuePackage ? `#### Package: \`${issuePackage}\`\n` : '')
168-
+ `#### Description:\n`
169-
+ `${description}\n`
170-
+ `#### Link: ${link}`;
171-
172-
return this.github.rest.issues.create({
173-
title, body, labels,
174-
owner: this.context.owner,
175-
repo: this.issueRepo,
176-
});
177-
}
23+
constructor(github, context, issueRepo) {
24+
this.github = github;
25+
this.issueRepo = issueRepo;
26+
this.context = {
27+
owner: context.repo.owner,
28+
repo: context.repo.repo,
29+
};
30+
}
31+
32+
async check() {
33+
const dependabotAlerts = await this.getDependabotAlerts();
34+
const codeqlAlerts = await this.getCodeqlAlerts();
35+
const existedIssues = await this.getExistedIssues();
36+
37+
this.alertDictionary = this.createAlertDictionary(existedIssues);
38+
39+
await this.closeSpoiledIssues();
40+
await this.createDependabotlIssues(dependabotAlerts);
41+
await this.createCodeqlIssues(codeqlAlerts);
42+
}
43+
44+
async getDependabotAlerts() {
45+
const { data } = await this.github.rest.dependabot.listAlertsForRepo({ state: STATES.open, ...this.context });
46+
47+
return data;
48+
}
49+
50+
async getCodeqlAlerts() {
51+
try {
52+
const { data } = await this.github.rest.codeScanning.listAlertsForRepo({ state: STATES.open, ...this.context });
53+
54+
return data;
55+
}
56+
catch (e) {
57+
if (e.message.includes('no analysis found') || e.message.includes('Advanced Security must be enabled for this repository to use code scanning'))
58+
return [];
59+
60+
throw e;
61+
}
62+
}
63+
64+
async getExistedIssues() {
65+
const { data: existedIssues } = await this.github.rest.issues.listForRepo({
66+
owner: this.context.owner,
67+
repo: this.issueRepo,
68+
labels: [LABELS.security],
69+
state: STATES.open,
70+
});
71+
72+
return existedIssues;
73+
}
74+
75+
createAlertDictionary(existedIssues) {
76+
return existedIssues.reduce((res, issue) => {
77+
const [, url, type] = issue.body.match(/(https:.*\/(dependabot|code-scanning)\/(\d+))/);
78+
79+
if (!url)
80+
return res;
81+
82+
if (type === ALERT_TYPES.dependabot) {
83+
const [, cveId] = issue.body.match(/CVE ID:\s*`(.*)`/);
84+
const [, ghsaId] = issue.body.match(/GHSA ID:\s*`(.*)`/);
85+
86+
res.set(issue.title, { issue, type, cveId, ghsaId });
87+
}
88+
else
89+
res.set(issue.title, { issue, type })
90+
91+
92+
return res;
93+
}, new Map());
94+
}
95+
96+
async closeSpoiledIssues() {
97+
const regExpAlertNumber = new RegExp(`(?<=\`${this.context.repo}\` - https:.*/dependabot/)\\d+`);
98+
for (const alert of this.alertDictionary.values()) {
99+
100+
if (alert.type === ALERT_TYPES.dependabot) {
101+
const alertNumber = alert.issue.body.match(regExpAlertNumber);
102+
103+
if (!alertNumber)
104+
continue;
105+
106+
const isAlertOpened = await this.isDependabotAlertOpened(alertNumber);
107+
108+
if (isAlertOpened)
109+
continue;
110+
111+
await this.updateIssue(alert, UPDATE_TYPE.closeTask);
112+
}
113+
}
114+
}
115+
116+
async isDependabotAlertOpened(alertNumber) {
117+
const alert = await this.getDependabotAlertInfo(alertNumber);
118+
119+
return alert.state === STATES.open;
120+
}
121+
122+
async getDependabotAlertInfo(alertNumber) {
123+
try {
124+
const { data } = await this.github.rest.dependabot.getAlert({ alert_number: alertNumber, ...this.context });
125+
126+
return data;
127+
}
128+
catch (e) {
129+
if (e.message.includes('No alert found for alert number'))
130+
return {};
131+
132+
throw e;
133+
}
134+
}
135+
136+
async updateIssue(alert, type) {
137+
const updates = {};
138+
139+
if (type === UPDATE_TYPE.addAlertToIssue) {
140+
const { issue } = this.alertDictionary.get(alert.security_advisory.summary);
141+
142+
updates.issue_number = issue.number;
143+
updates.body = issue.body.replace(/(?<=Repositories:)[\s\S]*?(?=####|$)/g, (match) => {
144+
return match + `- [ ] \`${this.context.repo}\` - ${alert.html_url}\n`;
145+
});
146+
}
147+
148+
if (type === UPDATE_TYPE.closeTask) {
149+
updates.body = alert.issue.body.replace(new RegExp(`\\[ \\](?= \`${this.context.repo}\`)`), '[x]');
150+
updates.state = !updates.body.match(/\[ \]/) ? STATES.closed : STATES.open;
151+
updates.issue_number = alert.issue.number;
152+
}
153+
154+
return this.github.rest.issues.update({
155+
owner: this.context.owner,
156+
repo: this.issueRepo,
157+
...updates,
158+
});
159+
}
160+
161+
162+
async createDependabotlIssues(dependabotAlerts) {
163+
for (const alert of dependabotAlerts) {
164+
if (this.needAddAlertToIssue(alert)) {
165+
await this.updateIssue(alert, UPDATE_TYPE.addAlertToIssue);
166+
}
167+
else if (this.needCreateIssue(alert)) {
168+
await this.createIssue({
169+
labels: [LABELS.dependabot, LABELS.security, alert.dependency.scope],
170+
originRepo: this.context.repo,
171+
summary: alert.security_advisory.summary,
172+
description: alert.security_advisory.description,
173+
link: alert.html_url,
174+
issuePackage: alert.dependency.package.name,
175+
cveId: alert.security_advisory.cve_id,
176+
ghsaId: alert.security_advisory.ghsa_id,
177+
});
178+
}
179+
}
180+
}
181+
182+
needAddAlertToIssue(alert) {
183+
const existedIssue = this.alertDictionary.get(alert.security_advisory.summary);
184+
185+
return existedIssue
186+
&& existedIssue.cveId === alert.security_advisory.cve_id
187+
&& existedIssue.ghsaId === alert.security_advisory.ghsa_id
188+
&& !existedIssue.issue.body.includes(`\`${this.context.repo}\``);
189+
}
190+
191+
async createCodeqlIssues(codeqlAlerts) {
192+
for (const alert of codeqlAlerts) {
193+
if (!this.needCreateIssue(alert, false))
194+
continue;
195+
196+
await this.createIssue({
197+
labels: [LABELS.codeql, LABELS.security],
198+
originRepo: this.context.repo,
199+
summary: alert.rule.description,
200+
description: alert.most_recent_instance.message.text,
201+
link: alert.html_url,
202+
}, false);
203+
}
204+
}
205+
206+
needCreateIssue(alert, isDependabotAlert = true) {
207+
const dictionaryKey = isDependabotAlert ? alert.security_advisory.summary : `[${this.context.repo}] ${alert.rule.description}`;
208+
209+
return !this.alertDictionary.get(dictionaryKey) && Date.now() - new Date(alert.created_at) <= 1000 * 60 * 60 * 24;
210+
}
211+
212+
async createIssue({ labels, originRepo, summary, description, link, issuePackage = '', cveId, ghsaId }, isDependabotAlert = true) {
213+
const title = isDependabotAlert ? `${summary}` : `[${originRepo}] ${summary}`;
214+
let body = ''
215+
+ `#### Repositories:\n`
216+
+ `- [ ] \`${originRepo}\` - ${link}\n`
217+
+ (issuePackage ? `#### Package: \`${issuePackage}\`\n` : '')
218+
+ `#### Description:\n`
219+
+ `${description}\n`;
220+
221+
if (isDependabotAlert)
222+
body += `\n#### CVE ID: \`${cveId}\`\n#### GHSA ID: \`${ghsaId}\``;
223+
224+
return this.github.rest.issues.create({
225+
title, body, labels,
226+
owner: this.context.owner,
227+
repo: this.issueRepo,
228+
});
229+
}
178230
}
179231

180232
export default SecurityChecker;

0 commit comments

Comments
 (0)