Compare commits

..
Author SHA1 Message Date
Federico Builes c77673abe0 Merge branch 'main' into fix-request-error-handling 2023-01-09 08:07:02 +01:00
23 changed files with 852 additions and 3336 deletions
-2
View File
@@ -100,5 +100,3 @@ Thumbs.db
# Ignore built ts files # Ignore built ts files
__tests__/runner/* __tests__/runner/*
lib/**/* lib/**/*
tmp
+18 -18
View File
@@ -66,25 +66,25 @@ jobs:
Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional. Configure this action by either inlining these options in your workflow file, or by using an external configuration file. All configuration options are optional.
| Option | Usage | Possible values | Default value | | Option | Usage | Possible values | Default value |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------------- | |-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------|---------------|
| `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` | | `fail-on-severity` | Defines the threshold for the level of severity. The action will fail on any pull requests that introduce vulnerabilities of the specified severity level or higher. | `low`, `moderate`, `high`, `critical` | `low` |
| `allow-licenses`* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none | | `allow-licenses`* | Contains a list of allowed licenses. The action will fail on pull requests that introduce dependencies with licenses that do not match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
| `deny-licenses`* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none | | `deny-licenses`* | Contains a list of prohibited licenses. The action will fail on pull requests that introduce dependencies with licenses that match the list. | Any [SPDX-compliant identifier(s)](https://spdx.org/licenses/) | none |
| `fail-on-scopes` | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. | `runtime`, `development`, `unknown` | `runtime` | | `fail-on-scopes`† | Contains a list of strings of the build environments you want to support. The action will fail on pull requests that introduce vulnerabilities in the scopes that match the list. |`runtime`, `development`, `unknown` | `runtime` |
| `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none | | `allow-ghsas` | Contains a list of GitHub Advisory Database IDs that can be skipped during detection. | Any GHSAs from the [GitHub Advisory Database](https://github.com/advisories) | none |
| `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` | | `license-check` | Enable or disable the license check performed by the action. | `true`, `false` | `true` |
| `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` | | `vulnerability-check` | Enable or disable the vulnerability check performed by the action. | `true`, `false` | `true` |
| `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 | | `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`. | `true`, `false` | `false` |
*not supported for use with GitHub Enterprise Server *not supported for use with GitHub Enterprise Server
†will be supported with GitHub Enterprise Server 3.8 †will be supported with GitHub Enterprise Server 3.8
### Inline Configuration ### Inline Configuration
You can pass options to the Dependency Review GitHub Action using your workflow file. You can pass options to the Dependency Review GitHub Action using your workflow file.
#### Example #### Example
@@ -112,10 +112,10 @@ jobs:
You can use an external configuration file to specify the settings for this action. It can be a local file or a file in an external repository. Refer to the following options for the specification. You can use an external configuration file to specify the settings for this action. It can be a local file or a file in an external repository. Refer to the following options for the specification.
| Option | Usage | Possible values | | Option | Usage | Possible values |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | |-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------|
| `config-file` | A path to a file in the current repository or an external repository. Use this syntax for external files: `OWNER/REPOSITORY/FILENAME@BRANCH` | **Local file**: `./.github/dependency-review-config.yml` <br> **External repo**: `github/octorepo/dependency-review-config.yml@main` | | `config-file` | A path to a file in the current repository or an external repository. Use this syntax for external files: `OWNER/REPOSITORY/FILENAME@BRANCH` | **Local file**: `./.github/dependency-review-config.yml` <br> **External repo**: `github/octorepo/dependency-review-config.yml@main` |
| `external-repo-token` | Specifies a token for fetching the configuration file. It is required if the file resides in a private external repository and for all GitHub Enterprise Server repositories. Create a token in [developer settings](https://github.com/settings/tokens). | Any token with `read` permissions to the repository hosting the config file. | | `external-repo-token` | Specifies a token for fetching the configuration file. It is required if the file resides in a private external repository and for all GitHub Enterprise Server repositories. Create a token in [developer settings](https://github.com/settings/tokens). | Any token with `read` permissions to the repository hosting the config file. |
#### Example #### Example
@@ -128,7 +128,7 @@ Start by specifying that you will be using an external configuration file:
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: And then create the file in the path you just specified:
```yaml ```yaml
fail-on-severity: 'critical' fail-on-severity: 'critical'
+4 -6
View File
@@ -5,13 +5,13 @@ import * as Utils from '../src/utils'
// GitHub Action inputs come in the form of environment variables // GitHub Action inputs come in the form of environment variables
// with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY) // with an INPUT prefix (e.g. INPUT_FAIL-ON-SEVERITY)
function setInput(input: string, value: string): void { function setInput(input: string, value: string) {
process.env[`INPUT_${input.toUpperCase()}`] = value process.env[`INPUT_${input.toUpperCase()}`] = value
} }
// We want a clean ENV before each test. We use `delete` // We want a clean ENV before each test. We use `delete`
// since we want `undefined` values and not empty strings. // since we want `undefined` values and not empty strings.
function clearInputs(): void { function clearInputs() {
const allowedOptions = [ const allowedOptions = [
'FAIL-ON-SEVERITY', 'FAIL-ON-SEVERITY',
'FAIL-ON-SCOPES', 'FAIL-ON-SCOPES',
@@ -22,11 +22,9 @@ function clearInputs(): void {
'VULNERABILITY-CHECK', 'VULNERABILITY-CHECK',
'CONFIG-FILE', 'CONFIG-FILE',
'BASE-REF', 'BASE-REF',
'HEAD-REF', 'HEAD-REF'
'COMMENT-SUMMARY-IN-PR'
] ]
// eslint-disable-next-line github/array-foreach
allowedOptions.forEach(option => { allowedOptions.forEach(option => {
delete process.env[`INPUT_${option.toUpperCase()}`] delete process.env[`INPUT_${option.toUpperCase()}`]
}) })
@@ -239,7 +237,7 @@ test('it supports comma-separated lists', async () => {
'config-file', 'config-file',
'./__tests__/fixtures/inline-license-config-sample.yml' './__tests__/fixtures/inline-license-config-sample.yml'
) )
const config = await readConfig() let config = await readConfig()
expect(config.allow_licenses).toEqual(['MIT', 'GPL-2.0-only']) expect(config.allow_licenses).toEqual(['MIT', 'GPL-2.0-only'])
}) })
+5 -5
View File
@@ -1,12 +1,12 @@
import {expect, test} from '@jest/globals' import {expect, test} from '@jest/globals'
import {Change} from '../src/schemas' import {Change, Changes} from '../src/schemas'
import { import {
filterChangesBySeverity, filterChangesBySeverity,
filterChangesByScopes, filterChangesByScopes,
filterAllowedAdvisories filterAllowedAdvisories
} from '../src/filter' } from '../src/filter'
const npmChange: Change = { let npmChange: Change = {
manifest: 'package.json', manifest: 'package.json',
change_type: 'added', change_type: 'added',
ecosystem: 'npm', ecosystem: 'npm',
@@ -26,7 +26,7 @@ const npmChange: Change = {
] ]
} }
const rubyChange: Change = { let rubyChange: Change = {
change_type: 'added', change_type: 'added',
manifest: 'Gemfile.lock', manifest: 'Gemfile.lock',
ecosystem: 'rubygems', ecosystem: 'rubygems',
@@ -52,7 +52,7 @@ const rubyChange: Change = {
] ]
} }
const noVulnNpmChange: Change = { let noVulnNpmChange: Change = {
manifest: 'package.json', manifest: 'package.json',
change_type: 'added', change_type: 'added',
ecosystem: 'npm', ecosystem: 'npm',
@@ -92,7 +92,7 @@ test('it properly filters changes by scope', async () => {
test('it properly handles undefined advisory IDs', async () => { test('it properly handles undefined advisory IDs', async () => {
const changes = [npmChange, rubyChange, noVulnNpmChange] const changes = [npmChange, rubyChange, noVulnNpmChange]
const result = filterAllowedAdvisories(undefined, changes) let result = filterAllowedAdvisories(undefined, changes)
expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange]) expect(result).toEqual([npmChange, rubyChange, noVulnNpmChange])
}) })
-36
View File
@@ -1,36 +0,0 @@
import {Change} from '../../src/schemas'
import {createTestVulnerability} from './create-test-vulnerability'
const defaultChange: Change = {
change_type: 'added',
manifest: 'package.json',
ecosystem: 'npm',
name: 'lodash',
version: '4.17.20',
package_url: 'pkg:npm/[email protected]',
license: 'MIT',
source_repository_url: 'https://github.com/lodash/lodash',
scope: 'runtime',
vulnerabilities: [
createTestVulnerability({
severity: 'high',
advisory_ghsa_id: 'GHSA-35jh-r3h4-6jhm',
advisory_summary: 'Command Injection in lodash',
advisory_url: 'https://github.com/advisories/GHSA-35jh-r3h4-6jhm'
}),
createTestVulnerability({
severity: 'moderate',
advisory_ghsa_id: 'GHSA-29mw-wpgm-hmr9',
advisory_summary:
'Regular Expression Denial of Service (ReDoS) in lodash',
advisory_url: 'https://github.com/advisories/GHSA-29mw-wpgm-hmr9'
})
]
}
const createTestChange = (overwrites: Partial<Change> = {}): Change => ({
...defaultChange,
...overwrites
})
export {createTestChange}
@@ -1,19 +0,0 @@
import {Change} from '../../src/schemas'
type Vulnerability = Change['vulnerabilities'][0]
const defaultTestVulnerability: Vulnerability = {
severity: 'high',
advisory_ghsa_id: 'GHSA-35jh-r3h4-6jhm',
advisory_summary: 'Command Injection in lodash',
advisory_url: 'https://github.com/advisories/GHSA-35jh-r3h4-6jhm'
}
const createTestVulnerability = (
overwrites: Partial<Vulnerability> = {}
): Vulnerability => ({
...defaultTestVulnerability,
...overwrites
})
export {createTestVulnerability}
+2 -5
View File
@@ -3,7 +3,7 @@ import {Change, Changes} from '../src/schemas'
let getInvalidLicenseChanges: Function let getInvalidLicenseChanges: Function
const npmChange: Change = { let npmChange: Change = {
manifest: 'package.json', manifest: 'package.json',
change_type: 'added', change_type: 'added',
ecosystem: 'npm', ecosystem: 'npm',
@@ -23,7 +23,7 @@ const npmChange: Change = {
] ]
} }
const rubyChange: Change = { let rubyChange: Change = {
change_type: 'added', change_type: 'added',
manifest: 'Gemfile.lock', manifest: 'Gemfile.lock',
ecosystem: 'rubygems', ecosystem: 'rubygems',
@@ -63,7 +63,6 @@ const mockOctokit = {
jest.mock('octokit', () => { jest.mock('octokit', () => {
return { return {
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
Octokit: class { Octokit: class {
constructor() { constructor() {
return mockOctokit return mockOctokit
@@ -79,7 +78,6 @@ beforeEach(async () => {
// true for BSD, false for all others // true for BSD, false for all others
return jest.fn((license: string, _: string): boolean => license === 'BSD') return jest.fn((license: string, _: string): boolean => license === 'BSD')
}) })
// eslint-disable-next-line @typescript-eslint/no-require-imports
;({getInvalidLicenseChanges} = require('../src/licenses')) ;({getInvalidLicenseChanges} = require('../src/licenses'))
}) })
@@ -142,7 +140,6 @@ test('it adds all licenses to unresolved if it is unable to determine the validi
throw new Error('Some Error') throw new Error('Some Error')
}) })
}) })
// eslint-disable-next-line @typescript-eslint/no-require-imports
;({getInvalidLicenseChanges} = require('../src/licenses')) ;({getInvalidLicenseChanges} = require('../src/licenses'))
const changes: Changes = [npmChange, rubyChange] const changes: Changes = [npmChange, rubyChange]
const invalidLicenses = await getInvalidLicenseChanges(changes, { const invalidLicenses = await getInvalidLicenseChanges(changes, {
-305
View File
@@ -1,305 +0,0 @@
import {expect, jest, test} from '@jest/globals'
import {Changes, ConfigurationOptions} from '../src/schemas'
import * as summary from '../src/summary'
import * as core from '@actions/core'
import {createTestChange} from './fixtures/create-test-change'
import {createTestVulnerability} from './fixtures/create-test-vulnerability'
afterEach(() => {
jest.clearAllMocks()
core.summary.emptyBuffer()
})
const emptyChanges: Changes = []
const emptyInvalidLicenseChanges = {
forbidden: [],
unresolved: [],
unlicensed: []
}
const defaultConfig: ConfigurationOptions = {
vulnerability_check: true,
license_check: true,
fail_on_severity: 'high',
fail_on_scopes: ['runtime'],
allow_ghsas: [],
allow_licenses: [],
deny_licenses: [],
comment_summary_in_pr: true
}
test('prints headline as h1', () => {
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
defaultConfig
)
const text = core.summary.stringify()
expect(text).toContain('<h1>Dependency Review</h1>')
})
test('only includes "No vulnerabilities or license issues found"-message if both are configured and nothing was found', () => {
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
defaultConfig
)
const text = core.summary.stringify()
expect(text).toContain('✅ No vulnerabilities or license issues found.')
})
test('only includes "No vulnerabilities found"-message if "license_check" is set to false and nothing was found', () => {
const config = {...defaultConfig, license_check: false}
summary.addSummaryToSummary(emptyChanges, emptyInvalidLicenseChanges, config)
const text = core.summary.stringify()
expect(text).toContain('✅ No vulnerabilities found.')
})
test('only includes "No license issues found"-message if "vulnerability_check" is set to false and nothing was found', () => {
const config = {...defaultConfig, vulnerability_check: false}
summary.addSummaryToSummary(emptyChanges, emptyInvalidLicenseChanges, config)
const text = core.summary.stringify()
expect(text).toContain('✅ No license issues found.')
})
test('does not include status section if nothing was found', () => {
summary.addSummaryToSummary(
emptyChanges,
emptyInvalidLicenseChanges,
defaultConfig
)
const text = core.summary.stringify()
expect(text).not.toContain('The following issues were found:')
})
test('includes count and status icons for all findings', () => {
const vulnerabilities = [
createTestChange({name: 'lodash'}),
createTestChange({name: 'underscore', package_url: 'test-url'})
]
const licenseIssues = {
forbidden: [createTestChange()],
unresolved: [createTestChange(), createTestChange()],
unlicensed: [createTestChange(), createTestChange(), createTestChange()]
}
summary.addSummaryToSummary(vulnerabilities, licenseIssues, defaultConfig)
const text = core.summary.stringify()
expect(text).toContain('❌ 2 vulnerable package(s)')
expect(text).toContain(
'❌ 2 package(s) with invalid SPDX license definitions'
)
expect(text).toContain('❌ 1 package(s) with incompatible licenses')
expect(text).toContain('⚠️ 3 package(s) with unknown licenses')
})
test('uses checkmarks for license issues if only vulnerabilities were found', () => {
const vulnerabilities = [createTestChange()]
summary.addSummaryToSummary(
vulnerabilities,
emptyInvalidLicenseChanges,
defaultConfig
)
const text = core.summary.stringify()
expect(text).toContain('❌ 1 vulnerable package(s)')
expect(text).toContain(
'✅ 0 package(s) with invalid SPDX license definitions'
)
expect(text).toContain('✅ 0 package(s) with incompatible licenses')
expect(text).toContain('✅ 0 package(s) with unknown licenses')
})
test('uses checkmarks for vulnerabilities if only license issues were found', () => {
const licenseIssues = {
forbidden: [createTestChange()],
unresolved: [],
unlicensed: []
}
summary.addSummaryToSummary(emptyChanges, licenseIssues, defaultConfig)
const text = core.summary.stringify()
expect(text).toContain('✅ 0 vulnerable package(s)')
expect(text).toContain(
'✅ 0 package(s) with invalid SPDX license definitions'
)
expect(text).toContain('❌ 1 package(s) with incompatible licenses')
expect(text).toContain('✅ 0 package(s) with unknown licenses')
})
test('addChangeVulnerabilitiesToSummary() - only includes section if any vulnerabilites found', () => {
summary.addChangeVulnerabilitiesToSummary(emptyChanges, 'low')
const text = core.summary.stringify()
expect(text).toEqual('')
})
test('addChangeVulnerabilitiesToSummary() - includes all vulnerabilities', () => {
const changes = [
createTestChange({name: 'lodash'}),
createTestChange({name: 'underscore', package_url: 'test-url'})
]
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
expect(text).toContain('<h2>Vulnerabilities</h2>')
expect(text).toContain('lodash')
expect(text).toContain('underscore')
})
test('addChangeVulnerabilitiesToSummary() - includes advisory url if available', () => {
const changes = [
createTestChange({
name: 'underscore',
vulnerabilities: [
createTestVulnerability({
advisory_summary: 'test-summary',
advisory_url: 'test-url'
})
]
})
]
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
expect(text).toContain('lodash')
expect(text).toContain('<a href="test-url">test-summary</a>')
})
test('addChangeVulnerabilitiesToSummary() - groups vulnerabilities of a single package', () => {
const changes = [
createTestChange({
name: 'package-with-multiple-vulnerabilities',
vulnerabilities: [
createTestVulnerability({advisory_summary: 'test-summary-1'}),
createTestVulnerability({advisory_summary: 'test-summary-2'})
]
})
]
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
expect(text.match('package-with-multiple-vulnerabilities')).toHaveLength(1)
expect(text).toContain('test-summary-1')
expect(text).toContain('test-summary-2')
})
test('addChangeVulnerabilitiesToSummary() - prints severity statement if above low', () => {
const changes = [createTestChange()]
summary.addChangeVulnerabilitiesToSummary(changes, 'medium')
const text = core.summary.stringify()
expect(text).toContain(
'Only included vulnerabilities with severity <strong>medium</strong> or higher.'
)
})
test('addChangeVulnerabilitiesToSummary() - does not print severity statment if it is set to "low"', () => {
const changes = [createTestChange()]
summary.addChangeVulnerabilitiesToSummary(changes, 'low')
const text = core.summary.stringify()
expect(text).not.toContain('Only included vulnerabilities')
})
test('addLicensesToSummary() - does not include entire section if no license issues found', () => {
summary.addLicensesToSummary(emptyInvalidLicenseChanges, defaultConfig)
const text = core.summary.stringify()
expect(text).toEqual('')
})
test('addLicensesToSummary() - includes all license issues in table', () => {
const licenseIssues = {
forbidden: [createTestChange()],
unresolved: [createTestChange(), createTestChange()],
unlicensed: [createTestChange(), createTestChange(), createTestChange()]
}
summary.addLicensesToSummary(licenseIssues, defaultConfig)
const text = core.summary.stringify()
expect(text).toContain('<h2>License Issues</h2>')
expect(text).toContain('<td>Incompatible License</td>')
expect(text).toContain('<td>Invalid SPDX License</td>')
expect(text).toContain('<td>Unknown License</td>')
})
test('addLicenseToSummary() - adds one table per manifest', () => {
const licenseIssues = {
forbidden: [
createTestChange({manifest: 'package.json'}),
createTestChange({manifest: '.github/workflows/test.yml'})
],
unresolved: [],
unlicensed: []
}
summary.addLicensesToSummary(licenseIssues, defaultConfig)
const text = core.summary.stringify()
expect(text).toContain('<h4><em>package.json</em></h4>')
expect(text).toContain('<h4><em>.github/workflows/test.yml</em></h4>')
})
test('addLicensesToSummary() - does not include specific license type sub-section if nothing is found', () => {
const licenseIssues = {
forbidden: [],
unlicensed: [],
unresolved: [createTestChange()]
}
summary.addLicensesToSummary(licenseIssues, defaultConfig)
const text = core.summary.stringify()
expect(text).not.toContain('<td>Incompatible License</td>')
expect(text).not.toContain('<td>Unknown License</td>')
expect(text).toContain('<td>Invalid SPDX License</td>')
})
test('addLicensesToSummary() - includes list of configured allowed licenses', () => {
const licenseIssues = {
forbidden: [createTestChange()],
unresolved: [],
unlicensed: []
}
const config: ConfigurationOptions = {
...defaultConfig,
allow_licenses: ['MIT', 'Apache-2.0']
}
summary.addLicensesToSummary(licenseIssues, config)
const text = core.summary.stringify()
expect(text).toContain('<strong>Allowed Licenses</strong>: MIT, Apache-2.0')
})
test('addLicensesToSummary() - includes configured denied license', () => {
const licenseIssues = {
forbidden: [createTestChange()],
unresolved: [],
unlicensed: []
}
const config: ConfigurationOptions = {
...defaultConfig,
deny_licenses: ['MIT']
}
summary.addLicensesToSummary(licenseIssues, config)
const text = core.summary.stringify()
expect(text).toContain('<strong>Denied Licenses</strong>: MIT')
})
-3
View File
@@ -41,9 +41,6 @@ inputs:
vulnerability-check: vulnerability-check:
description: A boolean to determine if vulnerability checks should be performed description: A boolean to determine if vulnerability checks should be performed
required: false required: false
comment-summary-in-pr:
description: A boolean to determine if the report should be posted as a comment in the PR itself. Setting this to true requires you to give the workflow the write permissions for pull-requests
required: false
runs: runs:
using: 'node16' using: 'node16'
main: 'dist/index.js' main: 'dist/index.js'
Generated Vendored
+265 -837
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long
+467 -1729
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -1,11 +1,11 @@
{ {
"name": "dependency-review-action", "name": "dependency-review-action",
"version": "3.0.3", "version": "3.0.2",
"private": true, "private": true,
"description": "A GitHub Action for Dependency Review", "description": "A GitHub Action for Dependency Review",
"main": "lib/main.js", "main": "lib/main.js",
"scripts": { "scripts": {
"build": "tsc -p tsconfig.build.json", "build": "tsc",
"format": "prettier --write '**/*.ts'", "format": "prettier --write '**/*.ts'",
"format-check": "prettier --check '**/*.ts'", "format-check": "prettier --check '**/*.ts'",
"lint": "eslint src/**/*.ts", "lint": "eslint src/**/*.ts",
@@ -27,36 +27,36 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.10.0",
"@actions/github": "^5.1.1", "@actions/github": "^5.1.1",
"@octokit/plugin-retry": "^4.1.1", "@octokit/plugin-retry": "^4.0.3",
"@octokit/request-error": "^2.1.0", "@octokit/request-error": "^2.1.0",
"ansi-styles": "^6.2.1", "ansi-styles": "^6.2.1",
"got": "^12.6.0", "got": "^12.5.3",
"nodemon": "^2.0.21", "nodemon": "^2.0.20",
"octokit": "^2.0.14", "octokit": "^2.0.11",
"spdx-expression-parse": "^3.0.1", "spdx-expression-parse": "^3.0.1",
"spdx-satisfies": "^5.0.1", "spdx-satisfies": "^5.0.1",
"yaml": "^2.2.1", "yaml": "^2.2.1",
"zod": "^3.21.0" "zod": "^3.20.2"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^16.18.14", "@types/node": "^16.18.11",
"@typescript-eslint/eslint-plugin": "^5.48.1", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.48.0", "@typescript-eslint/parser": "^5.48.0",
"@types/spdx-expression-parse": "^3.0.2", "@types/spdx-expression-parse": "^3.0.2",
"@types/spdx-satisfies": "^0.1.0", "@types/spdx-satisfies": "^0.1.0",
"@typescript-eslint/eslint-plugin": "^5.54.0", "@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.53.0", "@typescript-eslint/parser": "^5.47.1",
"@vercel/ncc": "^0.36.1", "@vercel/ncc": "^0.36.0",
"esbuild-register": "^3.4.2", "esbuild-register": "^3.4.2",
"eslint": "^8.35.0", "eslint": "^8.31.0",
"eslint-plugin-github": "^4.6.1", "eslint-plugin-github": "^4.6.0",
"eslint-plugin-jest": "^27.2.1", "eslint-plugin-jest": "^27.2.1",
"jest": "^27.5.1", "jest": "^27.5.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"nodemon": "^2.0.21", "nodemon": "^2.0.20",
"prettier": "2.8.4", "prettier": "2.8.2",
"ts-jest": "^27.1.4", "ts-jest": "^27.1.4",
"typescript": "^4.9.5" "typescript": "^4.9.4"
} }
} }
-114
View File
@@ -1,114 +0,0 @@
/**
* This scripts creates example markdown files for the summary in the ./tmp folder.
* You can use it to preview changes to the summary.
*
* You can execute it like this:
* npx ts-node scripts/create_summary.ts
*/
import {Changes, ConfigurationOptions} from '../src/schemas'
import {createTestChange} from '../__tests__/fixtures/create-test-change'
import {InvalidLicenseChanges} from '../src/licenses'
import * as fs from 'fs'
import * as core from '@actions/core'
import * as summary from '../src/summary'
import * as path from 'path'
const defaultConfig: ConfigurationOptions = {
vulnerability_check: true,
license_check: true,
fail_on_severity: 'high',
fail_on_scopes: ['runtime'],
allow_ghsas: [],
allow_licenses: ['MIT'],
deny_licenses: [],
comment_summary_in_pr: true
}
const tmpDir = path.resolve(__dirname, '../tmp')
const createExampleSummaries = async (): Promise<void> => {
await fs.promises.mkdir(tmpDir, {recursive: true})
await createNonIssueSummary()
await createFullSummary()
}
const createNonIssueSummary = async (): Promise<void> => {
await createSummary(
[],
{forbidden: [], unresolved: [], unlicensed: []},
defaultConfig,
'non-issue-summary.md'
)
}
const createFullSummary = async (): Promise<void> => {
const changes = [createTestChange()]
const licenses: InvalidLicenseChanges = {
forbidden: [
createTestChange({
name: 'underscore',
version: '1.12.0',
license: 'Apache 2.0'
})
],
unresolved: [
createTestChange({
name: 'octoinvader',
license: 'Non SPDX License'
}),
createTestChange({
name: 'owner/action-1',
license: 'XYZ-License',
version: 'v1.2.2',
manifest: '.github/workflows/action.yml'
})
],
unlicensed: [
createTestChange({
name: 'my-other-dependency',
license: null
}),
createTestChange({
name: 'owner/action-2',
version: 'main',
license: null,
manifest: '.github/workflows/action.yml'
})
]
}
await createSummary(changes, licenses, defaultConfig, 'full-summary.md')
}
async function createSummary(
vulnerabilities: Changes,
licenseIssues: InvalidLicenseChanges,
config: ConfigurationOptions,
fileName: string
): Promise<void> {
summary.addSummaryToSummary(vulnerabilities, licenseIssues, config)
summary.addChangeVulnerabilitiesToSummary(
vulnerabilities,
config.fail_on_severity
)
summary.addLicensesToSummary(licenseIssues, defaultConfig)
const allChanges = [
...vulnerabilities,
...licenseIssues.forbidden,
...licenseIssues.unresolved,
...licenseIssues.unlicensed
]
summary.addScannedDependencies(allChanges)
const text = core.summary.stringify()
await fs.promises.writeFile(path.resolve(tmpDir, fileName), text, {
flag: 'w'
})
core.summary.emptyBuffer()
}
createExampleSummaries()
-84
View File
@@ -1,84 +0,0 @@
import * as github from '@actions/github'
import * as core from '@actions/core'
import * as githubUtils from '@actions/github/lib/utils'
import * as retry from '@octokit/plugin-retry'
import {RequestError} from '@octokit/request-error'
const retryingOctokit = githubUtils.GitHub.plugin(retry.retry)
const octo = new retryingOctokit(
githubUtils.getOctokitOptions(core.getInput('repo-token', {required: true}))
)
// Comment Marker to identify an existing comment to update, so we don't spam the PR with comments
const COMMENT_MARKER = '<!-- dependency-review-pr-comment-marker -->'
export async function commentPr(summary: typeof core.summary): Promise<void> {
if (!github.context.payload.pull_request) {
core.warning(
'Not in the context of a pull request. Skipping comment creation.'
)
return
}
const commentBody = `${summary.stringify()}\n\n${COMMENT_MARKER}`
try {
const existingCommentId = await findCommentByMarker(COMMENT_MARKER)
if (existingCommentId) {
await octo.rest.issues.updateComment({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
comment_id: existingCommentId,
body: commentBody
})
} else {
await octo.rest.issues.createComment({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
issue_number: github.context.payload.pull_request.number,
body: commentBody
})
}
} catch (error) {
if (error instanceof RequestError && error.status === 403) {
core.warning(
`Unable to write summary to pull-request. Make sure you are giving this workflow the permission 'pull-requests: write'.`
)
} else {
if (error instanceof Error) {
core.warning(
`Unable to comment summary to pull-request, received error: ${error.message}`
)
} else {
core.warning(
'Unable to comment summary to pull-request: Unexpected fatal error'
)
}
}
}
}
async function findCommentByMarker(
commentBodyIncludes: string
): Promise<number | undefined> {
const commentsIterator = octo.paginate.iterator(
octo.rest.issues.listComments,
{
owner: github.context.repo.owner,
repo: github.context.repo.repo,
// We are already checking if we are in the context of a pull request in the caller
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
issue_number: github.context.payload.pull_request!.number
}
)
for await (const {data: comments} of commentsIterator) {
const existingComment = comments.find(comment =>
comment.body?.includes(commentBodyIncludes)
)
if (existingComment) return existingComment.id
}
return undefined
}
+1 -3
View File
@@ -34,7 +34,6 @@ function readInlineConfig(): ConfigurationOptionsPartial {
const vulnerability_check = getOptionalBoolean('vulnerability-check') const vulnerability_check = getOptionalBoolean('vulnerability-check')
const base_ref = getOptionalInput('base-ref') const base_ref = getOptionalInput('base-ref')
const head_ref = getOptionalInput('head-ref') const head_ref = getOptionalInput('head-ref')
const comment_summary_in_pr = getOptionalBoolean('comment-summary-in-pr')
validateLicenses('allow-licenses', allow_licenses) validateLicenses('allow-licenses', allow_licenses)
validateLicenses('deny-licenses', deny_licenses) validateLicenses('deny-licenses', deny_licenses)
@@ -48,8 +47,7 @@ function readInlineConfig(): ConfigurationOptionsPartial {
license_check, license_check,
vulnerability_check, vulnerability_check,
base_ref, base_ref,
head_ref, head_ref
comment_summary_in_pr
} }
return Object.fromEntries( return Object.fromEntries(
+2 -7
View File
@@ -14,24 +14,19 @@ import {isSPDXValid, octokitClient} from './utils'
* @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses. * @param { { allow?: string[], deny?: string[]}} licenses An object with `allow`/`deny` keys, each containing a list of licenses.
* @returns {Promise<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes * @returns {Promise<{Object.<string, Array.<Change>>}} A promise to a Record Object. The keys are strings, unlicensed, unresolved and forbidden. The values are a list of changes
*/ */
export type InvalidLicenseChangeTypes =
| 'unlicensed'
| 'unresolved'
| 'forbidden'
export type InvalidLicenseChanges = Record<InvalidLicenseChangeTypes, Changes>
export async function getInvalidLicenseChanges( export async function getInvalidLicenseChanges(
changes: Change[], changes: Change[],
licenses: { licenses: {
allow?: string[] allow?: string[]
deny?: string[] deny?: string[]
} }
): Promise<InvalidLicenseChanges> { ): Promise<Record<string, Changes>> {
const {allow, deny} = licenses const {allow, deny} = licenses
const groupedChanges = await groupChanges(changes) const groupedChanges = await groupChanges(changes)
const licensedChanges: Changes = groupedChanges.licensed const licensedChanges: Changes = groupedChanges.licensed
const invalidLicenseChanges: InvalidLicenseChanges = { const invalidLicenseChanges: Record<string, Changes> = {
unlicensed: groupedChanges.unlicensed, unlicensed: groupedChanges.unlicensed,
unresolved: [], unresolved: [],
forbidden: [] forbidden: []
+5 -15
View File
@@ -15,7 +15,6 @@ import * as summary from './summary'
import {getRefs} from './git-refs' import {getRefs} from './git-refs'
import {groupDependenciesByManifest} from './utils' import {groupDependenciesByManifest} from './utils'
import {commentPr} from './comment-pr'
async function run(): Promise<void> { async function run(): Promise<void> {
try { try {
@@ -29,11 +28,6 @@ async function run(): Promise<void> {
headRef: refs.head headRef: refs.head
}) })
if (!changes) {
core.info('No Dependency Changes found. Skipping Dependency Review.')
return
}
const minSeverity = config.fail_on_severity const minSeverity = config.fail_on_severity
const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes) const scopedChanges = filterChangesByScopes(config.fail_on_scopes, changes)
const filteredChanges = filterAllowedAdvisories( const filteredChanges = filterAllowedAdvisories(
@@ -41,7 +35,7 @@ async function run(): Promise<void> {
scopedChanges scopedChanges
) )
const vulnerableChanges = filterChangesBySeverity( const addedChanges = filterChangesBySeverity(
minSeverity, minSeverity,
filteredChanges filteredChanges
).filter( ).filter(
@@ -60,14 +54,13 @@ async function run(): Promise<void> {
) )
summary.addSummaryToSummary( summary.addSummaryToSummary(
vulnerableChanges, config.vulnerability_check ? addedChanges : null,
invalidLicenseChanges, config.license_check ? invalidLicenseChanges : null
config
) )
if (config.vulnerability_check) { if (config.vulnerability_check) {
summary.addChangeVulnerabilitiesToSummary(vulnerableChanges, minSeverity) summary.addChangeVulnerabilitiesToSummary(addedChanges, minSeverity)
printVulnerabilitiesBlock(vulnerableChanges, minSeverity) printVulnerabilitiesBlock(addedChanges, minSeverity)
} }
if (config.license_check) { if (config.license_check) {
summary.addLicensesToSummary(invalidLicenseChanges, config) summary.addLicensesToSummary(invalidLicenseChanges, config)
@@ -76,9 +69,6 @@ async function run(): Promise<void> {
summary.addScannedDependencies(changes) summary.addScannedDependencies(changes)
printScannedDependencies(changes) printScannedDependencies(changes)
if (config.comment_summary_in_pr) {
await commentPr(core.summary)
}
} catch (error) { } catch (error) {
if (error instanceof RequestError && error.status === 404) { if (error instanceof RequestError && error.status === 404) {
core.setFailed( core.setFailed(
+1 -2
View File
@@ -45,8 +45,7 @@ export const ConfigurationOptionsSchema = z
vulnerability_check: z.boolean().default(true), vulnerability_check: z.boolean().default(true),
config_file: z.string().optional(), config_file: z.string().optional(),
base_ref: z.string().optional(), base_ref: z.string().optional(),
head_ref: z.string().optional(), head_ref: z.string().optional()
comment_summary_in_pr: z.boolean().default(false)
}) })
.superRefine((config, context) => { .superRefine((config, context) => {
if (config.allow_licenses && config.deny_licenses) { if (config.allow_licenses && config.deny_licenses) {
+62 -119
View File
@@ -1,82 +1,50 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import {ConfigurationOptions, Changes} from './schemas' import {ConfigurationOptions, Changes} from './schemas'
import {SummaryTableRow} from '@actions/core/lib/summary' import {SummaryTableRow} from '@actions/core/lib/summary'
import {InvalidLicenseChanges, InvalidLicenseChangeTypes} from './licenses'
import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils' import {groupDependenciesByManifest, getManifestsSet, renderUrl} from './utils'
const icons = {
check: '✅',
cross: '❌',
warning: '⚠️'
}
export function addSummaryToSummary( export function addSummaryToSummary(
vulnerableChanges: Changes, addedPackages: Changes | null,
invalidLicenseChanges: InvalidLicenseChanges, invalidLicenseChanges: Record<string, Changes> | null
config: ConfigurationOptions
): void { ): void {
core.summary.addHeading('Dependency Review', 1)
if (
vulnerableChanges.length === 0 &&
countLicenseIssues(invalidLicenseChanges) === 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.`)
} else {
core.summary.addRaw(
`${icons.check} No vulnerabilities or license issues found.`
)
}
return
}
core.summary core.summary
.addRaw('The following issues were found:') .addHeading('Dependency Review')
.addRaw('We found:')
.addList([ .addList([
...(config.vulnerability_check ...(addedPackages
? [ ? [`${addedPackages.length} vulnerable package(s)`]
`${checkOrFailIcon(vulnerableChanges.length)} ${
vulnerableChanges.length
} vulnerable package(s)`
]
: []), : []),
...(config.license_check ...(invalidLicenseChanges
? [ ? [
`${checkOrFailIcon(invalidLicenseChanges.forbidden.length)} ${ `${invalidLicenseChanges.unresolved.length} package(s) with invalid SPDX license definitions`,
invalidLicenseChanges.forbidden.length `${invalidLicenseChanges.forbidden.length} package(s) with incompatible licenses`,
} package(s) with incompatible licenses`, `${invalidLicenseChanges.unlicensed.length} package(s) with unknown licenses.`
`${checkOrFailIcon(invalidLicenseChanges.unresolved.length)} ${
invalidLicenseChanges.unresolved.length
} package(s) with invalid SPDX license definitions`,
`${checkOrWarnIcon(invalidLicenseChanges.unlicensed.length)} ${
invalidLicenseChanges.unlicensed.length
} package(s) with unknown licenses.`
] ]
: []) : [])
]) ])
.addRaw('See the Details below.')
} }
export function addChangeVulnerabilitiesToSummary( export function addChangeVulnerabilitiesToSummary(
vulnerableChanges: Changes, addedPackages: Changes,
severity: string severity: string
): void { ): void {
if (vulnerableChanges.length === 0) { const rows: SummaryTableRow[] = []
const manifests = getManifestsSet(addedPackages)
core.summary
.addHeading('Vulnerabilities')
.addQuote(
`Vulnerabilities were filtered by minimum severity <strong>${severity}</strong>.`
)
if (addedPackages.length === 0) {
core.summary.addQuote('No vulnerabilities found in added packages.')
return return
} }
const rows: SummaryTableRow[] = []
const manifests = getManifestsSet(vulnerableChanges)
core.summary.addHeading('Vulnerabilities', 2)
for (const manifest of manifests) { for (const manifest of manifests) {
for (const change of vulnerableChanges.filter( for (const change of addedPackages.filter(
pkg => pkg.manifest === manifest pkg => pkg.manifest === manifest
)) { )) {
let previous_package = '' let previous_package = ''
@@ -104,7 +72,7 @@ export function addChangeVulnerabilitiesToSummary(
previous_version = change.version previous_version = change.version
} }
} }
core.summary.addHeading(`<em>${manifest}</em>`, 4).addTable([ core.summary.addHeading(`<em>${manifest}</em>`, 3).addTable([
[ [
{data: 'Name', header: true}, {data: 'Name', header: true},
{data: 'Version', header: true}, {data: 'Version', header: true},
@@ -114,24 +82,13 @@ export function addChangeVulnerabilitiesToSummary(
...rows ...rows
]) ])
} }
if (severity !== 'low') {
core.summary.addQuote(
`Only included vulnerabilities with severity <strong>${severity}</strong> or higher.`
)
}
} }
export function addLicensesToSummary( export function addLicensesToSummary(
invalidLicenseChanges: InvalidLicenseChanges, invalidLicenseChanges: Record<string, Changes>,
config: ConfigurationOptions config: ConfigurationOptions
): void { ): void {
if (countLicenseIssues(invalidLicenseChanges) === 0) { core.summary.addHeading('Licenses')
return
}
core.summary.addHeading('License Issues', 2)
printLicenseViolations(invalidLicenseChanges)
if (config.allow_licenses && config.allow_licenses.length > 0) { if (config.allow_licenses && config.allow_licenses.length > 0) {
core.summary.addQuote( core.summary.addQuote(
@@ -144,6 +101,11 @@ export function addLicensesToSummary(
) )
} }
if (Object.values(invalidLicenseChanges).every(item => item.length === 0)) {
core.summary.addQuote('No license violations detected.')
return
}
core.debug( core.debug(
`found ${invalidLicenseChanges.unlicensed.length} unknown licenses` `found ${invalidLicenseChanges.unlicensed.length} unknown licenses`
) )
@@ -151,43 +113,39 @@ export function addLicensesToSummary(
core.debug( core.debug(
`${invalidLicenseChanges.unresolved.length} licenses could not be validated` `${invalidLicenseChanges.unresolved.length} licenses could not be validated`
) )
printLicenseViolation(
'Incompatible Licenses',
invalidLicenseChanges.forbidden
)
printLicenseViolation('Unknown Licenses', invalidLicenseChanges.unlicensed)
printLicenseViolation(
'Invalid SPDX License Definitions',
invalidLicenseChanges.unresolved
)
} }
function printLicenseViolation(heading: string, changes: Changes): void {
core.summary.addHeading(heading, 5).addSeparator()
const licenseIssueTypes: InvalidLicenseChangeTypes[] = [ if (changes.length > 0) {
'forbidden', const rows: SummaryTableRow[] = []
'unresolved', const manifests = getManifestsSet(changes)
'unlicensed'
]
const issueTypeNames: Record<InvalidLicenseChangeTypes, string> = { for (const manifest of manifests) {
forbidden: 'Incompatible License', core.summary.addHeading(`<em>${manifest}</em>`, 4)
unresolved: 'Invalid SPDX License',
unlicensed: 'Unknown License'
}
function printLicenseViolations(changes: InvalidLicenseChanges): void { for (const change of changes.filter(pkg => pkg.manifest === manifest)) {
const rowsGroupedByManifest: Record<string, SummaryTableRow[]> = {} rows.push([
renderUrl(change.source_repository_url, change.name),
for (const issueType of licenseIssueTypes) { change.version,
for (const change of changes[issueType]) { formatLicense(change.license)
if (!rowsGroupedByManifest[change.manifest]) { ])
rowsGroupedByManifest[change.manifest] = []
} }
rowsGroupedByManifest[change.manifest].push([
renderUrl(change.source_repository_url, change.name),
change.version,
formatLicense(change.license),
issueTypeNames[issueType]
])
}
}
for (const [manifest, rows] of Object.entries(rowsGroupedByManifest)) { core.summary.addTable([['Package', 'Version', 'License'], ...rows])
core.summary.addHeading(`<em>${manifest}</em>`, 4) }
core.summary.addTable([ } else {
['Package', 'Version', 'License', 'Issue Type'], core.summary.addQuote(`No ${heading.toLowerCase()} detected.`)
...rows
])
} }
} }
@@ -202,7 +160,9 @@ export function addScannedDependencies(changes: Changes): void {
const dependencies = groupDependenciesByManifest(changes) const dependencies = groupDependenciesByManifest(changes)
const manifests = dependencies.keys() const manifests = dependencies.keys()
const summary = core.summary.addHeading('Scanned Manifest Files', 2) const summary = core.summary
.addHeading('Scanned Dependencies')
.addHeading(`We scanned ${dependencies.size} manifest files:`, 5)
for (const manifest of manifests) { for (const manifest of manifests) {
const deps = dependencies.get(manifest) const deps = dependencies.get(manifest)
@@ -214,20 +174,3 @@ export function addScannedDependencies(changes: Changes): void {
} }
} }
} }
function countLicenseIssues(
invalidLicenseChanges: InvalidLicenseChanges
): number {
return Object.values(invalidLicenseChanges).reduce(
(acc, val) => acc + val.length,
0
)
}
function checkOrFailIcon(count: number): string {
return count === 0 ? icons.check : icons.cross
}
function checkOrWarnIcon(count: number): string {
return count === 0 ? icons.check : icons.warning
}
-8
View File
@@ -1,8 +0,0 @@
{
"extends": "./tsconfig.json",
"include": ["src"],
"compilerOptions": {
"outDir": "./lib" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
}
}
+2 -1
View File
@@ -3,9 +3,10 @@
"target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"outDir": "./lib" /* Redirect output structure to the directory. */, "outDir": "./lib" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"strict": true /* Enable all strict type-checking options. */, "strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
}, },
"exclude": ["node_modules"] "exclude": ["node_modules", "**/*.test.ts"]
} }