├── .gitignore ├── src ├── tableFields.js ├── formatter.js ├── utils.js ├── extractor.js └── packageParser.js ├── docs └── npm-readme.md ├── example └── licence-checker.config.js ├── package.json ├── LICENSE ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled output 2 | /dist 3 | /node_modules/ 4 | 5 | # IDEs and editors 6 | /.idea/ 7 | -------------------------------------------------------------------------------- /src/tableFields.js: -------------------------------------------------------------------------------- 1 | const fields = [ 2 | { 3 | label: 'Name', 4 | name: 'name', 5 | }, 6 | { 7 | label: 'Version', 8 | name: 'installedVersion', 9 | }, 10 | { 11 | label: 'Author', 12 | name: 'author', 13 | }, 14 | { 15 | label: 'License type', 16 | name: 'licenseType', 17 | }, 18 | { 19 | label: 'Is allowed', 20 | name: 'isAllowed', 21 | }, 22 | ]; 23 | 24 | module.exports = { fields }; -------------------------------------------------------------------------------- /src/formatter.js: -------------------------------------------------------------------------------- 1 | const { arrayOfObjectsToArrayOfArrays } = require('./utils.js'); 2 | const { fields } = require('./tableFields.js'); 3 | const table = require('text-table'); 4 | 5 | function formatAsTable(dataAsArray) { 6 | let data = arrayOfObjectsToArrayOfArrays(dataAsArray); 7 | let labels = []; 8 | let lines = []; 9 | 10 | // create a labels array and a lines array 11 | // the lines will be the same length as the label's 12 | for (let i = 0; i < fields.length; i++) { 13 | let label = fields[i].label; 14 | labels.push(label); 15 | lines.push('-'.repeat(label.length)); 16 | } 17 | 18 | data.unshift(lines); 19 | data.unshift(labels); 20 | 21 | return table(data); 22 | } 23 | 24 | module.exports = formatAsTable; -------------------------------------------------------------------------------- /docs/npm-readme.md: -------------------------------------------------------------------------------- 1 | How to publish new version 2 | ========================= 3 | 4 | 1. First of all you need to create account on [NPM](https://www.npmjs.com/signup) or login if you already have account 5 | 2. Join to `techfides` organization 6 | 3. Create account on [GitHub](https://github.com/) or login if you already have account 7 | 4. Join to `techfides` organization 8 | 5. Clone [licence-checker](https://github.com/TechFides/tf-licence-checker) project 9 | 6. In your terminal login to NPM with techfides organization scope 10 | 1. `npm login --scope=@techfides` 11 | 7. Change the code a commit changes 12 | 8. Update package version 13 | 1. `npm version [ | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]` 14 | 9. Push changes to git 15 | 10. Publish new package version 16 | 1. `npm publish --access public` -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | function isObject(item) { 3 | return ( 4 | item !== null && (typeof item === 'object' || typeof item === 'function') 5 | ); 6 | } 7 | 8 | function isNullOrUndefined(value) { 9 | return value === undefined || value === null; 10 | } 11 | 12 | function arrayOfObjectsToArrayOfArrays(arrayOfObjects) { 13 | return arrayOfObjects.map((objectItem) => { 14 | let objectAsArray = Object.values(objectItem); 15 | return objectAsArray.map((arrayItem) => 16 | !isNullOrUndefined(arrayItem) ? arrayItem : 'n/a', 17 | ); 18 | }); 19 | } 20 | 21 | function getJsonDataFromFile(path) { 22 | const data = fs.readFileSync(path); 23 | return JSON.parse(data); 24 | } 25 | 26 | module.exports = { isObject, isNullOrUndefined, arrayOfObjectsToArrayOfArrays, getJsonDataFromFile }; -------------------------------------------------------------------------------- /example/licence-checker.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | allowedLicenses: [ 3 | '(Apache-2.0 OR MPL-1.1)', 4 | '(BSD-2-Clause OR WTFPL)', 5 | '(CC-BY-4.0 AND MIT)', 6 | '(MIT AND BSD-3-Clause)', 7 | '(MIT AND CC-BY-3.0)', 8 | '(MIT AND Zlib)', 9 | '(MIT OR Apache-2.0)', 10 | '(MIT OR CC0-1.0)', 11 | '(Unlicense OR Apache-2.0)', 12 | '(WTFPL OR MIT)', 13 | '0BSD', 14 | 'AFLv2.1', 15 | 'Apache 2.0', 16 | 'Apache License, Version 2.0', 17 | 'Apache-2.0', 18 | 'Apache2', 19 | 'BSD-2-Clause', 20 | 'BSD-3-Clause OR MIT', 21 | 'BSD-3-Clause', 22 | 'CC-BY-3.0', 23 | 'CC-BY-4.0', 24 | 'CC0-1.0', 25 | 'ISC', 26 | 'MIT', 27 | 'MIT,Apache2', 28 | 'MPL-1.1', 29 | 'WTFPL', 30 | 'Zlib', 31 | ], 32 | ignoredPackages: [], 33 | package: './package.json', 34 | checkDevDependencies: true, 35 | } 36 | 37 | module.exports = config; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tf-licence-checker", 3 | "version": "1.1.9", 4 | "main": "index.js", 5 | "type": "commonjs", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "bin": { 10 | "tf-licence-checker": "index.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+ssh://git@github.com/TechFides/tf-licence-checker.git" 15 | }, 16 | "dependencies": { 17 | "text-table": "^0.2.0", 18 | "yargs": "^17.6.2" 19 | }, 20 | "keywords": [], 21 | "author": "TechFides Solutions s.r.o.", 22 | "license": "MIT", 23 | "description": "Check allowed licences in your project", 24 | "devDependencies": { 25 | "@types/yargs": "^17.0.22" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/TechFides/tf-licence-checker/issues" 29 | }, 30 | "homepage": "https://github.com/TechFides/tf-licence-checker#readme", 31 | "directories": { 32 | "example": "example" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TechFides 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/extractor.js: -------------------------------------------------------------------------------- 1 | const { isObject } = require('./utils.js'); 2 | 3 | function extractLicense(packageJSONContent) { 4 | const notAvailableText = 'n/a'; 5 | 6 | if (typeof packageJSONContent.license === 'string') { 7 | return packageJSONContent.license; 8 | } 9 | 10 | if (typeof packageJSONContent.license === 'object') { 11 | return packageJSONContent.license.type; 12 | } 13 | 14 | let licenseType; 15 | if (Array.isArray(packageJSONContent.licenses)) { 16 | licenseType = ''; 17 | for (let i = 0; i < packageJSONContent.licenses.length; i++) { 18 | if (i > 0) licenseType += ', '; 19 | 20 | if (typeof packageJSONContent.licenses[i] === 'string') { 21 | licenseType += packageJSONContent.licenses[i]; 22 | } else { 23 | licenseType += packageJSONContent.licenses[i].type; 24 | } 25 | } 26 | if (licenseType.length === 0) { 27 | licenseType = notAvailableText; 28 | } 29 | } else { 30 | licenseType = notAvailableText; 31 | } 32 | return licenseType; 33 | } 34 | 35 | function extractAuthor(packageJSONContent) { 36 | let author = 'n/a'; 37 | if (isObject(packageJSONContent.author)) { 38 | author = packageJSONContent.author.name || ''; 39 | if (packageJSONContent.author.email) { 40 | if (author.length > 0) { 41 | author += ' '; 42 | } 43 | author += packageJSONContent.author.email; 44 | } 45 | 46 | if (packageJSONContent.author.url) { 47 | if (author.length > 0) { 48 | author += ' '; 49 | } 50 | author += packageJSONContent.author.url; 51 | } 52 | } else { 53 | if ( 54 | typeof packageJSONContent.author === 'string' || 55 | packageJSONContent.author instanceof String 56 | ) { 57 | author = packageJSONContent.author; 58 | } 59 | } 60 | 61 | return author; 62 | } 63 | 64 | module.exports = { extractLicense, extractAuthor }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const args = require('yargs').argv; 6 | const formatAsTable = require('./src/formatter.js'); 7 | const { getJsonDataFromFile, isNullOrUndefined } = require('./src/utils.js'); 8 | const { addPackagesToIndex, addLocalPackageData } = require('./src/packageParser.js'); 9 | 10 | const configPath = args.config; 11 | let config; 12 | 13 | if (configPath && fs.existsSync(path.resolve(process.cwd(), configPath))) { 14 | config = require(path.resolve(process.cwd(), configPath)); 15 | } else { 16 | config = require('./example/licence-checker.config.js'); 17 | } 18 | 19 | const packagePath = config.package || './package.json'; 20 | 21 | const resolvedPackageJson = path.resolve(process.cwd(), packagePath); 22 | 23 | let packageJson; 24 | if (fs.existsSync(resolvedPackageJson)) { 25 | packageJson = getJsonDataFromFile(resolvedPackageJson); 26 | } else { 27 | throw new Error( 28 | `Error: the file '${resolvedPackageJson}' is required to get installed versions of packages`, 29 | ); 30 | } 31 | 32 | if (!config.allowedLicenses) { 33 | throw new Error( 34 | `Error: the file '${configPath}' not contain field: "allowedLicenses"`, 35 | ); 36 | } 37 | 38 | let depsIndex = addPackagesToIndex(packageJson.dependencies); 39 | 40 | if (!isNullOrUndefined(packageJson.devDependencies) && config.checkDevDependencies) { 41 | depsIndex = depsIndex.concat(addPackagesToIndex(packageJson.devDependencies)); 42 | } 43 | 44 | const packagesData = depsIndex.map((dependency) => { 45 | return addLocalPackageData(dependency, process.cwd(), config); 46 | }); 47 | 48 | console.log(formatAsTable(packagesData)); 49 | 50 | let failJob = false; 51 | packagesData.forEach((licence) => { 52 | if (!licence.isAllowed) { 53 | const moduleInfo = (licenseItem) => `MODULE NAME: ${licenseItem.name} 54 | | LICENSE: ${licenseItem.licenseType} 55 | | AUTHOR: ${licenseItem.author} 56 | | VERSION: ${licenseItem.installedVersion} 57 | `; 58 | const msg = moduleInfo(licence); 59 | 60 | console.log('\x1b[40m\x1b[31m%s\x1b[0m', `BLACKLISTED LICENSE AT ${msg}`); 61 | failJob = true; 62 | } 63 | }); 64 | 65 | if (failJob) { 66 | process.exit(1); 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | NPM [TechFides](https://team.techfides.cz/) Licence Checker 2 | =================== 3 | 4 | Check your installed packages and their licences against allowed licences list. 5 | 6 | Installation: 7 | ``` 8 | npm install tf-licence-checker --save-dev 9 | ``` 10 | 11 | Create config file in your project 12 | * example: `licence-checker.config.js` 13 | 14 | If licence checker not found config file, default will be used 15 | 16 | Config options 17 | -------------- 18 | * `allowedLicenses` array allowed licences 19 | * `ignoredPackages` ignored packages that will not be included in report 20 | * `package` path to your package.json file, default: `./package.json` 21 | * `checkDevDependencies` include dev dependencies in report 22 | 23 | Config example 24 | -------------- 25 | ``` 26 | const config = { 27 | allowedLicenses: [ 28 | '(Apache-2.0 OR MPL-1.1)', 29 | '(BSD-2-Clause OR WTFPL)', 30 | '(CC-BY-4.0 AND MIT)', 31 | '(MIT AND BSD-3-Clause)', 32 | '(MIT AND CC-BY-3.0)', 33 | '(MIT AND Zlib)', 34 | '(MIT OR Apache-2.0)', 35 | '(MIT OR CC0-1.0)', 36 | '(Unlicense OR Apache-2.0)', 37 | '(WTFPL OR MIT)', 38 | '0BSD', 39 | 'AFLv2.1', 40 | 'Apache 2.0', 41 | 'Apache License, Version 2.0', 42 | 'Apache-2.0', 43 | 'Apache2', 44 | 'BSD-2-Clause', 45 | 'BSD-3-Clause OR MIT', 46 | 'BSD-3-Clause', 47 | 'CC-BY-3.0', 48 | 'CC-BY-4.0', 49 | 'CC0-1.0', 50 | 'ISC', 51 | 'MIT', 52 | 'MIT,Apache2', 53 | 'MPL-1.1', 54 | 'WTFPL', 55 | 'Zlib', 56 | ], 57 | ignoredPackages: [], 58 | package: './package.json', 59 | checkDevDependencies: true, 60 | } 61 | 62 | module.exports = config; 63 | ``` 64 | 65 | Usage 66 | ----- 67 | 68 | Report show table with installed packages and blacklisted licences used in your project. 69 | 70 | Add script to your package.json file: 71 | ``` 72 | "licence-checker": "tf-licence-checker --config={pathToConfig}" 73 | ``` 74 | 75 | or to use default config: 76 | ``` 77 | "licence-checker": "tf-licence-checker" 78 | ``` 79 | 80 | and then run: 81 | ``` 82 | npm run licence-checker 83 | ``` 84 | 85 | Table columns: 86 | * `name` package name 87 | * `version` package version 88 | * `author` package author 89 | * `licence type` package licence type 90 | * `is alllowed` whenever is licence allowed to use 91 | 92 | ``` 93 | Name Version Author License type Is allowed 94 | ---- ------- ------ ------------ ---------- 95 | text-table 0.2.0 James Halliday mail@substack.net http://substack.net MIT true 96 | yargs 17.6.2 n/a MIT true 97 | @types/yargs 17.0.22 n/a MIT true 98 | ag-grid-enterprise 27.3.0 Niall Crosby Commercial false 99 | 100 | BLACKLISTED LICENSE AT MODULE NAME: ag-grid-enterprise 101 | | LICENSE: Commercial 102 | | AUTHOR: Niall Crosby 103 | | VERSION: 27.3.0 104 | ``` 105 | -------------------------------------------------------------------------------- /src/packageParser.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const { getJsonDataFromFile } = require('./utils.js'); 4 | const { 5 | extractAuthor, 6 | extractLicense 7 | } = require('./extractor.js'); 8 | 9 | function addLocalPackageData(dependency, projectRootPath, { allowedLicenses = [], ignoredPackages = [] }) { 10 | const notAvailableText = 'n/a'; 11 | let item = {}; 12 | 13 | let projectPackageJsonPath = projectRootPath; 14 | let oldProjectPackageJsonPath = ''; 15 | 16 | item.installedVersion = notAvailableText; 17 | item.author = notAvailableText; 18 | item.licenseType = notAvailableText; 19 | 20 | let packageFolderName; 21 | if (dependency.alias.length === 0) { 22 | packageFolderName = dependency.fullName; 23 | } else { 24 | packageFolderName = dependency.alias; 25 | } 26 | 27 | do { 28 | const itemPackageJsonPath = path.resolve( 29 | projectPackageJsonPath, 30 | 'node_modules', 31 | packageFolderName, 32 | 'package.json', 33 | ); 34 | 35 | if (fs.existsSync(itemPackageJsonPath)) { 36 | const packageJSONContent = getJsonDataFromFile(itemPackageJsonPath); 37 | if (packageJSONContent?.version !== undefined) { 38 | const licenseType = extractLicense(packageJSONContent); 39 | item = { 40 | name: packageJSONContent.name, 41 | installedVersion: packageJSONContent.version, 42 | author: extractAuthor(packageJSONContent), 43 | licenseType, 44 | isAllowed: 45 | allowedLicenses.includes(licenseType) || 46 | ignoredPackages.includes(packageJSONContent.name), 47 | }; 48 | break; 49 | } 50 | } 51 | oldProjectPackageJsonPath = projectPackageJsonPath; 52 | projectPackageJsonPath = path.dirname(projectPackageJsonPath); 53 | } while (projectPackageJsonPath !== oldProjectPackageJsonPath); 54 | 55 | if (item.installedVersion === notAvailableText) { 56 | console.log('found no package.json file for', dependency.fullName); 57 | } 58 | 59 | return item; 60 | } 61 | 62 | function addPackagesToIndex(packages) { 63 | const packageIndex = []; 64 | // iterate over packages and prepare urls before I call the registry 65 | for (const key in packages) { 66 | let name = key; 67 | let fullName = key; 68 | let alias = ''; 69 | let version = packages[key]; 70 | 71 | if (version.startsWith('npm:')) { 72 | alias = fullName; 73 | const aliasBase = version.substring(4); 74 | const versionSeparator = aliasBase.lastIndexOf('@'); 75 | fullName = aliasBase.substring(0, versionSeparator); 76 | name = fullName; 77 | version = aliasBase.substring(versionSeparator + 1); 78 | } 79 | 80 | let scope = undefined; 81 | if (name.indexOf('@') === 0) { 82 | const scopeSeparator = name.indexOf('/'); 83 | scope = name.substring(1, scopeSeparator); 84 | name = name.substring(scopeSeparator + 1, name.length); 85 | } 86 | 87 | const newEntry = { 88 | fullName: fullName, 89 | alias: alias, 90 | name: name, 91 | version: version, 92 | scope: scope, 93 | }; 94 | 95 | const indexOfNewEntry = packageIndex.findIndex( 96 | (entry) => 97 | entry.name === newEntry.name && 98 | entry.version === newEntry.version && 99 | entry.scope === newEntry.scope, 100 | ); 101 | 102 | if (indexOfNewEntry === -1) { 103 | packageIndex.push(newEntry); 104 | } 105 | } 106 | return packageIndex; 107 | } 108 | 109 | module.exports = { addLocalPackageData, addPackagesToIndex }; --------------------------------------------------------------------------------