diff --git a/.github/workflows/snyk-github-issue-creator.yml b/.github/workflows/snyk-github-issue-creator.yml deleted file mode 100644 index ad50eef0cc..0000000000 --- a/.github/workflows/snyk-github-issue-creator.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Create and Update Github Issues from Snyk report - -on: - [push, pull_request] - # workflow_dispatch: - # pull_request: - # schedule: - # - cron: '0 */4 * * *' # every 4 hours - -jobs: - sync: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - name: Run Snyk to check for vulnerabilities - uses: snyk/actions/node@master - continue-on-error: - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - with: - args: --yarn-workspaces --strict-out-of-sync=false - json: true - - - name: Run the Snyk Github Issue command - run: yarn ts-node scripts/snyk-github-issue-sync.ts diff --git a/.github/workflows/snyk-monitor.yml b/.github/workflows/snyk-monitor.yml index 0adb6c5bcb..32df37f35b 100644 --- a/.github/workflows/snyk-monitor.yml +++ b/.github/workflows/snyk-monitor.yml @@ -43,9 +43,14 @@ jobs: --org=backstage-dgh --strict-out-of-sync=false --sarif-file-output=snyk.sarif + --json-file-output=snyk.json + json: true env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - name: Upload Snyk report uses: github/codeql-action/upload-sarif@v1 with: sarif_file: snyk.sarif + + - name: Update Github issues + run: yarn ts-node scripts/snyk-github-issue-sync.ts diff --git a/package.json b/package.json index 77ec43ee24..94de50e792 100644 --- a/package.json +++ b/package.json @@ -59,8 +59,8 @@ "@microsoft/tsdoc": "^0.13.2" }, "devDependencies": { - "@octokit/rest": "^18.12.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", @@ -70,6 +70,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" diff --git a/scripts/snyk-github-issue-sync.ts b/scripts/snyk-github-issue-sync.ts index ed5082717d..d7f64cf1ff 100644 --- a/scripts/snyk-github-issue-sync.ts +++ b/scripts/snyk-github-issue-sync.ts @@ -13,46 +13,65 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// eslint-disable-next-line import/no-extraneous-dependencies +/* 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'; -// Pattern for a GitHub Issue title -// Snyk vulnerability [Vulnerability ID] - type Vulnerability = { description: string; + packages: { + name: string; + target: string; + }[]; snykId: string; - packages: Set; }; -// Remember to fix me! -const GH_OWNER = 'orkohunter'; +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> => { 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', + 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 = /\([([A-Z0-9-]+)\])/.exec(issue.title); + const match = SNYK_ID_REGEX.exec(issue.title); if (match && match[1]) { - snykGithubIssueMap[match[1]] = issue.id; + snykGithubIssueMap[match[1]] = issue.number; + } else { + console.log(`Unmatched Snyk ID for ${issue.title}`); } } } @@ -60,86 +79,119 @@ const fetchSnykGithubIssueMap = async (): Promise> => { 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 generateIssueBody = (vulnerability: Vulnerability) => ` +## Affecting Packages/Plugins -const createGithubIssue = (vulnerability: Vulnerability) => { +${Array.from(vulnerability.packages).map( + ({ name, target }) => `* [${name}](${target})\n`, +)} + +${vulnerability.description} +`; + +const createGithubIssue = async (vulnerability: Vulnerability) => { console.log( - `Create issue for vulnerability ${ - vulnerability.snykId - } affecting packages ${Array.from(vulnerability.packages)}`, + `Create Github Issue for Snyk Vulnerability ${vulnerability.snykId}`, ); - octokit.issues.create({ - owner: GH_OWNER, - repo: GH_REPO, - title: `Snyk vulnerability [${vulnerability.snykId}]`, - labels: ['snyk-vulnerability', 'help wanted'], - body: generateIssueBody(vulnerability), + + 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 = ( +const updateGithubIssue = async ( githubIssueId: number, vulnerability: Vulnerability, ) => { console.log( - `Update issue ${githubIssueId} for vulnerability ${vulnerability.snykId}`, + `Update Github Issue #${githubIssueId} for Snky Vulnerability ${vulnerability.snykId}`, ); - // TODO(hhogg): Update github issue with the contents from a Snyk issue. + + if (!isDryRun) { + await octokit.issues.update({ + owner: GH_OWNER, + repo: GH_REPO, + issue_number: githubIssueId, + body: generateIssueBody(vulnerability), + }); + } }; -const closeGithubIssue = (githubIssueId: number) => { - console.log(`Delete issue ${githubIssueId}`); - // TODO(hhogg): Delete a github issue +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 = {}; - // 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]), - }; + // 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, + }, + ], + }; + } } - } - }, - ); - }); + }, + ); + }, + ); - // Loop over the grouped vulnerabilities and create/update accordingly - Object.entries(vulnerabilityStore).forEach(([id, vulnerability]) => { + for (const [id, vulnerability] of Object.entries(vulnerabilityStore)) { if (snykGithubIssueMap[id]) { - updateGithubIssue(snykGithubIssueMap[id], vulnerability); + await updateGithubIssue(snykGithubIssueMap[id], vulnerability); } else { - createGithubIssue(vulnerability); + await createGithubIssue(vulnerability); } - }); + } - // Loop over the Github issues and delete accordingly. - Object.entries(snykGithubIssueMap).forEach(([snykId, githubIssueId]) => { - if (!snykGithubIssueMap[snykId]) { - closeGithubIssue(githubIssueId); + for (const [snykId, githubIssueId] of Object.entries(snykGithubIssueMap)) { + if (!vulnerabilityStore[snykId]) { + await closeGithubIssue(githubIssueId); } - }); + } } main().catch(error => {