diff --git a/sechub-pds-solutions/checkmarx/env b/sechub-pds-solutions/checkmarx/env index 967a1ccb8..253d8fbaf 100644 --- a/sechub-pds-solutions/checkmarx/env +++ b/sechub-pds-solutions/checkmarx/env @@ -12,3 +12,5 @@ BUILD_TYPE=download # The Checkmarx Wrapper version to use # See: https://github.com/mercedes-benz/sechub/releases CHECKMARX_WRAPPER_VERSION="1.3.0" +PDS_ENCRYPTION_ALGORITHM="NONE" +PDS_ENCRYPTION_SECRET_KEY="" \ No newline at end of file diff --git a/sechub-pds-solutions/findsecuritybugs/env b/sechub-pds-solutions/findsecuritybugs/env index cb9dc5e97..e37fa266f 100644 --- a/sechub-pds-solutions/findsecuritybugs/env +++ b/sechub-pds-solutions/findsecuritybugs/env @@ -2,3 +2,5 @@ FINDSECURITYBUGS_VERSION="1.13.0" # The Spotbugs version to use. See https://github.com/spotbugs/spotbugs/releases SPOTBUGS_VERSION="4.8.3" +PDS_ENCRYPTION_ALGORITHM="NONE" +PDS_ENCRYPTION_SECRET_KEY="" \ No newline at end of file diff --git a/sechub-pds-solutions/gitleaks/env b/sechub-pds-solutions/gitleaks/env index 7645b8551..b0387fe34 100644 --- a/sechub-pds-solutions/gitleaks/env +++ b/sechub-pds-solutions/gitleaks/env @@ -16,3 +16,5 @@ BUILD_TYPE=download # The Secret-Validation Wrapper version to use # See: https://github.com/mercedes-benz/sechub/releases SECRETVALIDATION_WRAPPER_VERSION="1.1.0" +PDS_ENCRYPTION_ALGORITHM="NONE" +PDS_ENCRYPTION_SECRET_KEY="" \ No newline at end of file diff --git a/sechub-pds-solutions/gosec/env b/sechub-pds-solutions/gosec/env index 12ba91a52..5d736aaa6 100644 --- a/sechub-pds-solutions/gosec/env +++ b/sechub-pds-solutions/gosec/env @@ -1,2 +1,4 @@ # See: https://github.com/securego/gosec/releases GOSEC_VERSION="2.16.0" +PDS_ENCRYPTION_ALGORITHM="NONE" +PDS_ENCRYPTION_SECRET_KEY="" \ No newline at end of file diff --git a/sechub-pds-solutions/iac/env b/sechub-pds-solutions/iac/env index b4c1cacc0..4e1d017a2 100644 --- a/sechub-pds-solutions/iac/env +++ b/sechub-pds-solutions/iac/env @@ -2,3 +2,5 @@ # uncomment to use local image # BASE_IMAGE="pds-base_pds" KICS_VERSION="2.1.3" +PDS_ENCRYPTION_ALGORITHM="NONE" +PDS_ENCRYPTION_SECRET_KEY="" \ No newline at end of file diff --git a/sechub-pds-solutions/owaspzap/env b/sechub-pds-solutions/owaspzap/env index a569e932d..e1965fe02 100644 --- a/sechub-pds-solutions/owaspzap/env +++ b/sechub-pds-solutions/owaspzap/env @@ -22,3 +22,5 @@ PDS_CONFIG_EXECUTE_WORKER_THREAD_COUNT=1 #ZAP_PROXY_HOST=127.0.0.1 #ZAP_PROXY_PORT=9999 ZAP_PROXY_FOR_PDS_TARGET_TYPE=INTERNET +PDS_ENCRYPTION_ALGORITHM="NONE" +PDS_ENCRYPTION_SECRET_KEY="" \ No newline at end of file diff --git a/sechub-pds-solutions/prepare/env b/sechub-pds-solutions/prepare/env index 19f0cd716..bb3f657e2 100644 --- a/sechub-pds-solutions/prepare/env +++ b/sechub-pds-solutions/prepare/env @@ -37,4 +37,6 @@ PDS_PREPARE_MODULE_GIT_REMOVE_GIT_FILES_BEFORE_UPLOAD="true" PDS_PREPARE_MODULE_GIT_CLONE_WITHOUT_GIT_HISTORY="true" # SKOPEO # Enable/ Disable skopeo module -PDS_PREPARE_MODULE_SKOPEO_ENABLED="true" \ No newline at end of file +PDS_PREPARE_MODULE_SKOPEO_ENABLED="true" +PDS_ENCRYPTION_ALGORITHM="NONE" +PDS_ENCRYPTION_SECRET_KEY="" \ No newline at end of file diff --git a/sechub-pds-solutions/scancode/env b/sechub-pds-solutions/scancode/env index fd3739528..b0f52bdc5 100644 --- a/sechub-pds-solutions/scancode/env +++ b/sechub-pds-solutions/scancode/env @@ -5,3 +5,5 @@ SCANCODE_VERSION="32.0.4" SPDX_TOOL_VERSION="1.1.7" PDS_MAX_FILE_UPLOAD_BYTES=26214400000 +PDS_ENCRYPTION_ALGORITHM="NONE" +PDS_ENCRYPTION_SECRET_KEY="" \ No newline at end of file diff --git a/sechub-pds-solutions/tern/env b/sechub-pds-solutions/tern/env index 0b3aa6304..0c39a3c59 100644 --- a/sechub-pds-solutions/tern/env +++ b/sechub-pds-solutions/tern/env @@ -5,3 +5,5 @@ TERN_VERSION="2.12.1" SCANCODE_VERSION="32.0.4" PDS_MAX_FILE_UPLOAD_BYTES=26214400000 +PDS_ENCRYPTION_ALGORITHM="NONE" +PDS_ENCRYPTION_SECRET_KEY="" \ No newline at end of file diff --git a/sechub-pds-solutions/xray/env b/sechub-pds-solutions/xray/env index ec7da32d2..3fd4adb3b 100644 --- a/sechub-pds-solutions/xray/env +++ b/sechub-pds-solutions/xray/env @@ -15,3 +15,5 @@ BUILD_TYPE=download # The Xray Wrapper version to use # See: https://github.com/mercedes-benz/sechub/releases XRAY_WRAPPER_VERSION="1.0.0" +PDS_ENCRYPTION_ALGORITHM="NONE" +PDS_ENCRYPTION_SECRET_KEY="" \ No newline at end of file diff --git a/sechub-solution/docker/SecHub-Debian.dockerfile b/sechub-solution/docker/SecHub-Debian.dockerfile index 0de68d2d6..3d73e2bb2 100644 --- a/sechub-solution/docker/SecHub-Debian.dockerfile +++ b/sechub-solution/docker/SecHub-Debian.dockerfile @@ -13,7 +13,7 @@ ARG BASE_IMAGE ARG TARGETARCH # Build args -ARG BUILD_TYPE="download" +ARG BUILD_TYPE ARG SECHUB_VERSION ARG TAG="" diff --git a/sechub-web-ui/.env b/sechub-web-ui/.env index 6ca560f87..016b70e58 100644 --- a/sechub-web-ui/.env +++ b/sechub-web-ui/.env @@ -5,6 +5,7 @@ VITE_API_HOST=http://localhost:3000 # Optional variables needed for testing with SecHub Server Basic Auth +# Note: if your password includes special characters e.g. $ you must write /$ VITE_API_LOCAL_DEV=false VITE_API_USER='example-test-user' VITE_API_PASSWORD='example-api-token' \ No newline at end of file diff --git a/sechub-web-ui/README.md b/sechub-web-ui/README.md index d5ee0f159..84d9f7c04 100644 --- a/sechub-web-ui/README.md +++ b/sechub-web-ui/README.md @@ -40,22 +40,44 @@ To start the development server with hot-reload, run the following command. The npm run dev ``` -> If you receive an empty page, do a reload (sometimes it needs a little bit of time until everything is setup correctly) +> If you receive an empty page or buttons do not work, do a reload (sometimes it needs a little bit of time until everything is setup correctly) > Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script. -#### Running in Development mode with sechub for testing +#### Running in Development mode with SecHub Integrationtest Server for testing -1. Start SecHub Integration Test Server (or Docker Server) +1. Start SecHub Integration Test Server (or Docker Server) +(for the correct run configuration follow the [developer guide](https://mercedes-benz.github.io/sechub/latest/sechub-developer-quickstart-guide.html#run-integration-tests-from-ide)) 2. Configure your `.env.local` file by copying `.env` to `.env.local` and adjusting the variables as needed. Set `VITE_API_LOCAL_DEV=true` Set `VITE_API_USER` to your SecHub user Set `VITE_API_PASSWORD` to your SecHub Api Token 3. Set `VITE_API_HOST` to the URL of your application http://localhost:3000 - this is because of the proxy defined in the Vite dev server to avoid CORS Issues 4. Start the SPA in Development mode (npm run dev) +5. (Optional) See the /test-setups/setup-integration-test-server.sh script for setups (please note: the executor is only needed when PDS is used) Happy Testing! +#### Running in Development mode with SecHub Integrationtest Server and PDS Integrationtest Server (using Mocked scan products) +> Only useful If you want to get mocked scan results +1. Follow the steps above +2. Start the integration test PDS +(for the correct run configuration follow the [developer guide](https://mercedes-benz.github.io/sechub/latest/sechub-developer-quickstart-guide.html#run-integration-tests-from-ide)) +3. (Optional) Initial setup: execute /test-setups/setup-integration-test-server.sh + +#### Running in Development mode with SecHub Server and PDS as Docker Container +> Only useful If you want to get real scan results +1. Start the SecHub Server as Docker Container (see sechub-solution/01-...) +2. Start the required PDS as Docker (e.g. sechub-pds-solutions/gosec/05-...) +3. Set up PDS in sechub-solution/setups/ e.g. setup-gosec.sh +4. Configure your `.env.local` file by copying `.env` to `.env.local` and adjusting the variables as needed. +Set `VITE_API_LOCAL_DEV=true` +Set `VITE_API_USER` to your SecHub user +Set `VITE_API_PASSWORD` to your SecHub Api Token +5. Make sure your user is assigned to the project you want to scan + +Now you can test your web-ui with sechub and real scans! + ### Building for Production Set Environment Variables: diff --git a/sechub-web-ui/package-lock.json b/sechub-web-ui/package-lock.json index 5f7e2c0eb..6a52b299a 100644 --- a/sechub-web-ui/package-lock.json +++ b/sechub-web-ui/package-lock.json @@ -11,8 +11,10 @@ "@mdi/font": "7.4.47", "@openapitools/openapi-generator-cli": "^2.15.3", "core-js": "^3.37.1", + "crypto-js": "^4.2.0", "pinia": "^2.3.0", "roboto-fontface": "*", + "uuid": "^11.0.3", "vue": "^3.4.31", "vue-i18n": "^10.0.5", "vuetify": "^3.6.11" @@ -20,6 +22,7 @@ "devDependencies": { "@babel/types": "^7.24.7", "@intlify/unplugin-vue-i18n": "^6.0.0", + "@types/crypto-js": "^4.2.2", "@types/node": "^20.14.10", "@vitejs/plugin-vue": "^5.0.5", "@vue/eslint-config-typescript": "^13.0.0", @@ -1523,6 +1526,13 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, + "node_modules/@types/crypto-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.2.2.tgz", + "integrity": "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -2816,6 +2826,12 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", + "license": "MIT" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -7171,6 +7187,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vite": { "version": "5.4.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", diff --git a/sechub-web-ui/package.json b/sechub-web-ui/package.json index ffe48f1ef..b54c86290 100644 --- a/sechub-web-ui/package.json +++ b/sechub-web-ui/package.json @@ -12,8 +12,10 @@ "@mdi/font": "7.4.47", "@openapitools/openapi-generator-cli": "^2.15.3", "core-js": "^3.37.1", + "crypto-js": "^4.2.0", "pinia": "^2.3.0", "roboto-fontface": "*", + "uuid": "^11.0.3", "vue": "^3.4.31", "vue-i18n": "^10.0.5", "vuetify": "^3.6.11" @@ -21,6 +23,7 @@ "devDependencies": { "@babel/types": "^7.24.7", "@intlify/unplugin-vue-i18n": "^6.0.0", + "@types/crypto-js": "^4.2.2", "@types/node": "^20.14.10", "@vitejs/plugin-vue": "^5.0.5", "@vue/eslint-config-typescript": "^13.0.0", diff --git a/sechub-web-ui/src/components.d.ts b/sechub-web-ui/src/components.d.ts index 005e46954..bce1077da 100644 --- a/sechub-web-ui/src/components.d.ts +++ b/sechub-web-ui/src/components.d.ts @@ -13,8 +13,13 @@ declare module 'vue' { Project: typeof import('./components/Project.vue')['default'] ProjectDetails: typeof import('./components/ProjectDetails.vue')['default'] ProjectDetailsFab: typeof import('./components/ProjectDetailsFab.vue')['default'] + ProjectJobList: typeof import('./components/ProjectJobList.vue')['default'] ProjectsList: typeof import('./components/ProjectsList.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + ScanCreate: typeof import('./components/ScanCreate.vue')['default'] + ScanFileUpload: typeof import('./components/ScanFileUpload.vue')['default'] + ScanFileUploadTest: typeof import('./components/ScanFileUploadTest.vue')['default'] + ScanTypeSelect: typeof import('./components/ScanTypeSelect.vue')['default'] } } diff --git a/sechub-web-ui/src/components/AppHeader.vue b/sechub-web-ui/src/components/AppHeader.vue index 541fd47e7..ab9798f51 100644 --- a/sechub-web-ui/src/components/AppHeader.vue +++ b/sechub-web-ui/src/components/AppHeader.vue @@ -54,7 +54,7 @@ diff --git a/sechub-web-ui/src/components/ProjectsList.vue b/sechub-web-ui/src/components/ProjectsList.vue index c5d569d93..136e8e9c0 100644 --- a/sechub-web-ui/src/components/ProjectsList.vue +++ b/sechub-web-ui/src/components/ProjectsList.vue @@ -48,26 +48,27 @@ - + diff --git a/sechub-web-ui/src/components/ScanFileUpload.vue b/sechub-web-ui/src/components/ScanFileUpload.vue new file mode 100644 index 000000000..2b61f7d49 --- /dev/null +++ b/sechub-web-ui/src/components/ScanFileUpload.vue @@ -0,0 +1,135 @@ + + + + diff --git a/sechub-web-ui/src/components/ScanTypeSelect.vue b/sechub-web-ui/src/components/ScanTypeSelect.vue new file mode 100644 index 000000000..08492db59 --- /dev/null +++ b/sechub-web-ui/src/components/ScanTypeSelect.vue @@ -0,0 +1,49 @@ + + + diff --git a/sechub-web-ui/src/i18n/locales/en.json b/sechub-web-ui/src/i18n/locales/en.json index ae7995eda..8e066dda5 100644 --- a/sechub-web-ui/src/i18n/locales/en.json +++ b/sechub-web-ui/src/i18n/locales/en.json @@ -4,6 +4,7 @@ "OWNED": "owned", "MEMBER": "member", "NO_PROJECTS_ASSIGNED": "You are not assigned to any projects.", + "ERROR_MESSAGE_FETCHING_PROJECTS": "ProjectAPI error fetching assigned projects.", "NO_JOBS_RUNNED": "No jobs were started for this project.", "ERROR_FETCHING_DATA": "Could not load data from Server.", "HEADER_JOB_TABLE_STATUS": "State", @@ -18,5 +19,30 @@ "PROJECT_DETAILS_MEMBERS": "Members", "PROJECT_DETAILS_NOT_OWNED": "You are not the owner or an admin and not allowed to view project members.", "PROJECT_DETAILS_TOOLTIP_OPEN": "Open project details", - "PROJECT_DETAILS_TOOLTIP_CLOSE": "Close project details" + "PROJECT_DETAILS_TOOLTIP_CLOSE": "Close project details", + "SCAN_CREATE_TITLE": "Create new Scan", + "SCAN_CREATE_SELECT_SCAN_TYPE": "Select Scan Type", + "SCAN_CREATE_FILE_UPLOAD": "File Upload", + "SCAN_CREATE_FILE_UPLOAD_NOTE_TITLE": "Important for secretScan", + "SCAN_CREATE_FILE_UPLOAD_NOTE_ONE": "1. Put your code in the structure __data__/web-ui-upload/your-code", + "SCAN_CREATE_FILE_UPLOAD_NOTE_TWO": "2. Create __data__.zip", + "SCAN_CREATE_FILE_UPLOAD_NOTE_THREE": "3. Upload __data__.zip", + "SCAN_CREATE_FILE_UPLOAD_INPUT": "Upload File", + "SCAN_CREATE_FILE_UPLOAD_PROGRESS": "Uploading your data...", + "SCAN_CREATE_FILE_UPLOAD_INPUT_ERROR_TITLE": "Upload Error. Please select a valid file", + "SCAN_CREATE_FILE_UPLOAD_INPUT_ERROR_ZIP": "File must be a valid .zip containing the code you wish to scan.", + "SCAN_CREATE_FILE_UPLOAD_INPUT_ERROR_TAR": "File must be a valid .tar containing the binary you wish to scan.", + "SCAN_CREATE_BINARIES": "Binaires", + "SCAN_CREATE_SOURCE_CODE": "Source Code", + "SCAN_CREATE_CODE_SCAN": "codeScan", + "SCAN_CREATE_SECRET_SCAN": "secretScan", + "SCAN_CREATE_SCAN_START": "Scan", + "SCAN_CREATE_SCAN_CONFIGURATION": "Scan Configuration", + "SCAN_ERROR_ALERT_TITLE": "Ooops, your scan failed", + "SCAN_ERROR_ALERT_JOB_NOT_CREATED": "Could not create a new Job.", + "SCAN_ERROR_ALERT_SOURCE_UPLOAD_FAILED": "Could not upload sources. Your Zip file might be too big or is not correctly packed. We recommend using the cli client for large files.", + "SCAN_ERROR_ALERT_BINARY_UPLOAD_FAILED": "Could not upload binaries. Your Tar file might be too big or is not correctly packed. We recommend using the cli client for large files.", + "SCAN_ERROR_ALERT_JOB_NOT_APPROVED": "Could not approve Job.", + "SCAN_ERROR_ALERT_NO_DATA_SECTION": "Could not get data section from configuration. Internal Error.", + "SCAN_ERROR_ALERT_GENERIC": "An internal error ocurred." } \ No newline at end of file diff --git a/sechub-web-ui/src/pages/[id]/index.vue b/sechub-web-ui/src/pages/[id]/index.vue new file mode 100644 index 000000000..5b27b5e80 --- /dev/null +++ b/sechub-web-ui/src/pages/[id]/index.vue @@ -0,0 +1,4 @@ + + diff --git a/sechub-web-ui/src/pages/[id].vue b/sechub-web-ui/src/pages/[id]/scan.vue similarity index 78% rename from sechub-web-ui/src/pages/[id].vue rename to sechub-web-ui/src/pages/[id]/scan.vue index 9dfada12c..72b66b445 100644 --- a/sechub-web-ui/src/pages/[id].vue +++ b/sechub-web-ui/src/pages/[id]/scan.vue @@ -1,4 +1,4 @@ diff --git a/sechub-web-ui/src/services/defaultClient.ts b/sechub-web-ui/src/services/defaultClient.ts index 822b52a6e..ca5bb7b18 100644 --- a/sechub-web-ui/src/services/defaultClient.ts +++ b/sechub-web-ui/src/services/defaultClient.ts @@ -3,12 +3,16 @@ import configurationApi from './configurationService' import projectApi from './productAdministrationService' import systemApi from './systemApiService' import otherApi from './otherService' +import executionApi from './executionService/executionService' +import scanService from './executionService/ScanService' const defaultClient = { withProjectApi: projectApi, withSystemApi: systemApi, withConfigurationApi: configurationApi, withOtherApi: otherApi, + withExecutionApi: executionApi, + withScanService: scanService, } export default defaultClient diff --git a/sechub-web-ui/src/services/executionService/FormDataBodyBuilder.ts b/sechub-web-ui/src/services/executionService/FormDataBodyBuilder.ts new file mode 100644 index 000000000..21554be49 --- /dev/null +++ b/sechub-web-ui/src/services/executionService/FormDataBodyBuilder.ts @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +// This is implemented according to the sechub default client in Java +// Instead of Multipart (Java) we use FromData (TypeScript) +type FormDataContentType = 'STRING' | 'FILE' | 'STREAM' | 'BOUNDARY'; + +interface FormDataContent { + name: string; + type: FormDataContentType; + value?: string; + file?: File; + stream?: () => Promise; + filename?: string; + contentType?: string; +} + +export class FormDataBodyBuilder { + private formDataContent: FormDataContent[] = [] + private boundary: string + + constructor (boundary: string) { + this.boundary = boundary + } + + public async build (): Promise { + if (this.formDataContent.length === 0) { + throw new Error('Must have at least one part to build a formData message.') + } + + this.formDataContent.push(this.boundaryPart()) + + const parts: (string | Uint8Array)[] = [] + + for (const part of this.formDataContent) { + parts.push(this.constructPartHeader(part)) + + if (part.type === 'FILE' && part.file) { + await this.appendFileInChunks(part.file, parts) + } else if (part.type === 'STREAM' && part.stream) { + parts.push(new Uint8Array(await part.stream())) + } + + if (part.type !== 'BOUNDARY') { + parts.push(new TextEncoder().encode('\r\n')) + } + } + return new Blob(parts) + } + + public addString (name: string, value: string): this { + this.formDataContent.push({ name, type: 'STRING', value }) + return this + } + + public addFile (name: string, file: File): this { + this.formDataContent.push({ name, type: 'FILE', file, contentType: file.type }) + return this + } + + public addStream (name: string, stream: () => Promise, filename: string, contentType: string): this { + this.formDataContent.push({ name, type: 'STREAM', stream, filename, contentType }) + return this + } + + private boundaryPart (): FormDataContent { + return { name: '', type: 'BOUNDARY', value: `--${this.boundary}--` } + } + + private constructPartHeader (part: FormDataContent): Uint8Array { + if (part.type === 'STRING') { + return new TextEncoder().encode( + `--${this.boundary}\r\nContent-Disposition: form-data; name="${part.name}"\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n${part.value}` + ) + } else if (part.type === 'BOUNDARY') { + return new TextEncoder().encode(`--${this.boundary}--\r\n`) + } else { + const filename = part.file ? part.file.name : part.filename + const contentType = part.contentType || 'application/octet-stream' + return new TextEncoder().encode( + `--${this.boundary}\r\nContent-Disposition: form-data; name="${part.name}"; filename="${filename}"\r\nContent-Type: ${contentType}\r\n\r\n` + ) + } + } + + private async appendFileInChunks (file: File, parts: (string | Uint8Array)[]): Promise { + // const chunkSize = 64 * 1024 * 1024 // 64MB chunks + const chunkSize = 8192 // 8KB chunks + for (let offset = 0; offset < file.size; offset += chunkSize) { + const chunk = file.slice(offset, offset + chunkSize) + const arrayBuffer = await chunk.arrayBuffer() + parts.push(new Uint8Array(arrayBuffer)) + } + } +} diff --git a/sechub-web-ui/src/services/executionService/ScanService.ts b/sechub-web-ui/src/services/executionService/ScanService.ts new file mode 100644 index 000000000..874a6d129 --- /dev/null +++ b/sechub-web-ui/src/services/executionService/ScanService.ts @@ -0,0 +1,100 @@ +// SPDX-License-Identifier: MIT +import { + SchedulerResult, + SecHubConfiguration, + UserApproveJobRequest, + UserCreateNewJobRequest, +} from '@/generated-sources/openapi' +import executionApi from './executionService' +import { createSha256Checksum } from '../../utils/cryptoUtils' +import { UserUploadsBinariesWorkaroundRequest, UserUploadSourceCodeWorkaroundRequest } from '@/services/executionService/executionService' +import i18n from '@/i18n' + +// Implements the scan of a file in three steps: creating a Job, uploading the data and approve the job +class ScanService { + async scan (configuration: SecHubConfiguration, projectId: string, file: File): Promise { + const errorMessages: string[] = [] + try { + const jobId = await this.createJob(configuration) + if (jobId) { + await this.uploadData(configuration, jobId, file, errorMessages) + await this.approveJob(projectId, jobId, errorMessages) + } else { + errorMessages.push(i18n.global.t('SCAN_ERROR_ALERT_JOB_NOT_CREATED')) + } + } catch (error) { + console.error('Scan failed:', error) + errorMessages.push(i18n.global.t('SCAN_ERROR_ALERT_GENERIC')) + } + return errorMessages + } + + private async createJob (configuration: SecHubConfiguration): Promise { + const requestParameters: UserCreateNewJobRequest = { + projectId: configuration.projectId, + secHubConfiguration: configuration, + } + + try { + const result: SchedulerResult = await executionApi.userCreateNewJob(requestParameters) + return result.jobId + } catch (error) { + console.error('Job creation failed:', error) + return undefined + } + } + + private async uploadData (configuration: SecHubConfiguration, jobId: string, file: File, errorMessages: string[]) { + const checksum: string = await createSha256Checksum(file) + + if (configuration.data?.sources) { + const requestParameters: UserUploadSourceCodeWorkaroundRequest = { + projectId: configuration.projectId, + jobUUID: jobId, + checkSum: checksum, + file, + } + + try { + await executionApi.userUploadSourceCode(requestParameters) + } catch (error) { + console.error('Source code upload failed:', error) + errorMessages.push(i18n.global.t('SCAN_ERROR_ALERT_SOURCE_UPLOAD_FAILED')) + } + } else if (configuration.data?.binaries) { + const size: string = file.size.toString() + const requestParameters: UserUploadsBinariesWorkaroundRequest = { + projectId: configuration.projectId, + jobUUID: jobId, + checkSum: checksum, + xFileSize: size, + file, + } + + try { + await executionApi.userUploadsBinaries(requestParameters) + } catch (error) { + console.error('Binary upload failed:', error) + errorMessages.push(i18n.global.t('SCAN_ERROR_ALERT_BINARY_UPLOAD_FAILED')) + } + } else { + errorMessages.push(i18n.global.t('SCAN_ERROR_ALERT_NO_DATA_SECTION')) + } + } + + private async approveJob (projectId: string, jobId: string, errorMessages: string[]) { + const requestParameters: UserApproveJobRequest = { + projectId, + jobUUID: jobId, + } + + try { + await executionApi.userApproveJob(requestParameters) + } catch (error) { + console.error('Job approval failed:', error) + errorMessages.push(i18n.global.t('SCAN_ERROR_ALERT_JOB_NOT_APPROVED')) + } + } +} + +export default new ScanService() diff --git a/sechub-web-ui/src/services/executionService/executionService.ts b/sechub-web-ui/src/services/executionService/executionService.ts new file mode 100644 index 000000000..c5e9be6e9 --- /dev/null +++ b/sechub-web-ui/src/services/executionService/executionService.ts @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: MIT +import { SecHubExecutionApi } from '@/generated-sources/openapi' +import * as runtime from '@/generated-sources/openapi/runtime' +import apiConfig from '../configuration' +import { v4 as uuidv4 } from 'uuid' +import { FormDataBodyBuilder } from './FormDataBodyBuilder' + +export interface UserUploadSourceCodeWorkaroundRequest { + projectId: string; + jobUUID: string; + checkSum: string; + file: File; +} + +export interface UserUploadsBinariesWorkaroundRequest { + projectId: string; + jobUUID: string; + file: File; + xFileSize: string; + checkSum: string; +} + +// We use this class to override the upload functions generated by the openapi client +class SecHubExecutionApiWorkaround extends SecHubExecutionApi { + async userUploadSourceCode (requestParameters: UserUploadSourceCodeWorkaroundRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.userUploadSourceCodeRaw(requestParameters, initOverrides) + } + + async userUploadSourceCodeRaw (requestParameters: UserUploadSourceCodeWorkaroundRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.projectId == null) { + throw new runtime.RequiredError( + 'projectId', + 'Required parameter "projectId" was null or undefined when calling userUploadSourceCode().' + ) + } + + if (requestParameters.jobUUID == null) { + throw new runtime.RequiredError( + 'jobUUID', + 'Required parameter "jobUUID" was null or undefined when calling userUplosadSourceCode().' + ) + } + + if (requestParameters.checkSum == null) { + throw new runtime.RequiredError( + 'checkSum', + 'Required parameter "checkSum" was null or undefined when calling userUploadSourceCode().' + ) + } + + if (requestParameters.file == null) { + throw new runtime.RequiredError( + 'file', + 'Required parameter "file" was null or undefined when calling userUploadSourceCode().' + ) + } + + // Create boundary (not generated by openapi) + const boundary = uuidv4() + + // building header with boundary + const headerParameters: runtime.HTTPHeaders = {} + headerParameters['Content-Type'] = `multipart/form-data; boundary=${boundary}` + if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { + headerParameters.Authorization = 'Basic ' + btoa(this.configuration.username + ':' + this.configuration.password) + } + + // building body + const builder = new FormDataBodyBuilder(boundary) + builder.addString('checkSum', requestParameters.checkSum) + builder.addFile('file', requestParameters.file) + const formData = await builder.build() + + const response = await this.request({ + path: `/api/project/{projectId}/job/{jobUUID}/sourcecode`.replace(`{${'projectId'}}`, encodeURIComponent(String(requestParameters.projectId))).replace(`{${'jobUUID'}}`, encodeURIComponent(String(requestParameters.jobUUID))), + method: 'POST', + headers: headerParameters, + body: formData, + }, initOverrides) + + return new runtime.VoidApiResponse(response) + } + + async userUploadsBinaries (requestParameters: UserUploadsBinariesWorkaroundRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + await this.userUploadsBinariesRaw(requestParameters, initOverrides) + } + + async userUploadsBinariesRaw (requestParameters: UserUploadsBinariesWorkaroundRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters.projectId == null) { + throw new runtime.RequiredError( + 'projectId', + 'Required parameter "projectId" was null or undefined when calling userUploadsBinaries().' + ) + } + + if (requestParameters.jobUUID == null) { + throw new runtime.RequiredError( + 'jobUUID', + 'Required parameter "jobUUID" was null or undefined when calling userUploadsBinaries().' + ) + } + + if (requestParameters.file == null) { + throw new runtime.RequiredError( + 'file', + 'Required parameter "file" was null or undefined when calling userUploadsBinaries().' + ) + } + + if (requestParameters.xFileSize == null) { + throw new runtime.RequiredError( + 'xFileSize', + 'Required parameter "xFileSize" was null or undefined when calling userUploadsBinaries().' + ) + } + + if (requestParameters.checkSum == null) { + throw new runtime.RequiredError( + 'checkSum', + 'Required parameter "checkSum" was null or undefined when calling userUploadsBinaries().' + ) + } + + // Adding content type and boundary (not generated) + const boundary = uuidv4() + + // building header + const headerParameters: runtime.HTTPHeaders = {} + headerParameters['Content-Type'] = `multipart/form-data; boundary=${boundary}` + headerParameters['x-file-size'] = requestParameters.xFileSize + if (this.configuration && (this.configuration.username !== undefined || this.configuration.password !== undefined)) { + headerParameters.Authorization = 'Basic ' + btoa(this.configuration.username + ':' + this.configuration.password) + } + + // building body + const builder = new FormDataBodyBuilder(boundary) + builder.addString('checkSum', requestParameters.checkSum) + builder.addFile('file', requestParameters.file) + const formData = await builder.build() + + const response = await this.request({ + path: `/api/project/{projectId}/job/{jobUUID}/binaries`.replace(`{${'projectId'}}`, encodeURIComponent(String(requestParameters.projectId))).replace(`{${'jobUUID'}}`, encodeURIComponent(String(requestParameters.jobUUID))), + method: 'POST', + headers: headerParameters, + body: formData, + }, initOverrides) + + return new runtime.VoidApiResponse(response) + } +} + +const executionApi = new SecHubExecutionApiWorkaround(apiConfig) +export default executionApi diff --git a/sechub-web-ui/src/typed-router.d.ts b/sechub-web-ui/src/typed-router.d.ts index 9cfb62a16..a75be2f18 100644 --- a/sechub-web-ui/src/typed-router.d.ts +++ b/sechub-web-ui/src/typed-router.d.ts @@ -20,6 +20,7 @@ declare module 'vue-router/auto-routes' { */ export interface RouteNamedMap { '/': RouteRecordInfo<'/', '/', Record, Record>, - '/[id]': RouteRecordInfo<'/[id]', '/:id', { id: ParamValue }, { id: ParamValue }>, + '/[id]/': RouteRecordInfo<'/[id]/', '/:id', { id: ParamValue }, { id: ParamValue }>, + '/[id]/scan': RouteRecordInfo<'/[id]/scan', '/:id/scan', { id: ParamValue }, { id: ParamValue }>, } } diff --git a/sechub-web-ui/src/utils/cryptoUtils.ts b/sechub-web-ui/src/utils/cryptoUtils.ts new file mode 100644 index 000000000..168ea012d --- /dev/null +++ b/sechub-web-ui/src/utils/cryptoUtils.ts @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +import CryptoJS from 'crypto-js' + +export async function createSha256Checksum (file: File): Promise { + const chunkSize = 64 * 1024 * 1024 // 64MB chunks + const fileReader = new FileReader() + let offset = 0 + const sha256 = CryptoJS.algo.SHA256.create() + + return new Promise((resolve, reject) => { + fileReader.onload = (event: ProgressEvent) => { + if (!event.target) { + reject(new Error('FileReader event target is null')) + return + } + + const data = event.target.result as ArrayBuffer + const wordArray = CryptoJS.lib.WordArray.create(data) + sha256.update(wordArray) + + offset += chunkSize + if (offset < file.size) { + readNextChunk() + } else { + const hash = sha256.finalize() + resolve(hash.toString(CryptoJS.enc.Hex)) + } + } + + fileReader.onerror = error => { + reject(error) + } + + function readNextChunk () { + const slice = file.slice(offset, offset + chunkSize) + fileReader.readAsArrayBuffer(slice) + } + + readNextChunk() + }) +} diff --git a/sechub-web-ui/src/utils/projectUtils.ts b/sechub-web-ui/src/utils/projectUtils.ts index b4d073986..cf1ee1440 100644 --- a/sechub-web-ui/src/utils/projectUtils.ts +++ b/sechub-web-ui/src/utils/projectUtils.ts @@ -1,5 +1,8 @@ // SPDX-License-Identifier: MIT export function formatDate (dateString: string) { + if (dateString === '') { + return + } const date = new Date(dateString) const day = String(date.getDate()).padStart(2, '0') const month = String(date.getMonth() + 1).padStart(2, '0') diff --git a/sechub-web-ui/src/utils/scanConfigUtils.ts b/sechub-web-ui/src/utils/scanConfigUtils.ts new file mode 100644 index 000000000..6f66f3b5e --- /dev/null +++ b/sechub-web-ui/src/utils/scanConfigUtils.ts @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +import { + SecHubCodeScanConfiguration, + SecHubConfiguration, + SecHubDataConfiguration, + SecHubFileSystemConfiguration, + SecHubSecretScanConfiguration, +} from '@/generated-sources/openapi' + +export function buildSecHubConfiguration (scanTypes: string[], uploadFile: File, fileType: string, projectId: string): SecHubConfiguration { + const UNIQUE_NAME = 'web-ui-upload' + + const fileSystemConfig: SecHubFileSystemConfiguration = { + files: [uploadFile.name], + } + + const dataConfiguration: SecHubDataConfiguration = { + sources: fileType === 'sources' ? [{ name: UNIQUE_NAME, fileSystem: fileSystemConfig }] : undefined, + binaries: fileType === 'binaries' ? [{ name: UNIQUE_NAME, fileSystem: fileSystemConfig }] : undefined, + } + + const codeScanConfiguration: SecHubCodeScanConfiguration = {} + const secretScanConfiguration: SecHubSecretScanConfiguration = {} + + if (scanTypes.includes('codeScan')) { + codeScanConfiguration.use = [UNIQUE_NAME] + } + + if (scanTypes.includes('secretScan')) { + secretScanConfiguration.use = [UNIQUE_NAME] + } + + const config: SecHubConfiguration = { + apiVersion: '1.0', + projectId, + data: dataConfiguration, + } + + // adding scan types to configuration + if (codeScanConfiguration.use) { + config.codeScan = codeScanConfiguration + } + + if (secretScanConfiguration.use) { + config.secretScan = secretScanConfiguration + } + + return config +} diff --git a/sechub-web-ui/test-setups/gosec-executor.json b/sechub-web-ui/test-setups/gosec-executor.json new file mode 100644 index 000000000..3fbbff38d --- /dev/null +++ b/sechub-web-ui/test-setups/gosec-executor.json @@ -0,0 +1,39 @@ +{ + "name": "pds-gosec", + "productIdentifier": "PDS_CODESCAN", + "executorVersion": 1, + "enabled": true, + "setup": { + "baseURL": "https://localhost:8444", + "credentials": { + "user": "pds-inttest-techuser", + "password": "pds-inttest-apitoken" + }, + "jobParameters": [ + { + "key": "pds.config.productidentifier", + "value": "PDS_SOLUTION_GOSEC_MOCKED" + }, + { + "key": "pds.config.use.sechub.storage", + "value": false + }, + { + "key": "pds.mocking.disabled", + "value": true + }, + { + "key": "sechub.productexecutor.pds.timeout.minutes", + "value": 60 + }, + { + "key": "sechub.productexecutor.pds.timetowait.nextcheck.milliseconds", + "value": 500 + }, + { + "key": "sechub.productexecutor.pds.trustall.certificates", + "value": true + } + ] + } +} diff --git a/sechub-web-ui/test-setups/setup-integration-test-server.sh b/sechub-web-ui/test-setups/setup-integration-test-server.sh new file mode 100644 index 000000000..101c671b8 --- /dev/null +++ b/sechub-web-ui/test-setups/setup-integration-test-server.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# SPDX-License-Identifier: MIT +export SECHUB_APITOKEN=''; +export SECHUB_USERID=; +export SECHUB_SERVER=https://localhost:8443; +export TEST_PROJECT_NAME="test-gosec" + +../sechub-developertools/scripts/sechub-api.sh project_create $TEST_PROJECT_NAME $SECHUB_USERID +../sechub-developertools/scripts/sechub-api.sh project_assign_user $TEST_PROJECT_NAME $SECHUB_USERID + +# creates and assigns a mocked executor, the result will always be RED +../sechub-developertools/scripts/sechub-api.sh executor_create gosec-executor.json +../sechub-developertools/scripts/sechub-api.sh profile_create gosec-profile pds-gosec +../sechub-developertools/scripts/sechub-api.sh project_assign_profile $TEST_PROJECT_NAME gosec-profile