From 5866fb93dc193e389d5f529755a3afdee5b1e333 Mon Sep 17 00:00:00 2001 From: Franco Stramana Date: Tue, 23 Jan 2024 11:58:42 -0300 Subject: [PATCH] SCP-52 Adds reports table --- .github/linters/.eslintrc.yml | 3 +- dist/index.js | 249 ++++++++++++++++----------- src/main.ts | 11 +- src/policies/license-policy-check.ts | 14 +- src/services/report.service.ts | 13 ++ src/services/result.interfaces.ts | 142 +++++++-------- src/services/result.service.ts | 36 ++-- src/services/result.test.ts | 18 +- 8 files changed, 276 insertions(+), 210 deletions(-) create mode 100644 src/services/report.service.ts diff --git a/.github/linters/.eslintrc.yml b/.github/linters/.eslintrc.yml index a8d186c..9d6fc76 100644 --- a/.github/linters/.eslintrc.yml +++ b/.github/linters/.eslintrc.yml @@ -80,5 +80,6 @@ rules: '@typescript-eslint/semi': ['error', 'always'], '@typescript-eslint/space-before-function-paren': 'off', '@typescript-eslint/type-annotation-spacing': 'error', - '@typescript-eslint/unbound-method': 'error' + '@typescript-eslint/unbound-method': 'error', + 'github/array-foreach' : 'off' } diff --git a/dist/index.js b/dist/index.js index 6f51b2f..dc7357a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -30149,7 +30149,112 @@ exports.CHECK_NAME = 'SCANOSS Policy Checker'; /***/ }), -/***/ 1958: +/***/ 399: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.run = void 0; +const core = __importStar(__nccwpck_require__(2186)); +const exec = __importStar(__nccwpck_require__(1514)); +const result_service_1 = __nccwpck_require__(2414); +const github_utils_1 = __nccwpck_require__(7889); +const license_policy_check_1 = __nccwpck_require__(2266); +const report_service_1 = __nccwpck_require__(2467); +/** + * The main function for the action. + * @returns {Promise} Resolves when the action is complete. + */ +async function run() { + try { + const repoDir = process.env.GITHUB_WORKSPACE; + const outputPath = 'results.json'; + // create policies + const policies = [new license_policy_check_1.LicensePolicyCheck()]; + policies.forEach(async (policy) => policy.start()); + // options to get standar output + const options = {}; + let output = ''; + options.listeners = { + stdout: (data) => { + output += data.toString(); + }, + stderr: (data) => { + output += data.toString(); + } + }; + options.silent = true; + // run scan + await exec.exec(`docker run -v "${repoDir}":"/scanoss" ghcr.io/scanoss/scanoss-py:v1.9.0 scan . --output ${outputPath}`, [], options); + const scannerResults = await (0, result_service_1.readResult)(outputPath); + const licenses = (0, result_service_1.getLicenses)(scannerResults); + // create reports + const licensesReport = (0, report_service_1.getLicensesReport)(licenses); + // run policies // TODO: define run action for each policy + policies.forEach(async (policy) => await policy.run(licensesReport)); + if ((0, github_utils_1.isPullRequest)()) { + (0, github_utils_1.createCommentOnPR)(licensesReport); + } + // set outputs for other workflow steps to use + core.setOutput('licenses', licenses.toString()); + core.setOutput('output-command', output); + core.setOutput('result-filepath', outputPath); + } + catch (error) { + // fail the workflow run if an error occurs + if (error instanceof Error) + core.setFailed(error.message); + } +} +exports.run = run; + + +/***/ }), + +/***/ 2266: +/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.LicensePolicyCheck = void 0; +const app_config_1 = __nccwpck_require__(9014); +const policy_check_1 = __nccwpck_require__(3702); +class LicensePolicyCheck extends policy_check_1.PolicyCheck { + constructor() { + super(`${app_config_1.CHECK_NAME}: Licenses Policy`); + } +} +exports.LicensePolicyCheck = LicensePolicyCheck; + + +/***/ }), + +/***/ 3702: /***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; @@ -30178,12 +30283,12 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.GitHubCheck = void 0; +exports.PolicyCheck = void 0; const github_1 = __nccwpck_require__(5438); const core = __importStar(__nccwpck_require__(2186)); const github_utils_1 = __nccwpck_require__(7889); const NO_INITIALIZATE = -1; -class GitHubCheck { +class PolicyCheck { octokit; // TODO: type from actions/github ? checkName; checkRunId; @@ -30193,18 +30298,18 @@ class GitHubCheck { this.checkName = checkName; this.checkRunId = NO_INITIALIZATE; } - async present() { + async start() { // Promise const result = await this.octokit.rest.checks.create({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, name: this.checkName, - head_sha: (0, github_utils_1.getSha)() + head_sha: (0, github_utils_1.getSHA)() }); this.checkRunId = result.data.id; return result.data; } - async finish(conclusion, summary, text) { + async run(text) { // Promise if (this.checkRunId === NO_INITIALIZATE) throw new Error(`Error on finish. Check "${this.checkName}" is not created.`); @@ -30213,103 +30318,38 @@ class GitHubCheck { repo: github_1.context.repo.repo, check_run_id: this.checkRunId, status: 'completed', - conclusion, + conclusion: 'success', output: { title: this.checkName, - summary, + summary: 'Policy checker completed successfully', text } }); return result.data; } } -exports.GitHubCheck = GitHubCheck; +exports.PolicyCheck = PolicyCheck; /***/ }), -/***/ 399: -/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { +/***/ 2467: +/***/ ((__unused_webpack_module, exports) => { "use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); - __setModuleDefault(result, mod); - return result; -}; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.run = void 0; -const core = __importStar(__nccwpck_require__(2186)); -const exec = __importStar(__nccwpck_require__(1514)); -const result_service_1 = __nccwpck_require__(2414); -const app_config_1 = __nccwpck_require__(9014); -const github_check_1 = __nccwpck_require__(1958); -const github_utils_1 = __nccwpck_require__(7889); -/** - * The main function for the action. - * @returns {Promise} Resolves when the action is complete. - */ -async function run() { - try { - const repoDir = process.env.GITHUB_WORKSPACE; - const outputPath = 'results.json'; - // init check - const check = new github_check_1.GitHubCheck(app_config_1.CHECK_NAME); - await check.present(); - // Declara las opciones para ejecutar el exec - const options = {}; - let output = ''; - options.listeners = { - stdout: (data) => { - output += data.toString(); - }, - stderr: (data) => { - output += data.toString(); - } - }; - options.silent = true; - // run scan - await exec.exec(`docker run -v "${repoDir}":"/scanoss" ghcr.io/scanoss/scanoss-py:v1.9.0 scan . --output ${outputPath}`, [], options); - const scannerResults = await (0, result_service_1.readResult)(outputPath); - const licenses = (0, result_service_1.getLicenses)(scannerResults); - // get reports - const licenseReport = 'Here are the licenses found:'; - // finish - await check.finish('success', 'Code analysis completed successfully', licenseReport); - if ((0, github_utils_1.isPullRequest)()) { - (0, github_utils_1.createCommentOnPR)(licenseReport); - } - // set outputs for other workflow steps to use - core.setOutput('licenses', licenses.toString()); - core.setOutput('output-command', output); - core.setOutput('result-filepath', outputPath); - } - catch (error) { - // fail the workflow run if an error occurs - if (error instanceof Error) - core.setFailed(error.message); - } +exports.getLicensesReport = void 0; +function getLicensesReport(licenses) { + let markdownTable = '| SPDX ID | Copyleft | URL |\n'; + markdownTable += '| ------- | -------- | --- |\n'; + licenses.forEach(license => { + const copyleftIcon = license.copyleft ? ':heavy_check_mark:' : ':x:'; + markdownTable += `| ${license.spdxid} | ${copyleftIcon} | ${license.url ? `[Link](${license.url})` : ''} |\n`; + }); + return markdownTable; } -exports.run = run; +exports.getLicensesReport = getLicensesReport; /***/ }), @@ -30370,26 +30410,39 @@ async function readResult(filepath) { } exports.readResult = readResult; function getLicenses(results) { - // { spdxid, copyleft, url } - const licenses = new Set(); + const licenses = new Array(); for (const component of Object.values(results)) { for (const c of component) { + if (c.id === result_interfaces_1.ComponentID.FILE || c.id === result_interfaces_1.ComponentID.SNIPPET) { + for (const l of c.licenses) { + licenses.push({ + spdxid: l.name, + copyleft: !l.copyleft ? null : l.copyleft === 'yes' ? true : false, + url: l?.url ? l.url : null + }); + } + } if (c.id === result_interfaces_1.ComponentID.DEPENDENCY) { const dependencies = c.dependencies; for (const d of dependencies) { for (const l of d.licenses) { - licenses.add(l.spdx_id); + if (!l.spdx_id) + continue; + licenses.push({ spdxid: l.spdx_id, copyleft: null, url: null }); } } } - if (c.id === result_interfaces_1.ComponentID.FILE || c.id === result_interfaces_1.ComponentID.SNIPPET) { - for (const l of c.licenses) { - licenses.add(l.name); - } - } } } - return Array.from(licenses); + const seenSpdxIds = new Set(); + const uniqueLicenses = licenses.filter(license => { + if (!seenSpdxIds.has(license.spdxid)) { + seenSpdxIds.add(license.spdxid); + return true; + } + return false; + }); + return uniqueLicenses; } exports.getLicenses = getLicenses; @@ -30425,7 +30478,7 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.createCommentOnPR = exports.getSha = exports.isPullRequest = void 0; +exports.createCommentOnPR = exports.getSHA = exports.isPullRequest = void 0; const github_1 = __nccwpck_require__(5438); const core = __importStar(__nccwpck_require__(2186)); const prEvents = ['pull_request', 'pull_request_review', 'pull_request_review_comment']; @@ -30433,7 +30486,7 @@ function isPullRequest() { return prEvents.includes(github_1.context.eventName); } exports.isPullRequest = isPullRequest; -function getSha() { +function getSHA() { let sha = github_1.context.sha; if (isPullRequest()) { const pull = github_1.context.payload.pull_request; @@ -30443,7 +30496,7 @@ function getSha() { } return sha; } -exports.getSha = getSha; +exports.getSHA = getSHA; async function createCommentOnPR(message) { const GITHUB_TOKEN = core.getInput('github-token'); const octokit = (0, github_1.getOctokit)(GITHUB_TOKEN); diff --git a/src/main.ts b/src/main.ts index 10a84a0..dcb25cd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,6 +3,7 @@ import * as exec from '@actions/exec'; import { getLicenses, readResult } from './services/result.service'; import { createCommentOnPR, isPullRequest } from './utils/github.utils'; import { LicensePolicyCheck } from './policies/license-policy-check'; +import { getLicensesReport } from './services/report.service'; /** * The main function for the action. @@ -15,7 +16,7 @@ export async function run(): Promise { // create policies const policies = [new LicensePolicyCheck()]; - policies.forEach(policy => policy.start()); + policies.forEach(async policy => policy.start()); // options to get standar output const options: exec.ExecOptions = {}; @@ -40,14 +41,14 @@ export async function run(): Promise { const scannerResults = await readResult(outputPath); const licenses = getLicenses(scannerResults); - // get reports - const licenseReport = 'Here are the licenses found:'; + // create reports + const licensesReport = getLicensesReport(licenses); // run policies // TODO: define run action for each policy - policies.forEach(async (policy) => await policy.run(licenseReport)); + policies.forEach(async policy => await policy.run(licensesReport)); if (isPullRequest()) { - createCommentOnPR(licenseReport); + createCommentOnPR(licensesReport); } // set outputs for other workflow steps to use diff --git a/src/policies/license-policy-check.ts b/src/policies/license-policy-check.ts index abe18ed..5f35d6c 100644 --- a/src/policies/license-policy-check.ts +++ b/src/policies/license-policy-check.ts @@ -1,10 +1,8 @@ -import { CHECK_NAME } from "src/app.config"; -import { PolicyCheck } from "./policy-check"; +import { CHECK_NAME } from '../app.config'; +import { PolicyCheck } from './policy-check'; export class LicensePolicyCheck extends PolicyCheck { - - constructor() { - super(`${CHECK_NAME}: Licenses Policy`); - } - -} \ No newline at end of file + constructor() { + super(`${CHECK_NAME}: Licenses Policy`); + } +} diff --git a/src/services/report.service.ts b/src/services/report.service.ts new file mode 100644 index 0000000..fc2b6f6 --- /dev/null +++ b/src/services/report.service.ts @@ -0,0 +1,13 @@ +import { Licenses } from './result.service'; + +export function getLicensesReport(licenses: Licenses[]): string { + let markdownTable = '| SPDX ID | Copyleft | URL |\n'; + markdownTable += '| ------- | -------- | --- |\n'; + + licenses.forEach(license => { + const copyleftIcon = license.copyleft ? ':heavy_check_mark:' : ':x:'; + markdownTable += `| ${license.spdxid} | ${copyleftIcon} | ${license.url ? `[Link](${license.url})` : ''} |\n`; + }); + + return markdownTable; +} diff --git a/src/services/result.interfaces.ts b/src/services/result.interfaces.ts index 32aa595..deda551 100644 --- a/src/services/result.interfaces.ts +++ b/src/services/result.interfaces.ts @@ -1,4 +1,4 @@ -export type ScannerResults = Record +export type ScannerResults = Record; export enum ComponentID { NONE = 'none', @@ -8,89 +8,89 @@ export enum ComponentID { } interface CommonComponent { - id: ComponentID - status: string + id: ComponentID; + status: string; } export interface DependencyComponent extends CommonComponent { dependencies: { licenses: { - is_spdx_approved: boolean - name: string - spdx_id: string - }[] - purl: string - url: string - version: string - }[] + is_spdx_approved: boolean; + name: string; + spdx_id: string; + }[]; + purl: string; + url: string; + version: string; + }[]; } export interface ScannerComponent extends CommonComponent { - lines: string - oss_lines: string - matched: string - purl: string[] - vendor: string - component: string - version: string - latest: string - url: string - release_date: string - file: string - url_hash: string - file_hash: string - source_hash: string - file_url: string + lines: string; + oss_lines: string; + matched: string; + purl: string[]; + vendor: string; + component: string; + version: string; + latest: string; + url: string; + release_date: string; + file: string; + url_hash: string; + file_hash: string; + source_hash: string; + file_url: string; licenses: { - name: string - patent_hints: string - copyleft: string - checklist_url: string - osadl_updated: string - source: string - incompatible_with?: string - url: string - }[] + name: string; + patent_hints: string; + copyleft: string; + checklist_url: string; + osadl_updated: string; + source: string; + incompatible_with?: string; + url: string; + }[]; dependencies: { - vendor: string - component: string - version: string - source: string - }[] + vendor: string; + component: string; + version: string; + source: string; + }[]; copyrights: { - name: string - source: string - }[] + name: string; + source: string; + }[]; vulnerabilities: { - ID: string - CVE: string - severity: string - reported: string - introduced: string - patched: string - summary: string - source: string - }[] + ID: string; + CVE: string; + severity: string; + reported: string; + introduced: string; + patched: string; + summary: string; + source: string; + }[]; quality: { - score: string - source: string - }[] - cryptography: any[] + score: string; + source: string; + }[]; + cryptography: any[]; health: { - creation_date: string - issues: number - last_push: string - last_update: string - watchers: number - country: string - stars: number - forks: number - } + creation_date: string; + issues: number; + last_push: string; + last_update: string; + watchers: number; + country: string; + stars: number; + forks: number; + }; server: { - version: string - kb_version: { monthly: string; daily: string } - hostname: string - flags: string - elapsed: string - } + version: string; + kb_version: { monthly: string; daily: string }; + hostname: string; + flags: string; + elapsed: string; + }; } diff --git a/src/services/result.service.ts b/src/services/result.service.ts index 53aed68..2e4f5e6 100644 --- a/src/services/result.service.ts +++ b/src/services/result.service.ts @@ -1,19 +1,19 @@ -import { ComponentID, DependencyComponent, ScannerComponent, ScannerResults } from './result.interfaces' -import * as fs from 'fs' +import { ComponentID, DependencyComponent, ScannerComponent, ScannerResults } from './result.interfaces'; +import * as fs from 'fs'; export async function readResult(filepath: string): Promise { - const content = await fs.promises.readFile(filepath, 'utf-8') - return JSON.parse(content) as ScannerResults + const content = await fs.promises.readFile(filepath, 'utf-8'); + return JSON.parse(content) as ScannerResults; } export interface Licenses { - spdxid: string - copyleft: boolean | null - url: string | null + spdxid: string; + copyleft: boolean | null; + url: string | null; } export function getLicenses(results: ScannerResults): Licenses[] { - const licenses = new Array() + const licenses = new Array(); for (const component of Object.values(results)) { for (const c of component) { @@ -23,30 +23,30 @@ export function getLicenses(results: ScannerResults): Licenses[] { spdxid: l.name, copyleft: !l.copyleft ? null : l.copyleft === 'yes' ? true : false, url: l?.url ? l.url : null - }) + }); } } if (c.id === ComponentID.DEPENDENCY) { - const dependencies = (c as DependencyComponent).dependencies + const dependencies = (c as DependencyComponent).dependencies; for (const d of dependencies) { for (const l of d.licenses) { - if (!l.spdx_id) continue - licenses.push({ spdxid: l.spdx_id, copyleft: null, url: null }) + if (!l.spdx_id) continue; + licenses.push({ spdxid: l.spdx_id, copyleft: null, url: null }); } } } } } - const seenSpdxIds = new Set() + const seenSpdxIds = new Set(); const uniqueLicenses = licenses.filter(license => { if (!seenSpdxIds.has(license.spdxid)) { - seenSpdxIds.add(license.spdxid) - return true + seenSpdxIds.add(license.spdxid); + return true; } - return false - }) + return false; + }); - return uniqueLicenses + return uniqueLicenses; } diff --git a/src/services/result.test.ts b/src/services/result.test.ts index 78ce3a9..40efac8 100644 --- a/src/services/result.test.ts +++ b/src/services/result.test.ts @@ -1,5 +1,5 @@ -import { ScannerResults } from './result.interfaces' -import { getLicenses, Licenses } from './result.service' +import { ScannerResults } from './result.interfaces'; +import { getLicenses, Licenses } from './result.service'; const licenseTableTest: { name: string; description: string; content: string; licenses: Licenses[] }[] = [ { @@ -47,16 +47,16 @@ const licenseTableTest: { name: string; description: string; content: string; li { spdxid: '0BSD', url: null, copyleft: null } ] } -] +]; describe('Test Results service', () => { for (const t of licenseTableTest) { it(`${t.name}`, () => { - const scannerResults = JSON.parse(t.content) as ScannerResults - const licenses = getLicenses(scannerResults) + const scannerResults = JSON.parse(t.content) as ScannerResults; + const licenses = getLicenses(scannerResults); - const sortFn = (a: Licenses, b: Licenses) => a.spdxid.localeCompare(b.spdxid) - expect(licenses.sort(sortFn)).toEqual(t.licenses.sort(sortFn)) - }) + const sortFn = (a: Licenses, b: Licenses): number => a.spdxid.localeCompare(b.spdxid); + expect(licenses.sort(sortFn)).toEqual(t.licenses.sort(sortFn)); + }); } -}) +});