Add patched version column to vulnerability summary with multi-range support (#5)

* Initial plan

* Initial plan for adding patched versions to vulnerability summary

Co-authored-by: felickz <1760475+felickz@users.noreply.github.com>

* Add patched version column to vulnerability summary table

Co-authored-by: felickz <1760475+felickz@users.noreply.github.com>

* Optimize API calls to use Set and Promise.all for better performance

Co-authored-by: felickz <1760475+felickz@users.noreply.github.com>

* Remove type assertions and optimize ecosystem lookups with normalization

Co-authored-by: felickz <1760475+felickz@users.noreply.github.com>

* Extract patch version type checking into helper function for clarity

Co-authored-by: felickz <1760475+felickz@users.noreply.github.com>

* Support multiple version ranges per package in advisory lookup

Handle scenarios where the same package has multiple vulnerable version ranges
with different patched versions (e.g., GHSA-gwq6-fmvp-qp68 with .NET packages).

- Store all vulnerability entries with version ranges, not just one per ecosystem
- Implement version range matching to select correct patch version
- Match package by ecosystem, name, AND version range
- Add comprehensive test for multi-range scenario

Co-authored-by: felickz <1760475+felickz@users.noreply.github.com>

* Fix first_patched_version extraction to handle string format

The GitHub Advisory API returns first_patched_version as a simple string
(e.g., "112.0.0"), not an object with an identifier field. Updated
extractPatchVersionId to handle string format primarily while maintaining
backward compatibility with object format.

- Fixed extractPatchVersionId to check for string first
- Updated test mocks to match actual API response format
- Added specific test for RestSharp GHSA-4rr6-2v9v-wcpc case
- Verified with actual API responses from multiple advisories

Fixes issue where patched versions were showing as "N/A" instead of
the actual version number.

Co-authored-by: felickz <1760475+felickz@users.noreply.github.com>

* Rebuild dist with patched version fix

The previous fix to extractPatchVersionId wasn't deployed because the dist/
folder (compiled JavaScript) wasn't rebuilt. This commit rebuilds and packages
the action with npm run build && npm run package to include the fix.

Changes in dist/:
- Updated extractPatchVersionId to handle string format first
- Includes all async vulnerability summary logic with API calls
- Properly extracts patched versions from GitHub Advisory API

This should resolve the issue where patched versions showed as "N/A" in
actual GitHub Actions runs.

Co-authored-by: felickz <1760475+felickz@users.noreply.github.com>

* Add comprehensive debug logging for patch version lookup

Added detailed debug logging to help troubleshoot patch version issues:
- Log when fetching advisory data from API
- Log number of vulnerability entries found
- Log each patch info entry added with details
- Log when no patch version is found
- Log during lookup phase with package details
- Log when patch version is found vs not found
- Log available entries when no match is found

This will make it much easier to diagnose issues in GitHub Actions debug mode.

Co-authored-by: felickz <1760475+felickz@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: felickz <1760475+felickz@users.noreply.github.com>
This commit is contained in:
Copilot
2026-02-06 18:12:20 -05:00
committed by GitHub
parent 98884d411b
commit 2af9bac14d
7 changed files with 911 additions and 68 deletions
+144 -13
View File
@@ -1,9 +1,22 @@
import {expect, jest, test} from '@jest/globals'
import {expect, jest, test, beforeEach} from '@jest/globals'
import {Changes, ConfigurationOptions, Scorecard} from '../src/schemas'
import * as summary from '../src/summary'
import * as core from '@actions/core'
import {createTestChange} from './fixtures/create-test-change'
import {createTestVulnerability} from './fixtures/create-test-vulnerability'
import * as utils from '../src/utils'
const mockOctokitRequest = jest.fn<any>()
beforeEach(() => {
jest.spyOn(utils, 'octokitClient').mockReturnValue({
request: mockOctokitRequest
} as any)
mockOctokitRequest.mockResolvedValue({
data: {vulnerabilities: []}
})
})
afterEach(() => {
jest.clearAllMocks()
@@ -315,19 +328,19 @@ test('uses checkmarks for vulnerabilities if only license issues were found', ()
expect(text).toContain('✅ 0 package(s) with unknown licenses')
})
test('addChangeVulnerabilitiesToSummary() - only includes section if any vulnerabilities found', () => {
summary.addChangeVulnerabilitiesToSummary(emptyChanges, 'low')
test('addChangeVulnerabilitiesToSummary() - only includes section if any vulnerabilities found', async () => {
await summary.addChangeVulnerabilitiesToSummary(emptyChanges, 'low')
const text = core.summary.stringify()
expect(text).toEqual('')
})
test('addChangeVulnerabilitiesToSummary() - includes all vulnerabilities', () => {
test('addChangeVulnerabilitiesToSummary() - includes all vulnerabilities', async () => {
const changes = [
createTestChange({name: 'lodash'}),
createTestChange({name: 'underscore', package_url: 'test-url'})
]
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
expect(text).toContain('<h2>Vulnerabilities</h2>')
@@ -335,7 +348,7 @@ test('addChangeVulnerabilitiesToSummary() - includes all vulnerabilities', () =>
expect(text).toContain('underscore')
})
test('addChangeVulnerabilitiesToSummary() - includes advisory url if available', () => {
test('addChangeVulnerabilitiesToSummary() - includes advisory url if available', async () => {
const changes = [
createTestChange({
name: 'underscore',
@@ -348,14 +361,14 @@ test('addChangeVulnerabilitiesToSummary() - includes advisory url if available',
})
]
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
expect(text).toContain('lodash')
expect(text).toContain('<a href="test-url">test-summary</a>')
})
test('addChangeVulnerabilitiesToSummary() - groups vulnerabilities of a single package', () => {
test('addChangeVulnerabilitiesToSummary() - groups vulnerabilities of a single package', async () => {
const changes = [
createTestChange({
name: 'package-with-multiple-vulnerabilities',
@@ -366,7 +379,7 @@ test('addChangeVulnerabilitiesToSummary() - groups vulnerabilities of a single p
})
]
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
expect(text.match('package-with-multiple-vulnerabilities')).toHaveLength(1)
@@ -374,10 +387,10 @@ test('addChangeVulnerabilitiesToSummary() - groups vulnerabilities of a single p
expect(text).toContain('test-summary-2')
})
test('addChangeVulnerabilitiesToSummary() - prints severity statement if above low', () => {
test('addChangeVulnerabilitiesToSummary() - prints severity statement if above low', async () => {
const changes = [createTestChange()]
summary.addChangeVulnerabilitiesToSummary(changes, 'medium')
await summary.addChangeVulnerabilitiesToSummary(changes, 'medium')
const text = core.summary.stringify()
expect(text).toContain(
@@ -385,15 +398,24 @@ test('addChangeVulnerabilitiesToSummary() - prints severity statement if above l
)
})
test('addChangeVulnerabilitiesToSummary() - does not print severity statement if it is set to "low"', () => {
test('addChangeVulnerabilitiesToSummary() - does not print severity statement if it is set to "low"', async () => {
const changes = [createTestChange()]
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
expect(text).not.toContain('Only included vulnerabilities')
})
test('addChangeVulnerabilitiesToSummary() - includes patched version column', async () => {
const changes = [createTestChange()]
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
expect(text).toContain('Patched Version')
})
test('addLicensesToSummary() - does not include entire section if no license issues found', () => {
summary.addLicensesToSummary(emptyInvalidLicenseChanges, defaultConfig)
const text = core.summary.stringify()
@@ -508,3 +530,112 @@ test('addLicensesToSummary() - includes allowed dependency licences', () => {
'<details><summary><strong>Excluded from license check</strong>:</summary> MIT, Apache-2.0</details>'
)
})
test('addChangeVulnerabilitiesToSummary() - handles multiple version ranges for same package', async () => {
// Simulates GHSA-gwq6-fmvp-qp68 scenario with multiple version ranges
const pkg8 = createTestChange({
ecosystem: 'nuget',
name: 'Microsoft.NetCore.App.Runtime.linux-arm',
version: '8.0.1',
vulnerabilities: [
createTestVulnerability({
advisory_ghsa_id: 'GHSA-test-multi',
advisory_summary: 'Test Multi-Range Advisory',
severity: 'high'
})
]
})
const pkg9 = createTestChange({
ecosystem: 'nuget',
name: 'Microsoft.NetCore.App.Runtime.linux-arm',
version: '9.0.1',
vulnerabilities: [
createTestVulnerability({
advisory_ghsa_id: 'GHSA-test-multi',
advisory_summary: 'Test Multi-Range Advisory',
severity: 'high'
})
]
})
// Mock API response with multiple version ranges for same package
mockOctokitRequest.mockResolvedValueOnce({
data: {
vulnerabilities: [
{
package: {
ecosystem: 'NuGet',
name: 'Microsoft.NetCore.App.Runtime.linux-arm'
},
vulnerable_version_range: '>= 8.0.0, <= 8.0.20',
first_patched_version: '8.0.21'
},
{
package: {
ecosystem: 'NuGet',
name: 'Microsoft.NetCore.App.Runtime.linux-arm'
},
vulnerable_version_range: '>= 9.0.0, <= 9.0.9',
first_patched_version: '9.0.10'
}
]
}
})
const changes = [pkg8, pkg9]
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
// Both packages should have correct patched versions based on their version ranges
expect(text).toContain('8.0.21')
expect(text).toContain('9.0.10')
expect(mockOctokitRequest).toHaveBeenCalledWith('GET /advisories/{ghsa_id}', {
ghsa_id: 'GHSA-test-multi'
})
})
test('addChangeVulnerabilitiesToSummary() - handles RestSharp GHSA-4rr6-2v9v-wcpc case', async () => {
const pkg = createTestChange({
ecosystem: 'nuget',
name: 'RestSharp',
version: '111.4.1',
vulnerabilities: [
createTestVulnerability({
advisory_ghsa_id: 'GHSA-4rr6-2v9v-wcpc',
advisory_summary:
"CRLF Injection in RestSharp's `RestRequest.AddHeader` method",
severity: 'moderate'
})
]
})
// Mock API response matching actual GitHub Advisory Database response
mockOctokitRequest.mockResolvedValueOnce({
data: {
vulnerabilities: [
{
package: {
ecosystem: 'nuget',
name: 'RestSharp'
},
vulnerable_version_range: '>= 107.0.0-preview.1, < 112.0.0',
first_patched_version: '112.0.0'
}
]
}
})
const changes = [pkg]
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
// Should show the correct patched version
expect(text).toContain('112.0.0')
expect(text).not.toContain('N/A')
expect(mockOctokitRequest).toHaveBeenCalledWith('GET /advisories/{ghsa_id}', {
ghsa_id: 'GHSA-4rr6-2v9v-wcpc'
})
})
Generated Vendored
+179 -44
View File
@@ -779,7 +779,7 @@ function run() {
let issueFound = false;
if (config.vulnerability_check) {
core.setOutput('vulnerable-changes', JSON.stringify(vulnerableChanges));
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity);
yield summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity);
issueFound || (issueFound = yield printVulnerabilitiesBlock(vulnerableChanges, minSeverity, warnOnly));
}
if (config.license_check) {
@@ -1628,6 +1628,15 @@ var __importStar = (this && this.__importStar) || (function () {
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.addSummaryToSummary = addSummaryToSummary;
exports.addChangeVulnerabilitiesToSummary = addChangeVulnerabilitiesToSummary;
@@ -1644,6 +1653,60 @@ const icons = {
warning: '⚠️'
};
const MAX_SCANNED_FILES_BYTES = 1048576;
// Helper to check if a version falls within a vulnerable range
// Supports basic semver comparisons like ">= 8.0.0, <= 8.0.20"
function versionInRange(version, range) {
if (!version || !range)
return false;
// Parse version into comparable parts
const vParts = version.split('.').map(p => parseInt(p, 10));
// Handle range formats like ">= 8.0.0, <= 8.0.20"
const conditions = range.split(',').map(c => c.trim());
for (const condition of conditions) {
const match = condition.match(/([><=]+)\s*(\d+(?:\.\d+)*)/);
if (!match)
continue;
const [, operator, rangeVer] = match;
const rParts = rangeVer.split('.').map(p => parseInt(p, 10));
// Compare versions part by part
let cmp = 0;
for (let i = 0; i < Math.max(vParts.length, rParts.length); i++) {
const v = vParts[i] || 0;
const r = rParts[i] || 0;
if (v > r) {
cmp = 1;
break;
}
else if (v < r) {
cmp = -1;
break;
}
}
// Check if condition is satisfied
if (operator === '>=' && cmp < 0)
return false;
if (operator === '>' && cmp <= 0)
return false;
if (operator === '<=' && cmp > 0)
return false;
if (operator === '<' && cmp >= 0)
return false;
if (operator === '=' && cmp !== 0)
return false;
}
return true;
}
function extractPatchVersionId(patchData) {
// Handle string format (current API response)
if (typeof patchData === 'string')
return patchData;
// Handle object format with identifier field (for backward compatibility)
if (patchData && typeof patchData === 'object' && 'identifier' in patchData) {
const id = patchData.identifier;
return typeof id === 'string' ? id : null;
}
return null;
}
// generates the DR report summary and caches it to the Action's core.summary.
// returns the DR summary string, ready to be posted as a PR comment if the
// final DR report is too large
@@ -1725,51 +1788,123 @@ function countScorecardWarnings(scorecard, config) {
}, 0);
}
function addChangeVulnerabilitiesToSummary(vulnerableChanges, severity) {
if (vulnerableChanges.length === 0) {
return;
}
const rows = [];
const manifests = (0, utils_1.getManifestsSet)(vulnerableChanges);
core.summary.addHeading('Vulnerabilities', 2);
for (const manifest of manifests) {
for (const change of vulnerableChanges.filter(pkg => pkg.manifest === manifest)) {
let previous_package = '';
let previous_version = '';
for (const vuln of change.vulnerabilities) {
const sameAsPrevious = previous_package === change.name &&
previous_version === change.version;
if (!sameAsPrevious) {
rows.push([
(0, utils_1.renderUrl)(change.source_repository_url, change.name),
change.version,
(0, utils_1.renderUrl)(vuln.advisory_url, vuln.advisory_summary),
vuln.severity
]);
}
else {
rows.push([
{ data: '', colspan: '2' },
(0, utils_1.renderUrl)(vuln.advisory_url, vuln.advisory_summary),
vuln.severity
]);
}
previous_package = change.name;
previous_version = change.version;
return __awaiter(this, void 0, void 0, function* () {
if (vulnerableChanges.length === 0) {
return;
}
const rows = [];
const manifests = (0, utils_1.getManifestsSet)(vulnerableChanges);
// Build set of unique advisories to query
const advisorySet = new Set();
for (const pkg of vulnerableChanges) {
for (const vuln of pkg.vulnerabilities) {
advisorySet.add(vuln.advisory_ghsa_id);
}
}
core.summary.addHeading(`<em>${manifest}</em>`, 4).addTable([
[
{ data: 'Name', header: true },
{ data: 'Version', header: true },
{ data: 'Vulnerability', header: true },
{ data: 'Severity', header: true }
],
...rows
]);
}
if (severity !== 'low') {
core.summary.addQuote(`Only included vulnerabilities with severity <strong>${severity}</strong> or higher.`);
}
// Query GitHub API for patch info in parallel
// Store all vulnerability entries (may be multiple per package with different ranges)
const patchInfo = {};
const apiClient = (0, utils_1.octokitClient)();
yield Promise.all(Array.from(advisorySet).map((advId) => __awaiter(this, void 0, void 0, function* () {
try {
core.debug(`Fetching advisory data for ${advId}`);
const apiResult = yield apiClient.request('GET /advisories/{ghsa_id}', {
ghsa_id: advId
});
patchInfo[advId] = [];
const vulnList = apiResult.data.vulnerabilities || [];
core.debug(`Found ${vulnList.length} vulnerability entries for ${advId}`);
for (const v of vulnList) {
if (v.package && v.package.ecosystem) {
const normalizedEco = v.package.ecosystem.toLowerCase();
const pkgName = v.package.name || '';
const vulnRange = v.vulnerable_version_range || '';
const patchVerId = extractPatchVersionId(v.first_patched_version);
if (patchVerId) {
patchInfo[advId].push({
eco: normalizedEco,
pkg: pkgName,
range: vulnRange,
patch: patchVerId
});
core.debug(`Added patch info for ${pkgName} (${normalizedEco}): ${patchVerId} for range ${vulnRange}`);
}
else {
core.debug(`No patch version found for ${pkgName} (${normalizedEco}) in ${advId}`);
}
}
}
}
catch (e) {
core.debug(`API call failed for ${advId}: ${e}`);
patchInfo[advId] = [];
}
})));
core.summary.addHeading('Vulnerabilities', 2);
for (const manifest of manifests) {
for (const change of vulnerableChanges.filter(pkg => pkg.manifest === manifest)) {
let previous_package = '';
let previous_version = '';
for (const vuln of change.vulnerabilities) {
const sameAsPrevious = previous_package === change.name &&
previous_version === change.version;
// Look up patch version by matching package name, ecosystem, and version range
let patchVer = 'N/A';
const advData = patchInfo[vuln.advisory_ghsa_id];
if (advData && advData.length > 0) {
const normalizedEco = change.ecosystem.toLowerCase();
core.debug(`Looking up patch for ${change.name}@${change.version} (${normalizedEco}) in ${vuln.advisory_ghsa_id}`);
// Find matching entry by ecosystem, package name, and version range
const matchingEntry = advData.find(entry => entry.eco === normalizedEco &&
entry.pkg === change.name &&
versionInRange(change.version, entry.range));
if (matchingEntry) {
patchVer = matchingEntry.patch;
core.debug(`Found patch version ${patchVer} for ${change.name}@${change.version}`);
}
else {
core.debug(`No matching patch found for ${change.name}@${change.version}. Available entries: ${JSON.stringify(advData)}`);
}
}
else {
core.debug(`No advisory data available for ${vuln.advisory_ghsa_id}`);
}
if (!sameAsPrevious) {
rows.push([
(0, utils_1.renderUrl)(change.source_repository_url, change.name),
change.version,
(0, utils_1.renderUrl)(vuln.advisory_url, vuln.advisory_summary),
vuln.severity,
patchVer
]);
}
else {
rows.push([
{ data: '', colspan: '2' },
(0, utils_1.renderUrl)(vuln.advisory_url, vuln.advisory_summary),
vuln.severity,
patchVer
]);
}
previous_package = change.name;
previous_version = change.version;
}
}
core.summary.addHeading(`<em>${manifest}</em>`, 4).addTable([
[
{ data: 'Name', header: true },
{ data: 'Version', header: true },
{ data: 'Vulnerability', header: true },
{ data: 'Severity', header: true },
{ data: 'Patched Version', header: true }
],
...rows
]);
}
if (severity !== 'low') {
core.summary.addQuote(`Only included vulnerabilities with severity <strong>${severity}</strong> or higher.`);
}
});
}
function addLicensesToSummary(invalidLicenseChanges, config) {
if (countLicenseIssues(invalidLicenseChanges) === 0) {
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+423 -1
View File
@@ -421,6 +421,7 @@
"version": "7.23.2",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.2.tgz",
"integrity": "sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ==",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.22.13",
@@ -898,6 +899,74 @@
"node": ">=14.17"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
@@ -911,7 +980,346 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
@@ -1424,6 +1832,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
"integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
"peer": true,
"dependencies": {
"@babel/core": "^7.11.6",
"@jest/types": "^29.6.3",
@@ -1449,6 +1858,7 @@
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"peer": true,
"dependencies": {
"@jest/schemas": "^29.6.3",
"@types/istanbul-lib-coverage": "^2.0.0",
@@ -1656,6 +2066,7 @@
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz",
"integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
@@ -2272,6 +2683,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz",
"integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.21.0",
@@ -2307,6 +2719,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz",
"integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.21.0",
"@typescript-eslint/types": "6.21.0",
@@ -2546,6 +2959,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"dev": true,
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2944,6 +3358,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
"integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
"peer": true,
"dependencies": {
"@jest/transform": "^29.7.0",
"@types/babel__core": "^7.1.14",
@@ -3152,6 +3567,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001541",
"electron-to-chromium": "^1.4.535",
@@ -3997,6 +4413,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
"integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true,
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -4231,6 +4648,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz",
"integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.4.0",
"@typescript-eslint/types": "7.4.0",
@@ -5936,6 +6354,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz",
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -6374,6 +6793,7 @@
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
"integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
"peer": true,
"dependencies": {
"@jest/types": "^29.6.3",
"@types/node": "*",
@@ -7430,6 +7850,7 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -8560,6 +8981,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
+1 -1
View File
@@ -130,7 +130,7 @@ async function createSummary(
scorecard,
config
)
summary.addChangeVulnerabilitiesToSummary(
await summary.addChangeVulnerabilitiesToSummary(
vulnerabilities,
config.fail_on_severity
)
+4 -1
View File
@@ -193,7 +193,10 @@ async function run(): Promise<void> {
if (config.vulnerability_check) {
core.setOutput('vulnerable-changes', JSON.stringify(vulnerableChanges))
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
await summary.addChangeVulnerabilitiesToSummary(
vulnerableChanges,
minSeverity
)
issueFound ||= await printVulnerabilitiesBlock(
vulnerableChanges,
minSeverity,
+159 -7
View File
@@ -2,7 +2,12 @@ import * as core from '@actions/core'
import {SummaryTableRow} from '@actions/core/lib/summary'
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
import {Change, Changes, ConfigurationOptions, Scorecard} from './schemas'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
import {
groupDependenciesByManifest,
getManifestsSet,
renderUrl,
octokitClient
} from './utils'
const icons = {
check: '✅',
@@ -12,6 +17,62 @@ const icons = {
const MAX_SCANNED_FILES_BYTES = 1048576
// Helper to check if a version falls within a vulnerable range
// Supports basic semver comparisons like ">= 8.0.0, <= 8.0.20"
function versionInRange(version: string, range: string): boolean {
if (!version || !range) return false
// Parse version into comparable parts
const vParts = version.split('.').map(p => parseInt(p, 10))
// Handle range formats like ">= 8.0.0, <= 8.0.20"
const conditions = range.split(',').map(c => c.trim())
for (const condition of conditions) {
const match = condition.match(/([><=]+)\s*(\d+(?:\.\d+)*)/)
if (!match) continue
const [, operator, rangeVer] = match
const rParts = rangeVer.split('.').map(p => parseInt(p, 10))
// Compare versions part by part
let cmp = 0
for (let i = 0; i < Math.max(vParts.length, rParts.length); i++) {
const v = vParts[i] || 0
const r = rParts[i] || 0
if (v > r) {
cmp = 1
break
} else if (v < r) {
cmp = -1
break
}
}
// Check if condition is satisfied
if (operator === '>=' && cmp < 0) return false
if (operator === '>' && cmp <= 0) return false
if (operator === '<=' && cmp > 0) return false
if (operator === '<' && cmp >= 0) return false
if (operator === '=' && cmp !== 0) return false
}
return true
}
function extractPatchVersionId(patchData: unknown): string | null {
// Handle string format (current API response)
if (typeof patchData === 'string') return patchData
// Handle object format with identifier field (for backward compatibility)
if (patchData && typeof patchData === 'object' && 'identifier' in patchData) {
const id = (patchData as {identifier: unknown}).identifier
return typeof id === 'string' ? id : null
}
return null
}
// generates the DR report summary and caches it to the Action's core.summary.
// returns the DR summary string, ready to be posted as a PR comment if the
// final DR report is too large
@@ -132,18 +193,77 @@ function countScorecardWarnings(
)
}
export function addChangeVulnerabilitiesToSummary(
export async function addChangeVulnerabilitiesToSummary(
vulnerableChanges: Changes,
severity: string
): void {
): Promise<void> {
if (vulnerableChanges.length === 0) {
return
}
const rows: SummaryTableRow[] = []
const manifests = getManifestsSet(vulnerableChanges)
// Build set of unique advisories to query
const advisorySet = new Set<string>()
for (const pkg of vulnerableChanges) {
for (const vuln of pkg.vulnerabilities) {
advisorySet.add(vuln.advisory_ghsa_id)
}
}
// Query GitHub API for patch info in parallel
// Store all vulnerability entries (may be multiple per package with different ranges)
const patchInfo: Record<
string,
{eco: string; pkg: string; range: string; patch: string}[]
> = {}
const apiClient = octokitClient()
await Promise.all(
Array.from(advisorySet).map(async advId => {
try {
core.debug(`Fetching advisory data for ${advId}`)
const apiResult = await apiClient.request('GET /advisories/{ghsa_id}', {
ghsa_id: advId
})
patchInfo[advId] = []
const vulnList = apiResult.data.vulnerabilities || []
core.debug(
`Found ${vulnList.length} vulnerability entries for ${advId}`
)
for (const v of vulnList) {
if (v.package && v.package.ecosystem) {
const normalizedEco = v.package.ecosystem.toLowerCase()
const pkgName = v.package.name || ''
const vulnRange = v.vulnerable_version_range || ''
const patchVerId = extractPatchVersionId(v.first_patched_version)
if (patchVerId) {
patchInfo[advId].push({
eco: normalizedEco,
pkg: pkgName,
range: vulnRange,
patch: patchVerId
})
core.debug(
`Added patch info for ${pkgName} (${normalizedEco}): ${patchVerId} for range ${vulnRange}`
)
} else {
core.debug(
`No patch version found for ${pkgName} (${normalizedEco}) in ${advId}`
)
}
}
}
} catch (e) {
core.debug(`API call failed for ${advId}: ${e}`)
patchInfo[advId] = []
}
})
)
core.summary.addHeading('Vulnerabilities', 2)
for (const manifest of manifests) {
@@ -157,18 +277,49 @@ export function addChangeVulnerabilitiesToSummary(
previous_package === change.name &&
previous_version === change.version
// Look up patch version by matching package name, ecosystem, and version range
let patchVer = 'N/A'
const advData = patchInfo[vuln.advisory_ghsa_id]
if (advData && advData.length > 0) {
const normalizedEco = change.ecosystem.toLowerCase()
core.debug(
`Looking up patch for ${change.name}@${change.version} (${normalizedEco}) in ${vuln.advisory_ghsa_id}`
)
// Find matching entry by ecosystem, package name, and version range
const matchingEntry = advData.find(
entry =>
entry.eco === normalizedEco &&
entry.pkg === change.name &&
versionInRange(change.version, entry.range)
)
if (matchingEntry) {
patchVer = matchingEntry.patch
core.debug(
`Found patch version ${patchVer} for ${change.name}@${change.version}`
)
} else {
core.debug(
`No matching patch found for ${change.name}@${change.version}. Available entries: ${JSON.stringify(advData)}`
)
}
} else {
core.debug(`No advisory data available for ${vuln.advisory_ghsa_id}`)
}
if (!sameAsPrevious) {
rows.push([
renderUrl(change.source_repository_url, change.name),
change.version,
renderUrl(vuln.advisory_url, vuln.advisory_summary),
vuln.severity
vuln.severity,
patchVer
])
} else {
rows.push([
{data: '', colspan: '2'},
renderUrl(vuln.advisory_url, vuln.advisory_summary),
vuln.severity
vuln.severity,
patchVer
])
}
previous_package = change.name
@@ -180,7 +331,8 @@ export function addChangeVulnerabilitiesToSummary(
{data: 'Name', header: true},
{data: 'Version', header: true},
{data: 'Vulnerability', header: true},
{data: 'Severity', header: true}
{data: 'Severity', header: true},
{data: 'Patched Version', header: true}
],
...rows
])