Updated script to create, update and close github issues
Signed-off-by: Harry Hogg <hhogg@spotify.com>
This commit is contained in:
@@ -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 => {
|
||||
|
||||
Reference in New Issue
Block a user