From 41bc2c1f68a15419844e82ff94dcab91b95bc646 Mon Sep 17 00:00:00 2001 From: Harminder Date: Tue, 1 Aug 2023 16:03:43 +1000 Subject: [PATCH 1/9] initial checkin --- .../20230731063755-add-security-permission-col.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 db/migrations/20230731063755-add-security-permission-col.js diff --git a/db/migrations/20230731063755-add-security-permission-col.js b/db/migrations/20230731063755-add-security-permission-col.js new file mode 100644 index 0000000000..38dc23c959 --- /dev/null +++ b/db/migrations/20230731063755-add-security-permission-col.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn("Subscriptions", "isSecurityPermissionsAccepted", { + type: Sequelize.BOOLEAN, + allowNull: false, + defaultValue: false + }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn("Subscriptions", "isSecurityPermissionsAccepted"); + } +}; From bf33a17471bfd6e321637b9e037bb6353d8b5c1f Mon Sep 17 00:00:00 2001 From: Harminder Date: Thu, 3 Aug 2023 09:44:08 +1000 Subject: [PATCH 2/9] inital checkin --- src/github/installation.test.ts | 225 ++++++++++++++++++ src/github/installation.ts | 101 ++++++++ src/models/subscription.ts | 4 +- .../webhook/webhook-receiver-post.test.ts | 30 ++- .../github/webhook/webhook-receiver-post.ts | 9 +- .../subscription-installation-service.ts | 21 +- 6 files changed, 375 insertions(+), 15 deletions(-) create mode 100644 src/github/installation.test.ts create mode 100644 src/github/installation.ts diff --git a/src/github/installation.test.ts b/src/github/installation.test.ts new file mode 100644 index 0000000000..5ab8c30562 --- /dev/null +++ b/src/github/installation.test.ts @@ -0,0 +1,225 @@ +import { when } from "jest-when"; +import { envVars } from "../config/env"; +import { getLogger } from "../config/logger"; +import { Installation } from "../models/installation"; +import { Subscription } from "../models/subscription"; +import { WebhookContext } from "../routes/github/webhook/webhook-context"; +import { GITHUB_CLOUD_API_BASEURL, GITHUB_CLOUD_BASEURL } from "./client/github-client-constants"; +import { installationWebhookHandler } from "./installation"; +import { BooleanFlags, booleanFlag } from "../config/feature-flags"; +import { submitSecurityWorkspaceToLink } from "../services/subscription-installation-service"; +import { GitHubServerApp } from "../models/github-server-app"; +import { v4 as uuid } from "uuid"; + +jest.mock("utils/webhook-utils"); +jest.mock("config/feature-flags"); +jest.mock("services/subscription-installation-service"); + +const GITHUB_INSTALLATION_ID = 1234; +const GHES_GITHUB_APP_ID = 111; +const GHES_GITHUB_UUID = "xxx-xxx-xxx-xxx"; +const GHES_GITHUB_APP_CLIENT_ID = "client-id"; + +describe("InstallationWebhookHandler", () => { + let jiraClient: any; + let util: any; + let gitHubServerApp; + let ghesGitHubAppId; + + describe("GitHub Cloud", () => { + + beforeEach(async () => { + jiraClient = { baseURL: jiraHost }; + util = null; + + await Subscription.create({ + gitHubInstallationId: GITHUB_INSTALLATION_ID, + jiraHost + }); + + await Installation.create({ + jiraHost, + clientKey: "client-key", + encryptedSharedSecret: "shared-secret" + }); + + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(true); + + }); + + describe.each( + ["created", "new_permissions_accepted"] + )("should not set security permissions accepted field in subscriptions when FF is disabled", (action) => { + it(`${action} action`, async () => { + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(false); + await installationWebhookHandler(getWebhookContext({ cloud: true, action }), jiraClient, util, GITHUB_INSTALLATION_ID); + const subscription = await Subscription.findOneForGitHubInstallationId(GITHUB_INSTALLATION_ID, undefined); + expect(subscription?.isSecurityPermissionsAccepted).toBeFalsy(); + + }); + }); + describe.each( + ["created", "new_permissions_accepted"] + )("should set security permissions accepted field in subscriptions", (action) => { + it(`${action} action`, async () => { + await installationWebhookHandler(getWebhookContext({ cloud: true, action }), jiraClient, util, GITHUB_INSTALLATION_ID); + const subscription = await Subscription.findOneForGitHubInstallationId(GITHUB_INSTALLATION_ID, undefined); + expect(subscription?.isSecurityPermissionsAccepted).toBeTruthy(); + + }); + }); + + it("should not set security permissions accepted field if the payload doesn't contain secret_scanning_alerts permission", async () => { + const webhookContext = getWebhookContext({ cloud: true }); + delete webhookContext.payload.installation.permissions["secret_scanning_alerts"]; + await installationWebhookHandler(webhookContext, jiraClient, util, GITHUB_INSTALLATION_ID); + const subscription = await Subscription.findOneForGitHubInstallationId(GITHUB_INSTALLATION_ID, undefined); + expect(subscription?.isSecurityPermissionsAccepted).toBeFalsy(); + + }); + + it("should not set security permissions accepted field if the payload doesn't contain security_events permission", async () => { + const webhookContext = getWebhookContext({ cloud: true }); + delete webhookContext.payload.installation.permissions["security_events"]; + await installationWebhookHandler(webhookContext, jiraClient, util, GITHUB_INSTALLATION_ID); + const subscription = await Subscription.findOneForGitHubInstallationId(GITHUB_INSTALLATION_ID, undefined); + expect(subscription?.isSecurityPermissionsAccepted).toBeFalsy(); + + }); + + it("should not set security permissions accepted field if the payload doesn't contain vulnerability_alerts permission", async () => { + const webhookContext = getWebhookContext({ cloud: true }); + delete webhookContext.payload.installation.permissions["vulnerability_alerts"]; + await installationWebhookHandler(webhookContext, jiraClient, util, GITHUB_INSTALLATION_ID); + const subscription = await Subscription.findOneForGitHubInstallationId(GITHUB_INSTALLATION_ID, undefined); + expect(subscription?.isSecurityPermissionsAccepted).toBeFalsy(); + + }); + + it("should submit workspace to link for new_permissions_accepted action", async () => { + const webhookContext = getWebhookContext({ cloud: true }); + await installationWebhookHandler(webhookContext, jiraClient, util, GITHUB_INSTALLATION_ID); + const subscription = await Subscription.findOneForGitHubInstallationId(GITHUB_INSTALLATION_ID, undefined); + const installation = await Installation.getForHost(jiraHost); + expect(submitSecurityWorkspaceToLink).toBeCalledTimes(1); + expect(submitSecurityWorkspaceToLink).toBeCalledWith(installation, subscription, expect.anything()); + + }); + + it("should not submit workspace to link for created action", async () => { + const webhookContext = getWebhookContext({ cloud: true, action: "created" }); + await installationWebhookHandler(webhookContext, jiraClient, util, GITHUB_INSTALLATION_ID); + expect(submitSecurityWorkspaceToLink).toBeCalledTimes(0); + + }); + it("should throw error if subscription is not found", async () => { + const webhookContext = getWebhookContext({ cloud: true }); + await expect(installationWebhookHandler(webhookContext, jiraClient, util, 0)).rejects.toThrow("Subscription not found"); + }); + + }); + describe("GitHub Enterprise Server", () => { + + beforeEach(async () => { + jiraClient = { baseURL: jiraHost }; + util = null; + + await Installation.create({ + jiraHost, + clientKey: "client-key", + encryptedSharedSecret: "shared-secret" + }); + + gitHubServerApp = await GitHubServerApp.install({ + uuid: uuid(), + appId: GHES_GITHUB_APP_ID, + installationId: 456, + gitHubAppName: "test-github-server-app", + gitHubBaseUrl: gheUrl, + gitHubClientId: "client-id", + gitHubClientSecret: "client-secret", + privateKey: "private-key", + webhookSecret: "webhook-secret" + }, jiraHost); + ghesGitHubAppId = gitHubServerApp.id; + await Subscription.create({ + gitHubInstallationId: GITHUB_INSTALLATION_ID, + jiraHost, + jiraClientKey: "client-key", + gitHubAppId: gitHubServerApp.id + }); + + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(true); + + }); + describe.each( + ["created", "new_permissions_accepted"] + )("should not set security permissions accepted field in subscriptions when FF is disabled", (action) => { + it(`${action} action`, async () => { + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(false); + await installationWebhookHandler(getWebhookContext({ cloud: false, action }), jiraClient, util, GITHUB_INSTALLATION_ID); + const subscription = await Subscription.findOneForGitHubInstallationId(GITHUB_INSTALLATION_ID, undefined); + expect(subscription?.isSecurityPermissionsAccepted).toBeFalsy(); + + }); + }); + + describe.each( + ["created", "new_permissions_accepted"] + )("should set security permissions accepted field in subscriptions", (action) => { + it(`${action} action`, async () => { + await installationWebhookHandler(getWebhookContext({ cloud: false, action }), jiraClient, util, GITHUB_INSTALLATION_ID); + const subscription = await Subscription.findOneForGitHubInstallationId(GITHUB_INSTALLATION_ID, ghesGitHubAppId); + expect(subscription?.isSecurityPermissionsAccepted).toBeTruthy(); + + }); + }); + + }); + + const getWebhookContext = ({ cloud, action = "new_permissions_accepted" }: { cloud: boolean, action?: string }) => { + return new WebhookContext({ + id: "1", + name: "installation", + log: getLogger("test"), + action, + payload: getPayload(), + gitHubAppConfig: cloud ? { + uuid: undefined, + gitHubAppId: undefined, + appId: parseInt(envVars.APP_ID), + clientId: envVars.GITHUB_CLIENT_ID, + gitHubBaseUrl: GITHUB_CLOUD_BASEURL, + gitHubApiUrl: GITHUB_CLOUD_API_BASEURL + } : { + uuid: GHES_GITHUB_UUID, + gitHubAppId: ghesGitHubAppId, + appId: GHES_GITHUB_APP_ID, + clientId: GHES_GITHUB_APP_CLIENT_ID, + gitHubBaseUrl: gheUrl, + gitHubApiUrl: gheUrl + } + }); + }; + + const getPayload = () => { + return { + "installation": { + "id": GITHUB_INSTALLATION_ID, + "permissions": { + "issues": "write", + "actions": "write", + "contents": "write", + "metadata": "read", + "workflows": "write", + "deployments": "write", + "pull_requests": "write", + "security_events": "read", + "vulnerability_alerts": "read", + "secret_scanning_alerts": "read" + } + } + }; + }; + +}); \ No newline at end of file diff --git a/src/github/installation.ts b/src/github/installation.ts new file mode 100644 index 0000000000..7e2cd4f5d8 --- /dev/null +++ b/src/github/installation.ts @@ -0,0 +1,101 @@ +import { WebhookContext } from "routes/github/webhook/webhook-context"; +import { JiraClient } from "../jira/client/jira-client"; +import { BooleanFlags, booleanFlag } from "../config/feature-flags"; +import { InstallationEvent } from "@octokit/webhooks-types"; +import { Subscription } from "../models/subscription"; +import { submitSecurityWorkspaceToLink } from "../services/subscription-installation-service"; +import { Installation } from "../models/installation"; +import { emitWebhookProcessedMetrics } from "../util/webhook-utils"; +import Logger from "bunyan"; + +const SECURITY_PERMISSIONS = ["secret_scanning_alerts", "security_events", "vulnerability_alerts"]; + +export const installationWebhookHandler = async ( + context: WebhookContext, + jiraClient: JiraClient, + _util, + gitHubInstallationId: number +): Promise => { + + if (!await booleanFlag(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, jiraClient.baseURL)) { + return; + } + const { + action, + payload: { + installation: { + permissions + } + }, + gitHubAppConfig: { + gitHubAppId + } + } = context; + + const jiraHost = jiraClient.baseURL; + + const logger = context.log.child({ + gitHubInstallationId, + jiraHost + }); + + const installation = await Installation.getForHost(jiraHost); + if (!installation) { + throw new Error(`Installation not found`); + } + + const subscription = await Subscription.getSingleInstallation(jiraHost, gitHubInstallationId, gitHubAppId); + if (!subscription) { + throw new Error(`Subscription not found`); + } + + let jiraResponse; + + try { + if (action === "created" && hasSecurityPermissions(permissions)) { + return await setSecurityPermissionAccepted(subscription, logger); + + } else if (action === "new_permissions_accepted" && hasSecurityPermissions(permissions) && !subscription.isSecurityPermissionsAccepted) { + await setSecurityPermissionAccepted(subscription, logger); + + jiraResponse = await submitSecurityWorkspaceToLink(installation, subscription, logger); + logger.info({ subscriptionId: subscription.id }, "Linked security workspace"); + } + + const webhookReceived = context.webhookReceived; + webhookReceived && emitWebhookProcessedMetrics( + new Date(webhookReceived).getTime(), + `installation-${action}`, + jiraClient.baseURL, + logger, + jiraResponse?.status, + gitHubAppId + ); + + } catch (err) { + logger.warn({ err }, "Failed to submit security workspace to Jira"); + const webhookReceived = context.webhookReceived; + webhookReceived && emitWebhookProcessedMetrics( + new Date(webhookReceived).getTime(), + "installation", + jiraClient.baseURL, + logger, + 500, + gitHubAppId + ); + } +}; + +const hasSecurityPermissions = (permissions: InstallationEvent["installation"]["permissions"]) => { + return SECURITY_PERMISSIONS.every(securityPermission => securityPermission in permissions); +}; + +const setSecurityPermissionAccepted = async (subscription: Subscription, logger: Logger) => { + try { + if (subscription) { + await subscription.update({ isSecurityPermissionsAccepted: true }); + } + } catch (err) { + logger.warn({ err }, "Failed to set security permissions accepted field in Subscriptions"); + } +}; \ No newline at end of file diff --git a/src/models/subscription.ts b/src/models/subscription.ts index ad5d5dd40e..6574c35764 100644 --- a/src/models/subscription.ts +++ b/src/models/subscription.ts @@ -43,6 +43,7 @@ export class Subscription extends Model { repositoryStatus?: TaskStatus; gitHubAppId: number | undefined; avatarUrl: string | undefined; + isSecurityPermissionsAccepted: boolean; static async getAllForHost(jiraHost: string, gitHubAppId?: number): Promise { return this.findAll({ @@ -273,7 +274,8 @@ Subscription.init({ avatarUrl: { type: DataTypes.STRING, allowNull: true - } + }, + isSecurityPermissionsAccepted: DataTypes.BOOLEAN }, { sequelize }); export interface SubscriptionPayload { diff --git a/src/routes/github/webhook/webhook-receiver-post.test.ts b/src/routes/github/webhook/webhook-receiver-post.test.ts index 3c9251499b..b1eaf5204f 100644 --- a/src/routes/github/webhook/webhook-receiver-post.test.ts +++ b/src/routes/github/webhook/webhook-receiver-post.test.ts @@ -14,8 +14,9 @@ import { envVars } from "config/env"; import { GITHUB_CLOUD_API_BASEURL, GITHUB_CLOUD_BASEURL } from "~/src/github/client/github-client-constants"; import { dependabotAlertWebhookHandler } from "~/src/github/dependabot-alert"; import { Subscription } from "~/src/models/subscription"; -import { DependabotAlertEvent, Schema, SecretScanningAlertEvent } from "@octokit/webhooks-types"; +import { DependabotAlertEvent, InstallationEvent, Schema, SecretScanningAlertEvent } from "@octokit/webhooks-types"; import { secretScanningAlertWebhookHandler } from "~/src/github/secret-scanning-alert"; +import { installationWebhookHandler } from "~/src/github/installation"; jest.mock("~/src/middleware/github-webhook-middleware"); jest.mock("~/src/config/feature-flags"); @@ -334,6 +335,33 @@ describe("webhook-receiver-post", () => { gitHubAppConfig: gitHubAppConfigForGHES() })); }); + describe.each( + ["created", "new_permissions_accepted"] + )("should call installation handler", (action) => { + it(`${action} action`, async() => { + req = createGHESReqForEvent("installation", action, EXIST_GHES_UUID, { installation: { id: 123 } } as unknown as InstallationEvent); + const spy = jest.fn(); + jest.mocked(GithubWebhookMiddleware).mockImplementation(() => spy); + jest.mocked(Subscription.findOneForGitHubInstallationId).mockReturnValue(Promise.resolve({ jiraHost: "https://test-instnace.atlassian.net" } as unknown as Subscription)); + await WebhookReceiverPost(injectRawBodyToReq(req), res); + expect(GithubWebhookMiddleware).toBeCalledWith(installationWebhookHandler); + expect(spy).toBeCalledWith(expect.objectContaining({ + id: "100", + name: "installation", + gitHubAppConfig: gitHubAppConfigForGHES() + })); + }); + + }); + + it("should not call installation handler for other than created/new_permissions_accepted action", async () => { + req = createGHESReqForEvent("installation", "suspend", EXIST_GHES_UUID, { installation: { id: 123 } } as unknown as InstallationEvent); + const spy = jest.fn(); + jest.mocked(GithubWebhookMiddleware).mockImplementation(() => spy); + jest.mocked(Subscription.findOneForGitHubInstallationId).mockReturnValue(Promise.resolve({ jiraHost: "https://test-instnace.atlassian.net" } as unknown as Subscription)); + await WebhookReceiverPost(injectRawBodyToReq(req), res); + expect(GithubWebhookMiddleware).toBeCalledTimes(0); + }); }); diff --git a/src/routes/github/webhook/webhook-receiver-post.ts b/src/routes/github/webhook/webhook-receiver-post.ts index 1f353d4f8a..04b7630210 100644 --- a/src/routes/github/webhook/webhook-receiver-post.ts +++ b/src/routes/github/webhook/webhook-receiver-post.ts @@ -20,6 +20,7 @@ import { GITHUB_CLOUD_API_BASEURL, GITHUB_CLOUD_BASEURL } from "~/src/github/cli import { dependabotAlertWebhookHandler } from "~/src/github/dependabot-alert"; import { extraLoggerInfo } from "./webhook-logging-extra"; import { secretScanningAlertWebhookHandler } from "~/src/github/secret-scanning-alert"; +import { installationWebhookHandler } from "~/src/github/installation"; export const WebhookReceiverPost = async (request: Request, response: Response): Promise => { const eventName = request.headers["x-github-event"] as string; @@ -137,6 +138,12 @@ const webhookRouter = async (context: WebhookContext) => { break; case "secret_scanning_alert": await GithubWebhookMiddleware(secretScanningAlertWebhookHandler)(context); + break; + case "installation": + if (context.action === "created" || context.action === "new_permissions_accepted") { + await GithubWebhookMiddleware(installationWebhookHandler)(context); + } + break; } }; @@ -164,7 +171,7 @@ const getWebhookSecrets = async (uuid?: string): Promise<{ webhookSecrets: Array * If we ever need to rotate the webhook secrets for Enterprise Customers, * we can add it in the array: ` [ webhookSecret ]` */ - return { webhookSecrets: [ webhookSecret ], gitHubServerApp }; + return { webhookSecrets: [webhookSecret], gitHubServerApp }; } return { diff --git a/src/services/subscription-installation-service.ts b/src/services/subscription-installation-service.ts index fbd8f0a4f6..419cb4937c 100644 --- a/src/services/subscription-installation-service.ts +++ b/src/services/subscription-installation-service.ts @@ -107,8 +107,12 @@ export const verifyAdminPermsAndFinishInstallation = log.info({ subscriptionId: subscription.id }, "Subscription was created"); if (await booleanFlag(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, installation.jiraHost)) { - await submitSecurityWorkspaceToLink(installation, subscription, log); - log.info({ subscriptionId: subscription.id }, "Linked security workspace"); + try { + await submitSecurityWorkspaceToLink(installation, subscription, log); + log.info({ subscriptionId: subscription.id }, "Linked security workspace"); + } catch (err) { + log.warn({ err }, "Failed to submit security workspace to Jira"); + } } await Promise.all( @@ -146,18 +150,11 @@ export const verifyAdminPermsAndFinishInstallation = } }; -const submitSecurityWorkspaceToLink = async ( +export const submitSecurityWorkspaceToLink = async ( installation: Installation, subscription: Subscription, logger: Logger ) => { - - try { - const jiraClient = await JiraClient.getNewClient(installation, logger); - await jiraClient.linkedWorkspace(subscription.id); - - } catch (err) { - logger.warn({ err }, "Failed to submit security workspace to Jira"); - } - + const jiraClient = await JiraClient.getNewClient(installation, logger); + return await jiraClient.linkedWorkspace(subscription.id); }; From a4ee04928a602783392daeeb047c2588902f300d Mon Sep 17 00:00:00 2001 From: Harminder Date: Thu, 3 Aug 2023 14:24:20 +1000 Subject: [PATCH 3/9] refacored --- src/github/installation.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/github/installation.ts b/src/github/installation.ts index 7e2cd4f5d8..282212a079 100644 --- a/src/github/installation.ts +++ b/src/github/installation.ts @@ -22,6 +22,7 @@ export const installationWebhookHandler = async ( } const { action, + log: logger, payload: { installation: { permissions @@ -34,11 +35,6 @@ export const installationWebhookHandler = async ( const jiraHost = jiraClient.baseURL; - const logger = context.log.child({ - gitHubInstallationId, - jiraHost - }); - const installation = await Installation.getForHost(jiraHost); if (!installation) { throw new Error(`Installation not found`); @@ -92,9 +88,7 @@ const hasSecurityPermissions = (permissions: InstallationEvent["installation"][" const setSecurityPermissionAccepted = async (subscription: Subscription, logger: Logger) => { try { - if (subscription) { - await subscription.update({ isSecurityPermissionsAccepted: true }); - } + await subscription.update({ isSecurityPermissionsAccepted: true }); } catch (err) { logger.warn({ err }, "Failed to set security permissions accepted field in Subscriptions"); } From 51281ca66438c95d7bb52b27f6831764045b4dce Mon Sep 17 00:00:00 2001 From: Harminder Date: Fri, 4 Aug 2023 11:51:41 +1000 Subject: [PATCH 4/9] update error message --- src/github/installation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/github/installation.ts b/src/github/installation.ts index 282212a079..4effc79242 100644 --- a/src/github/installation.ts +++ b/src/github/installation.ts @@ -55,7 +55,7 @@ export const installationWebhookHandler = async ( await setSecurityPermissionAccepted(subscription, logger); jiraResponse = await submitSecurityWorkspaceToLink(installation, subscription, logger); - logger.info({ subscriptionId: subscription.id }, "Linked security workspace"); + logger.info({ subscriptionId: subscription.id }, "Linked security workspace via backfill"); } const webhookReceived = context.webhookReceived; @@ -69,7 +69,7 @@ export const installationWebhookHandler = async ( ); } catch (err) { - logger.warn({ err }, "Failed to submit security workspace to Jira"); + logger.warn({ err }, "Failed to submit security workspace to Jira via backfill"); const webhookReceived = context.webhookReceived; webhookReceived && emitWebhookProcessedMetrics( new Date(webhookReceived).getTime(), From 3b04ff2548f9e5472c729bfcde85c9c50ecad094 Mon Sep 17 00:00:00 2001 From: Harminder Date: Mon, 7 Aug 2023 16:06:34 +1000 Subject: [PATCH 5/9] initial checkin --- src/github/installation.test.ts | 22 +++++++++++++++--- src/github/installation.ts | 23 ++++++++++++------- .../api-reset-subscription-failed-tasks.ts | 4 ++-- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/github/installation.test.ts b/src/github/installation.test.ts index 5ab8c30562..a168c200a6 100644 --- a/src/github/installation.test.ts +++ b/src/github/installation.test.ts @@ -10,10 +10,12 @@ import { BooleanFlags, booleanFlag } from "../config/feature-flags"; import { submitSecurityWorkspaceToLink } from "../services/subscription-installation-service"; import { GitHubServerApp } from "../models/github-server-app"; import { v4 as uuid } from "uuid"; +import { findOrStartSync } from "../sync/sync-utils"; jest.mock("utils/webhook-utils"); jest.mock("config/feature-flags"); jest.mock("services/subscription-installation-service"); +jest.mock("../sync/sync-utils"); const GITHUB_INSTALLATION_ID = 1234; const GHES_GITHUB_APP_ID = 111; @@ -96,13 +98,26 @@ describe("InstallationWebhookHandler", () => { }); - it("should submit workspace to link for new_permissions_accepted action", async () => { + it("should not set security permissions accepted field if the payload doesn't contain security events", async () => { + const webhookContext = getWebhookContext({ cloud: true }); + webhookContext.payload.installation.events = []; + await installationWebhookHandler(webhookContext, jiraClient, util, GITHUB_INSTALLATION_ID); + const subscription = await Subscription.findOneForGitHubInstallationId(GITHUB_INSTALLATION_ID, undefined); + expect(subscription?.isSecurityPermissionsAccepted).toBeFalsy(); + + }); + + it("should submit workspace to link and trigger security backfill for new_permissions_accepted action", async () => { const webhookContext = getWebhookContext({ cloud: true }); await installationWebhookHandler(webhookContext, jiraClient, util, GITHUB_INSTALLATION_ID); const subscription = await Subscription.findOneForGitHubInstallationId(GITHUB_INSTALLATION_ID, undefined); const installation = await Installation.getForHost(jiraHost); expect(submitSecurityWorkspaceToLink).toBeCalledTimes(1); expect(submitSecurityWorkspaceToLink).toBeCalledWith(installation, subscription, expect.anything()); + expect(findOrStartSync).toBeCalledTimes(1); + expect(findOrStartSync).toBeCalledWith( + subscription, expect.anything(), "full", subscription?.backfillSince, ["dependabotAlert"], { "source": "webhook-security-permissions-accepted" } + ); }); @@ -141,7 +156,7 @@ describe("InstallationWebhookHandler", () => { privateKey: "private-key", webhookSecret: "webhook-secret" }, jiraHost); - ghesGitHubAppId = gitHubServerApp.id; + ghesGitHubAppId = gitHubServerApp.id; await Subscription.create({ gitHubInstallationId: GITHUB_INSTALLATION_ID, jiraHost, @@ -217,7 +232,8 @@ describe("InstallationWebhookHandler", () => { "security_events": "read", "vulnerability_alerts": "read", "secret_scanning_alerts": "read" - } + }, + "events": ["secret_scanning_alert", "code_scanning_alert", "dependabot_alert"] } }; }; diff --git a/src/github/installation.ts b/src/github/installation.ts index 4effc79242..f801c2500c 100644 --- a/src/github/installation.ts +++ b/src/github/installation.ts @@ -7,8 +7,10 @@ import { submitSecurityWorkspaceToLink } from "../services/subscription-installa import { Installation } from "../models/installation"; import { emitWebhookProcessedMetrics } from "../util/webhook-utils"; import Logger from "bunyan"; +import { findOrStartSync } from "../sync/sync-utils"; const SECURITY_PERMISSIONS = ["secret_scanning_alerts", "security_events", "vulnerability_alerts"]; +const SECURITY_EVENTS = ["secret_scanning_alert", "code_scanning_alert", "dependabot_alert"]; export const installationWebhookHandler = async ( context: WebhookContext, @@ -25,7 +27,8 @@ export const installationWebhookHandler = async ( log: logger, payload: { installation: { - permissions + permissions, + events } }, gitHubAppConfig: { @@ -48,14 +51,17 @@ export const installationWebhookHandler = async ( let jiraResponse; try { - if (action === "created" && hasSecurityPermissions(permissions)) { + if (action === "created" && hasSecurityPermissionsAndEvents(permissions, events)) { return await setSecurityPermissionAccepted(subscription, logger); - } else if (action === "new_permissions_accepted" && hasSecurityPermissions(permissions) && !subscription.isSecurityPermissionsAccepted) { - await setSecurityPermissionAccepted(subscription, logger); - + } else if (action === "new_permissions_accepted" && hasSecurityPermissionsAndEvents(permissions, events) && !subscription.isSecurityPermissionsAccepted) { jiraResponse = await submitSecurityWorkspaceToLink(installation, subscription, logger); logger.info({ subscriptionId: subscription.id }, "Linked security workspace via backfill"); + + await findOrStartSync(subscription, logger, "full", subscription.backfillSince, ["dependabotAlert"], { source: "webhook-security-permissions-accepted" }); + logger.info({ subscriptionId: subscription.id }, "Triggered security backfill successfully"); + + await setSecurityPermissionAccepted(subscription, logger); } const webhookReceived = context.webhookReceived; @@ -69,7 +75,7 @@ export const installationWebhookHandler = async ( ); } catch (err) { - logger.warn({ err }, "Failed to submit security workspace to Jira via backfill"); + logger.warn({ err }, "Failed to submit security workspace to Jira or trigger backfill via backfill"); const webhookReceived = context.webhookReceived; webhookReceived && emitWebhookProcessedMetrics( new Date(webhookReceived).getTime(), @@ -82,8 +88,9 @@ export const installationWebhookHandler = async ( } }; -const hasSecurityPermissions = (permissions: InstallationEvent["installation"]["permissions"]) => { - return SECURITY_PERMISSIONS.every(securityPermission => securityPermission in permissions); +const hasSecurityPermissionsAndEvents = (permissions: InstallationEvent["installation"]["permissions"], events: InstallationEvent["installation"]["events"]) => { + return SECURITY_PERMISSIONS.every(securityPermission => securityPermission in permissions) && + SECURITY_EVENTS.every((securityEvent: any) => events.includes(securityEvent)); }; const setSecurityPermissionAccepted = async (subscription: Subscription, logger: Logger) => { diff --git a/src/routes/api/api-reset-subscription-failed-tasks.ts b/src/routes/api/api-reset-subscription-failed-tasks.ts index 2bcc9fe058..7fa77b9243 100644 --- a/src/routes/api/api-reset-subscription-failed-tasks.ts +++ b/src/routes/api/api-reset-subscription-failed-tasks.ts @@ -5,12 +5,12 @@ import { TaskType } from "../../sync/sync.types"; interface OutputLogRecord { repoSyncStateId: number, - targetTask: "pull" | "branch" | "commit" | "build" | "deployment" | string + targetTask: "pull" | "branch" | "commit" | "build" | "deployment" | "dependabotAlert" | string } export const ApiResetSubscriptionFailedTasks = async (req: Request, res: Response): Promise => { const subscriptionId = req.body.subscriptionId; - const targetTasks = req.body.targetTasks as TaskType[] || ["pull", "branch", "commit", "build", "deployment"]; + const targetTasks = req.body.targetTasks as TaskType[] || ["pull", "branch", "commit", "build", "deployment", "dependabotAlert"]; if (!subscriptionId) { res.status(400).send("please provide subscriptionId"); From 9c11e6bd184ef6a514b0e96d1a9f5837d156c583 Mon Sep 17 00:00:00 2001 From: Harminder Date: Wed, 9 Aug 2023 09:19:00 +1000 Subject: [PATCH 6/9] inital checkin --- ...230808062435-add-secret-scanning-alert-cols.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 db/migrations/20230808062435-add-secret-scanning-alert-cols.js diff --git a/db/migrations/20230808062435-add-secret-scanning-alert-cols.js b/db/migrations/20230808062435-add-secret-scanning-alert-cols.js new file mode 100644 index 0000000000..3598eb5ccb --- /dev/null +++ b/db/migrations/20230808062435-add-secret-scanning-alert-cols.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn("RepoSyncStates", "secretScanningAlertFrom", { type: Sequelize.DATE, allowNull: true }); + await queryInterface.addColumn("RepoSyncStates", "secretScanningAlertStatus", { type: Sequelize.ENUM("pending", "complete", "failed"), allowNull: true }); + await queryInterface.addColumn("RepoSyncStates", "secretScanningAlertCursor", { type: Sequelize.STRING, allowNull: true }); + }, + + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn("RepoSyncStates", "secretScanningAlertFrom"); + await queryInterface.removeColumn("RepoSyncStates", "secretScanningAlertStatus"); + await queryInterface.removeColumn("RepoSyncStates", "secretScanningAlertCursor"); + } +}; From a0190f7818b3e5ed54d4ce06db6a142a23851c8e Mon Sep 17 00:00:00 2001 From: Harminder Date: Thu, 10 Aug 2023 15:31:11 +1000 Subject: [PATCH 7/9] initial sync --- src/github/client/github-client.types.ts | 7 + .../client/github-installation-client.ts | 9 + .../client/secret-scanning-alert.types.ts | 13 + src/github/installation.ts | 2 +- src/models/reposyncstate.ts | 20 +- src/routes/jira/sync/jira-sync-post.test.ts | 2 +- src/routes/jira/sync/jira-sync-post.ts | 2 +- src/sync/installation.test.ts | 8 +- src/sync/installation.ts | 8 +- src/sync/scheduler.test.ts | 33 ++- src/sync/scheduler.ts | 2 +- src/sync/secret-scanning-alerts.test.ts | 228 ++++++++++++++++++ src/sync/secret-scanning-alerts.ts | 113 +++++++++ src/sync/sync.types.ts | 2 +- .../transform-secret-scanning-alert.test.ts | 1 + .../transform-secret-scanning-alert.ts | 1 + test/fixtures/api/secret-scanning-alerts.json | 21 ++ test/utils/database-state-creator.ts | 7 + 18 files changed, 464 insertions(+), 15 deletions(-) create mode 100644 src/github/client/secret-scanning-alert.types.ts create mode 100644 src/sync/secret-scanning-alerts.test.ts create mode 100644 src/sync/secret-scanning-alerts.ts create mode 100644 test/fixtures/api/secret-scanning-alerts.json diff --git a/src/github/client/github-client.types.ts b/src/github/client/github-client.types.ts index 642a2a232d..e8eb583727 100644 --- a/src/github/client/github-client.types.ts +++ b/src/github/client/github-client.types.ts @@ -30,6 +30,13 @@ export type GetPullRequestParams = { page?: number; } +export type GetSecretScanningAlertRequestParams = { + sort?: string; + direction?: string; + per_page?: number; + page?: number; +} + export type GraphQlQueryResponse = { data: ResponseData; errors?: GraphQLError[]; diff --git a/src/github/client/github-installation-client.ts b/src/github/client/github-installation-client.ts index a883f74d02..a4a5b8c231 100644 --- a/src/github/client/github-installation-client.ts +++ b/src/github/client/github-installation-client.ts @@ -29,6 +29,7 @@ import { ActionsListRepoWorkflowRunsResponseEnhanced, CreateReferenceBody, GetPullRequestParams, + GetSecretScanningAlertRequestParams, PaginatedAxiosResponse, ReposGetContentsResponse } from "./github-client.types"; @@ -38,6 +39,7 @@ import { GithubClientError, GithubClientGraphQLError } from "~/src/github/client import { cloneDeep } from "lodash"; import { BooleanFlags, booleanFlag } from "config/feature-flags"; import { logCurlOutputInChunks, runCurl } from "utils/curl/curl-utils"; +import { SecretScanningAlertResponseItem } from "./secret-scanning-alert.types"; // Unfortunately, the type is not exposed in Octokit... // https://docs.github.com/en/rest/pulls/review-requests?apiVersion=2022-11-28#get-all-requested-reviewers-for-a-pull-request @@ -74,6 +76,13 @@ export class GitHubInstallationClient extends GitHubClient { this.gitHubServerAppId = gshaId; } + + public async getSecretScanningAlerts(owner: string, repo: string, secretScanningAlertRequestParams: GetSecretScanningAlertRequestParams): Promise> { + return await this.get(`/repos/{owner}/{repo}/secret-scanning/alerts`, secretScanningAlertRequestParams, { + owner, + repo + }); + } /** * Lists pull requests for the given repository. */ diff --git a/src/github/client/secret-scanning-alert.types.ts b/src/github/client/secret-scanning-alert.types.ts new file mode 100644 index 0000000000..806143449a --- /dev/null +++ b/src/github/client/secret-scanning-alert.types.ts @@ -0,0 +1,13 @@ +export type SecretScanningAlertResponseItem = { + number: number, + created_at: string, + url: string, + html_url: string, + locations_url: string, + state: "open" | "resolved", + resolution?: string, + resolved_at?: string, + resolution_comment?: string, + secret_type: string, + secret_type_display_name: string + } \ No newline at end of file diff --git a/src/github/installation.ts b/src/github/installation.ts index f801c2500c..602a6b0a64 100644 --- a/src/github/installation.ts +++ b/src/github/installation.ts @@ -75,7 +75,7 @@ export const installationWebhookHandler = async ( ); } catch (err) { - logger.warn({ err }, "Failed to submit security workspace to Jira or trigger backfill via backfill"); + logger.warn({ err }, "Failed to submit security workspace to Jira or trigger backfill"); const webhookReceived = context.webhookReceived; webhookReceived && emitWebhookProcessedMetrics( new Date(webhookReceived).getTime(), diff --git a/src/models/reposyncstate.ts b/src/models/reposyncstate.ts index f19a90c924..e5bede09d3 100644 --- a/src/models/reposyncstate.ts +++ b/src/models/reposyncstate.ts @@ -35,6 +35,7 @@ export interface RepoSyncStateProperties { buildStatus?: TaskStatus; deploymentStatus?: TaskStatus; dependabotAlertStatus?: TaskStatus; + secretScanningAlertStatus?: TaskStatus, branchCursor?: string; commitCursor?: string; issueCursor?: string; @@ -42,12 +43,14 @@ export interface RepoSyncStateProperties { buildCursor?: string; deploymentCursor?: string; dependabotAlertCursor?: string; + secretScanningAlertCursor?: string; commitFrom?: Date; branchFrom?: Date; pullFrom?: Date; buildFrom?: Date; deploymentFrom?: Date; dependabotAlertFrom?: Date; + secretScanningAlertFrom?: Date; forked?: boolean; repoPushedAt: Date; repoUpdatedAt: Date; @@ -76,6 +79,7 @@ export class RepoSyncState extends Model implements RepoSyncStateProperties { buildStatus?: TaskStatus; deploymentStatus?: TaskStatus; dependabotAlertStatus?: TaskStatus; + secretScanningAlertStatus?: TaskStatus; branchCursor?: string; commitCursor?: string; issueCursor?: string; @@ -83,12 +87,14 @@ export class RepoSyncState extends Model implements RepoSyncStateProperties { buildCursor?: string; deploymentCursor?: string; dependabotAlertCursor?: string; + secretScanningAlertCursor?: string; commitFrom?: Date; branchFrom?: Date; pullFrom?: Date; buildFrom?: Date; deploymentFrom?: Date; dependabotAlertFrom?: Date; + secretScanningAlertFrom?: Date; forked?: boolean; repoPushedAt: Date; repoUpdatedAt: Date; @@ -135,7 +141,8 @@ export class RepoSyncState extends Model implements RepoSyncStateProperties { commitStatus: "failed", buildStatus: "failed", deploymentStatus: "failed", - dependabotAlertStatus: "failed" + dependabotAlertStatus: "failed", + secretScanningAlertStatus: "failed" } } }); @@ -152,7 +159,8 @@ export class RepoSyncState extends Model implements RepoSyncStateProperties { commitStatus: "failed", buildStatus: "failed", deploymentStatus: "failed", - dependabotAlertStatus: "failed" + dependabotAlertStatus: "failed", + secretScanningAlertStatus: "failed" } } })); @@ -362,7 +370,10 @@ export class RepoSyncState extends Model implements RepoSyncStateProperties { commitFrom: null, dependabotAlertStatus: null, dependabotAlertCursor: null, - dependabotAlertFrom: null + dependabotAlertFrom: null, + secretScanningAlertFrom: null, + secretScanningAlertStatus: null, + secretScanningAlertCursor: null }, { where: { subscriptionId: subscription.id @@ -410,6 +421,7 @@ RepoSyncState.init({ buildStatus: DataTypes.ENUM("pending", "complete", "failed"), deploymentStatus: DataTypes.ENUM("pending", "complete", "failed"), dependabotAlertStatus: DataTypes.ENUM("pending", "complete", "failed"), + secretScanningAlertStatus: DataTypes.ENUM("pending", "complete", "failed"), branchCursor: STRING, commitCursor: STRING, issueCursor: STRING, @@ -417,12 +429,14 @@ RepoSyncState.init({ buildCursor: STRING, deploymentCursor: STRING, dependabotAlertCursor: STRING, + secretScanningAlertCursor: STRING, commitFrom: DATE, branchFrom: DATE, pullFrom: DATE, buildFrom: DATE, deploymentFrom: DATE, dependabotAlertFrom: DATE, + secretScanningAlertFrom: DATE, forked: BOOLEAN, repoPushedAt: DATE, repoUpdatedAt: DATE, diff --git a/src/routes/jira/sync/jira-sync-post.test.ts b/src/routes/jira/sync/jira-sync-post.test.ts index 57d3907814..acd734de5c 100644 --- a/src/routes/jira/sync/jira-sync-post.test.ts +++ b/src/routes/jira/sync/jira-sync-post.test.ts @@ -132,7 +132,7 @@ describe("sync", () => { installationId: installationIdForServer, jiraHost, commitsFromDate: commitsFromDate.toISOString(), - targetTasks: ["pull", "branch", "commit", "build", "deployment", "dependabotAlert"], + targetTasks: ["pull", "branch", "commit", "build", "deployment", "dependabotAlert", "secretScanningAlert"], gitHubAppConfig: expect.objectContaining({ gitHubAppId: gitHubServerApp.id, uuid: gitHubServerApp.uuid }) }), expect.anything(), expect.anything()); }); diff --git a/src/routes/jira/sync/jira-sync-post.ts b/src/routes/jira/sync/jira-sync-post.ts index 9b3bc00d54..12010b8a49 100644 --- a/src/routes/jira/sync/jira-sync-post.ts +++ b/src/routes/jira/sync/jira-sync-post.ts @@ -81,5 +81,5 @@ const determineSyncTypeAndTargetTasks = async (syncTypeFromReq: string, subscrip return { syncType: "full", targetTasks: undefined }; } - return { syncType: "partial", targetTasks: ["pull", "branch", "commit", "build", "deployment", "dependabotAlert"] }; + return { syncType: "partial", targetTasks: ["pull", "branch", "commit", "build", "deployment", "dependabotAlert", "secretScanningAlert"] }; }; diff --git a/src/sync/installation.test.ts b/src/sync/installation.test.ts index 47bcc071da..3b4d360d4c 100644 --- a/src/sync/installation.test.ts +++ b/src/sync/installation.test.ts @@ -329,8 +329,8 @@ describe("sync/installation", () => { describe("getTargetTasks", () => { it("should return all tasks if no target tasks present", async () => { - expect(getTargetTasks()).toEqual(["pull", "branch", "commit", "build", "deployment", "dependabotAlert"]); - expect(getTargetTasks([])).toEqual(["pull", "branch", "commit", "build", "deployment", "dependabotAlert"]); + expect(getTargetTasks()).toEqual(["pull", "branch", "commit", "build", "deployment", "dependabotAlert", "secretScanningAlert"]); + expect(getTargetTasks([])).toEqual(["pull", "branch", "commit", "build", "deployment", "dependabotAlert", "secretScanningAlert"]); }); it("should return single target task", async () => { @@ -566,7 +566,9 @@ describe("sync/installation", () => { expect(repoSync.dependabotAlertFrom).toBeNull(); }); - describe.each(["pull", "commit", "build", "deployment", "dependabotAlert"] as TaskType[])("Update jobs status for each tasks", (taskType: TaskType) => { + describe.each( + ["pull", "commit", "build", "deployment", "dependabotAlert", "secretScanningAlert"] as TaskType[] + )("Update jobs status for each tasks", (taskType: TaskType) => { const colTaskFrom = `${taskType}From`; const task: Task = { task: taskType, diff --git a/src/sync/installation.ts b/src/sync/installation.ts index 81e423abbb..64daa6f097 100644 --- a/src/sync/installation.ts +++ b/src/sync/installation.ts @@ -26,6 +26,7 @@ import { sendAnalytics } from "utils/analytics-client"; import { AnalyticsEventTypes, AnalyticsTrackEventsEnum } from "interfaces/common"; import { getNextTasks } from "~/src/sync/scheduler"; import { getDependabotAlertTask } from "./dependabot-alerts"; +import { getSecretScanningAlertTask } from "./secret-scanning-alerts"; const tasks: TaskProcessors = { repository: getRepositoryTask, @@ -34,10 +35,11 @@ const tasks: TaskProcessors = { commit: getCommitTask, build: getBuildTask, deployment: getDeploymentTask, - dependabotAlert: getDependabotAlertTask + dependabotAlert: getDependabotAlertTask, + secretScanningAlert: getSecretScanningAlertTask }; -const allTaskTypes: TaskType[] = ["pull", "branch", "commit", "build", "deployment", "dependabotAlert"]; +const allTaskTypes: TaskType[] = ["pull", "branch", "commit", "build", "deployment", "dependabotAlert", "secretScanningAlert"]; const allTasksExceptBranch = without(allTaskTypes, "branch"); export const getTargetTasks = (targetTasks?: TaskType[]): TaskType[] => { @@ -235,9 +237,11 @@ const sendPayloadToJira = async (task: TaskType, jiraClient, jiraPayload, reposi }); break; case "dependabotAlert": + case "secretScanningAlert":{ await jiraClient.security.submitVulnerabilities(jiraPayload, { operationType: "BACKFILL" }); + } break; default: await jiraClient.devinfo.repository.update(jiraPayload, { diff --git a/src/sync/scheduler.test.ts b/src/sync/scheduler.test.ts index ad63eeeab6..0dedd5ac7d 100644 --- a/src/sync/scheduler.test.ts +++ b/src/sync/scheduler.test.ts @@ -241,7 +241,7 @@ describe("scheduler", () => { expect(task.task).toEqual("commit"); }); }); - it("should not filter by dependabot alerts task if ENABLE_GITHUB_SECURITY_IN_JIRA FF is off", async () => { + it("should filter dependabot alerts task if ENABLE_GITHUB_SECURITY_IN_JIRA FF is off", async () => { when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(false); configureRateLimit(10000, 10000); const repoSyncStates = await RepoSyncState.findAllFromSubscription(subscription); @@ -255,7 +255,7 @@ describe("scheduler", () => { expect(tasks.otherTasks.length).toEqual(0); }); - it("should not filter by dependabot alerts task if ENABLE_GITHUB_SECURITY_IN_JIRA FF is on", async () => { + it("should not filter dependabot alerts task if ENABLE_GITHUB_SECURITY_IN_JIRA FF is on", async () => { when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(true); configureRateLimit(10000, 10000); const repoSyncStates = await RepoSyncState.findAllFromSubscription(subscription); @@ -271,4 +271,33 @@ describe("scheduler", () => { expect(task.task).toEqual("dependabotAlert"); }); }); + it("should filter secret scanning alerts task if ENABLE_GITHUB_SECURITY_IN_JIRA FF is off", async () => { + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(false); + configureRateLimit(10000, 10000); + const repoSyncStates = await RepoSyncState.findAllFromSubscription(subscription); + await Promise.all(repoSyncStates.map((record) => { + record.secretScanningAlertStatus = "pending"; + return record.save(); + })); + githubUserTokenNock(DatabaseStateCreator.GITHUB_INSTALLATION_ID); + const tasks = await getNextTasks(subscription, ["secretScanningAlert"], getLogger("test")); + expect(tasks.mainTask).toBeUndefined(); + expect(tasks.otherTasks.length).toEqual(0); + }); + it("should not filter secret scanning alerts task if ENABLE_GITHUB_SECURITY_IN_JIRA FF is on", async () => { + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(true); + configureRateLimit(10000, 10000); + const repoSyncStates = await RepoSyncState.findAllFromSubscription(subscription); + await Promise.all(repoSyncStates.map((record) => { + record.secretScanningAlertStatus = "pending"; + return record.save(); + })); + + githubUserTokenNock(DatabaseStateCreator.GITHUB_INSTALLATION_ID); + const tasks = await getNextTasks(subscription, ["secretScanningAlert"], getLogger("test")); + expect(tasks.mainTask!.task).toEqual("secretScanningAlert"); + tasks.otherTasks.forEach(task => { + expect(task.task).toEqual("secretScanningAlert"); + }); + }); }); diff --git a/src/sync/scheduler.ts b/src/sync/scheduler.ts index 97f0151668..15fe89e62e 100644 --- a/src/sync/scheduler.ts +++ b/src/sync/scheduler.ts @@ -172,7 +172,7 @@ export const getNextTasks = async (subscription: Subscription, targetTasks: Task let tasks = getTargetTasks(targetTasks); if (!await booleanFlag(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, subscription.jiraHost)) { - tasks = without(tasks, "dependabotAlert"); + tasks = without(tasks, "dependabotAlert", "secretScanningAlert"); } const nSubTasks = await estimateNumberOfSubtasks(subscription, logger); diff --git a/src/sync/secret-scanning-alerts.test.ts b/src/sync/secret-scanning-alerts.test.ts new file mode 100644 index 0000000000..fed461078e --- /dev/null +++ b/src/sync/secret-scanning-alerts.test.ts @@ -0,0 +1,228 @@ +import { DatabaseStateCreator } from "~/test/utils/database-state-creator"; +import secretScanningAlerts from "fixtures/api/secret-scanning-alerts.json"; +import { processInstallation } from "./installation"; +import { Hub } from "@sentry/node"; +import { getLogger } from "../config/logger"; +import { BackfillMessagePayload } from "../sqs/sqs.types"; +import { waitUntil } from "~/test/utils/wait-until"; +import { BooleanFlags, booleanFlag } from "../config/feature-flags"; +import { when } from "jest-when"; +import { GitHubServerApp } from "../models/github-server-app"; + + +jest.mock("config/feature-flags"); +describe("sync/secret-scanning-alerts", () => { + + const sentry: Hub = { setUser: jest.fn() } as any; + const MOCK_SYSTEM_TIMESTAMP_SEC = 12345678; + + + describe("cloud", () => { + + const mockBackfillQueueSendMessage = jest.fn(); + + const verifyMessageSent = async (data: BackfillMessagePayload, delaySec?: number) => { + await waitUntil(async () => { + expect(githubNock).toBeDone(); + expect(jiraNock).toBeDone(); + }); + expect(mockBackfillQueueSendMessage.mock.calls).toHaveLength(1); + expect(mockBackfillQueueSendMessage.mock.calls[0][0]).toEqual(data); + expect(mockBackfillQueueSendMessage.mock.calls[0][1]).toEqual(delaySec || 0); + }; + beforeEach(async () => { + + await new DatabaseStateCreator() + .withActiveRepoSyncState() + .repoSyncStatePendingForSecretScanningAlerts() + .create(); + + mockSystemTime(MOCK_SYSTEM_TIMESTAMP_SEC); + + }); + it("should send secret scanning alerts to Jira", async () => { + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(true); + const data = { installationId: DatabaseStateCreator.GITHUB_INSTALLATION_ID, jiraHost }; + githubNock + .get("/repos/integrations/test-repo-name/secret-scanning/alerts?per_page=20&page=1&sort=created&direction=desc") + .reply(200, secretScanningAlerts); + githubUserTokenNock(DatabaseStateCreator.GITHUB_INSTALLATION_ID); + jiraNock + .post("/rest/security/1.0/bulk", expectedResponseCloudServer()) + .reply(200); + + await expect(processInstallation(mockBackfillQueueSendMessage)(data, sentry, getLogger("test"))).toResolve(); + await verifyMessageSent(data); + }); + + it("should not send secret scanning alerts to Jira if FF is disabled", async () => { + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(false); + const data = { installationId: DatabaseStateCreator.GITHUB_INSTALLATION_ID, jiraHost }; + + await expect(processInstallation(mockBackfillQueueSendMessage)(data, sentry, getLogger("test"))).toResolve(); + expect(mockBackfillQueueSendMessage).not.toBeCalled(); + }); + + it("should not call Jira if no secret scanning alerts are found", async () => { + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(true); + const data = { installationId: DatabaseStateCreator.GITHUB_INSTALLATION_ID, jiraHost }; + githubNock + .get("/repos/integrations/test-repo-name/secret-scanning/alerts?per_page=20&page=1&sort=created&direction=desc") + .reply(200, []); + githubUserTokenNock(DatabaseStateCreator.GITHUB_INSTALLATION_ID); + // No Jira Nock + + await expect(processInstallation(mockBackfillQueueSendMessage)(data, sentry, getLogger("test"))).toResolve(); + await verifyMessageSent(data); + }); + }); + + describe("server", () => { + + const verifyMessageSent = async (data: BackfillMessagePayload, delaySec?: number) => { + await waitUntil(async () => { + expect(gheNock).toBeDone(); + expect(jiraNock).toBeDone(); + }); + expect(mockBackfillQueueSendMessage.mock.calls).toHaveLength(1); + expect(mockBackfillQueueSendMessage.mock.calls[0][0]).toEqual(data); + expect(mockBackfillQueueSendMessage.mock.calls[0][1]).toEqual(delaySec || 0); + }; + + const mockBackfillQueueSendMessage = jest.fn(); + let gitHubServerApp: GitHubServerApp; + + beforeEach(async () => { + + const builderResult = await new DatabaseStateCreator() + .forServer() + .withActiveRepoSyncState() + .repoSyncStatePendingForSecretScanningAlerts() + .create(); + + mockSystemTime(MOCK_SYSTEM_TIMESTAMP_SEC); + + gitHubServerApp = builderResult.gitHubServerApp!; + }); + + it("should send secret scanning alerts to Jira", async () => { + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(true); + const data = { + installationId: DatabaseStateCreator.GITHUB_INSTALLATION_ID, + jiraHost, + gitHubAppConfig: { + uuid: gitHubServerApp.uuid, + gitHubAppId: gitHubServerApp.id, + appId: gitHubServerApp.appId, + clientId: gitHubServerApp.gitHubClientId, + gitHubBaseUrl: gitHubServerApp.gitHubBaseUrl, + gitHubApiUrl: gitHubServerApp.gitHubBaseUrl + } + }; + gheUserTokenNock(DatabaseStateCreator.GITHUB_INSTALLATION_ID); + gheNock + .get("/api/v3/repos/integrations/test-repo-name/secret-scanning/alerts?per_page=20&page=1&sort=created&direction=desc") + .reply(200, secretScanningAlerts); + jiraNock + .post("/rest/security/1.0/bulk", expectedResponseGHEServer()) + .reply(200); + + await expect(processInstallation(mockBackfillQueueSendMessage)(data, sentry, getLogger("test"))).toResolve(); + await verifyMessageSent(data); + }); + + it("should not call Jira if no secret scanning alerts are found", async () => { + when(booleanFlag).calledWith(BooleanFlags.ENABLE_GITHUB_SECURITY_IN_JIRA, expect.anything()).mockResolvedValue(true); + const data = { + installationId: DatabaseStateCreator.GITHUB_INSTALLATION_ID, + jiraHost, + gitHubAppConfig: { + uuid: gitHubServerApp.uuid, + gitHubAppId: gitHubServerApp.id, + appId: gitHubServerApp.appId, + clientId: gitHubServerApp.gitHubClientId, + gitHubBaseUrl: gitHubServerApp.gitHubBaseUrl, + gitHubApiUrl: gitHubServerApp.gitHubBaseUrl + "/v3/api" + } + }; + gheUserTokenNock(DatabaseStateCreator.GITHUB_INSTALLATION_ID); + gheNock + .get("/api/v3/repos/integrations/test-repo-name/secret-scanning/alerts?per_page=20&page=1&sort=created&direction=desc") + .reply(200, []); + // No Jira Nock + + await expect(processInstallation(mockBackfillQueueSendMessage)(data, sentry, getLogger("test"))).toResolve(); + await verifyMessageSent(data); + }); + }); +}); + + +const expectedResponseCloudServer = () => ({ + "vulnerabilities": [ + { + "schemaVersion": "1.0", + "id": "d-1-12", + "updateSequenceNumber": 12345678, + "containerId": "1", + "displayName": "GitHub Personal Access Token", + "description": "Secret scanning alert", + "url": "https://github.com/test-owner/sample-repo/security/secret-scanning/12", + "type": "sast", + "introducedDate": "2023-08-04T04:33:44Z", + "lastUpdated": "2023-08-04T04:33:44Z", + "severity": { + "level": "critical" + }, + "identifiers": [ + { + "displayName": "github_personal_access_token", + "url": "https://github.com/test-owner/sample-repo/security/secret-scanning/12" + } + ], + "status": "open", + "additionalInfo": { + "content": "github_personal_access_token" + } + } + + ], + "properties": { + "gitHubInstallationId": DatabaseStateCreator.GITHUB_INSTALLATION_ID + }, + "operationType": "BACKFILL" +}); + +const expectedResponseGHEServer = () => ({ + "vulnerabilities": [ + { + "schemaVersion": "1.0", + "id": "d-6769746875626d79646f6d61696e636f6d-1-12", + "updateSequenceNumber": 12345678, + "containerId": "6769746875626d79646f6d61696e636f6d-1", + "displayName": "GitHub Personal Access Token", + "description": "Secret scanning alert", + "url": "https://github.com/test-owner/sample-repo/security/secret-scanning/12", + "type": "sast", + "introducedDate": "2023-08-04T04:33:44Z", + "lastUpdated": "2023-08-04T04:33:44Z", + "severity": { + "level": "critical" + }, + "identifiers": [ + { + "displayName": "github_personal_access_token", + "url": "https://github.com/test-owner/sample-repo/security/secret-scanning/12" + } + ], + "status": "open", + "additionalInfo": { + "content": "github_personal_access_token" + } + } + ], + "properties": { + "gitHubInstallationId": DatabaseStateCreator.GITHUB_INSTALLATION_ID + }, + "operationType": "BACKFILL" +}); \ No newline at end of file diff --git a/src/sync/secret-scanning-alerts.ts b/src/sync/secret-scanning-alerts.ts new file mode 100644 index 0000000000..fe87baaee0 --- /dev/null +++ b/src/sync/secret-scanning-alerts.ts @@ -0,0 +1,113 @@ +import { Repository } from "models/subscription"; +import { GitHubInstallationClient } from "../github/client/github-installation-client"; +import Logger from "bunyan"; +import { BackfillMessagePayload } from "~/src/sqs/sqs.types"; +import { transformRepositoryId } from "../transforms/transform-repository-id"; +import { getGitHubClientConfigFromAppId } from "../util/get-github-client-config"; +import { JiraVulnerabilityBulkSubmitData, JiraVulnerabilitySeverityEnum } from "../interfaces/jira"; +import { PageSizeAwareCounterCursor } from "./page-counter-cursor"; +import { SortDirection } from "../github/client/github-client.types"; +import { SecretScanningAlertResponseItem } from "../github/client/secret-scanning-alert.types"; +import { transformGitHubStateToJiraStatus } from "../transforms/transform-secret-scanning-alert"; + +export const getSecretScanningAlertTask = async ( + parentLogger: Logger, + gitHubClient: GitHubInstallationClient, + jiraHost: string, + repository: Repository, + cursor: string | undefined, + perPage: number, + messagePayload: BackfillMessagePayload) => { + + const logger = parentLogger.child({ backfillTask: "Secret scanning alerts" }); + const startTime = Date.now(); + + logger.info({ startTime }, "Secret scanning alerts task started"); + const fromDate = messagePayload?.commitsFromDate ? new Date(messagePayload.commitsFromDate) : undefined; + const smartCursor = new PageSizeAwareCounterCursor(cursor).scale(perPage); + + const { data: secretScanningAlerts } = await gitHubClient.getSecretScanningAlerts(repository.owner.login, repository.name, { + per_page: smartCursor.perPage, + page: smartCursor.pageNo, + sort: "created", + direction: SortDirection.DES + }); + + if (!secretScanningAlerts?.length) { + logger.info({ processingTime: Date.now() - startTime, jiraPayloadLength: 0 }, "Backfill task complete"); + return { + edges: [], + jiraPayload: undefined + }; + } + + if (areAllBuildsEarlierThanFromDate(secretScanningAlerts, fromDate)) { + logger.info({ processingTime: Date.now() - startTime, jiraPayloadLength: 0 }, "Backfill task complete"); + return { + edges: [], + jiraPayload: undefined + }; + } + logger.info(`Found ${secretScanningAlerts.length} secret scanning alerts`); + const nextPageCursorStr = smartCursor.copyWithPageNo(smartCursor.pageNo + 1).serialise(); + const edgesWithCursor = [{ secretScanningAlerts, cursor: nextPageCursorStr }]; + + const jiraPayload = await transformSecretScanningAlert(secretScanningAlerts, repository, jiraHost, logger, messagePayload.gitHubAppConfig?.gitHubAppId); + + logger.info({ processingTime: Date.now() - startTime, jiraPayloadLength: jiraPayload?.vulnerabilities?.length }, "Backfill task complete"); + return { + edges: edgesWithCursor, + jiraPayload + }; +}; + +const areAllBuildsEarlierThanFromDate = (alerts: SecretScanningAlertResponseItem[], fromDate: Date | undefined): boolean => { + + if (!fromDate) return false; + + return alerts.every(alert => { + const createdAt = new Date(alert.created_at); + return createdAt.getTime() < fromDate.getTime(); + }); + +}; + + +const transformSecretScanningAlert = async ( + alerts: SecretScanningAlertResponseItem[], + repository: Repository, + jiraHost: string, + logger: Logger, + gitHubAppId: number | undefined +): Promise => { + + const gitHubClientConfig = await getGitHubClientConfigFromAppId(gitHubAppId, jiraHost); + + const vulnerabilities = alerts.map((alert) => { + return { + schemaVersion: "1.0", + id: `d-${transformRepositoryId(repository.id, gitHubClientConfig.baseUrl)}-${alert.number}`, + updateSequenceNumber: Date.now(), + containerId: transformRepositoryId(repository.id, gitHubClientConfig.baseUrl), + displayName: alert.secret_type_display_name || `${alert.secret_type} secret exposed`, + description: "Secret scanning alert", + url: alert.html_url, + type: "sast", + introducedDate: alert.created_at, + lastUpdated: alert?.resolved_at || alert.created_at, + severity: { + level: JiraVulnerabilitySeverityEnum.CRITICAL + }, + identifiers: [{ + displayName: alert.secret_type, + url: alert.html_url + }], + status: transformGitHubStateToJiraStatus(alert.state, logger), + additionalInfo: { + content: alert.secret_type + } + }; + }); + return { vulnerabilities }; + +}; diff --git a/src/sync/sync.types.ts b/src/sync/sync.types.ts index 3cd07fcc06..7172e69d62 100644 --- a/src/sync/sync.types.ts +++ b/src/sync/sync.types.ts @@ -4,7 +4,7 @@ import { BackfillMessagePayload } from "~/src/sqs/sqs.types"; import Logger from "bunyan"; /* valid task types */ -export type TaskType = "repository" | "pull" | "commit" | "branch" | "build" | "deployment" | "dependabotAlert"; +export type TaskType = "repository" | "pull" | "commit" | "branch" | "build" | "deployment" | "dependabotAlert" | "secretScanningAlert"; export type SyncType = "full" | "partial"; diff --git a/src/transforms/transform-secret-scanning-alert.test.ts b/src/transforms/transform-secret-scanning-alert.test.ts index a84bd29b0b..4915cfb0ee 100644 --- a/src/transforms/transform-secret-scanning-alert.test.ts +++ b/src/transforms/transform-secret-scanning-alert.test.ts @@ -56,6 +56,7 @@ describe("transformSecretScanningAlert", () => { it.each([ ["created", JiraVulnerabilityStatusEnum.OPEN], ["reopened", JiraVulnerabilityStatusEnum.OPEN], + ["open", JiraVulnerabilityStatusEnum.OPEN], ["resolved", JiraVulnerabilityStatusEnum.CLOSED], ["revoked", JiraVulnerabilityStatusEnum.CLOSED], ["unmapped_state", JiraVulnerabilityStatusEnum.UNKNOWN] diff --git a/src/transforms/transform-secret-scanning-alert.ts b/src/transforms/transform-secret-scanning-alert.ts index d705b70bd4..7196242729 100644 --- a/src/transforms/transform-secret-scanning-alert.ts +++ b/src/transforms/transform-secret-scanning-alert.ts @@ -49,6 +49,7 @@ export const transformGitHubStateToJiraStatus = (state: string | undefined, logg switch (state) { case "created": case "reopened": + case "open": return JiraVulnerabilityStatusEnum.OPEN; case "resolved": case "revoked": diff --git a/test/fixtures/api/secret-scanning-alerts.json b/test/fixtures/api/secret-scanning-alerts.json new file mode 100644 index 0000000000..4cfc3b4013 --- /dev/null +++ b/test/fixtures/api/secret-scanning-alerts.json @@ -0,0 +1,21 @@ +[ + { + "number": 12, + "created_at": "2023-08-04T04:33:44Z", + "updated_at": "2023-08-04T04:33:44Z", + "url": "https://api.github.com/repos/test-owner/sample-repo/secret-scanning/alerts/12", + "html_url": "https://github.com/test-owner/sample-repo/security/secret-scanning/12", + "locations_url": "https://api.github.com/repos/test-owner/sample-repo/secret-scanning/alerts/12/locations", + "state": "open", + "secret_type": "github_personal_access_token", + "secret_type_display_name": "GitHub Personal Access Token", + "secret": "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", + "resolution": null, + "resolved_by": null, + "resolved_at": null, + "resolution_comment": null, + "push_protection_bypassed": false, + "push_protection_bypassed_by": null, + "push_protection_bypassed_at": null + } +] \ No newline at end of file diff --git a/test/utils/database-state-creator.ts b/test/utils/database-state-creator.ts index 11131f275f..8fd8747350 100644 --- a/test/utils/database-state-creator.ts +++ b/test/utils/database-state-creator.ts @@ -25,6 +25,7 @@ export class DatabaseStateCreator { private pendingForBuilds: boolean; private pendingForDeployments: boolean; private pendingForDependabotAlerts: boolean; + private pendingForSecretScanningAlerts: boolean; private buildsCustomCursor: string | undefined; private prsCustomCursor: string | undefined; @@ -86,6 +87,11 @@ export class DatabaseStateCreator { return this; } + public repoSyncStatePendingForSecretScanningAlerts() { + this.pendingForSecretScanningAlerts = true; + return this; + } + public repoSyncStateFailedForBranches() { this.failedForBranches = true; return this; @@ -143,6 +149,7 @@ export class DatabaseStateCreator { buildStatus: this.pendingForBuilds ? "pending" : "complete", deploymentStatus: this.pendingForDeployments ? "pending" : "complete", dependabotAlertStatus: this.pendingForDependabotAlerts ? "pending" : "complete", + secretScanningAlertStatus: this.pendingForSecretScanningAlerts ? "pending" : "complete", ... (this.buildsCustomCursor ? { buildCursor: this.buildsCustomCursor } : { }), ... (this.prsCustomCursor ? { pullCursor: this.prsCustomCursor } : { }), updatedAt: new Date(), From cbf7343e2c581ea276090a5291b28430dee0109d Mon Sep 17 00:00:00 2001 From: Harminder Date: Fri, 11 Aug 2023 11:52:44 +1000 Subject: [PATCH 8/9] code refacttored --- src/github/secret-scanning-alert.test.ts | 7 ++----- src/sync/secret-scanning-alerts.test.ts | 14 ++++---------- src/sync/secret-scanning-alerts.ts | 7 ++----- .../transform-secret-scanning-alert.test.ts | 7 ++----- src/transforms/transform-secret-scanning-alert.ts | 7 ++----- 5 files changed, 12 insertions(+), 30 deletions(-) diff --git a/src/github/secret-scanning-alert.test.ts b/src/github/secret-scanning-alert.test.ts index 701d7ad6c5..3d96d86aaf 100644 --- a/src/github/secret-scanning-alert.test.ts +++ b/src/github/secret-scanning-alert.test.ts @@ -136,7 +136,7 @@ describe("SecretScanningAlertWebhookHandler", () => { vulnerabilities: [ { schemaVersion: "1.0", - id: "d-456-123", + id: "s-456-123", updateSequenceNumber: Date.now(), containerId: "456", displayName: "personal_access_token secret exposed", @@ -152,10 +152,7 @@ describe("SecretScanningAlertWebhookHandler", () => { "displayName": "personal_access_token", "url":SAMPLE_SECURITY_URL }], - status: JIRA_VULNERABILITY_STATUS_ENUM_OPEN, - additionalInfo: { - content: "personal_access_token" - } + status: JIRA_VULNERABILITY_STATUS_ENUM_OPEN } ] }; diff --git a/src/sync/secret-scanning-alerts.test.ts b/src/sync/secret-scanning-alerts.test.ts index fed461078e..3e01b852dc 100644 --- a/src/sync/secret-scanning-alerts.test.ts +++ b/src/sync/secret-scanning-alerts.test.ts @@ -162,7 +162,7 @@ const expectedResponseCloudServer = () => ({ "vulnerabilities": [ { "schemaVersion": "1.0", - "id": "d-1-12", + "id": "s-1-12", "updateSequenceNumber": 12345678, "containerId": "1", "displayName": "GitHub Personal Access Token", @@ -180,10 +180,7 @@ const expectedResponseCloudServer = () => ({ "url": "https://github.com/test-owner/sample-repo/security/secret-scanning/12" } ], - "status": "open", - "additionalInfo": { - "content": "github_personal_access_token" - } + "status": "open" } ], @@ -197,7 +194,7 @@ const expectedResponseGHEServer = () => ({ "vulnerabilities": [ { "schemaVersion": "1.0", - "id": "d-6769746875626d79646f6d61696e636f6d-1-12", + "id": "s-6769746875626d79646f6d61696e636f6d-1-12", "updateSequenceNumber": 12345678, "containerId": "6769746875626d79646f6d61696e636f6d-1", "displayName": "GitHub Personal Access Token", @@ -215,10 +212,7 @@ const expectedResponseGHEServer = () => ({ "url": "https://github.com/test-owner/sample-repo/security/secret-scanning/12" } ], - "status": "open", - "additionalInfo": { - "content": "github_personal_access_token" - } + "status": "open" } ], "properties": { diff --git a/src/sync/secret-scanning-alerts.ts b/src/sync/secret-scanning-alerts.ts index fe87baaee0..501ce06121 100644 --- a/src/sync/secret-scanning-alerts.ts +++ b/src/sync/secret-scanning-alerts.ts @@ -86,7 +86,7 @@ const transformSecretScanningAlert = async ( const vulnerabilities = alerts.map((alert) => { return { schemaVersion: "1.0", - id: `d-${transformRepositoryId(repository.id, gitHubClientConfig.baseUrl)}-${alert.number}`, + id: `s-${transformRepositoryId(repository.id, gitHubClientConfig.baseUrl)}-${alert.number}`, updateSequenceNumber: Date.now(), containerId: transformRepositoryId(repository.id, gitHubClientConfig.baseUrl), displayName: alert.secret_type_display_name || `${alert.secret_type} secret exposed`, @@ -102,10 +102,7 @@ const transformSecretScanningAlert = async ( displayName: alert.secret_type, url: alert.html_url }], - status: transformGitHubStateToJiraStatus(alert.state, logger), - additionalInfo: { - content: alert.secret_type - } + status: transformGitHubStateToJiraStatus(alert.state, logger) }; }); return { vulnerabilities }; diff --git a/src/transforms/transform-secret-scanning-alert.test.ts b/src/transforms/transform-secret-scanning-alert.test.ts index 4915cfb0ee..8ab42f9c83 100644 --- a/src/transforms/transform-secret-scanning-alert.test.ts +++ b/src/transforms/transform-secret-scanning-alert.test.ts @@ -80,7 +80,7 @@ describe("transformSecretScanningAlert", () => { "containerId": "1", "description": "Secret scanning alert", "displayName": "personal_access_token secret exposed", - "id": "d-1-123", + "id": "s-1-123", "identifiers": [{ "displayName": "personal_access_token", "url": "https://sample/123" @@ -94,10 +94,7 @@ describe("transformSecretScanningAlert", () => { "status": "open", "type": "sast", "updateSequenceNumber": Date.now(), - "url": "https://sample/123", - "additionalInfo": { - "content": "personal_access_token" - } + "url": "https://sample/123" } ]); }); diff --git a/src/transforms/transform-secret-scanning-alert.ts b/src/transforms/transform-secret-scanning-alert.ts index 7196242729..602dfb0266 100644 --- a/src/transforms/transform-secret-scanning-alert.ts +++ b/src/transforms/transform-secret-scanning-alert.ts @@ -14,7 +14,7 @@ export const transformSecretScanningAlert = async (context: WebhookContext Date: Fri, 11 Aug 2023 12:00:14 +1000 Subject: [PATCH 9/9] missed typing file --- src/interfaces/jira.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interfaces/jira.ts b/src/interfaces/jira.ts index b6deb57068..42673cb7f7 100644 --- a/src/interfaces/jira.ts +++ b/src/interfaces/jira.ts @@ -210,7 +210,7 @@ export interface JiraVulnerability { severity: JiraVulnerabilitySeverity; identifiers: JiraVulnerabilityIdentifier[]; status: JiraVulnerabilityStatusEnum; - additionalInfo: JiraVulnerabilityAdditionalInfo; + additionalInfo?: JiraVulnerabilityAdditionalInfo; } export interface JiraVulnerabilitySeverity {