Updated script to create, update and close github issues

Signed-off-by: Harry Hogg <hhogg@spotify.com>
This commit is contained in:
Harry Hogg
2021-10-28 13:04:14 +01:00
parent bf76bb7a1d
commit 7205d37a14
4 changed files with 124 additions and 93 deletions
@@ -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
+5
View File
@@ -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
+2 -1
View File
@@ -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"
+117 -65
View File
@@ -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<string>;
};
// 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<Record<string, number>> => {
const snykGithubIssueMap: Record<string, number> = {};
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<Record<string, number>> => {
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<string, Vulnerability> = {};
// 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 => {