From 91892986013239d74723a58403d471678a3cfa19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:16:02 +0000 Subject: [PATCH 1/6] Initial plan From b65f4ef80dacc5debaf59fbc5ffaf6d71091ba96 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:24:36 +0000 Subject: [PATCH 2/6] Add security alert discovery to campaign discovery system Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- actions/setup/js/campaign_discovery.cjs | 162 ++++++++++++++++++++++++ 1 file changed, 162 insertions(+) diff --git a/actions/setup/js/campaign_discovery.cjs b/actions/setup/js/campaign_discovery.cjs index 79132d52a4..1daf126a56 100644 --- a/actions/setup/js/campaign_discovery.cjs +++ b/actions/setup/js/campaign_discovery.cjs @@ -221,6 +221,151 @@ async function searchByLabel(octokit, label, repos, orgs, maxItems, maxPages, cu return searchItems(octokit, searchQuery, `label: ${label}`, maxItems, maxPages, cursor, { label }); } +/** + * Discover security alerts for a repository + * @param {any} octokit - GitHub API client + * @param {string[]} repos - List of repositories to search (owner/repo format) + * @returns {Promise} Security alerts summary + */ +async function discoverSecurityAlerts(octokit, repos) { + if (!repos || repos.length === 0) { + core.warning("No repos specified for security alert discovery"); + return null; + } + + const alerts = { + code_scanning: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ ([]) }, + secret_scanning: { total: 0, by_state: {}, items: /** @type {any[]} */ ([]) }, + dependabot: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ ([]) }, + }; + + // Discover alerts for each repository + for (const repoFullName of repos) { + const [owner, repo] = repoFullName.split("/"); + if (!owner || !repo) { + core.warning(`Invalid repo format: ${repoFullName}. Expected owner/repo`); + continue; + } + + core.info(`Discovering security alerts for ${repoFullName}...`); + + // Code Scanning Alerts + try { + const response = await octokit.rest.codeScanning.listAlertsForRepo({ + owner, + repo, + per_page: 100, + state: "open", + }); + + const codeAlerts = response.data; + alerts.code_scanning.total += codeAlerts.length; + + for (const alert of codeAlerts) { + const severity = alert.rule?.severity || "unknown"; + alerts.code_scanning.by_severity[severity] = (alerts.code_scanning.by_severity[severity] || 0) + 1; + alerts.code_scanning.by_state[alert.state] = (alerts.code_scanning.by_state[alert.state] || 0) + 1; + + alerts.code_scanning.items.push({ + type: "code_scanning", + number: alert.number, + url: alert.html_url, + state: alert.state, + severity: severity, + rule_id: alert.rule?.id, + rule_description: alert.rule?.description, + created_at: alert.created_at, + updated_at: alert.updated_at, + repository: repoFullName, + }); + } + + core.info(` Code scanning: ${codeAlerts.length} open alerts`); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + core.warning(`Failed to fetch code scanning alerts for ${repoFullName}: ${err.message}`); + } + + // Secret Scanning Alerts + try { + const response = await octokit.rest.secretScanning.listAlertsForRepo({ + owner, + repo, + per_page: 100, + state: "open", + }); + + const secretAlerts = response.data; + alerts.secret_scanning.total += secretAlerts.length; + + for (const alert of secretAlerts) { + alerts.secret_scanning.by_state[alert.state] = (alerts.secret_scanning.by_state[alert.state] || 0) + 1; + + alerts.secret_scanning.items.push({ + type: "secret_scanning", + number: alert.number, + url: alert.html_url, + state: alert.state, + secret_type: alert.secret_type, + created_at: alert.created_at, + updated_at: alert.updated_at, + repository: repoFullName, + }); + } + + core.info(` Secret scanning: ${secretAlerts.length} open alerts`); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + core.warning(`Failed to fetch secret scanning alerts for ${repoFullName}: ${err.message}`); + } + + // Dependabot Alerts + try { + const response = await octokit.rest.dependabot.listAlertsForRepo({ + owner, + repo, + per_page: 100, + state: "open", + }); + + const dependabotAlerts = response.data; + alerts.dependabot.total += dependabotAlerts.length; + + for (const alert of dependabotAlerts) { + const severity = alert.security_advisory?.severity || "unknown"; + alerts.dependabot.by_severity[severity] = (alerts.dependabot.by_severity[severity] || 0) + 1; + alerts.dependabot.by_state[alert.state] = (alerts.dependabot.by_state[alert.state] || 0) + 1; + + alerts.dependabot.items.push({ + type: "dependabot", + number: alert.number, + url: alert.html_url, + state: alert.state, + severity: severity, + package_name: alert.security_advisory?.package?.name, + created_at: alert.created_at, + updated_at: alert.updated_at, + repository: repoFullName, + }); + } + + core.info(` Dependabot: ${dependabotAlerts.length} open alerts`); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + core.warning(`Failed to fetch Dependabot alerts for ${repoFullName}: ${err.message}`); + } + } + + // Log summary + const totalAlerts = alerts.code_scanning.total + alerts.secret_scanning.total + alerts.dependabot.total; + core.info(`✓ Security alert discovery complete: ${totalAlerts} total alerts`); + core.info(` Code scanning: ${alerts.code_scanning.total}`); + core.info(` Secret scanning: ${alerts.secret_scanning.total}`); + core.info(` Dependabot: ${alerts.dependabot.total}`); + + return alerts; +} + /** * Main discovery function * @param {any} config - Configuration object @@ -333,6 +478,13 @@ async function discover(config) { const budgetExhausted = itemsBudgetExhausted || pagesBudgetExhausted; const exhaustedReason = budgetExhausted ? (itemsBudgetExhausted ? "max_items_reached" : "max_pages_reached") : null; + // Security alert discovery (for security-focused campaigns) + let securityAlerts = null; + if (campaignId.toLowerCase().includes("security")) { + core.info("Security-focused campaign detected - discovering security alerts..."); + securityAlerts = await discoverSecurityAlerts(octokit, repos); + } + // Build manifest const manifest = { schema_version: MANIFEST_VERSION, @@ -359,6 +511,11 @@ async function discover(config) { items: allItems, }; + // Add security alerts to manifest if discovered + if (securityAlerts) { + manifest.security_alerts = securityAlerts; + } + // Save cursor if provided if (cursorPath) { saveCursor(cursorPath, cursor); @@ -374,6 +531,11 @@ async function discover(config) { core.info(`Summary: ${needsAddCount} to add, ${needsUpdateCount} to update`); + if (securityAlerts) { + const totalSecurityAlerts = securityAlerts.code_scanning.total + securityAlerts.secret_scanning.total + securityAlerts.dependabot.total; + core.info(`Security alerts: ${totalSecurityAlerts} total (${securityAlerts.code_scanning.total} code scanning, ${securityAlerts.secret_scanning.total} secret scanning, ${securityAlerts.dependabot.total} dependabot)`); + } + return manifest; } From c9e9714640700d13d63f9993559990ce837a4be6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:26:37 +0000 Subject: [PATCH 3/6] Add comprehensive tests for security alert discovery Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- actions/setup/js/campaign_discovery.cjs | 7 +- actions/setup/js/campaign_discovery.test.cjs | 193 ++++++++++++++++++- 2 files changed, 196 insertions(+), 4 deletions(-) diff --git a/actions/setup/js/campaign_discovery.cjs b/actions/setup/js/campaign_discovery.cjs index 1daf126a56..73aba6dffd 100644 --- a/actions/setup/js/campaign_discovery.cjs +++ b/actions/setup/js/campaign_discovery.cjs @@ -234,9 +234,9 @@ async function discoverSecurityAlerts(octokit, repos) { } const alerts = { - code_scanning: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ ([]) }, - secret_scanning: { total: 0, by_state: {}, items: /** @type {any[]} */ ([]) }, - dependabot: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ ([]) }, + code_scanning: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ [] }, + secret_scanning: { total: 0, by_state: {}, items: /** @type {any[]} */ [] }, + dependabot: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ [] }, }; // Discover alerts for each repository @@ -615,6 +615,7 @@ async function main() { module.exports = { main, discover, + discoverSecurityAlerts, normalizeItem, searchByTrackerId, searchByLabel, diff --git a/actions/setup/js/campaign_discovery.test.cjs b/actions/setup/js/campaign_discovery.test.cjs index 68b524b0d6..9b1b12451a 100644 --- a/actions/setup/js/campaign_discovery.test.cjs +++ b/actions/setup/js/campaign_discovery.test.cjs @@ -1,6 +1,6 @@ // @ts-check import { describe, it, expect, beforeEach, vi } from "vitest"; -import { normalizeItem, loadCursor, saveCursor, searchByTrackerId, searchByLabel, searchItems, buildScopeParts, discover } from "./campaign_discovery.cjs"; +import { normalizeItem, loadCursor, saveCursor, searchByTrackerId, searchByLabel, searchItems, buildScopeParts, discover, discoverSecurityAlerts } from "./campaign_discovery.cjs"; import fs from "fs"; import path from "path"; @@ -952,4 +952,195 @@ describe("campaign_discovery", () => { expect(result.items).toHaveLength(50); }); }); + + describe("discoverSecurityAlerts", () => { + it("should discover code scanning alerts", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ + data: [ + { + number: 1, + html_url: "https://github.com/owner/repo/security/code-scanning/1", + state: "open", + rule: { + id: "go/unsafe-quoting", + severity: "critical", + description: "Unsafe quoting in code", + }, + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-02T00:00:00Z", + }, + { + number: 2, + html_url: "https://github.com/owner/repo/security/code-scanning/2", + state: "open", + rule: { + id: "js/xss", + severity: "high", + description: "Cross-site scripting vulnerability", + }, + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-02T00:00:00Z", + }, + ], + }), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); + + expect(result).not.toBeNull(); + expect(result.code_scanning.total).toBe(2); + expect(result.code_scanning.by_severity.critical).toBe(1); + expect(result.code_scanning.by_severity.high).toBe(1); + expect(result.code_scanning.items).toHaveLength(2); + expect(result.code_scanning.items[0]).toMatchObject({ + type: "code_scanning", + number: 1, + severity: "critical", + rule_id: "go/unsafe-quoting", + }); + }); + + it("should discover secret scanning alerts", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ + data: [ + { + number: 10, + html_url: "https://github.com/owner/repo/security/secret-scanning/10", + state: "open", + secret_type: "github_personal_access_token", + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-02T00:00:00Z", + }, + ], + }), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); + + expect(result).not.toBeNull(); + expect(result.secret_scanning.total).toBe(1); + expect(result.secret_scanning.items).toHaveLength(1); + expect(result.secret_scanning.items[0]).toMatchObject({ + type: "secret_scanning", + number: 10, + secret_type: "github_personal_access_token", + }); + }); + + it("should discover dependabot alerts", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockResolvedValue({ + data: [ + { + number: 20, + html_url: "https://github.com/owner/repo/security/dependabot/20", + state: "open", + security_advisory: { + severity: "high", + package: { name: "lodash" }, + }, + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-02T00:00:00Z", + }, + ], + }), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); + + expect(result).not.toBeNull(); + expect(result.dependabot.total).toBe(1); + expect(result.dependabot.items).toHaveLength(1); + expect(result.dependabot.items[0]).toMatchObject({ + type: "dependabot", + number: 20, + severity: "high", + package_name: "lodash", + }); + }); + + it("should handle API errors gracefully", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); + + // Should return empty results instead of throwing + expect(result).not.toBeNull(); + expect(result.code_scanning.total).toBe(0); + expect(result.secret_scanning.total).toBe(0); + expect(result.dependabot.total).toBe(0); + }); + + it("should return null if no repos specified", async () => { + const octokit = {}; + const result = await discoverSecurityAlerts(octokit, []); + expect(result).toBeNull(); + }); + + it("should handle invalid repo format", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["invalid-repo-format"]); + + // Should skip invalid repos but still return results + expect(result).not.toBeNull(); + expect(result.code_scanning.total).toBe(0); + }); + }); }); From cc8ad1b04baabfffb535bc6406be261989609f8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 08:45:56 +0000 Subject: [PATCH 4/6] Refactor: Move security alert discovery to campaign-specific script - Reverted generic campaign_discovery.cjs changes to remove coupling - Created security_alert_discovery.cjs as campaign-specific script - Moved all security alert discovery logic to dedicated file - Added comprehensive tests (7 tests, all passing) - Original campaign_discovery tests still pass (34 tests) Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- actions/setup/js/campaign_discovery.cjs | 163 ------------ actions/setup/js/campaign_discovery.test.cjs | 193 +------------- actions/setup/js/security_alert_discovery.cjs | 234 +++++++++++++++++ .../js/security_alert_discovery.test.cjs | 245 ++++++++++++++++++ 4 files changed, 480 insertions(+), 355 deletions(-) create mode 100644 actions/setup/js/security_alert_discovery.cjs create mode 100644 actions/setup/js/security_alert_discovery.test.cjs diff --git a/actions/setup/js/campaign_discovery.cjs b/actions/setup/js/campaign_discovery.cjs index 73aba6dffd..79132d52a4 100644 --- a/actions/setup/js/campaign_discovery.cjs +++ b/actions/setup/js/campaign_discovery.cjs @@ -221,151 +221,6 @@ async function searchByLabel(octokit, label, repos, orgs, maxItems, maxPages, cu return searchItems(octokit, searchQuery, `label: ${label}`, maxItems, maxPages, cursor, { label }); } -/** - * Discover security alerts for a repository - * @param {any} octokit - GitHub API client - * @param {string[]} repos - List of repositories to search (owner/repo format) - * @returns {Promise} Security alerts summary - */ -async function discoverSecurityAlerts(octokit, repos) { - if (!repos || repos.length === 0) { - core.warning("No repos specified for security alert discovery"); - return null; - } - - const alerts = { - code_scanning: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ [] }, - secret_scanning: { total: 0, by_state: {}, items: /** @type {any[]} */ [] }, - dependabot: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ [] }, - }; - - // Discover alerts for each repository - for (const repoFullName of repos) { - const [owner, repo] = repoFullName.split("/"); - if (!owner || !repo) { - core.warning(`Invalid repo format: ${repoFullName}. Expected owner/repo`); - continue; - } - - core.info(`Discovering security alerts for ${repoFullName}...`); - - // Code Scanning Alerts - try { - const response = await octokit.rest.codeScanning.listAlertsForRepo({ - owner, - repo, - per_page: 100, - state: "open", - }); - - const codeAlerts = response.data; - alerts.code_scanning.total += codeAlerts.length; - - for (const alert of codeAlerts) { - const severity = alert.rule?.severity || "unknown"; - alerts.code_scanning.by_severity[severity] = (alerts.code_scanning.by_severity[severity] || 0) + 1; - alerts.code_scanning.by_state[alert.state] = (alerts.code_scanning.by_state[alert.state] || 0) + 1; - - alerts.code_scanning.items.push({ - type: "code_scanning", - number: alert.number, - url: alert.html_url, - state: alert.state, - severity: severity, - rule_id: alert.rule?.id, - rule_description: alert.rule?.description, - created_at: alert.created_at, - updated_at: alert.updated_at, - repository: repoFullName, - }); - } - - core.info(` Code scanning: ${codeAlerts.length} open alerts`); - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - core.warning(`Failed to fetch code scanning alerts for ${repoFullName}: ${err.message}`); - } - - // Secret Scanning Alerts - try { - const response = await octokit.rest.secretScanning.listAlertsForRepo({ - owner, - repo, - per_page: 100, - state: "open", - }); - - const secretAlerts = response.data; - alerts.secret_scanning.total += secretAlerts.length; - - for (const alert of secretAlerts) { - alerts.secret_scanning.by_state[alert.state] = (alerts.secret_scanning.by_state[alert.state] || 0) + 1; - - alerts.secret_scanning.items.push({ - type: "secret_scanning", - number: alert.number, - url: alert.html_url, - state: alert.state, - secret_type: alert.secret_type, - created_at: alert.created_at, - updated_at: alert.updated_at, - repository: repoFullName, - }); - } - - core.info(` Secret scanning: ${secretAlerts.length} open alerts`); - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - core.warning(`Failed to fetch secret scanning alerts for ${repoFullName}: ${err.message}`); - } - - // Dependabot Alerts - try { - const response = await octokit.rest.dependabot.listAlertsForRepo({ - owner, - repo, - per_page: 100, - state: "open", - }); - - const dependabotAlerts = response.data; - alerts.dependabot.total += dependabotAlerts.length; - - for (const alert of dependabotAlerts) { - const severity = alert.security_advisory?.severity || "unknown"; - alerts.dependabot.by_severity[severity] = (alerts.dependabot.by_severity[severity] || 0) + 1; - alerts.dependabot.by_state[alert.state] = (alerts.dependabot.by_state[alert.state] || 0) + 1; - - alerts.dependabot.items.push({ - type: "dependabot", - number: alert.number, - url: alert.html_url, - state: alert.state, - severity: severity, - package_name: alert.security_advisory?.package?.name, - created_at: alert.created_at, - updated_at: alert.updated_at, - repository: repoFullName, - }); - } - - core.info(` Dependabot: ${dependabotAlerts.length} open alerts`); - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - core.warning(`Failed to fetch Dependabot alerts for ${repoFullName}: ${err.message}`); - } - } - - // Log summary - const totalAlerts = alerts.code_scanning.total + alerts.secret_scanning.total + alerts.dependabot.total; - core.info(`✓ Security alert discovery complete: ${totalAlerts} total alerts`); - core.info(` Code scanning: ${alerts.code_scanning.total}`); - core.info(` Secret scanning: ${alerts.secret_scanning.total}`); - core.info(` Dependabot: ${alerts.dependabot.total}`); - - return alerts; -} - /** * Main discovery function * @param {any} config - Configuration object @@ -478,13 +333,6 @@ async function discover(config) { const budgetExhausted = itemsBudgetExhausted || pagesBudgetExhausted; const exhaustedReason = budgetExhausted ? (itemsBudgetExhausted ? "max_items_reached" : "max_pages_reached") : null; - // Security alert discovery (for security-focused campaigns) - let securityAlerts = null; - if (campaignId.toLowerCase().includes("security")) { - core.info("Security-focused campaign detected - discovering security alerts..."); - securityAlerts = await discoverSecurityAlerts(octokit, repos); - } - // Build manifest const manifest = { schema_version: MANIFEST_VERSION, @@ -511,11 +359,6 @@ async function discover(config) { items: allItems, }; - // Add security alerts to manifest if discovered - if (securityAlerts) { - manifest.security_alerts = securityAlerts; - } - // Save cursor if provided if (cursorPath) { saveCursor(cursorPath, cursor); @@ -531,11 +374,6 @@ async function discover(config) { core.info(`Summary: ${needsAddCount} to add, ${needsUpdateCount} to update`); - if (securityAlerts) { - const totalSecurityAlerts = securityAlerts.code_scanning.total + securityAlerts.secret_scanning.total + securityAlerts.dependabot.total; - core.info(`Security alerts: ${totalSecurityAlerts} total (${securityAlerts.code_scanning.total} code scanning, ${securityAlerts.secret_scanning.total} secret scanning, ${securityAlerts.dependabot.total} dependabot)`); - } - return manifest; } @@ -615,7 +453,6 @@ async function main() { module.exports = { main, discover, - discoverSecurityAlerts, normalizeItem, searchByTrackerId, searchByLabel, diff --git a/actions/setup/js/campaign_discovery.test.cjs b/actions/setup/js/campaign_discovery.test.cjs index 9b1b12451a..68b524b0d6 100644 --- a/actions/setup/js/campaign_discovery.test.cjs +++ b/actions/setup/js/campaign_discovery.test.cjs @@ -1,6 +1,6 @@ // @ts-check import { describe, it, expect, beforeEach, vi } from "vitest"; -import { normalizeItem, loadCursor, saveCursor, searchByTrackerId, searchByLabel, searchItems, buildScopeParts, discover, discoverSecurityAlerts } from "./campaign_discovery.cjs"; +import { normalizeItem, loadCursor, saveCursor, searchByTrackerId, searchByLabel, searchItems, buildScopeParts, discover } from "./campaign_discovery.cjs"; import fs from "fs"; import path from "path"; @@ -952,195 +952,4 @@ describe("campaign_discovery", () => { expect(result.items).toHaveLength(50); }); }); - - describe("discoverSecurityAlerts", () => { - it("should discover code scanning alerts", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ - data: [ - { - number: 1, - html_url: "https://github.com/owner/repo/security/code-scanning/1", - state: "open", - rule: { - id: "go/unsafe-quoting", - severity: "critical", - description: "Unsafe quoting in code", - }, - created_at: "2025-01-01T00:00:00Z", - updated_at: "2025-01-02T00:00:00Z", - }, - { - number: 2, - html_url: "https://github.com/owner/repo/security/code-scanning/2", - state: "open", - rule: { - id: "js/xss", - severity: "high", - description: "Cross-site scripting vulnerability", - }, - created_at: "2025-01-01T00:00:00Z", - updated_at: "2025-01-02T00:00:00Z", - }, - ], - }), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); - - expect(result).not.toBeNull(); - expect(result.code_scanning.total).toBe(2); - expect(result.code_scanning.by_severity.critical).toBe(1); - expect(result.code_scanning.by_severity.high).toBe(1); - expect(result.code_scanning.items).toHaveLength(2); - expect(result.code_scanning.items[0]).toMatchObject({ - type: "code_scanning", - number: 1, - severity: "critical", - rule_id: "go/unsafe-quoting", - }); - }); - - it("should discover secret scanning alerts", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ - data: [ - { - number: 10, - html_url: "https://github.com/owner/repo/security/secret-scanning/10", - state: "open", - secret_type: "github_personal_access_token", - created_at: "2025-01-01T00:00:00Z", - updated_at: "2025-01-02T00:00:00Z", - }, - ], - }), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); - - expect(result).not.toBeNull(); - expect(result.secret_scanning.total).toBe(1); - expect(result.secret_scanning.items).toHaveLength(1); - expect(result.secret_scanning.items[0]).toMatchObject({ - type: "secret_scanning", - number: 10, - secret_type: "github_personal_access_token", - }); - }); - - it("should discover dependabot alerts", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockResolvedValue({ - data: [ - { - number: 20, - html_url: "https://github.com/owner/repo/security/dependabot/20", - state: "open", - security_advisory: { - severity: "high", - package: { name: "lodash" }, - }, - created_at: "2025-01-01T00:00:00Z", - updated_at: "2025-01-02T00:00:00Z", - }, - ], - }), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); - - expect(result).not.toBeNull(); - expect(result.dependabot.total).toBe(1); - expect(result.dependabot.items).toHaveLength(1); - expect(result.dependabot.items[0]).toMatchObject({ - type: "dependabot", - number: 20, - severity: "high", - package_name: "lodash", - }); - }); - - it("should handle API errors gracefully", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); - - // Should return empty results instead of throwing - expect(result).not.toBeNull(); - expect(result.code_scanning.total).toBe(0); - expect(result.secret_scanning.total).toBe(0); - expect(result.dependabot.total).toBe(0); - }); - - it("should return null if no repos specified", async () => { - const octokit = {}; - const result = await discoverSecurityAlerts(octokit, []); - expect(result).toBeNull(); - }); - - it("should handle invalid repo format", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["invalid-repo-format"]); - - // Should skip invalid repos but still return results - expect(result).not.toBeNull(); - expect(result.code_scanning.total).toBe(0); - }); - }); }); diff --git a/actions/setup/js/security_alert_discovery.cjs b/actions/setup/js/security_alert_discovery.cjs new file mode 100644 index 0000000000..fc42ca026b --- /dev/null +++ b/actions/setup/js/security_alert_discovery.cjs @@ -0,0 +1,234 @@ +// @ts-check +/// + +/** + * Security Alert Discovery + * + * Campaign-specific discovery script for security alerts. + * Discovers code scanning, secret scanning, and Dependabot alerts + * for repositories in scope. + * + * This is a specialized discovery script for security-focused campaigns + * and should be called as a custom pre-compute step in the campaign workflow. + * + * Outputs: + * - Manifest file: ./.gh-aw/security-alerts.json + * + * Environment variables: + * - GH_AW_DISCOVERY_REPOS: Comma-separated list of repos (owner/repo format) + */ + +const fs = require("fs"); +const path = require("path"); + +/** + * Discover security alerts for repositories + * @param {any} octokit - GitHub API client + * @param {string[]} repos - List of repositories to search (owner/repo format) + * @returns {Promise} Security alerts summary + */ +async function discoverSecurityAlerts(octokit, repos) { + if (!repos || repos.length === 0) { + core.warning("No repos specified for security alert discovery"); + return null; + } + + const alerts = { + code_scanning: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ ([]) }, + secret_scanning: { total: 0, by_state: {}, items: /** @type {any[]} */ ([]) }, + dependabot: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ ([]) }, + }; + + // Discover alerts for each repository + for (const repoFullName of repos) { + const [owner, repo] = repoFullName.split("/"); + if (!owner || !repo) { + core.warning(`Invalid repo format: ${repoFullName}. Expected owner/repo`); + continue; + } + + core.info(`Discovering security alerts for ${repoFullName}...`); + + // Code Scanning Alerts + try { + const response = await octokit.rest.codeScanning.listAlertsForRepo({ + owner, + repo, + per_page: 100, + state: "open", + }); + + const codeAlerts = response.data; + alerts.code_scanning.total += codeAlerts.length; + + for (const alert of codeAlerts) { + const severity = alert.rule?.severity || "unknown"; + alerts.code_scanning.by_severity[severity] = (alerts.code_scanning.by_severity[severity] || 0) + 1; + alerts.code_scanning.by_state[alert.state] = (alerts.code_scanning.by_state[alert.state] || 0) + 1; + + alerts.code_scanning.items.push({ + type: "code_scanning", + number: alert.number, + url: alert.html_url, + state: alert.state, + severity: severity, + rule_id: alert.rule?.id, + rule_description: alert.rule?.description, + created_at: alert.created_at, + updated_at: alert.updated_at, + repository: repoFullName, + }); + } + + core.info(` Code scanning: ${codeAlerts.length} open alerts`); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + core.warning(`Failed to fetch code scanning alerts for ${repoFullName}: ${err.message}`); + } + + // Secret Scanning Alerts + try { + const response = await octokit.rest.secretScanning.listAlertsForRepo({ + owner, + repo, + per_page: 100, + state: "open", + }); + + const secretAlerts = response.data; + alerts.secret_scanning.total += secretAlerts.length; + + for (const alert of secretAlerts) { + alerts.secret_scanning.by_state[alert.state] = (alerts.secret_scanning.by_state[alert.state] || 0) + 1; + + alerts.secret_scanning.items.push({ + type: "secret_scanning", + number: alert.number, + url: alert.html_url, + state: alert.state, + secret_type: alert.secret_type, + created_at: alert.created_at, + updated_at: alert.updated_at, + repository: repoFullName, + }); + } + + core.info(` Secret scanning: ${secretAlerts.length} open alerts`); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + core.warning(`Failed to fetch secret scanning alerts for ${repoFullName}: ${err.message}`); + } + + // Dependabot Alerts + try { + const response = await octokit.rest.dependabot.listAlertsForRepo({ + owner, + repo, + per_page: 100, + state: "open", + }); + + const dependabotAlerts = response.data; + alerts.dependabot.total += dependabotAlerts.length; + + for (const alert of dependabotAlerts) { + const severity = alert.security_advisory?.severity || "unknown"; + alerts.dependabot.by_severity[severity] = (alerts.dependabot.by_severity[severity] || 0) + 1; + alerts.dependabot.by_state[alert.state] = (alerts.dependabot.by_state[alert.state] || 0) + 1; + + alerts.dependabot.items.push({ + type: "dependabot", + number: alert.number, + url: alert.html_url, + state: alert.state, + severity: severity, + package_name: alert.security_advisory?.package?.name, + created_at: alert.created_at, + updated_at: alert.updated_at, + repository: repoFullName, + }); + } + + core.info(` Dependabot: ${dependabotAlerts.length} open alerts`); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + core.warning(`Failed to fetch Dependabot alerts for ${repoFullName}: ${err.message}`); + } + } + + // Log summary + const totalAlerts = alerts.code_scanning.total + alerts.secret_scanning.total + alerts.dependabot.total; + core.info(`✓ Security alert discovery complete: ${totalAlerts} total alerts`); + core.info(` Code scanning: ${alerts.code_scanning.total}`); + core.info(` Secret scanning: ${alerts.secret_scanning.total}`); + core.info(` Dependabot: ${alerts.dependabot.total}`); + + return alerts; +} + +/** + * Main entry point + */ +async function main() { + try { + // Read configuration from environment variables + const repos = (process.env.GH_AW_DISCOVERY_REPOS || "") + .split(",") + .map(r => r.trim()) + .filter(r => r.length > 0); + + if (!repos.length) { + throw new Error("GH_AW_DISCOVERY_REPOS environment variable is required"); + } + + core.info(`Starting security alert discovery for: ${repos.join(", ")}`); + + // Discover security alerts + const alerts = await discoverSecurityAlerts(github, repos); + + if (!alerts) { + throw new Error("Security alert discovery returned no results"); + } + + // Write manifest to output file + const outputDir = "./.gh-aw"; + const outputPath = path.join(outputDir, "security-alerts.json"); + + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const manifest = { + schema_version: "v1", + generated_at: new Date().toISOString(), + repos: repos, + alerts: alerts, + }; + + fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2)); + core.info(`Security alerts manifest written to ${outputPath}`); + + // Set output for GitHub Actions + core.setOutput("manifest-path", outputPath); + core.setOutput("total-alerts", alerts.code_scanning.total + alerts.secret_scanning.total + alerts.dependabot.total); + core.setOutput("code-scanning-total", alerts.code_scanning.total); + core.setOutput("secret-scanning-total", alerts.secret_scanning.total); + core.setOutput("dependabot-total", alerts.dependabot.total); + + // Log summary + core.info(`✓ Security alert discovery complete`); + core.info(` Total alerts: ${alerts.code_scanning.total + alerts.secret_scanning.total + alerts.dependabot.total}`); + core.info(` Code scanning: ${alerts.code_scanning.total}`); + core.info(` Secret scanning: ${alerts.secret_scanning.total}`); + core.info(` Dependabot: ${alerts.dependabot.total}`); + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + core.setFailed(`Security alert discovery failed: ${err.message}`); + throw err; + } +} + +module.exports = { + main, + discoverSecurityAlerts, +}; diff --git a/actions/setup/js/security_alert_discovery.test.cjs b/actions/setup/js/security_alert_discovery.test.cjs new file mode 100644 index 0000000000..93723dabe5 --- /dev/null +++ b/actions/setup/js/security_alert_discovery.test.cjs @@ -0,0 +1,245 @@ +// @ts-check +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { discoverSecurityAlerts } from "./security_alert_discovery.cjs"; + +// Mock core +global.core = { + info: vi.fn(), + warning: vi.fn(), + error: vi.fn(), + setFailed: vi.fn(), + getInput: vi.fn(), + setOutput: vi.fn(), +}; + +describe("security_alert_discovery", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe("discoverSecurityAlerts", () => { + it("should discover code scanning alerts", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ + data: [ + { + number: 1, + html_url: "https://github.com/owner/repo/security/code-scanning/1", + state: "open", + rule: { + id: "go/unsafe-quoting", + severity: "critical", + description: "Unsafe quoting in code", + }, + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-02T00:00:00Z", + }, + { + number: 2, + html_url: "https://github.com/owner/repo/security/code-scanning/2", + state: "open", + rule: { + id: "js/xss", + severity: "high", + description: "Cross-site scripting vulnerability", + }, + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-02T00:00:00Z", + }, + ], + }), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); + + expect(result).not.toBeNull(); + expect(result.code_scanning.total).toBe(2); + expect(result.code_scanning.by_severity.critical).toBe(1); + expect(result.code_scanning.by_severity.high).toBe(1); + expect(result.code_scanning.items).toHaveLength(2); + expect(result.code_scanning.items[0]).toMatchObject({ + type: "code_scanning", + number: 1, + severity: "critical", + rule_id: "go/unsafe-quoting", + }); + }); + + it("should discover secret scanning alerts", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ + data: [ + { + number: 10, + html_url: "https://github.com/owner/repo/security/secret-scanning/10", + state: "open", + secret_type: "github_personal_access_token", + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-02T00:00:00Z", + }, + ], + }), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); + + expect(result).not.toBeNull(); + expect(result.secret_scanning.total).toBe(1); + expect(result.secret_scanning.items).toHaveLength(1); + expect(result.secret_scanning.items[0]).toMatchObject({ + type: "secret_scanning", + number: 10, + secret_type: "github_personal_access_token", + }); + }); + + it("should discover dependabot alerts", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockResolvedValue({ + data: [ + { + number: 20, + html_url: "https://github.com/owner/repo/security/dependabot/20", + state: "open", + security_advisory: { + severity: "high", + package: { name: "lodash" }, + }, + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-02T00:00:00Z", + }, + ], + }), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); + + expect(result).not.toBeNull(); + expect(result.dependabot.total).toBe(1); + expect(result.dependabot.items).toHaveLength(1); + expect(result.dependabot.items[0]).toMatchObject({ + type: "dependabot", + number: 20, + severity: "high", + package_name: "lodash", + }); + }); + + it("should handle API errors gracefully", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); + + // Should return empty results instead of throwing + expect(result).not.toBeNull(); + expect(result.code_scanning.total).toBe(0); + expect(result.secret_scanning.total).toBe(0); + expect(result.dependabot.total).toBe(0); + }); + + it("should return null if no repos specified", async () => { + const octokit = {}; + const result = await discoverSecurityAlerts(octokit, []); + expect(result).toBeNull(); + }); + + it("should handle invalid repo format", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["invalid-repo-format"]); + + // Should skip invalid repos but still return results + expect(result).not.toBeNull(); + expect(result.code_scanning.total).toBe(0); + }); + + it("should aggregate alerts from multiple repos", async () => { + const octokit = { + rest: { + codeScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ + data: [ + { + number: 1, + html_url: "https://github.com/owner/repo/security/code-scanning/1", + state: "open", + rule: { id: "test", severity: "high", description: "Test" }, + created_at: "2025-01-01T00:00:00Z", + updated_at: "2025-01-02T00:00:00Z", + }, + ], + }), + }, + secretScanning: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + dependabot: { + listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), + }, + }, + }; + + const result = await discoverSecurityAlerts(octokit, ["owner/repo1", "owner/repo2"]); + + expect(result).not.toBeNull(); + // Should have called the API for each repo + expect(octokit.rest.codeScanning.listAlertsForRepo).toHaveBeenCalledTimes(2); + // Should aggregate totals from both repos + expect(result.code_scanning.total).toBe(2); + }); + }); +}); From 323ccef3b5d25b84263256f8b56bf78c004743d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 10:08:48 +0000 Subject: [PATCH 5/6] Revert security_alert_discovery script - use label-based discovery instead Remove security_alert_discovery.cjs and tests per architectural feedback. The correct approach is for workers to create labeled issues/PRs for each security alert, then the existing generic discovery finds them. Discovery should be based on worker labels, not hard-coded scripts. Each campaign has different needs - the generic label-based discovery provides the flexibility needed. Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- actions/setup/js/security_alert_discovery.cjs | 234 ----------------- .../js/security_alert_discovery.test.cjs | 245 ------------------ 2 files changed, 479 deletions(-) delete mode 100644 actions/setup/js/security_alert_discovery.cjs delete mode 100644 actions/setup/js/security_alert_discovery.test.cjs diff --git a/actions/setup/js/security_alert_discovery.cjs b/actions/setup/js/security_alert_discovery.cjs deleted file mode 100644 index fc42ca026b..0000000000 --- a/actions/setup/js/security_alert_discovery.cjs +++ /dev/null @@ -1,234 +0,0 @@ -// @ts-check -/// - -/** - * Security Alert Discovery - * - * Campaign-specific discovery script for security alerts. - * Discovers code scanning, secret scanning, and Dependabot alerts - * for repositories in scope. - * - * This is a specialized discovery script for security-focused campaigns - * and should be called as a custom pre-compute step in the campaign workflow. - * - * Outputs: - * - Manifest file: ./.gh-aw/security-alerts.json - * - * Environment variables: - * - GH_AW_DISCOVERY_REPOS: Comma-separated list of repos (owner/repo format) - */ - -const fs = require("fs"); -const path = require("path"); - -/** - * Discover security alerts for repositories - * @param {any} octokit - GitHub API client - * @param {string[]} repos - List of repositories to search (owner/repo format) - * @returns {Promise} Security alerts summary - */ -async function discoverSecurityAlerts(octokit, repos) { - if (!repos || repos.length === 0) { - core.warning("No repos specified for security alert discovery"); - return null; - } - - const alerts = { - code_scanning: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ ([]) }, - secret_scanning: { total: 0, by_state: {}, items: /** @type {any[]} */ ([]) }, - dependabot: { total: 0, by_severity: {}, by_state: {}, items: /** @type {any[]} */ ([]) }, - }; - - // Discover alerts for each repository - for (const repoFullName of repos) { - const [owner, repo] = repoFullName.split("/"); - if (!owner || !repo) { - core.warning(`Invalid repo format: ${repoFullName}. Expected owner/repo`); - continue; - } - - core.info(`Discovering security alerts for ${repoFullName}...`); - - // Code Scanning Alerts - try { - const response = await octokit.rest.codeScanning.listAlertsForRepo({ - owner, - repo, - per_page: 100, - state: "open", - }); - - const codeAlerts = response.data; - alerts.code_scanning.total += codeAlerts.length; - - for (const alert of codeAlerts) { - const severity = alert.rule?.severity || "unknown"; - alerts.code_scanning.by_severity[severity] = (alerts.code_scanning.by_severity[severity] || 0) + 1; - alerts.code_scanning.by_state[alert.state] = (alerts.code_scanning.by_state[alert.state] || 0) + 1; - - alerts.code_scanning.items.push({ - type: "code_scanning", - number: alert.number, - url: alert.html_url, - state: alert.state, - severity: severity, - rule_id: alert.rule?.id, - rule_description: alert.rule?.description, - created_at: alert.created_at, - updated_at: alert.updated_at, - repository: repoFullName, - }); - } - - core.info(` Code scanning: ${codeAlerts.length} open alerts`); - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - core.warning(`Failed to fetch code scanning alerts for ${repoFullName}: ${err.message}`); - } - - // Secret Scanning Alerts - try { - const response = await octokit.rest.secretScanning.listAlertsForRepo({ - owner, - repo, - per_page: 100, - state: "open", - }); - - const secretAlerts = response.data; - alerts.secret_scanning.total += secretAlerts.length; - - for (const alert of secretAlerts) { - alerts.secret_scanning.by_state[alert.state] = (alerts.secret_scanning.by_state[alert.state] || 0) + 1; - - alerts.secret_scanning.items.push({ - type: "secret_scanning", - number: alert.number, - url: alert.html_url, - state: alert.state, - secret_type: alert.secret_type, - created_at: alert.created_at, - updated_at: alert.updated_at, - repository: repoFullName, - }); - } - - core.info(` Secret scanning: ${secretAlerts.length} open alerts`); - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - core.warning(`Failed to fetch secret scanning alerts for ${repoFullName}: ${err.message}`); - } - - // Dependabot Alerts - try { - const response = await octokit.rest.dependabot.listAlertsForRepo({ - owner, - repo, - per_page: 100, - state: "open", - }); - - const dependabotAlerts = response.data; - alerts.dependabot.total += dependabotAlerts.length; - - for (const alert of dependabotAlerts) { - const severity = alert.security_advisory?.severity || "unknown"; - alerts.dependabot.by_severity[severity] = (alerts.dependabot.by_severity[severity] || 0) + 1; - alerts.dependabot.by_state[alert.state] = (alerts.dependabot.by_state[alert.state] || 0) + 1; - - alerts.dependabot.items.push({ - type: "dependabot", - number: alert.number, - url: alert.html_url, - state: alert.state, - severity: severity, - package_name: alert.security_advisory?.package?.name, - created_at: alert.created_at, - updated_at: alert.updated_at, - repository: repoFullName, - }); - } - - core.info(` Dependabot: ${dependabotAlerts.length} open alerts`); - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - core.warning(`Failed to fetch Dependabot alerts for ${repoFullName}: ${err.message}`); - } - } - - // Log summary - const totalAlerts = alerts.code_scanning.total + alerts.secret_scanning.total + alerts.dependabot.total; - core.info(`✓ Security alert discovery complete: ${totalAlerts} total alerts`); - core.info(` Code scanning: ${alerts.code_scanning.total}`); - core.info(` Secret scanning: ${alerts.secret_scanning.total}`); - core.info(` Dependabot: ${alerts.dependabot.total}`); - - return alerts; -} - -/** - * Main entry point - */ -async function main() { - try { - // Read configuration from environment variables - const repos = (process.env.GH_AW_DISCOVERY_REPOS || "") - .split(",") - .map(r => r.trim()) - .filter(r => r.length > 0); - - if (!repos.length) { - throw new Error("GH_AW_DISCOVERY_REPOS environment variable is required"); - } - - core.info(`Starting security alert discovery for: ${repos.join(", ")}`); - - // Discover security alerts - const alerts = await discoverSecurityAlerts(github, repos); - - if (!alerts) { - throw new Error("Security alert discovery returned no results"); - } - - // Write manifest to output file - const outputDir = "./.gh-aw"; - const outputPath = path.join(outputDir, "security-alerts.json"); - - if (!fs.existsSync(outputDir)) { - fs.mkdirSync(outputDir, { recursive: true }); - } - - const manifest = { - schema_version: "v1", - generated_at: new Date().toISOString(), - repos: repos, - alerts: alerts, - }; - - fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2)); - core.info(`Security alerts manifest written to ${outputPath}`); - - // Set output for GitHub Actions - core.setOutput("manifest-path", outputPath); - core.setOutput("total-alerts", alerts.code_scanning.total + alerts.secret_scanning.total + alerts.dependabot.total); - core.setOutput("code-scanning-total", alerts.code_scanning.total); - core.setOutput("secret-scanning-total", alerts.secret_scanning.total); - core.setOutput("dependabot-total", alerts.dependabot.total); - - // Log summary - core.info(`✓ Security alert discovery complete`); - core.info(` Total alerts: ${alerts.code_scanning.total + alerts.secret_scanning.total + alerts.dependabot.total}`); - core.info(` Code scanning: ${alerts.code_scanning.total}`); - core.info(` Secret scanning: ${alerts.secret_scanning.total}`); - core.info(` Dependabot: ${alerts.dependabot.total}`); - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - core.setFailed(`Security alert discovery failed: ${err.message}`); - throw err; - } -} - -module.exports = { - main, - discoverSecurityAlerts, -}; diff --git a/actions/setup/js/security_alert_discovery.test.cjs b/actions/setup/js/security_alert_discovery.test.cjs deleted file mode 100644 index 93723dabe5..0000000000 --- a/actions/setup/js/security_alert_discovery.test.cjs +++ /dev/null @@ -1,245 +0,0 @@ -// @ts-check -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { discoverSecurityAlerts } from "./security_alert_discovery.cjs"; - -// Mock core -global.core = { - info: vi.fn(), - warning: vi.fn(), - error: vi.fn(), - setFailed: vi.fn(), - getInput: vi.fn(), - setOutput: vi.fn(), -}; - -describe("security_alert_discovery", () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe("discoverSecurityAlerts", () => { - it("should discover code scanning alerts", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ - data: [ - { - number: 1, - html_url: "https://github.com/owner/repo/security/code-scanning/1", - state: "open", - rule: { - id: "go/unsafe-quoting", - severity: "critical", - description: "Unsafe quoting in code", - }, - created_at: "2025-01-01T00:00:00Z", - updated_at: "2025-01-02T00:00:00Z", - }, - { - number: 2, - html_url: "https://github.com/owner/repo/security/code-scanning/2", - state: "open", - rule: { - id: "js/xss", - severity: "high", - description: "Cross-site scripting vulnerability", - }, - created_at: "2025-01-01T00:00:00Z", - updated_at: "2025-01-02T00:00:00Z", - }, - ], - }), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); - - expect(result).not.toBeNull(); - expect(result.code_scanning.total).toBe(2); - expect(result.code_scanning.by_severity.critical).toBe(1); - expect(result.code_scanning.by_severity.high).toBe(1); - expect(result.code_scanning.items).toHaveLength(2); - expect(result.code_scanning.items[0]).toMatchObject({ - type: "code_scanning", - number: 1, - severity: "critical", - rule_id: "go/unsafe-quoting", - }); - }); - - it("should discover secret scanning alerts", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ - data: [ - { - number: 10, - html_url: "https://github.com/owner/repo/security/secret-scanning/10", - state: "open", - secret_type: "github_personal_access_token", - created_at: "2025-01-01T00:00:00Z", - updated_at: "2025-01-02T00:00:00Z", - }, - ], - }), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); - - expect(result).not.toBeNull(); - expect(result.secret_scanning.total).toBe(1); - expect(result.secret_scanning.items).toHaveLength(1); - expect(result.secret_scanning.items[0]).toMatchObject({ - type: "secret_scanning", - number: 10, - secret_type: "github_personal_access_token", - }); - }); - - it("should discover dependabot alerts", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockResolvedValue({ - data: [ - { - number: 20, - html_url: "https://github.com/owner/repo/security/dependabot/20", - state: "open", - security_advisory: { - severity: "high", - package: { name: "lodash" }, - }, - created_at: "2025-01-01T00:00:00Z", - updated_at: "2025-01-02T00:00:00Z", - }, - ], - }), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); - - expect(result).not.toBeNull(); - expect(result.dependabot.total).toBe(1); - expect(result.dependabot.items).toHaveLength(1); - expect(result.dependabot.items[0]).toMatchObject({ - type: "dependabot", - number: 20, - severity: "high", - package_name: "lodash", - }); - }); - - it("should handle API errors gracefully", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockRejectedValue(new Error("API error")), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["owner/repo"]); - - // Should return empty results instead of throwing - expect(result).not.toBeNull(); - expect(result.code_scanning.total).toBe(0); - expect(result.secret_scanning.total).toBe(0); - expect(result.dependabot.total).toBe(0); - }); - - it("should return null if no repos specified", async () => { - const octokit = {}; - const result = await discoverSecurityAlerts(octokit, []); - expect(result).toBeNull(); - }); - - it("should handle invalid repo format", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["invalid-repo-format"]); - - // Should skip invalid repos but still return results - expect(result).not.toBeNull(); - expect(result.code_scanning.total).toBe(0); - }); - - it("should aggregate alerts from multiple repos", async () => { - const octokit = { - rest: { - codeScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ - data: [ - { - number: 1, - html_url: "https://github.com/owner/repo/security/code-scanning/1", - state: "open", - rule: { id: "test", severity: "high", description: "Test" }, - created_at: "2025-01-01T00:00:00Z", - updated_at: "2025-01-02T00:00:00Z", - }, - ], - }), - }, - secretScanning: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - dependabot: { - listAlertsForRepo: vi.fn().mockResolvedValue({ data: [] }), - }, - }, - }; - - const result = await discoverSecurityAlerts(octokit, ["owner/repo1", "owner/repo2"]); - - expect(result).not.toBeNull(); - // Should have called the API for each repo - expect(octokit.rest.codeScanning.listAlertsForRepo).toHaveBeenCalledTimes(2); - // Should aggregate totals from both repos - expect(result.code_scanning.total).toBe(2); - }); - }); -}); From dab23e157b86b018d73062447c8a62182c121e9f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:01:25 +0000 Subject: [PATCH 6/6] Relax code-scanning-fixer to discover all severity levels Update code-scanning-fixer workflow to handle all security alert severities instead of only critical/high. This enables the worker to create PRs for all 22 security alerts, not just the highest severity subset. Changes: - Remove severity filtering - list ALL open alerts - Prioritize by severity (critical > high > medium > low > warning > note > error) - Update description, mission, and instructions - Update exit messages to reflect all severities - Recompiled workflow lock file Co-authored-by: mnkiefer <8320933+mnkiefer@users.noreply.github.com> --- .../workflows/code-scanning-fixer.lock.yml | 43 ++++++++----------- .github/workflows/code-scanning-fixer.md | 41 ++++++++---------- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/.github/workflows/code-scanning-fixer.lock.yml b/.github/workflows/code-scanning-fixer.lock.yml index 4f01177da0..8a224e2e38 100644 --- a/.github/workflows/code-scanning-fixer.lock.yml +++ b/.github/workflows/code-scanning-fixer.lock.yml @@ -19,7 +19,7 @@ # gh aw compile # For more information: https://github.com/githubnext/gh-aw/blob/main/.github/aw/github-agentic-workflows.md # -# Automatically fixes critical and high severity code scanning alerts by creating pull requests with remediation +# Automatically fixes code scanning alerts by creating pull requests with remediation name: "Code Scanning Fixer" "on": @@ -618,7 +618,7 @@ jobs: cat << 'PROMPT_EOF' >> "$GH_AW_PROMPT" # Code Scanning Alert Fixer Agent - You are a security-focused code analysis agent that automatically fixes critical and high severity code scanning alerts. + You are a security-focused code analysis agent that automatically fixes code scanning alerts of all severity levels. ## Important Guidelines @@ -637,8 +637,8 @@ jobs: Your goal is to: 1. **Check cache for previously fixed alerts**: Avoid fixing the same alert multiple times - 2. **List critical and high severity alerts**: Find all open code scanning alerts with critical or high severity - 3. **Select an unfixed alert**: Pick the first critical or high severity alert that hasn't been fixed recently + 2. **List all open alerts**: Find all open code scanning alerts (prioritizing by severity: critical, high, medium, low, warning, note, error) + 3. **Select an unfixed alert**: Pick the highest severity unfixed alert that hasn't been fixed recently 4. **Analyze the vulnerability**: Understand the security issue and its context 5. **Generate a fix**: Create code changes that address the security issue 6. **Create Pull Request**: Submit a pull request with the fix @@ -654,30 +654,25 @@ jobs: - If the file doesn't exist, treat it as empty (no alerts fixed yet) - Build a set of alert numbers that have been fixed to avoid re-fixing them - ### 2. List Critical and High Severity Alerts + ### 2. List All Open Alerts - Use the GitHub MCP server to list all open code scanning alerts with critical or high severity: - - First, call `github-list_code_scanning_alerts` tool with the following parameters for critical alerts: + Use the GitHub MCP server to list all open code scanning alerts: + - Call `github-list_code_scanning_alerts` tool with the following parameters: - `owner`: "githubnext" (the repository owner) - `repo`: "gh-aw" (the repository name) - - `state`: "open" - - `severity`: "critical" - - Then, call `github-list_code_scanning_alerts` tool again with the following parameters for high alerts: - - `owner`: "githubnext" (the repository owner) - - `repo`: "gh-aw" (the repository name) - - `state`: "open" - - `severity`: "high" - - Combine the results from both calls, prioritizing critical alerts over high severity alerts - - If no critical or high severity alerts are found, log "No unfixed critical or high severity alerts found" and exit gracefully + - `state`: "open" + - Do NOT filter by severity - get all alerts + - Sort the results by severity (prioritize: critical > high > medium > low > warning > note > error) + - If no open alerts are found, log "No unfixed security alerts found. All alerts have been addressed!" and exit gracefully - If you encounter tool errors, report them clearly and exit gracefully rather than trying workarounds - - Create a list of alert numbers from the results + - Create a list of alert numbers from the results, sorted by severity (highest first) ### 3. Select an Unfixed Alert - From the list of critical and high severity alerts: + From the list of all open alerts (sorted by severity): - Exclude any alert numbers that are in the cache (already fixed) - - Select the first alert from the filtered list (critical alerts are prioritized) - - If no unfixed critical or high severity alerts remain, exit gracefully with message: "No unfixed critical or high severity alerts found. All critical and high severity issues have been addressed!" + - Select the first alert from the filtered list (highest severity unfixed alert) + - If no unfixed alerts remain, exit gracefully with message: "No unfixed security alerts found. All alerts have been addressed!" ### 4. Get Alert Details @@ -688,7 +683,7 @@ jobs: - `alertNumber`: The alert number from step 3 - Extract key information: - Alert number - - Severity level (should be "critical" or "high") + - Severity level (critical, high, medium, low, warning, note, or error) - Rule ID and description - File path and line number - Vulnerable code snippet @@ -771,7 +766,7 @@ jobs: ## Security Guidelines - - **Critical and High Severity Only**: Only fix critical and high severity alerts as specified in the requirements + - **All Severity Levels**: Fix security alerts of all severities (prioritizing critical, high, medium, low, warning, note, error in that order) - **Minimal Changes**: Make only the changes necessary to fix the security issue - **No Breaking Changes**: Ensure the fix doesn't break existing functionality - **Best Practices**: Follow security best practices for the specific vulnerability type @@ -792,7 +787,7 @@ jobs: ## Error Handling If any step fails: - - **No Critical or High Severity Alerts**: Log "No critical or high severity alerts found" and exit gracefully + - **No Open Alerts**: Log "No unfixed security alerts found. All alerts have been addressed!" and exit gracefully - **All Alerts Already Fixed**: Log success message and exit gracefully - **Read Error**: Report the error and exit - **Fix Generation Failed**: Document why the fix couldn't be automated and exit @@ -1193,7 +1188,7 @@ jobs: uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 env: WORKFLOW_NAME: "Code Scanning Fixer" - WORKFLOW_DESCRIPTION: "Automatically fixes critical and high severity code scanning alerts by creating pull requests with remediation" + WORKFLOW_DESCRIPTION: "Automatically fixes code scanning alerts by creating pull requests with remediation" HAS_PATCH: ${{ needs.agent.outputs.has_patch }} with: script: | diff --git a/.github/workflows/code-scanning-fixer.md b/.github/workflows/code-scanning-fixer.md index a181f8652c..f45a790395 100644 --- a/.github/workflows/code-scanning-fixer.md +++ b/.github/workflows/code-scanning-fixer.md @@ -1,6 +1,6 @@ --- name: Code Scanning Fixer -description: Automatically fixes critical and high severity code scanning alerts by creating pull requests with remediation +description: Automatically fixes code scanning alerts by creating pull requests with remediation on: workflow_dispatch: skip-if-match: 'is:pr is:open in:title "[code-scanning-fix]"' @@ -34,7 +34,7 @@ timeout-minutes: 20 # Code Scanning Alert Fixer Agent -You are a security-focused code analysis agent that automatically fixes critical and high severity code scanning alerts. +You are a security-focused code analysis agent that automatically fixes code scanning alerts of all severity levels. ## Important Guidelines @@ -53,8 +53,8 @@ You are a security-focused code analysis agent that automatically fixes critical Your goal is to: 1. **Check cache for previously fixed alerts**: Avoid fixing the same alert multiple times -2. **List critical and high severity alerts**: Find all open code scanning alerts with critical or high severity -3. **Select an unfixed alert**: Pick the first critical or high severity alert that hasn't been fixed recently +2. **List all open alerts**: Find all open code scanning alerts (prioritizing by severity: critical, high, medium, low, warning, note, error) +3. **Select an unfixed alert**: Pick the highest severity unfixed alert that hasn't been fixed recently 4. **Analyze the vulnerability**: Understand the security issue and its context 5. **Generate a fix**: Create code changes that address the security issue 6. **Create Pull Request**: Submit a pull request with the fix @@ -70,30 +70,25 @@ Before selecting an alert, check the cache memory to see which alerts have been - If the file doesn't exist, treat it as empty (no alerts fixed yet) - Build a set of alert numbers that have been fixed to avoid re-fixing them -### 2. List Critical and High Severity Alerts +### 2. List All Open Alerts -Use the GitHub MCP server to list all open code scanning alerts with critical or high severity: -- First, call `github-list_code_scanning_alerts` tool with the following parameters for critical alerts: +Use the GitHub MCP server to list all open code scanning alerts: +- Call `github-list_code_scanning_alerts` tool with the following parameters: - `owner`: "githubnext" (the repository owner) - `repo`: "gh-aw" (the repository name) - - `state`: "open" - - `severity`: "critical" -- Then, call `github-list_code_scanning_alerts` tool again with the following parameters for high alerts: - - `owner`: "githubnext" (the repository owner) - - `repo`: "gh-aw" (the repository name) - - `state`: "open" - - `severity`: "high" -- Combine the results from both calls, prioritizing critical alerts over high severity alerts -- If no critical or high severity alerts are found, log "No unfixed critical or high severity alerts found" and exit gracefully + - `state`: "open" + - Do NOT filter by severity - get all alerts +- Sort the results by severity (prioritize: critical > high > medium > low > warning > note > error) +- If no open alerts are found, log "No unfixed security alerts found. All alerts have been addressed!" and exit gracefully - If you encounter tool errors, report them clearly and exit gracefully rather than trying workarounds -- Create a list of alert numbers from the results +- Create a list of alert numbers from the results, sorted by severity (highest first) ### 3. Select an Unfixed Alert -From the list of critical and high severity alerts: +From the list of all open alerts (sorted by severity): - Exclude any alert numbers that are in the cache (already fixed) -- Select the first alert from the filtered list (critical alerts are prioritized) -- If no unfixed critical or high severity alerts remain, exit gracefully with message: "No unfixed critical or high severity alerts found. All critical and high severity issues have been addressed!" +- Select the first alert from the filtered list (highest severity unfixed alert) +- If no unfixed alerts remain, exit gracefully with message: "No unfixed security alerts found. All alerts have been addressed!" ### 4. Get Alert Details @@ -104,7 +99,7 @@ Get detailed information about the selected alert using `github-get_code_scannin - `alertNumber`: The alert number from step 3 - Extract key information: - Alert number - - Severity level (should be "critical" or "high") + - Severity level (critical, high, medium, low, warning, note, or error) - Rule ID and description - File path and line number - Vulnerable code snippet @@ -187,7 +182,7 @@ After successfully creating the pull request: ## Security Guidelines -- **Critical and High Severity Only**: Only fix critical and high severity alerts as specified in the requirements +- **All Severity Levels**: Fix security alerts of all severities (prioritizing critical, high, medium, low, warning, note, error in that order) - **Minimal Changes**: Make only the changes necessary to fix the security issue - **No Breaking Changes**: Ensure the fix doesn't break existing functionality - **Best Practices**: Follow security best practices for the specific vulnerability type @@ -208,7 +203,7 @@ Each line is a separate JSON object representing one fixed alert. ## Error Handling If any step fails: -- **No Critical or High Severity Alerts**: Log "No critical or high severity alerts found" and exit gracefully +- **No Open Alerts**: Log "No unfixed security alerts found. All alerts have been addressed!" and exit gracefully - **All Alerts Already Fixed**: Log success message and exit gracefully - **Read Error**: Report the error and exit - **Fix Generation Failed**: Document why the fix couldn't be automated and exit