From 99aa988001b22c64313fd5f28231a1401c539a90 Mon Sep 17 00:00:00 2001 From: Gabi Dobocan Date: Fri, 3 Feb 2023 23:35:20 +0200 Subject: [PATCH] feat: output license usage and issues --- src/index.js | 17 +- src/issues/license.js | 164 ++++++++++++++++++ src/issues/licenses.json | 5 + src/{vulnerabilities => issues}/utils.js | 0 .../vulnerabilities.js} | 0 5 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 src/issues/license.js create mode 100644 src/issues/licenses.json rename src/{vulnerabilities => issues}/utils.js (100%) rename src/{vulnerabilities/dependencies.js => issues/vulnerabilities.js} (100%) diff --git a/src/index.js b/src/index.js index 968da94..1b605cf 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,8 @@ const {getDependencyGraph, addDependencyGraphData} = require('sandworm-utils'); -const {getDependencyVulnerabilities} = require('./vulnerabilities/dependencies'); +const {getDependencyVulnerabilities} = require('./issues/vulnerabilities'); +const { getLicenseIssues, getLicenseUsage } = require('./issues/license'); const {buildTree, buildTreemap} = require('./charts'); -const {getReports} = require('./vulnerabilities/utils'); +const {getReports} = require('./issues/utils'); const csv = require('./charts/csv'); const getReport = async ({ @@ -24,6 +25,8 @@ const getReport = async ({ onProgress({type: 'start', stage: 'vulnerabilities'}); let dependencyVulnerabilities; let rootVulnerabilities; + let licenseUsage; + let licenseIssues; try { dependencyVulnerabilities = await getDependencyVulnerabilities({ @@ -41,6 +44,14 @@ const getReport = async ({ } catch (error) { errors.push(error); } + + try { + licenseUsage = await getLicenseUsage({dependencies: dGraph.prodDependencies}); + licenseIssues = await getLicenseIssues({licenseUsage, packageGraph}); + } catch (error) { + errors.push(error); + } + onProgress({type: 'end', stage: 'vulnerabilities'}); const options = { @@ -85,6 +96,8 @@ const getReport = async ({ dependencyGraph: dGraph, dependencyVulnerabilities: dependencyVulnerabilities.filter(({findings: {affects}}) => affects.length), rootVulnerabilities, + licenseUsage, + licenseIssues, svgs, csv: csvData, allDependencies: jsonData, diff --git a/src/issues/license.js b/src/issues/license.js new file mode 100644 index 0000000..4d24e4c --- /dev/null +++ b/src/issues/license.js @@ -0,0 +1,164 @@ +const licenseGroups = require('./licenses.json'); +const {getFindings} = require('./utils'); + +const LICENSE_TYPES = [ + 'Public Domain', + 'Permissive', + 'Weakly Protective', + 'Strongly Protective', + 'Network Protective', + 'Uncategorized', + 'Invalid', +]; + +const DEFAULT_POLICY = { + high: ['cat:Network Protective', 'cat:Strongly Protective'], + moderate: ['cat:Weakly Protective'], +}; + +const typeForLicense = (license) => { + if (!license || license === 'N/A') { + return 'N/A'; + } + if (license.includes(' ')) { + // Parse simple AND/OR SPDX expressions + if ((license.match(/\s/g) || []).length === 2) { + let expressionLicenses; + let condition; + + if (license.match(/ or /i)) { + condition = 'or'; + expressionLicenses = license.replace(/[()]/g, '').split(/ or /i); + } else if (license.match(/ and /i)) { + condition = 'and'; + expressionLicenses = license.replace(/[()]/g, '').split(/ and /i); + } + + if (expressionLicenses) { + const expressionTypes = [ + typeForLicense(expressionLicenses[0]), + typeForLicense(expressionLicenses[1]), + ]; + + if ([expressionTypes[0], expressionTypes[1]].includes('Invalid')) { + return 'Invalid'; + } + + const aggregateExpressionType = + LICENSE_TYPES[ + condition === 'or' + ? Math.min( + LICENSE_TYPES.indexOf(expressionTypes[0]), + LICENSE_TYPES.indexOf(expressionTypes[1]), + ) + : Math.max( + LICENSE_TYPES.indexOf(expressionTypes[0]), + LICENSE_TYPES.indexOf(expressionTypes[1]), + ) + ]; + + return aggregateExpressionType; + } + } + return 'Expression'; + } + return licenseGroups.types.find(({licenses}) => licenses.includes(license))?.type || 'Invalid'; +}; + +module.exports = { + typeForLicense, + getLicenseUsage: ({dependencies = []}) => { + const licenseUsage = dependencies.reduce((agg, {name, version, license}) => { + const licenseName = license || 'N/A'; + + return { + ...agg, + [licenseName]: { + meta: { + type: typeForLicense(licenseName), + }, + dependencies: (agg[licenseName]?.dependencies || []).concat([{name, version}]), + }, + }; + }, {}); + + return licenseUsage; + }, + + getLicenseIssues: ({licenseUsage, packageGraph}) => { + const issues = []; + const allLicenses = Object.keys(licenseUsage); + + if (allLicenses.includes('N/A')) { + issues.push({ + severity: 'critical', + title: 'Dependencies have no specified license', + dependencies: licenseUsage['N/A'].dependencies, + }); + } + + Object.entries(licenseUsage).forEach(([licenseName, {meta, dependencies}]) => { + const licenseType = meta?.type; + + if (!licenseName.includes(' ') && !['Expression', 'N/A'].includes(licenseType)) { + if (!licenseGroups.osiApproved.includes(licenseName)) { + issues.push({ + severity: 'low', + title: `Dependencies use a license that is not OSI approved: ${licenseName}`, + dependencies, + }); + } + } + + if (!licenseType || licenseType === 'Uncategorized') { + issues.push({ + severity: 'high', + title: `Dependencies use an atypical license: ${licenseName}`, + dependencies, + }); + } else if (licenseType === 'Invalid') { + issues.push({ + severity: 'high', + title: `Dependencies use an invalid SPDX license: ${licenseName}`, + dependencies, + }); + } else if (licenseType === 'Expression') { + issues.push({ + severity: 'high', + title: `Dependencies use a custom license expression: ${licenseName}`, + dependencies, + }); + } + + Object.entries(DEFAULT_POLICY).forEach(([severity, includes]) => { + if (includes.includes(licenseName)) { + issues.push({ + severity, + title: `Dependencies use potentially problematic license: ${licenseName}`, + dependencies, + }); + } else if (includes.includes(`cat:${licenseType}`)) { + issues.push({ + severity, + title: `Dependencies use ${licenseType} license: ${licenseName}`, + dependencies, + }); + } + }); + }); + + return issues.reduce( + (agg, {severity, title, dependencies}) => + agg.concat( + dependencies.map(({name, version}) => ({ + severity, + title, + name, + version, + findings: getFindings(packageGraph, name, version), + })), + ), + [], + ); + }, +}; diff --git a/src/issues/licenses.json b/src/issues/licenses.json new file mode 100644 index 0000000..98c8064 --- /dev/null +++ b/src/issues/licenses.json @@ -0,0 +1,5 @@ +{ + "types": [{"type":"Network Protective","licenses":["AGPL-1.0","AGPL-1.0-only","AGPL-1.0-or-later","AGPL-3.0","AGPL-3.0-only","AGPL-3.0-or-later","CPAL-1.0","EUPL-1.0","EUPL-1.1","EUPL-1.2","RPL-1.1","RPL-1.5","SSPL-1.0"]},{"type":"Uncategorized","licenses":["AAL","Abstyles","Adobe-2006","Adobe-Glyph","ADSL","Afmparse","Aladdin","AMDPLPA","AML","AMPAS","ANTLR-PD","ANTLR-PD-fallback","APAFML","App-s2p","Arphic-1999","Baekmuk","Bahyph","Barr","Bitstream-Vera","blessing","BlueOak-1.0.0","Borceux","BSD-1-Clause","BSD-2-Clause-Patent","BSD-2-Clause-Views","BSL-1.0","BUSL-1.1","bzip2-1.0.5","bzip2-1.0.6","C-UDA-1.0","CAL-1.0","CAL-1.0-Combined-Work-Exception","Caldera","CC-BY-1.0","CC-BY-2.0","CC-BY-2.5","CC-BY-2.5-AU","CC-BY-3.0","CC-BY-3.0-AT","CC-BY-3.0-DE","CC-BY-3.0-IGO","CC-BY-3.0-NL","CC-BY-3.0-US","CC-BY-4.0","CC-BY-NC-1.0","CC-BY-NC-2.0","CC-BY-NC-2.5","CC-BY-NC-3.0","CC-BY-NC-3.0-DE","CC-BY-NC-4.0","CC-BY-NC-ND-1.0","CC-BY-NC-ND-2.0","CC-BY-NC-ND-2.5","CC-BY-NC-ND-3.0","CC-BY-NC-ND-3.0-DE","CC-BY-NC-ND-3.0-IGO","CC-BY-NC-ND-4.0","CC-BY-NC-SA-1.0","CC-BY-NC-SA-2.0","CC-BY-NC-SA-2.0-FR","CC-BY-NC-SA-2.0-UK","CC-BY-NC-SA-2.5","CC-BY-NC-SA-3.0","CC-BY-NC-SA-3.0-DE","CC-BY-NC-SA-3.0-IGO","CC-BY-NC-SA-4.0","CC-BY-ND-1.0","CC-BY-ND-2.0","CC-BY-ND-2.5","CC-BY-ND-3.0","CC-BY-ND-3.0-DE","CC-BY-ND-4.0","CC-BY-SA-1.0","CC-BY-SA-2.0","CC-BY-SA-2.0-UK","CC-BY-SA-2.1-JP","CC-BY-SA-2.5","CC-BY-SA-3.0","CC-BY-SA-3.0-AT","CC-BY-SA-3.0-DE","CC-BY-SA-4.0","CC-PDDC","CDL-1.0","CDLA-Permissive-1.0","CDLA-Permissive-2.0","CDLA-Sharing-1.0","CERN-OHL-1.1","CERN-OHL-1.2","CERN-OHL-P-2.0","CERN-OHL-S-2.0","CERN-OHL-W-2.0","checkmk","COIL-1.0","Community-Spec-1.0","copyleft-next-0.3.0","copyleft-next-0.3.1","CPOL-1.02","Crossword","CrystalStacker","Cube","curl","D-FSL-1.0","diffmark","DL-DE-BY-2.0","DOC","Dotseqn","DRL-1.0","dvipdfm","ECL-1.0","eCos-2.0","eGenix","Elastic-2.0","Entessa","EPICS","etalab-2.0","EUDatagrid","Eurosym","Fair","FDK-AAC","Frameworx-1.0","FreeBSD-DOC","FreeImage","FSFAP","FSFUL","FSFULLR","FSFULLRWD","GD","GFDL-1.1","GFDL-1.1-invariants-only","GFDL-1.1-invariants-or-later","GFDL-1.1-no-invariants-only","GFDL-1.1-no-invariants-or-later","GFDL-1.1-only","GFDL-1.1-or-later","GFDL-1.2","GFDL-1.2-invariants-only","GFDL-1.2-invariants-or-later","GFDL-1.2-no-invariants-only","GFDL-1.2-no-invariants-or-later","GFDL-1.2-only","GFDL-1.2-or-later","GFDL-1.3","GFDL-1.3-invariants-only","GFDL-1.3-invariants-or-later","GFDL-1.3-no-invariants-only","GFDL-1.3-no-invariants-or-later","GFDL-1.3-only","GFDL-1.3-or-later","Giftware","GL2PS","Glide","Glulxe","GLWTPL","gnuplot","HaskellReport","Hippocratic-2.1","HPND","HPND-sell-variant","HTMLTIDY","IBM-pibs","IJG","ImageMagick","Imlib2","Info-ZIP","Intel","Intel-ACPI","Jam","JPNIC","Knuth-CTAN","LAL-1.2","LAL-1.3","Latex2e","Leptonica","LGPLLR","Libpng","libpng-2.0","libselinux-1.0","libtiff","libutil-David-Nugent","LiLiQ-P-1.1","LiLiQ-R-1.1","LiLiQ-Rplus-1.1","Linux-man-pages-copyleft","Linux-OpenIB","LPL-1.0","LPL-1.02","LZMA-SDK-9.11-to-9.20","LZMA-SDK-9.22","MakeIndex","Minpack","MirOS","MIT-Modern-Variant","MIT-open-group","MITNFA","mpi-permissive","mpich2","mplus","MS-LPL","MTLL","MulanPSL-1.0","MulanPSL-2.0","Multics","Mup","NAIST-2003","NBPL-1.0","NCGL-UK-2.0","NCSA","Net-SNMP","NetCDF","Newsletr","NGPL","NICTA-1.0","NIST-PD","NIST-PD-fallback","NLOD-1.0","NLOD-2.0","Noweb","NPL-1.0","NPL-1.1","NPOSL-3.0","NRL","NTP","NTP-0","Nunit","O-UDA-1.0","OCCT-PL","OCLC-2.0","ODC-By-1.0","OFL-1.0","OFL-1.0-no-RFN","OFL-1.0-RFN","OFL-1.1","OFL-1.1-no-RFN","OFL-1.1-RFN","OGC-1.0","OGDL-Taiwan-1.0","OGL-Canada-2.0","OGL-UK-1.0","OGL-UK-2.0","OGL-UK-3.0","OGTSL","OML","OPL-1.0","OPUBL-1.0","OSET-PL-2.1","Parity-6.0.0","Parity-7.0.0","Plexus","PolyForm-Noncommercial-1.0.0","PolyForm-Small-Business-1.0.0","PostgreSQL","PSF-2.0","psfrag","psutils","Python-2.0.1","Qhull","Rdisc","RPSL-1.0","RSA-MD","Saxpath","SCEA","SchemeReport","Sendmail-8.23","SGI-B-1.0","SGI-B-1.1","SGI-B-2.0","SHL-0.5","SHL-0.51","SimPL-2.0","SMLNJ","SMPPL","SNIA","Spencer-86","Spencer-94","Spencer-99","SSH-OpenSSH","SSH-short","StandardML-NJ","SWL","TAPR-OHL-1.0","TCP-wrappers","TMate","TORQUE-1.1","TOSL","TU-Berlin-1.0","TU-Berlin-2.0","UCL-1.0","Unicode-DFS-2015","Unicode-DFS-2016","Unicode-TOU","UPL-1.0","VOSTROM","VSL-1.0","Wsuipa","wxWindows","X11-distribute-modifications-variant","Xerox","Xnet","xpp","XSkat","Zed","Zend-2.0"]},{"type":"Permissive","licenses":["0BSD","AFL-1.1","AFL-1.2","AFL-2.0","AFL-2.1","AFL-3.0","Apache-1.0","Apache-1.1","Apache-2.0","APSL-1.0","Artistic-2.0","Beerware","BSD-2-Clause","BSD-2-Clause-FreeBSD","BSD-2-Clause-NetBSD","BSD-3-Clause","BSD-3-Clause-Attribution","BSD-3-Clause-Clear","BSD-3-Clause-LBNL","BSD-3-Clause-Modification","BSD-3-Clause-No-Military-License","BSD-3-Clause-No-Nuclear-License","BSD-3-Clause-No-Nuclear-License-2014","BSD-3-Clause-No-Nuclear-Warranty","BSD-3-Clause-Open-MPI","BSD-4-Clause","BSD-4-Clause-Shortened","BSD-4-Clause-UC","BSD-Protection","BSD-Source-Code","CECILL-1.0","CECILL-1.1","CECILL-2.0","CECILL-2.1","CECILL-B","CNRI-Jython","CNRI-Python","CNRI-Python-GPL-Compatible","Condor-1.1","DSDP","ECL-2.0","EFL-1.0","EFL-2.0","FTL","ICU","iMatix","ISC","JasPer-2.0","JSON","MIT","MIT-0","MIT-advertising","MIT-CMU","MIT-enna","MIT-feh","Naumen","NLPL","OLDAP-1.1","OLDAP-1.2","OLDAP-1.3","OLDAP-1.4","OLDAP-2.0","OLDAP-2.0.1","OLDAP-2.1","OLDAP-2.2","OLDAP-2.2.1","OLDAP-2.2.2","OLDAP-2.3","OLDAP-2.4","OLDAP-2.5","OLDAP-2.6","OLDAP-2.7","OLDAP-2.8","OpenSSL","PHP-3.0","PHP-3.01","Python-2.0","Ruby","Sendmail","TCL","W3C","W3C-19980720","W3C-20150513","WTFPL","X11","XFree86-1.1","xinetd","Zlib","zlib-acknowledgement","ZPL-1.1","ZPL-2.0","ZPL-2.1"]},{"type":"Strongly Protective","licenses":["APL-1.0","CPL-1.0","EPL-1.0","EPL-2.0","GPL-1.0","GPL-1.0+","GPL-1.0-only","GPL-1.0-or-later","GPL-2.0","GPL-2.0+","GPL-2.0-only","GPL-2.0-or-later","GPL-2.0-with-autoconf-exception","GPL-2.0-with-bison-exception","GPL-2.0-with-classpath-exception","GPL-2.0-with-font-exception","GPL-2.0-with-GCC-exception","GPL-3.0","GPL-3.0+","GPL-3.0-only","GPL-3.0-or-later","GPL-3.0-with-autoconf-exception","GPL-3.0-with-GCC-exception","IPA","MS-RL","ODbL-1.0","OSL-1.0","OSL-1.1","OSL-2.0","OSL-2.1","OSL-3.0","QPL-1.0","Vim"]},{"type":"Public Domain","licenses":["CC0-1.0","PDDL-1.0","SAX-PD","Unlicense"]},{"type":"Weakly Protective","licenses":["APSL-1.1","APSL-1.2","APSL-2.0","Artistic-1.0","Artistic-1.0-cl8","Artistic-1.0-Perl","BitTorrent-1.0","BitTorrent-1.1","CATOSL-1.1","CDDL-1.0","CDDL-1.1","CECILL-C","ClArtistic","CUA-OPL-1.0","ErlPL-1.1","gSOAP-1.3b","Interbase-1.0","IPL-1.0","LGPL-2.0","LGPL-2.0+","LGPL-2.0-only","LGPL-2.0-or-later","LGPL-2.1","LGPL-2.1+","LGPL-2.1-only","LGPL-2.1-or-later","LGPL-3.0","LGPL-3.0+","LGPL-3.0-only","LGPL-3.0-or-later","LPPL-1.0","LPPL-1.1","LPPL-1.2","LPPL-1.3a","LPPL-1.3c","Motosoto","MPL-1.0","MPL-1.1","MPL-2.0","MPL-2.0-no-copyleft-exception","MS-PL","NASA-1.3","Nokia","NOSL","RHeCos-1.1","RSCPL","SISSL","SISSL-1.2","Sleepycat","SPL-1.0","SugarCRM-1.1.3","Watcom-1.0","YPL-1.0","YPL-1.1","Zimbra-1.3","Zimbra-1.4"]}], + "osiApproved": ["0BSD","AAL","AFL-1.1","AFL-1.2","AFL-2.0","AFL-2.1","AFL-3.0","AGPL-3.0","AGPL-3.0-only","AGPL-3.0-or-later","Apache-1.1","Apache-2.0","APL-1.0","APSL-1.0","APSL-1.1","APSL-1.2","APSL-2.0","Artistic-1.0","Artistic-1.0-cl8","Artistic-1.0-Perl","Artistic-2.0","BSD-1-Clause","BSD-2-Clause","BSD-2-Clause-Patent","BSD-3-Clause","BSD-3-Clause-LBNL","BSL-1.0","CAL-1.0","CAL-1.0-Combined-Work-Exception","CATOSL-1.1","CDDL-1.0","CECILL-2.1","CERN-OHL-P-2.0","CERN-OHL-S-2.0","CERN-OHL-W-2.0","CNRI-Python","CPAL-1.0","CPL-1.0","CUA-OPL-1.0","ECL-1.0","ECL-2.0","EFL-1.0","EFL-2.0","Entessa","EPL-1.0","EPL-2.0","EUDatagrid","EUPL-1.1","EUPL-1.2","Fair","Frameworx-1.0","GPL-2.0","GPL-2.0+","GPL-2.0-only","GPL-2.0-or-later","GPL-3.0","GPL-3.0+","GPL-3.0-only","GPL-3.0-or-later","GPL-3.0-with-GCC-exception","HPND","Intel","IPA","IPL-1.0","ISC","Jam","LGPL-2.0","LGPL-2.0+","LGPL-2.0-only","LGPL-2.0-or-later","LGPL-2.1","LGPL-2.1+","LGPL-2.1-only","LGPL-2.1-or-later","LGPL-3.0","LGPL-3.0+","LGPL-3.0-only","LGPL-3.0-or-later","LiLiQ-P-1.1","LiLiQ-R-1.1","LiLiQ-Rplus-1.1","LPL-1.0","LPL-1.02","LPPL-1.3c","MirOS","MIT","MIT-0","MIT-Modern-Variant","Motosoto","MPL-1.0","MPL-1.1","MPL-2.0","MPL-2.0-no-copyleft-exception","MS-PL","MS-RL","MulanPSL-2.0","Multics","NASA-1.3","Naumen","NCSA","NGPL","Nokia","NPOSL-3.0","NTP","OCLC-2.0","OFL-1.1","OFL-1.1-no-RFN","OFL-1.1-RFN","OGTSL","OLDAP-2.8","OSET-PL-2.1","OSL-1.0","OSL-2.0","OSL-2.1","OSL-3.0","PHP-3.0","PHP-3.01","PostgreSQL","Python-2.0","QPL-1.0","RPL-1.1","RPL-1.5","RPSL-1.0","RSCPL","SimPL-2.0","SISSL","Sleepycat","SPL-1.0","UCL-1.0","Unicode-DFS-2016","Unlicense","UPL-1.0","VSL-1.0","W3C","Watcom-1.0","wxWindows","Xnet","Zlib","ZPL-2.0","ZPL-2.1"], + "deprecated": ["AGPL-1.0","AGPL-3.0","BSD-2-Clause-FreeBSD","BSD-2-Clause-NetBSD","bzip2-1.0.5","eCos-2.0","GFDL-1.1","GFDL-1.2","GFDL-1.3","GPL-1.0","GPL-1.0+","GPL-2.0","GPL-2.0+","GPL-2.0-with-autoconf-exception","GPL-2.0-with-bison-exception","GPL-2.0-with-classpath-exception","GPL-2.0-with-font-exception","GPL-2.0-with-GCC-exception","GPL-3.0","GPL-3.0+","GPL-3.0-with-autoconf-exception","GPL-3.0-with-GCC-exception","LGPL-2.0","LGPL-2.0+","LGPL-2.1","LGPL-2.1+","LGPL-3.0","LGPL-3.0+","Nunit","StandardML-NJ","wxWindows"] +} \ No newline at end of file diff --git a/src/vulnerabilities/utils.js b/src/issues/utils.js similarity index 100% rename from src/vulnerabilities/utils.js rename to src/issues/utils.js diff --git a/src/vulnerabilities/dependencies.js b/src/issues/vulnerabilities.js similarity index 100% rename from src/vulnerabilities/dependencies.js rename to src/issues/vulnerabilities.js