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 `
- Package name |
+ Package |
+ Repository |
Vulnerability Version Range |
Patched Version |
Severity |
@@ -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)