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 diff --git a/github-actions/scan/README.adoc b/github-actions/scan/README.adoc index 28f945113b..8d6c3e5de3 100644 --- a/github-actions/scan/README.adoc +++ b/github-actions/scan/README.adoc @@ -94,25 +94,29 @@ 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 -The following table lists the output variables available after this SecHub GitHub Action has completed: +==== 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 +The following table lists the environment variables containing result data after this SecHub GitHub Action has completed: [cols="20%,40%,40%"] |=== -| Output Name | Description | Expected Values +| Environment variable | 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)` +| 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)` |=== -You can access them after the action has run with `${{ steps..outputs. }}` +You can access them after the action has run with `${{ env. }}` === Build @@ -129,6 +133,16 @@ 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! + +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 ==== Unit tests @@ -142,10 +156,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: + +[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 (both in integration test mode) instead of using the `01-start` script. +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 +199,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 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/__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__/output-helper.test.ts b/github-actions/scan/__test__/output-helper.test.ts new file mode 100644 index 0000000000..22a84716a0 --- /dev/null +++ b/github-actions/scan/__test__/output-helper.test.ts @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +import * as outputHelper from '../src/output-helper'; +import * as core from '@actions/core'; + +jest.mock('@actions/core'); + +describe('storeOutput', () => { + const mockedCore = core as jest.Mocked; + + it('test-key shall set SECHUB_OUTPUT_TEST_KEY', () => { + /* execute */ + outputHelper.storeOutput('test-key', 'test value1'); + + /* test */ + expect(mockedCore.exportVariable).toBeCalledWith('SECHUB_OUTPUT_TEST_KEY', 'test value1'); + }); + +}); \ 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/__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 */ diff --git a/github-actions/scan/dist/index.js b/github-actions/scan/dist/index.js index da1b79d485..89e5803b74 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,50 @@ function getFieldFromJson(field, jsonData) { return currentKey; } +;// CONCATENATED MODULE: ./src/output-helper.ts + +/** + * 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) { + // 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 // SPDX-License-Identifier: MIT @@ -28439,6 +28484,7 @@ function getFieldFromJson(field, jsonData) { + const NEW_LINE_SEPARATOR = '\n'; /** * Collect all necessary report data, downloads additional report formats (e.g. 'html') if necessary @@ -28662,7 +28708,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 +28748,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 +46064,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 @@ -46190,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/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", 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..c26a1002a8 --- /dev/null +++ b/github-actions/scan/src/output-helper.ts @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +import { setOutput } from '@actions/core/lib/core'; +import { exportVariable } from '@actions/core/lib/core'; + +/** + * 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) { + // 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}`); + } + + // 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); + +} 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. }