From c5a01461815b313234795ddbe60a9d8fae2c77d4 Mon Sep 17 00:00:00 2001 From: Albert Tregnaghi Date: Fri, 13 Dec 2024 10:49:27 +0100 Subject: [PATCH 1/6] Handle outputs problem #3481 - introduced output helper - storing now outputs directly to file by own helper method instead of using core.setOutput(..) --- github-actions/scan/README.adoc | 17 ++++- .../scan/__test__/output-helper.test.ts | 46 +++++++++++++ .../scan/__test__/post-scan.test.ts | 67 ++++++++++--------- github-actions/scan/dist/index.js | 33 +++++++-- github-actions/scan/src/main.ts | 2 +- github-actions/scan/src/output-helper.ts | 23 +++++++ github-actions/scan/src/post-scan.ts | 7 +- 7 files changed, 152 insertions(+), 43 deletions(-) create mode 100644 github-actions/scan/__test__/output-helper.test.ts create mode 100644 github-actions/scan/src/output-helper.ts diff --git a/github-actions/scan/README.adoc b/github-actions/scan/README.adoc index 28f945113b..e5fd3f57de 100644 --- a/github-actions/scan/README.adoc +++ b/github-actions/scan/README.adoc @@ -142,10 +142,19 @@ npm run test ==== Integration-Test As a precondition to run the integration tests locally you have to +execute `01-start.sh $secHubServerVersion $sechubServerPortNr $pdsVersion $pdsPortN` +inside the integration test folder. -- execute `__test__/01-start.sh $secHubServerVersion $sechubServerPortNr $pdsVersion $pdsPortNr` +An example: -TIP: You can also start a SecHub server and a PDS (both in integration test mode) instead of using the `01-start` script. +[source,bash] +---- +# Next lines will start a SecHub server of version 2.4.0 and a PDS with version 2.1.0 +cd ./github-actions/scan/__test__/integrationtest +./01-start.sh 2.4.0 8443 2.1.0 8444 +---- + +TIP: You can also start a SecHub server and a PDS from IDE (both in integration test mode) instead of using the `01-start` script. After the script has been executed, you can execute integration tests multiple times via following command: @@ -176,7 +185,9 @@ In this setup the tests can be executed from sidebar and from links created insi [TIP] ==== -Unfortunately, the Jest UI integration works only for npm script "test". But to handle integration tests different (the tests shall only be executed when all is build and servers are started) they are not executed by "test" script. +Unfortunately, the Jest UI integration works only for npm script "test". +But to handle integration tests different (the tests shall only be executed +when all is build and servers are started) they are not executed by "test" script. If you want to **debug an integration test**, there is a temporary workaround necessary while you debug the test: diff --git a/github-actions/scan/__test__/output-helper.test.ts b/github-actions/scan/__test__/output-helper.test.ts new file mode 100644 index 0000000000..0fabd3d1fb --- /dev/null +++ b/github-actions/scan/__test__/output-helper.test.ts @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +import * as outputHelper from '../src/output-helper'; + +import * as fs from 'fs'; +import * as path from 'path'; + +describe('storeOutput', () => { + const outputPath = path.join(__dirname, 'test_output.txt'); + + beforeAll(() => { + process.env.GITHUB_OUTPUT = outputPath; + }); + + afterEach(() => { + if (fs.existsSync(outputPath)) { + fs.unlinkSync(outputPath); + } + }); + + it('should append a line with key=value to the file', () => { + /* execute */ + outputHelper.storeOutput('TEST_KEY', 'TEST_VALUE'); + + /* test */ + const content = fs.readFileSync(outputPath, 'utf8'); + expect(content).toBe('TEST_KEY=TEST_VALUE\n'); + }); + + it('should append multiple lines correctly', () => { + /* execute */ + outputHelper.storeOutput('KEY1', 'VALUE1'); + outputHelper.storeOutput('KEY2', 'VALUE2'); + + /* test */ + const content = fs.readFileSync(outputPath, 'utf8'); + expect(content).toBe('KEY1=VALUE1\nKEY2=VALUE2\n'); + }); + + it('should throw an error if GITHUB_OUTPUT is not set', () => { + /* prepare */ + delete process.env.GITHUB_OUTPUT; + + /* execute + test */ + expect(() => outputHelper.storeOutput('KEY', 'VALUE')).toThrow('GITHUB_OUTPUT environment variable is not set'); + }); +}); \ No newline at end of file diff --git a/github-actions/scan/__test__/post-scan.test.ts b/github-actions/scan/__test__/post-scan.test.ts index 8f1d49ce3e..ada369953b 100644 --- a/github-actions/scan/__test__/post-scan.test.ts +++ b/github-actions/scan/__test__/post-scan.test.ts @@ -1,12 +1,15 @@ // SPDX-License-Identifier: MIT import * as core from '@actions/core'; +import * as outputHelper from '../src/output-helper'; import { collectReportData, reportOutputs } from '../src/post-scan'; import { getReport } from '../src/sechub-cli'; import { LAUNCHER_CONTEXT_DEFAULTS } from '../src/launcher'; jest.mock('@actions/core'); +jest.mock('../src/output-helper'); const mockedCore = core as jest.Mocked; +const mockedOutputHelper = outputHelper as jest.Mocked; jest.mock('../src/sechub-cli'); const mockedGetReport = getReport as jest.MockedFunction; @@ -20,7 +23,7 @@ describe('collectReportData', function () { /* prepare */ const testContext = Object.create(LAUNCHER_CONTEXT_DEFAULTS); - testContext.reportFormats= []; + testContext.reportFormats = []; /* execute */ collectReportData(testContext); @@ -33,7 +36,7 @@ describe('collectReportData', function () { it('format "json" - logs called 1 time , getReport never called', function () { /* prepare */ const testContext = Object.create(LAUNCHER_CONTEXT_DEFAULTS); - testContext.reportFormats= ['json']; + testContext.reportFormats = ['json']; /* execute */ collectReportData(testContext); @@ -47,8 +50,8 @@ describe('collectReportData', function () { /* prepare */ const testContext = Object.create(LAUNCHER_CONTEXT_DEFAULTS); - testContext.reportFormats= ['json','html']; - testContext.jobUUID=1234; // necessary for download + testContext.reportFormats = ['json', 'html']; + testContext.jobUUID = 1234; // necessary for download collectReportData(testContext); @@ -61,8 +64,8 @@ describe('collectReportData', function () { /* prepare */ const testContext = Object.create(LAUNCHER_CONTEXT_DEFAULTS); - testContext.reportFormats= ['json','html']; - testContext.jobUUID=1234; // necessary for download + testContext.reportFormats = ['json', 'html']; + testContext.jobUUID = 1234; // necessary for download /* execute */ collectReportData(testContext); @@ -78,11 +81,11 @@ describe('collectReportData', function () { /* prepare */ const testContext = Object.create(LAUNCHER_CONTEXT_DEFAULTS); - testContext.reportFormats= ['json','html','xyz','bla']; - testContext.jobUUID=1234; // necessary for download - + testContext.reportFormats = ['json', 'html', 'xyz', 'bla']; + testContext.jobUUID = 1234; // necessary for download + fsMock.readFileSync = jest.fn(() => '{"test": "test"}'); // Mock an empty JSON report - const sampleJson = {'test': 'test'}; + const sampleJson = { 'test': 'test' }; /* execute */ collectReportData(testContext); @@ -90,7 +93,7 @@ describe('collectReportData', function () { /* test */ expect(mockedCore.info).toHaveBeenCalledTimes(4); // "json, html, xyz, bla" - 4 times logged (valid format check is not done here) expect(mockedGetReport).toHaveBeenCalledTimes(3); // we fetch not json via getReport again (already done before), so only "html, xyz, bla" used - + expect(testContext.secHubReportJsonObject).toEqual(sampleJson); // json object is available }); @@ -136,13 +139,13 @@ describe('reportOutputs', function () { /* test */ expect(mockedCore.debug).toHaveBeenCalledTimes(6); - expect(mockedCore.setOutput).toHaveBeenCalledTimes(6); - expect(mockedCore.setOutput).toBeCalledWith('scan-trafficlight', 'RED'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-count', '2'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-high', '1'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-medium', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-low', '1'); - expect(mockedCore.setOutput).toBeCalledWith('scan-readable-summary', 'SecHub reported traffic light color RED with 2 findings, categorized as follows: HIGH (1), LOW (1)'); + expect(mockedOutputHelper.storeOutput).toHaveBeenCalledTimes(6); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-trafficlight', 'RED'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-count', '2'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-high', '1'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-medium', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-low', '1'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-readable-summary', 'SecHub reported traffic light color RED with 2 findings, categorized as follows: HIGH (1), LOW (1)'); }); it('calls set github output with correct values when JSON report did not exist', function () { @@ -152,13 +155,13 @@ describe('reportOutputs', function () { /* test */ expect(mockedCore.debug).toHaveBeenCalledTimes(7); expect(mockedCore.debug).toBeCalledWith('No findings reported to be categorized.'); - expect(mockedCore.setOutput).toHaveBeenCalledTimes(6); - expect(mockedCore.setOutput).toBeCalledWith('scan-trafficlight', 'FAILURE'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-count', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-high', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-medium', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-low', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-readable-summary', 'SecHub scan could not be executed.'); + expect(mockedOutputHelper.storeOutput).toHaveBeenCalledTimes(6); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-trafficlight', 'FAILURE'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-count', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-high', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-medium', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-low', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-readable-summary', 'SecHub scan could not be executed.'); }); it('calls set github output with correct values when traffic light is green without findings.', function () { @@ -180,13 +183,13 @@ describe('reportOutputs', function () { /* test */ expect(mockedCore.debug).toHaveBeenCalledTimes(6); - expect(mockedCore.setOutput).toHaveBeenCalledTimes(6); - expect(mockedCore.setOutput).toBeCalledWith('scan-trafficlight', 'GREEN'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-count', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-high', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-medium', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-findings-low', '0'); - expect(mockedCore.setOutput).toBeCalledWith('scan-readable-summary', 'SecHub reported traffic light color GREEN without findings'); + expect(mockedOutputHelper.storeOutput).toHaveBeenCalledTimes(6); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-trafficlight', 'GREEN'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-count', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-high', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-medium', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-findings-low', '0'); + expect(mockedOutputHelper.storeOutput).toBeCalledWith('scan-readable-summary', 'SecHub reported traffic light color GREEN without findings'); }); }); diff --git a/github-actions/scan/dist/index.js b/github-actions/scan/dist/index.js index da1b79d485..aaf960bcaa 100644 --- a/github-actions/scan/dist/index.js +++ b/github-actions/scan/dist/index.js @@ -28403,6 +28403,7 @@ function getReport(jobUUID, reportFormat, context) { } ;// CONCATENATED MODULE: ./src/json-helper.ts +// SPDX-License-Identifier: MIT /** * Reads the given field from JSON. @@ -28428,6 +28429,27 @@ function getFieldFromJson(field, jsonData) { return currentKey; } +;// CONCATENATED MODULE: ./src/github-output.ts +// SPDX-License-Identifier: MIT + +const NEW_LINE_SEPARATOR = '\n'; +/** + * Sets the value of an output variable for the GitHub Action. + * This method is a replacement of usage of core.setOutput(..) method. + * There were problems with core.setOutput(...), see + * - https://github.com/mercedes-benz/sechub/issues/3481#issuecomment-2539015176 and + * - https://github.com/actions/toolkit/issues/1218 + * + */ +function storeOutput(field, value) { + const filePath = process.env['GITHUB_OUTPUT'] || ''; + if (filePath) { + } + else { + core.setOutput(field, value); + } +} + ;// CONCATENATED MODULE: ./src/post-scan.ts // SPDX-License-Identifier: MIT @@ -28439,7 +28461,8 @@ function getFieldFromJson(field, jsonData) { -const NEW_LINE_SEPARATOR = '\n'; + +const post_scan_NEW_LINE_SEPARATOR = '\n'; /** * Collect all necessary report data, downloads additional report formats (e.g. 'html') if necessary */ @@ -28513,7 +28536,7 @@ async function uploadArtifact(context, name, files) { if (core.isDebug()) { const filesInWorkspace = (0,external_child_process_.execFileSync)('ls', [rootDirectory], { encoding: 'utf-8' - }).split(NEW_LINE_SEPARATOR); + }).split(post_scan_NEW_LINE_SEPARATOR); for (const fileName of filesInWorkspace) { core.debug(fileName); } @@ -28536,7 +28559,7 @@ function resolveReportNameForScanJob(context) { const workspaceDir = sanitize(getWorkspaceDir()); const filesInWorkspace = (0,external_child_process_.execFileSync)('ls', [workspaceDir], { encoding: 'utf-8' - }).split(NEW_LINE_SEPARATOR); + }).split(post_scan_NEW_LINE_SEPARATOR); if (!context.jobUUID) { core.error('Illegal state: No job uuid resolved - not allowed at this point'); return ''; @@ -28662,7 +28685,7 @@ function buildSummary(trafficLight, totalFindings, findings) { function setOutput(field, value, dataFormat) { value = value !== null && value !== void 0 ? value : (dataFormat === 'number' ? 0 : 'FAILURE'); core.debug(`Output ${field} set to ${value}`); - core.setOutput(field, value.toString()); // Ensure value is converted to a string as GitHub Actions expects output variables to be strings. + storeOutput(field, value.toString()); // Ensure value is converted to a string as GitHub Actions expects output variables to be strings. } ;// CONCATENATED MODULE: ./src/projectname-resolver.ts @@ -28702,6 +28725,7 @@ function projectname_resolver_asJsonObject(text) { // EXTERNAL MODULE: external "os" var external_os_ = __nccwpck_require__(2037); ;// CONCATENATED MODULE: ./src/platform-helper.ts +// SPDX-License-Identifier: MIT function getPlatform() { return external_os_.platform(); @@ -46017,6 +46041,7 @@ const { parseHTML: esm_parseHTML } = static_namespaceObject; const { root: esm_root } = static_namespaceObject; //# sourceMappingURL=index.js.map ;// CONCATENATED MODULE: ./src/client-version-helper.ts +// SPDX-License-Identifier: MIT diff --git a/github-actions/scan/src/main.ts b/github-actions/scan/src/main.ts index f4cb84aa3f..4cf6dbc799 100644 --- a/github-actions/scan/src/main.ts +++ b/github-actions/scan/src/main.ts @@ -6,7 +6,7 @@ import { handleError } from './action-helper'; main().catch(handleError); async function main(): Promise { - // Seperated launcher and main method. + // Separated launcher and main method. // Reason: launch mechanism would be loaded on imports // before we can handle mocking in integration tests! await launch(); diff --git a/github-actions/scan/src/output-helper.ts b/github-actions/scan/src/output-helper.ts new file mode 100644 index 0000000000..d3ef3dcc72 --- /dev/null +++ b/github-actions/scan/src/output-helper.ts @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT + +import * as fs from 'fs'; + +/** + * Sets the value of an output variable for the GitHub Action. + * This method is a replacement of usage of core.setOutput(..) method. + * There were problems with core.setOutput(...), see + * - https://github.com/mercedes-benz/sechub/issues/3481#issuecomment-2539015176 and + * - https://github.com/actions/toolkit/issues/1218 + * + */ +export function storeOutput(field: string, value: string) { + const outputFilePath = process.env['GITHUB_OUTPUT'] || ''; + + if (!outputFilePath) { + throw new Error('GITHUB_OUTPUT environment variable is not set'); + } + + const outputLine = `${field}=${value}\n`; + + fs.appendFileSync(outputFilePath, outputLine, { encoding: 'utf8' }); +} diff --git a/github-actions/scan/src/post-scan.ts b/github-actions/scan/src/post-scan.ts index e7ffa5fca6..3442bf097c 100644 --- a/github-actions/scan/src/post-scan.ts +++ b/github-actions/scan/src/post-scan.ts @@ -8,8 +8,9 @@ import { LaunchContext } from './launcher'; import { logExitCode } from './exitcode'; import { getReport } from './sechub-cli'; import { getFieldFromJson } from './json-helper'; -import { execFileSync } from "child_process"; -import { sanitize } from "./shell-arg-sanitizer"; +import { execFileSync } from 'child_process'; +import { sanitize } from './shell-arg-sanitizer'; +import { storeOutput } from './output-helper'; const NEW_LINE_SEPARATOR = '\n'; @@ -280,5 +281,5 @@ function setOutput(field: string, value: any, dataFormat: string) { value = value ?? (dataFormat === 'number' ? 0 : 'FAILURE'); core.debug(`Output ${field} set to ${value}`); - core.setOutput(field, value.toString()); // Ensure value is converted to a string as GitHub Actions expects output variables to be strings. + storeOutput(field, value.toString()); // Ensure value is converted to a string as GitHub Actions expects output variables to be strings. } From 1aebe35df4de67739513b1f2bec3e3e2eec481e9 Mon Sep 17 00:00:00 2001 From: Albert Tregnaghi Date: Fri, 13 Dec 2024 12:37:52 +0100 Subject: [PATCH 2/6] Avoid unnecessary output in tests #3735 --- .../__test__/client-version-helper.test.ts | 16 ++++++++++++++ .../__test__/configuration-builder.test.ts | 21 ++++++++++++------- .../scan/__test__/init-scan.test.ts | 16 ++++++++++++++ .../scan/__test__/shell-arg-sanitizer.test.ts | 21 ++++++++++++++++++- 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/github-actions/scan/__test__/client-version-helper.test.ts b/github-actions/scan/__test__/client-version-helper.test.ts index d8227497b2..72f9052e4f 100644 --- a/github-actions/scan/__test__/client-version-helper.test.ts +++ b/github-actions/scan/__test__/client-version-helper.test.ts @@ -3,6 +3,22 @@ import { getClientVersion } from '../src/client-version-helper'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import * as core from '@actions/core'; + +jest.mock('@actions/core'); + +const mockDebug = core.debug as jest.MockedFunction; + +const debugEnabled = false; + +beforeEach(() => { + mockDebug.mockImplementation((message: string | Error) => { + if (debugEnabled) { + console.log(`Debug: ${message}`); + } + }); + mockDebug.mockClear(); +}); describe('getClientVersion', function () { diff --git a/github-actions/scan/__test__/configuration-builder.test.ts b/github-actions/scan/__test__/configuration-builder.test.ts index 2787db9307..1b8e238e5d 100644 --- a/github-actions/scan/__test__/configuration-builder.test.ts +++ b/github-actions/scan/__test__/configuration-builder.test.ts @@ -6,7 +6,12 @@ import { SecHubConfigurationModelBuilderData } from '../src/configuration-builde jest.mock('@actions/core'); -function dumpModel(model: SecHubConfigurationModel){ +const debugEnabled = false; + +function logDebug(model: SecHubConfigurationModel){ + if (! debugEnabled){ + return; + } const json = JSON.stringify(model, null, 2); // pretty printed output console.log('json='+json); @@ -34,7 +39,7 @@ describe('configuration-builder', function() { const model= configBuilder.createSecHubConfigurationModel(builderData); /* test */ - dumpModel(model); + logDebug(model); expect(model.apiVersion).toEqual('1.0'); @@ -66,7 +71,7 @@ describe('configuration-builder', function() { const model= configBuilder.createSecHubConfigurationModel(builderData); /* test */ - dumpModel(model); + logDebug(model); expect(model.apiVersion).toEqual('1.0'); @@ -101,7 +106,7 @@ describe('configuration-builder', function() { const model= configBuilder.createSecHubConfigurationModel(builderData); /* test */ - dumpModel(model); + logDebug(model); expect(model.apiVersion).toEqual('1.0'); @@ -138,7 +143,7 @@ describe('configuration-builder', function() { const model= configBuilder.createSecHubConfigurationModel(builderData); /* test */ - dumpModel(model); + logDebug(model); expect(model.apiVersion).toEqual('1.0'); @@ -172,7 +177,7 @@ describe('configuration-builder', function() { const model= configBuilder.createSecHubConfigurationModel(builderData); /* test */ - dumpModel(model); + logDebug(model); expect(model.apiVersion).toEqual('1.0'); @@ -206,7 +211,7 @@ describe('configuration-builder', function() { const model= configBuilder.createSecHubConfigurationModel(builderData); /* test */ - dumpModel(model); + logDebug(model); expect(model.apiVersion).toEqual('1.0'); @@ -241,7 +246,7 @@ describe('configuration-builder', function() { const model= configBuilder.createSecHubConfigurationModel(builderData); /* test */ - dumpModel(model); + logDebug(model); expect(model.apiVersion).toEqual('1.0'); diff --git a/github-actions/scan/__test__/init-scan.test.ts b/github-actions/scan/__test__/init-scan.test.ts index 94c3fa99f9..16737206ea 100644 --- a/github-actions/scan/__test__/init-scan.test.ts +++ b/github-actions/scan/__test__/init-scan.test.ts @@ -5,6 +5,22 @@ import {initReportFormats, initSecHubJson} from '../src/init-scan'; jest.mock('./../src/configuration-builder'); import {SecHubConfigurationModelBuilderData, createSecHubConfigJsonFile} from '../src/configuration-builder'; +import * as core from '@actions/core'; + +jest.mock('@actions/core'); + +const mockInfo = core.info as jest.MockedFunction; + +const debugEnabled = false; + +beforeEach(() => { + mockInfo.mockImplementation((message: string | Error) => { + if (debugEnabled) { + console.log(`Info: ${message}`); + } + }); + mockInfo.mockClear(); +}); describe('initSecHubJson', function () { it('throws error if configPath is set, but file does not exist', function () { diff --git a/github-actions/scan/__test__/shell-arg-sanitizer.test.ts b/github-actions/scan/__test__/shell-arg-sanitizer.test.ts index f03ed1a19d..bd4c5021f8 100644 --- a/github-actions/scan/__test__/shell-arg-sanitizer.test.ts +++ b/github-actions/scan/__test__/shell-arg-sanitizer.test.ts @@ -1,8 +1,27 @@ +/* eslint-disable indent */ // SPDX-License-Identifier: MIT import * as shellArgSanitizer from '../src/shell-arg-sanitizer'; +import * as core from '@actions/core'; + +jest.mock('@actions/core'); + +const mockError = core.error as jest.MockedFunction; + +const debugEnabled = false; + +beforeEach(() => { + mockError.mockImplementation((message: string | Error) => { + if (debugEnabled) { + console.log(`Error: ${message}`); + } + }); + mockError.mockClear(); +}); + describe('sanitize', () => { + test.each([ ['rm -rf /; echo hacked'], // Command chaining ['echo $(whoami)'], // Command substitution @@ -69,7 +88,7 @@ describe('sanitize', () => { (arg) => { /* test */ expect(() => shellArgSanitizer.sanitize(arg)).not.toThrow(); - }); + }); it('removes whitespaces', function () { /* prepare */ From 81b026c237c90be78538949e41243a44d3299d9e Mon Sep 17 00:00:00 2001 From: Albert Tregnaghi Date: Fri, 13 Dec 2024 12:50:40 +0100 Subject: [PATCH 3/6] Integrate nodes.js module cache + use npm install - had problems with axios mock adapter dependency at CI/CD build so changed to npm install - to provide a fast build the nodes.js modules are now cached. --- .github/workflows/github-action-scan.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github-action-scan.yml b/.github/workflows/github-action-scan.yml index 5824a3323b..b9e8f62b1f 100644 --- a/.github/workflows/github-action-scan.yml +++ b/.github/workflows/github-action-scan.yml @@ -29,14 +29,22 @@ jobs: steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - + + - name: Cache Node.js modules + uses: actions/cache@v2 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - name: Use Node.js uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af with: node-version: 22 - - name: Clean install - run: npm ci + - name: Install + run: npm install - name: Build run: npm run build From 4ac750f45b5cdaa1e6da33c5e5b8810e55b2820e Mon Sep 17 00:00:00 2001 From: Albert Tregnaghi Date: Fri, 13 Dec 2024 14:46:43 +0100 Subject: [PATCH 4/6] Provide deploy.sh script for deployments #3741 --- github-actions/scan/README.adoc | 21 +++++++++++++++ github-actions/scan/deploy.sh | 45 ++++++++++++++++++++++++++++++++ github-actions/scan/package.json | 1 + 3 files changed, 67 insertions(+) create mode 100755 github-actions/scan/deploy.sh diff --git a/github-actions/scan/README.adoc b/github-actions/scan/README.adoc index e5fd3f57de..635439bdae 100644 --- a/github-actions/scan/README.adoc +++ b/github-actions/scan/README.adoc @@ -129,6 +129,27 @@ npm run build This runs the ncc compiler and transpiles the files from the src folder into the `dist/` folder. +=== Deployment +A GitHub action needs a transpiled `index.js` to be used as an action from workflows. + +As long as we do not provide a new index.js the old action is still in usage, even when the source code has +changed. If we do not build the file and commit and push it to git repository, the action will not +be available! + +Of course the steps can be done manual, but for convenience a script is avialable: + +[source,bash] +---- +./deploy.sh +---- + +This will +- setup (npm install) +- build (npm run build) +- test (npm run test) +- verify (only index.js is changed now) +- deploy - when no error happened - deploy the new `index.js` file by doing a commit and a push to the remote repository + === Test ==== Unit tests diff --git a/github-actions/scan/deploy.sh b/github-actions/scan/deploy.sh new file mode 100755 index 0000000000..45010c11a0 --- /dev/null +++ b/github-actions/scan/deploy.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +set -e + +echo "--------------------------------" +echo "Deployment of SecHub scan action" +echo "--------------------------------" +echo "[SETUP]" +npm install + +echo "[BUILD]" +npm run build + +echo "[TEST]" +npm run test + +# Check for any changes in the repository +changed_files=$(git diff --name-only HEAD) + +# Check if there are no changes +if [ -z "$changed_files" ]; then + echo "No changes detected in the repository." + echo "[CANCELED]" + + exit 5 +fi + +# Check if the only changed file is index.js +if [ "$changed_files" != "github-actions/scan/dist/index.js" ]; then + echo "Changes detected in files other than index.js (only):" + echo "$changed_files" + echo "" + echo "This may not happen on a deployment! Check the other changes" + echo "[FAILED]" + exit 1 +fi +echo "Only index.js has changes, deployment is possible." +echo "[DEPLOY]" + +git add --all +git commit -m "GitHub action (scan) deployment" +git push + + + diff --git a/github-actions/scan/package.json b/github-actions/scan/package.json index a2b1b52947..37db237428 100644 --- a/github-actions/scan/package.json +++ b/github-actions/scan/package.json @@ -5,6 +5,7 @@ "main": "dist/main.js", "scripts": { "build": "ncc build src/main.ts", + "deploy" : "./deploy.sh", "cleanBuild": "ncc cache clean;ncc build src/main.ts", "lint": "eslint src", "prettier": "npx prettier --write src", From 40acb9c91bd189bff6f692ef6089633f58f256f3 Mon Sep 17 00:00:00 2001 From: Albert Tregnaghi Date: Fri, 13 Dec 2024 14:49:47 +0100 Subject: [PATCH 5/6] Switched from outputs to environment output #3481 - directly writing to the file did not work - directly using set-output command did not work - using SECHUB_OUTPUT* environment values as alternative - changed documentation - wrote tests --- github-actions/scan/README.adoc | 22 ++++---- .../scan/__test__/output-helper.test.ts | 40 +++----------- github-actions/scan/dist/index.js | 53 +++++++++++++------ github-actions/scan/src/output-helper.ts | 48 +++++++++++++---- 4 files changed, 95 insertions(+), 68 deletions(-) diff --git a/github-actions/scan/README.adoc b/github-actions/scan/README.adoc index 635439bdae..7dda30e0f3 100644 --- a/github-actions/scan/README.adoc +++ b/github-actions/scan/README.adoc @@ -96,18 +96,22 @@ If no custom `sechub.json` is provided, it will be generated from the remaining === Outputs -The following table lists the output variables available after this SecHub GitHub Action has completed: +==== Output +Because of problems with GitHub action and outputs (see https://github.com/mercedes-benz/sechub/issues/3481 ) SecHub no longer supports outputs but provides environment variables instead. + +==== Environment variables for output +The following table lists the environment variables available after this SecHub GitHub Action has completed: [cols="20%,40%,40%"] |=== -| Output Name | Description | Expected Values - -| scan-trafficlight | The color of the traffic light reported by SecHub if the scan ran successfully, otherwise `FAILURE`. | One of `GREEN`, `YELLOW`, `RED`, or `FAILURE`. -| scan-findings-count | The total number of findings reported by SecHub. Returns 0 if the scan didn't complete. | 0 -| scan-findings-high | The number of high-level findings reported by SecHub. | 0 -| scan-findings-medium | The number of medium-level findings reported by SecHub. | 0 -| scan-findings-low | The number of low-level findings reported by SecHub. | 0 -| scan-readable-summary| A human-readable summary of the scan outcome, including the traffic light color, findings count, and their distribution. | For example, `SecHub scan could not be executed` if an error occurred. Otherwise, i.e. `SecHub reported traffic light color YELLOW with 15 findings, categorized as follows: MEDIUM (8), LOW (7)` +| Environment variable | Description | Expected Values + +| SECHUB_OUTPUT_SCAN_TRAFFICLIGHT | The color of the traffic light reported by SecHub if the scan ran successfully, otherwise `FAILURE`. | One of `GREEN`, `YELLOW`, `RED`, or `FAILURE`. +| SECHUB_OUTPUT_SCAN_FINDINGS_COUNT | The total number of findings reported by SecHub. Returns 0 if the scan didn't complete. | 0 +| SECHUB_OUTPUT_SCAN_FINDINGS_HIGH | The number of high-level findings reported by SecHub. | 0 +| SECHUB_OUTPUT_SCAN_FINDINGS_MEDIUM | The number of medium-level findings reported by SecHub. | 0 +| SECHUB_OUTPUT_SCAN_FINDINGS_LOW | The number of low-level findings reported by SecHub. | 0 +| SECHUB_OUTPUT_SCAN_READABLE_SUMMARY| A human-readable summary of the scan outcome, including the traffic light color, findings count, and their distribution. | For example, `SecHub scan could not be executed` if an error occurred. Otherwise, i.e. `SecHub reported traffic light color YELLOW with 15 findings, categorized as follows: MEDIUM (8), LOW (7)` |=== diff --git a/github-actions/scan/__test__/output-helper.test.ts b/github-actions/scan/__test__/output-helper.test.ts index 0fabd3d1fb..22a84716a0 100644 --- a/github-actions/scan/__test__/output-helper.test.ts +++ b/github-actions/scan/__test__/output-helper.test.ts @@ -1,46 +1,18 @@ // SPDX-License-Identifier: MIT import * as outputHelper from '../src/output-helper'; +import * as core from '@actions/core'; -import * as fs from 'fs'; -import * as path from 'path'; +jest.mock('@actions/core'); describe('storeOutput', () => { - const outputPath = path.join(__dirname, 'test_output.txt'); + const mockedCore = core as jest.Mocked; - beforeAll(() => { - process.env.GITHUB_OUTPUT = outputPath; - }); - - afterEach(() => { - if (fs.existsSync(outputPath)) { - fs.unlinkSync(outputPath); - } - }); - - it('should append a line with key=value to the file', () => { + it('test-key shall set SECHUB_OUTPUT_TEST_KEY', () => { /* execute */ - outputHelper.storeOutput('TEST_KEY', 'TEST_VALUE'); + outputHelper.storeOutput('test-key', 'test value1'); /* test */ - const content = fs.readFileSync(outputPath, 'utf8'); - expect(content).toBe('TEST_KEY=TEST_VALUE\n'); + expect(mockedCore.exportVariable).toBeCalledWith('SECHUB_OUTPUT_TEST_KEY', 'test value1'); }); - it('should append multiple lines correctly', () => { - /* execute */ - outputHelper.storeOutput('KEY1', 'VALUE1'); - outputHelper.storeOutput('KEY2', 'VALUE2'); - - /* test */ - const content = fs.readFileSync(outputPath, 'utf8'); - expect(content).toBe('KEY1=VALUE1\nKEY2=VALUE2\n'); - }); - - it('should throw an error if GITHUB_OUTPUT is not set', () => { - /* prepare */ - delete process.env.GITHUB_OUTPUT; - - /* execute + test */ - expect(() => outputHelper.storeOutput('KEY', 'VALUE')).toThrow('GITHUB_OUTPUT environment variable is not set'); - }); }); \ No newline at end of file diff --git a/github-actions/scan/dist/index.js b/github-actions/scan/dist/index.js index aaf960bcaa..89e5803b74 100644 --- a/github-actions/scan/dist/index.js +++ b/github-actions/scan/dist/index.js @@ -28429,25 +28429,48 @@ function getFieldFromJson(field, jsonData) { return currentKey; } -;// CONCATENATED MODULE: ./src/github-output.ts -// SPDX-License-Identifier: MIT +;// CONCATENATED MODULE: ./src/output-helper.ts -const NEW_LINE_SEPARATOR = '\n'; /** - * Sets the value of an output variable for the GitHub Action. - * This method is a replacement of usage of core.setOutput(..) method. + * Sets the value of an output (environment ) variable for the GitHub Action. + * This method is a workaround because of problems with of core.setOutput(..) method. * There were problems with core.setOutput(...), see * - https://github.com/mercedes-benz/sechub/issues/3481#issuecomment-2539015176 and * - https://github.com/actions/toolkit/issues/1218 + * - https://github.com/actions/toolkit/issues/1906 + * + * As a workaround we provide instead of output + * special SecHub ouput environment variables with naming convention "SECHUB_OUTPUT_${fieldAdopted}" + * + * `fieldAdopted` is same as `field`, but uppercased and `-` will be replaced by `_` + * + * For example: `scan-readable-summary` will become `SECHUB_OUTPUT_SCAN_READABLE_SUMMARY` * + * If debugging is enabled in action the setting will be logged. */ function storeOutput(field, value) { - const filePath = process.env['GITHUB_OUTPUT'] || ''; - if (filePath) { - } - else { - core.setOutput(field, value); - } + // export the output to an "output" variable (this works) + const envVarName = `SECHUB_OUTPUT_${field.toUpperCase().replace(/-/g, '_')}`; + (0,core.exportVariable)(envVarName, value); + if (process.env.ACTIONS_RUNNER_DEBUG === 'true') { + // Print the environment variable for debugging + console.log(`Exported environment variable ${envVarName} with value: ${value}`); + } + // 1. This following out commented code was thought as a workaround + // for https://github.com/actions/toolkit/issues/1218 + // Because the GITHUB_OUTPUT file from a worfklow step (which worked) did not contain + // crypto.randomUUID() parts we tried to write the key/value file "normally" without + // the crypto parts, but It did not appear inside context output, means it didn't work + // (even when it the exact file structure as done by an echo ?!?!) + // But we keep it here for documentation: + // const outputFilePath = process.env['GITHUB_OUTPUT'] || ''; + // if (!outputFilePath) { + // throw new Error('GITHUB_OUTPUT environment variable is not set'); + // } + // const outputLine = `${field}=${value}\n`; + // fs.appendFileSync(outputFilePath, outputLine, { encoding: 'utf8' }); + // 2. Offical way by core API (does not work) + // setOutput(field,value); } ;// CONCATENATED MODULE: ./src/post-scan.ts @@ -28462,7 +28485,7 @@ function storeOutput(field, value) { -const post_scan_NEW_LINE_SEPARATOR = '\n'; +const NEW_LINE_SEPARATOR = '\n'; /** * Collect all necessary report data, downloads additional report formats (e.g. 'html') if necessary */ @@ -28536,7 +28559,7 @@ async function uploadArtifact(context, name, files) { if (core.isDebug()) { const filesInWorkspace = (0,external_child_process_.execFileSync)('ls', [rootDirectory], { encoding: 'utf-8' - }).split(post_scan_NEW_LINE_SEPARATOR); + }).split(NEW_LINE_SEPARATOR); for (const fileName of filesInWorkspace) { core.debug(fileName); } @@ -28559,7 +28582,7 @@ function resolveReportNameForScanJob(context) { const workspaceDir = sanitize(getWorkspaceDir()); const filesInWorkspace = (0,external_child_process_.execFileSync)('ls', [workspaceDir], { encoding: 'utf-8' - }).split(post_scan_NEW_LINE_SEPARATOR); + }).split(NEW_LINE_SEPARATOR); if (!context.jobUUID) { core.error('Illegal state: No job uuid resolved - not allowed at this point'); return ''; @@ -46215,7 +46238,7 @@ async function postScan(context) { main().catch(handleError); async function main() { - // Seperated launcher and main method. + // Separated launcher and main method. // Reason: launch mechanism would be loaded on imports // before we can handle mocking in integration tests! await launch(); diff --git a/github-actions/scan/src/output-helper.ts b/github-actions/scan/src/output-helper.ts index d3ef3dcc72..c26a1002a8 100644 --- a/github-actions/scan/src/output-helper.ts +++ b/github-actions/scan/src/output-helper.ts @@ -1,23 +1,51 @@ // SPDX-License-Identifier: MIT - -import * as fs from 'fs'; +import { setOutput } from '@actions/core/lib/core'; +import { exportVariable } from '@actions/core/lib/core'; /** - * Sets the value of an output variable for the GitHub Action. - * This method is a replacement of usage of core.setOutput(..) method. + * Sets the value of an output (environment ) variable for the GitHub Action. + * This method is a workaround because of problems with of core.setOutput(..) method. * There were problems with core.setOutput(...), see * - https://github.com/mercedes-benz/sechub/issues/3481#issuecomment-2539015176 and * - https://github.com/actions/toolkit/issues/1218 + * - https://github.com/actions/toolkit/issues/1906 + * + * As a workaround we provide instead of output + * special SecHub ouput environment variables with naming convention "SECHUB_OUTPUT_${fieldAdopted}" + * + * `fieldAdopted` is same as `field`, but uppercased and `-` will be replaced by `_` + * + * For example: `scan-readable-summary` will become `SECHUB_OUTPUT_SCAN_READABLE_SUMMARY` * + * If debugging is enabled in action the setting will be logged. */ export function storeOutput(field: string, value: string) { - const outputFilePath = process.env['GITHUB_OUTPUT'] || ''; - - if (!outputFilePath) { - throw new Error('GITHUB_OUTPUT environment variable is not set'); + // export the output to an "output" variable (this works) + const envVarName = `SECHUB_OUTPUT_${field.toUpperCase().replace(/-/g, '_')}`; + exportVariable(envVarName, value); + if (process.env.ACTIONS_RUNNER_DEBUG === 'true') { + // Print the environment variable for debugging + console.log(`Exported environment variable ${envVarName} with value: ${value}`); } - const outputLine = `${field}=${value}\n`; + // 1. This following out commented code was thought as a workaround + // for https://github.com/actions/toolkit/issues/1218 + // Because the GITHUB_OUTPUT file from a worfklow step (which worked) did not contain + // crypto.randomUUID() parts we tried to write the key/value file "normally" without + // the crypto parts, but It did not appear inside context output, means it didn't work + // (even when it the exact file structure as done by an echo ?!?!) + // But we keep it here for documentation: + + // const outputFilePath = process.env['GITHUB_OUTPUT'] || ''; + // if (!outputFilePath) { + // throw new Error('GITHUB_OUTPUT environment variable is not set'); + // } + + // const outputLine = `${field}=${value}\n`; + // fs.appendFileSync(outputFilePath, outputLine, { encoding: 'utf8' }); + + + // 2. Offical way by core API (does not work) + // setOutput(field,value); - fs.appendFileSync(outputFilePath, outputLine, { encoding: 'utf8' }); } From 1291fb3998ec8c478b66bae17bd902c1e462ed8b Mon Sep 17 00:00:00 2001 From: Albert Tregnaghi Date: Tue, 17 Dec 2024 10:33:54 +0100 Subject: [PATCH 6/6] Removed deploy.sh script and updated documentation #3741 - we have already the github action workflow `.github/workflows/release-github-action.yml` which automates the deployment process so removed deploy.sh - added information about deployment workflow inside README.adoc - updated some documentation part and fixed typo in README.adoc --- github-actions/scan/README.adoc | 29 +++++++-------------- github-actions/scan/deploy.sh | 45 --------------------------------- 2 files changed, 9 insertions(+), 65 deletions(-) delete mode 100755 github-actions/scan/deploy.sh diff --git a/github-actions/scan/README.adoc b/github-actions/scan/README.adoc index 7dda30e0f3..8d6c3e5de3 100644 --- a/github-actions/scan/README.adoc +++ b/github-actions/scan/README.adoc @@ -94,13 +94,13 @@ The following variables take priority over the configuration file: If no custom `sechub.json` is provided, it will be generated from the remaining specified variables and used. However, if a custom `sechub.json` is provided, no separate configuration will be created, meaning the remaining set variables will essentially be ignored. ==== -=== Outputs +=== Use SecHub results in GitHub workflows -==== Output -Because of problems with GitHub action and outputs (see https://github.com/mercedes-benz/sechub/issues/3481 ) SecHub no longer supports outputs but provides environment variables instead. +==== GitHub Output +Because of problems with GitHub outputs (see https://github.com/mercedes-benz/sechub/issues/3481 ) SecHub no longer supports outputs but provides environment variables instead. -==== Environment variables for output -The following table lists the environment variables available after this SecHub GitHub Action has completed: +==== Environment variables +The following table lists the environment variables containing result data after this SecHub GitHub Action has completed: [cols="20%,40%,40%"] |=== @@ -116,7 +116,7 @@ The following table lists the environment variables available after this SecHub |=== -You can access them after the action has run with `${{ steps..outputs. }}` +You can access them after the action has run with `${{ env. }}` === Build @@ -140,19 +140,8 @@ As long as we do not provide a new index.js the old action is still in usage, ev changed. If we do not build the file and commit and push it to git repository, the action will not be available! -Of course the steps can be done manual, but for convenience a script is avialable: - -[source,bash] ----- -./deploy.sh ----- - -This will -- setup (npm install) -- build (npm run build) -- test (npm run test) -- verify (only index.js is changed now) -- deploy - when no error happened - deploy the new `index.js` file by doing a commit and a push to the remote repository +The complete deployment process is automated by `.github/workflows/release-github-action.yml` which will create a +PR which will do all necessary steps. === Test @@ -212,7 +201,7 @@ In this setup the tests can be executed from sidebar and from links created insi ==== Unfortunately, the Jest UI integration works only for npm script "test". But to handle integration tests different (the tests shall only be executed -when all is build and servers are started) they are not executed by "test" script. +when all is built and servers are started) they are not executed by "test" script. If you want to **debug an integration test**, there is a temporary workaround necessary while you debug the test: diff --git a/github-actions/scan/deploy.sh b/github-actions/scan/deploy.sh deleted file mode 100755 index 45010c11a0..0000000000 --- a/github-actions/scan/deploy.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - -set -e - -echo "--------------------------------" -echo "Deployment of SecHub scan action" -echo "--------------------------------" -echo "[SETUP]" -npm install - -echo "[BUILD]" -npm run build - -echo "[TEST]" -npm run test - -# Check for any changes in the repository -changed_files=$(git diff --name-only HEAD) - -# Check if there are no changes -if [ -z "$changed_files" ]; then - echo "No changes detected in the repository." - echo "[CANCELED]" - - exit 5 -fi - -# Check if the only changed file is index.js -if [ "$changed_files" != "github-actions/scan/dist/index.js" ]; then - echo "Changes detected in files other than index.js (only):" - echo "$changed_files" - echo "" - echo "This may not happen on a deployment! Check the other changes" - echo "[FAILED]" - exit 1 -fi -echo "Only index.js has changes, deployment is possible." -echo "[DEPLOY]" - -git add --all -git commit -m "GitHub action (scan) deployment" -git push - - -