Files
dependency-review-action/__tests__/licenses.test.ts
T
Kevin Dangoor f199659a6a Allowing dependencies works with no licenses
When using the `allow-dependencies-licenses` option, the packages listed
there should be allowed even if they have no license. This wasn't
working because the filtering for allowed dependencies was done
specifically on the list of packages that had licenses, leaving a
separate list (unfiltered) for packages with no licenses. With this
change, we filter out any changes for packages that have been allowed
_before_ we retrieve licenses.

Fixes #889
2025-05-08 16:31:46 -04:00

342 lines
9.4 KiB
TypeScript

import {expect, jest, test} from '@jest/globals'
import {Change, Changes} from '../src/schemas'
import {getInvalidLicenseChanges} from '../src/licenses'
const npmChange: Change = {
manifest: 'package.json',
change_type: 'added',
ecosystem: 'npm',
name: 'Reeuhq',
version: '1.0.2',
package_url: 'pkg:npm/[email protected]',
license: 'MIT',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'critical',
advisory_ghsa_id: 'first-random_string',
advisory_summary: 'very dangerous',
advisory_url: 'github.com/future-funk'
}
]
}
const rubyChange: Change = {
change_type: 'added',
manifest: 'Gemfile.lock',
ecosystem: 'rubygems',
name: 'actionsomething',
version: '3.2.0',
package_url: 'pkg:gem/[email protected]',
license: 'BSD-3-Clause',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'moderate',
advisory_ghsa_id: 'second-random_string',
advisory_summary: 'not so dangerous',
advisory_url: 'github.com/future-funk'
},
{
severity: 'low',
advisory_ghsa_id: 'third-random_string',
advisory_summary: 'dont page me',
advisory_url: 'github.com/future-funk'
}
]
}
const pipChange: Change = {
change_type: 'added',
manifest: 'requirements.txt',
ecosystem: 'pip',
name: 'package-1',
version: '1.1.1',
package_url: 'pkg:pypi/[email protected]',
license: 'MIT',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'moderate',
advisory_ghsa_id: 'second-random_string',
advisory_summary: 'not so dangerous',
advisory_url: 'github.com/future-funk'
},
{
severity: 'low',
advisory_ghsa_id: 'third-random_string',
advisory_summary: 'dont page me',
advisory_url: 'github.com/future-funk'
}
]
}
const complexLicenseChange: Change = {
change_type: 'added',
manifest: 'requirements.txt',
ecosystem: 'pip',
name: 'package-1',
version: '1.1.1',
package_url: 'pkg:pypi/[email protected]',
license: 'MIT AND Apache-2.0',
source_repository_url: 'github.com/some-repo',
scope: 'runtime',
vulnerabilities: [
{
severity: 'moderate',
advisory_ghsa_id: 'second-random_string',
advisory_summary: 'not so dangerous',
advisory_url: 'github.com/future-funk'
},
{
severity: 'low',
advisory_ghsa_id: 'third-random_string',
advisory_summary: 'dont page me',
advisory_url: 'github.com/future-funk'
}
]
}
const unlicensedChange: Change = {
change_type: 'added',
manifest: '.github/workflows/ci.yml',
ecosystem: 'actions',
name: 'foo-org/actions-repo/.github/workflows/some-action.yml',
version: '1.1.1',
package_url:
'pkg:githubactions/foo-org/actions-repo/.github/workflows/[email protected]',
license: null,
source_repository_url: 'github.com/some-repo',
scope: 'development',
vulnerabilities: []
}
jest.mock('@actions/core')
const mockOctokit = {
rest: {
licenses: {
getForRepo: jest
.fn()
.mockReturnValue({data: {license: {spdx_id: 'AGPL'}}})
}
}
}
jest.mock('octokit', () => {
return {
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
Octokit: class {
constructor() {
return mockOctokit
}
}
}
})
beforeEach(async () => {
jest.resetModules()
})
test('it adds license outside the allow list to forbidden changes', async () => {
const changes: Changes = [
npmChange, // MIT license
rubyChange // BSD license
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD-3-Clause']
})
expect(forbidden[0]).toBe(npmChange)
expect(forbidden.length).toEqual(1)
})
test('it adds license inside the deny list to forbidden changes', async () => {
const changes: Changes = [
npmChange, // MIT license
rubyChange // BSD license
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['BSD-3-Clause']
})
expect(forbidden[0]).toBe(rubyChange)
expect(forbidden.length).toEqual(1)
})
test('it handles allowed complex licenses', async () => {
const changes: Changes = [
complexLicenseChange // MIT AND Apache-2.0 license
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['MIT', 'Apache-2.0']
})
expect(forbidden.length).toEqual(0)
})
test('it handles complex licenses not all on the allow list', async () => {
const changes: Changes = [
complexLicenseChange // MIT AND Apache-2.0 license
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['MIT']
})
expect(forbidden.length).toEqual(1)
})
test('it does not add license outside the allow list to forbidden changes if it is in removed changes', async () => {
const changes: Changes = [
{...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'}
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD-3-Clause']
})
expect(forbidden).toStrictEqual([])
})
test('it does not add license inside the deny list to forbidden changes if it is in removed changes', async () => {
const changes: Changes = [
{...npmChange, change_type: 'removed'},
{...rubyChange, change_type: 'removed'}
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
deny: ['BSD-3-Clause']
})
expect(forbidden).toStrictEqual([])
})
test('it adds license outside the allow list to forbidden changes if it is in both added and removed changes', async () => {
const changes: Changes = [
{...npmChange, change_type: 'removed'},
npmChange,
{...rubyChange, change_type: 'removed'}
]
const {forbidden} = await getInvalidLicenseChanges(changes, {
allow: ['BSD-3-Clause']
})
expect(forbidden).toStrictEqual([npmChange])
})
test('it adds all licenses to unresolved if it is unable to determine the validity', async () => {
const changes: Changes = [
{...npmChange, license: 'Foo'},
{...rubyChange, license: 'Bar'}
]
const invalidLicenses = await getInvalidLicenseChanges(changes, {
allow: ['Apache-2.0']
})
expect(invalidLicenses.forbidden.length).toEqual(0)
expect(invalidLicenses.unlicensed.length).toEqual(0)
expect(invalidLicenses.unresolved.length).toEqual(2)
})
test('it does not filter out changes that are on the exclusions list', async () => {
const changes: Changes = [pipChange, npmChange, rubyChange]
const licensesConfig = {
allow: ['BSD-3-Clause'],
licenseExclusions: ['pkg:pypi/[email protected]', 'pkg:npm/[email protected]']
}
const invalidLicenses = await getInvalidLicenseChanges(
changes,
licensesConfig
)
expect(invalidLicenses.forbidden.length).toEqual(0)
})
test('it does not fail when the packages dont have a valid PURL', async () => {
const emptyPurlChange = pipChange
emptyPurlChange.package_url = ''
const changes: Changes = [emptyPurlChange, npmChange, rubyChange]
const licensesConfig = {
allow: ['BSD-3-Clause'],
licenseExclusions: ['pkg:pypi/[email protected]', 'pkg:npm/[email protected]']
}
const invalidLicenses = await getInvalidLicenseChanges(
changes,
licensesConfig
)
expect(invalidLicenses.forbidden.length).toEqual(1)
})
test('it does filters out changes if they are not on the exclusions list', async () => {
const changes: Changes = [pipChange, npmChange, rubyChange]
const licensesConfig = {
allow: ['BSD-3-Clause'],
licenseExclusions: [
'pkg:pypi/[email protected]',
'pkg:npm/[email protected]'
]
}
const invalidLicenses = await getInvalidLicenseChanges(
changes,
licensesConfig
)
expect(invalidLicenses.forbidden.length).toEqual(2)
expect(invalidLicenses.forbidden[0]).toBe(pipChange)
expect(invalidLicenses.forbidden[1]).toBe(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 {unlicensed} = await getInvalidLicenseChanges(
[nullLicenseChange, rubyChange],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).toHaveBeenNthCalledWith(1, {
owner: 'some-owner',
repo: 'some-repo'
})
expect(unlicensed.length).toEqual(0)
})
test('it does not call licenses API endpoint for change with null license and invalid source_repository_url ', async () => {
const {unlicensed} = await getInvalidLicenseChanges(
[{...npmChange, license: null}],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unlicensed.length).toEqual(1)
})
test('it does not call licenses API endpoint if licenses for all changes are present', async () => {
const {unlicensed} = await getInvalidLicenseChanges(
[npmChange, rubyChange],
{}
)
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unlicensed.length).toEqual(0)
})
test('it does not call licenses API if the package is excluded', async () => {
const {unlicensed} = await getInvalidLicenseChanges([unlicensedChange], {
licenseExclusions: [
'pkg:githubactions/foo-org/actions-repo/.github/workflows/some-action.yml'
]
})
expect(mockOctokit.rest.licenses.getForRepo).not.toHaveBeenCalled()
expect(unlicensed.length).toEqual(0)
})
})