Compare commits
140 Commits
external-config
...
v2
| Author | SHA1 | Date | |
|---|---|---|---|
| 0efb1d1d84 | |||
| d4f6425aa4 | |||
| 49a61bd9bd | |||
| 06c01e11e8 | |||
| 4538b29c27 | |||
| 4153ec555a | |||
| 7c8d0843f9 | |||
| fc00198e43 | |||
| 80e573b784 | |||
| b5c3d1e723 | |||
| 7fd272118a | |||
| 3c9a31f5a0 | |||
| d8fba3fdc1 | |||
| e805dd89e8 | |||
| 32276cb73d | |||
| fe226ac019 | |||
| b759175bdb | |||
| 6af054f363 | |||
| 6f32cb0afd | |||
| 2791afab72 | |||
| a8b5c8c24e | |||
| 12a250de95 | |||
| 917e5af203 | |||
| ba6dba6225 | |||
| 63154658bc | |||
| f84c5813e5 | |||
| 228a6404a2 | |||
| c84947f64b | |||
| 71dbf10e60 | |||
| f9deefc2e9 | |||
| 0e5d083be1 | |||
| 2f428eec67 | |||
| dff2fdff0f | |||
| 12a171cf96 | |||
| 3156cf8998 | |||
| fd675ced9c | |||
| f7d03d8b76 | |||
| 7e41a6f1ee | |||
| 4c0961eff6 | |||
| d1e9a12830 | |||
| 2e3713aab8 | |||
| ba9d7c1389 | |||
| 0cd2781117 | |||
| 129f0ad973 | |||
| 0a88a4704b | |||
| 18069caed8 | |||
| 61cee4b12b | |||
| 94670a1af8 | |||
| 577d9714ad | |||
| 9ce6cb532b | |||
| 0b980b1ccd | |||
| bc5f6c2f39 | |||
| 9c96258789 | |||
| f076f221f4 | |||
| 88b817ec8d | |||
| 2dd6c6a3d7 | |||
| 1d9bfbbddf | |||
| f632f5f79d | |||
| ee42a6512f | |||
| 6f58092362 | |||
| b81bfe53ce | |||
| 5679c0f8be | |||
| 2018b3e66f | |||
| 463890c1ed | |||
| c9b9d23e75 | |||
| 4c14cfe593 | |||
| 5b70fe08e7 | |||
| 81216f689b | |||
| afbc15c97f | |||
| 8d974c4ee8 | |||
| cdad98596a | |||
| 0a0eb39992 | |||
| df3ceaf7f0 | |||
| 1997789b86 | |||
| 584e620d09 | |||
| 1fa34689ad | |||
| de2814d20e | |||
| eabc27054f | |||
| b486e073e9 | |||
| 03321307df | |||
| cc2a6ab32f | |||
| 5de8be4c40 | |||
| 1b8bd021a3 | |||
| 65d8cd176f | |||
| 6d500ff869 | |||
| 0259ed8420 | |||
| ec636f3d19 | |||
| 367e85631b | |||
| abf7b5a775 | |||
| ba85772f4b | |||
| 8d812df813 | |||
| 63e12b21ed | |||
| 0385b5b162 | |||
| 8e053e0f5e | |||
| e0ff0cf732 | |||
| ea65cbfc18 | |||
| 5bf43a89cd | |||
| 468485fc8e | |||
| 46c9f79a1f | |||
| cd3f55e8f9 | |||
| f832351766 | |||
| f96ed229f4 | |||
| 629703a27b | |||
| d05bfb69a5 | |||
| 02bcebdd6e | |||
| fbeabf7e29 | |||
| 0515f5cb39 | |||
| 2d1d679f58 | |||
| a3563a05bc | |||
| 8a20ddbf25 | |||
| 2a646668d9 | |||
| 60be833ffd | |||
| edc501a219 | |||
| 000837f2ac | |||
| 89f99d150a | |||
| 0ed41eff02 | |||
| dbe70eb550 | |||
| 78c7c01396 | |||
| 89a5c76329 | |||
| 4a6d691283 | |||
| b58d457243 | |||
| cc033856be | |||
| 8595e805a5 | |||
| fa10a7f0d6 | |||
| 6755d8aa71 | |||
| 375c537008 | |||
| 98f28ebe06 | |||
| 716b322ec9 | |||
| 12ae1bd550 | |||
| bcb52636bd | |||
| 241ff73141 | |||
| 062b749663 | |||
| 4f00b72b84 | |||
| 602f968ea2 | |||
| bd61ea0d9e | |||
| 8ec13c1f01 | |||
| 723ec8c0d3 | |||
| 2843194510 | |||
| 6944531f76 | |||
| 29cdbbed37 |
@@ -1 +0,0 @@
|
||||
fail-on-severity: low
|
||||
@@ -1,4 +1,5 @@
|
||||
event.json
|
||||
.ruby-version
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
|
||||
+24
-16
@@ -1,4 +1,5 @@
|
||||
# Contributing
|
||||
|
||||
[fork]: https://github.com/actions/dependency-review-action/fork
|
||||
[pr]: https://github.com/actions/dependency-review-action/compare
|
||||
[code-of-conduct]: CODE_OF_CONDUCT.md
|
||||
@@ -9,7 +10,6 @@ Contributions to this project are
|
||||
[released](https://help.github.com/articles/github-terms-of-service/#6-contributions-under-repository-license)
|
||||
to the public under the [project's open source license](LICENSE).
|
||||
|
||||
|
||||
Please note that this project is released with a [Contributor Code of
|
||||
Conduct][code-of-conduct]. By participating in this project you agree
|
||||
to abide by its terms.
|
||||
@@ -20,7 +20,6 @@ This Action makes an authenticated query to the Dependency Graph Diff
|
||||
API endpoint (`GET /repos/{owner}/{repo}/dependency-graph/compare/{basehead}`)
|
||||
to find out the set of added and removed dependencies for each manifest.
|
||||
|
||||
|
||||
### Bootstrapping the project
|
||||
|
||||
```
|
||||
@@ -35,7 +34,7 @@ npm install
|
||||
npm run test
|
||||
```
|
||||
|
||||
*Note*: We don't have any useful tests yet, contributions are welcome!
|
||||
_Note_: We don't have any useful tests yet, contributions are welcome!
|
||||
|
||||
## Local Development
|
||||
|
||||
@@ -56,16 +55,24 @@ Like this:
|
||||
$ GITHUB_TOKEN=my-secret-token ./scripts/scan_pr https://github.com/actions/dependency-review-action/pull/3
|
||||
```
|
||||
|
||||
[Configuration options](README.md#configuration-options) can be set by
|
||||
passing an external YAML [configuration file](README.md#configuration-file) to the
|
||||
`scan_pr` script with the `-c`/`--config-file` option:
|
||||
|
||||
```sh
|
||||
$ GITHUB_TOKEN=<token> ./scripts/scan_pr --config-file my_custom_config.yml <pr_url>
|
||||
```
|
||||
|
||||
## Submitting a pull request
|
||||
|
||||
0. [Fork][fork] and clone the repository
|
||||
0. Configure and install the dependencies: `npm install`
|
||||
0. Make sure the tests pass on your machine: `npm run test`
|
||||
0. Create a new branch: `git checkout -b my-branch-name`
|
||||
0. Make your change, add tests, and make sure the tests still pass
|
||||
0. Make sure to build and package before pushing: `npm run build && npm run package`
|
||||
0. Push to your fork and [submit a pull request][pr]
|
||||
0. Pat your self on the back and wait for your pull request to be reviewed and merged.
|
||||
1. Configure and install the dependencies: `npm install`
|
||||
2. Make sure the tests pass on your machine: `npm run test`
|
||||
3. Create a new branch: `git checkout -b my-branch-name`
|
||||
4. Make your change, add tests, and make sure the tests still pass
|
||||
5. Make sure to build and package before pushing: `npm run build && npm run package`
|
||||
6. Push to your fork and [submit a pull request][pr]
|
||||
7. Pat your self on the back and wait for your pull request to be reviewed and merged.
|
||||
|
||||
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
||||
|
||||
@@ -77,21 +84,21 @@ Here are a few things you can do that will increase the likelihood of your pull
|
||||
|
||||
1. Update the version number in [package.json](https://github.com/actions/dependency-review-action/blob/main/package.json).
|
||||
1. Go to [Draft a new
|
||||
release](https://github.com/actions/dependency-review-action/releases/new)
|
||||
in the Releases page.
|
||||
2. Make sure that the `Publish this Action to the GitHub Marketplace`
|
||||
checkbox is enabled
|
||||
release](https://github.com/actions/dependency-review-action/releases/new)
|
||||
in the Releases page.
|
||||
1. Make sure that the `Publish this Action to the GitHub Marketplace`
|
||||
checkbox is enabled
|
||||
|
||||
<img width="481" alt="Screenshot 2022-06-15 at 12 08 19" src="https://user-images.githubusercontent.com/2161/173822484-4b60d8b4-c674-4bff-b5ff-b0c4a3650ab7.png">
|
||||
|
||||
3. Click "Choose a tag" and then "Create new tag", where the tag name
|
||||
will be your version prefixed by a `v` (e.g. `v1.2.3`).
|
||||
will be your version prefixed by a `v` (e.g. `v1.2.3`).
|
||||
4. Use a version number for the release title (e.g. "1.2.3").
|
||||
|
||||
<img width="700" alt="Screenshot 2022-06-15 at 12 08 36" src="https://user-images.githubusercontent.com/2161/173822548-33ab3432-d679-4dc1-adf8-b50fdaf47de3.png">
|
||||
|
||||
5. Add your release notes. If this is a major version make sure to
|
||||
include a small description of the biggest changes in the new version.
|
||||
include a small description of the biggest changes in the new version.
|
||||
6. Click "Publish Release".
|
||||
|
||||
You now have a tag and release using the semver version you used
|
||||
@@ -102,6 +109,7 @@ automatically getting all the
|
||||
minor/patch updates.
|
||||
|
||||
To do this just checkout `main`, force-create a new annotated tag, and push it:
|
||||
|
||||
```
|
||||
git tag -fa v2 -m "Updating v2 to 2.3.4"
|
||||
git push origin v2 --force
|
||||
|
||||
@@ -67,19 +67,19 @@ jobs:
|
||||
Configure this action by either using an external configuration file,
|
||||
or by inlining these options in your workflow file.
|
||||
|
||||
### Options
|
||||
## Configuration Options
|
||||
|
||||
#### config-file
|
||||
### config-file
|
||||
|
||||
A string representing the path to an external configuraton file. By
|
||||
default external configuration files are not used.
|
||||
|
||||
**Possible values**: A string representing the absolute path to the
|
||||
configuration file.
|
||||
configuration file.
|
||||
|
||||
**Example**: `config-file: ./.github/dependency-review-config.yml`.
|
||||
|
||||
#### fail-on-severity
|
||||
### fail-on-severity
|
||||
|
||||
Configure the severity level for alerting. See "[Vulnerability Severity](https://github.com/actions/dependency-review-action#vulnerability-severity)".
|
||||
|
||||
@@ -87,7 +87,7 @@ Configure the severity level for alerting. See "[Vulnerability Severity](https:/
|
||||
|
||||
**Example**: `fail-on-severity: moderate`.
|
||||
|
||||
#### fail-on-scopes
|
||||
### fail-on-scopes
|
||||
|
||||
A list of strings representing the build environments you want to
|
||||
support. The default value is `development, runtime`.
|
||||
@@ -97,13 +97,14 @@ support. The default value is `development, runtime`.
|
||||
**Inline example**: `fail-on-scopes: development, runtime`
|
||||
|
||||
**YAML example**:
|
||||
|
||||
```yaml
|
||||
# this prevents scanning development dependencies
|
||||
fail-on-scopes:
|
||||
- runtime
|
||||
```
|
||||
|
||||
#### allow-licenses
|
||||
### allow-licenses
|
||||
|
||||
Only allow the licenses in this list. See "[Licenses](https://github.com/actions/dependency-review-action#licenses)".
|
||||
|
||||
@@ -113,13 +114,14 @@ https://docs.github.com/en/rest/licenses.
|
||||
**Inline example**: `allow-licenses: BSD-3-Clause, MIT`
|
||||
|
||||
**YAML example**:
|
||||
|
||||
```yaml
|
||||
allow-licenses:
|
||||
- BSD-3-Clause
|
||||
- MIT
|
||||
```
|
||||
|
||||
#### deny-licenses
|
||||
### deny-licenses
|
||||
|
||||
Add a custom list of licenses you want to block. See
|
||||
"[Licenses](https://github.com/actions/dependency-review-action#licenses)".
|
||||
@@ -130,13 +132,30 @@ https://docs.github.com/en/rest/licenses.
|
||||
**Inline example**: `deny-licenses: LGPL-2.0, BSD-2-Clause`
|
||||
|
||||
**YAML example**:
|
||||
|
||||
```yaml
|
||||
deny-licenses:
|
||||
- LGPL-2.0
|
||||
- BSD-2-Clause
|
||||
```
|
||||
|
||||
#### base-ref/head-ref
|
||||
### allow-ghsas
|
||||
|
||||
Add a custom list of GitHub Advisory IDs that can be skipped during detection.
|
||||
|
||||
**Possible values**: Any valid advisory GHSA ids.
|
||||
|
||||
**Inline example**: `allow-ghsas: GHSA-abcd-1234-5679, GHSA-efgh-1234-5679`
|
||||
|
||||
**YAML example**:
|
||||
|
||||
```yaml
|
||||
allow-ghsas:
|
||||
- GHSA-abcd-1234-5679
|
||||
- GHSA-efgh-1234-5679
|
||||
```
|
||||
|
||||
### base-ref/head-ref
|
||||
|
||||
Provide custom git references for the git base/head when performing
|
||||
the comparison. If you are using pull requests, or
|
||||
@@ -146,6 +165,7 @@ this. The values need to be specified for all other event types.
|
||||
**Possible values**: Any valid git ref(s) in your project.
|
||||
|
||||
**Example**:
|
||||
|
||||
```yaml
|
||||
base-ref: 8bb8a58d6a4028b6c2e314d5caaf273f57644896
|
||||
head-ref: 69af5638bf660cf218aad5709a4c100e42a2f37b
|
||||
@@ -163,18 +183,18 @@ file:
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@v2
|
||||
with:
|
||||
config-file: "./.github/dependency-review-config.yml"
|
||||
config-file: './.github/dependency-review-config.yml'
|
||||
```
|
||||
|
||||
And then create the file in the path you just specified. **All of these fields are
|
||||
optional**:
|
||||
|
||||
```yaml
|
||||
fail-on-severity: "critical"
|
||||
fail-on-severity: 'critical'
|
||||
allow-licenses:
|
||||
- "GPL-3.0"
|
||||
- "BSD-3-Clause"
|
||||
- "MIT"
|
||||
- 'GPL-3.0'
|
||||
- 'BSD-3-Clause'
|
||||
- 'MIT'
|
||||
```
|
||||
|
||||
### Inline Configuration
|
||||
|
||||
@@ -16,6 +16,7 @@ function clearInputs() {
|
||||
'FAIL-ON-SCOPES',
|
||||
'ALLOW-LICENSES',
|
||||
'DENY-LICENSES',
|
||||
'ALLOW-GHSAS',
|
||||
'CONFIG-FILE',
|
||||
'BASE-REF',
|
||||
'HEAD-REF'
|
||||
@@ -160,3 +161,17 @@ test('it raises an error when given invalid scope', async () => {
|
||||
setInput('fail-on-scopes', 'runtime, zombies')
|
||||
expect(() => readConfig()).toThrow()
|
||||
})
|
||||
|
||||
test('it defaults to an empty GHSA allowlist', async () => {
|
||||
const options = readConfig()
|
||||
expect(options.allow_ghsas).toEqual(undefined)
|
||||
})
|
||||
|
||||
test('it successfully parses GHSA allowlist', async () => {
|
||||
setInput('allow-ghsas', 'GHSA-abcd-1234-5679, GHSA-efgh-1234-5679')
|
||||
const options = readConfig()
|
||||
expect(options.allow_ghsas).toEqual([
|
||||
'GHSA-abcd-1234-5679',
|
||||
'GHSA-efgh-1234-5679'
|
||||
])
|
||||
})
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
import {filterChangesBySeverity, filterChangesByScopes} from '../src/filter'
|
||||
import {
|
||||
filterChangesBySeverity,
|
||||
filterChangesByScopes,
|
||||
filterAllowedAdvisories
|
||||
} from '../src/filter'
|
||||
|
||||
let npmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
@@ -48,6 +52,19 @@ let rubyChange: Change = {
|
||||
]
|
||||
}
|
||||
|
||||
let noVulnNpmChange: Change = {
|
||||
manifest: 'package.json',
|
||||
change_type: 'added',
|
||||
ecosystem: 'npm',
|
||||
name: 'helpful',
|
||||
version: '1.0.0',
|
||||
package_url: 'pkg:npm/helpful@1.0.0',
|
||||
license: 'MIT',
|
||||
source_repository_url: 'github.com/some-repo',
|
||||
scope: 'runtime',
|
||||
vulnerabilities: []
|
||||
}
|
||||
|
||||
test('it properly filters changes by severity', async () => {
|
||||
const changes = [npmChange, rubyChange]
|
||||
let result = filterChangesBySeverity('high', changes)
|
||||
@@ -72,3 +89,35 @@ test('it properly filters changes by scope', async () => {
|
||||
result = filterChangesByScopes(['runtime', 'development'], changes)
|
||||
expect(result).toEqual([npmChange, rubyChange])
|
||||
})
|
||||
|
||||
test('it properly handles undefined advisory IDs', async () => {
|
||||
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
||||
let result = filterAllowedAdvisories(undefined, changes)
|
||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||
})
|
||||
|
||||
test('it properly filters changes with allowed vulnerabilities', async () => {
|
||||
const changes = [npmChange, rubyChange, noVulnNpmChange]
|
||||
|
||||
let result = filterAllowedAdvisories(['notrealGHSAID'], changes)
|
||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||
|
||||
result = filterAllowedAdvisories(['first-random_string'], changes)
|
||||
expect(result).toEqual([rubyChange, noVulnNpmChange])
|
||||
|
||||
result = filterAllowedAdvisories(
|
||||
['second-random_string', 'third-random_string'],
|
||||
changes
|
||||
)
|
||||
expect(result).toEqual([npmChange, noVulnNpmChange])
|
||||
|
||||
result = filterAllowedAdvisories(
|
||||
['first-random_string', 'second-random_string', 'third-random_string'],
|
||||
changes
|
||||
)
|
||||
expect(result).toEqual([noVulnNpmChange])
|
||||
|
||||
// if we have a change with multiple vulnerabilities but only one is allowed, we still should not filter out that change
|
||||
result = filterAllowedAdvisories(['second-random_string'], changes)
|
||||
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {expect, test} from '@jest/globals'
|
||||
import {expect, jest, test} from '@jest/globals'
|
||||
import {Change, Changes} from '../src/schemas'
|
||||
import {getDeniedLicenseChanges} from '../src/licenses'
|
||||
|
||||
@@ -48,15 +48,41 @@ let rubyChange: Change = {
|
||||
]
|
||||
}
|
||||
|
||||
jest.mock('@actions/core')
|
||||
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
licenses: {
|
||||
getForRepo: jest
|
||||
.fn()
|
||||
.mockReturnValue({data: {license: {spdx_id: 'AGPL'}}})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jest.mock('octokit', () => {
|
||||
return {
|
||||
Octokit: class {
|
||||
constructor() {
|
||||
return mockOctokit
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('it fails if a license outside the allow list is found', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: ['BSD']
|
||||
})
|
||||
expect(invalidChanges[0]).toBe(npmChange)
|
||||
})
|
||||
|
||||
test('it fails if a license inside the deny list is found', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
const [invalidChanges] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
|
||||
const [invalidChanges] = await getDeniedLicenseChanges(changes, {
|
||||
deny: ['BSD']
|
||||
})
|
||||
expect(invalidChanges[0]).toBe(rubyChange)
|
||||
})
|
||||
|
||||
@@ -64,7 +90,7 @@ test('it fails if a license inside the deny list is found', async () => {
|
||||
// thing we want in the system. Please remove this test after refactoring.
|
||||
test('it fails all license checks when allow is provided an empty array', async () => {
|
||||
const changes: Changes = [npmChange, rubyChange]
|
||||
let [invalidChanges, _] = getDeniedLicenseChanges(changes, {
|
||||
let [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: [],
|
||||
deny: ['BSD']
|
||||
})
|
||||
@@ -76,7 +102,9 @@ test('it does not fail if a license outside the allow list is found in removed c
|
||||
{...npmChange, change_type: 'removed'},
|
||||
{...rubyChange, change_type: 'removed'}
|
||||
]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: ['BSD']
|
||||
})
|
||||
expect(invalidChanges).toStrictEqual([])
|
||||
})
|
||||
|
||||
@@ -85,7 +113,9 @@ test('it does not fail if a license inside the deny list is found in removed cha
|
||||
{...npmChange, change_type: 'removed'},
|
||||
{...rubyChange, change_type: 'removed'}
|
||||
]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {deny: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
deny: ['BSD']
|
||||
})
|
||||
expect(invalidChanges).toStrictEqual([])
|
||||
})
|
||||
|
||||
@@ -95,6 +125,47 @@ test('it fails if a license outside the allow list is found in both of added and
|
||||
npmChange,
|
||||
{...rubyChange, change_type: 'removed'}
|
||||
]
|
||||
const [invalidChanges, _] = getDeniedLicenseChanges(changes, {allow: ['BSD']})
|
||||
const [invalidChanges, _] = await getDeniedLicenseChanges(changes, {
|
||||
allow: ['BSD']
|
||||
})
|
||||
expect(invalidChanges).toStrictEqual([npmChange])
|
||||
})
|
||||
|
||||
describe('GH License API fallback', () => {
|
||||
test('it calls licenses endpoint if atleast one of the changes has null license and valid source_repository_url', async () => {
|
||||
const nullLicenseChange = {
|
||||
...npmChange,
|
||||
license: null,
|
||||
source_repository_url: 'http://github.com/some-owner/some-repo'
|
||||
}
|
||||
const [_, unknownChanges] = await getDeniedLicenseChanges(
|
||||
[nullLicenseChange, rubyChange],
|
||||
{}
|
||||
)
|
||||
|
||||
expect(mockOctokit.rest.licenses.getForRepo).toHaveBeenNthCalledWith(1, {
|
||||
owner: 'some-owner',
|
||||
repo: 'some-repo'
|
||||
})
|
||||
expect(unknownChanges.length).toEqual(0)
|
||||
})
|
||||
|
||||
test('it does not call licenses API endpoint for change with null license and invalid source_repository_url ', async () => {
|
||||
const [_, unknownChanges] = await getDeniedLicenseChanges(
|
||||
[{...npmChange, license: null}],
|
||||
{}
|
||||
)
|
||||
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
|
||||
expect(unknownChanges.length).toEqual(1)
|
||||
})
|
||||
|
||||
test('it does not call licenses API endpoint if licenses for all changes are present', async () => {
|
||||
const [_, unknownChanges] = await getDeniedLicenseChanges(
|
||||
[npmChange, rubyChange],
|
||||
{}
|
||||
)
|
||||
|
||||
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
|
||||
expect(unknownChanges.length).toEqual(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,6 +29,9 @@ inputs:
|
||||
deny-licenses:
|
||||
description: Comma-separated list of forbidden licenses (e.g. "MIT, GPL 3.0, BSD 2 Clause")
|
||||
required: false
|
||||
allow-ghsas:
|
||||
description: Comma-separated list of allowed Github Advisory IDs (e.g. "GHSA-abcd-1234-5679, GHSA-efgh-1234-5679")
|
||||
required: false
|
||||
runs:
|
||||
using: 'node16'
|
||||
main: 'dist/index.js'
|
||||
|
||||
+16837
-173
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1049
File diff suppressed because it is too large
Load Diff
Generated
+2292
-330
File diff suppressed because it is too large
Load Diff
+16
-14
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dependency-review-action",
|
||||
"version": "2.2.0",
|
||||
"version": "2.5.1",
|
||||
"private": true,
|
||||
"description": "A GitHub Action for Dependency Review",
|
||||
"main": "lib/main.js",
|
||||
@@ -25,30 +25,32 @@
|
||||
"author": "GitHub",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.9.1",
|
||||
"@actions/github": "^5.0.3",
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/github": "^5.1.1",
|
||||
"@octokit/plugin-retry": "^3.0.9",
|
||||
"@octokit/request-error": "^3.0.1",
|
||||
"ansi-styles": "^6.1.1",
|
||||
"got": "^12.5.0",
|
||||
"@octokit/request-error": "^3.0.2",
|
||||
"ansi-styles": "^6.2.1",
|
||||
"got": "^12.5.2",
|
||||
"nodemon": "^2.0.20",
|
||||
"yaml": "^2.1.1",
|
||||
"octokit": "^2.0.9",
|
||||
"yaml": "^2.1.3",
|
||||
"zod": "^3.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.59",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.0",
|
||||
"@typescript-eslint/parser": "^5.38.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/node": "^16.18.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"esbuild-register": "^3.3.3",
|
||||
"eslint": "^8.23.1",
|
||||
"eslint-plugin-github": "^4.3.7",
|
||||
"eslint-plugin-jest": "^27.0.4",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-plugin-github": "^4.4.0",
|
||||
"eslint-plugin-jest": "^27.1.3",
|
||||
"jest": "^27.5.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"nodemon": "^2.0.20",
|
||||
"prettier": "2.7.1",
|
||||
"ts-jest": "^27.1.4",
|
||||
"typescript": "^4.8.3"
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
+34
-3
@@ -3,22 +3,52 @@ require 'json'
|
||||
require 'tempfile'
|
||||
require 'open3'
|
||||
require 'bundler/inline'
|
||||
require 'optparse'
|
||||
|
||||
gemfile do
|
||||
source 'https://rubygems.org'
|
||||
gem 'octokit'
|
||||
end
|
||||
|
||||
config_file = nil
|
||||
github_token = ENV["GITHUB_TOKEN"]
|
||||
|
||||
if !github_token || github_token.empty?
|
||||
puts "Please set the GITHUB_TOKEN environment variable"
|
||||
exit -1
|
||||
end
|
||||
|
||||
arg = /(?<repo_nwo>[\w\-]+\/[\w\-]+)\/pull\/(?<pr_number>\d+)/.match(ARGV[0])
|
||||
op = OptionParser.new do |opts|
|
||||
usage = <<EOF
|
||||
Run Dependency Review on a repository.
|
||||
|
||||
\e[1mUsage:\e[22m
|
||||
scripts/scan_pr [options] <pr_url>
|
||||
|
||||
\e[1mExample:\e[22m
|
||||
scripts/scan_pr https://github.com/actions/dependency-review-action/pull/294
|
||||
|
||||
EOF
|
||||
|
||||
opts.banner = usage
|
||||
|
||||
opts.on('-c', '--config-file <FILE>', 'Use an external configuration file') do |cf|
|
||||
config_file = cf
|
||||
end
|
||||
|
||||
opts.on("-h", "--help", "Prints this help") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
end
|
||||
|
||||
op.parse!
|
||||
|
||||
# make sure we have a NWO somewhere in the parameters
|
||||
arg = /(?<repo_nwo>[\w\-]+\/[\w\-]+)\/pull\/(?<pr_number>\d+)/.match(ARGV.join(" "))
|
||||
|
||||
if arg.nil?
|
||||
puts "Usage: script/scan_pr <pr_url>"
|
||||
puts op
|
||||
exit -1
|
||||
end
|
||||
|
||||
@@ -33,7 +63,8 @@ event_file.write("{ \"pull_request\": #{pr.to_h.to_json}}")
|
||||
event_file.close
|
||||
|
||||
action_inputs = {
|
||||
"repo-token" => github_token
|
||||
"repo-token": github_token,
|
||||
"config-file": config_file
|
||||
}
|
||||
|
||||
dev_cmd_env = {
|
||||
|
||||
+7
-4
@@ -47,21 +47,24 @@ export function readInlineConfig(): ConfigurationOptions {
|
||||
.default(['runtime'])
|
||||
.parse(parseList(getOptionalInput('fail-on-scopes')))
|
||||
|
||||
const allow_licenses = getOptionalInput('allow-licenses')
|
||||
const deny_licenses = getOptionalInput('deny-licenses')
|
||||
const allow_licenses = parseList(getOptionalInput('allow-licenses'))
|
||||
const deny_licenses = parseList(getOptionalInput('deny-licenses'))
|
||||
|
||||
if (allow_licenses !== undefined && deny_licenses !== undefined) {
|
||||
throw new Error("Can't specify both allow_licenses and deny_licenses")
|
||||
}
|
||||
|
||||
const allow_ghsas = parseList(getOptionalInput('allow-ghsas'))
|
||||
|
||||
const base_ref = getOptionalInput('base-ref')
|
||||
const head_ref = getOptionalInput('head-ref')
|
||||
|
||||
return {
|
||||
fail_on_severity,
|
||||
fail_on_scopes,
|
||||
allow_licenses: parseList(allow_licenses),
|
||||
deny_licenses: parseList(deny_licenses),
|
||||
allow_licenses,
|
||||
deny_licenses,
|
||||
allow_ghsas,
|
||||
base_ref,
|
||||
head_ref
|
||||
}
|
||||
|
||||
+45
-1
@@ -35,9 +35,13 @@ export function filterChangesBySeverity(
|
||||
}
|
||||
|
||||
export function filterChangesByScopes(
|
||||
scopes: Scope[],
|
||||
scopes: Scope[] | undefined,
|
||||
changes: Changes
|
||||
): Changes {
|
||||
if (scopes === undefined) {
|
||||
return []
|
||||
}
|
||||
|
||||
const filteredChanges = changes.filter(change => {
|
||||
// if there is no scope on the change (Enterprise Server API for now), we will assume it is a runtime scope
|
||||
const scope = change.scope || 'runtime'
|
||||
@@ -46,3 +50,43 @@ export function filterChangesByScopes(
|
||||
|
||||
return filteredChanges
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out changes that are allowed by the allow_ghsas config
|
||||
* option. We want to remove these changes before we do any
|
||||
* processing.
|
||||
* @param ghsas - list of GHSA IDs to allow
|
||||
* @param changes - list of changes to filter
|
||||
* @returns a list of changes with the allowed GHSAs removed
|
||||
*/
|
||||
export function filterAllowedAdvisories(
|
||||
ghsas: string[] | undefined,
|
||||
changes: Changes
|
||||
): Changes {
|
||||
if (ghsas === undefined) {
|
||||
return changes
|
||||
}
|
||||
|
||||
const filteredChanges = changes.filter(change => {
|
||||
const noAdvisories =
|
||||
change.vulnerabilities === undefined ||
|
||||
change.vulnerabilities.length === 0
|
||||
|
||||
if (noAdvisories) {
|
||||
return true
|
||||
}
|
||||
|
||||
let allAllowedAdvisories = true
|
||||
// if there's at least one advisory that is not allowlisted, we will keep the change
|
||||
for (const vulnerability of change.vulnerabilities) {
|
||||
if (!ghsas.includes(vulnerability.advisory_ghsa_id)) {
|
||||
allAllowedAdvisories = false
|
||||
}
|
||||
if (!allAllowedAdvisories) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return filteredChanges
|
||||
}
|
||||
|
||||
+65
-4
@@ -1,3 +1,5 @@
|
||||
import * as core from '@actions/core'
|
||||
import {Octokit} from 'octokit'
|
||||
import {Change} from './schemas'
|
||||
|
||||
/**
|
||||
@@ -10,21 +12,27 @@ import {Change} from './schemas'
|
||||
* we will ignore the deny list.
|
||||
* @param {Change[]} changes The list of changes to filter.
|
||||
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
|
||||
* @returns {[Array<Change>, Array<Change]} A tuple where the first element is the list of denied changes and the second one is the list of changes with unknown licenses
|
||||
* @returns {Promise<[Array.<Change>, Array.<Change>]>} A promise to a 2 element tuple. The first element is the list of denied changes and the second one is the list of changes with unknown licenses
|
||||
*/
|
||||
export function getDeniedLicenseChanges(
|
||||
export async function getDeniedLicenseChanges(
|
||||
changes: Change[],
|
||||
licenses: {
|
||||
allow?: string[]
|
||||
deny?: string[]
|
||||
}
|
||||
): [Change[], Change[]] {
|
||||
): Promise<[Change[], Change[]]> {
|
||||
const {allow, deny} = licenses
|
||||
|
||||
const disallowed: Change[] = []
|
||||
const unknown: Change[] = []
|
||||
|
||||
for (const change of changes) {
|
||||
const consolidatedChanges = changes.some(
|
||||
({source_repository_url, license}) => !license && source_repository_url
|
||||
)
|
||||
? await setGHLicenses(changes)
|
||||
: changes
|
||||
|
||||
for (const change of consolidatedChanges) {
|
||||
if (change.change_type === 'removed') {
|
||||
continue
|
||||
}
|
||||
@@ -47,3 +55,56 @@ export function getDeniedLicenseChanges(
|
||||
|
||||
return [disallowed, unknown]
|
||||
}
|
||||
|
||||
const fetchGHLicense = async (
|
||||
owner: string,
|
||||
repo: string
|
||||
): Promise<string | null> => {
|
||||
const octokit = new Octokit({
|
||||
auth: core.getInput('repo-token', {required: true})
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await octokit.rest.licenses.getForRepo({owner, repo})
|
||||
return response.data.license?.spdx_id ?? null
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const parseGitHubURL = (url: string): {owner: string; repo: string} | null => {
|
||||
try {
|
||||
const parsed = new URL(url)
|
||||
if (parsed.host !== 'github.com') {
|
||||
return null
|
||||
}
|
||||
const components = parsed.pathname.split('/')
|
||||
if (components.length < 3) {
|
||||
return null
|
||||
}
|
||||
return {owner: components[1], repo: components[2]}
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const setGHLicenses = async (changes: Change[]): Promise<Change[]> => {
|
||||
const updatedChanges = changes.map(async change => {
|
||||
if (change.license !== null || change.source_repository_url === null) {
|
||||
return change
|
||||
}
|
||||
|
||||
const githubUrl = parseGitHubURL(change.source_repository_url)
|
||||
|
||||
if (githubUrl === null) {
|
||||
return change
|
||||
}
|
||||
|
||||
return {
|
||||
...change,
|
||||
license: await fetchGHLicense(githubUrl.owner, githubUrl.repo)
|
||||
}
|
||||
})
|
||||
|
||||
return Promise.all(updatedChanges)
|
||||
}
|
||||
|
||||
+113
-54
@@ -3,13 +3,19 @@ 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, Scope} from './schemas'
|
||||
import {Change, Severity, Changes} from './schemas'
|
||||
import {readConfig} from '../src/config'
|
||||
import {filterChangesBySeverity, filterChangesByScopes} from '../src/filter'
|
||||
import {
|
||||
filterChangesBySeverity,
|
||||
filterChangesByScopes,
|
||||
filterAllowedAdvisories
|
||||
} from '../src/filter'
|
||||
import {getDeniedLicenseChanges} from './licenses'
|
||||
import * as summary from './summary'
|
||||
import {getRefs} from './git-refs'
|
||||
|
||||
import {groupDependenciesByManifest} from './utils'
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
const config = readConfig()
|
||||
@@ -22,21 +28,16 @@ async function run(): Promise<void> {
|
||||
headRef: refs.head
|
||||
})
|
||||
|
||||
const minSeverity = config.fail_on_severity
|
||||
let failed = false
|
||||
|
||||
const licenses = {
|
||||
allow: config.allow_licenses,
|
||||
deny: config.deny_licenses
|
||||
}
|
||||
|
||||
const scopes = config.fail_on_scopes
|
||||
|
||||
const scopedChanges = filterChangesByScopes(scopes as Scope[], changes)
|
||||
const minSeverity = config.fail_on_severity as Severity
|
||||
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
|
||||
const filteredChanges = filterAllowedAdvisories(
|
||||
config.allow_ghsas,
|
||||
scopedChanges
|
||||
)
|
||||
|
||||
const addedChanges = filterChangesBySeverity(
|
||||
minSeverity as Severity,
|
||||
scopedChanges
|
||||
minSeverity,
|
||||
filteredChanges
|
||||
).filter(
|
||||
change =>
|
||||
change.change_type === 'added' &&
|
||||
@@ -44,38 +45,22 @@ async function run(): Promise<void> {
|
||||
change.vulnerabilities.length > 0
|
||||
)
|
||||
|
||||
const [licenseErrors, unknownLicenses] = getDeniedLicenseChanges(
|
||||
scopedChanges,
|
||||
licenses
|
||||
const [licenseErrors, unknownLicenses] = await getDeniedLicenseChanges(
|
||||
filteredChanges,
|
||||
{
|
||||
allow: config.allow_licenses,
|
||||
deny: config.deny_licenses
|
||||
}
|
||||
)
|
||||
|
||||
summary.addSummaryToSummary(addedChanges, licenseErrors, unknownLicenses)
|
||||
|
||||
if (addedChanges.length > 0) {
|
||||
for (const change of addedChanges) {
|
||||
printChangeVulnerabilities(change)
|
||||
}
|
||||
failed = true
|
||||
}
|
||||
|
||||
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity || '')
|
||||
|
||||
if (licenseErrors.length > 0) {
|
||||
printLicensesError(licenseErrors)
|
||||
core.setFailed('Dependency review detected incompatible licenses.')
|
||||
}
|
||||
|
||||
printNullLicenses(unknownLicenses)
|
||||
|
||||
summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
|
||||
summary.addLicensesToSummary(licenseErrors, unknownLicenses, config)
|
||||
summary.addScannedDependencies(changes)
|
||||
|
||||
if (failed) {
|
||||
core.setFailed('Dependency review detected vulnerable packages.')
|
||||
} else {
|
||||
core.info(
|
||||
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`
|
||||
)
|
||||
}
|
||||
printVulnerabilitiesBlock(addedChanges, minSeverity)
|
||||
printLicensesBlock(licenseErrors, unknownLicenses)
|
||||
printScannedDependencies(changes)
|
||||
} catch (error) {
|
||||
if (error instanceof RequestError && error.status === 404) {
|
||||
core.setFailed(
|
||||
@@ -97,6 +82,29 @@ async function run(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function printVulnerabilitiesBlock(
|
||||
addedChanges: Change[],
|
||||
minSeverity: Severity
|
||||
): void {
|
||||
let failed = false
|
||||
core.group('Vulnerabilities', async () => {
|
||||
if (addedChanges.length > 0) {
|
||||
for (const change of addedChanges) {
|
||||
printChangeVulnerabilities(change)
|
||||
}
|
||||
failed = true
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
core.setFailed('Dependency review detected vulnerable packages.')
|
||||
} else {
|
||||
core.info(
|
||||
`Dependency review did not detect any vulnerable packages with severity level "${minSeverity}" or higher.`
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function printChangeVulnerabilities(change: Change): void {
|
||||
for (const vuln of change.vulnerabilities) {
|
||||
core.info(
|
||||
@@ -110,18 +118,17 @@ function printChangeVulnerabilities(change: Change): void {
|
||||
}
|
||||
}
|
||||
|
||||
function renderSeverity(
|
||||
severity: 'critical' | 'high' | 'moderate' | 'low'
|
||||
): string {
|
||||
const color = (
|
||||
{
|
||||
critical: 'red',
|
||||
high: 'red',
|
||||
moderate: 'yellow',
|
||||
low: 'grey'
|
||||
} as const
|
||||
)[severity]
|
||||
return `${styles.color[color].open}(${severity} severity)${styles.color[color].close}`
|
||||
function printLicensesBlock(
|
||||
licenseErrors: Change[],
|
||||
unknownLicenses: Change[]
|
||||
): void {
|
||||
core.group('Licenses', async () => {
|
||||
if (licenseErrors.length > 0) {
|
||||
printLicensesError(licenseErrors)
|
||||
core.setFailed('Dependency review detected incompatible licenses.')
|
||||
}
|
||||
printNullLicenses(unknownLicenses)
|
||||
})
|
||||
}
|
||||
|
||||
function printLicensesError(changes: Change[]): void {
|
||||
@@ -150,4 +157,56 @@ function printNullLicenses(changes: Change[]): void {
|
||||
}
|
||||
}
|
||||
|
||||
function renderSeverity(
|
||||
severity: 'critical' | 'high' | 'moderate' | 'low'
|
||||
): string {
|
||||
const color = (
|
||||
{
|
||||
critical: 'red',
|
||||
high: 'red',
|
||||
moderate: 'yellow',
|
||||
low: 'grey'
|
||||
} as const
|
||||
)[severity]
|
||||
return `${styles.color[color].open}(${severity} severity)${styles.color[color].close}`
|
||||
}
|
||||
|
||||
function renderScannedDependency(change: Change): string {
|
||||
const changeType: string = change.change_type
|
||||
|
||||
if (changeType !== 'added' && changeType !== 'removed') {
|
||||
throw new Error(`Unexpected change type: ${changeType}`)
|
||||
}
|
||||
|
||||
const color = (
|
||||
{
|
||||
added: 'green',
|
||||
removed: 'red'
|
||||
} as const
|
||||
)[changeType]
|
||||
|
||||
const icon = (
|
||||
{
|
||||
added: '+',
|
||||
removed: '-'
|
||||
} as const
|
||||
)[changeType]
|
||||
|
||||
return `${styles.color[color].open}${icon} ${change.name}@${change.version}${styles.color[color].close}`
|
||||
}
|
||||
|
||||
function printScannedDependencies(changes: Changes): void {
|
||||
core.group('Dependency Changes', async () => {
|
||||
const dependencies = groupDependenciesByManifest(changes)
|
||||
|
||||
for (const manifestName of dependencies.keys()) {
|
||||
const manifestChanges = dependencies.get(manifestName) || []
|
||||
core.info(`File: ${styles.bold.open}${manifestName}${styles.bold.close}`)
|
||||
for (const change of manifestChanges) {
|
||||
core.info(`${renderScannedDependency(change)}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
@@ -40,6 +40,7 @@ export const ConfigurationOptionsSchema = z
|
||||
fail_on_scopes: z.array(z.enum(SCOPES)).default(['runtime']),
|
||||
allow_licenses: z.array(z.string()).default([]),
|
||||
deny_licenses: z.array(z.string()).default([]),
|
||||
allow_ghsas: z.array(z.string()).default([]),
|
||||
config_file: z.string().optional().default('false'),
|
||||
base_ref: z.string(),
|
||||
head_ref: z.string()
|
||||
|
||||
+19
-11
@@ -1,6 +1,7 @@
|
||||
import * as core from '@actions/core'
|
||||
import {ConfigurationOptions, Change, Changes} from './schemas'
|
||||
import {SummaryTableRow} from '@actions/core/lib/summary'
|
||||
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
|
||||
|
||||
export function addSummaryToSummary(
|
||||
addedPackages: Changes,
|
||||
@@ -20,7 +21,7 @@ export function addChangeVulnerabilitiesToSummary(
|
||||
): void {
|
||||
const rows: SummaryTableRow[] = []
|
||||
|
||||
const manifests = getManifests(addedPackages)
|
||||
const manifests = getManifestsSet(addedPackages)
|
||||
|
||||
core.summary
|
||||
.addHeading('Vulnerabilities')
|
||||
@@ -99,7 +100,7 @@ export function addLicensesToSummary(
|
||||
|
||||
if (licenseErrors.length > 0) {
|
||||
const rows: SummaryTableRow[] = []
|
||||
const manifests = getManifests(licenseErrors)
|
||||
const manifests = getManifestsSet(licenseErrors)
|
||||
|
||||
core.summary.addHeading('Incompatible Licenses', 3).addSeparator()
|
||||
|
||||
@@ -125,7 +126,7 @@ export function addLicensesToSummary(
|
||||
|
||||
if (unknownLicenses.length > 0) {
|
||||
const rows: SummaryTableRow[] = []
|
||||
const manifests = getManifests(unknownLicenses)
|
||||
const manifests = getManifestsSet(unknownLicenses)
|
||||
|
||||
core.debug(
|
||||
`found ${manifests.entries.length} manifests for unknown licenses`
|
||||
@@ -150,14 +151,21 @@ export function addLicensesToSummary(
|
||||
}
|
||||
}
|
||||
|
||||
function getManifests(changes: Changes): Set<string> {
|
||||
return new Set(changes.flatMap(c => c.manifest))
|
||||
}
|
||||
export function addScannedDependencies(changes: Changes): void {
|
||||
const dependencies = groupDependenciesByManifest(changes)
|
||||
const manifests = dependencies.keys()
|
||||
|
||||
function renderUrl(url: string | null, text: string): string {
|
||||
if (url) {
|
||||
return `<a href="${url}">${text}</a>`
|
||||
} else {
|
||||
return text
|
||||
const summary = core.summary
|
||||
.addHeading('Scanned Dependencies')
|
||||
.addRaw(`We scanned ${dependencies.size} manifest files:`)
|
||||
|
||||
for (const manifest of manifests) {
|
||||
const deps = dependencies.get(manifest)
|
||||
if (deps) {
|
||||
const dependencyNames = deps.map(
|
||||
dependency => `<li>${dependency.name}@${dependency.version}</li>`
|
||||
)
|
||||
summary.addRaw(`<h3>${manifest}</h3><ul>${dependencyNames.join('')}</ul>`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import {Changes} from './schemas'
|
||||
|
||||
export function groupDependenciesByManifest(
|
||||
changes: Changes
|
||||
): Map<string, Changes> {
|
||||
const dependencies: Map<string, Changes> = new Map()
|
||||
for (const change of changes) {
|
||||
const manifestName = change.manifest
|
||||
|
||||
if (dependencies.get(manifestName) === undefined) {
|
||||
dependencies.set(manifestName, [])
|
||||
}
|
||||
|
||||
dependencies.get(manifestName)?.push(change)
|
||||
}
|
||||
|
||||
return dependencies
|
||||
}
|
||||
|
||||
export function getManifestsSet(changes: Changes): Set<string> {
|
||||
return new Set(changes.flatMap(c => c.manifest))
|
||||
}
|
||||
|
||||
export function renderUrl(url: string | null, text: string): string {
|
||||
if (url) {
|
||||
return `<a href="${url}">${text}</a>`
|
||||
} else {
|
||||
return text
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user