Compare commits

...

90 Commits

Author SHA1 Message Date
Justin Hutchings 9a82545adb Remove Ruby from devcontainer 2024-03-21 11:28:18 -07:00
Federico Builes 0fa40c3c10 bumping to 4.2.3. 2024-03-20 17:57:26 +01:00
Federico Builes 1f6240f54c Merge pull request #707 from laughedelic/feat/data-outputs
Add outputs for the changes data
2024-03-20 17:47:40 +01:00
Federico Builes b751d41e7e Merge pull request #702 from actions/dependabot/npm_and_yarn/nodemon-3.1.0
Bump nodemon from 3.0.3 to 3.1.0
2024-03-20 06:48:20 +01:00
Federico Builes 6183eb9d2b Merge pull request #703 from actions/dependabot/npm_and_yarn/eslint-plugin-jest-27.9.0
Bump eslint-plugin-jest from 27.6.3 to 27.9.0
2024-03-20 06:48:14 +01:00
laughedelic 6585cc5f01 fix run syntax 2024-03-19 21:23:25 +01:00
laughedelic 218a76cbd5 add clarification about output usage hygiene 2024-03-19 21:22:12 +01:00
laughedelic d78d095945 revert changes in CI 2024-03-19 19:48:45 +01:00
Federico Builes 36297aa214 Merge pull request #716 from actions/dependabot/npm_and_yarn/types/node-20.11.28
Bump @types/node from 20.11.19 to 20.11.28
2024-03-18 05:01:38 +01:00
dependabot[bot] 1e69a8c24a Bump @types/node from 20.11.19 to 20.11.28
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.11.19 to 20.11.28.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 02:01:11 +00:00
Jon Janego 2f10643938 Merge pull request #715 from actions/jonjanego-patch-2
Update README.md
2024-03-15 11:24:37 -05:00
Jon Janego 1eb83b5560 Update README.md
minor text clarifications to keep consistent with wording in (the docs)[https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/configuring-dependency-review]
2024-03-15 11:22:32 -05:00
laughedelic 47b7acc8e3 update dist 2024-03-15 03:22:15 +01:00
laughedelic 16bfb3360c Merge branch 'main' into feat/data-outputs 2024-03-15 03:21:07 +01:00
Alexey Alekhin bc6a1f0dd4 avoid using if: always() 2024-03-15 03:01:16 +01:00
Alexey Alekhin e38e634e54 Apply suggestions for docs
Co-authored-by: Federico Builes <febuiles@github.com>
2024-03-15 02:32:15 +01:00
Jon Janego 5574be321f Merge pull request #712 from actions/jonjanego-patch-1
Update stale.yaml and CONTRIBUTING.md
2024-03-14 15:23:18 -05:00
Jon Janego aef51371b7 Update CONTRIBUTING.md
explaining stalebot
2024-03-14 14:44:38 -05:00
Jon Janego 3b70c9966e Update stale.yaml
fixing some comments, adding a keep label for issues, explicitly defining labeling behavior for issues
2024-03-14 14:43:08 -05:00
Federico Builes adaed32746 Merge pull request #709 from jhutchings1/scorecard
Add support for calculating OpenSSF Scorecards
2024-03-14 06:30:21 +01:00
Justin Hutchings 4ce120135b Fix OpenSSF Scorecard display issue 2024-03-13 16:23:23 +00:00
Justin Hutchings 0e8bc32a54 Fix prettier linting 2024-03-12 22:06:54 +00:00
Justin Hutchings f875e6ec1d Simplify truthiness check 2024-03-12 21:49:01 +00:00
Justin Hutchings 72666694f0 Fix broken tests, clean up dead code 2024-03-12 21:32:27 +00:00
Justin Hutchings 7dc5f537be Add scorecard to summary and count scorecard warnings 2024-03-12 20:47:25 +00:00
Justin Hutchings ac600387ca Add tests 2024-03-12 17:55:10 +00:00
Justin Hutchings d186d663df Automatically collapse the scorecard table 2024-03-11 22:23:03 +00:00
Justin Hutchings b7fdb4c8e2 Remove unused import 2024-03-11 22:19:09 +00:00
Justin Hutchings ba6b805e18 Remove dead code, complete printScorecardBlock 2024-03-11 22:17:28 +00:00
Justin Hutchings 70801db78f Revert line number implementation 2024-03-11 22:05:19 +00:00
Justin Hutchings 5bc19761c5 Add debugging 2024-03-08 03:00:15 +00:00
Justin Hutchings 5ba0d0fe17 Add debugging 2024-03-08 02:53:41 +00:00
Justin Hutchings 6a74ebd41e Fix column number implementation 2024-03-08 02:49:15 +00:00
Justin Hutchings 250250e73d Refactor schema, add line numbers to warnings 2024-03-08 02:31:11 +00:00
Justin Hutchings cb0a0415fb Update dist 2024-03-08 01:30:46 +00:00
Justin Hutchings 296bf3ab1b Add docs, implement warning behavior 2024-03-08 01:29:53 +00:00
Justin Hutchings 59d4782b76 Add links to summary 2024-03-06 20:14:19 +00:00
Justin Hutchings e878bf8824 Fix bug with protocol prefixes 2024-03-06 20:06:26 +00:00
Justin Hutchings 1b21f392ca Fix scorecard bug 2024-03-06 19:44:54 +00:00
Justin Hutchings 111227a118 Refactor scorecard API implementation 2024-03-06 14:43:49 +00:00
Justin Hutchings a1258f2a2e Fix icon issues 2024-03-04 20:07:08 +00:00
Justin Hutchings 29b9ef447a Fix icons and undefined/null checks 2024-03-04 20:03:39 +00:00
Justin Hutchings b5a1aee21a Add debugging 2024-03-04 19:45:36 +00:00
Justin Hutchings b3d2872ac7 Update dist 2024-03-04 19:39:13 +00:00
Justin Hutchings 5bace73db3 Fix undefined/null checks 2024-03-04 19:38:52 +00:00
Justin Hutchings f8ebb4b946 Add formatting around warning for low scorecard levels 2024-03-04 19:34:29 +00:00
laughedelic 84b80e6e84 add checks for the outputs 2024-03-04 19:53:14 +01:00
Justin Hutchings 1251834b92 Add dependencies without scorecards to scorecards table 2024-03-04 18:51:52 +00:00
Justin Hutchings 94125c4b1e Fix formatting issues 2024-03-04 18:38:53 +00:00
Justin Hutchings 9843156266 Improve summary formatting 2024-03-04 18:28:43 +00:00
Justin Hutchings 2fcc6a1c72 Fix config implementation 2024-03-04 18:17:53 +00:00
Justin Hutchings ea64ae9d4d Fix config mapping issue 2024-03-04 18:11:46 +00:00
Justin Hutchings 5955069e69 Add debugging 2024-03-04 18:04:06 +00:00
laughedelic 05fcfa49e0 add a note about outputs size limit 2024-03-04 18:58:48 +01:00
laughedelic 75be7f0c0c clarify docs, add a usage example 2024-03-04 18:53:03 +01:00
Justin Hutchings 7d2e20d06d Stub out summary implementation for scorecards 2024-03-04 17:52:17 +00:00
Jon Janego 97f7ba06d0 Update CONTRIBUTING.md
some more examples of good contribution standards
2024-03-04 10:31:16 -06:00
Justin Hutchings 2bc3ecb19b Fix type issues 2024-03-03 06:50:11 +00:00
Justin Hutchings c286ea91b0 Add nullish to types 2024-03-03 06:08:47 +00:00
Justin Hutchings 6bcbf042ff Fix OpenSSF Scorecard Score retrieval 2024-03-03 05:59:37 +00:00
Justin Hutchings 43286afc54 Add debugging 2024-03-03 05:48:33 +00:00
Justin Hutchings 764e39e792 Attempt to fix type issues 2024-03-03 05:43:20 +00:00
Justin Hutchings bf2683a10c Fixing url error 2024-03-03 05:30:49 +00:00
Justin Hutchings f1b66d10c9 Remove dependency on PackageURL 2024-03-03 05:26:55 +00:00
Justin Hutchings ffd129c285 Refactor types, add printing 2024-03-03 05:24:07 +00:00
Justin Hutchings 72d5b06a68 Add more error handling 2024-03-03 01:34:03 +00:00
Justin Hutchings b2ddac1749 Remove custom type to go around errors 2024-03-03 01:27:54 +00:00
Justin Hutchings f357c751be fix extra slash 2024-03-03 01:21:27 +00:00
Justin Hutchings e16e218fdc Encode URI component to fix 404 2024-03-03 01:15:16 +00:00
Justin Hutchings f87cc241f7 Fixing bugs 2024-03-03 01:10:08 +00:00
Justin Hutchings d641d3a261 Fix bugs in scorecard 2024-03-03 01:05:36 +00:00
Justin Hutchings 4230610a70 Add exception handling 2024-03-03 00:46:24 +00:00
Justin Hutchings af5438b06f Add debugging 2024-03-03 00:31:13 +00:00
Justin Hutchings 781bff117a Fix prettier issues, and JSON error in main 2024-03-03 00:25:40 +00:00
Justin Hutchings 7a8ce509c9 Update dist 2024-03-03 00:17:26 +00:00
Justin Hutchings f419e37c19 Fix build breaks 2024-03-03 00:15:42 +00:00
Justin Hutchings 3d70a3cf05 Draft integration with deps.dev 2024-03-02 22:37:50 +00:00
laughedelic e7aef164a1 chore: build and package 2024-03-02 05:10:30 +01:00
laughedelic ab8c3848de docs: update readme 2024-03-02 05:09:49 +01:00
laughedelic eecc9aab88 feat: add action outputs for different types of changes 2024-03-02 04:55:58 +01:00
dependabot[bot] 7f632dbe1f Bump eslint-plugin-jest from 27.6.3 to 27.9.0
Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 27.6.3 to 27.9.0.
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v27.6.3...v27.9.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-jest
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-26 01:35:46 +00:00
dependabot[bot] ba18fafa8d Bump nodemon from 3.0.3 to 3.1.0
Bumps [nodemon](https://github.com/remy/nodemon) from 3.0.3 to 3.1.0.
- [Release notes](https://github.com/remy/nodemon/releases)
- [Commits](https://github.com/remy/nodemon/compare/v3.0.3...v3.1.0)

---
updated-dependencies:
- dependency-name: nodemon
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-26 01:35:37 +00:00
Federico Builes 40eb2b8b00 Merge pull request #698 from jsoref/set-comment-as-output
Set comment as output
2024-02-21 12:36:50 +01:00
Josh Soref db6a5119ce Set comment as output
Move the logic from the caller as a negation instead of trying to
negate it by hand.
2024-02-20 14:19:19 -05:00
Federico Builes 9129d7d40b don't set output on every run 2024-02-20 18:47:36 +01:00
Jon Janego a1be843151 Update stale.yaml
Adding stale checks to issues
2024-02-20 10:25:09 -06:00
Federico Builes 587ff57efd Don't use if: always() in examples. 2024-02-19 18:11:35 +01:00
Federico Builes be8bc500ee Merge branch 'output-comment' 2024-02-19 17:26:04 +01:00
Federico Builes cb180bf383 Merge pull request #696 from actions/output-comment
Expose dependency comment content
2024-02-19 17:23:55 +01:00
Federico Builes b2ea187fd2 bumping action version 2024-02-19 17:21:55 +01:00
19 changed files with 794 additions and 72 deletions
+1 -4
View File
@@ -2,8 +2,5 @@
"name": "Dependency Review Action",
"image": "mcr.microsoft.com/devcontainers/typescript-node:18",
"postCreateCommand": "npm install",
"remoteUser": "node",
"features": {
"ghcr.io/devcontainers/features/ruby:1": {}
}
"remoteUser": "node"
}
+8 -5
View File
@@ -1,6 +1,7 @@
name: Close stale PRs
name: Close stale PRs and Issues
permissions:
issues: write
pull-requests: write
on:
@@ -12,14 +13,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9.0.0
name: Clean up stale PRs
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. You can: comment on the issue or remove the stale label to hold stale 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 stale bot. Please see CONTRIBUTING.md for more policy details."
stale-pr-label: "Stale"
stale-issue-label: "Stale"
exempt-pr-labels: "Keep" # a "Keep" label will keep the PR from being closed as stale
exempt-issue-labels: "Keep" # a "Keep" label will keep the issue from being closed as stale
days-before-pr-stale: 180 # when the PR is considered stale
days-before-pr-close: 15 # when the PR is closed by the bot,
days-before-issue-stale: -1 # prevents issues from being tagged by the bot
days-before-issue-close: -1 # prevents issues from being closed by the bot
days-before-pr-close: 15 # when the PR is closed by the bot
days-before-issue-stale: 180 # when the issue is considered stale
days-before-issue-close: 15 # when the issue is closed by the bot
exempt-assignees: 'advanced-security-dependency-graph'
ascending: true
+7 -1
View File
@@ -77,9 +77,11 @@ $ GITHUB_TOKEN=<token> ./scripts/scan_pr --config-file my_custom_config.yml <pr_
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
- Write tests.
- Add unit tests for new features.
- Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Write a [good commit message](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
- Add examples of the usage to [examples.md](examples.md)
- Link to a sample PR in a custom repository running your version of the Action.
## Cutting a new release
@@ -116,6 +118,10 @@ git tag -fa v4 -m "Updating v4 to 4.0.1"
git push origin v4 --force
```
## Stalebot
We have begun using a [Stalebot action](https://github.com/actions/stale) to help keep the Issues and Pull requests backlogs tidy. You can see the configuration [here](.github/workflows/stalebot.yml). If you'd like to keep an issue open after getting a stalebot warning, simply comment on it and it'll reset the clock.
## Resources
- [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/)
+24 -3
View File
@@ -77,12 +77,14 @@ Configure this action by either inlining these options in your workflow file, or
| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` |
| `allow-dependencies-licenses`\* | Contains a list of packages that will be excluded from license checks. | Any package(s) in [purl](https://github.com/package-url/purl-spec) format | none |
| `base-ref`/`head-ref` | Provide custom git references for the git base/head when performing the comparison check. This is only used for event types other than `pull_request` and `pull_request_target`. | Any valid git ref(s) in your project | none |
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job permission `pull-requests: write`. | `always`, `on-failure`, `never` | `never` |
| `comment-summary-in-pr` | Enable or disable reporting the review summary as a comment in the pull request. If enabled, you must give the workflow or job the `pull-requests: write` permission. | `always`, `on-failure`, `never` | `never` |
| `deny-packages` | Any number of packages to block in a PR. | Package(s) in [purl](https://github.com/package-url/purl-spec) format | empty |
| `deny-groups` | Any number of groups (namespaces) to block in a PR. | Namespace(s) in [purl](https://github.com/package-url/purl-spec) format (no package name, no version number) | empty |
| `retry-on-snapshot-warnings`\* | Enable or disable retrying the action every 10 seconds while waiting for dependency submission actions to complete. | `true`, `false` | `false` |
| `retry-on-snapshot-warnings-timeout`\* | Maximum amount of time (in seconds) to retry the action while waiting for dependency submission actions to complete. | Any positive integer | 120 |
| `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-levels` | 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 |
\*not supported for use with GitHub Enterprise Server
@@ -150,7 +152,7 @@ For more examples of how to use this action and its configuration options, see t
### Considerations
- Checking for licenses is not supported on Enterprise Server as the API does not return license information.
- The action will only accept one of the two `license` parameters; an error will be raised if you provide both.
- The `allow-licenses` and `deny-licenses` options are mutually exclusive; an error will be raised if you provide both.
- We don't have license information for all of your dependents. If we can't detect the license for a dependency **we will inform you, but the action won't fail**.
## Blocking pull requests
@@ -159,7 +161,26 @@ The Dependency Review GitHub Action check will only block a pull request from be
## Outputs
`comment-content` is generated with the same content as would be present in a Dependency Review Action comment.
- `comment-content` is generated with the same content as would be present in a Dependency Review Action comment.
- `dependency-changes` holds all dependency changes in a JSON format. The following outputs are subsets of `dependency-changes` filtered based on the configuration:
- `vulnerable-changes` holds information about dependency changes with vulnerable dependencies in a JSON format.
- `invalid-license-changes` holds information about invalid or non-compliant license dependency changes in a JSON format.
- `denied-changes` holds information about denied dependency changes in a JSON format.
> [!NOTE]
> Action outputs are unicode strings [with a 1MB size limit](https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#outputs-for-docker-container-and-javascript-actions).
> [!IMPORTANT]
> If you use these outputs in a run-step, you must store the ouput data in an envrioment variable instead of using the output directly. Using an output directly might break shell scripts. For example:
>
> ```yaml
> env:
> VULNERABLE_CHANGES: ${{ steps.review.outputs.vulnerable-changes }}
> run: |
> echo "$VULNERABLE_CHANGES" | jq
> ```
>
> instead of direct `echo '${{ steps.review.outputs.vulnerable-changes }}'`. See [examples](docs/examples.md) for more.
## Getting help
+40
View File
@@ -0,0 +1,40 @@
import {expect, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
import {getScorecardLevels, getProjectUrl} from '../src/scorecard'
const npmChange: Change = {
manifest: 'package.json',
change_type: 'added',
ecosystem: 'npm',
name: 'type-is',
version: '1.6.18',
package_url: 'pkg:npm/type-is@1.6.18',
license: 'MIT',
source_repository_url: 'github.com/jshttp/type-is',
scope: 'runtime',
vulnerabilities: [
{
severity: 'critical',
advisory_ghsa_id: 'first-random_string',
advisory_summary: 'very dangerous',
advisory_url: 'github.com/future-funk'
}
]
}
test('Get scorecard from API', async () => {
const changes: Changes = [npmChange]
const scorecard = await getScorecardLevels(changes)
expect(scorecard).not.toBeNull()
expect(scorecard.dependencies).toHaveLength(1)
expect(scorecard.dependencies[0].scorecard?.score).toBeGreaterThan(0)
})
test('Get project URL from deps.dev API', async () => {
const result = await getProjectUrl(
npmChange.ecosystem,
npmChange.name,
npmChange.version
)
expect(result).not.toBeNull()
})
+36 -2
View File
@@ -1,5 +1,5 @@
import {expect, jest, test} from '@jest/globals'
import {Changes, ConfigurationOptions} from '../src/schemas'
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'
@@ -16,6 +16,9 @@ const emptyInvalidLicenseChanges = {
unresolved: [],
unlicensed: []
}
const emptyScorecard: Scorecard = {
dependencies: []
}
const defaultConfig: ConfigurationOptions = {
vulnerability_check: true,
license_check: true,
@@ -29,7 +32,9 @@ const defaultConfig: ConfigurationOptions = {
comment_summary_in_pr: true,
retry_on_snapshot_warnings: false,
retry_on_snapshot_warnings_timeout: 120,
warn_only: false
warn_only: false,
warn_on_openssf_scorecard_level: 3,
show_openssf_scorecard: false
}
const changesWithEmptyManifests: Changes = [
@@ -71,11 +76,32 @@ const changesWithEmptyManifests: Changes = [
}
]
const scorecard: Scorecard = {
dependencies: [
{
change: {
change_type: 'added',
manifest: '',
ecosystem: 'unknown',
name: 'castore',
version: '0.1.17',
package_url: 'pkg:hex/castore@0.1.17',
license: null,
source_repository_url: null,
scope: 'runtime',
vulnerabilities: []
},
scorecard: null
}
]
}
test('prints headline as h1', () => {
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
scorecard,
defaultConfig
)
const text = core.summary.stringify()
@@ -88,6 +114,7 @@ test('only includes "No vulnerabilities or license issues found"-message if both
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
emptyScorecard,
defaultConfig
)
const text = core.summary.stringify()
@@ -101,6 +128,7 @@ test('only includes "No vulnerabilities found"-message if "license_check" is set
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
emptyScorecard,
config
)
const text = core.summary.stringify()
@@ -114,6 +142,7 @@ test('only includes "No license issues found"-message if "vulnerability_check" i
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
emptyScorecard,
config
)
const text = core.summary.stringify()
@@ -126,6 +155,7 @@ test('groups dependencies with empty manifest paths together', () => {
changesWithEmptyManifests,
emptyInvalidLicenseChanges,
emptyChanges,
emptyScorecard,
defaultConfig
)
summary.addScannedDependencies(changesWithEmptyManifests)
@@ -143,6 +173,7 @@ test('does not include status section if nothing was found', () => {
emptyChanges,
emptyInvalidLicenseChanges,
emptyChanges,
emptyScorecard,
defaultConfig
)
const text = core.summary.stringify()
@@ -165,6 +196,7 @@ test('includes count and status icons for all findings', () => {
vulnerabilities,
licenseIssues,
emptyChanges,
emptyScorecard,
defaultConfig
)
@@ -184,6 +216,7 @@ test('uses checkmarks for license issues if only vulnerabilities were found', ()
vulnerabilities,
emptyInvalidLicenseChanges,
emptyChanges,
emptyScorecard,
defaultConfig
)
@@ -207,6 +240,7 @@ test('uses checkmarks for vulnerabilities if only license issues were found', ()
emptyChanges,
licenseIssues,
emptyChanges,
emptyScorecard,
defaultConfig
)
+16
View File
@@ -65,9 +65,25 @@ inputs:
description: When set to `true` this action will always complete with success, overriding the `fail-on-severity` parameter.
required: false
default: false
show-openssf-scorecard:
description: Show a summary of the OpenSSF Scorecard scores.
required: false
default: true
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
default: 3
outputs:
comment-content:
description: Prepared dependency report comment
dependency-changes:
description: All dependency changes (JSON)
vulnerable-changes:
description: Vulnerable dependency changes (JSON)
invalid-license-changes:
description: Invalid license dependency changes (JSON)
denied-changes:
description: Denied dependency changes (JSON)
runs:
using: 'node20'
Generated Vendored
+307 -17
View File
@@ -60,9 +60,9 @@ function commentPr(summary, config) {
return __awaiter(this, void 0, void 0, function* () {
const commentContent = summary.stringify();
core.setOutput('comment-content', commentContent);
if (config.comment_summary_in_pr !== 'always' &&
config.comment_summary_in_pr === 'on-failure' &&
process.exitCode !== core.ExitCode.Failure) {
if (!(config.comment_summary_in_pr === 'always' ||
(config.comment_summary_in_pr === 'on-failure' &&
process.exitCode === core.ExitCode.Failure))) {
return;
}
if (!github.context.payload.pull_request) {
@@ -565,6 +565,7 @@ const request_error_1 = __nccwpck_require__(537);
const config_1 = __nccwpck_require__(6373);
const filter_1 = __nccwpck_require__(134);
const licenses_1 = __nccwpck_require__(3247);
const scorecard_1 = __nccwpck_require__(307);
const summary = __importStar(__nccwpck_require__(8608));
const git_refs_1 = __nccwpck_require__(1086);
const utils_1 = __nccwpck_require__(918);
@@ -635,22 +636,32 @@ function run() {
core.debug(`Filtered Changes: ${JSON.stringify(filteredChanges)}`);
core.debug(`Config Deny Packages: ${JSON.stringify(config)}`);
const deniedChanges = yield (0, deny_1.getDeniedChanges)(filteredChanges, config.deny_packages, config.deny_groups);
summary.addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, config);
const scorecard = yield (0, scorecard_1.getScorecardLevels)(filteredChanges);
summary.addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, scorecard, config);
if (snapshot_warnings) {
summary.addSnapshotWarnings(config, snapshot_warnings);
}
if (config.vulnerability_check) {
core.setOutput('vulnerable-changes', JSON.stringify(vulnerableChanges));
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity);
printVulnerabilitiesBlock(vulnerableChanges, minSeverity, warnOnly);
}
if (config.license_check) {
core.setOutput('invalid-license-changes', JSON.stringify(invalidLicenseChanges));
summary.addLicensesToSummary(invalidLicenseChanges, config);
printLicensesBlock(invalidLicenseChanges, warnOnly);
}
if (config.deny_packages || config.deny_groups) {
core.setOutput('denied-changes', JSON.stringify(deniedChanges));
summary.addDeniedToSummary(deniedChanges);
printDeniedDependencies(deniedChanges, config);
}
if (config.show_openssf_scorecard) {
summary.addScorecardToSummary(scorecard, config);
printScorecardBlock(scorecard, config);
createScorecardWarnings(scorecard, config);
}
core.setOutput('dependency-changes', JSON.stringify(changes));
summary.addScannedDependencies(changes);
printScannedDependencies(changes);
yield (0, comment_pr_1.commentPr)(core.summary, config);
@@ -740,6 +751,20 @@ function printNullLicenses(changes) {
core.info(`${ansi_styles_1.default.bold.open}${change.manifest} » ${change.name}@${change.version}${ansi_styles_1.default.bold.close}`);
}
}
function printScorecardBlock(scorecard, config) {
core.group('Scorecard', () => __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d;
if (scorecard) {
for (const dependency of scorecard.dependencies) {
if (((_a = dependency.scorecard) === null || _a === void 0 ? void 0 : _a.score) &&
((_b = dependency.scorecard) === null || _b === void 0 ? void 0 : _b.score) < config.warn_on_openssf_scorecard_level) {
core.info(`${ansi_styles_1.default.color.red.open}${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${(_c = dependency === null || dependency === void 0 ? void 0 : dependency.scorecard) === null || _c === void 0 ? void 0 : _c.score}${ansi_styles_1.default.red.close}`);
}
core.info(`${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${(_d = dependency === null || dependency === void 0 ? void 0 : dependency.scorecard) === null || _d === void 0 ? void 0 : _d.score}`);
}
}
}));
}
function renderSeverity(severity) {
const color = {
critical: 'red',
@@ -787,6 +812,20 @@ function printDeniedDependencies(changes, config) {
}
}));
}
function createScorecardWarnings(scorecards, config) {
var _a, _b, _c;
return __awaiter(this, void 0, void 0, function* () {
// Iterate through the list of scorecards, and if the score is less than the threshold, send a warning
for (const dependency of scorecards.dependencies) {
if (((_a = dependency.scorecard) === null || _a === void 0 ? void 0 : _a.score) &&
((_b = dependency.scorecard) === null || _b === void 0 ? void 0 : _b.score) < config.warn_on_openssf_scorecard_level) {
core.warning(`${dependency.change.ecosystem}/${dependency.change.name} has an OpenSSF Scorecard of ${(_c = dependency.scorecard) === null || _c === void 0 ? void 0 : _c.score}, which is less than this repository's threshold of ${config.warn_on_openssf_scorecard_level}.`, {
title: 'OpenSSF Scorecard Warning'
});
}
}
});
}
run();
@@ -821,7 +860,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0;
exports.ScorecardSchema = exports.ScorecardApiSchema = exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0;
const z = __importStar(__nccwpck_require__(3301));
exports.SEVERITIES = ['critical', 'high', 'moderate', 'low'];
exports.SCOPES = ['unknown', 'runtime', 'development'];
@@ -868,6 +907,8 @@ exports.ConfigurationOptionsSchema = z
head_ref: z.string().optional(),
retry_on_snapshot_warnings: z.boolean().default(false),
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),
comment_summary_in_pr: z
.union([
z.preprocess(val => (val === 'true' ? true : val === 'false' ? false : val), z.boolean()),
@@ -911,6 +952,152 @@ exports.ComparisonResponseSchema = z.object({
changes: z.array(exports.ChangeSchema),
snapshot_warnings: z.string()
});
exports.ScorecardApiSchema = z.object({
date: z.string(),
repo: z
.object({
name: z.string(),
commit: z.string()
})
.nullish(),
scorecard: z
.object({
version: z.string(),
commit: z.string()
})
.nullish(),
checks: z
.array(z.object({
name: z.string(),
documentation: z.object({
shortDescription: z.string(),
url: z.string()
}),
score: z.string(),
reason: z.string(),
details: z.array(z.string())
}))
.nullish(),
score: z.number().nullish()
});
exports.ScorecardSchema = z.object({
dependencies: z.array(z.object({
change: exports.ChangeSchema,
scorecard: exports.ScorecardApiSchema.nullish()
}))
});
/***/ }),
/***/ 307:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
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.getProjectUrl = exports.getScorecardLevels = void 0;
const core = __importStar(__nccwpck_require__(2186));
function getScorecardLevels(changes) {
return __awaiter(this, void 0, void 0, function* () {
const data = { dependencies: [] };
for (const change of changes) {
const ecosystem = change.ecosystem;
const packageName = change.name;
const version = change.version;
//Get the project repository
let repositoryUrl = change.source_repository_url;
//If the repository_url includes the protocol, remove it
if (repositoryUrl === null || repositoryUrl === void 0 ? void 0 : repositoryUrl.startsWith('https://')) {
repositoryUrl = repositoryUrl.replace('https://', '');
}
// If GitHub API doesn't have the repository URL, query deps.dev for it.
if (repositoryUrl) {
// Call the deps.dev API to get the repository URL from there
repositoryUrl = yield getProjectUrl(ecosystem, packageName, version);
}
// Get the scorecard API response from the scorecards API
let scorecardApi = null;
if (repositoryUrl) {
try {
scorecardApi = yield getScorecard(repositoryUrl);
}
catch (error) {
core.debug(`Error querying for scorecard: ${error.message}`);
}
}
data.dependencies.push({
change,
scorecard: scorecardApi
});
}
return data;
});
}
exports.getScorecardLevels = getScorecardLevels;
function getScorecard(repositoryUrl) {
return __awaiter(this, void 0, void 0, function* () {
const apiRoot = 'https://api.securityscorecards.dev/';
let scorecardResponse = {};
const url = `${apiRoot}/projects/${repositoryUrl}`;
const response = yield fetch(url);
if (response.ok) {
scorecardResponse = yield response.json();
}
else {
core.debug(`Couldn't get scorecard data for ${repositoryUrl}`);
}
return scorecardResponse;
});
}
function getProjectUrl(ecosystem, packageName, version) {
return __awaiter(this, void 0, void 0, function* () {
core.debug(`Getting deps.dev data for ${packageName} ${version}`);
const depsDevAPIRoot = 'https://api.deps.dev';
const url = `${depsDevAPIRoot}/v3alpha/systems/${ecosystem}/packages/${packageName}/versions/${version}`;
const response = yield fetch(url);
if (response.ok) {
const data = yield response.json();
if (data.relatedProjects.length > 0) {
return data.relatedProjects[0].projectKey.id;
}
}
return '';
});
}
exports.getProjectUrl = getProjectUrl;
/***/ }),
@@ -944,7 +1131,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.addDeniedToSummary = exports.addSnapshotWarnings = exports.addScannedDependencies = exports.addLicensesToSummary = exports.addChangeVulnerabilitiesToSummary = exports.addSummaryToSummary = void 0;
exports.addDeniedToSummary = exports.addSnapshotWarnings = exports.addScorecardToSummary = exports.addScannedDependencies = exports.addLicensesToSummary = exports.addChangeVulnerabilitiesToSummary = exports.addSummaryToSummary = void 0;
const core = __importStar(__nccwpck_require__(2186));
const utils_1 = __nccwpck_require__(918);
const icons = {
@@ -952,19 +1139,24 @@ const icons = {
cross: '❌',
warning: '⚠️'
};
function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, config) {
function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedChanges, scorecard, config) {
const scorecardWarnings = countScorecardWarnings(scorecard, config);
const licenseIssues = countLicenseIssues(invalidLicenseChanges);
core.summary.addHeading('Dependency Review', 1);
if (vulnerableChanges.length === 0 &&
countLicenseIssues(invalidLicenseChanges) === 0 &&
deniedChanges.length === 0) {
if (!config.license_check) {
core.summary.addRaw(`${icons.check} No vulnerabilities found.`);
}
else if (!config.vulnerability_check) {
core.summary.addRaw(`${icons.check} No license issues found.`);
licenseIssues === 0 &&
deniedChanges.length === 0 &&
scorecardWarnings === 0) {
const issueTypes = [
config.vulnerability_check ? 'vulnerabilities' : '',
config.license_check ? 'license issues' : '',
config.show_openssf_scorecard ? 'OpenSSF Scorecard issues' : ''
];
if (issueTypes.filter(Boolean).length === 0) {
core.summary.addRaw(`${icons.check} No issues found.`);
}
else {
core.summary.addRaw(`${icons.check} No vulnerabilities or license issues found.`);
core.summary.addRaw(`${icons.check} No ${issueTypes.filter(Boolean).join(' or ')} found.`);
}
return;
}
@@ -987,11 +1179,26 @@ function addSummaryToSummary(vulnerableChanges, invalidLicenseChanges, deniedCha
? [
`${checkOrWarnIcon(deniedChanges.length)} ${deniedChanges.length} package(s) denied.`
]
: []),
...(config.show_openssf_scorecard && scorecardWarnings > 0
? [
`${checkOrWarnIcon(scorecardWarnings)} ${scorecardWarnings ? scorecardWarnings : 'No'} packages with OpenSSF Scorecard issues.`
]
: [])
])
.addRaw('See the Details below.');
}
exports.addSummaryToSummary = addSummaryToSummary;
function countScorecardWarnings(scorecard, config) {
return scorecard.dependencies.reduce((total, dependency) => {
var _a, _b;
return total +
(((_a = dependency.scorecard) === null || _a === void 0 ? void 0 : _a.score) &&
((_b = dependency.scorecard) === null || _b === void 0 ? void 0 : _b.score) < config.warn_on_openssf_scorecard_level
? 1
: 0);
}, 0);
}
function addChangeVulnerabilitiesToSummary(vulnerableChanges, severity) {
if (vulnerableChanges.length === 0) {
return;
@@ -1125,6 +1332,49 @@ function snapshotWarningRecommendation(config, warnings) {
}
return 'Re-running this action after a short time may resolve the issue.';
}
function addScorecardToSummary(scorecard, config) {
var _a, _b, _c, _d, _e, _f, _g, _h;
core.summary.addHeading('OpenSSF Scorecard', 2);
if (scorecard.dependencies.length > 10) {
core.summary.addRaw(`<details><summary>Scorecard details</summary>`, true);
}
core.summary.addRaw(`<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>`, true);
for (const dependency of scorecard.dependencies) {
core.debug('Adding scorecard to summary');
core.debug(`Overall score ${(_a = dependency.scorecard) === null || _a === void 0 ? void 0 : _a.score}`);
// Set the icon based on the overall score value
let overallIcon = '';
if ((_b = dependency.scorecard) === null || _b === void 0 ? void 0 : _b.score) {
overallIcon =
((_c = dependency.scorecard) === null || _c === void 0 ? void 0 : _c.score) < config.warn_on_openssf_scorecard_level
? ':warning:'
: ':green_circle:';
}
//Add a row for the dependency
core.summary.addRaw(`<tr><td>${dependency.change.source_repository_url ? `<a href="https://${dependency.change.source_repository_url}">` : ''} ${dependency.change.ecosystem}/${dependency.change.name} ${dependency.change.source_repository_url ? `</a>` : ''}</td><td>${dependency.change.version}</td>
<td>${overallIcon} ${((_d = dependency.scorecard) === null || _d === void 0 ? void 0 : _d.score) === undefined || ((_e = dependency.scorecard) === null || _e === void 0 ? void 0 : _e.score) === null ? 'Unknown' : (_f = dependency.scorecard) === null || _f === void 0 ? void 0 : _f.score}</td>`, false);
//Add details table in the last column
if (((_g = dependency.scorecard) === null || _g === void 0 ? void 0 : _g.checks) !== undefined) {
let detailsTable = '<table><tr><th>Check</th><th>Score</th><th>Reason</th></tr>';
for (const check of ((_h = dependency.scorecard) === null || _h === void 0 ? void 0 : _h.checks) || []) {
const icon = parseFloat(check.score) < config.warn_on_openssf_scorecard_level
? ':warning:'
: ':green_circle:';
detailsTable += `<tr><td>${check.name}</td><td>${icon} ${check.score}</td><td>${check.reason}</td></tr>`;
}
detailsTable += `</table>`;
core.summary.addRaw(`<td><details><summary>Details</summary>${detailsTable}</details></td></tr>`, true);
}
else {
core.summary.addRaw('<td>Unknown</td></tr>', true);
}
}
core.summary.addRaw(`</table>`);
if (scorecard.dependencies.length > 10) {
core.summary.addRaw(`</details>`);
}
}
exports.addScorecardToSummary = addScorecardToSummary;
function addSnapshotWarnings(config, warnings) {
core.summary.addHeading('Snapshot Warnings', 2);
core.summary.addQuote(`${icons.warning}: ${warnings}`);
@@ -49488,6 +49738,8 @@ function readInlineConfig() {
const retry_on_snapshot_warnings = getOptionalBoolean('retry-on-snapshot-warnings');
const retry_on_snapshot_warnings_timeout = getOptionalNumber('retry-on-snapshot-warnings-timeout');
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');
validatePURL(allow_dependencies_licenses);
validateLicenses('allow-licenses', allow_licenses);
validateLicenses('deny-licenses', deny_licenses);
@@ -49507,7 +49759,9 @@ function readInlineConfig() {
comment_summary_in_pr,
retry_on_snapshot_warnings,
retry_on_snapshot_warnings_timeout,
warn_only
warn_only,
show_openssf_scorecard,
warn_on_openssf_scorecard_level
};
return Object.fromEntries(Object.entries(keys).filter(([_, value]) => value !== undefined));
}
@@ -49758,7 +50012,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0;
exports.ScorecardSchema = exports.ScorecardApiSchema = exports.ComparisonResponseSchema = exports.ChangesSchema = exports.ConfigurationOptionsSchema = exports.PullRequestSchema = exports.ChangeSchema = exports.SeveritySchema = exports.SCOPES = exports.SEVERITIES = void 0;
const z = __importStar(__nccwpck_require__(3301));
exports.SEVERITIES = ['critical', 'high', 'moderate', 'low'];
exports.SCOPES = ['unknown', 'runtime', 'development'];
@@ -49805,6 +50059,8 @@ exports.ConfigurationOptionsSchema = z
head_ref: z.string().optional(),
retry_on_snapshot_warnings: z.boolean().default(false),
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),
comment_summary_in_pr: z
.union([
z.preprocess(val => (val === 'true' ? true : val === 'false' ? false : val), z.boolean()),
@@ -49848,6 +50104,40 @@ exports.ComparisonResponseSchema = z.object({
changes: z.array(exports.ChangeSchema),
snapshot_warnings: z.string()
});
exports.ScorecardApiSchema = z.object({
date: z.string(),
repo: z
.object({
name: z.string(),
commit: z.string()
})
.nullish(),
scorecard: z
.object({
version: z.string(),
commit: z.string()
})
.nullish(),
checks: z
.array(z.object({
name: z.string(),
documentation: z.object({
shortDescription: z.string(),
url: z.string()
}),
score: z.string(),
reason: z.string(),
details: z.array(z.string())
}))
.nullish(),
score: z.number().nullish()
});
exports.ScorecardSchema = z.object({
dependencies: z.array(z.object({
change: exports.ChangeSchema,
scorecard: exports.ScorecardApiSchema.nullish()
}))
});
/***/ }),
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+16 -6
View File
@@ -166,7 +166,8 @@ jobs:
## Getting the results of the action in a later step
Using the `comment-content` output you can get the results of the action in a workflow step.
- `comment-content` contains the output of the results comment for the entire run.
`dependency-changes`, `vulnerable-changes`, `invalid-license-changes` and `denied-changes` are all JSON objects that allow you to access individual sets of changes.
```yaml
name: 'Dependency Review'
@@ -189,12 +190,21 @@ jobs:
fail-on-severity: critical
deny-licenses: LGPL-2.0, BSD-2-Clause
- name: 'Report'
if: always() # make sure this step runs even if the previous failed
# make sure this step runs even if the previous failed
if: ${{ failure() && steps.review.conclusion == 'failure' }}
shell: bash
env:
comment: ${{ steps.review.outputs.comment-content }}
run: |
echo "$comment" # do something with the comment
env: # store comment HTML data in an environment variable
COMMENT: ${{ steps.review.outputs.comment-content }}
run: | # do something with the comment:
echo "$COMMENT"
- name: 'List vulnerable dependencies'
# make sure this step runs even if the previous failed
if: ${{ failure() && steps.review.conclusion == 'failure' }}
shell: bash
env: # store JSON data in an environment variable
VULNERABLE_CHANGES: ${{ steps.review.outputs.vulnerable-changes }}
run: | # do something with the JSON:
echo "$VULNERABLE_CHANGES" | jq '.[].package_url'
```
## Exclude dependencies from the license check
+14 -14
View File
@@ -1,12 +1,12 @@
{
"name": "dependency-review-action",
"version": "4.1.1",
"version": "4.2.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dependency-review-action",
"version": "4.1.1",
"version": "4.2.3",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.1",
@@ -35,10 +35,10 @@
"esbuild-register": "^3.5.0",
"eslint": "^8.56.0",
"eslint-plugin-github": "^4.10.1",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^5.1.3",
"js-yaml": "^4.1.0",
"nodemon": "^3.0.3",
"nodemon": "^3.1.0",
"prettier": "3.2.5",
"typescript": "^5.3.3"
}
@@ -2089,9 +2089,9 @@
}
},
"node_modules/@types/node": {
"version": "20.11.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
"integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
"version": "20.11.28",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.28.tgz",
"integrity": "sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -3811,9 +3811,9 @@
}
},
"node_modules/eslint-plugin-jest": {
"version": "27.6.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.3.tgz",
"integrity": "sha512-+YsJFVH6R+tOiO3gCJon5oqn4KWc+mDq2leudk8mrp8RFubLOo9CVyi3cib4L7XMpxExmkmBZQTPDYVBzgpgOA==",
"version": "27.9.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz",
"integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==",
"dev": true,
"dependencies": {
"@typescript-eslint/utils": "^5.10.0"
@@ -3822,7 +3822,7 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
},
"peerDependencies": {
"@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0",
"@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0",
"eslint": "^7.0.0 || ^8.0.0",
"jest": "*"
},
@@ -6144,9 +6144,9 @@
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
},
"node_modules/nodemon": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz",
"integrity": "sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz",
"integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==",
"dev": true,
"dependencies": {
"chokidar": "^3.5.2",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "dependency-review-action",
"version": "4.1.1",
"version": "4.2.3",
"private": true,
"description": "A GitHub Action for Dependency Review",
"main": "lib/main.js",
@@ -51,10 +51,10 @@
"esbuild-register": "^3.5.0",
"eslint": "^8.56.0",
"eslint-plugin-github": "^4.10.1",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^5.1.3",
"js-yaml": "^4.1.0",
"nodemon": "^3.0.3",
"nodemon": "^3.1.0",
"prettier": "3.2.5",
"typescript": "^5.3.3"
}
+31 -3
View File
@@ -6,7 +6,7 @@
* npx ts-node scripts/create_summary.ts
*/
import {Change, Changes, ConfigurationOptions} from '../src/schemas'
import {Change, Changes, ConfigurationOptions, Scorecard} from '../src/schemas'
import {createTestChange} from '../__tests__/fixtures/create-test-change'
import {InvalidLicenseChanges} from '../src/licenses'
import * as fs from 'fs'
@@ -33,7 +33,29 @@ const defaultConfig: ConfigurationOptions = {
comment_summary_in_pr: true,
retry_on_snapshot_warnings: false,
retry_on_snapshot_warnings_timeout: 120,
warn_only: false
warn_only: false,
warn_on_openssf_scorecard_level: 3,
show_openssf_scorecard: true
}
const scorecard: Scorecard = {
dependencies: [
{
change: {
change_type: 'added',
manifest: '',
ecosystem: 'unknown',
name: 'castore',
version: '0.1.17',
package_url: 'pkg:hex/castore@0.1.17',
license: null,
source_repository_url: null,
scope: 'runtime',
vulnerabilities: []
},
scorecard: null
}
]
}
const tmpDir = path.resolve(__dirname, '../tmp')
@@ -101,7 +123,13 @@ async function createSummary(
config: ConfigurationOptions,
fileName: string
): Promise<void> {
summary.addSummaryToSummary(vulnerabilities, licenseIssues, denied, config)
summary.addSummaryToSummary(
vulnerabilities,
licenseIssues,
denied,
scorecard,
config
)
summary.addChangeVulnerabilitiesToSummary(
vulnerabilities,
config.fail_on_severity
+5 -3
View File
@@ -22,9 +22,11 @@ export async function commentPr(
core.setOutput('comment-content', commentContent)
if (
config.comment_summary_in_pr !== 'always' &&
config.comment_summary_in_pr === 'on-failure' &&
process.exitCode !== core.ExitCode.Failure
!(
config.comment_summary_in_pr === 'always' ||
(config.comment_summary_in_pr === 'on-failure' &&
process.exitCode === core.ExitCode.Failure)
)
) {
return
}
+7 -1
View File
@@ -48,6 +48,10 @@ function readInlineConfig(): ConfigurationOptionsPartial {
'retry-on-snapshot-warnings-timeout'
)
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'
)
validatePURL(allow_dependencies_licenses)
validateLicenses('allow-licenses', allow_licenses)
@@ -69,7 +73,9 @@ function readInlineConfig(): ConfigurationOptionsPartial {
comment_summary_in_pr,
retry_on_snapshot_warnings,
retry_on_snapshot_warnings_timeout,
warn_only
warn_only,
show_openssf_scorecard,
warn_on_openssf_scorecard_level
}
return Object.fromEntries(
+66 -1
View File
@@ -3,7 +3,13 @@ import * as dependencyGraph from './dependency-graph'
import * as github from '@actions/github'
import styles from 'ansi-styles'
import {RequestError} from '@octokit/request-error'
import {Change, Severity, Changes, ConfigurationOptions} from './schemas'
import {
Change,
Severity,
Changes,
ConfigurationOptions,
Scorecard
} from './schemas'
import {readConfig} from '../src/config'
import {
filterChangesBySeverity,
@@ -11,6 +17,7 @@ import {
filterAllowedAdvisories
} from '../src/filter'
import {getInvalidLicenseChanges} from './licenses'
import {getScorecardLevels} from './scorecard'
import * as summary from './summary'
import {getRefs} from './git-refs'
@@ -118,10 +125,13 @@ async function run(): Promise<void> {
config.deny_groups
)
const scorecard = await getScorecardLevels(filteredChanges)
summary.addSummaryToSummary(
vulnerableChanges,
invalidLicenseChanges,
deniedChanges,
scorecard,
config
)
@@ -130,18 +140,30 @@ async function run(): Promise<void> {
}
if (config.vulnerability_check) {
core.setOutput('vulnerable-changes', JSON.stringify(vulnerableChanges))
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity)
printVulnerabilitiesBlock(vulnerableChanges, minSeverity, warnOnly)
}
if (config.license_check) {
core.setOutput(
'invalid-license-changes',
JSON.stringify(invalidLicenseChanges)
)
summary.addLicensesToSummary(invalidLicenseChanges, config)
printLicensesBlock(invalidLicenseChanges, warnOnly)
}
if (config.deny_packages || config.deny_groups) {
core.setOutput('denied-changes', JSON.stringify(deniedChanges))
summary.addDeniedToSummary(deniedChanges)
printDeniedDependencies(deniedChanges, config)
}
if (config.show_openssf_scorecard) {
summary.addScorecardToSummary(scorecard, config)
printScorecardBlock(scorecard, config)
createScorecardWarnings(scorecard, config)
}
core.setOutput('dependency-changes', JSON.stringify(changes))
summary.addScannedDependencies(changes)
printScannedDependencies(changes)
await commentPr(core.summary, config)
@@ -257,6 +279,29 @@ function printNullLicenses(changes: Changes): void {
}
}
function printScorecardBlock(
scorecard: Scorecard,
config: ConfigurationOptions
): void {
core.group('Scorecard', async () => {
if (scorecard) {
for (const dependency of scorecard.dependencies) {
if (
dependency.scorecard?.score &&
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
) {
core.info(
`${styles.color.red.open}${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${dependency?.scorecard?.score}${styles.red.close}`
)
}
core.info(
`${dependency.change.ecosystem}/${dependency.change.name}: OpenSSF Scorecard Score: ${dependency?.scorecard?.score}`
)
}
}
})
}
function renderSeverity(
severity: 'critical' | 'high' | 'moderate' | 'low'
): string {
@@ -325,4 +370,24 @@ function printDeniedDependencies(
})
}
async function createScorecardWarnings(
scorecards: Scorecard,
config: ConfigurationOptions
): Promise<void> {
// Iterate through the list of scorecards, and if the score is less than the threshold, send a warning
for (const dependency of scorecards.dependencies) {
if (
dependency.scorecard?.score &&
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
) {
core.warning(
`${dependency.change.ecosystem}/${dependency.change.name} has an OpenSSF Scorecard of ${dependency.scorecard?.score}, which is less than this repository's threshold of ${config.warn_on_openssf_scorecard_level}.`,
{
title: 'OpenSSF Scorecard Warning'
}
)
}
}
}
run()
+44
View File
@@ -51,6 +51,8 @@ export const ConfigurationOptionsSchema = z
head_ref: z.string().optional(),
retry_on_snapshot_warnings: z.boolean().default(false),
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),
comment_summary_in_pr: z
.union([
z.preprocess(
@@ -100,9 +102,51 @@ export const ComparisonResponseSchema = z.object({
snapshot_warnings: z.string()
})
export const ScorecardApiSchema = z.object({
date: z.string(),
repo: z
.object({
name: z.string(),
commit: z.string()
})
.nullish(),
scorecard: z
.object({
version: z.string(),
commit: z.string()
})
.nullish(),
checks: z
.array(
z.object({
name: z.string(),
documentation: z.object({
shortDescription: z.string(),
url: z.string()
}),
score: z.string(),
reason: z.string(),
details: z.array(z.string())
})
)
.nullish(),
score: z.number().nullish()
})
export const ScorecardSchema = z.object({
dependencies: z.array(
z.object({
change: ChangeSchema,
scorecard: ScorecardApiSchema.nullish()
})
)
})
export type Change = z.infer<typeof ChangeSchema>
export type Changes = z.infer<typeof ChangesSchema>
export type ComparisonResponse = z.infer<typeof ComparisonResponseSchema>
export type ConfigurationOptions = z.infer<typeof ConfigurationOptionsSchema>
export type Severity = z.infer<typeof SeveritySchema>
export type Scope = (typeof SCOPES)[number]
export type Scorecard = z.infer<typeof ScorecardSchema>
export type ScorecardApi = z.infer<typeof ScorecardApiSchema>
+73
View File
@@ -0,0 +1,73 @@
import {Change, Scorecard, ScorecardApi} from './schemas'
import * as core from '@actions/core'
export async function getScorecardLevels(
changes: Change[]
): Promise<Scorecard> {
const data: Scorecard = {dependencies: []} as Scorecard
for (const change of changes) {
const ecosystem = change.ecosystem
const packageName = change.name
const version = change.version
//Get the project repository
let repositoryUrl = change.source_repository_url
//If the repository_url includes the protocol, remove it
if (repositoryUrl?.startsWith('https://')) {
repositoryUrl = repositoryUrl.replace('https://', '')
}
// If GitHub API doesn't have the repository URL, query deps.dev for it.
if (repositoryUrl) {
// Call the deps.dev API to get the repository URL from there
repositoryUrl = await getProjectUrl(ecosystem, packageName, version)
}
// Get the scorecard API response from the scorecards API
let scorecardApi: ScorecardApi | null = null
if (repositoryUrl) {
try {
scorecardApi = await getScorecard(repositoryUrl)
} catch (error: unknown) {
core.debug(`Error querying for scorecard: ${(error as Error).message}`)
}
}
data.dependencies.push({
change,
scorecard: scorecardApi
})
}
return data
}
async function getScorecard(repositoryUrl: string): Promise<ScorecardApi> {
const apiRoot = 'https://api.securityscorecards.dev/'
let scorecardResponse: ScorecardApi = {} as ScorecardApi
const url = `${apiRoot}/projects/${repositoryUrl}`
const response = await fetch(url)
if (response.ok) {
scorecardResponse = await response.json()
} else {
core.debug(`Couldn't get scorecard data for ${repositoryUrl}`)
}
return scorecardResponse
}
export async function getProjectUrl(
ecosystem: string,
packageName: string,
version: string
): Promise<string> {
core.debug(`Getting deps.dev data for ${packageName} ${version}`)
const depsDevAPIRoot = 'https://api.deps.dev'
const url = `${depsDevAPIRoot}/v3alpha/systems/${ecosystem}/packages/${packageName}/versions/${version}`
const response = await fetch(url)
if (response.ok) {
const data = await response.json()
if (data.relatedProjects.length > 0) {
return data.relatedProjects[0].projectKey.id
}
}
return ''
}
+95 -8
View File
@@ -1,5 +1,5 @@
import * as core from '@actions/core'
import {ConfigurationOptions, Changes, Change} from './schemas'
import {ConfigurationOptions, Changes, Change, Scorecard} from './schemas'
import {SummaryTableRow} from '@actions/core/lib/summary'
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
@@ -14,22 +14,30 @@ export function addSummaryToSummary(
vulnerableChanges: Changes,
invalidLicenseChanges: InvalidLicenseChanges,
deniedChanges: Changes,
scorecard: Scorecard,
config: ConfigurationOptions
): void {
const scorecardWarnings = countScorecardWarnings(scorecard, config)
const licenseIssues = countLicenseIssues(invalidLicenseChanges)
core.summary.addHeading('Dependency Review', 1)
if (
vulnerableChanges.length === 0 &&
countLicenseIssues(invalidLicenseChanges) === 0 &&
deniedChanges.length === 0
licenseIssues === 0 &&
deniedChanges.length === 0 &&
scorecardWarnings === 0
) {
if (!config.license_check) {
core.summary.addRaw(`${icons.check} No vulnerabilities found.`)
} else if (!config.vulnerability_check) {
core.summary.addRaw(`${icons.check} No license issues found.`)
const issueTypes = [
config.vulnerability_check ? 'vulnerabilities' : '',
config.license_check ? 'license issues' : '',
config.show_openssf_scorecard ? 'OpenSSF Scorecard issues' : ''
]
if (issueTypes.filter(Boolean).length === 0) {
core.summary.addRaw(`${icons.check} No issues found.`)
} else {
core.summary.addRaw(
`${icons.check} No vulnerabilities or license issues found.`
`${icons.check} No ${issueTypes.filter(Boolean).join(' or ')} found.`
)
}
@@ -65,11 +73,31 @@ export function addSummaryToSummary(
deniedChanges.length
} package(s) denied.`
]
: []),
...(config.show_openssf_scorecard && scorecardWarnings > 0
? [
`${checkOrWarnIcon(scorecardWarnings)} ${scorecardWarnings ? scorecardWarnings : 'No'} packages with OpenSSF Scorecard issues.`
]
: [])
])
.addRaw('See the Details below.')
}
function countScorecardWarnings(
scorecard: Scorecard,
config: ConfigurationOptions
): number {
return scorecard.dependencies.reduce(
(total, dependency) =>
total +
(dependency.scorecard?.score &&
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
? 1
: 0),
0
)
}
export function addChangeVulnerabilitiesToSummary(
vulnerableChanges: Changes,
severity: string
@@ -249,6 +277,65 @@ function snapshotWarningRecommendation(
return 'Re-running this action after a short time may resolve the issue.'
}
export function addScorecardToSummary(
scorecard: Scorecard,
config: ConfigurationOptions
): void {
core.summary.addHeading('OpenSSF Scorecard', 2)
if (scorecard.dependencies.length > 10) {
core.summary.addRaw(`<details><summary>Scorecard details</summary>`, true)
}
core.summary.addRaw(
`<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>`,
true
)
for (const dependency of scorecard.dependencies) {
core.debug('Adding scorecard to summary')
core.debug(`Overall score ${dependency.scorecard?.score}`)
// Set the icon based on the overall score value
let overallIcon = ''
if (dependency.scorecard?.score) {
overallIcon =
dependency.scorecard?.score < config.warn_on_openssf_scorecard_level
? ':warning:'
: ':green_circle:'
}
//Add a row for the dependency
core.summary.addRaw(
`<tr><td>${dependency.change.source_repository_url ? `<a href="https://${dependency.change.source_repository_url}">` : ''} ${dependency.change.ecosystem}/${dependency.change.name} ${dependency.change.source_repository_url ? `</a>` : ''}</td><td>${dependency.change.version}</td>
<td>${overallIcon} ${dependency.scorecard?.score === undefined || dependency.scorecard?.score === null ? 'Unknown' : dependency.scorecard?.score}</td>`,
false
)
//Add details table in the last column
if (dependency.scorecard?.checks !== undefined) {
let detailsTable =
'<table><tr><th>Check</th><th>Score</th><th>Reason</th></tr>'
for (const check of dependency.scorecard?.checks || []) {
const icon =
parseFloat(check.score) < config.warn_on_openssf_scorecard_level
? ':warning:'
: ':green_circle:'
detailsTable += `<tr><td>${check.name}</td><td>${icon} ${check.score}</td><td>${check.reason}</td></tr>`
}
detailsTable += `</table>`
core.summary.addRaw(
`<td><details><summary>Details</summary>${detailsTable}</details></td></tr>`,
true
)
} else {
core.summary.addRaw('<td>Unknown</td></tr>', true)
}
}
core.summary.addRaw(`</table>`)
if (scorecard.dependencies.length > 10) {
core.summary.addRaw(`</details>`)
}
}
export function addSnapshotWarnings(
config: ConfigurationOptions,
warnings: string