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
342 lines
9.4 KiB
TypeScript
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)
|
|
})
|
|
})
|