From f1a2b0c10012df46350ff56db6cc54a0dbf22134 Mon Sep 17 00:00:00 2001 From: Popov Aleksey Date: Tue, 28 Nov 2023 17:50:43 +0200 Subject: [PATCH 1/7] feat: added scope for dependabot issues --- workflows/check-security-alerts.yml | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/workflows/check-security-alerts.yml b/workflows/check-security-alerts.yml index 2c0c022..d071988 100644 --- a/workflows/check-security-alerts.yml +++ b/workflows/check-security-alerts.yml @@ -18,9 +18,12 @@ jobs: const { owner, repo } = context.repo; const state = 'open'; - const dependabotLabel = 'dependabot'; - const codeqlLabel = 'codeql'; - const securityLabel = 'security notification'; + + const labels = { + dependabot: 'dependabot', + codeq: 'codeql', + security: 'security notification', + }; async function getDependabotAlerts () { const dependabotListAlertsUrl = `https://api.github.com/repos/${ owner }/${ repo }/dependabot/alerts?state=${ state }`; @@ -68,7 +71,7 @@ jobs: const dependabotAlerts = await getDependabotAlerts(); const codeqlAlerts = await getCodeqlAlerts(); - const {data: existedIssues} = await github.rest.issues.listForRepo({ owner, repo, labels: [securityLabel], state }); + const {data: existedIssues} = await github.rest.issues.listForRepo({ owner, repo, labels: [labels.security], state }); const issueDictionary = existedIssues.reduce((res, issue) => { const alertUrl = issue.body.match(/Link:\s*(https.*\d*)/)?.[1]; @@ -85,7 +88,7 @@ jobs: createIssue({ owner, repo: '${{ secrets.SECURITY_ISSUE_REPO }}', - labels: [dependabotLabel, securityLabel], + labels: [labels.dependabot, labels.security, alert.dependency.scope], originRepo: repo, summary: alert.security_advisory.summary, description: alert.security_advisory.description, @@ -100,7 +103,7 @@ jobs: createIssue({ owner, repo: '${{ secrets.SECURITY_ISSUE_REPO }}', - labels: [codeqlLabel, securityLabel], + labels: [labels.codeql, labels.security], originRepo: repo, summary: alert.rule.description, description: alert.most_recent_instance.message.text, From bef03a85b4e31e21612a47e3514f7b483420819c Mon Sep 17 00:00:00 2001 From: Popov Aleksey Date: Wed, 29 Nov 2023 14:58:17 +0200 Subject: [PATCH 2/7] feat: added removing spoiled issues --- workflows/check-security-alerts.yml | 43 ++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/workflows/check-security-alerts.yml b/workflows/check-security-alerts.yml index d071988..a22c2c8 100644 --- a/workflows/check-security-alerts.yml +++ b/workflows/check-security-alerts.yml @@ -25,6 +25,27 @@ jobs: security: 'security notification', }; + async function getDependabotAlertInfo (alertNumber) { + const dependabotListAlertsUrl = `https://api.github.com/repos/${ owner }/${ repo }/dependabot/alerts/${ alertNumber }`; + const dependabotRequestOptions = { + headers: { 'Authorization': 'Bearer ${{ secrets.ACTIVE_TOKEN }}' } + } + + const response = await fetch(dependabotListAlertsUrl, dependabotRequestOptions); + const data = await response.json(); + + if (data.state) + return data; + + return null; + } + + async function isDependabotAlertOpen (alertNumber) { + const alert = await getDependabotAlertInfo(alertNumber); + + return alert?.state == 'open'; + } + async function getDependabotAlerts () { const dependabotListAlertsUrl = `https://api.github.com/repos/${ owner }/${ repo }/dependabot/alerts?state=${ state }`; const dependabotRequestOptions = { @@ -65,23 +86,37 @@ jobs: } function needCreateIssue (alert) { - return !issueDictionary[alert.html_url] + return !alertDictionary[alert.html_url] && Date.now() - new Date(alert.created_at) <= 1000 * 60 * 60 * 24; } + + async function closeIssue (number) { + return github.rest.issues.create({ owner, repo, issue_number: number, state: 'closed' }); + } const dependabotAlerts = await getDependabotAlerts(); const codeqlAlerts = await getCodeqlAlerts(); const {data: existedIssues} = await github.rest.issues.listForRepo({ owner, repo, labels: [labels.security], state }); - const issueDictionary = existedIssues.reduce((res, issue) => { - const alertUrl = issue.body.match(/Link:\s*(https.*\d*)/)?.[1]; + const alertDictionary = existedIssues.reduce((res, issue) => { + const [,alertUrl, alertNumber] = issue.body.match(/Link:\s*(https.*?(\d+)$)/)?.[1]; if (alertUrl) - res[alertUrl] = issue; + return res; + + res[alertUrl] = { + issue, + number: alertNumber, + }; return res; }, {}) + alertDictionary.forEach(alert => { + if (!isDependabotAlertOpen(alert.number)) + closeIssue(alert.issue.number) + }) + dependabotAlerts.forEach(alert => { if (!needCreateIssue(alert)) return; From d62d949653b72683191209be278101382a767e06 Mon Sep 17 00:00:00 2001 From: Popov Aleksey Date: Wed, 29 Nov 2023 15:03:15 +0200 Subject: [PATCH 3/7] fix: fixed creating alertDictionary --- workflows/check-security-alerts.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workflows/check-security-alerts.yml b/workflows/check-security-alerts.yml index a22c2c8..210ac03 100644 --- a/workflows/check-security-alerts.yml +++ b/workflows/check-security-alerts.yml @@ -99,9 +99,9 @@ jobs: const {data: existedIssues} = await github.rest.issues.listForRepo({ owner, repo, labels: [labels.security], state }); const alertDictionary = existedIssues.reduce((res, issue) => { - const [,alertUrl, alertNumber] = issue.body.match(/Link:\s*(https.*?(\d+)$)/)?.[1]; + const [,alertUrl, alertNumber] = issue.body.match(/Link:\s*(https.*?(\d+)$)/); - if (alertUrl) + if (!alertUrl) return res; res[alertUrl] = { From d31d48b810e3211c651dd81807bff34eb7178d15 Mon Sep 17 00:00:00 2001 From: Popov Aleksey Date: Wed, 29 Nov 2023 15:08:27 +0200 Subject: [PATCH 4/7] refactor: code refactoring --- workflows/check-security-alerts.yml | 140 ++++++++++++++-------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/workflows/check-security-alerts.yml b/workflows/check-security-alerts.yml index 210ac03..71c457c 100644 --- a/workflows/check-security-alerts.yml +++ b/workflows/check-security-alerts.yml @@ -25,75 +25,6 @@ jobs: security: 'security notification', }; - async function getDependabotAlertInfo (alertNumber) { - const dependabotListAlertsUrl = `https://api.github.com/repos/${ owner }/${ repo }/dependabot/alerts/${ alertNumber }`; - const dependabotRequestOptions = { - headers: { 'Authorization': 'Bearer ${{ secrets.ACTIVE_TOKEN }}' } - } - - const response = await fetch(dependabotListAlertsUrl, dependabotRequestOptions); - const data = await response.json(); - - if (data.state) - return data; - - return null; - } - - async function isDependabotAlertOpen (alertNumber) { - const alert = await getDependabotAlertInfo(alertNumber); - - return alert?.state == 'open'; - } - - async function getDependabotAlerts () { - const dependabotListAlertsUrl = `https://api.github.com/repos/${ owner }/${ repo }/dependabot/alerts?state=${ state }`; - const dependabotRequestOptions = { - headers: { 'Authorization': 'Bearer ${{ secrets.ACTIVE_TOKEN }}' } - } - - const response = await fetch(dependabotListAlertsUrl, dependabotRequestOptions); - const data = await response.json(); - - // If data isn't arry somethig goes wrong - if (Array.isArray(data)) - return data; - - return []; - } - - async function getCodeqlAlerts () { - // When CodeQL is turned of it throws error - try { - const { data } = await github.rest.codeScanning.listAlertsForRepo({ owner, repo, state }); - - return data; - } catch (_) { - return []; - } - } - - async function createIssue ({owner, repo, labels, originRepo, summary, description, link, package = ''}) { - const title = `[${originRepo}] ${summary}`; - const body = '' - + `#### Repository: \`${ originRepo }\`\n` - + (!!package ? `#### Package: \`${ package }\`\n` : '') - + `#### Description:\n` - + `${ description }\n` - + `#### Link: ${ link }` - - return github.rest.issues.create({ owner, repo, title, body, labels }); - } - - function needCreateIssue (alert) { - return !alertDictionary[alert.html_url] - && Date.now() - new Date(alert.created_at) <= 1000 * 60 * 60 * 24; - } - - async function closeIssue (number) { - return github.rest.issues.create({ owner, repo, issue_number: number, state: 'closed' }); - } - const dependabotAlerts = await getDependabotAlerts(); const codeqlAlerts = await getCodeqlAlerts(); const {data: existedIssues} = await github.rest.issues.listForRepo({ owner, repo, labels: [labels.security], state }); @@ -144,4 +75,73 @@ jobs: description: alert.most_recent_instance.message.text, link: alert.html_url, }) - }); \ No newline at end of file + }); + + async function getDependabotAlerts () { + const dependabotListAlertsUrl = `https://api.github.com/repos/${ owner }/${ repo }/dependabot/alerts?state=${ state }`; + const dependabotRequestOptions = { + headers: { 'Authorization': 'Bearer ${{ secrets.ACTIVE_TOKEN }}' } + } + + const response = await fetch(dependabotListAlertsUrl, dependabotRequestOptions); + const data = await response.json(); + + // If data isn't arry somethig goes wrong + if (Array.isArray(data)) + return data; + + return []; + } + + async function getCodeqlAlerts () { + // When CodeQL is turned of it throws error + try { + const { data } = await github.rest.codeScanning.listAlertsForRepo({ owner, repo, state }); + + return data; + } catch (_) { + return []; + } + } + + async function isDependabotAlertOpen (alertNumber) { + const alert = await getDependabotAlertInfo(alertNumber); + + return alert?.state == 'open'; + } + + async function getDependabotAlertInfo (alertNumber) { + const dependabotListAlertsUrl = `https://api.github.com/repos/${ owner }/${ repo }/dependabot/alerts/${ alertNumber }`; + const dependabotRequestOptions = { + headers: { 'Authorization': 'Bearer ${{ secrets.ACTIVE_TOKEN }}' } + } + + const response = await fetch(dependabotListAlertsUrl, dependabotRequestOptions); + const data = await response.json(); + + if (data.state) + return data; + + return null; + } + + function needCreateIssue (alert) { + return !alertDictionary[alert.html_url] + && Date.now() - new Date(alert.created_at) <= 1000 * 60 * 60 * 24; + } + + async function createIssue ({owner, repo, labels, originRepo, summary, description, link, package = ''}) { + const title = `[${originRepo}] ${summary}`; + const body = '' + + `#### Repository: \`${ originRepo }\`\n` + + (!!package ? `#### Package: \`${ package }\`\n` : '') + + `#### Description:\n` + + `${ description }\n` + + `#### Link: ${ link }` + + return github.rest.issues.create({ owner, repo, title, body, labels }); + } + + async function closeIssue (number) { + return github.rest.issues.create({ owner, repo, issue_number: number, state: 'closed' }); + } \ No newline at end of file From 534658d7074517cc4468b7a7ca0db914e3458856 Mon Sep 17 00:00:00 2001 From: Popov Aleksey Date: Wed, 29 Nov 2023 17:08:17 +0200 Subject: [PATCH 5/7] fix: different fixes --- workflows/check-security-alerts.yml | 37 ++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/workflows/check-security-alerts.yml b/workflows/check-security-alerts.yml index 71c457c..beef8f6 100644 --- a/workflows/check-security-alerts.yml +++ b/workflows/check-security-alerts.yml @@ -9,7 +9,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v7 with: github-token: ${{ secrets.ACTIVE_TOKEN }} script: | @@ -27,7 +27,12 @@ jobs: const dependabotAlerts = await getDependabotAlerts(); const codeqlAlerts = await getCodeqlAlerts(); - const {data: existedIssues} = await github.rest.issues.listForRepo({ owner, repo, labels: [labels.security], state }); + const {data: existedIssues} = await github.rest.issues.listForRepo({ + owner, + repo: '${{ secrets.SECURITY_ISSUE_REPO }}', + labels: [labels.security], + state + }); const alertDictionary = existedIssues.reduce((res, issue) => { const [,alertUrl, alertNumber] = issue.body.match(/Link:\s*(https.*?(\d+)$)/); @@ -38,15 +43,27 @@ jobs: res[alertUrl] = { issue, number: alertNumber, + isDependabot: alertUrl.includes('dependabot'), }; return res; }, {}) - alertDictionary.forEach(alert => { - if (!isDependabotAlertOpen(alert.number)) - closeIssue(alert.issue.number) - }) + for (const key in alertDictionary) { + var alert = alertDictionary[key]; + + if (alert.isDependabot) { + const isAlertOpened = await isDependabotAlertOpened(alert.number); + + if (isAlertOpened) + continue; + + await closeIssue({owner, + repo: '${{ secrets.SECURITY_ISSUE_REPO }}', + issue_number: alert.issue.number + }) + } + } dependabotAlerts.forEach(alert => { if (!needCreateIssue(alert)) @@ -104,7 +121,7 @@ jobs: } } - async function isDependabotAlertOpen (alertNumber) { + async function isDependabotAlertOpened (alertNumber) { const alert = await getDependabotAlertInfo(alertNumber); return alert?.state == 'open'; @@ -142,6 +159,8 @@ jobs: return github.rest.issues.create({ owner, repo, title, body, labels }); } - async function closeIssue (number) { - return github.rest.issues.create({ owner, repo, issue_number: number, state: 'closed' }); + async function closeIssue ({ owner, repo, issue_number}) { + const state = 'closed'; + + return github.rest.issues.update({ owner, repo, issue_number, state }); } \ No newline at end of file From e0b54cf2a88bd0a6fa675360682311d317faf938 Mon Sep 17 00:00:00 2001 From: Popov Aleksey Date: Wed, 29 Nov 2023 22:16:29 +0200 Subject: [PATCH 6/7] refactor: some refactoring --- workflows/check-security-alerts.yml | 90 ++++++++++++++--------------- 1 file changed, 42 insertions(+), 48 deletions(-) diff --git a/workflows/check-security-alerts.yml b/workflows/check-security-alerts.yml index beef8f6..9da8049 100644 --- a/workflows/check-security-alerts.yml +++ b/workflows/check-security-alerts.yml @@ -17,7 +17,11 @@ jobs: return; const { owner, repo } = context.repo; - const state = 'open'; + + const states = { + open: 'open', + closed: 'closed', + }; const labels = { dependabot: 'dependabot', @@ -31,19 +35,18 @@ jobs: owner, repo: '${{ secrets.SECURITY_ISSUE_REPO }}', labels: [labels.security], - state + state: states.open, }); const alertDictionary = existedIssues.reduce((res, issue) => { - const [,alertUrl, alertNumber] = issue.body.match(/Link:\s*(https.*?(\d+)$)/); + const [,url, number] = issue.body.match(/Link:\s*(https.*?(\d+)$)/); - if (!alertUrl) + if (!url) return res; - res[alertUrl] = { - issue, - number: alertNumber, - isDependabot: alertUrl.includes('dependabot'), + res[url] = { + issue, number, + isDependabot: url.includes('dependabot'), }; return res; @@ -58,10 +61,7 @@ jobs: if (isAlertOpened) continue; - await closeIssue({owner, - repo: '${{ secrets.SECURITY_ISSUE_REPO }}', - issue_number: alert.issue.number - }) + await closeIssue(alert.issue.number) } } @@ -69,8 +69,7 @@ jobs: if (!needCreateIssue(alert)) return; - createIssue({ owner, - repo: '${{ secrets.SECURITY_ISSUE_REPO }}', + createIssue({ labels: [labels.dependabot, labels.security, alert.dependency.scope], originRepo: repo, summary: alert.security_advisory.summary, @@ -84,8 +83,7 @@ jobs: if (!needCreateIssue(alert)) return; - createIssue({ owner, - repo: '${{ secrets.SECURITY_ISSUE_REPO }}', + createIssue({ labels: [labels.codeql, labels.security], originRepo: repo, summary: alert.rule.description, @@ -95,51 +93,41 @@ jobs: }); async function getDependabotAlerts () { - const dependabotListAlertsUrl = `https://api.github.com/repos/${ owner }/${ repo }/dependabot/alerts?state=${ state }`; - const dependabotRequestOptions = { - headers: { 'Authorization': 'Bearer ${{ secrets.ACTIVE_TOKEN }}' } - } - - const response = await fetch(dependabotListAlertsUrl, dependabotRequestOptions); - const data = await response.json(); - - // If data isn't arry somethig goes wrong - if (Array.isArray(data)) - return data; + const { data } = await github.rest.dependabot.listAlertsForRepo({ owner, repo, state: states.open }); - return []; + return data; } async function getCodeqlAlerts () { - // When CodeQL is turned of it throws error try { - const { data } = await github.rest.codeScanning.listAlertsForRepo({ owner, repo, state }); + const { data } = await github.rest.codeScanning.listAlertsForRepo({ owner, repo, state: states.open }); return data; - } catch (_) { - return []; + } catch (e) { + if (e.message.includes('no analysis found')) + return []; + + throw e; } } async function isDependabotAlertOpened (alertNumber) { const alert = await getDependabotAlertInfo(alertNumber); - return alert?.state == 'open'; + return alert.state === states.open; } async function getDependabotAlertInfo (alertNumber) { - const dependabotListAlertsUrl = `https://api.github.com/repos/${ owner }/${ repo }/dependabot/alerts/${ alertNumber }`; - const dependabotRequestOptions = { - headers: { 'Authorization': 'Bearer ${{ secrets.ACTIVE_TOKEN }}' } - } - - const response = await fetch(dependabotListAlertsUrl, dependabotRequestOptions); - const data = await response.json(); + try { + const { data } = await github.rest.dependabot.getAlert({ owner, repo, alert_number: alertNumber }); - if (data.state) return data; + } catch (e) { + if (e.message.includes('No alert found for alert number')) + return {}; - return null; + throw e; + } } function needCreateIssue (alert) { @@ -147,7 +135,7 @@ jobs: && Date.now() - new Date(alert.created_at) <= 1000 * 60 * 60 * 24; } - async function createIssue ({owner, repo, labels, originRepo, summary, description, link, package = ''}) { + async function createIssue ({labels, originRepo, summary, description, link, package = ''}) { const title = `[${originRepo}] ${summary}`; const body = '' + `#### Repository: \`${ originRepo }\`\n` @@ -156,11 +144,17 @@ jobs: + `${ description }\n` + `#### Link: ${ link }` - return github.rest.issues.create({ owner, repo, title, body, labels }); + return github.rest.issues.create({ + owner, title, body, labels, + repo: '${{ secrets.SECURITY_ISSUE_REPO }}', + }); } - async function closeIssue ({ owner, repo, issue_number}) { - const state = 'closed'; - - return github.rest.issues.update({ owner, repo, issue_number, state }); + async function closeIssue (issueNumber) { + return github.rest.issues.update({ + owner, + repo: '${{ secrets.SECURITY_ISSUE_REPO }}', + issue_number: issueNumber, + state: states.closed + }); } \ No newline at end of file From 0ff0fb064c1503d04dd02744c87aa98dbbb83b67 Mon Sep 17 00:00:00 2001 From: Popov Aleksey Date: Mon, 4 Dec 2023 12:57:11 +0200 Subject: [PATCH 7/7] refactor: moved script to separate file --- configs/sync-workflows.yml | 2 + scripts/security-checker.mjs | 177 ++++++++++++++++++++++++++++ workflows/check-security-alerts.yml | 150 ++--------------------- 3 files changed, 186 insertions(+), 143 deletions(-) create mode 100644 scripts/security-checker.mjs diff --git a/configs/sync-workflows.yml b/configs/sync-workflows.yml index 7b3a5fa..07867c5 100644 --- a/configs/sync-workflows.yml +++ b/configs/sync-workflows.yml @@ -49,3 +49,5 @@ group: dest: .github/workflows/ - source: configs/labels.yml dest: .github/labels.yml + - source: scripts/check-security-alerts.mjs + dest: .github/scripts/check-security-alerts.mjs diff --git a/scripts/security-checker.mjs b/scripts/security-checker.mjs new file mode 100644 index 0000000..841c82e --- /dev/null +++ b/scripts/security-checker.mjs @@ -0,0 +1,177 @@ +const STATES = { + open: 'open', + closed: 'closed', +}; + +const LABELS = { + dependabot: 'dependabot', + codeq: 'codeql', + security: 'security notification', +}; + +class SecurityChecker { + constructor (github, context, issueRepo) { + this.github = github; + this.issueRepo = issueRepo; + this.context = { + owner: context.repo.owner, + repo: context.repo.repo, + }; + } + + async check () { + const dependabotAlerts = await this.getDependabotAlerts(); + const codeqlAlerts = await this.getCodeqlAlerts(); + const existedIssues = await this.getExistedIssues(); + + this.alertDictionary = this.createAlertDictionary(existedIssues); + + await this.closeSpoiledIssues(); + this.createDependabotlIssues(dependabotAlerts); + this.createCodeqlIssues(codeqlAlerts); + } + + async getDependabotAlerts () { + const { data } = await this.github.rest.dependabot.listAlertsForRepo({ state: STATES.open, ...this.context }); + + return data; + } + + async getCodeqlAlerts () { + try { + const { data } = await this.github.rest.codeScanning.listAlertsForRepo({ state: STATES.open, ...this.context }); + + return data; + } + catch (e) { + if (e.message.includes('no analysis found')) + return []; + + throw e; + } + } + + async getExistedIssues () { + const { data: existedIssues } = await this.github.rest.issues.listForRepo({ + owner: this.context.owner, + repo: this.issueRepo, + labels: [LABELS.security], + state: STATES.open, + }); + + return existedIssues; + } + + createAlertDictionary (existedIssues) { + return existedIssues.reduce((res, issue) => { + const [, url, number] = issue.body.match(/Link:\s*(https.*?(\d+)$)/); + + if (!url) + return res; + + res[url] = { + issue, number, + isDependabot: url.includes('dependabot'), + }; + + return res; + }, {}); + } + + async closeSpoiledIssues () { + for (const key in this.alertDictionary) { + const alert = this.alertDictionary[key]; + + if (alert.isDependabot) { + const isAlertOpened = await this.isDependabotAlertOpened(alert.number); + + if (isAlertOpened) + continue; + + await this.closeIssue(alert.issue.number); + } + } + } + + async isDependabotAlertOpened (alertNumber) { + const alert = await this.getDependabotAlertInfo(alertNumber); + + return alert.state === STATES.open; + } + + async getDependabotAlertInfo (alertNumber) { + try { + const { data } = await this.github.rest.dependabot.getAlert({ alert_number: alertNumber, ...this.context }); + + return data; + } + catch (e) { + if (e.message.includes('No alert found for alert number')) + return {}; + + throw e; + } + } + + async closeIssue (issueNumber) { + return this.github.rest.issues.update({ + owner: this.context.owner, + repo: this.issueRepo, + issue_number: issueNumber, + state: STATES.closed, + }); + } + + async createDependabotlIssues (dependabotAlerts) { + dependabotAlerts.forEach(alert => { + if (!this.needCreateIssue(alert)) + return; + + this.createIssue({ + labels: [LABELS.dependabot, LABELS.security, alert.dependency.scope], + originRepo: this.context.repo, + summary: alert.security_advisory.summary, + description: alert.security_advisory.description, + link: alert.html_url, + issuePackage: alert.dependency.package.name, + }); + }); + } + + async createCodeqlIssues (codeqlAlerts) { + codeqlAlerts.forEach(alert => { + if (!this.needCreateIssue(alert)) + return; + + this.createIssue({ + labels: [LABELS.codeql, LABELS.security], + originRepo: this.context.repo, + summary: alert.rule.description, + description: alert.most_recent_instance.message.text, + link: alert.html_url, + }); + }); + } + + needCreateIssue (alert) { + return !this.alertDictionary[alert.html_url]; + } + + async createIssue ({ labels, originRepo, summary, description, link, issuePackage = '' }) { + const title = `[${originRepo}] ${summary}`; + const body = '' + + `#### Repository: \`${originRepo}\`\n` + + (issuePackage ? `#### Package: \`${issuePackage}\`\n` : '') + + `#### Description:\n` + + `${description}\n` + + `#### Link: ${link}`; + + return this.github.rest.issues.create({ + title, body, labels, + owner: this.context.owner, + repo: this.issueRepo, + }); + } +} + +export default SecurityChecker; diff --git a/workflows/check-security-alerts.yml b/workflows/check-security-alerts.yml index 9da8049..a699fbf 100644 --- a/workflows/check-security-alerts.yml +++ b/workflows/check-security-alerts.yml @@ -9,152 +9,16 @@ jobs: check: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: latest - uses: actions/github-script@v7 with: github-token: ${{ secrets.ACTIVE_TOKEN }} script: | - if (!'${{secrets.SECURITY_ISSUE_REPO}}') - return; + const {default: SecurityChecker} = await import('${{ github.workspace }}/.github/scripts/security-checker.mjs') - const { owner, repo } = context.repo; + const securityChecker = new SecurityChecker(github, context, '${{secrets.SECURITY_ISSUE_REPO}}'); - const states = { - open: 'open', - closed: 'closed', - }; - - const labels = { - dependabot: 'dependabot', - codeq: 'codeql', - security: 'security notification', - }; - - const dependabotAlerts = await getDependabotAlerts(); - const codeqlAlerts = await getCodeqlAlerts(); - const {data: existedIssues} = await github.rest.issues.listForRepo({ - owner, - repo: '${{ secrets.SECURITY_ISSUE_REPO }}', - labels: [labels.security], - state: states.open, - }); - - const alertDictionary = existedIssues.reduce((res, issue) => { - const [,url, number] = issue.body.match(/Link:\s*(https.*?(\d+)$)/); - - if (!url) - return res; - - res[url] = { - issue, number, - isDependabot: url.includes('dependabot'), - }; - - return res; - }, {}) - - for (const key in alertDictionary) { - var alert = alertDictionary[key]; - - if (alert.isDependabot) { - const isAlertOpened = await isDependabotAlertOpened(alert.number); - - if (isAlertOpened) - continue; - - await closeIssue(alert.issue.number) - } - } - - dependabotAlerts.forEach(alert => { - if (!needCreateIssue(alert)) - return; - - createIssue({ - labels: [labels.dependabot, labels.security, alert.dependency.scope], - originRepo: repo, - summary: alert.security_advisory.summary, - description: alert.security_advisory.description, - link: alert.html_url, - package: alert.dependency.package.name - }) - }); - - codeqlAlerts.forEach(alert => { - if (!needCreateIssue(alert)) - return; - - createIssue({ - labels: [labels.codeql, labels.security], - originRepo: repo, - summary: alert.rule.description, - description: alert.most_recent_instance.message.text, - link: alert.html_url, - }) - }); - - async function getDependabotAlerts () { - const { data } = await github.rest.dependabot.listAlertsForRepo({ owner, repo, state: states.open }); - - return data; - } - - async function getCodeqlAlerts () { - try { - const { data } = await github.rest.codeScanning.listAlertsForRepo({ owner, repo, state: states.open }); - - return data; - } catch (e) { - if (e.message.includes('no analysis found')) - return []; - - throw e; - } - } - - async function isDependabotAlertOpened (alertNumber) { - const alert = await getDependabotAlertInfo(alertNumber); - - return alert.state === states.open; - } - - async function getDependabotAlertInfo (alertNumber) { - try { - const { data } = await github.rest.dependabot.getAlert({ owner, repo, alert_number: alertNumber }); - - return data; - } catch (e) { - if (e.message.includes('No alert found for alert number')) - return {}; - - throw e; - } - } - - function needCreateIssue (alert) { - return !alertDictionary[alert.html_url] - && Date.now() - new Date(alert.created_at) <= 1000 * 60 * 60 * 24; - } - - async function createIssue ({labels, originRepo, summary, description, link, package = ''}) { - const title = `[${originRepo}] ${summary}`; - const body = '' - + `#### Repository: \`${ originRepo }\`\n` - + (!!package ? `#### Package: \`${ package }\`\n` : '') - + `#### Description:\n` - + `${ description }\n` - + `#### Link: ${ link }` - - return github.rest.issues.create({ - owner, title, body, labels, - repo: '${{ secrets.SECURITY_ISSUE_REPO }}', - }); - } - - async function closeIssue (issueNumber) { - return github.rest.issues.update({ - owner, - repo: '${{ secrets.SECURITY_ISSUE_REPO }}', - issue_number: issueNumber, - state: states.closed - }); - } \ No newline at end of file + await securityChecker.check();