From 19e08726bc5e1db9a255a0c64400393bcc5564d0 Mon Sep 17 00:00:00 2001 From: Kunal Nagar <2741371+kunalnagar@users.noreply.github.com> Date: Mon, 5 Aug 2024 18:03:55 -0400 Subject: [PATCH] feat: Support Org and Enterprise level alerts (#190) Fixes #187 --- .github/workflows/ci.yml | 2 + action.yml | 8 +++- src/destinations/email.ts | 14 +++---- src/destinations/microsoft-teams.ts | 7 +++- src/destinations/pager-duty.ts | 2 +- src/destinations/slack.ts | 5 ++- src/destinations/zenduty.ts | 5 ++- src/entities/alert.ts | 41 ++++++++++++++++++- src/fetch-alerts.ts | 63 +++++++++++++++++++++++++++-- src/main.ts | 42 ++++++++++++++----- 10 files changed, 156 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb8181c3..37bc97ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,8 @@ jobs: uses: ./ with: token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + # org: ${{ secrets.ORG_NAME }} + # enterprise: ${{ secrets.ENTERPRISE_NAME }} # microsoft_teams_webhook: ${{ secrets.MICROSOFT_TEAMS_WEBHOOK }} # slack_webhook: ${{ secrets.SLACK_WEBHOOK }} # severity: low,medium diff --git a/action.yml b/action.yml index aac83a4c..6af172ff 100644 --- a/action.yml +++ b/action.yml @@ -1,9 +1,13 @@ name: 'check-cve' -description: 'Send GitHub vulnerability alerts to multiple platforms like Slack, PagerDuty.' +description: 'Send GitHub vulnerability alerts to multiple platforms.' author: '@kunalnagar' inputs: token: description: 'GitHub Personal Access Token' + org: + description: 'Org name to support Org level alerts: https://docs.github.com/en/rest/dependabot/alerts?apiVersion=2022-11-28#list-dependabot-alerts-for-an-organization' + enterprise: + description: 'Enterprise name to support Enterprise level alerts: https://docs.github.com/en/rest/dependabot/alerts?apiVersion=2022-11-28#list-dependabot-alerts-for-an-enterprise' microsoft_teams_webhook: description: 'Microsoft Teams Channel Webhook URL. More info: https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/add-incoming-webhook' slack_webhook: @@ -38,7 +42,7 @@ inputs: severity: description: 'Comma separated list of severities. E.g. low,medium,high,critical (NO SPACES BETWEEN COMMA AND SEVERITY)' ecosystem: - description: "A comma-separated list of ecosystems. If specified, only alerts for these ecosystems will be returned." + description: 'A comma-separated list of ecosystems. If specified, only alerts for these ecosystems will be returned.' branding: icon: 'alert-octagon' color: 'red' diff --git a/src/destinations/email.ts b/src/destinations/email.ts index 4017704d..84b63bb8 100644 --- a/src/destinations/email.ts +++ b/src/destinations/email.ts @@ -7,6 +7,7 @@ import { Alert, getFullRepositoryNameFromAlert } from '../entities' const createTableRow = (alert: Alert): string => ` ${alert.packageName} + ${getFullRepositoryNameFromAlert(alert)} ${alert.vulnerability?.vulnerableVersionRange} ${alert.vulnerability?.firstPatchedVersion} ${alert.advisory?.severity} @@ -23,7 +24,8 @@ const createTable = (alerts: Alert[]): string => { return ` - + + @@ -39,9 +41,7 @@ const createTable = (alerts: Alert[]): string => { const createEmailBody = (alerts: Alert[]): string => `

Hello,

-

You are receiving this message as you have set up email notifications for vulnerabilities in ${getFullRepositoryNameFromAlert( - alerts[0], - )} via ${ACTION_SHORT_SUMMARY}.

+

You are receiving this message as you have set up email notifications for vulnerabilities via ${ACTION_SHORT_SUMMARY}.

${createTable(alerts)} ` @@ -56,11 +56,7 @@ export const sendAlertsToEmailSmtp = async ( await transporter.sendMail({ from: emailFrom, bcc: emailList, - subject: - subject || - `${ACTION_SHORT_SUMMARY} - ${ - alerts.length - } vulnerabilities in ${getFullRepositoryNameFromAlert(alerts[0])}`, + subject: subject || ACTION_SHORT_SUMMARY, html: createEmailBody(alerts), }) } diff --git a/src/destinations/microsoft-teams.ts b/src/destinations/microsoft-teams.ts index 4232c479..8ab6ff46 100644 --- a/src/destinations/microsoft-teams.ts +++ b/src/destinations/microsoft-teams.ts @@ -9,7 +9,7 @@ import { request, } from '../utils' import { ACTION_SHORT_SUMMARY } from '../constants' -import { Alert } from '../entities' +import { Alert, getFullRepositoryNameFromAlert } from '../entities' const createTableRow = (key: string, value: string): Row => { const row = createRow() @@ -53,7 +53,10 @@ export const sendAlertsToMicrosoftTeams = async ( alerts.forEach((alert) => { const container = createContainer(true, true) - container.addItem(createTableRow('Package Name', alert.packageName)) + container.addItem(createTableRow('Package', alert.packageName)) + container.addItem( + createTableRow('Repository', getFullRepositoryNameFromAlert(alert)), + ) container.addItem( createTableRow( 'Vulnerability Version Range', diff --git a/src/destinations/pager-duty.ts b/src/destinations/pager-duty.ts index fc0f50bd..faaa0d48 100644 --- a/src/destinations/pager-duty.ts +++ b/src/destinations/pager-duty.ts @@ -12,7 +12,7 @@ export const sendAlertsToPagerDuty = async ( routing_key: integrationKey, event_action: 'trigger', payload: { - summary: `You have ${alerts.length} vulnerabilities in ${alerts[0].repository.owner}/${alerts[0].repository.name}`, + summary: `You have ${alerts.length} vulnerabilities`, source: 'GitHub Dependabot Alerts', severity: 'info', custom_details: { ...alerts }, diff --git a/src/destinations/slack.ts b/src/destinations/slack.ts index 489f8fe7..0cd043f1 100644 --- a/src/destinations/slack.ts +++ b/src/destinations/slack.ts @@ -2,7 +2,7 @@ import { IncomingWebhook } from '@slack/webhook' import { KnownBlock } from '@slack/types' import { ACTION_ICON, ACTION_SHORT_SUMMARY } from '../constants' -import { Alert } from '../entities' +import { Alert, getFullRepositoryNameFromAlert } from '../entities' export const MAX_COUNT_SLACK = 30 @@ -33,7 +33,8 @@ const createAlertBlock = (alert: Alert): KnownBlock => ({ text: { type: 'mrkdwn', text: ` -*Package name:* ${alert.packageName} +*Package:* ${alert.packageName} +*Repository:* ${getFullRepositoryNameFromAlert(alert)} *Vulnerability Version Range:* ${alert.vulnerability?.vulnerableVersionRange} *Patched Version:* ${alert.vulnerability?.firstPatchedVersion} *Severity:* ${alert.advisory?.severity} diff --git a/src/destinations/zenduty.ts b/src/destinations/zenduty.ts index 7bb71be5..ba44dd00 100644 --- a/src/destinations/zenduty.ts +++ b/src/destinations/zenduty.ts @@ -1,5 +1,5 @@ import { ACTION_SHORT_SUMMARY } from '../constants' -import { Alert } from '../entities' +import { Alert, getFullRepositoryNameFromAlert } from '../entities' import { request } from '../utils' export const sendAlertsToZenduty = async ( @@ -16,7 +16,8 @@ export const sendAlertsToZenduty = async ( ` alerts.forEach((alert) => { summary += ` - Package name: ${alert.packageName} + Package: ${alert.packageName} + Repository: ${getFullRepositoryNameFromAlert(alert)} Vulnerability Version Range: ${alert.vulnerability?.vulnerableVersionRange} Patched Version: ${alert.vulnerability?.firstPatchedVersion} Severity: ${alert.advisory?.severity} diff --git a/src/entities/alert.ts b/src/entities/alert.ts index ebd24063..e76f6c95 100644 --- a/src/entities/alert.ts +++ b/src/entities/alert.ts @@ -15,7 +15,7 @@ export interface Alert { createdAt: string } -export const toAlert = ( +export const toRepositoryAlert = ( dependabotAlert: DependabotAlert, repositoryName: string, repositoryOwner: string, @@ -33,3 +33,42 @@ export const toAlert = ( : undefined, createdAt: dependabotAlert.created_at, }) + +export type DependabotOrgAlert = + Endpoints['GET /orgs/{org}/dependabot/alerts']['response']['data'][0] + +export const toOrgAlert = (dependabotOrgAlert: DependabotOrgAlert): Alert => ({ + repository: { + name: dependabotOrgAlert.repository.name, + owner: dependabotOrgAlert.repository.owner.login, + }, + packageName: dependabotOrgAlert.security_vulnerability.package.name || '', + advisory: dependabotOrgAlert.security_advisory + ? toAdvisory(dependabotOrgAlert.security_advisory) + : undefined, + vulnerability: dependabotOrgAlert.security_vulnerability + ? toVulnerability(dependabotOrgAlert.security_vulnerability) + : undefined, + createdAt: dependabotOrgAlert.created_at, +}) + +export type DependabotEnterpriseAlert = + Endpoints['GET /enterprises/{enterprise}/dependabot/alerts']['response']['data'][0] + +export const toEnterpriseAlert = ( + dependabotEnterpriseAlert: DependabotEnterpriseAlert, +): Alert => ({ + repository: { + name: dependabotEnterpriseAlert.repository.name, + owner: dependabotEnterpriseAlert.repository.owner.login, + }, + packageName: + dependabotEnterpriseAlert.security_vulnerability.package.name || '', + advisory: dependabotEnterpriseAlert.security_advisory + ? toAdvisory(dependabotEnterpriseAlert.security_advisory) + : undefined, + vulnerability: dependabotEnterpriseAlert.security_vulnerability + ? toVulnerability(dependabotEnterpriseAlert.security_vulnerability) + : undefined, + createdAt: dependabotEnterpriseAlert.created_at, +}) diff --git a/src/fetch-alerts.ts b/src/fetch-alerts.ts index 06a90060..86b399bc 100644 --- a/src/fetch-alerts.ts +++ b/src/fetch-alerts.ts @@ -1,8 +1,13 @@ import { Octokit } from '@octokit/rest' -import { Alert, toAlert } from './entities' +import { + Alert, + toRepositoryAlert, + toOrgAlert, + toEnterpriseAlert, +} from './entities' -export const fetchAlerts = async ( +export const fetchRepositoryAlerts = async ( gitHubPersonalAccessToken: string, repositoryName: string, repositoryOwner: string, @@ -25,7 +30,59 @@ export const fetchAlerts = async ( per_page: count, }) const alerts: Alert[] = response.data.map((dependabotAlert) => - toAlert(dependabotAlert, repositoryName, repositoryOwner), + toRepositoryAlert(dependabotAlert, repositoryName, repositoryOwner), + ) + return alerts +} + +export const fetchOrgAlerts = async ( + gitHubPersonalAccessToken: string, + org: string, + severity: string, + ecosystem: string, + count: number, +): Promise => { + const octokit = new Octokit({ + auth: gitHubPersonalAccessToken, + request: { + fetch, + }, + }) + const response = await octokit.dependabot.listAlertsForOrg({ + org, + state: 'open', + severity, + ecosystem: ecosystem.length > 0 ? ecosystem : undefined, + per_page: count, + }) + const alerts: Alert[] = response.data.map((dependabotOrgAlert) => + toOrgAlert(dependabotOrgAlert), + ) + return alerts +} + +export const fetchEnterpriseAlerts = async ( + gitHubPersonalAccessToken: string, + enterprise: string, + severity: string, + ecosystem: string, + count: number, +): Promise => { + const octokit = new Octokit({ + auth: gitHubPersonalAccessToken, + request: { + fetch, + }, + }) + const response = await octokit.dependabot.listAlertsForEnterprise({ + enterprise, + state: 'open', + severity, + ecosystem: ecosystem.length > 0 ? ecosystem : undefined, + per_page: count, + }) + const alerts: Alert[] = response.data.map((dependabotEnterpriseAlert) => + toEnterpriseAlert(dependabotEnterpriseAlert), ) return alerts } diff --git a/src/main.ts b/src/main.ts index 4c02a561..f5143cd7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,11 +9,18 @@ import { sendAlertsToEmailSmtp, validateSlackWebhookUrl, } from './destinations' -import { fetchAlerts } from './fetch-alerts' +import { + fetchRepositoryAlerts, + fetchOrgAlerts, + fetchEnterpriseAlerts, +} from './fetch-alerts' +import { Alert } from './entities' async function run(): Promise { try { const token = getInput('token') + const org = getInput('org') + const enterprise = getInput('enterprise') const microsoftTeamsWebhookUrl = getInput('microsoft_teams_webhook') const slackWebhookUrl = getInput('slack_webhook') const pagerDutyIntegrationKey = getInput('pager_duty_integration_key') @@ -32,16 +39,29 @@ async function run(): Promise { const count = parseInt(getInput('count')) const severity = getInput('severity') const ecosystem = getInput('ecosystem') - const { owner } = context.repo - const { repo } = context.repo - const alerts = await fetchAlerts( - token, - repo, - owner, - severity, - ecosystem, - count, - ) + + let alerts: Alert[] = [] + if (org) { + alerts = await fetchOrgAlerts(token, org, severity, ecosystem, count) + } else if (enterprise) { + alerts = await fetchEnterpriseAlerts( + token, + org, + severity, + ecosystem, + count, + ) + } else { + const { owner, repo } = context.repo + alerts = await fetchRepositoryAlerts( + token, + repo, + owner, + severity, + ecosystem, + count, + ) + } if (alerts.length > 0) { if (microsoftTeamsWebhookUrl) { await sendAlertsToMicrosoftTeams(microsoftTeamsWebhookUrl, alerts)
Package namePackageRepository Vulnerability Version Range Patched Version Severity