This repository has been archived by the owner on Apr 11, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add dependabot alert gh action
Signed-off-by: Vinayak Kukreja <vinakuk@amazon.com>
- Loading branch information
1 parent
ce7dbc7
commit 7799b9e
Showing
12 changed files
with
11,692 additions
and
15 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { Octokit } from '@octokit/rest'; | ||
|
||
const DEPENDABOT_SECURITY_INCIDENT_LABEL = 'dependabot-security-finding'; | ||
const P0_ISSUE_LABEL = 'priority/p0'; | ||
const TRIAGE_LABEL = 'needs-triage'; | ||
|
||
const owner = getRepositoryOwner(); | ||
const repository = getRepositoryName(); | ||
const client = createOctokitClient(); | ||
|
||
/** | ||
* Runs as part of Dependabot Security Notification workflow. | ||
* This creates an issue for any dependabot security alerts that github creates for the repository. | ||
*/ | ||
export async function run() { | ||
const existingIssues = await client.issues.listForRepo({ | ||
owner: owner, | ||
repo: repository, | ||
}); | ||
|
||
// This also returns pull requests, so making sure we are only considering issues | ||
// https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues | ||
const existingDependabotSecurityIssues = existingIssues.data.filter((issue) => | ||
issue.labels.includes(DEPENDABOT_SECURITY_INCIDENT_LABEL) && !('pull_request' in issue) && issue.state === 'open', | ||
); | ||
|
||
const dependabotSecurityIncidents = await client.dependabot.listAlertsForRepo({ | ||
owner: owner, | ||
repo: repository, | ||
}); | ||
|
||
const openSecurityIncidents = dependabotSecurityIncidents.data.filter((incident) => incident.state === 'open'); | ||
|
||
for (const incident of openSecurityIncidents) { | ||
const issueTitle = `[AUTOCUT] Dependabot Security Alert #${incident.number}`; | ||
|
||
const issueExists = existingDependabotSecurityIssues.find((issue) => issue.title === issueTitle); | ||
|
||
if (issueExists === undefined) { | ||
await createDependabotSecurityIssue(issueTitle, incident.html_url); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Helper method to create a dependabot security alert issue. | ||
* @param issueTitle The title of the issue to create. | ||
* @param incidentUrl The URL to the dependabot security alert. | ||
*/ | ||
async function createDependabotSecurityIssue(issueTitle: string, incidentUrl: string) { | ||
await client.issues.create({ | ||
owner: owner, | ||
repo: repository, | ||
title: issueTitle, | ||
body: `Github reported a new dependabot security incident at: ${incidentUrl}`, | ||
labels: [ | ||
DEPENDABOT_SECURITY_INCIDENT_LABEL, | ||
P0_ISSUE_LABEL, | ||
TRIAGE_LABEL, | ||
], | ||
}); | ||
} | ||
|
||
/** | ||
* Create an octokit client. | ||
* @returns Octokit | ||
*/ | ||
export function createOctokitClient(): Octokit { | ||
const token = process.env.GITHUB_TOKEN; | ||
|
||
if (!token) { | ||
throw new Error('GITHUB_TOKEN must be set'); | ||
} | ||
|
||
return new Octokit({ auth: token }); | ||
} | ||
|
||
/** | ||
* Retrieves the repository owner from environment | ||
* @returns Repository owner | ||
*/ | ||
export function getRepositoryOwner(): string { | ||
const ownerName = process.env.OWNER_NAME; | ||
|
||
if (!ownerName) { | ||
throw new Error('OWNER_NAME must be set'); | ||
} | ||
|
||
return ownerName; | ||
} | ||
|
||
/** | ||
* Retrieves the repository name from environment | ||
* @returns Repository name | ||
*/ | ||
export function getRepositoryName(): string { | ||
const repositoryName = process.env.REPO_NAME; | ||
|
||
if (!repositoryName) { | ||
throw new Error('REPO_NAME must be set'); | ||
} | ||
|
||
// Repository name is of format 'owner/repositoryName' | ||
// https://docs.github.com/en/actions/learn-github-actions/contexts#github-context | ||
return repositoryName.split('/')[1]; | ||
} | ||
|
||
run().catch((err) => { | ||
throw new Error(err); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1 @@ | ||
export class Hello { | ||
public sayHello() { | ||
return 'hello, world!'; | ||
} | ||
} | ||
export * from './createDependabotIssue'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
const issueNumber = 4; | ||
const issueTitle = `[AUTOCUT] Dependabot Security Alert #${issueNumber}`; | ||
const issueURL = 'some-url'; | ||
const issueBody = `Github reported a new dependabot security incident at: ${issueURL}`; | ||
const ownerName = 'cdk8s-mock-owner'; | ||
const repoName = 'cdk8s-mock-repo'; | ||
const rawRepoName = `${ownerName}/${repoName}`; | ||
const token = 'cdk8s-mock-token'; | ||
const P0_ISSUE_LABEL = 'priority/p0'; | ||
const TRIAGE_LABEL = 'needs-triage'; | ||
const DEPENDABOT_SECURITY_INCIDENT_LABEL = 'dependabot-security-finding'; | ||
|
||
const oldEnv = process.env; | ||
process.env = { | ||
...oldEnv, | ||
OWNER_NAME: ownerName, | ||
REPO_NAME: rawRepoName, | ||
GITHUB_TOKEN: token, | ||
}; | ||
|
||
const mockListIssues = jest.fn().mockResolvedValue({ | ||
data: [{ | ||
labels: [DEPENDABOT_SECURITY_INCIDENT_LABEL], | ||
state: 'open', | ||
}], | ||
}); | ||
|
||
const mockCreateIssue = jest.fn(); | ||
|
||
const mockListDependabotAlerts = jest.fn().mockResolvedValue({ | ||
data: [{ | ||
number: issueNumber, | ||
html_url: issueURL, | ||
state: 'open', | ||
}], | ||
}); | ||
|
||
import { createOctokitClient, getRepositoryName, getRepositoryOwner, run } from '../src/createDependabotIssue'; | ||
|
||
jest.mock('@octokit/rest', () => ({ | ||
Octokit: jest.fn().mockImplementation(() => ({ | ||
issues: { | ||
listForRepo: mockListIssues, | ||
create: mockCreateIssue, | ||
}, | ||
dependabot: { | ||
listAlertsForRepo: mockListDependabotAlerts, | ||
}, | ||
})), | ||
})); | ||
|
||
describe('security workflow script', () => { | ||
test('creates issue', async () => { | ||
await run(); | ||
|
||
expect(mockListIssues).toHaveBeenCalled(); | ||
expect(mockListIssues).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
}); | ||
|
||
expect(mockListDependabotAlerts).toHaveBeenCalled(); | ||
expect(mockListDependabotAlerts).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
}); | ||
|
||
expect(mockCreateIssue).toHaveBeenCalled(); | ||
expect(mockCreateIssue).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
title: issueTitle, | ||
body: issueBody, | ||
labels: [ | ||
DEPENDABOT_SECURITY_INCIDENT_LABEL, | ||
P0_ISSUE_LABEL, | ||
TRIAGE_LABEL, | ||
], | ||
}); | ||
}); | ||
|
||
test('does not create issue when there are no alerts', async () => { | ||
mockListDependabotAlerts.mockResolvedValueOnce({ | ||
data: [], | ||
}); | ||
|
||
await run(); | ||
|
||
expect(mockListIssues).toHaveBeenCalled(); | ||
expect(mockListIssues).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
}); | ||
|
||
expect(mockListDependabotAlerts).toHaveBeenCalled(); | ||
expect(mockListDependabotAlerts).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
}); | ||
|
||
expect(mockCreateIssue).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('does not create issue when issue already exists', async () => { | ||
mockListIssues.mockResolvedValueOnce({ | ||
data: [{ | ||
labels: [DEPENDABOT_SECURITY_INCIDENT_LABEL], | ||
title: issueTitle, | ||
state: 'open', | ||
}], | ||
}); | ||
|
||
await run(); | ||
|
||
expect(mockListIssues).toHaveBeenCalled(); | ||
expect(mockListIssues).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
}); | ||
|
||
expect(mockListDependabotAlerts).toHaveBeenCalled(); | ||
expect(mockListDependabotAlerts).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
}); | ||
|
||
expect(mockCreateIssue).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('does not create issue for any other status than open for security incident', async () => { | ||
mockListDependabotAlerts.mockResolvedValue({ | ||
data: [ | ||
{ | ||
number: 5, | ||
html_url: 'someUrl', | ||
state: 'dismissed', | ||
}, | ||
{ | ||
number: 6, | ||
html_url: 'anotherUrl', | ||
state: 'fixed', | ||
}, | ||
], | ||
}); | ||
|
||
await run(); | ||
|
||
expect(mockListIssues).toHaveBeenCalled(); | ||
expect(mockListIssues).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
}); | ||
|
||
expect(mockListDependabotAlerts).toHaveBeenCalled(); | ||
expect(mockListDependabotAlerts).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
}); | ||
|
||
expect(mockCreateIssue).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('disregards pull requests when creating issue', async () => { | ||
mockListIssues.mockResolvedValueOnce({ | ||
data: [{ | ||
labels: [DEPENDABOT_SECURITY_INCIDENT_LABEL], | ||
title: issueTitle, | ||
state: 'open', | ||
pull_request: 'some_pr', | ||
}], | ||
}); | ||
|
||
await run(); | ||
|
||
expect(mockListIssues).toHaveBeenCalled(); | ||
expect(mockListIssues).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
}); | ||
|
||
expect(mockListDependabotAlerts).toHaveBeenCalled(); | ||
expect(mockListDependabotAlerts).toHaveBeenCalledWith({ | ||
owner: ownerName, | ||
repo: repoName, | ||
}); | ||
|
||
expect(mockCreateIssue).not.toHaveBeenCalled(); | ||
}); | ||
|
||
test('throws if GITHUB_TOKEN is not present', () => { | ||
delete process.env.GITHUB_TOKEN; | ||
|
||
expect(() => createOctokitClient()).toThrow('GITHUB_TOKEN must be set'); | ||
}); | ||
|
||
test('throws if OWNER_NAME is not present', () => { | ||
delete process.env.OWNER_NAME; | ||
|
||
expect(() => getRepositoryOwner()).toThrow('OWNER_NAME must be set'); | ||
}); | ||
|
||
test('throws if REPO_NAME is not present', () => { | ||
delete process.env.REPO_NAME; | ||
|
||
expect(() => getRepositoryName()).toThrow('REPO_NAME must be set'); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.resetModules(); | ||
process.env = { | ||
...oldEnv, | ||
OWNER_NAME: ownerName, | ||
REPO_NAME: repoName, | ||
GITHUB_TOKEN: token, | ||
}; | ||
}); | ||
}); |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.