Add 'show-patched-versions' option to configuration and update summary handling
- Introduced 'show-patched-versions' input in action.yml to control visibility of patched versions in vulnerability summaries. - Updated default configuration and related functions to handle the new option. - Enhanced tests to verify behavior with and without the patched version column.
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
- [Overview](#overview)
|
||||
- [Viewing the results](#viewing-the-results)
|
||||
- [Installation](#installation)
|
||||
- [Installation (standard)](#installation-standard)
|
||||
- [Installation (GitHub Enterprise Server)](#installation-github-enterprise-server)
|
||||
- [Installation (standard)](#installation-standard)
|
||||
- [Installation (GitHub Enterprise Server)](#installation-github-enterprise-server)
|
||||
- [Configuration](#configuration)
|
||||
- [Configuration options](#configuration-options)
|
||||
- [Configuration methods](#configuration-methods)
|
||||
@@ -130,6 +130,7 @@ All configuration options are optional.
|
||||
| `warn-only`+ | When set to `true`, the action will log all vulnerabilities as warnings regardless of the severity, and the action will complete with a `success` status. This overrides the `fail-on-severity` option. | `true`, `false` | `false` |
|
||||
| `show-openssf-scorecard` | When set to `true`, the action will output information about all the known OpenSSF Scorecard scores for the dependencies changed in this pull request. | `true`, `false` | `true` |
|
||||
| `warn-on-openssf-scorecard-level` | When `show-openssf-scorecard-levels` is set to `true`, this option lets you configure the threshold for when a score is considered too low and gets a :warning: warning in the CI. | Any positive integer | 3 |
|
||||
| `show-patched-versions`\* | When set to `true`, the vulnerability summary table will include an additional column showing the first patched version for each vulnerability. This requires additional API calls to fetch advisory data. | `true`, `false` | `false` |
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
@@ -215,6 +216,7 @@ You can use an external configuration file to specify settings for this action.
|
||||
|
||||
3. Create the configuration file in the path you specified for `config-file`.
|
||||
4. In the configuration file, specify your chosen settings.
|
||||
|
||||
```yaml
|
||||
fail-on-severity: 'critical'
|
||||
allow-licenses:
|
||||
|
||||
@@ -47,7 +47,8 @@ const defaultConfig: ConfigurationOptions = {
|
||||
retry_on_snapshot_warnings_timeout: 120,
|
||||
warn_only: false,
|
||||
warn_on_openssf_scorecard_level: 3,
|
||||
show_openssf_scorecard: false
|
||||
show_openssf_scorecard: false,
|
||||
show_patched_versions: false
|
||||
}
|
||||
|
||||
const changesWithEmptyManifests: Changes = [
|
||||
@@ -407,15 +408,70 @@ test('addChangeVulnerabilitiesToSummary() - does not print severity statement if
|
||||
expect(text).not.toContain('Only included vulnerabilities')
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - includes patched version column', async () => {
|
||||
test('addChangeVulnerabilitiesToSummary() - does not include patched version column by default', async () => {
|
||||
const changes = [createTestChange()]
|
||||
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).not.toContain('Patched Version')
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - includes patched version column when enabled', async () => {
|
||||
const changes = [createTestChange()]
|
||||
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low', true)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).toContain('Patched Version')
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - skips patched version on GHES even when enabled', async () => {
|
||||
const originalUrl = process.env.GITHUB_SERVER_URL
|
||||
process.env.GITHUB_SERVER_URL = 'https://ghes.example.com'
|
||||
const warnSpy = jest.spyOn(core, 'warning')
|
||||
|
||||
const changes = [createTestChange()]
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low', true)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).not.toContain('Patched Version')
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'show-patched-versions is not supported on GitHub Enterprise Server. The Patched Version column will be omitted.'
|
||||
)
|
||||
expect(mockOctokitRequest).not.toHaveBeenCalled()
|
||||
|
||||
process.env.GITHUB_SERVER_URL = originalUrl
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - works normally on GHES when patched versions disabled', async () => {
|
||||
const originalUrl = process.env.GITHUB_SERVER_URL
|
||||
process.env.GITHUB_SERVER_URL = 'https://ghes.example.com'
|
||||
|
||||
const changes = [createTestChange()]
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low', false)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).not.toContain('Patched Version')
|
||||
expect(mockOctokitRequest).not.toHaveBeenCalled()
|
||||
|
||||
process.env.GITHUB_SERVER_URL = originalUrl
|
||||
})
|
||||
|
||||
test('addChangeVulnerabilitiesToSummary() - works normally on GHES with default (no third arg)', async () => {
|
||||
const originalUrl = process.env.GITHUB_SERVER_URL
|
||||
process.env.GITHUB_SERVER_URL = 'https://ghes.example.com'
|
||||
|
||||
const changes = [createTestChange()]
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
||||
|
||||
const text = core.summary.stringify()
|
||||
expect(text).not.toContain('Patched Version')
|
||||
expect(mockOctokitRequest).not.toHaveBeenCalled()
|
||||
|
||||
process.env.GITHUB_SERVER_URL = originalUrl
|
||||
})
|
||||
|
||||
test('addLicensesToSummary() - does not include entire section if no license issues found', () => {
|
||||
summary.addLicensesToSummary(emptyInvalidLicenseChanges, defaultConfig)
|
||||
const text = core.summary.stringify()
|
||||
@@ -584,7 +640,7 @@ test('addChangeVulnerabilitiesToSummary() - handles multiple version ranges for
|
||||
})
|
||||
|
||||
const changes = [pkg8, pkg9]
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low', true)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
|
||||
@@ -628,7 +684,7 @@ test('addChangeVulnerabilitiesToSummary() - handles RestSharp GHSA-4rr6-2v9v-wcp
|
||||
})
|
||||
|
||||
const changes = [pkg]
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low', true)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
|
||||
@@ -672,7 +728,7 @@ test('addChangeVulnerabilitiesToSummary() - handles version coercion for non-str
|
||||
})
|
||||
|
||||
const changes = [pkg]
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low', true)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
|
||||
@@ -714,7 +770,7 @@ test('addChangeVulnerabilitiesToSummary() - handles invalid versions in fail-ope
|
||||
})
|
||||
|
||||
const changes = [pkg]
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low')
|
||||
await summary.addChangeVulnerabilitiesToSummary(changes, 'low', true)
|
||||
|
||||
const text = core.summary.stringify()
|
||||
|
||||
@@ -765,7 +821,7 @@ test('addChangeVulnerabilitiesToSummary() - respects concurrency limit for API c
|
||||
}
|
||||
})
|
||||
|
||||
await summary.addChangeVulnerabilitiesToSummary(packages, 'low')
|
||||
await summary.addChangeVulnerabilitiesToSummary(packages, 'low', true)
|
||||
|
||||
// Verify that concurrency limit (10) was respected
|
||||
expect(maxConcurrent).toBeLessThanOrEqual(10)
|
||||
@@ -814,7 +870,7 @@ test('addChangeVulnerabilitiesToSummary() - completes all tasks even with varyin
|
||||
}
|
||||
)
|
||||
|
||||
await summary.addChangeVulnerabilitiesToSummary(packages, 'low')
|
||||
await summary.addChangeVulnerabilitiesToSummary(packages, 'low', true)
|
||||
|
||||
// Verify all 20 unique advisories were fetched and completed
|
||||
expect(completedAdvisories.size).toBe(20)
|
||||
|
||||
@@ -76,6 +76,9 @@ inputs:
|
||||
warn-on-openssf-scorecard-level:
|
||||
description: Numeric threshold for the OpenSSF Scorecard score. If the score is below this threshold, the action will warn you.
|
||||
required: false
|
||||
show-patched-versions:
|
||||
description: When set to `true`, the vulnerability summary table will include a column showing the first patched version for each vulnerability.
|
||||
required: false
|
||||
outputs:
|
||||
comment-content:
|
||||
description: Prepared dependency report comment
|
||||
|
||||
67
dist/index.js
generated
vendored
67
dist/index.js
generated
vendored
@@ -786,7 +786,7 @@ function run() {
|
||||
let issueFound = false;
|
||||
if (config.vulnerability_check) {
|
||||
core.setOutput('vulnerable-changes', JSON.stringify(vulnerableChanges));
|
||||
yield summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity);
|
||||
yield summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity, config.show_patched_versions);
|
||||
issueFound || (issueFound = yield printVulnerabilitiesBlock(vulnerableChanges, minSeverity, warnOnly));
|
||||
}
|
||||
if (config.license_check) {
|
||||
@@ -1311,6 +1311,7 @@ exports.ConfigurationOptionsSchema = z
|
||||
retry_on_snapshot_warnings_timeout: z.number().default(120),
|
||||
show_openssf_scorecard: z.boolean().optional().default(true),
|
||||
warn_on_openssf_scorecard_level: z.number().default(3),
|
||||
show_patched_versions: z.boolean().default(false),
|
||||
comment_summary_in_pr: z
|
||||
.union([
|
||||
z.preprocess(val => (val === 'true' ? true : val === 'false' ? false : val), z.boolean()),
|
||||
@@ -1878,17 +1879,25 @@ function promisePool(tasks, limit) {
|
||||
yield Promise.all(executing);
|
||||
});
|
||||
}
|
||||
function addChangeVulnerabilitiesToSummary(vulnerableChanges, severity) {
|
||||
return __awaiter(this, void 0, void 0, function* () {
|
||||
function addChangeVulnerabilitiesToSummary(vulnerableChanges_1, severity_1) {
|
||||
return __awaiter(this, arguments, void 0, function* (vulnerableChanges, severity, showPatchedVersions = false) {
|
||||
if (vulnerableChanges.length === 0) {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
if (showPatchedVersions) {
|
||||
if ((0, utils_1.isEnterprise)()) {
|
||||
core.warning('show-patched-versions is not supported on GitHub Enterprise Server. The Patched Version column will be omitted.');
|
||||
showPatchedVersions = false;
|
||||
}
|
||||
else {
|
||||
for (const pkg of vulnerableChanges) {
|
||||
for (const vuln of pkg.vulnerabilities) {
|
||||
advisorySet.add(vuln.advisory_ghsa_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Query GitHub API for patch info with concurrency limiting
|
||||
@@ -1992,34 +2001,43 @@ function addChangeVulnerabilitiesToSummary(vulnerableChanges, severity) {
|
||||
core.debug(`No advisory data available for ${vuln.advisory_ghsa_id}`);
|
||||
}
|
||||
if (!sameAsPrevious) {
|
||||
rows.push([
|
||||
const row = [
|
||||
(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
|
||||
]);
|
||||
vuln.severity
|
||||
];
|
||||
if (showPatchedVersions) {
|
||||
row.push(patchVer);
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
else {
|
||||
rows.push([
|
||||
const row = [
|
||||
{ data: '', colspan: '2' },
|
||||
(0, utils_1.renderUrl)(vuln.advisory_url, vuln.advisory_summary),
|
||||
vuln.severity,
|
||||
patchVer
|
||||
]);
|
||||
vuln.severity
|
||||
];
|
||||
if (showPatchedVersions) {
|
||||
row.push(patchVer);
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
previous_package = change.name;
|
||||
previous_version = change.version;
|
||||
}
|
||||
}
|
||||
const headerRow = [
|
||||
{ data: 'Name', header: true },
|
||||
{ data: 'Version', header: true },
|
||||
{ data: 'Vulnerability', header: true },
|
||||
{ data: 'Severity', header: true }
|
||||
];
|
||||
if (showPatchedVersions) {
|
||||
headerRow.push({ data: 'Patched Version', header: true });
|
||||
}
|
||||
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 }
|
||||
],
|
||||
headerRow,
|
||||
...rows
|
||||
]);
|
||||
}
|
||||
@@ -2251,6 +2269,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.groupDependenciesByManifest = groupDependenciesByManifest;
|
||||
exports.getManifestsSet = getManifestsSet;
|
||||
exports.renderUrl = renderUrl;
|
||||
exports.isEnterprise = isEnterprise;
|
||||
exports.octokitClient = octokitClient;
|
||||
const core = __importStar(__nccwpck_require__(37484));
|
||||
const octokit_1 = __nccwpck_require__(42373);
|
||||
@@ -98859,6 +98878,7 @@ function readInlineConfig() {
|
||||
const warn_only = getOptionalBoolean('warn-only');
|
||||
const show_openssf_scorecard = getOptionalBoolean('show-openssf-scorecard');
|
||||
const warn_on_openssf_scorecard_level = getOptionalNumber('warn-on-openssf-scorecard-level');
|
||||
const show_patched_versions = getOptionalBoolean('show-patched-versions');
|
||||
validateLicenses('allow-licenses', allow_licenses);
|
||||
validateLicenses('deny-licenses', deny_licenses);
|
||||
const keys = {
|
||||
@@ -98879,7 +98899,8 @@ function readInlineConfig() {
|
||||
retry_on_snapshot_warnings_timeout,
|
||||
warn_only,
|
||||
show_openssf_scorecard,
|
||||
warn_on_openssf_scorecard_level
|
||||
warn_on_openssf_scorecard_level,
|
||||
show_patched_versions
|
||||
};
|
||||
return Object.fromEntries(Object.entries(keys).filter(([_, value]) => value !== undefined));
|
||||
}
|
||||
@@ -99369,6 +99390,7 @@ exports.ConfigurationOptionsSchema = z
|
||||
retry_on_snapshot_warnings_timeout: z.number().default(120),
|
||||
show_openssf_scorecard: z.boolean().optional().default(true),
|
||||
warn_on_openssf_scorecard_level: z.number().default(3),
|
||||
show_patched_versions: z.boolean().default(false),
|
||||
comment_summary_in_pr: z
|
||||
.union([
|
||||
z.preprocess(val => (val === 'true' ? true : val === 'false' ? false : val), z.boolean()),
|
||||
@@ -99600,6 +99622,7 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||
exports.groupDependenciesByManifest = groupDependenciesByManifest;
|
||||
exports.getManifestsSet = getManifestsSet;
|
||||
exports.renderUrl = renderUrl;
|
||||
exports.isEnterprise = isEnterprise;
|
||||
exports.octokitClient = octokitClient;
|
||||
const core = __importStar(__nccwpck_require__(37484));
|
||||
const octokit_1 = __nccwpck_require__(42373);
|
||||
|
||||
2
dist/index.js.map
generated
vendored
2
dist/index.js.map
generated
vendored
File diff suppressed because one or more lines are too long
@@ -35,7 +35,8 @@ const defaultConfig: ConfigurationOptions = {
|
||||
retry_on_snapshot_warnings_timeout: 120,
|
||||
warn_only: false,
|
||||
warn_on_openssf_scorecard_level: 3,
|
||||
show_openssf_scorecard: true
|
||||
show_openssf_scorecard: true,
|
||||
show_patched_versions: false
|
||||
}
|
||||
|
||||
const scorecard: Scorecard = {
|
||||
|
||||
@@ -52,6 +52,7 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
const warn_on_openssf_scorecard_level = getOptionalNumber(
|
||||
'warn-on-openssf-scorecard-level'
|
||||
)
|
||||
const show_patched_versions = getOptionalBoolean('show-patched-versions')
|
||||
|
||||
validateLicenses('allow-licenses', allow_licenses)
|
||||
validateLicenses('deny-licenses', deny_licenses)
|
||||
@@ -74,7 +75,8 @@ function readInlineConfig(): ConfigurationOptionsPartial {
|
||||
retry_on_snapshot_warnings_timeout,
|
||||
warn_only,
|
||||
show_openssf_scorecard,
|
||||
warn_on_openssf_scorecard_level
|
||||
warn_on_openssf_scorecard_level,
|
||||
show_patched_versions
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
|
||||
@@ -207,7 +207,8 @@ async function run(): Promise<void> {
|
||||
core.setOutput('vulnerable-changes', JSON.stringify(vulnerableChanges))
|
||||
await summary.addChangeVulnerabilitiesToSummary(
|
||||
vulnerableChanges,
|
||||
minSeverity
|
||||
minSeverity,
|
||||
config.show_patched_versions
|
||||
)
|
||||
issueFound ||= await printVulnerabilitiesBlock(
|
||||
vulnerableChanges,
|
||||
|
||||
@@ -115,6 +115,7 @@ export const ConfigurationOptionsSchema = z
|
||||
retry_on_snapshot_warnings_timeout: z.number().default(120),
|
||||
show_openssf_scorecard: z.boolean().optional().default(true),
|
||||
warn_on_openssf_scorecard_level: z.number().default(3),
|
||||
show_patched_versions: z.boolean().default(false),
|
||||
comment_summary_in_pr: z
|
||||
.union([
|
||||
z.preprocess(
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
groupDependenciesByManifest,
|
||||
getManifestsSet,
|
||||
renderUrl,
|
||||
octokitClient
|
||||
octokitClient,
|
||||
isEnterprise
|
||||
} from './utils'
|
||||
import * as semver from 'semver'
|
||||
|
||||
@@ -277,7 +278,8 @@ async function promisePool(
|
||||
|
||||
export async function addChangeVulnerabilitiesToSummary(
|
||||
vulnerableChanges: Changes,
|
||||
severity: string
|
||||
severity: string,
|
||||
showPatchedVersions: boolean = false
|
||||
): Promise<void> {
|
||||
if (vulnerableChanges.length === 0) {
|
||||
return
|
||||
@@ -287,9 +289,18 @@ export async function addChangeVulnerabilitiesToSummary(
|
||||
|
||||
// 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)
|
||||
if (showPatchedVersions) {
|
||||
if (isEnterprise()) {
|
||||
core.warning(
|
||||
'show-patched-versions is not supported on GitHub Enterprise Server. The Patched Version column will be omitted.'
|
||||
)
|
||||
showPatchedVersions = false
|
||||
} else {
|
||||
for (const pkg of vulnerableChanges) {
|
||||
for (const vuln of pkg.vulnerabilities) {
|
||||
advisorySet.add(vuln.advisory_ghsa_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,33 +445,42 @@ export async function addChangeVulnerabilitiesToSummary(
|
||||
}
|
||||
|
||||
if (!sameAsPrevious) {
|
||||
rows.push([
|
||||
const row: SummaryTableRow = [
|
||||
renderUrl(change.source_repository_url, change.name),
|
||||
change.version,
|
||||
renderUrl(vuln.advisory_url, vuln.advisory_summary),
|
||||
vuln.severity,
|
||||
patchVer
|
||||
])
|
||||
vuln.severity
|
||||
]
|
||||
if (showPatchedVersions) {
|
||||
row.push(patchVer)
|
||||
}
|
||||
rows.push(row)
|
||||
} else {
|
||||
rows.push([
|
||||
const row: SummaryTableRow = [
|
||||
{data: '', colspan: '2'},
|
||||
renderUrl(vuln.advisory_url, vuln.advisory_summary),
|
||||
vuln.severity,
|
||||
patchVer
|
||||
])
|
||||
vuln.severity
|
||||
]
|
||||
if (showPatchedVersions) {
|
||||
row.push(patchVer)
|
||||
}
|
||||
rows.push(row)
|
||||
}
|
||||
previous_package = change.name
|
||||
previous_version = change.version
|
||||
}
|
||||
}
|
||||
const headerRow: SummaryTableRow = [
|
||||
{data: 'Name', header: true},
|
||||
{data: 'Version', header: true},
|
||||
{data: 'Vulnerability', header: true},
|
||||
{data: 'Severity', header: true}
|
||||
]
|
||||
if (showPatchedVersions) {
|
||||
headerRow.push({data: 'Patched Version', header: true})
|
||||
}
|
||||
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}
|
||||
],
|
||||
headerRow,
|
||||
...rows
|
||||
])
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function renderUrl(url: string | null, text: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function isEnterprise(): boolean {
|
||||
export function isEnterprise(): boolean {
|
||||
const serverUrl = new URL(
|
||||
process.env['GITHUB_SERVER_URL'] ?? 'https://github.com'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user