Merge pull request #7746 from backstage/mob/snyk-issue-creation-workflow
Opening Github issues for Snyk vulnerabilities
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
name: 'Snyk Github Issue Sync'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 */4 * * *'
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Create Snyk report
|
||||
uses: snyk/actions/node@master
|
||||
with:
|
||||
args: >
|
||||
--yarn-workspaces
|
||||
--org=backstage-dgh
|
||||
--strict-out-of-sync=false
|
||||
--json-file-output=snyk.json
|
||||
json: true
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
- name: Update Github issues
|
||||
run: yarn ts-node scripts/snyk-github-issue-sync.ts
|
||||
+3
-1
@@ -58,9 +58,10 @@
|
||||
"@microsoft/tsdoc": "^0.13.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@changesets/cli": "^2.14.0",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
"@spotify/prettier-config": "^11.0.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"command-exists": "^1.2.9",
|
||||
"concurrently": "^6.0.0",
|
||||
"eslint-plugin-notice": "^0.9.10",
|
||||
@@ -68,6 +69,7 @@
|
||||
"husky": "^6.0.0",
|
||||
"lerna": "^4.0.0",
|
||||
"lint-staged": "^11.1.2",
|
||||
"minimist": "^1.2.5",
|
||||
"prettier": "^2.2.1",
|
||||
"shx": "^0.3.2",
|
||||
"yarn-lock-check": "^1.0.5"
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import minimist from 'minimist';
|
||||
// Generated by GitHub workflow .github/workflows/snyk-github-issue-creator
|
||||
import synkJsonOutput from '../snyk.json';
|
||||
|
||||
type Vulnerability = {
|
||||
description: string;
|
||||
packages: {
|
||||
name: string;
|
||||
target: string;
|
||||
}[];
|
||||
snykId: string;
|
||||
};
|
||||
|
||||
const argv = minimist(process.argv.slice(2));
|
||||
|
||||
const GH_OWNER = 'backstage';
|
||||
const GH_REPO = 'backstage';
|
||||
const SNYK_GH_LABEL = 'snyk-vulnerability';
|
||||
const SNYK_ID_REGEX = /\[([A-Z0-9-:]+)]/i;
|
||||
|
||||
const isDryRun = 'dryrun' in argv;
|
||||
|
||||
if (!process.env.GITHUB_TOKEN) {
|
||||
console.error('GITHUB_TOKEN is not set. Please provide a Github token');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const octokit = new Octokit({
|
||||
auth: process.env.GITHUB_TOKEN,
|
||||
});
|
||||
|
||||
if (isDryRun) {
|
||||
console.log(
|
||||
'⚠️ Running in dryrun mode, no issues will be updated on Github ⚠️',
|
||||
);
|
||||
}
|
||||
|
||||
const fetchSnykGithubIssueMap = async (): Promise<Record<string, number>> => {
|
||||
const snykGithubIssueMap: Record<string, number> = {};
|
||||
|
||||
const iterator = octokit.paginate.iterator(octokit.rest.issues.listForRepo, {
|
||||
owner: GH_OWNER,
|
||||
repo: GH_REPO,
|
||||
per_page: 100,
|
||||
state: 'open',
|
||||
labels: SNYK_GH_LABEL,
|
||||
});
|
||||
|
||||
for await (const { data: issues } of iterator) {
|
||||
for (const issue of issues) {
|
||||
// Gets the Vulnerability ID from square braces
|
||||
const match = SNYK_ID_REGEX.exec(issue.title);
|
||||
|
||||
if (match && match[1]) {
|
||||
snykGithubIssueMap[match[1]] = issue.number;
|
||||
} else {
|
||||
console.log(`Unmatched Snyk ID for ${issue.title}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return snykGithubIssueMap;
|
||||
};
|
||||
|
||||
const generateIssueBody = (vulnerability: Vulnerability) => `
|
||||
## Affecting Packages/Plugins
|
||||
|
||||
${Array.from(vulnerability.packages).map(
|
||||
({ name, target }) => `* [${name}](${target})\n`,
|
||||
)}
|
||||
|
||||
${vulnerability.description}
|
||||
`;
|
||||
|
||||
const createGithubIssue = async (vulnerability: Vulnerability) => {
|
||||
console.log(
|
||||
`Create Github Issue for Snyk Vulnerability ${vulnerability.snykId}`,
|
||||
);
|
||||
|
||||
vulnerability.packages.forEach(({ name, target }) => {
|
||||
console.log(`- ${name} [${target}]`);
|
||||
});
|
||||
|
||||
if (!isDryRun) {
|
||||
await octokit.issues.create({
|
||||
owner: GH_OWNER,
|
||||
repo: GH_REPO,
|
||||
title: `Snyk vulnerability [${vulnerability.snykId}]`,
|
||||
labels: [SNYK_GH_LABEL, 'help wanted'],
|
||||
body: generateIssueBody(vulnerability),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateGithubIssue = async (
|
||||
githubIssueId: number,
|
||||
vulnerability: Vulnerability,
|
||||
) => {
|
||||
console.log(
|
||||
`Update Github Issue #${githubIssueId} for Snky Vulnerability ${vulnerability.snykId}`,
|
||||
);
|
||||
|
||||
if (!isDryRun) {
|
||||
await octokit.issues.update({
|
||||
owner: GH_OWNER,
|
||||
repo: GH_REPO,
|
||||
issue_number: githubIssueId,
|
||||
body: generateIssueBody(vulnerability),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const closeGithubIssue = async (githubIssueId: number) => {
|
||||
console.log(`Closing Github Issue #${githubIssueId}`);
|
||||
|
||||
if (!isDryRun) {
|
||||
await octokit.issues.update({
|
||||
owner: GH_OWNER,
|
||||
repo: GH_REPO,
|
||||
issue_number: githubIssueId,
|
||||
state: 'closed',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const snykGithubIssueMap = await fetchSnykGithubIssueMap();
|
||||
const vulnerabilityStore: Record<string, Vulnerability> = {};
|
||||
|
||||
// Group the Snyk vulnerabilities, and link back to the affecting packages.
|
||||
synkJsonOutput.forEach(
|
||||
({ projectName, displayTargetFile, vulnerabilities }) => {
|
||||
vulnerabilities.forEach(
|
||||
({ id, description }: { id: string; description: string }) => {
|
||||
if (id !== undefined && description !== undefined) {
|
||||
if (vulnerabilityStore[id]) {
|
||||
if (
|
||||
!vulnerabilityStore[id].packages.some(
|
||||
({ name }) => name === projectName,
|
||||
)
|
||||
) {
|
||||
vulnerabilityStore[id].packages.push({
|
||||
name: projectName,
|
||||
target: displayTargetFile,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
vulnerabilityStore[id] = {
|
||||
description,
|
||||
snykId: id,
|
||||
packages: [
|
||||
{
|
||||
name: projectName,
|
||||
target: displayTargetFile,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
for (const [id, vulnerability] of Object.entries(vulnerabilityStore)) {
|
||||
if (snykGithubIssueMap[id]) {
|
||||
await updateGithubIssue(snykGithubIssueMap[id], vulnerability);
|
||||
} else {
|
||||
await createGithubIssue(vulnerability);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [snykId, githubIssueId] of Object.entries(snykGithubIssueMap)) {
|
||||
if (!vulnerabilityStore[snykId]) {
|
||||
await closeGithubIssue(githubIssueId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(error => {
|
||||
console.error(error.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -4893,6 +4893,19 @@
|
||||
before-after-hook "^2.1.0"
|
||||
universal-user-agent "^6.0.0"
|
||||
|
||||
"@octokit/core@^3.5.1":
|
||||
version "3.5.1"
|
||||
resolved "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz#8601ceeb1ec0e1b1b8217b960a413ed8e947809b"
|
||||
integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw==
|
||||
dependencies:
|
||||
"@octokit/auth-token" "^2.4.4"
|
||||
"@octokit/graphql" "^4.5.8"
|
||||
"@octokit/request" "^5.6.0"
|
||||
"@octokit/request-error" "^2.0.5"
|
||||
"@octokit/types" "^6.0.3"
|
||||
before-after-hook "^2.2.0"
|
||||
universal-user-agent "^6.0.0"
|
||||
|
||||
"@octokit/endpoint@^6.0.1":
|
||||
version "6.0.3"
|
||||
resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.3.tgz#dd09b599662d7e1b66374a177ab620d8cdf73487"
|
||||
@@ -4927,6 +4940,11 @@
|
||||
"@octokit/types" "^6.12.2"
|
||||
btoa-lite "^1.0.0"
|
||||
|
||||
"@octokit/openapi-types@^11.2.0":
|
||||
version "11.2.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6"
|
||||
integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==
|
||||
|
||||
"@octokit/openapi-types@^7.3.2":
|
||||
version "7.3.2"
|
||||
resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-7.3.2.tgz#065ce49b338043ec7f741316ce06afd4d459d944"
|
||||
@@ -4937,6 +4955,13 @@
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437"
|
||||
integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw==
|
||||
|
||||
"@octokit/plugin-paginate-rest@^2.16.8":
|
||||
version "2.17.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz#32e9c7cab2a374421d3d0de239102287d791bce7"
|
||||
integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==
|
||||
dependencies:
|
||||
"@octokit/types" "^6.34.0"
|
||||
|
||||
"@octokit/plugin-paginate-rest@^2.6.2":
|
||||
version "2.7.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.0.tgz#6bb7b043c246e0654119a6ec4e72a172c9e2c7f3"
|
||||
@@ -4949,6 +4974,11 @@
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz#394d59ec734cd2f122431fbaf05099861ece3c44"
|
||||
integrity sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg==
|
||||
|
||||
"@octokit/plugin-request-log@^1.0.4":
|
||||
version "1.0.4"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85"
|
||||
integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods@5.3.1":
|
||||
version "5.3.1"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.3.1.tgz#deddce769b4ec3179170709ab42e4e9e6195aaa9"
|
||||
@@ -4957,6 +4987,14 @@
|
||||
"@octokit/types" "^6.16.2"
|
||||
deprecation "^2.3.1"
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods@^5.12.0":
|
||||
version "5.13.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz#8c46109021a3412233f6f50d28786f8e552427ba"
|
||||
integrity sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==
|
||||
dependencies:
|
||||
"@octokit/types" "^6.34.0"
|
||||
deprecation "^2.3.1"
|
||||
|
||||
"@octokit/request-error@^2.0.0", "@octokit/request-error@^2.0.2", "@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0":
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677"
|
||||
@@ -4988,6 +5026,16 @@
|
||||
"@octokit/plugin-request-log" "^1.0.2"
|
||||
"@octokit/plugin-rest-endpoint-methods" "5.3.1"
|
||||
|
||||
"@octokit/rest@^18.12.0":
|
||||
version "18.12.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz#f06bc4952fc87130308d810ca9d00e79f6988881"
|
||||
integrity sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==
|
||||
dependencies:
|
||||
"@octokit/core" "^3.5.1"
|
||||
"@octokit/plugin-paginate-rest" "^2.16.8"
|
||||
"@octokit/plugin-request-log" "^1.0.4"
|
||||
"@octokit/plugin-rest-endpoint-methods" "^5.12.0"
|
||||
|
||||
"@octokit/types@^5.0.0", "@octokit/types@^5.0.1":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b"
|
||||
@@ -5002,6 +5050,13 @@
|
||||
dependencies:
|
||||
"@octokit/openapi-types" "^7.3.2"
|
||||
|
||||
"@octokit/types@^6.34.0":
|
||||
version "6.34.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz#c6021333334d1ecfb5d370a8798162ddf1ae8218"
|
||||
integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==
|
||||
dependencies:
|
||||
"@octokit/openapi-types" "^11.2.0"
|
||||
|
||||
"@octokit/webhooks-methods@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.npmjs.org/@octokit/webhooks-methods/-/webhooks-methods-2.0.0.tgz#1108b9ea661ca6c81e4a8bfa63a09eb27d5bc2db"
|
||||
@@ -9898,6 +9953,11 @@ before-after-hook@^2.1.0:
|
||||
resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635"
|
||||
integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==
|
||||
|
||||
before-after-hook@^2.2.0:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
|
||||
integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==
|
||||
|
||||
better-opn@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.npmjs.org/better-opn/-/better-opn-2.1.1.tgz#94a55b4695dc79288f31d7d0e5f658320759f7c6"
|
||||
|
||||
Reference in New Issue
Block a user