From f199659a6a39762ca0753d8a2fb41192144f257a Mon Sep 17 00:00:00 2001 From: Kevin Dangoor Date: Thu, 8 May 2025 16:31:46 -0400 Subject: [PATCH] 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 --- __tests__/licenses.test.ts | 25 +++++++++++++++ src/licenses.ts | 65 ++++++++++++++++++++------------------ 2 files changed, 60 insertions(+), 30 deletions(-) diff --git a/__tests__/licenses.test.ts b/__tests__/licenses.test.ts index 2204500..f803219 100644 --- a/__tests__/licenses.test.ts +++ b/__tests__/licenses.test.ts @@ -100,6 +100,20 @@ const complexLicenseChange: Change = { ] } +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/some-action.yml@1.1.1', + license: null, + source_repository_url: 'github.com/some-repo', + scope: 'development', + vulnerabilities: [] +} + jest.mock('@actions/core') const mockOctokit = { @@ -313,4 +327,15 @@ describe('GH License API fallback', () => { 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) + }) }) diff --git a/src/licenses.ts b/src/licenses.ts index 1f20f5f..650cbe5 100644 --- a/src/licenses.ts +++ b/src/licenses.ts @@ -1,6 +1,6 @@ import {Change, Changes} from './schemas' import {octokitClient} from './utils' -import {parsePURL} from './purl' +import {parsePURL, PackageURL} from './purl' import * as spdx from './spdx' /** @@ -36,34 +36,8 @@ export async function getInvalidLicenseChanges( } ) - const groupedChanges = await groupChanges(changes) + const groupedChanges = await groupChanges(changes, licenseExclusions) - // Takes the changes from the groupedChanges object and filters out the ones that are part of the exclusions list - // It does by creating a new PackageURL object from the change and comparing it to the exclusions list - groupedChanges.licensed = groupedChanges.licensed.filter(change => { - if (change.package_url.length === 0) { - return true - } - - const changeAsPackageURL = parsePURL(encodeURI(change.package_url)) - - // We want to find if the licenseExclusion list contains the PackageURL of the Change - // If it does, we want to filter it out and therefore return false - // If it doesn't, we want to keep it and therefore return true - if ( - licenseExclusions !== null && - licenseExclusions !== undefined && - licenseExclusions.findIndex( - exclusion => - exclusion.type === changeAsPackageURL.type && - exclusion.name === changeAsPackageURL.name - ) !== -1 - ) { - return false - } else { - return true - } - }) const licensedChanges: Changes = groupedChanges.licensed const invalidLicenseChanges: InvalidLicenseChanges = { @@ -172,16 +146,47 @@ const truncatedDGLicense = (license: string): boolean => license.length === 255 && !spdx.isValid(license) async function groupChanges( - changes: Changes + changes: Changes, + licenseExclusions: PackageURL[] | null = null ): Promise> { const result: Record = { licensed: [], unlicensed: [] } + let candidateChanges = changes + + // If a package is excluded from license checking, we don't bother trying to + // fetch the license for it and we leave it off of the `licensed` and + // `unlicensed` lists. + if (licenseExclusions !== null && licenseExclusions !== undefined) { + candidateChanges = candidateChanges.filter(change => { + if (change.package_url.length === 0) { + return true + } + + const changeAsPackageURL = parsePURL(encodeURI(change.package_url)) + + // We want to find if the licenseExclusion list contains the PackageURL of the Change + // If it does, we want to filter it out and therefore return false + // If it doesn't, we want to keep it and therefore return true + if ( + licenseExclusions.findIndex( + exclusion => + exclusion.type === changeAsPackageURL.type && + exclusion.name === changeAsPackageURL.name + ) !== -1 + ) { + return false + } else { + return true + } + }) + } + const ghChanges = [] - for (const change of changes) { + for (const change of candidateChanges) { if (change.change_type === 'removed') { continue }