Compare commits

...

21 Commits

Author SHA1 Message Date
Eric Sorenson f5b971718e Merge pull request #1067 from ahpook/ahpook/custom-instructions
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (ruby) (push) Has been cancelled
Add .github/copilot-instructions.md for Copilot coding agent
2026-03-06 14:41:37 -08:00
Eric Sorenson f51df6d455 Updates from code review 2026-03-05 20:39:09 -08:00
Eric Sorenson cffae74507 Add .github/copilot-instructions.md for Copilot coding agent
Add instructions file to help Copilot coding agent work efficiently with
this repository. Includes build/validation commands, project layout,
CI checks, style rules, testing patterns, and important notes about
the codebase conventions.
2026-03-05 18:46:42 -08:00
Eric Sorenson 2031cfc080 Merge pull request #1064 from actions/ahpook/release-4.9.0
Updates for release 4.9.0
2026-03-03 14:08:16 -08:00
Eric Sorenson d02fa39f79 Updates for release 4.9.0
- Bumps dependencies to fix vulnerabilities, supersedes dependabot PRs
- New version in package.json
- Slight correction to the release process in CONTRIBUTING.md
- Rebuilds dist/ packaged files

Closes #1062 #1063 #1028 #972 #971 #970
2026-03-02 16:15:13 -08:00
Eric Sorenson 4038a34c4b Merge pull request #1021 from actions/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 4 to 6
2026-03-02 16:00:21 -08:00
Eric Sorenson a632b8386b Merge pull request #1058 from actions/dependabot/github_actions/actions/stale-10.2.0
Bump actions/stale from 10.1.0 to 10.2.0
2026-03-02 15:59:31 -08:00
Eric Sorenson 57a3d46a7b Merge pull request #1060 from jantiebot/main
fix: only get scorecard levels if user wants to see the OpenSSF scorecard
2026-02-27 15:05:18 -08:00
Eric Sorenson 5ecdc4b578 Merge pull request #1045 from forks-felickz/main
Feat: Add `Patched Version` to `Vulnerabilities` summary
2026-02-27 15:03:52 -08:00
Chad Bentz e8c2f9a12c fix: remove inferrable type annotation to pass eslint 2026-02-27 22:58:04 +00:00
Chad Bentz 0e129e113c Prettier - Refactor summary table rendering for improved readability 2026-02-27 22:30:03 +00:00
Chad Bentz aa60746a92 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.
2026-02-27 14:58:54 -05:00
Chad Bentz e404798400 Merge upstream actions/dependency-review-action main
Syncs fork with upstream, resolving conflicts in package.json
(keeping semver + upgrading spdx-expression-parse to ^4.0.0),
regenerating package-lock.json and dist/ folder.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-27 14:04:27 -05:00
jantiebot 24398f008e chore: revert dist changes 2026-02-27 12:41:22 +01:00
jantiebot 7863651912 fix: only get scorecard levels if user wants to see the OpenSSF scorecard 2026-02-26 18:16:44 +01:00
dependabot[bot] 17d14c08d9 Bump actions/stale from 10.1.0 to 10.2.0
Bumps [actions/stale](https://github.com/actions/stale) from 10.1.0 to 10.2.0.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v10.1.0...v10.2.0)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 01:42:33 +00:00
Copilot a6c34d8785 Address review feedback: deterministic tests, cached normalization, simplified promisePool (#9)
* Initial plan

* Apply PR review comments: deterministic delays, cached normalization, simplified promisePool

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

* Improve comment clarity for ecoLower field

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>
2026-02-18 06:33:39 -05:00
Copilot 539c79be65 Implement review feedback: concurrency limiting, semver coercion, logging improvements, and test coverage (#8)
* Initial plan

* Implement PR review comments: concurrency limiting, semver coerce, improved logging, test fixes

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

* Fix promise pool race condition and remove .then() usage

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

* Add tests for semver coercion and promise pool concurrency, simplify Map to Set

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>
2026-02-09 14:28:09 -05:00
Copilot ee66ea100d Implement review fixes: semver library, scoping, case-insensitive matching, error logging, and configurable fail behavior (#7)
* Initial plan

* Implement PR review comment fixes: semver library, error handling, case-insensitive matching, and rows scoping

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

* Fix formatting and rebuild dist folder

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

* Fix fail-closed logic and remove redundant @types/semver

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

* Apply review feedback: fix empty range handling, add trimming, implement range check caching

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

* Apply review feedback: align fail-closed behavior for empty version, fix TypeScript typing, normalize cache keys

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

* Fix linter errors, optimize cache keys, and improve trimming logic

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

* Add fail-open option for patch selection and optimize with preTrimmed flag

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

* Enforce fail-closed with explicit validation, fix debug messages, normalize cache keys

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

* Fix unreachable ternary in debug message and eliminate duplicate trim operation

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

* Normalize eco comparison and add preNormalized option to avoid duplicate range conversion

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

* Remove unnecessary cache, fix function signature, and correct semver comment

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

* Make includePrerelease conditional based on version type to preserve range semantics

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

* Improve debug message to report both invalid version and range when applicable

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

* Convert to JSDoc, add explicit type annotation, and remove redundant initializer

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>
2026-02-08 16:05:04 -05:00
Copilot 2af9bac14d 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>
2026-02-06 18:12:20 -05:00
dependabot[bot] 1d60e0d095 Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-27 18:20:43 +00:00
19 changed files with 30307 additions and 658 deletions
+111
View File
@@ -0,0 +1,111 @@
# Copilot Coding Agent Instructions
Trust these instructions. Only search the codebase if information here is incomplete or found to be in error.
## Repository Overview
**dependency-review-action** is a GitHub Action (TypeScript/Node.js 20) that scans pull requests for dependency changes, raising errors for vulnerabilities or invalid licenses. It queries the GitHub Dependency Review API, evaluates changes against configured rules, and produces job summaries and PR comments. The action entry point is `dist/index.js` (bundled via `ncc`). The repo is small (~15 source files, ~15 test files).
## Build & Validation Commands
For CI-parity installs and local validation, run `npm ci --ignore-scripts` before other commands. This is the install step used in CI; release workflows may follow different install instructions (see CONTRIBUTING).
| Task | Command | Notes |
| ------------ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| Install | `npm ci --ignore-scripts` | ~45s. Use `--ignore-scripts` for CI-parity installs; release workflows may use `npm i` per CONTRIBUTING. |
| Build | `npm run build` | Compiles `src/*.ts``lib/*.js` via `tsc -p tsconfig.build.json`. ~5s. |
| Test | `npm test` | Runs Jest. ~8s. All tests should pass. |
| Lint | `npm run lint` | ESLint on `src/**/*.ts`. Ignore the TS version warning—it still passes. |
| Format check | `npm run format-check` | Prettier check on `**/*.ts`. |
| Format fix | `npm run format` | Auto-fix formatting with Prettier. |
| Package | `npm run package` | Bundles the action entrypoint (`package.json#main`) → `dist/index.js` via `ncc`. ~7s. Do NOT include `dist/` changes in non-release PRs. |
| All | `npm run all` | Runs: build → format → lint → package → test (in that order). |
### Validation Sequence After Making Changes
Always run these commands in this order to validate changes:
```sh
npm run build
npm run format-check
npm run lint
npm test
```
If format-check fails, run `npm run format` to auto-fix, then re-check.
### CI Checks (`.github/workflows/ci.yml`)
CI runs on PRs (excluding `**.md` changes) with Node 20:
1. **test** job: `npm ci --ignore-scripts``npm test`
2. **lint** job: `npm ci --ignore-scripts``npm run format-check``npm run lint`
Additional workflows: `dependency-review.yml` (self-test), `codeql.yml` (CodeQL analysis), `stale.yaml` (stale issues).
## Project Layout
```
src/ # TypeScript source (edit these files)
main.ts # Entry point — orchestrates the action (532 lines)
schemas.ts # Zod schemas & TypeScript types for all data structures
config.ts # Reads action inputs + external YAML config
dependency-graph.ts # GitHub API client for dependency diff
filter.ts # Filters changes by severity, scope, allowed advisories
licenses.ts # License validation against allow/deny lists
deny.ts # Package/group deny-listing logic
purl.ts # Package URL (PURL) parser
spdx.ts # SPDX license expression handling
scorecard.ts # OpenSSF Scorecard integration
summary.ts # Summary/report generation (736 lines, largest module)
comment-pr.ts # Posts/updates PR comments with results
git-refs.ts # Resolves base/head git refs from event payload
utils.ts # Shared utilities (Octokit client, grouping helpers)
lib/ # Compiled JS output (from `npm run build`). Gitignored.
dist/ # Bundled action (from `npm run package`). Committed but do NOT include changes in normal PRs - only pull requests which are creating new releases should have these files changed.
__tests__/ # Jest test files (*.test.ts)
test-helpers.ts # setInput()/clearInputs() helpers for test env vars
fixtures/ # YAML config samples and factory helpers
create-test-change.ts # Factory for mock Change objects
create-test-vulnerability.ts # Factory for mock vulnerability objects
scripts/ # Dev/debug utilities (scan_pr for manual testing, create_summary.ts for preview)
action.yml # Action metadata — inputs, outputs, and `runs.main: dist/index.js`
```
### Configuration Files
| File | Purpose |
| --------------------- | ---------------------------------------------------------------------------- |
| `tsconfig.json` | Base TypeScript config (ES6 target, CommonJS, strict mode) |
| `tsconfig.build.json` | Build config — extends base, includes only `src/`, outputs to `lib/` |
| `jest.config.js` | Jest config — uses `ts-jest`, matches `**/*.test.ts` |
| `.eslintrc.json` | ESLint — `plugin:github/recommended`, strict TS rules, no semicolons |
| `.prettierrc.json` | Prettier — no semis, single quotes, no bracket spacing, trailing comma: none |
| `.prettierignore` | Ignores `dist/`, `lib/`, `node_modules/` |
### Key TypeScript/Style Rules
- No semicolons (enforced by ESLint and Prettier)
- Single quotes, no trailing commas
- `@typescript-eslint/no-explicit-any: error` — never use `any`
- `@typescript-eslint/explicit-function-return-type: error` — all functions need return types (expressions exempt)
- Unused function parameters/args must be prefixed with `_` (e.g. `_unused`); unused variables should be removed
- Use Zod schemas (in `src/schemas.ts`) for all data validation and type definitions
- Config option defaults belong in Zod schemas, NOT in `action.yml`
### Testing Patterns
- Tests use Jest with `ts-jest` transform — no build step needed before running tests
- Use `__tests__/test-helpers.ts` `setInput(name, value)` to mock action inputs (sets `INPUT_*` env vars)
- Use `__tests__/fixtures/create-test-change.ts` and `create-test-vulnerability.ts` for test data factories
- Test files follow `__tests__/<module>.test.ts` naming convention
- Tests run directly against TypeScript source (not compiled JS)
### Important Notes
- The action runs on `node20` (declared in `action.yml`)
- Source imports often use relative `../src/` paths (e.g. `import {readConfig} from '../src/config'`)
- Adding a new action input requires changes in: `action.yml` (input definition), `src/schemas.ts` (Zod schema with default), `src/config.ts` (reading the input), and relevant source/test files
- `dist/index.js` is committed for GitHub Actions but PR contributors should NOT include `dist/` changes — maintainers handle rebuilding
- The `lib/` directory is gitignored
- Scorecard tests make real HTTP calls to `api.securityscorecards.dev` and `deps.dev`
+2 -2
View File
@@ -17,7 +17,7 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
@@ -30,7 +30,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
+1 -1
View File
@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout Repository'
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Dependency Review
uses: ./
+1 -1
View File
@@ -12,7 +12,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10.1.0
- uses: actions/stale@v10.2.0
name: Clean up stale PRs and Issues
with:
stale-pr-message: "👋 This pull request has been marked as stale because it has been open with no activity for 180 days. You can: comment on the PR or remove the stale label to hold stalebot off for a while, add the `Keep` label to hold stale off permanently, or do nothing. If you do nothing, this pull request will be closed eventually by the stalebot. Please see CONTRIBUTING.md for more policy details."
+2 -1
View File
@@ -108,7 +108,8 @@ _Note: these instructions are for maintainers_
- Create a local branch based on the `main` of the upstream repo.
- Update the version number in [package.json](https://github.com/actions/dependency-review-action/blob/main/package.json) and run `npm i` to update the lockfile.
- Update the dist files by running `npm run build` and `npm run package`
- Go to [Draft a new release](https://github.com/actions/dependency-review-action/releases/new) in the Releases page.
- Submit a PR based on your branch and have another maintainer review/approve it.
- Once merged, go to [Draft a new release](https://github.com/actions/dependency-review-action/releases/new) in the Releases page.
- Make sure that the `Publish this Action to the GitHub Marketplace` checkbox is enabled
<img width="481" alt="Screen showing Release Action with Publish this Action to the GitHub Marketplace checked" src="https://user-images.githubusercontent.com/2161/173822484-4b60d8b4-c674-4bff-b5ff-b0c4a3650ab7.png">
+4 -2
View File
@@ -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:
+383 -15
View File
@@ -1,12 +1,25 @@
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()
jest.restoreAllMocks()
core.summary.emptyBuffer()
})
@@ -34,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 = [
@@ -315,19 +329,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 +349,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 +362,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 +380,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 +388,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 +399,79 @@ 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() - 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()
@@ -508,3 +586,293 @@ 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', true)
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', true)
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'
})
})
test('addChangeVulnerabilitiesToSummary() - handles version coercion for non-strict semver versions', async () => {
// Test that versions like "8.0" (without patch version) can be coerced to "8.0.0"
// for successful range matching in fail-open mode (patch selection)
const pkg = createTestChange({
ecosystem: 'npm',
name: 'test-package',
version: '8.0', // Non-strict semver version
vulnerabilities: [
createTestVulnerability({
advisory_ghsa_id: 'GHSA-test-1234',
advisory_summary: 'Test vulnerability',
severity: 'high'
})
]
})
mockOctokitRequest.mockResolvedValueOnce({
data: {
vulnerabilities: [
{
package: {
ecosystem: 'npm',
name: 'test-package'
},
vulnerable_version_range: '>= 8.0.0, < 9.0.0',
first_patched_version: '9.0.0'
}
]
}
})
const changes = [pkg]
await summary.addChangeVulnerabilitiesToSummary(changes, 'low', true)
const text = core.summary.stringify()
// Should coerce "8.0" to "8.0.0" and successfully match the range,
// showing the patched version instead of N/A
expect(text).toContain('9.0.0')
expect(text).not.toContain('N/A')
})
test('addChangeVulnerabilitiesToSummary() - handles invalid versions in fail-open mode', async () => {
// Test that completely invalid versions that can't be coerced
// still return N/A gracefully in fail-open mode
const pkg = createTestChange({
ecosystem: 'npm',
name: 'test-package',
version: 'invalid-version-string',
vulnerabilities: [
createTestVulnerability({
advisory_ghsa_id: 'GHSA-test-5678',
advisory_summary: 'Test vulnerability',
severity: 'high'
})
]
})
mockOctokitRequest.mockResolvedValueOnce({
data: {
vulnerabilities: [
{
package: {
ecosystem: 'npm',
name: 'test-package'
},
vulnerable_version_range: '>= 1.0.0, < 2.0.0',
first_patched_version: '2.0.0'
}
]
}
})
const changes = [pkg]
await summary.addChangeVulnerabilitiesToSummary(changes, 'low', true)
const text = core.summary.stringify()
// Should show N/A since version can't be coerced or matched
expect(text).toContain('N/A')
})
test('addChangeVulnerabilitiesToSummary() - respects concurrency limit for API calls', async () => {
// Create 15 packages with different vulnerabilities to test concurrency limiting
const packages = Array.from({length: 15}, (_, i) =>
createTestChange({
ecosystem: 'npm',
name: `package-${i}`,
version: '1.0.0',
vulnerabilities: [
createTestVulnerability({
advisory_ghsa_id: `GHSA-test-${i.toString().padStart(4, '0')}`,
advisory_summary: `Vulnerability ${i}`,
severity: 'high'
})
]
})
)
// Track concurrent calls
let maxConcurrent = 0
let currentConcurrent = 0
mockOctokitRequest.mockImplementation(async () => {
currentConcurrent++
maxConcurrent = Math.max(maxConcurrent, currentConcurrent)
// Simulate async API call with a small deterministic delay
await new Promise(resolve => setTimeout(resolve, 5))
currentConcurrent--
return {
data: {
vulnerabilities: [
{
package: {ecosystem: 'npm', name: 'test'},
vulnerable_version_range: '>= 1.0.0, < 2.0.0',
first_patched_version: '2.0.0'
}
]
}
}
})
await summary.addChangeVulnerabilitiesToSummary(packages, 'low', true)
// Verify that concurrency limit (10) was respected
expect(maxConcurrent).toBeLessThanOrEqual(10)
// Verify all 15 unique advisories were fetched
expect(mockOctokitRequest).toHaveBeenCalledTimes(15)
})
test('addChangeVulnerabilitiesToSummary() - completes all tasks even with varying durations', async () => {
// Test that promise pool doesn't lose tasks when some complete faster than others
const packages = Array.from({length: 20}, (_, i) =>
createTestChange({
ecosystem: 'npm',
name: `package-${i}`,
version: '1.0.0',
vulnerabilities: [
createTestVulnerability({
advisory_ghsa_id: `GHSA-vary-${i.toString().padStart(4, '0')}`,
advisory_summary: `Vulnerability ${i}`,
severity: 'high'
})
]
})
)
const completedAdvisories = new Set<string>()
mockOctokitRequest.mockImplementation(
async (path: string, params: {ghsa_id: string}) => {
// Variable delay to simulate real-world API response times
const delay = Math.random() * 50
await new Promise(resolve => setTimeout(resolve, delay))
completedAdvisories.add(params.ghsa_id)
return {
data: {
vulnerabilities: [
{
package: {ecosystem: 'npm', name: 'test'},
vulnerable_version_range: '>= 1.0.0, < 2.0.0',
first_patched_version: '2.0.0'
}
]
}
}
}
)
await summary.addChangeVulnerabilitiesToSummary(packages, 'low', true)
// Verify all 20 unique advisories were fetched and completed
expect(completedAdvisories.size).toBe(20)
expect(mockOctokitRequest).toHaveBeenCalledTimes(20)
})
+3
View File
@@ -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
Generated Vendored
+29328 -506
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+133 -101
View File
@@ -1,12 +1,12 @@
{
"name": "dependency-review-action",
"version": "4.8.3",
"version": "4.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dependency-review-action",
"version": "4.8.3",
"version": "4.9.0",
"license": "MIT",
"dependencies": {
"@actions/artifact": "^5.0.1",
@@ -20,6 +20,7 @@
"got": "^14.4.7",
"jest": "^29.7.0",
"octokit": "^3.1.2",
"semver": "^7.7.4",
"spdx-expression-parse": "^4.0.0",
"spdx-satisfies": "^6.0.0",
"ts-jest": "^29.4.1",
@@ -54,14 +55,14 @@
}
},
"node_modules/@actions/artifact": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@actions/artifact/-/artifact-5.0.1.tgz",
"integrity": "sha512-dHJ5rHduhCKUikKTT9eXeWoUvfKia3IjR1sO/VTAV3DVAL4yMTRnl2iO5mcfiBjySHLwPNezwENAVskKYU5ymw==",
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/@actions/artifact/-/artifact-5.0.3.tgz",
"integrity": "sha512-FIEG8Kum0wABZnktJvFi1xuVPc31xrunhZwLCvjrCGISQOm0ifyo7cjqf6PHiEeqoWMa5HIGOsB+lGM4aKCseA==",
"license": "MIT",
"dependencies": {
"@actions/core": "^2.0.0",
"@actions/github": "^6.0.1",
"@actions/http-client": "^3.0.0",
"@actions/http-client": "^3.0.2",
"@azure/storage-blob": "^12.29.1",
"@octokit/core": "^5.2.1",
"@octokit/plugin-request-log": "^1.0.4",
@@ -94,13 +95,13 @@
}
},
"node_modules/@actions/artifact/node_modules/@actions/http-client": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz",
"integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.2.tgz",
"integrity": "sha512-JP38FYYpyqvUsz+Igqlc/JG6YO9PaKuvqjM3iGvaLqFnJ7TFmcLyy2IDrY0bI0qCQug8E9K+elv5ZNfw62ZJzA==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^5.28.5"
"undici": "^6.23.0"
}
},
"node_modules/@actions/artifact/node_modules/@actions/io": {
@@ -134,6 +135,15 @@
"@octokit/openapi-types": "^12.11.0"
}
},
"node_modules/@actions/artifact/node_modules/undici": {
"version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"license": "MIT",
"engines": {
"node": ">=18.17"
}
},
"node_modules/@actions/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
@@ -169,9 +179,10 @@
}
},
"node_modules/@actions/http-client": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.0.tgz",
"integrity": "sha512-q+epW0trjVUUHboliPb4UF9g2msf+w61b32tAkFEwL/IwP0DQWgbCMM0Hbe3e3WXSKz5VcUXbzJQgy8Hkra/Lg==",
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^5.25.4"
@@ -3026,10 +3037,11 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -3163,12 +3175,12 @@
}
},
"node_modules/archiver-utils/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -4634,22 +4646,21 @@
}
},
"node_modules/eslint-plugin-github/node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz",
"integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
"integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "7.4.0",
"@typescript-eslint/type-utils": "7.4.0",
"@typescript-eslint/utils": "7.4.0",
"@typescript-eslint/visitor-keys": "7.4.0",
"debug": "^4.3.4",
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/type-utils": "7.18.0",
"@typescript-eslint/utils": "7.18.0",
"@typescript-eslint/visitor-keys": "7.18.0",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
"semver": "^7.5.4",
"ts-api-utils": "^1.0.1"
"ts-api-utils": "^1.3.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -4669,15 +4680,16 @@
}
},
"node_modules/eslint-plugin-github/node_modules/@typescript-eslint/parser": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz",
"integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.4.0",
"@typescript-eslint/types": "7.4.0",
"@typescript-eslint/typescript-estree": "7.4.0",
"@typescript-eslint/visitor-keys": "7.4.0",
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.18.0",
"@typescript-eslint/typescript-estree": "7.18.0",
"@typescript-eslint/visitor-keys": "7.18.0",
"debug": "^4.3.4"
},
"engines": {
@@ -4697,13 +4709,14 @@
}
},
"node_modules/eslint-plugin-github/node_modules/@typescript-eslint/scope-manager": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz",
"integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
"integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.4.0",
"@typescript-eslint/visitor-keys": "7.4.0"
"@typescript-eslint/types": "7.18.0",
"@typescript-eslint/visitor-keys": "7.18.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -4714,15 +4727,16 @@
}
},
"node_modules/eslint-plugin-github/node_modules/@typescript-eslint/type-utils": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz",
"integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
"integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.4.0",
"@typescript-eslint/utils": "7.4.0",
"@typescript-eslint/typescript-estree": "7.18.0",
"@typescript-eslint/utils": "7.18.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
"ts-api-utils": "^1.3.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -4741,10 +4755,11 @@
}
},
"node_modules/eslint-plugin-github/node_modules/@typescript-eslint/types": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz",
"integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
"integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || >=20.0.0"
},
@@ -4754,19 +4769,20 @@
}
},
"node_modules/eslint-plugin-github/node_modules/@typescript-eslint/typescript-estree": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz",
"integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
"integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.4.0",
"@typescript-eslint/visitor-keys": "7.4.0",
"@typescript-eslint/types": "7.18.0",
"@typescript-eslint/visitor-keys": "7.18.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"minimatch": "9.0.3",
"semver": "^7.5.4",
"ts-api-utils": "^1.0.1"
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -4782,18 +4798,16 @@
}
},
"node_modules/eslint-plugin-github/node_modules/@typescript-eslint/utils": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz",
"integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
"integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "7.4.0",
"@typescript-eslint/types": "7.4.0",
"@typescript-eslint/typescript-estree": "7.4.0",
"semver": "^7.5.4"
"@typescript-eslint/scope-manager": "7.18.0",
"@typescript-eslint/types": "7.18.0",
"@typescript-eslint/typescript-estree": "7.18.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -4807,13 +4821,14 @@
}
},
"node_modules/eslint-plugin-github/node_modules/@typescript-eslint/visitor-keys": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz",
"integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==",
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
"integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.4.0",
"eslint-visitor-keys": "^3.4.1"
"@typescript-eslint/types": "7.18.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -4834,12 +4849,13 @@
}
},
"node_modules/eslint-plugin-github/node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -5251,10 +5267,22 @@
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true
},
"node_modules/fast-xml-builder": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz",
"integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/NaturalIntelligence"
}
],
"license": "MIT"
},
"node_modules/fast-xml-parser": {
"version": "5.3.6",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz",
"integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==",
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz",
"integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==",
"funding": [
{
"type": "github",
@@ -5263,6 +5291,7 @@
],
"license": "MIT",
"dependencies": {
"fast-xml-builder": "^1.0.0",
"strnum": "^2.1.2"
},
"bin": {
@@ -5826,10 +5855,11 @@
"license": "BSD-3-Clause"
},
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
@@ -7151,9 +7181,9 @@
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT"
},
"node_modules/lodash.camelcase": {
@@ -7319,9 +7349,10 @@
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -8046,9 +8077,9 @@
}
},
"node_modules/readdir-glob/node_modules/minimatch": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
"version": "5.1.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz",
"integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==",
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -8285,9 +8316,9 @@
}
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -8799,12 +8830,13 @@
}
},
"node_modules/ts-api-utils": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz",
"integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==",
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
"integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=16.13.0"
"node": ">=16"
},
"peerDependencies": {
"typescript": ">=4.2.0"
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dependency-review-action",
"version": "4.8.3",
"version": "4.9.0",
"private": true,
"description": "A GitHub Action for Dependency Review",
"main": "lib/main.js",
@@ -36,6 +36,7 @@
"got": "^14.4.7",
"jest": "^29.7.0",
"octokit": "^3.1.2",
"semver": "^7.7.4",
"spdx-expression-parse": "^4.0.0",
"spdx-satisfies": "^6.0.0",
"ts-jest": "^29.4.1",
+3 -2
View File
@@ -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 = {
@@ -130,7 +131,7 @@ async function createSummary(
scorecard,
config
)
summary.addChangeVulnerabilitiesToSummary(
await summary.addChangeVulnerabilitiesToSummary(
vulnerabilities,
config.fail_on_severity
)
+3 -1
View File
@@ -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(
+10 -3
View File
@@ -186,8 +186,11 @@ async function run(): Promise<void> {
)
// generate informational scorecard entries for all added changes in the PR
const scorecardChanges = getScorecardChanges(changes)
const scorecard = await getScorecardLevels(scorecardChanges)
let scorecard: Scorecard = {dependencies: []}
if (config.show_openssf_scorecard) {
const scorecardChanges = getScorecardChanges(changes)
scorecard = await getScorecardLevels(scorecardChanges)
}
const minSummary = summary.addSummaryToSummary(
vulnerableChanges,
@@ -205,7 +208,11 @@ 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,
config.show_patched_versions
)
issueFound ||= await printVulnerabilitiesBlock(
vulnerableChanges,
minSeverity,
+1
View File
@@ -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(
+317 -19
View File
@@ -2,7 +2,14 @@ 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,
isEnterprise
} from './utils'
import * as semver from 'semver'
const icons = {
check: '✅',
@@ -11,6 +18,109 @@ const icons = {
}
const MAX_SCANNED_FILES_BYTES = 1048576
const API_CONCURRENCY_LIMIT = 10 // Limit concurrent API requests to avoid rate limiting
/**
* Helper to check if a version falls within a vulnerable range.
* Uses the `semver` library for proper prerelease handling and range parsing.
*
* @param version - The version to check (can be pre-trimmed).
* @param range - The version range to check against (can be pre-trimmed and/or pre-normalized).
* @param options - Configuration options.
* @param options.preTrimmed - If true, assumes inputs are already trimmed (optimization).
* @param options.preNormalized - If true, assumes range is already normalized (comma-to-space conversion done).
* @param options.failClosed - If true, returns true (vulnerable) on errors; if false, returns false (no match).
* @returns `true` if the version is considered within the vulnerable range (or on fail-closed), otherwise `false`.
*/
function versionInRange(
version: string | undefined,
range: string | undefined,
options: {
preTrimmed?: boolean
preNormalized?: boolean
failClosed?: boolean
} = {}
): boolean {
const {preTrimmed = false, preNormalized = false, failClosed = true} = options
// Trim inputs if not pre-trimmed
const trimmedVersion = preTrimmed ? version : version?.trim() || ''
const trimmedRange = preTrimmed ? range : range?.trim() || ''
if (!trimmedVersion) {
if (failClosed) {
core.debug(
`Empty or missing version for range "${range}". Treating as vulnerable (fail closed).`
)
}
return failClosed
}
if (!trimmedRange) {
if (failClosed) {
core.debug(
`Empty or missing version range for version "${version}". Treating as vulnerable (fail closed).`
)
}
return failClosed
}
// Convert GitHub API range format to semver-compatible format if not already normalized
// GitHub uses: ">= 8.0.0, <= 8.0.20"
// Semver accepts: ">= 8.0.0 <= 8.0.20" (operators may be followed by a space)
const semverRange = preNormalized
? trimmedRange
: trimmedRange.replace(/,\s*/g, ' ')
// Validate version and range explicitly to enforce fail-closed semantics
// semver.satisfies() typically returns false for invalid inputs without throwing
let validVersion = semver.valid(trimmedVersion)
const validRange = semver.validRange(semverRange)
// For fail-open mode (patch selection), try coercing invalid versions
// to handle common real-world formats like "8.0", date-based versions, etc.
if (!validVersion && !failClosed) {
const coerced = semver.coerce(trimmedVersion)
if (coerced) {
validVersion = coerced.version
core.debug(
`Coerced version "${trimmedVersion}" to "${validVersion}" for range matching`
)
}
}
if (!validVersion || !validRange) {
if (failClosed) {
const issues: string[] = []
if (!validVersion) issues.push('version')
if (!validRange) issues.push('version range')
core.debug(
`Invalid ${issues.join(' and ')}: version="${version}", range="${range}". Treating as vulnerable (fail closed).`
)
}
return failClosed
}
// Both version and range are valid; perform the satisfies check
// Only include prereleases when the version being checked is itself a prerelease
// to avoid changing range semantics globally
const isPrerelease = semver.prerelease(validVersion) !== null
return semver.satisfies(validVersion, validRange, {
includePrerelease: isPrerelease
})
}
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
@@ -132,21 +242,142 @@ function countScorecardWarnings(
)
}
export function addChangeVulnerabilitiesToSummary(
/**
* Execute promises with a concurrency limit to avoid overwhelming APIs.
* @param tasks - Array of functions that return promises
* @param limit - Maximum number of concurrent promises
*/
async function promisePool(
tasks: (() => Promise<void>)[],
limit: number
): Promise<void> {
const executing: Set<Promise<void>> = new Set()
for (const task of tasks) {
// Execute task and clean up
const wrappedPromise = (async () => {
await task()
})()
executing.add(wrappedPromise)
// When promise completes, remove it from the executing set
wrappedPromise.finally(() => {
executing.delete(wrappedPromise)
})
// Wait if we've hit the concurrency limit
if (executing.size >= limit) {
await Promise.race(executing)
}
}
// Wait for all remaining promises
await Promise.all(executing)
}
export async function addChangeVulnerabilitiesToSummary(
vulnerableChanges: Changes,
severity: string
): void {
severity: string,
showPatchedVersions = false
): 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>()
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)
}
}
}
}
// Query GitHub API for patch info with concurrency limiting
// Store all vulnerability entries (may be multiple per package with different ranges)
// Pre-normalize ecosystem, package name, and range to avoid repeated work in rendering
const patchInfo: Record<
string,
{
eco: string
pkg: string
range: string
patch: string
ecoLower: string
pkgLower: string
normalizedRange: string
}[]
> = {}
const apiClient = octokitClient()
// Create tasks for promise pool
const tasks = Array.from(advisorySet).map(advId => async () => {
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) {
// Pre-normalize and cache values to avoid repeated work in rendering loop
const trimmedRange = vulnRange.trim()
const normalizedRange = trimmedRange.replace(/,\s*/g, ' ')
patchInfo[advId].push({
eco: normalizedEco,
pkg: pkgName,
range: vulnRange,
patch: patchVerId,
ecoLower: normalizedEco, // Ecosystem already normalized to lowercase
pkgLower: pkgName.toLowerCase(),
normalizedRange
})
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) {
const errorMessage = e instanceof Error ? e.message : String(e)
core.debug(`API call failed for ${advId}: ${errorMessage}`)
patchInfo[advId] = []
}
})
// Execute API calls with concurrency limit
await promisePool(tasks, API_CONCURRENCY_LIMIT)
core.summary.addHeading('Vulnerabilities', 2)
for (const manifest of manifests) {
// Create fresh rows array for each manifest to avoid accumulation
const rows: SummaryTableRow[] = []
for (const change of vulnerableChanges.filter(
pkg => pkg.manifest === manifest
)) {
@@ -157,33 +388,100 @@ 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 advisoryEntries = patchInfo[vuln.advisory_ghsa_id]
if (advisoryEntries && advisoryEntries.length > 0) {
const ecoLowercase = change.ecosystem.toLowerCase()
const packageLowercase = change.name.toLowerCase()
const normalizedChangeVersion = change.version.trim()
core.debug(
`Looking up patch for ${change.name}@${change.version} (${ecoLowercase}) in ${vuln.advisory_ghsa_id}`
)
// Find matching entry by ecosystem, package name (case-insensitive), and version range
// Use pre-normalized values from cache to avoid repeated lowercasing and range conversion
let foundEntry:
| {eco: string; pkg: string; range: string; patch: string}
| undefined
for (const vulnEntry of advisoryEntries) {
if (vulnEntry.ecoLower !== ecoLowercase) continue
if (vulnEntry.pkgLower !== packageLowercase) continue
// Use fail-open (failClosed: false) for patch selection to avoid
// incorrectly matching on invalid ranges
// Use preTrimmed and preNormalized optimizations since we've done both
const isInRange = versionInRange(
normalizedChangeVersion,
vulnEntry.normalizedRange,
{preTrimmed: true, preNormalized: true, failClosed: false}
)
if (isInRange) {
foundEntry = vulnEntry
break
}
}
if (foundEntry) {
patchVer = foundEntry.patch
core.debug(
`Found patch version ${patchVer} for ${change.name}@${change.version}`
)
} else {
const maxLoggedEntries = 5
const entriesPreview = advisoryEntries
.slice(0, maxLoggedEntries)
.map(
entry =>
`${entry.eco}:${entry.pkg} ${entry.range} -> ${entry.patch}`
)
core.debug(
`No matching patch found for ${change.name}@${change.version}. Available entries (showing up to ${Math.min(advisoryEntries.length, maxLoggedEntries)} of ${advisoryEntries.length}): ${entriesPreview.join('; ')}`
)
}
} else {
core.debug(`No advisory data available for ${vuln.advisory_ghsa_id}`)
}
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
])
]
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
])
]
if (showPatchedVersions) {
row.push(patchVer)
}
rows.push(row)
}
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}
],
...rows
])
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([headerRow, ...rows])
}
if (severity !== 'low') {
+1 -1
View File
@@ -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'
)