/* * 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-next-line import/no-extraneous-dependencies import { Octokit } from '@octokit/rest'; // Generated by GitHub workflow .github/workflows/snyk-github-issue-creator import synkJsonOutput from '../snyk.json'; // Pattern for a GitHub Issue title // Snyk vulnerability [Vulnerability ID] type Vulnerability = { description: string; snykId: string; packages: Set; }; // Remember to fix me! const GH_OWNER = 'orkohunter'; const GH_REPO = 'backstage'; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN, }); const fetchSnykGithubIssueMap = async (): Promise> => { const snykGithubIssueMap: Record = {}; const iterator = octokit.paginate.iterator(octokit.rest.issues.listForRepo, { // TODO(Harry/Himanshu): Use a CLI flag for these values. owner: GH_OWNER, repo: GH_REPO, per_page: 100, labels: 'snyk-vulnerability', }); for await (const { data: issues } of iterator) { for (const issue of issues) { // Gets the Vulnerability ID from square braces const match = /\([([A-Z0-9-]+)\])/.exec(issue.title); if (match && match[1]) { snykGithubIssueMap[match[1]] = issue.id; } } } return snykGithubIssueMap; }; const generateIssueBody = (vulnerability: Vulnerability) => { let issueBody = ''; issueBody += '## Affecting Packages/Plugins\n'; vulnerability.packages.forEach(pkgName => { issueBody += `* ${pkgName}\n`; }); // TODO: Use displayTargetFile in snyk.json to create hyperlinks issueBody += '\n'; issueBody += vulnerability.description; return issueBody; }; const createGithubIssue = (vulnerability: Vulnerability) => { console.log( `Create issue for vulnerability ${ vulnerability.snykId } affecting packages ${Array.from(vulnerability.packages)}`, ); octokit.issues.create({ owner: GH_OWNER, repo: GH_REPO, title: `Snyk vulnerability [${vulnerability.snykId}]`, labels: ['snyk-vulnerability', 'help wanted'], body: generateIssueBody(vulnerability), }); }; const updateGithubIssue = ( githubIssueId: number, vulnerability: Vulnerability, ) => { console.log( `Update issue ${githubIssueId} for vulnerability ${vulnerability.snykId}`, ); // TODO(hhogg): Update github issue with the contents from a Snyk issue. }; const closeGithubIssue = (githubIssueId: number) => { console.log(`Delete issue ${githubIssueId}`); // TODO(hhogg): Delete a github issue }; async function main() { const snykGithubIssueMap = await fetchSnykGithubIssueMap(); const vulnerabilityStore: Record = {}; // Group the Snyk vulnerabilities, and aggregate the affecting packages. synkJsonOutput.forEach(({ projectName, vulnerabilities }) => { vulnerabilities.forEach( ({ id, description }: { id: string; description: string }) => { if (id !== undefined && description !== undefined) { if (vulnerabilityStore[id]) { vulnerabilityStore[id].packages.add(projectName); } else { vulnerabilityStore[id] = { description, snykId: id, packages: new Set([projectName]), }; } } }, ); }); // Loop over the grouped vulnerabilities and create/update accordingly Object.entries(vulnerabilityStore).forEach(([id, vulnerability]) => { if (snykGithubIssueMap[id]) { updateGithubIssue(snykGithubIssueMap[id], vulnerability); } else { createGithubIssue(vulnerability); } }); // Loop over the Github issues and delete accordingly. Object.entries(snykGithubIssueMap).forEach(([snykId, githubIssueId]) => { if (!snykGithubIssueMap[snykId]) { closeGithubIssue(githubIssueId); } }); } main().catch(error => { console.error(error.stack); process.exit(1); });