From e15c5a92093d3ac9c8f1d8c7b6260d84cf2adf0e Mon Sep 17 00:00:00 2001 From: Franco Stramana Date: Mon, 26 Feb 2024 10:19:33 -0300 Subject: [PATCH] SCP-138 Adds policy report: copyleft --- __tests__/result-service.test.ts | 18 ++++++- dist/index.js | 76 +++++++++++++++++++++++---- src/policies/copyleft-policy-check.ts | 42 ++++++++++++--- src/policies/policy-check.ts | 4 +- src/services/result.service.ts | 22 ++++++-- src/utils/markdown.util.ts | 13 +++++ 6 files changed, 153 insertions(+), 22 deletions(-) create mode 100644 src/utils/markdown.util.ts diff --git a/__tests__/result-service.test.ts b/__tests__/result-service.test.ts index 4104279..3dc4dd8 100644 --- a/__tests__/result-service.test.ts +++ b/__tests__/result-service.test.ts @@ -1,5 +1,5 @@ import { ScannerResults } from '../src/services/result.interfaces'; -import { getLicenses, License } from '../src/services/result.service'; +import { getComponents, getLicenses, License } from '../src/services/result.service'; const licenseTableTest: { name: string; description: string; content: string; licenses: License[] }[] = [ { @@ -60,3 +60,19 @@ describe('Test Results service', () => { }); } }); + +describe('Test components service', () => { + const t = licenseTableTest[3]; + it(`test c`, () => { + const scannerResults = JSON.parse(t.content) as ScannerResults; + const components = getComponents(scannerResults); + const util = require('util'); + // console.log(util.inspect(compoments, {showHidden: false, depth: null, colors: true})) + + const componentsWithCopyleft = components.filter(component => + component.licenses.some(license => !!license.copyleft) + ); + + console.log(util.inspect(componentsWithCopyleft, { showHidden: false, depth: null, colors: true })); + }); +}); diff --git a/dist/index.js b/dist/index.js index 7587dbe..64a2560 100644 --- a/dist/index.js +++ b/dist/index.js @@ -125850,21 +125850,43 @@ exports.CopyleftPolicyCheck = void 0; const app_config_1 = __nccwpck_require__(29014); const policy_check_1 = __nccwpck_require__(63702); const result_service_1 = __nccwpck_require__(32414); +const markdown_util_1 = __nccwpck_require__(88623); class CopyleftPolicyCheck extends policy_check_1.PolicyCheck { constructor() { super(`${app_config_1.CHECK_NAME}: Copyleft Policy`); } async run(scannerResults) { super.run(scannerResults); - const licenses = (0, result_service_1.getLicenses)(scannerResults); - const hasCopyleft = licenses.some(license => !!license.copyleft); - if (!hasCopyleft) { - this.success('Completed succesfully', 'Not copyleft licenses were found'); + const components = (0, result_service_1.getComponents)(scannerResults); + // Filter copyleft components + const componentsWithCopyleft = components.filter(component => component.licenses.some(license => !!license.copyleft)); + const summary = this.getSummary(componentsWithCopyleft); + const details = this.getDetails(componentsWithCopyleft); + if (componentsWithCopyleft.length === 0) { + return this.success(summary, details); } else { - this.reject('Completed failure', 'Copyleft licenses were found:'); // TODO: create a table with copyleft licenses + return this.reject(summary, details); } } + getSummary(components) { + return components.length === 0 + ? '### :white_check_mark: Policy Pass \n ' + '#### ' + 'Not copyleft components were found' + : '### :x: Policy Fail \n' + '#### ' + components.length + ' component(s) with copyleft licenses were found'; + } + getDetails(components) { + if (components.length === 0) + return ''; + const headers = ['Component', 'Version', 'License', 'URL', 'Copyleft']; + const rows = []; + components.forEach(component => { + component.licenses.forEach(license => { + const copyleftIcon = license.copyleft ? ':x:' : ' '; + rows.push([component.purl, component.version, license.spdxid, `${license.url || ''}`, copyleftIcon]); + }); + }); + return (0, markdown_util_1.generateTable)(headers, rows); + } } exports.CopyleftPolicyCheck = CopyleftPolicyCheck; @@ -125945,10 +125967,10 @@ class PolicyCheck { core.debug(`Running policy check: ${this.checkName}`); } async success(summary, text) { - await this.finish(CONCLUSION.Success, summary, text); + return await this.finish(CONCLUSION.Success, summary, text); } async reject(summary, text) { - await this.finish(inputs.POLICIES_HALT_ON_FAILURE ? CONCLUSION.Failure : CONCLUSION.Neutral, summary, text); + return await this.finish(inputs.POLICIES_HALT_ON_FAILURE ? CONCLUSION.Failure : CONCLUSION.Neutral, summary, text); } async finish(conclusion, summary, text) { core.debug(`Finish policy check: ${this.checkName}. (conclusion=${conclusion})`); @@ -126219,18 +126241,30 @@ function getComponents(results) { for (const c of component) { if (c.id === result_interfaces_1.ComponentID.FILE || c.id === result_interfaces_1.ComponentID.SNIPPET) { components.push({ - purl: c.purl[0] + purl: c.purl[0], + version: c.version, + licenses: c.licenses.map(l => ({ + spdxid: l.name, + copyleft: !l.copyleft ? null : l.copyleft === 'yes' ? true : false, + url: l?.url ? l.url : null, + count: 1 + })) }); } if (c.id === result_interfaces_1.ComponentID.DEPENDENCY) { const dependencies = c.dependencies; for (const d of dependencies) { - components.push({ purl: d.purl }); + components.push({ + purl: d.purl, + version: d.version, + licenses: d.licenses.map(l => ({ spdxid: l.spdx_id, copyleft: null, url: null, count: 1 })) + }); } } } } - return Array.from(new Set(components)); //Remove duplicated + //Merge duplicates purls + return components; } exports.getComponents = getComponents; function getLicenses(results) { @@ -126441,6 +126475,28 @@ async function createCommentOnPR(message) { exports.createCommentOnPR = createCommentOnPR; +/***/ }), + +/***/ 88623: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.generateTable = void 0; +const generateTable = (headers, rows) => { + const COL_SEP = ' | '; + const LINE_BREAK = ' \n '; + let md = COL_SEP + headers.join(COL_SEP) + COL_SEP + LINE_BREAK; + md += COL_SEP + new Array(headers.length).fill('-').join(COL_SEP) + COL_SEP + LINE_BREAK; + rows.forEach(row => { + md += COL_SEP + row.join(COL_SEP) + COL_SEP + LINE_BREAK; + }); + return md; +}; +exports.generateTable = generateTable; + + /***/ }), /***/ 31156: diff --git a/src/policies/copyleft-policy-check.ts b/src/policies/copyleft-policy-check.ts index 742b0b5..a1094a9 100644 --- a/src/policies/copyleft-policy-check.ts +++ b/src/policies/copyleft-policy-check.ts @@ -1,7 +1,8 @@ import { ScannerResults } from '../services/result.interfaces'; import { CHECK_NAME } from '../app.config'; import { PolicyCheck } from './policy-check'; -import { getLicenses } from '../services/result.service'; +import { Component, getComponents, getLicenses } from '../services/result.service'; +import { generateTable } from 'src/utils/markdown.util'; export class CopyleftPolicyCheck extends PolicyCheck { constructor() { @@ -10,13 +11,42 @@ export class CopyleftPolicyCheck extends PolicyCheck { async run(scannerResults: ScannerResults): Promise { super.run(scannerResults); - const licenses = getLicenses(scannerResults); + const components = getComponents(scannerResults); - const hasCopyleft = licenses.some(license => !!license.copyleft); - if (!hasCopyleft) { - this.success('Completed succesfully', 'Not copyleft licenses were found'); + // Filter copyleft components + const componentsWithCopyleft = components.filter(component => + component.licenses.some(license => !!license.copyleft) + ); + + const summary = this.getSummary(componentsWithCopyleft); + const details = this.getDetails(componentsWithCopyleft); + + if (componentsWithCopyleft.length === 0) { + return this.success(summary, details); } else { - this.reject('Completed failure', 'Copyleft licenses were found:'); // TODO: create a table with copyleft licenses + return this.reject(summary, details); } } + + private getSummary(components: Component[]): string { + return components.length === 0 + ? '### :white_check_mark: Policy Pass \n ' + '#### ' + 'Not copyleft components were found' + : '### :x: Policy Fail \n' + '#### ' + components.length + ' component(s) with copyleft licenses were found'; + } + + private getDetails(components: Component[]): string { + if (components.length === 0) return ''; + + const headers = ['Component', 'Version', 'License', 'URL', 'Copyleft']; + const rows: string[][] = []; + + components.forEach(component => { + component.licenses.forEach(license => { + const copyleftIcon = license.copyleft ? ':x:' : ' '; + rows.push([component.purl, component.version, license.spdxid, `${license.url || ''}`, copyleftIcon]); + }); + }); + + return generateTable(headers, rows); + } } diff --git a/src/policies/policy-check.ts b/src/policies/policy-check.ts index a020dfc..3861e47 100644 --- a/src/policies/policy-check.ts +++ b/src/policies/policy-check.ts @@ -55,11 +55,11 @@ export abstract class PolicyCheck { } protected async success(summary: string, text: string): Promise { - await this.finish(CONCLUSION.Success, summary, text); + return await this.finish(CONCLUSION.Success, summary, text); } protected async reject(summary: string, text: string): Promise { - await this.finish(inputs.POLICIES_HALT_ON_FAILURE ? CONCLUSION.Failure : CONCLUSION.Neutral, summary, text); + return await this.finish(inputs.POLICIES_HALT_ON_FAILURE ? CONCLUSION.Failure : CONCLUSION.Neutral, summary, text); } protected async finish(conclusion: CONCLUSION | undefined, summary: string, text: string): Promise { diff --git a/src/services/result.service.ts b/src/services/result.service.ts index 401592b..5686990 100644 --- a/src/services/result.service.ts +++ b/src/services/result.service.ts @@ -12,7 +12,10 @@ export interface License { export interface Component { purl: string; + version: string; + licenses: License[]; } + export function getComponents(results: ScannerResults): Component[] { const components = new Array(); @@ -20,20 +23,33 @@ export function getComponents(results: ScannerResults): Component[] { for (const c of component) { if (c.id === ComponentID.FILE || c.id === ComponentID.SNIPPET) { components.push({ - purl: (c as ScannerComponent).purl[0] + purl: (c as ScannerComponent).purl[0], + version: (c as ScannerComponent).version, + licenses: (c as ScannerComponent).licenses.map(l => ({ + spdxid: l.name, + copyleft: !l.copyleft ? null : l.copyleft === 'yes' ? true : false, + url: l?.url ? l.url : null, + count: 1 + })) }); } if (c.id === ComponentID.DEPENDENCY) { const dependencies = (c as DependencyComponent).dependencies; for (const d of dependencies) { - components.push({ purl: d.purl }); + components.push({ + purl: d.purl, + version: d.version, + licenses: d.licenses.map(l => ({ spdxid: l.spdx_id, copyleft: null, url: null, count: 1 })) + }); } } } } - return Array.from(new Set(components)); //Remove duplicated + //Merge duplicates purls + + return components; } export function getLicenses(results: ScannerResults): License[] { diff --git a/src/utils/markdown.util.ts b/src/utils/markdown.util.ts new file mode 100644 index 0000000..f5eb93f --- /dev/null +++ b/src/utils/markdown.util.ts @@ -0,0 +1,13 @@ +export const generateTable = (headers: string[], rows: string[][]): string => { + const COL_SEP = ' | '; + const LINE_BREAK = ' \n '; + + let md = COL_SEP + headers.join(COL_SEP) + COL_SEP + LINE_BREAK; + md += COL_SEP + new Array(headers.length).fill('-').join(COL_SEP) + COL_SEP + LINE_BREAK; + + rows.forEach(row => { + md += COL_SEP + row.join(COL_SEP) + COL_SEP + LINE_BREAK; + }); + + return md; +};