Skip to content
This repository has been archived by the owner on Apr 11, 2023. It is now read-only.

Commit

Permalink
feat: add dependabot alert gh action
Browse files Browse the repository at this point in the history
Signed-off-by: Vinayak Kukreja <vinakuk@amazon.com>
  • Loading branch information
vinayak-kukreja committed Mar 7, 2023
1 parent ce7dbc7 commit 7799b9e
Show file tree
Hide file tree
Showing 12 changed files with 11,692 additions and 15 deletions.
8 changes: 8 additions & 0 deletions .projen/deps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 2 additions & 4 deletions .projenrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ const project = new GitHubActionTypeScriptProject({
defaultReleaseBranch: 'main',
devDeps: ['projen-github-action-typescript'],
name: 'cdk8s-action',

// deps: [], /* Runtime dependencies of this module. */
// description: undefined, /* The description is just a string that helps people understand the purpose of the package. */
// packageName: undefined, /* The "name" in package.json. */
deps: ['@octokit/rest'],
bundledDeps: ['@octokit/rest'],
});
project.synth();
10,601 changes: 10,601 additions & 0 deletions dist/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/index.js.map

Large diffs are not rendered by default.

645 changes: 645 additions & 0 deletions dist/licenses.txt

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/sourcemap-register.js

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 110 additions & 0 deletions src/createDependabotIssue.ts
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);
});
6 changes: 1 addition & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
export class Hello {
public sayHello() {
return 'hello, world!';
}
}
export * from './createDependabotIssue';
217 changes: 217 additions & 0 deletions test/createDependabotIssue.test.ts
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,
};
});
});
5 changes: 0 additions & 5 deletions test/hello.test.ts

This file was deleted.

Loading

0 comments on commit 7799b9e

Please sign in to comment.