From aec7ea5763de95d2a2765b65d6974e4077eb5758 Mon Sep 17 00:00:00 2001 From: Agustin Isasmendi Date: Mon, 26 Feb 2024 15:44:48 +0100 Subject: [PATCH] SCP-135 Includes policies on summary report --- action.yml | 1 + dist/index.js | 113 ++++++++++++++++++++++----------- src/main.ts | 14 ++-- src/policies/policy-check.ts | 51 ++++++++++++--- src/services/report.service.ts | 47 ++++++++------ src/utils/markdown.utils.ts | 14 ++-- 6 files changed, 163 insertions(+), 77 deletions(-) diff --git a/action.yml b/action.yml index c338553..8041779 100644 --- a/action.yml +++ b/action.yml @@ -46,6 +46,7 @@ inputs: required: false default: ${{ github.token }} + # Define your outputs here. outputs: result-filepath: diff --git a/dist/index.js b/dist/index.js index 5739dbc..886732b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -125812,19 +125812,24 @@ async function run() { core.debug(`SCANOSS Scan Action started...`); // create policies core.debug(`Creating policies`); + //Read declared policies on input parameter 'policies' and create an instance for each one. const policies = policy_manager_1.policyManager.getPolicies(); - policies.forEach(async (policy) => policy.start()); + for (const policy of policies) { + await policy.start(); + } // run scan const { scan, stdout } = await scan_service_1.scanService.scan(); await (0, scan_service_1.uploadResults)(); // run policies - policies.forEach(async (policy) => await policy.run(scan)); + for (const policy of policies) { + await policy.run(scan); + } if ((0, github_utils_1.isPullRequest)()) { // create reports const report = (0, report_service_1.generateSummary)(scan); - (0, github_utils_1.createCommentOnPR)(report); + await (0, github_utils_1.createCommentOnPR)(report); } - await (0, report_service_1.generateJobSummary)(scan); + await (0, report_service_1.generateJobSummary)(scan, policies); // set outputs for other workflow steps to use core.setOutput(outputs.RESULT_FILEPATH, inputs.OUTPUT_FILEPATH); core.setOutput(outputs.STDOUT_SCAN_COMMAND, stdout); @@ -125922,12 +125927,11 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.PolicyCheck = exports.CONCLUSION = void 0; +exports.PolicyCheck = exports.STATUS = exports.CONCLUSION = void 0; const github_1 = __nccwpck_require__(95438); const core = __importStar(__nccwpck_require__(42186)); const github_utils_1 = __nccwpck_require__(17889); const inputs = __importStar(__nccwpck_require__(483)); -const UNINITIALIZED = -1; var CONCLUSION; (function (CONCLUSION) { CONCLUSION["ActionRequired"] = "action_required"; @@ -125939,14 +125943,26 @@ var CONCLUSION; CONCLUSION["Stale"] = "stale"; CONCLUSION["TimedOut"] = "timed_out"; })(CONCLUSION || (exports.CONCLUSION = CONCLUSION = {})); +var STATUS; +(function (STATUS) { + STATUS["UNINITIALIZED"] = "UNINITIALIZED"; + STATUS["INITIALIZED"] = "INITIALIZED"; + STATUS["RUNNING"] = "RUNNING"; + STATUS["FINISHED"] = "FINISHED"; +})(STATUS || (exports.STATUS = STATUS = {})); class PolicyCheck { octokit; checkName; checkRunId; + _raw; + _status; + _conclusion; constructor(checkName) { this.octokit = (0, github_1.getOctokit)(inputs.GITHUB_TOKEN); this.checkName = checkName; - this.checkRunId = UNINITIALIZED; + this._status = STATUS.UNINITIALIZED; + this._conclusion = CONCLUSION.Neutral; + this.checkRunId = -1; } async start() { const result = await this.octokit.rest.checks.create({ @@ -125956,30 +125972,48 @@ class PolicyCheck { head_sha: (0, github_utils_1.getSHA)() }); this.checkRunId = result.data.id; + this._raw = result.data; + this._status = STATUS.INITIALIZED; return result.data; } get name() { return this.checkName; } + get conclusion() { + return this._conclusion; + } + get raw() { + return this._raw; + } + get url() { + return `${github_1.context.serverUrl}/${github_1.context.repo.owner}/${github_1.context.repo.repo}/actions/runs/${github_1.context.runId}/job/${this.raw.id}`; + } async run(scannerResults) { - if (this.checkRunId === UNINITIALIZED) + if (this._status === STATUS.UNINITIALIZED) throw new Error(`Error on finish. Policy "${this.checkName}" is not created.`); core.debug(`Running policy check: ${this.checkName}`); + this._status = STATUS.RUNNING; } async success(summary, text) { - return await this.finish(CONCLUSION.Success, summary, text); + this._conclusion = CONCLUSION.Success; + return await this.finish(summary, text); } async reject(summary, text) { - return await this.finish(inputs.POLICIES_HALT_ON_FAILURE ? CONCLUSION.Failure : CONCLUSION.Neutral, summary, text); + if (inputs.POLICIES_HALT_ON_FAILURE) + this._conclusion = CONCLUSION.Failure; + else + this._conclusion = CONCLUSION.Neutral; + return await this.finish(summary, text); } - async finish(conclusion, summary, text) { - core.debug(`Finish policy check: ${this.checkName}. (conclusion=${conclusion})`); + async finish(summary, text) { + core.debug(`Finish policy check: ${this.checkName}. (conclusion=${this._conclusion})`); + this._status = STATUS.FINISHED; const result = await this.octokit.rest.checks.update({ owner: github_1.context.repo.owner, repo: github_1.context.repo.repo, check_run_id: this.checkRunId, status: 'completed', - conclusion, + conclusion: this._conclusion, output: { title: this.checkName, summary, @@ -126144,6 +126178,8 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.generateJobSummary = exports.generateSummary = exports.getLicensesTable = void 0; const result_service_1 = __nccwpck_require__(32414); const core = __importStar(__nccwpck_require__(42186)); +const policy_check_1 = __nccwpck_require__(63702); +const markdown_utils_1 = __nccwpck_require__(96011); function getLicensesTable(licenses) { let markdownTable = '| License | Copyleft | URL |\n'; markdownTable += '| ------- | -------- | --- |\n'; @@ -126166,10 +126202,10 @@ function generateSummary(scannerResults) { return content; } exports.generateSummary = generateSummary; -async function generateJobSummary(scannerResults) { +async function generateJobSummary(scannerResults, policies) { const licenses = (0, result_service_1.getLicenses)(scannerResults); licenses.sort((l1, l2) => l2.count - l1.count); - const genPie = (licenses) => { + const LicensesPie = (licenses) => { let pie = ` %%{init: { "pie" : {"textPosition": "0.75"} ,"themeVariables": {"pieSectionTextSize": "0px", "pie1": "#E8B34B", "pie1":"#E8B34B","pie2":"#E22C2C","pie3":"#5754D0", @@ -126183,25 +126219,32 @@ async function generateJobSummary(scannerResults) { }); return pie; }; - const genTable = (licenses) => { - const rows = []; - rows.push([ - { data: 'License', header: true }, - { data: 'Copyleft', header: true }, - { data: 'URL', header: true } - ]); + const LicensesTable = (licenses) => { + const HEADERS = ['License', 'Copyleft', 'URL']; + const ROWS = []; licenses.forEach(l => { const copyleftIcon = l.copyleft ? ':x:' : ' '; - rows.push([l.spdxid, copyleftIcon, `${l.url || ''}`]); + ROWS.push([l.spdxid, copyleftIcon, `${l.url || ''}`]); }); - return rows; + return (0, markdown_utils_1.generateTable)(HEADERS, ROWS); + }; + const PoliciesTable = (policies) => { + const HEADERS = ['Policy', 'Status', 'Details']; + const ROWS = []; + policies.forEach(p => { + const statusIcon = p.conclusion === policy_check_1.CONCLUSION.Success ? ':white_check_mark:' : ':x:'; + ROWS.push([p.name, statusIcon, `[More Details](${p.url})`]); + }); + return (0, markdown_utils_1.generateTable)(HEADERS, ROWS); }; await core.summary - .addHeading('Scan Report Section') - .addCodeBlock(genPie(licenses), 'mermaid') - .addRaw('
') - .addTable(genTable(licenses)) - .addRaw('
') + .addHeading('Scan Report Section', 2) + .addHeading('Licenses', 3) + .addCodeBlock(LicensesPie(licenses), 'mermaid') + .addRaw(LicensesTable(licenses)) + .addSeparator() + .addHeading('Policies', 3) + .addRaw(PoliciesTable(policies)) .write(); } exports.generateJobSummary = generateJobSummary; @@ -126486,13 +126529,11 @@ 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; + return ` + ${COL_SEP} ${headers.join(COL_SEP)} ${COL_SEP} + ${COL_SEP + new Array(headers.length).fill('-').join(COL_SEP) + COL_SEP} + ${rows.map(row => COL_SEP + row.join(COL_SEP) + COL_SEP).join('\n')} + `; }; exports.generateTable = generateTable; diff --git a/src/main.ts b/src/main.ts index b0cacc7..c51d311 100644 --- a/src/main.ts +++ b/src/main.ts @@ -17,23 +17,29 @@ export async function run(): Promise { // create policies core.debug(`Creating policies`); + + //Read declared policies on input parameter 'policies' and create an instance for each one. const policies = policyManager.getPolicies(); - policies.forEach(async policy => policy.start()); + for (const policy of policies) { + await policy.start(); + } // run scan const { scan, stdout } = await scanService.scan(); await uploadResults(); // run policies - policies.forEach(async policy => await policy.run(scan)); + for (const policy of policies) { + await policy.run(scan); + } if (isPullRequest()) { // create reports const report = generateSummary(scan); - createCommentOnPR(report); + await createCommentOnPR(report); } - await generateJobSummary(scan); + await generateJobSummary(scan, policies); // set outputs for other workflow steps to use core.setOutput(outputs.RESULT_FILEPATH, inputs.OUTPUT_FILEPATH); core.setOutput(outputs.STDOUT_SCAN_COMMAND, stdout); diff --git a/src/policies/policy-check.ts b/src/policies/policy-check.ts index 3861e47..2933d5c 100644 --- a/src/policies/policy-check.ts +++ b/src/policies/policy-check.ts @@ -5,8 +5,6 @@ import { ScannerResults } from '../services/result.interfaces'; import { GitHub } from '@actions/github/lib/utils'; import * as inputs from '../app.input'; -const UNINITIALIZED = -1; - export enum CONCLUSION { ActionRequired = 'action_required', Cancelled = 'cancelled', @@ -18,6 +16,13 @@ export enum CONCLUSION { TimedOut = 'timed_out' } +export enum STATUS { + UNINITIALIZED = 'UNINITIALIZED', + INITIALIZED = 'INITIALIZED', + RUNNING = 'RUNNING', + FINISHED = 'FINISHED' +} + export abstract class PolicyCheck { private octokit: InstanceType; @@ -25,10 +30,18 @@ export abstract class PolicyCheck { private checkRunId: number; + private _raw: any; + + private _status: STATUS; + + private _conclusion: CONCLUSION; + constructor(checkName: string) { this.octokit = getOctokit(inputs.GITHUB_TOKEN); this.checkName = checkName; - this.checkRunId = UNINITIALIZED; + this._status = STATUS.UNINITIALIZED; + this._conclusion = CONCLUSION.Neutral; + this.checkRunId = -1; } async start(): Promise { @@ -40,37 +53,57 @@ export abstract class PolicyCheck { }); this.checkRunId = result.data.id; + this._raw = result.data; + this._status = STATUS.INITIALIZED; return result.data; } get name(): string { return this.checkName; } + + get conclusion(): CONCLUSION { + return this._conclusion; + } + + get raw(): any { + return this._raw; + } + + get url(): string { + return `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/job/${this.raw.id}`; + } + async run(scannerResults: ScannerResults): Promise { - if (this.checkRunId === UNINITIALIZED) + if (this._status === STATUS.UNINITIALIZED) throw new Error(`Error on finish. Policy "${this.checkName}" is not created.`); core.debug(`Running policy check: ${this.checkName}`); + this._status = STATUS.RUNNING; } protected async success(summary: string, text: string): Promise { - return await this.finish(CONCLUSION.Success, summary, text); + this._conclusion = CONCLUSION.Success; + return await this.finish(summary, text); } protected async reject(summary: string, text: string): Promise { - return await this.finish(inputs.POLICIES_HALT_ON_FAILURE ? CONCLUSION.Failure : CONCLUSION.Neutral, summary, text); + if (inputs.POLICIES_HALT_ON_FAILURE) this._conclusion = CONCLUSION.Failure; + else this._conclusion = CONCLUSION.Neutral; + return await this.finish(summary, text); } - protected async finish(conclusion: CONCLUSION | undefined, summary: string, text: string): Promise { - core.debug(`Finish policy check: ${this.checkName}. (conclusion=${conclusion})`); + protected async finish(summary: string, text: string): Promise { + core.debug(`Finish policy check: ${this.checkName}. (conclusion=${this._conclusion})`); + this._status = STATUS.FINISHED; const result = await this.octokit.rest.checks.update({ owner: context.repo.owner, repo: context.repo.repo, check_run_id: this.checkRunId, status: 'completed', - conclusion, + conclusion: this._conclusion, output: { title: this.checkName, summary, diff --git a/src/services/report.service.ts b/src/services/report.service.ts index 98d846a..67d1208 100644 --- a/src/services/report.service.ts +++ b/src/services/report.service.ts @@ -1,8 +1,8 @@ import { ScannerResults } from './result.interfaces'; import { License, getLicenses } from './result.service'; import * as core from '@actions/core'; -import { SummaryTableRow } from '@actions/core/lib/summary'; - +import { CONCLUSION, PolicyCheck } from '../policies/policy-check'; +import { generateTable } from '../utils/markdown.utils'; export function getLicensesTable(licenses: License[]): string { let markdownTable = '| License | Copyleft | URL |\n'; markdownTable += '| ------- | -------- | --- |\n'; @@ -29,11 +29,11 @@ export function generateSummary(scannerResults: ScannerResults): string { return content; } -export async function generateJobSummary(scannerResults: ScannerResults): Promise { +export async function generateJobSummary(scannerResults: ScannerResults, policies: PolicyCheck[]): Promise { const licenses = getLicenses(scannerResults); licenses.sort((l1, l2) => l2.count - l1.count); - const genPie = (licenses: License[]): string => { + const LicensesPie = (licenses: License[]): string => { let pie = ` %%{init: { "pie" : {"textPosition": "0.75"} ,"themeVariables": {"pieSectionTextSize": "0px", "pie1": "#E8B34B", "pie1":"#E8B34B","pie2":"#E22C2C","pie3":"#5754D0", @@ -49,27 +49,36 @@ export async function generateJobSummary(scannerResults: ScannerResults): Promis return pie; }; - const genTable = (licenses: License[]): SummaryTableRow[] => { - const rows: SummaryTableRow[] = []; - - rows.push([ - { data: 'License', header: true }, - { data: 'Copyleft', header: true }, - { data: 'URL', header: true } - ]); + const LicensesTable = (licenses: License[]): string => { + const HEADERS: string[] = ['License', 'Copyleft', 'URL']; + const ROWS: string[][] = []; licenses.forEach(l => { const copyleftIcon = l.copyleft ? ':x:' : ' '; - rows.push([l.spdxid, copyleftIcon, `${l.url || ''}`]); + ROWS.push([l.spdxid, copyleftIcon, `${l.url || ''}`]); }); - return rows; + return generateTable(HEADERS, ROWS); + }; + + const PoliciesTable = (policies: PolicyCheck[]): string => { + const HEADERS = ['Policy', 'Status', 'Details']; + const ROWS: string[][] = []; + + policies.forEach(p => { + const statusIcon = p.conclusion === CONCLUSION.Success ? ':white_check_mark:' : ':x:'; + ROWS.push([p.name, statusIcon, `[More Details](${p.url})`]); + }); + + return generateTable(HEADERS, ROWS); }; await core.summary - .addHeading('Scan Report Section') - .addCodeBlock(genPie(licenses), 'mermaid') - .addRaw('
') - .addTable(genTable(licenses)) - .addRaw('
') + .addHeading('Scan Report Section', 2) + .addHeading('Licenses', 3) + .addCodeBlock(LicensesPie(licenses), 'mermaid') + .addRaw(LicensesTable(licenses)) + .addSeparator() + .addHeading('Policies', 3) + .addRaw(PoliciesTable(policies)) .write(); } diff --git a/src/utils/markdown.utils.ts b/src/utils/markdown.utils.ts index f5eb93f..2d1303a 100644 --- a/src/utils/markdown.utils.ts +++ b/src/utils/markdown.utils.ts @@ -1,13 +1,9 @@ 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; + return ` + ${COL_SEP} ${headers.join(COL_SEP)} ${COL_SEP} + ${COL_SEP + new Array(headers.length).fill('-').join(COL_SEP) + COL_SEP} + ${rows.map(row => COL_SEP + row.join(COL_SEP) + COL_SEP).join('\n')} + `; };