Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add allow only option #103

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
42 changes: 31 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,15 +71,19 @@ node_modules/.bin/license-checker scan --failOn <license>

#### 🚩 <a name="options"></a>Options

| Option | Description | Requiered | Type | Default |
|---|---|---|---|---|
| --start | Path of the initial json to look for | false | string | `process.cwd()` |
| --failOn | Fail (exit with code 1) if any package license does not satisfies any license in the provided list | true | string[] | |
| --outputFileName | Name of the report file generated | false | string | `license-report-<timestamp>.md` |
| --errorReportFileName | Name of the error report file generated when a license in the `failOn` option is found | false | string | `license-error-<timestamp>.md` |
| --disableErrorReport | Flag to disable the error report file generation | false | boolean | `false` |
| --disableReport | Flag to disable the report file generation, whether there is an error or not | false | boolean | `false` |
| --customHeader | Name of a text file containing the custom header to add at the start of the generated report | false | string | This application makes use of the following open source packages: |
| Option | Description | Requiered | Type | Default |
|-----------------------|-----------------------------------------------------------------------------------------------------------------------|---|---|---|
| --start | Path of the initial json to look for | false | string | `process.cwd()` |
| --failOn | Fail (exit with code 1) if at least one package license **satisfies** one of the licenses in the provided list | true | string[] | |
| --allowOnly | Fail (exit with code 1) if at least one package license **does not satisfy** one of the licenses in the provided list | true | string[] | |
| --outputFileName | Name of the report file generated | false | string | `license-report-<timestamp>.md` |
| --errorReportFileName | Name of the error report file generated when a license in the `failOn` option is found | false | string | `license-error-<timestamp>.md` |
| --disableErrorReport | Flag to disable the error report file generation | false | boolean | `false` |
| --disableReport | Flag to disable the report file generation, whether there is an error or not | false | boolean | `false` |
| --customHeader | Name of a text file containing the custom header to add at the start of the generated report | false | string | This application makes use of the following open source packages: |


> ❗The options `--failOn` and `--allowOnly` are mutually exclusive. You must use one of them.

## 🧑‍💻 <a name="examples"></a>Examples

Expand All @@ -95,7 +99,7 @@ If the value provided is not SPDX compliant, the process fails (exit error 1).

### scan command

All the values provided in the `failOn` list must be [SPDX](https://spdx.dev/specifications/) compliant. Otherwise, an error will be thrown (exit error 1).
All the values provided in the `failOn` or `allowOnly` list must be [SPDX](https://spdx.dev/specifications/) compliant. Otherwise, an error will be thrown (exit error 1).
Check the [SPDX license list](https://spdx.org/licenses/).

```sh
Expand All @@ -105,14 +109,30 @@ npx @onebeyond/license-checker scan --failOn MIT GPL-1.0+
The input list is transformed into a SPDX expression with the `OR` logical operator. In the example, that is `MIT OR GPL-1.0+`.
If any of the packages' licenses satisfies that expression, the process fails (exit error 1).

SPDX compliance and `OR` input concatenation also apply for the `allowOnly` option:

```sh
npx @onebeyond/license-checker scan --allowOnly MIT GPL-1.0+
```

In this case, all the packages' licenses must be either `MIT` or `GPL-1.0+`.

Arguments to `failOn` and `allowOnly` are not limited to one license. Expressions with logical operators are also accepted:

```sh
npx @onebeyond/license-checker scan --allowOnly "MIT AND Apache-2.0" GPL-1.0+
```

In this example, all the packages' licenses must be either `MIT AND Apache-2.0` **or** `GPL-1.0+`.

## 🔗 Useful links

- [Licensing a repository](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/licensing-a-repository)
- [Choose a license](https://choosealicense.com/appendix/)

## ⚠️ Temporal issue

An issue in `spdx-satisfies` has been found and it's pending resolution. Until then, GFDL 1x licenses are not supported and an error will be thrown if either packages or failOn arguments contain it.
An issue in `spdx-satisfies` has been found, and it's pending resolution. Until then, GFDL 1x licenses are not supported and an error will be thrown if either packages or failOn arguments contain it.

## Contributors ✨

Expand Down
2 changes: 2 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
const yargs = require('yargs')(process.argv.slice(2));
const { checkArgs } = require('./src/utils');

// https://github.com/yargs/yargs/blob/main/docs/advanced.md#commanddirdirectory-opts
module.exports = yargs
.parserConfiguration({ 'camel-case-expansion': false })
.commandDir('commands')
.demandCommand()
.check(checkArgs)
.help()
.alias('help', 'h')
.argv;
9 changes: 7 additions & 2 deletions commands/scan.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ exports.builder = {
default: process.cwd()
},
failOn: {
description: 'fail (exit with code 1) if any package license does not satisfies any license in the provided list',
description: 'fail (exit with code 1) if at least one package license satisfies one of the licenses in the provided list ',
type: 'array',
demandOption: true
conflicts: 'allowOnly'
},
allowOnly: {
description: 'fail (exit with code 1) if at least one package license does not satisfy one of the licenses in the provided list',
type: 'array',
conflicts: 'failOn'
},
outputFileName: {
description: 'name of the output file generated',
Expand Down
16 changes: 10 additions & 6 deletions src/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,22 @@ const check = (license) => {
};

const scan = async (options) => {
const { failOn } = options;
const { failOn, allowOnly } = options;

if (!failOn) throw new Error('Error: You must provide a list of licenses to fail on in the "failOn" option.');
const allowSelected = !!allowOnly;
const licensesList = allowSelected ? allowOnly : failOn;

checkLicenseError(failOn); // @TODO Remove after issue has been solved
checkSPDXCompliance(failOn);
const bannedLicenses = generateSPDXExpression(failOn);
checkLicenseError(licensesList); // @TODO Remove after issue has been solved
checkSPDXCompliance(licensesList);
const spdxLicensesExpression = generateSPDXExpression(licensesList);

const packages = await checker.parsePackages(options.start);
const packageList = getPackageInfoList(packages);

const { forbidden: forbiddenPackages, nonCompliant: invalidPackages } = checkPackagesLicenses(bannedLicenses, packageList);
const {
forbidden: forbiddenPackages,
nonCompliant: invalidPackages
} = checkPackagesLicenses(spdxLicensesExpression, packageList, allowSelected);
if (invalidPackages.length) {
logger.warn(`The following package licenses are not SPDX compliant and cannot be validated:\n${invalidPackages.map(pkg => ` > ${pkg.package} | ${pkg.licenses}`).join('\n')}`);
}
Expand Down
43 changes: 34 additions & 9 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const formatForbiddenLicenseError = licenses => {
[licenses]: !stats[licenses] ? 1 : stats[licenses] + 1
}), {});

const header = `Found ${licenses.length} packages with licenses defined by the --failOn flag:`;
const header = `Found ${licenses.length} packages with licenses defined by the provided option:`;
const lines = Object
.entries(forbiddenLicenseStats)
.map(([license, value]) => ` > ${value} packages with license ${license}`)
Expand All @@ -66,7 +66,7 @@ const checkSPDXCompliance = (licenses = []) => {
const invalidLicenses = licenses.filter(arg => !isSPDXCompliant(arg));
if (invalidLicenses.length) {
throw new Error(
`The following licenses are not SPDX compliant. Please, use the --checkLicense option to validate your input:\n${invalidLicenses.join(' | ')}`
`The following licenses are not SPDX compliant. Please, use the "check" command to validate your input:\n${invalidLicenses.join(' | ')}`
);
}
};
Expand All @@ -80,7 +80,7 @@ const checkLicenseError = (licenses = []) => {
const errorLicenses = licenses.some(isLicenseError);
if (errorLicenses) {
throw new Error(
'Your failOn list contains a GFDL-1.x licenses and they are temporary unallowed. There\'s an issue pending to solve.'
'Your licenses list contains a GFDL-1.x licenses and they are temporary unallowed. There\'s an issue pending to solve.'
);
}
};
Expand All @@ -93,37 +93,62 @@ const checkLicenseError = (licenses = []) => {
*/
const isLicenseError = (license = '') => licensesExceptions.includes(license);

/**
* Filter out licenses from the SPDX complete list depending on the value of the shouldSatisfy flag.
* @param {string} expression - A valid SPDX expression
* @param {boolean} shouldSatisfy - If true, the result will yield all the licenses that satisfies the incoming expression. If false,
* the result will yield any other license that do not match the expression
* @return {string[]} - List of resulting licenses
*/
const getValidLicenses = (expression, shouldSatisfy) => spdxIds.filter(id => {
if (isLicenseError(id)) return false;// @TODO Refactor after issue has been solved
const isSatisfied = satisfiesSPDXLicense(id, expression);
return shouldSatisfy ? isSatisfied : !isSatisfied;
});

/**
* Subtracts the expression from the full list of SPDX ids and check the result (the allowed licenses) against the list of packages.
* If the license of the package itself is not SPDX compliant, the package will be included on the "nonCompliant" list.
* If a package license does not satisfy the allowed SPDX id list, the package will be included on the "forbidden" list.
* @param {string} expression - A SPDX expression
* @param {object[]} packages - A list of packages to be checked against the SPDX expression
* @param {boolean} allow - Determines the license check. If true, forbidden will contain all the packages that
* do not comply with the expression. If false, forbidden will contain all the packages that comply with the expression
* @return {{forbidden: object[], nonCompliant: object[]}} - A couple of lists including the packages that satisfy the SPDX expression
* and the packages with a non SPDX compliant license
*/
const checkPackagesLicenses = (expression, packages) => {
const validSpdxIds = expression && spdxIds.filter(id => !isLicenseError(id) && !satisfiesSPDXLicense(id, expression)); // @TODO Refactor after issue has been solved
const allowedLicensesExp = expression && generateSPDXExpression(validSpdxIds);
const checkPackagesLicenses = (expression, packages, allow = false) => {
const validSpdxIds = getValidLicenses(expression, allow);

const allowedLicensesExp = generateSPDXExpression(validSpdxIds);

return packages.reduce((total, pkg) => {
const { licenses } = pkg;

if (!isSPDXCompliant(licenses)) return { ...total, nonCompliant: [...total.nonCompliant, pkg] };

const isSatisfiedLicense = expression && !satisfiesSPDXLicense(licenses, allowedLicensesExp);
if (isSatisfiedLicense) return { ...total, forbidden: [...total.forbidden, pkg] };
if (!satisfiesSPDXLicense(licenses, allowedLicensesExp)) return { ...total, forbidden: [...total.forbidden, pkg] };

return total;
}, { forbidden: [], nonCompliant: [] });
};

const checkArgs = (args) => {
if (args._[0] === 'scan') {
const { failOn, allowOnly } = args;
if (!failOn && !allowOnly) throw new Error('You need to provide the "failOn" or "allowOnly" option.');
if ((failOn && !failOn.length) || (allowOnly && !allowOnly.length)) throw new Error('You need to provide at least one license.');
}
return true;
};

module.exports = {
getPackageInfoList,
formatForbiddenLicenseError,
generateSPDXExpression,
checkSPDXCompliance,
checkPackagesLicenses,
isLicenseError,
checkLicenseError
checkLicenseError,
checkArgs
};
Loading