diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca65c8a --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.gitlab-ci.yml.bck +node_modules/* +imgs/.DS_Store diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b3750f3..b727cf6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,6 +29,7 @@ Cli Version Check: script: - npm i - node ./cancelOldPipeline.js + - node ./utility/updateCommitStatus.js - | # Define installation directory and version file path export CLI_FOLDER="$(pwd)/veracode-cli" @@ -218,4 +219,4 @@ workflow: PIPELINE_NAME: "${PROJECT_NAME} - SCA Scan" - if: '$EXECUTE_IAC == "true"' variables: - PIPELINE_NAME: "${PROJECT_NAME} - IAC Scan" + PIPELINE_NAME: "${PROJECT_NAME} - IAC Scan" \ No newline at end of file diff --git a/cancelOldPipeline.js b/cancelOldPipeline.js index 711423a..f227288 100644 --- a/cancelOldPipeline.js +++ b/cancelOldPipeline.js @@ -21,19 +21,43 @@ async function cancelOldPipeline() { return; } + // Find the current pipeline in the list to get its full timestamp with milliseconds + const currentPipeline = pipelines.find(p => p.id === Number(currentPipelineId)); + const currentPipelineCreatedAtFull = currentPipeline ? currentPipeline.created_at : currentPipelineCreatedAt; + + console.log('#### DEBUG - Current Pipeline Info ####'); + console.log('currentPipelineId:', currentPipelineId); + console.log('currentPipelineCreatedAt (env):', currentPipelineCreatedAt); + console.log('currentPipelineCreatedAt (from API):', currentPipelineCreatedAtFull); + console.log('#### DEBUG - Current Pipeline Info ####'); + for (const pipeline of pipelines) { const pipelineId = pipeline.id; + console.log('#### DEBUG - Cancel Old Pipeline ####'); + console.log('pipelineId:', pipelineId); + console.log('currentPipelineId:', currentPipelineId); + console.log('pipeline.created_at:', pipeline.created_at); + console.log('#### DEBUG - Cancel Old Pipeline ####'); + // Skip current pipeline itself if (pipelineId === Number(currentPipelineId)) { console.log(`Skipping current pipeline ${pipelineId}`); continue; } - // Convert current pipeline creation time to epoch milliseconds - const currentEpoch = new Date(currentPipelineCreatedAt).getTime(); + // Convert pipeline creation times to epoch milliseconds + // Use the full timestamp from API for accurate comparison + const currentEpoch = new Date(currentPipelineCreatedAtFull).getTime(); const createdEpoch = new Date(pipeline.created_at).getTime(); + console.log('#### DEBUG - Time Comparison ####'); + console.log('currentEpoch:', currentEpoch); + console.log('createdEpoch:', createdEpoch); + console.log('Difference (ms):', createdEpoch - currentEpoch); + console.log('Is createdEpoch > currentEpoch?', createdEpoch > currentEpoch); + console.log('#### DEBUG - Time Comparison ####'); + // Skip newer pipelines if (createdEpoch > currentEpoch) { console.log(`Skipping newer pipeline ${pipelineId} created at ${pipeline.created_at}`); diff --git a/displayScanResult.js b/displayScanResult.js index 8a55af3..97f4a42 100644 --- a/displayScanResult.js +++ b/displayScanResult.js @@ -1,7 +1,7 @@ const { createWikiPage, createComment } = require('./utility/service'); const { scaResult, pipelineResult, policyResult, iacResult } = require('./utility/utils'); const { appConfig } = require('./config'); -async function displayScanResult(scanResult, warningMessage = "") { +async function displayScanResult(scanResult, warningMessage = "", debug = null) { const executePipeline = process.env.EXECUTE_PIPELINE; const executeSca = process.env.EXECUTE_SCA; const executeIac = process.env.EXECUTE_IAC; @@ -13,17 +13,18 @@ async function displayScanResult(scanResult, warningMessage = "") { if (eventName === appConfig().pullRequestEventName || eventName === appConfig().pushEventName) { try { if ((scanType === "IaC" && Object.entries(scanResult).length > 0) || scanResult.length > 0) { - let formattedContent = scanType === 'SCA' ? scaResult(scanResult[0]) : scanType === 'Pipeline' ? pipelineResult(scanResult) : scanType === 'IaC' ? iacResult(scanResult) : policyResult(scanResult); + const sourceBranch = process.env.SOURCE_BRANCH; + let formattedContent = scanType === 'SCA' ? scaResult(scanResult[0]) : scanType === 'Pipeline' ? await pipelineResult(scanResult, sourceBranch, projectUrl, debug) : scanType === 'IaC' ? iacResult(scanResult) : policyResult(scanResult); const wikiContent = `${scanType} Scan completed. :white_check_mark:\n\n` + formattedContent; const createWikiResponse = await createWikiPage(scanType, projectUrl, wikiContent); - const commentContent = createWikiResponse.hasOwnProperty('wikiUrl') && createWikiResponse?.wikiUrl !== "" ? `${scanType} Scan completed. :white_check_mark:\n\n` + formattedContent : `${scanType} Scan completed. :white_check_mark:\n\n` + formattedContent; + const commentContent = createWikiResponse.hasOwnProperty('wikiUrl') && createWikiResponse?.wikiUrl !== "" ? `${scanType} Scan completed.\n\n` + formattedContent : `${scanType} Scan completed.\n\n` + formattedContent; await createComment(projectUrl, mergeRequestId, eventName, commitSha, commentContent); } else { let commentContent = ""; if (warningMessage) { - commentContent = `${scanType} Scan completed. :warning:\n\n` + `${warningMessage}\n`; + commentContent = `${scanType} Scan completed.\n\n` + `${warningMessage}\n`; } else { - commentContent = `${scanType} Scan completed. :white_check_mark:\n\n` + 'No Vulnerability found.\n'; + commentContent = `${scanType} Scan completed.\n\n` + 'No Vulnerability found.\n'; } await createComment(projectUrl, mergeRequestId, eventName, commitSha, commentContent); diff --git a/imgs/High.png b/imgs/High.png new file mode 100644 index 0000000..72904da Binary files /dev/null and b/imgs/High.png differ diff --git a/imgs/Informational.png b/imgs/Informational.png new file mode 100644 index 0000000..566d211 Binary files /dev/null and b/imgs/Informational.png differ diff --git a/imgs/Low.png b/imgs/Low.png new file mode 100644 index 0000000..dd3159c Binary files /dev/null and b/imgs/Low.png differ diff --git a/imgs/Medium.png b/imgs/Medium.png new file mode 100644 index 0000000..a686240 Binary files /dev/null and b/imgs/Medium.png differ diff --git a/imgs/Very_High.png b/imgs/Very_High.png new file mode 100644 index 0000000..1088bae Binary files /dev/null and b/imgs/Very_High.png differ diff --git a/imgs/Very_Low.png b/imgs/Very_Low.png new file mode 100644 index 0000000..37256f4 Binary files /dev/null and b/imgs/Very_Low.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..db81c46 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,521 @@ +{ + "name": "veracode-gitlab-workflow-integration", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "veracode-gitlab-workflow-integration", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "axios": "^1.7.3", + "crypto": "^1.0.1", + "crypto-js": "^4.2.0", + "execa": "5.1.1", + "xml2js": "^0.6.2" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, + "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/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + } + } +} diff --git a/utility/common.js b/utility/common.js index 835e970..8f9d67a 100644 --- a/utility/common.js +++ b/utility/common.js @@ -264,8 +264,8 @@ async function getVeracodePolicyByName(vid, vkey, policyName) { async function getPolicyByName(vid, vkey, policyName) { const resource = { resourceUri: veracodeConfig().policyUri, - queryAttribute: 'name', - queryValue: encodeURIComponent(policyName) + queryAttribute1: 'name', + queryValue1: encodeURIComponent(policyName) }; const response = await getResourceByAttribute(vid, vkey, resource); return response; @@ -305,7 +305,7 @@ async function getApplicationFindings(appGuid, vid, vkey) { if (!findingsResponse._embedded) { console.log('No Policy scan found, lets look for sandbox scan findings'); const getSandboxGUID = { - resourceUri: `${veracodeConfig().findingsUri}/${appGuid}/sandboxes`, + resourceUri: `${veracodeConfig().applicationUri}/${appGuid}/sandboxes`, queryAttribute1: '', queryValue1: '', }; @@ -527,6 +527,7 @@ module.exports = { deleteResourceById, getApplicationFindings, veracodePolicyVerification, + getPolicyByName, getLatestVersion, downloadAndExtractCli, setScanResultError, diff --git a/utility/labels.js b/utility/labels.js index 2b227a0..a4fffe6 100644 --- a/utility/labels.js +++ b/utility/labels.js @@ -1,32 +1,32 @@ const VERACODE_FLAW_LABELS = { "Very High": { 'name': 'VeracodeFlaw: Very High', - 'color': '#A90533', + 'color': '#D92B85', 'description': 'A Veracode Flaw, Very High severity', }, High: { 'name': 'VeracodeFlaw: High', - 'color': '#DD3B35', + 'color': '#E61F25', 'description': 'A Veracode Flaw, High severity' }, Medium: { 'name': 'VeracodeFlaw: Medium', - 'color': '#FF7D00', + 'color': '#FD7333', 'description': 'A Veracode Flaw, Medium severity' }, Low: { 'name': 'VeracodeFlaw: Low', - 'color': '#FFBE00', + 'color': '#FFCC33', 'description': 'A Veracode Flaw, Low severity' }, "Very Low":{ 'name': 'VeracodeFlaw: Very Low', - 'color': '#33ADD2', + 'color': '#C9DA2C', 'description': 'A Veracode Flaw, Very Low severity', }, Informational: { 'name': 'VeracodeFlaw: Informational', - 'color': '#0270D3', + 'color': '#8DBD3E', 'description': 'A Veracode Flaw, Informational severity', }, Unknown: { diff --git a/utility/service.js b/utility/service.js index 0b2eb59..2939393 100644 --- a/utility/service.js +++ b/utility/service.js @@ -106,7 +106,96 @@ async function createComment(projectUrl, mergeRequestId, eventName, commitSha, f try { let reqData; let url; + + // For merge requests, check if a matching comment already exists if(eventName === appConfig().pullRequestEventName){ + // Extract scan type and expected patterns from the new content + const contentLines = formattedContent.split('\n'); + const firstLine = contentLines[0] || ''; + + // Extract scan type from first line (e.g., "Pipeline Scan completed." or "Pipeline Scan completed.") + const scanTypeMatch = firstLine.match(/(\w+)\s+Scan\s+completed/); + const scanType = scanTypeMatch ? scanTypeMatch[1] : ''; + + // Expected scan message patterns (these appear in the content) + const expectedPatterns = [ + '**Veracode IaC/Secrets Scan found vulnerabilities/misconfigurations/secrets**', + '**Veracode SCA Scan found vulnerabilities**', + '**Veracode Static Scan found flaws**' + ]; + + // Check if content contains Veracode image and one of the expected patterns + const hasVeracodeImage = formattedContent.includes('![Veracode]') || formattedContent.includes('veracodePlatformLogoSmall.png'); + const hasExpectedPattern = expectedPatterns.some(pattern => formattedContent.includes(pattern)); + + // Only check for existing comments if we have a valid pattern + if (scanType && hasVeracodeImage && hasExpectedPattern) { + try { + // Fetch all notes for this merge request + const notesUrl = `https://${hostName}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/notes`; + let allNotes = []; + let page = 1; + + while (true) { + const response = await axios.get(notesUrl, { + ...headers, + params: { + per_page: 100, + page: page + } + }); + + const notes = response.data; + if (notes.length === 0) break; + + allNotes = [...allNotes, ...notes]; + + const totalPages = parseInt(response.headers['x-total-pages'] || '1'); + if (page >= totalPages) break; + page++; + } + + // Find matching comment by checking: + // 1. First line contains scan type + "Scan completed" + // 2. Contains Veracode image + // 3. Contains one of the expected scan message patterns + const matchingNote = allNotes.find(note => { + if (!note.body) return false; + + const noteLines = note.body.split('\n'); + const noteFirstLine = noteLines[0] || ''; + + // Check if first line matches scan type pattern + const noteScanTypeMatch = noteFirstLine.match(/(\w+)\s+Scan\s+completed/); + const noteScanType = noteScanTypeMatch ? noteScanTypeMatch[1] : ''; + + // Check if note contains Veracode image + const noteHasImage = note.body.includes('![Veracode]') || note.body.includes('veracodePlatformLogoSmall.png'); + + // Check if note contains one of the expected patterns + const noteMatchesPattern = expectedPatterns.some(pattern => note.body.includes(pattern)); + + // Match if scan type matches, has image, and matches pattern + return noteScanType === scanType && noteHasImage && noteMatchesPattern; + }); + + if (matchingNote) { + // Update existing comment + const updateUrl = `https://${hostName}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/notes/${matchingNote.id}`; + reqData = { + body: formattedContent + }; + await axios.put(updateUrl, reqData, headers); + console.log(`Updated existing comment (note ID: ${matchingNote.id}) under the ${projectUrl} project for ${infoText}`); + return; + } + } catch (error) { + console.log(`Error while checking for existing comments, will create new one:`, error.response?.data || error.message); + // Continue to create new comment if check fails + } + } + + // Create new comment (either no match found or not a merge request with expected pattern) url = `https://${hostName}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/notes` reqData = { body: formattedContent @@ -171,4 +260,238 @@ async function cancelPipeline(hostName, veracodeProjectId, pipelineId) { } } -module.exports = {checkLabelExists, createLabels, createIssue, listExistingOpenIssues, createWikiPage, createComment, fetchAllPipelines, getPipelineVariables, cancelPipeline} \ No newline at end of file +async function updateCommitStatus(MR_SHA, STATE, PIPELINE_NAME, CI_PIPELINE_URL, DESCRIPTION, DEBUG) { + if (DEBUG === "true") { + console.log('#### DEBUG - Update Commit Status ####'); + console.log('MR_SHA:', MR_SHA); + console.log('STATE:', STATE); + console.log('PIPELINE_NAME:', PIPELINE_NAME); + console.log('CI_PIPELINE_URL:', CI_PIPELINE_URL); + console.log('DESCRIPTION:', DESCRIPTION); + console.log('hostName:', hostName); + console.log('projectId:', projectId); + console.log('#### DEBUG - Update Commit Status ####'); + } + + try { + const url = `https://${hostName}/api/v4/projects/${projectId}/statuses/${MR_SHA}`; + const reqData = { + state: STATE, + name: PIPELINE_NAME, + target_url: CI_PIPELINE_URL, + description: DESCRIPTION + }; + + const response = await axios.post(url, reqData, headers); + + if (response.status >= 200 && response.status < 300) { + console.log("Commit status updated successfully"); + return response.data; + } else { + console.error("MR couldn't be updated"); + return null; + } + } catch (error) { + console.error("MR couldn't be updated", error.response?.data || error.message); + return null; + } +} + +// Helper function to calculate similarity between two strings (simple Levenshtein-like) +function calculateSimilarity(str1, str2) { + const longer = str1.length > str2.length ? str1 : str2; + const shorter = str1.length > str2.length ? str2 : str1; + if (longer.length === 0) return 1.0; + + const distance = levenshteinDistance(longer.toLowerCase(), shorter.toLowerCase()); + return (longer.length - distance) / longer.length; +} + +// Simple Levenshtein distance calculation +function levenshteinDistance(str1, str2) { + const matrix = []; + for (let i = 0; i <= str2.length; i++) { + matrix[i] = [i]; + } + for (let j = 0; j <= str1.length; j++) { + matrix[0][j] = j; + } + for (let i = 1; i <= str2.length; i++) { + for (let j = 1; j <= str1.length; j++) { + if (str2.charAt(i - 1) === str1.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j - 1] + 1, + matrix[i][j - 1] + 1, + matrix[i - 1][j] + 1 + ); + } + } + } + return matrix[str2.length][str1.length]; +} + +async function getSourceFilePath(filePath, branch, projectUrl, lineNumber = null, debug = null) { + try { + if (!filePath || !branch || !projectUrl) { + console.log("Error: Missing required parameters for getSourceFilePath"); + return null; + } + + // Extract filename from the path + const fileName = filePath.split('/').pop() || filePath.split('\\').pop(); + const normalizedFilePath = filePath.startsWith('/') ? filePath.substring(1) : filePath; + + if (debug === "true") { + console.log('#### Debug - getSourceFilePath - Fuzzy Search ####'); + console.log('Searching for file:', fileName); + console.log('Original path:', filePath); + console.log('Branch:', branch); + console.log('#### Debug - getSourceFilePath - Fuzzy Search ####'); + } + + // Get repository tree recursively to search for files + let foundFilePath = null; + try { + const treeUrl = `https://${hostName}/api/v4/projects/${encodeURIComponent(projectId)}/repository/tree`; + let allFiles = []; + let page = 1; + const perPage = 100; + + // Fetch all files recursively + while (true) { + const response = await axios.get(treeUrl, { + ...headers, + params: { + ref: branch, + recursive: true, + per_page: perPage, + page: page + } + }); + + const files = response.data.filter(item => item.type === 'blob'); + allFiles = allFiles.concat(files); + + // Check if there are more pages + const totalPages = parseInt(response.headers['x-total-pages'] || '1'); + if (page >= totalPages) break; + page++; + } + + if (debug === "true") { + console.log(`Found ${allFiles.length} files in repository`); + } + + // First, try exact match + let exactMatch = allFiles.find(file => + file.path === normalizedFilePath || + file.path.endsWith(normalizedFilePath) || + file.path === filePath + ); + + if (exactMatch) { + foundFilePath = exactMatch.path; + if (debug === "true") { + console.log(`Exact match found: ${foundFilePath}`); + } + } else { + // Try to find by filename + let filenameMatches = allFiles.filter(file => + file.name === fileName || + file.name.toLowerCase() === fileName.toLowerCase() + ); + + if (filenameMatches.length === 1) { + foundFilePath = filenameMatches[0].path; + if (debug === "true") { + console.log(`Single filename match found: ${foundFilePath}`); + } + } else if (filenameMatches.length > 1) { + // Multiple files with same name - use fuzzy matching on path + let bestMatch = null; + let bestScore = 0; + + for (const file of filenameMatches) { + // Calculate similarity between original path and found path + const pathSimilarity = calculateSimilarity(normalizedFilePath, file.path); + if (pathSimilarity > bestScore) { + bestScore = pathSimilarity; + bestMatch = file; + } + } + + if (bestMatch && bestScore > 0.3) { // Threshold for similarity + foundFilePath = bestMatch.path; + if (debug === "true") { + console.log(`Fuzzy match found (score: ${bestScore.toFixed(2)}): ${foundFilePath}`); + } + } else { + // Use the first match if no good fuzzy match + foundFilePath = filenameMatches[0].path; + if (debug === "true") { + console.log(`Using first filename match: ${foundFilePath}`); + } + } + } else { + // No exact filename match - try fuzzy search on all files + let bestMatch = null; + let bestScore = 0; + + for (const file of allFiles) { + const nameSimilarity = calculateSimilarity(fileName, file.name); + const pathSimilarity = calculateSimilarity(normalizedFilePath, file.path); + const combinedScore = (nameSimilarity * 0.7) + (pathSimilarity * 0.3); + + if (combinedScore > bestScore) { + bestScore = combinedScore; + bestMatch = file; + } + } + + if (bestMatch && bestScore > 0.5) { // Threshold for fuzzy match + foundFilePath = bestMatch.path; + if (debug === "true") { + console.log(`Fuzzy match found (score: ${bestScore.toFixed(2)}): ${foundFilePath}`); + } + } + } + } + } catch (error) { + if (debug === "true") { + console.log(`Error searching repository tree: ${error.response?.data || error.message}`); + } + // Fallback to original path if search fails + foundFilePath = normalizedFilePath; + } + + // If no match found, use original path + if (!foundFilePath) { + if (debug === "true") { + console.log(`No match found, using original path: ${normalizedFilePath}`); + } + foundFilePath = normalizedFilePath; + } + + // Encode the file path for URL + const encodedFilePath = foundFilePath.split('/').map(segment => encodeURIComponent(segment)).join('/'); + + // Construct the GitLab blob URL + let fileUrl = `${projectUrl}/-/blob/${encodeURIComponent(branch)}/${encodedFilePath}`; + + if (lineNumber) { + fileUrl += `#L${lineNumber}`; + } + + if (debug === "true") { + console.log(`Final file URL: ${fileUrl}`); + } + return fileUrl; + } catch (error) { + console.log("Error constructing source file path:", error.message); + return null; + } +} + +module.exports = {checkLabelExists, createLabels, createIssue, listExistingOpenIssues, createWikiPage, createComment, fetchAllPipelines, getPipelineVariables, cancelPipeline, updateCommitStatus, getSourceFilePath} \ No newline at end of file diff --git a/utility/updateCommitStatus.js b/utility/updateCommitStatus.js new file mode 100644 index 0000000..ae3231a --- /dev/null +++ b/utility/updateCommitStatus.js @@ -0,0 +1,61 @@ +const { updateCommitStatus } = require("./service"); + +const commitSha = process.env.COMMIT_SHA; +const state = 'running'; +const pipelineName = process.env.PIPELINE_NAME ; +const ciPipelineUrl = process.env.CI_PIPELINE_URL; +const description = process.env.PIPELINE_NAME+' started'; +const debug = process.env.DEBUG; + +async function updateStatus() { + if (debug === "true") { + console.log('#### DEBUG - Update Commit Status ####'); + console.log('commitSha:', commitSha); + console.log('state:', state); + console.log('pipelineName:', pipelineName); + console.log('ciPipelineUrl:', ciPipelineUrl); + console.log('description:', description); + console.log('#### DEBUG - Update Commit Status ####'); + } + try { + if (!commitSha) { + console.log("Error: Commit SHA not found. Please set CI_COMMIT_SHA or COMMIT_SHA environment variable."); + process.exit(0); + } + + if (!ciPipelineUrl) { + console.log("Error: CI_PIPELINE_URL not found."); + process.exit(0); + } + + if (pipelineName && pipelineName.includes('Sandbox Scan')) { + console.log("No Need to update MR status for a Sandbox Scan"); + process.exit(0); + } + + if (pipelineName && pipelineName.includes('Policy Scan')) { + console.log("No Need to update MR status for a Policy Scan"); + process.exit(0); + } + + if (debug === "true") { + console.log(`Updating commit status for SHA: ${commitSha}`); + console.log(`State: ${state}`); + console.log(`Pipeline Name: ${pipelineName}`); + console.log(`Description: ${description}`); + console.log('#### DEBUG - Update Commit Status ####'); + } + + const result = await updateCommitStatus(commitSha, state, pipelineName, ciPipelineUrl, description, debug); + + if (!result) { + console.log("MR couldn't be updated"); + } + + console.log("Commit status updated successfully"); + } catch (error) { + console.log("MR couldn't be updated", error.message); + } +} + +updateStatus(); diff --git a/utility/utils.js b/utility/utils.js index 3f7b0ba..013a0c3 100644 --- a/utility/utils.js +++ b/utility/utils.js @@ -326,66 +326,387 @@ const severityRank = { "INFORMATIONAL":0 }; -function initialScanInfo(){ - return '- :red_circle: **Critical**: Immediate attention required.\n' + - '- :orange_circle: **High**: Serious but slightly lower priority than Critical.\n' + - '- :yellow_circle: **Medium**: Moderate risk, to be fixed in a reasonable timeframe.\n' + - '- :green_circle: **Low**: Minimal risk, can be addressed in the normal course of development.\n' + - '- :blue_circle: **Very Low**: Recommendations or low-priority findings.\n' + - '- :white_circle: **Informational**: No security impact, can be ignored.\n' +/** + * Normalizes severity strings from different scan types to a standard format + * @param {string} severity - Severity string from scan results + * @returns {string} Normalized severity string + */ +function normalizeSeverity(severity) { + if (!severity) return ''; + + // Normalize to string and trim whitespace + const normalized = String(severity).trim(); + + // Handle uppercase (IaC uses CRITICAL, HIGH, etc.) + const upperSeverity = normalized.toUpperCase(); + + // Map to standard severity names + if (upperSeverity === 'CRITICAL' || normalized === 'Critical') { + return 'Critical'; + } else if (upperSeverity === 'HIGH' || normalized === 'High' || normalized === ' High') { + return 'High'; + } else if (upperSeverity === 'MEDIUM' || normalized === 'Medium') { + return 'Medium'; + } else if (upperSeverity === 'LOW' || normalized === 'Low' || normalized === 'Low Risk') { + return 'Low'; + } else if (upperSeverity === 'VERY_LOW' || normalized === 'Very Low') { + return 'Very Low'; + } else if (upperSeverity === 'INFORMATIONAL' || normalized === 'Informational') { + return 'Informational'; + } + + // Return original if no match + return normalized; +} + +/** + * Gets the severity image URL from the project repository + * Images are stored in the imgs folder in the same repository, always on 'main' branch + * @param {string} severity - Severity string (will be normalized) + * @param {string} projectUrl - Optional project URL (falls back to CI_PROJECT_URL) + * @returns {string} Image URL or empty string if construction fails + */ +function getSeverityImageUrl(severity, projectUrl = null) { + // Normalize severity first + const normalizedSeverity = normalizeSeverity(severity); + + // Get project URL from environment variable (CI_PROJECT_URL) or fall back to passed parameter + const currentProjectUrl = process.env.CI_PROJECT_URL || projectUrl; + if (!currentProjectUrl) return ''; + + // Map severity to image filename + const imageMap = { + 'Critical': 'Very_High.png', + 'High': 'High.png', + 'Medium': 'Medium.png', + 'Low': 'Low.png', + 'Very Low': 'Very_Low.png', + 'Informational': 'Informational.png' + }; + + const imageFile = imageMap[normalizedSeverity]; + if (!imageFile) return ''; + + // Construct GitLab raw file URL by appending the path to CI_PROJECT_URL + // Format: {CI_PROJECT_URL}/-/raw/main/imgs/{filename} + try { + // Remove trailing slash from project URL if present + const baseUrl = currentProjectUrl.replace(/\/$/, ''); + return `${baseUrl}/-/raw/main/imgs/${encodeURIComponent(imageFile)}`; + } catch (error) { + console.log('Error constructing image URL:', error.message); + return ''; + } +} + +/** + * Creates a colored indicator using images for severity display + * @param {string} severity - Severity string (will be normalized) + * @param {string} projectUrl - Optional project URL (falls back to CI_PROJECT_URL) + * @returns {string} Markdown image indicator or empty string if image URL can't be constructed + */ +function getColoredIndicator(severity, projectUrl = null) { + const imageUrl = getSeverityImageUrl(severity, projectUrl); + if (imageUrl) { + const normalizedSeverity = normalizeSeverity(severity); + return `![${normalizedSeverity}](${imageUrl})`; + } + // Fallback to empty string if image URL can't be constructed + return ''; +} + +function initialScanInfo(scanType = 'Pipeline', scanResults = null, severityColors = null, projectUrl = null, branch = null){ + let scanMessage = ''; + + switch(scanType) { + case 'Pipeline': + scanMessage = '![Veracode](https://analysiscenter.veracode.com/images/interface/veracodePlatformLogoSmall.png)
**Veracode Static Scan found flaws**'; + break; + case 'Sandbox': + scanMessage = '![Veracode](https://analysiscenter.veracode.com/images/interface/veracodePlatformLogoSmall.png)
**Veracode Static Scan found flaws**'; + break; + case 'Policy': + scanMessage = '![Veracode](https://analysiscenter.veracode.com/images/interface/veracodePlatformLogoSmall.png)
**Veracode Static Scan found flaws**'; + break; + case 'SCA': + scanMessage = '![Veracode](https://analysiscenter.veracode.com/images/interface/veracodePlatformLogoSmall.png)
**Veracode SCA Scan found vulnerabilities**'; + break; + case 'IaC': + scanMessage = '![Veracode](https://analysiscenter.veracode.com/images/interface/veracodePlatformLogoSmall.png)
**Veracode IaC/Secrets Scan found vulnerabilities/misconfigurations/secrets**'; + break; + default: + scanMessage = '![Veracode](https://analysiscenter.veracode.com/images/interface/veracodePlatformLogoSmall.png)
**Veracode Scan found issues**'; + } + + let output = scanMessage + '\n\n'; + + // Add severity breakdown table for Pipeline scans + if (scanType === 'Pipeline' && scanResults && Array.isArray(scanResults) && scanResults.length > 0) { + // Count findings by severity + const severityCounts = { + 'Critical': 0, + 'High': 0, + 'Medium': 0, + 'Low': 0, + 'Very Low': 0, + 'Informational': 0 + }; + + scanResults.forEach(result => { + const severityText = severityType(result.severity); + if (severityCounts.hasOwnProperty(severityText)) { + severityCounts[severityText]++; + } + }); + + // Create severity breakdown table + output += '### Findings by Severity\n\n'; + output += '| Severity | Count |\n'; + output += '|----------|-------|\n'; + output += `| ${getColoredIndicator('Critical', projectUrl)} | ${severityCounts['Critical']} |\n`; + output += `| ${getColoredIndicator('High', projectUrl)} | ${severityCounts['High']} |\n`; + output += `| ${getColoredIndicator('Medium', projectUrl)} | ${severityCounts['Medium']} |\n`; + output += `| ${getColoredIndicator('Low', projectUrl)} | ${severityCounts['Low']} |\n`; + output += `| ${getColoredIndicator('Very Low', projectUrl)} | ${severityCounts['Very Low']} |\n`; + output += `| ${getColoredIndicator('Informational', projectUrl)} | ${severityCounts['Informational']} |\n`; + output += `| **Total** | **${scanResults.length}** |\n\n`; + } + + // Add severity breakdown table for SCA scans + if (scanType === 'SCA' && scanResults && scanResults.vulnerabilities && Array.isArray(scanResults.vulnerabilities)) { + // Count findings by severity (each vulnerability-library combination counts as one) + const severityCounts = { + 'Critical': 0, + 'High': 0, + 'Medium': 0, + 'Low': 0, + 'Very Low': 0, + 'Informational': 0 + }; + + let totalCount = 0; + scanResults.vulnerabilities.forEach(vulnerability => { + const severityText = scaSeverityType(vulnerability.cvss3Score); + // Count each library affected by this vulnerability + const libraryCount = vulnerability.libraries ? vulnerability.libraries.length : 1; + totalCount += libraryCount; + + // Normalize severity text (handle "Low Risk" and " High" with space) + const normalizedSeverity = normalizeSeverity(severityText); + if (severityCounts.hasOwnProperty(normalizedSeverity)) { + severityCounts[normalizedSeverity] += libraryCount; + } + }); + + // Create severity breakdown table + output += '### Findings by Severity\n\n'; + output += '| Severity | Count |\n'; + output += '|----------|-------|\n'; + output += `| ${getColoredIndicator('Critical', projectUrl)} | ${severityCounts['Critical']} |\n`; + output += `| ${getColoredIndicator('High', projectUrl)} | ${severityCounts['High']} |\n`; + output += `| ${getColoredIndicator('Medium', projectUrl)} | ${severityCounts['Medium']} |\n`; + output += `| ${getColoredIndicator('Low', projectUrl)} | ${severityCounts['Low']} |\n`; + output += `| ${getColoredIndicator('Very Low', projectUrl)} | ${severityCounts['Very Low']} |\n`; + output += `| ${getColoredIndicator('Informational', projectUrl)} | ${severityCounts['Informational']} |\n`; + output += `| **Total** | **${totalCount}** |\n\n`; + } + + // Add severity breakdown table for IaC scans + if (scanType === 'IaC' && scanResults) { + // Initialize counts for all severities across all three types + const severityOrder = ['Critical', 'High', 'Medium', 'Low', 'Very Low', 'Informational']; + const counts = { + 'Critical': { vulnerabilities: 0, misconfigurations: 0, secrets: 0 }, + 'High': { vulnerabilities: 0, misconfigurations: 0, secrets: 0 }, + 'Medium': { vulnerabilities: 0, misconfigurations: 0, secrets: 0 }, + 'Low': { vulnerabilities: 0, misconfigurations: 0, secrets: 0 }, + 'Very Low': { vulnerabilities: 0, misconfigurations: 0, secrets: 0 }, + 'Informational': { vulnerabilities: 0, misconfigurations: 0, secrets: 0 } + }; + + // Count vulnerabilities by severity + const vulnerabilityData = scanResults?.vulnerabilities?.matches || []; + vulnerabilityData.forEach(result => { + const severity = normalizeSeverity(result.vulnerability.severity); + if (counts.hasOwnProperty(severity)) { + counts[severity].vulnerabilities++; + } + }); + + // Count misconfigurations by severity + const misconfigurations = scanResults?.configs || []; + misconfigurations.forEach(result => { + const severity = normalizeSeverity(result.Severity); + if (counts.hasOwnProperty(severity)) { + counts[severity].misconfigurations++; + } + }); + + // Count secrets by severity + const secrets = scanResults?.secrets || []; + secrets.forEach(result => { + const severity = normalizeSeverity(result.Severity); + if (counts.hasOwnProperty(severity)) { + counts[severity].secrets++; + } + }); + + // Create severity breakdown table + output += '### Findings by Severity\n\n'; + output += '| Severity | Vulnerability | Misconfiguration | Secrets |\n'; + output += '|----------|---------------|------------------|--------|\n'; + severityOrder.forEach(severity => { + output += `| ${getColoredIndicator(severity, projectUrl)} | ${counts[severity].vulnerabilities} | ${counts[severity].misconfigurations} | ${counts[severity].secrets} |\n`; + }); + output += '\n'; + } + + return output; } function scaResult(scanResult){ - const vulnerabilities = scanResult.vulnerabilities.sort((a,b)=> b.cvss3Score - a.cvss3Score); const libraries = scanResult.libraries; - let output = initialScanInfo(); + const projectUrl = process.env.CI_PROJECT_URL || process.env.PROJECT_URL; + + // Sort vulnerabilities by severity (Very High/Critical first) + // Map normalized severity to severityRank keys + const getSeverityRank = (severity) => { + const normalized = normalizeSeverity(severity); + // Map normalized severity to severityRank keys + if (normalized === 'Critical') return severityRank.CRITICAL; + if (normalized === 'High') return severityRank.HIGH; + if (normalized === 'Medium') return severityRank.MEDIUM; + if (normalized === 'Low') return severityRank.LOW; + if (normalized === 'Very Low') return severityRank.VERY_LOW; + if (normalized === 'Informational') return severityRank.INFORMATIONAL; + return 0; + }; + + const vulnerabilities = scanResult.vulnerabilities.sort((a, b) => { + const severityA = scaSeverityType(a.cvss3Score); + const severityB = scaSeverityType(b.cvss3Score); + const rankA = getSeverityRank(severityA); + const rankB = getSeverityRank(severityB); + // If same rank, sort by CVSS score as tiebreaker + if (rankB === rankA) { + return b.cvss3Score - a.cvss3Score; + } + return rankB - rankA; // Descending order (highest severity first) + }); + + let output = initialScanInfo('SCA', scanResult, null, projectUrl); output+= '
\n'+ 'Scan Details\n\n'+ - '| Vulnerability ID | Severity | Description | Library | Version |\n' + - '| ---------------- | -------- | ----------- | ------- | ------- |\n'; + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''; + + + vulnerabilities.forEach((vulnerability) => { vulnerability.libraries.forEach((library)=>{ const libId = library._links.ref.split('/')[4]; const lib = libraries[libId]; - output += - `| ${vulnerability.cve !== null ? `CVE-${vulnerability.cve}` : `NO-CVE`} `+ - `| ${scaSeverityType(vulnerability.cvss3Score)} ` + - `| ${vulnerability.title} ` + - `| ${lib.name} ` + - `| ${lib.versions[0].version} |\n`; + const severityText = scaSeverityType(vulnerability.cvss3Score); + const severityDisplay = `${getColoredIndicator(severityText, projectUrl)}`; + + output += ` + + + + + + `; + }); }); - output += '\n' + output += '
Severity Vulnerability ID
${severityDisplay} ${vulnerability.cve !== null ? `CVE-${vulnerability.cve}` : `NO-CVE`}
+

${vulnerability.title}

+

${lib.name}: ${lib.versions[0].version}

+
Findings Details +

${vulnerability.overview}

+

Latest Release: ${lib.latestRelease}

+

Latest safe version: ${lib.recommendedVersion}

+

CVSS Score: ${vulnerability.cvss3Score}

+

CVSS Vector: ${vulnerability.cvss3Vector}

+

EPSS Score: ${vulnerability.exploitability.epssScore}

+

EPSS Percentile: ${vulnerability.exploitability.epssPercentile}

+

EPSS Model Version: ${vulnerability.exploitability.epssModelVersion}

+
+
\n'; return output; } -function pipelineResult(scanResult){ - let output = initialScanInfo(); +const { getSourceFilePath: getSourceFilePathService } = require('./service'); + +async function getSourceFilePath(filePath, branch, projectUrl, lineNumber, debug) { + return await getSourceFilePathService(filePath, branch, projectUrl, lineNumber, debug); +} +async function pipelineResult(scanResult, branch, projectUrl, debug){ + let output = initialScanInfo('Pipeline', scanResult, null, projectUrl, branch); + + // Sort results by severity (Very High/Critical first) + const sortedResults = [...scanResult].sort((a, b) => { + // Severity is numeric: 0=Informational, 1=Very Low, 2=Low, 3=Medium, 4=High, 5=Critical + return b.severity - a.severity; // Descending order (highest severity first) + }); + output+= '
\n'+ 'Scan Details\n\n'+ - '| CWE ID | Severity | Issue Type | Source File |\n' + - '| ------ | -------- | ---------- | ----------- |\n'; - scanResult.forEach((result) => { - output += - `| ${result.cwe_id} `+ - `| ${severityType(result.severity)} ` + - `| ${result.issue_type} ` + - `| Line ${result.files.source_file.line}: ${result.files.source_file.file} |\n`; - }); - output += '
\n' + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''+ + ''; + + for (const result of sortedResults) { + const filePath = result.files.source_file.file; + const lineNumber = result.files.source_file.line; + const sourceFileLink = await getSourceFilePath(filePath, branch, projectUrl, lineNumber, debug); + const displayLink = sourceFileLink || `${filePath}:${lineNumber}`; + const severityText = severityType(result.severity); + const severityDisplay = `${getColoredIndicator(severityText, projectUrl)}`; + + output += + ` + + + + `; + } + output += '
Severity CWE ID Source File
Issue Type
Details
${severityDisplay} ${result.cwe_id} [${filePath}:${lineNumber}](${displayLink})
${result.issue_type}
Flaw Details ${result.display_text}
\n'; return output; } function policyResult(scanResult){ - let output = initialScanInfo(); + const projectUrl = process.env.CI_PROJECT_URL || process.env.PROJECT_URL; + + // Sort results by severity (Very High/Critical first) + const sortedResults = [...scanResult].sort((a, b) => { + // Severity is numeric: 0=Informational, 1=Very Low, 2=Low, 3=Medium, 4=High, 5=Critical + return b.static.severity - a.static.severity; // Descending order (highest severity first) + }); + + let output = initialScanInfo('Policy', null, null, projectUrl); output+= '
\n'+ 'Scan Details\n\n'+ '| CWE ID | Severity | Issue Type | Category | Source File |\n' + '| ------ | -------- | ---------- | -------- | ----------- |\n'; - scanResult.forEach((result) => { + sortedResults.forEach((result) => { + const severityText = severityType(result.static.severity); + const severityDisplay = `${getColoredIndicator(severityText, projectUrl)} ${severityText}`; output += `| ${result.static.cwe_id} `+ - `| ${severityType(result.static.severity)} ` + + `| ${severityDisplay} ` + `| ${result.static.issue_type} ` + `| ${result.static.category} ` + `| Line ${result.static.line}: ${result.static.source_file} |\n`; @@ -396,12 +717,12 @@ function policyResult(scanResult){ function iacResult(scanResult){ + const projectUrl = process.env.CI_PROJECT_URL || process.env.PROJECT_URL; + let output = initialScanInfo('IaC', scanResult, null, projectUrl); - let output = initialScanInfo(); - - let IaCVulnerabilities = extractIaCVulnerabilities(scanResult); - let IaCMisconfigurations = extractIaCMisconfigurations(scanResult); - let IaCSecrets = extractIaCSecrets(scanResult); + let IaCVulnerabilities = extractIaCVulnerabilities(scanResult, projectUrl); + let IaCMisconfigurations = extractIaCMisconfigurations(scanResult, projectUrl); + let IaCSecrets = extractIaCSecrets(scanResult, projectUrl); output += IaCVulnerabilities; output += IaCMisconfigurations; @@ -410,7 +731,7 @@ function iacResult(scanResult){ return output; } -function extractIaCVulnerabilities(scanResult){ +function extractIaCVulnerabilities(scanResult, projectUrl = null){ let output = ""; const vulnerabilityData = scanResult?.vulnerabilities?.matches || []; @@ -439,14 +760,15 @@ function extractIaCVulnerabilities(scanResult){ '| Severity | Name | Vulnerability | Installed | Fixed-In | Type | Message |\n' + '| -------- | -------- | ------------- | --------- | --------------| ----------- | -------------- |\n'; formattedVulnerabilities.forEach((result) => { - output += `| ${result.SEVERITY} | ${result.NAME} | ${result.VULNERABILITY} | ${result.INSTALLED} | ${result["FIXED_IN"]} | ${result.TYPE} | ${result.MESSAGE} |\n`; + const severityDisplay = `${getColoredIndicator(result.SEVERITY, projectUrl)}`; + output += `| ${severityDisplay} | ${result.NAME} | ${result.VULNERABILITY} | ${result.INSTALLED} | ${result["FIXED_IN"]} | ${result.TYPE} | ${result.MESSAGE} |\n`; }); output += '\n
\n'; return output; } -function extractIaCMisconfigurations(scanResults) { +function extractIaCMisconfigurations(scanResults, projectUrl = null) { let output = ""; const Misconfigurations = scanResults?.configs; @@ -472,8 +794,9 @@ function extractIaCMisconfigurations(scanResults) { '| SEVERITY | TITLE | ID | PROVIDER | MESSAGE |\n' + '| ------- | -------- | ----- | --------- | -------------- |\n'; formattedData.forEach((result) => { + const severityDisplay = `${getColoredIndicator(result.SEVERITY, projectUrl)}`; output += - `| ${result.SEVERITY} ` + + `| ${severityDisplay} ` + `| ${result.TITLE} ` + `| ${result.ID} ` + `| Line ${result.PROVIDER} `+ @@ -484,7 +807,7 @@ function extractIaCMisconfigurations(scanResults) { return output; } -function extractIaCSecrets(scanResult){ +function extractIaCSecrets(scanResult, projectUrl = null){ let output = ""; const IacSecreteData = scanResult?.secrets || []; @@ -509,7 +832,8 @@ function extractIaCSecrets(scanResult){ '| Severity | SECRET_TYPE | FILE | MESSAGE |\n' + '| -------- | ----------- | ------------- | ----------------- |\n'; formattedIacSecret.forEach((result) => { - output += `| ${result.SEVERITY} | ${result.SECRET_TYPE} | ${result.FILE} | ${result.MESSAGE} |\n`; + const severityDisplay = `${getColoredIndicator(result.SEVERITY, projectUrl)}`; + output += `| ${severityDisplay} | ${result.SECRET_TYPE} | ${result.FILE} | ${result.MESSAGE} |\n`; }); output += '\n\n'; diff --git a/veracode-scans/iac-scan/iac-scan.js b/veracode-scans/iac-scan/iac-scan.js index a2806e5..8cd1eba 100644 --- a/veracode-scans/iac-scan/iac-scan.js +++ b/veracode-scans/iac-scan/iac-scan.js @@ -3,8 +3,9 @@ const path = require('path'); const { exitOnFailure, updateErrorMessage, uploadArtifact } = require('../../utility/utils'); const execa = require('execa'); const displayScanResult = require('../../displayScanResult'); +const { updateCommitStatus } = require('../../utility/service'); -async function iacScan(sourceBranch, breakBuildOnFinding, breakBuildOnError, userErrorMessage, debug) { +async function iacScan(sourceBranch, breakBuildOnFinding, breakBuildOnError, userErrorMessage, debug, commitSha, pipelineName, ciPipelineUrl) { const veracodeDir = path.dirname(require.main.filename); const veracodeCliPath = path.resolve(veracodeDir, 'veracode-cli'); const veracodeExecutable = path.join(veracodeCliPath, 'veracode'); @@ -73,17 +74,36 @@ async function iacScan(sourceBranch, breakBuildOnFinding, breakBuildOnError, use if (jsonOutput?.vulnerabilities?.matches?.length == 0 && !jsonOutput["policy-results"][0].failures) { console.log(tableOutput); console.log(`Veracode IAC scan executed successfully. No Vulnerabilities found !!`); + const description = pipelineName+' no findings'; + const pipelineStatusUpdateFailed = await updateCommitStatus(commitSha, 'success', pipelineName, ciPipelineUrl, description, debug); + if (!pipelineStatusUpdateFailed) { + console.error("IAC/secret Scan status update failed"); + exitOnFailure(breakBuildOnError); + } } else { await uploadArtifact(veracodeArtifactsDir,"IacScan","IacScan.json",JSON.stringify(resultsJSON, null, 2)); console.log(`Vulnerability detected in the repository !!`); console.error(tableOutput); await displayScanResult(resultsJSON); + const description = pipelineName+' findings'; + const pipelineStatusUpdateFailed = await updateCommitStatus(commitSha, 'failed', pipelineName, ciPipelineUrl, description, debug); + if (!pipelineStatusUpdateFailed) { + console.error("IAC/secret Scan status update failed"); + exitOnFailure(breakBuildOnError); + } + exitOnFailure(breakBuildOnError); } } catch (error) { console.log(breakBuildOnError) error = updateErrorMessage(breakBuildOnError, userErrorMessage, error.message); console.error(`Error occurred during IAC scan: ${error}`); + const description = pipelineName+' failed'; + const pipelineStatusUpdateFailed = await updateCommitStatus(commitSha, 'failed', pipelineName, ciPipelineUrl, description, debug); + if (!pipelineStatusUpdateFailed) { + console.error("IAC/secret Scan status update failed"); + exitOnFailure(breakBuildOnError); + } exitOnFailure(breakBuildOnError); } } diff --git a/veracode-scans/pipeline-scan/pipeline.js b/veracode-scans/pipeline-scan/pipeline.js index ad673ff..50979ab 100644 --- a/veracode-scans/pipeline-scan/pipeline.js +++ b/veracode-scans/pipeline-scan/pipeline.js @@ -4,33 +4,34 @@ const { SCAN, STATUS } = require('../../config/constants'); const { appConfig } = require('../../config'); const { exitOnFailure, updateErrorMessage, uploadArtifact } = require('../../utility/utils'); const execa = require('execa'); -const { getApplicationByName, getApplicationFindings, isProfileExists, veracodePolicyVerification, validateCredential } = require('../../utility/common'); +const { getApplicationByName, getApplicationFindings, isProfileExists, veracodePolicyVerification, validateCredential, getPolicyByName } = require('../../utility/common'); +const { updateCommitStatus } = require('../../utility/service'); const pipelineScanIssue = require('../../veracode-issues/pipelineScanIssue'); const displayScanResult = require('../../displayScanResult'); const { execSync } = require('child_process'); -async function pipelineScan(apiId, apiKey, appProfileName, filterMitigatedFlaws, breakBuildOnFinding, breakBuildOnError, userErrorMessage, policyName, breakBuildOnInvalidPolicy, createIssue, debug) { +async function pipelineScan(apiId, apiKey, appProfileName, filterMitigatedFlaws, breakBuildOnFinding, breakBuildOnError, userErrorMessage, policyName, breakBuildOnInvalidPolicy, createIssue, debug, commitSha, pipelineName, ciPipelineUrl) { const veracodeArtifactsDir = path.join(__dirname, '../../veracode-artifacts'); try { const isCredentialValid = await validateCredential(apiId, apiKey); if (!isCredentialValid) { - await displayScanResult([], "Veracode credentials are invalid or expired."); + await displayScanResult([], "Veracode credentials are invalid or expired.", debug); exitOnFailure(breakBuildOnError); return; } const invalidPolicy = await veracodePolicyVerification(apiId, apiKey, policyName, breakBuildOnInvalidPolicy); if (invalidPolicy) { - await displayScanResult([], "Invalid Veracode Policy name."); + await displayScanResult([], "Invalid Veracode Policy name.", debug); exitOnFailure(breakBuildOnInvalidPolicy); } const artifacts = await fs.promises.readdir(veracodeArtifactsDir); const scanResults = await Promise.all( artifacts.map((artifact) => - executePipelineScan(veracodeArtifactsDir, artifact, apiId, apiKey, debug) + executePipelineScan(veracodeArtifactsDir, artifact, apiId, apiKey, debug, policyName) ) ); @@ -46,9 +47,15 @@ async function pipelineScan(apiId, apiKey, appProfileName, filterMitigatedFlaws, const artifactName = failedScan.artifact.replace(/\.[^/.]+$/, ''); const simplifiedFileName = (resultFileName) => resultFileName.includes('pipeline') ? 'pipeline.json' : 'filtered_results.json'; + - // Loop through each result file in the failed scan + // Loop through each result file in the failed scan only if the result file is *filtered_results.json for (const resultFileName of failedScan.results) { + // Only process files that include "filtered_results.json" + if (!resultFileName.includes('filtered_results.json')) { + console.log(`Skipping ${resultFileName} - not a filtered_results.json file`); + continue; + } try { // Check if the result file exists if (!fs.existsSync(resultFileName)) { @@ -59,7 +66,6 @@ async function pipelineScan(apiId, apiKey, appProfileName, filterMitigatedFlaws, // Read and parse the result file let rawData = fs.readFileSync(resultFileName); let resultsJSON = JSON.parse(rawData.toString()); - // Awaiting async operations for upload and flaw mitigation await uploadArtifact(veracodeArtifactsDir, artifactName, simplifiedFileName(resultFileName), JSON.stringify(resultsJSON, null, 2)); const isFilterMitigatedFlaws = filterMitigatedFlaws === 'true' ? true : filterMitigatedFlaws === 'false' ? false : filterMitigatedFlaws; @@ -96,7 +102,7 @@ async function pipelineScan(apiId, apiKey, appProfileName, filterMitigatedFlaws, }; if (filteredResult.findings.length > 0) { - await displayScanResult(filteredResult.findings); + await displayScanResult(filteredResult.findings, "", debug); if (createIssue) { await pipelineScanIssue(filteredResult); @@ -104,15 +110,29 @@ async function pipelineScan(apiId, apiKey, appProfileName, filterMitigatedFlaws, pipelineResult.result = JSON.stringify(filteredResult, null, 2); pipelineResult.status = STATUS.Findings; - pipelineResult.message = 'Vulnerability detected in the repository'; + pipelineResult.message = 'Flaws detected in the repository'; + const description = pipelineName+' findings'; + const pipelineStatusUpdateFailed = await updateCommitStatus(commitSha, 'failed', pipelineName, ciPipelineUrl, description, debug); + if (!pipelineStatusUpdateFailed) { + console.error("Pipeline status update failed"); + exitOnFailure(breakBuildOnError); + return pipelineResult; + } exitOnFailure(breakBuildOnFinding); return pipelineResult; } else { - await displayScanResult([]); + await displayScanResult([], "", debug); console.log('No pipeline findings, exiting and updating the GitLab check status to success'); pipelineResult.message = 'No pipeline findings.'; pipelineResult.status = STATUS.Success; + const description = pipelineName+' - no findings'; + const pipelineStatusUpdateStart = await updateCommitStatus(commitSha, 'success', pipelineName, ciPipelineUrl, description, debug); + if (!pipelineStatusUpdateStart) { + console.error("Pipeline status update failed"); + exitOnFailure(breakBuildOnError); + return; + } return pipelineResult; } } catch (error) { @@ -122,7 +142,101 @@ async function pipelineScan(apiId, apiKey, appProfileName, filterMitigatedFlaws, } } -async function executePipelineScan(veracodeArtifactsDir, artifactName, apiId, apiKey, debug) { +async function getPolicyParameter(apiId, apiKey, policyName, debug) { + if (!policyName || policyName === '') { + return ''; + } + + try { + // Determine region for logging + if (debug === "true") { + if (apiId.startsWith('vera01ei-')) { + console.log('Region: EU'); + } else { + console.log('Region: US'); + } + } + + // Get policy details (getPolicyByName handles region detection internally) + const responseData = await getPolicyByName(apiId, apiKey, policyName); + + if (debug === "true") { + console.log('---- DEBUG OUTPUT START ----'); + console.log('---- getPolicyParameter() - find the policy via API ----'); + console.log('---- Response Data ----'); + console.log(JSON.stringify(responseData)); + console.log('---- DEBUG OUTPUT END ----'); + } + + if (!responseData || responseData.page.total_elements === 0) { + if (debug === "true") { + console.log('NO POLICY FOUND - NO POLICY WILL BE USED TO RATE FINDINGS'); + } + return ''; + } + + const policyVersion = responseData._embedded.policy_versions[0]; + if (!policyVersion) { + if (debug === "true") { + console.log('NO POLICY FOUND - NO POLICY WILL BE USED TO RATE FINDINGS'); + } + return ''; + } + + if (policyVersion.type === 'BUILTIN') { + if (debug === "true") { + console.log('Built-in Policy is required'); + console.log(`Setting policy to ${policyName}`); + } + return ` --policy_name "${policyName}"`; + } else if (policyVersion.type === 'CUSTOMER') { + if (debug === "true") { + console.log('Custom Policy is required'); + console.log(`Downloading custom policy file and setting policy to ${policyName}`); + } + + // Download custom policy file + const pipelineScanJarPath = path.join(__dirname, 'pipeline-scan.jar'); + const policyCommand = `java -jar ${pipelineScanJarPath} -vid ${apiId} -vkey ${apiKey} --request_policy "${policyName}"`; + + try { + execSync(policyCommand, { stdio: 'inherit', encoding: 'utf-8' }); + + if (debug === "true") { + console.log('---- DEBUG OUTPUT START ----'); + console.log('---- getPolicyParameter() - custom policy download ----'); + console.log(`---- Policy Download command: ${policyCommand}`); + console.log('---- DEBUG OUTPUT END ----'); + } + + const policyFileName = policyName.replace(/ /gi, "_"); + if (debug === "true") { + console.log(`Policy File Name: ${policyFileName}`); + } + return ` --policy_file ${policyFileName}.json`; + } catch (error) { + console.error(`Error downloading custom policy: ${error.message}`); + return ''; + } + } else { + if (debug === "true") { + console.log('Something went wrong with fetching the correct policy'); + } + return ''; + } + } catch (error) { + if (debug === "true") { + console.log('---- DEBUG OUTPUT START ----'); + console.log('---- getPolicyParameter() - find policy via API catch error ----'); + console.log('---- Error:', error.message); + console.log('---- DEBUG OUTPUT END ----'); + } + console.error(`Error getting policy parameter: ${error.message}`); + return ''; + } +} + +async function executePipelineScan(veracodeArtifactsDir, artifactName, apiId, apiKey, debug, policyName) { const pipelineResultFileName = `${artifactName}-` + appConfig().pipelineScanFile; const filteredResultFileName = `${artifactName}-` + appConfig().filteredScanFile; @@ -130,6 +244,13 @@ async function executePipelineScan(veracodeArtifactsDir, artifactName, apiId, ap const artifactFilePath = path.join(veracodeArtifactsDir, artifactName); const pipelineScanJarPath = path.join(__dirname, 'pipeline-scan.jar'); let pipelineScanCommand = `java -jar ${pipelineScanJarPath} -vid ${apiId} -vkey ${apiKey} -f ${artifactFilePath} -jf ${pipelineResultFileName} -fjf ${filteredResultFileName}`; + + // Add policy parameter if policy name is provided + if (policyName && policyName !== '') { + const policyParameter = await getPolicyParameter(apiId, apiKey, policyName, debug); + pipelineScanCommand += policyParameter; + } + if(debug === "true") pipelineScanCommand += ' -V true'; execSync(pipelineScanCommand, { stdio: 'inherit' }); diff --git a/veracode-scans/sca-scan/sca-scan.js b/veracode-scans/sca-scan/sca-scan.js index 2948e99..f8f4956 100644 --- a/veracode-scans/sca-scan/sca-scan.js +++ b/veracode-scans/sca-scan/sca-scan.js @@ -3,8 +3,9 @@ const path = require('path'); const { attacheResult, exitOnFailure, updateErrorMessage } = require('../../utility/utils'); const scaScanIssue = require('../../veracode-issues/scaScanIssue'); const displayScanResult = require('../../displayScanResult'); +const { updateCommitStatus } = require('../../utility/service'); -async function scaScan(clone_url, scaAgenToken, scaUrl, sourceBranch, breakBuildOnFinding, breakBuildOnError, userErrorMessage, createIssue, debug) { +async function scaScan(clone_url, scaAgenToken, scaUrl, sourceBranch, breakBuildOnFinding, breakBuildOnError, userErrorMessage, createIssue, debug, commitSha, pipelineName, ciPipelineUrl) { try { let command = `curl -sSL https://download.sourceclear.com/ci.sh | sh -s -- scan --url ${clone_url} --ref ${sourceBranch} --recursive --allow-dirty`; if(debug === "true") @@ -22,6 +23,13 @@ async function scaScan(clone_url, scaAgenToken, scaUrl, sourceBranch, breakBuild await displayScanResult([]); console.log(`Veracode SCA scan executed successfully.`); console.log(output); + const description = pipelineName+' no findings'; + const pipelineStatusUpdateFailed = await updateCommitStatus(commitSha, 'success', pipelineName, ciPipelineUrl, description, debug); + if (!pipelineStatusUpdateFailed) { + console.error("SCA Scan status update failed"); + exitOnFailure(breakBuildOnError); + } + } else { await displayScanResult(parsedOutput.records); if (createIssue) { @@ -29,11 +37,24 @@ async function scaScan(clone_url, scaAgenToken, scaUrl, sourceBranch, breakBuild } console.log(`Veracode SCA scan executed successfully.`); console.log(output); + const description = pipelineName+' findings'; + const pipelineStatusUpdateFailed = await updateCommitStatus(commitSha, 'failed', pipelineName, ciPipelineUrl, description, debug); + if (!pipelineStatusUpdateFailed) { + console.error("SCA Scan status update failed"); + exitOnFailure(breakBuildOnError); + } + exitOnFailure(breakBuildOnFinding); } } catch (error) { error = updateErrorMessage(breakBuildOnError, userErrorMessage, error.message); console.error(`Error occurred during SCA scan: ${error}`); + const description = pipelineName+' failed'; + const pipelineStatusUpdateFailed = await updateCommitStatus(commitSha, 'failed', pipelineName, ciPipelineUrl, description, debug); + if (!pipelineStatusUpdateFailed) { + console.error("SCA Scan status update failed"); + exitOnFailure(breakBuildOnError); + } exitOnFailure(breakBuildOnError); } } diff --git a/veracodeScan.js b/veracodeScan.js index 615fe36..3c0f8ce 100644 --- a/veracodeScan.js +++ b/veracodeScan.js @@ -37,9 +37,13 @@ async function veracodeScan() { const repoUrl = process.env.PROJECT_URL; const debug = process.env.DEBUG; + const commitSha = process.env.COMMIT_SHA; + const pipelineName = process.env.PIPELINE_NAME; + const ciPipelineUrl = process.env.CI_PIPELINE_URL; + if (executePipeline) { console.log(`Executing pipeline scan on ${projectName} repo for ${sourceBranch} branch`); - await pipelineScan(apiId, appKey, appProfileName, filterMitigatedFlaws, breakBuildOnFinding, breakBuildOnError, userErrorMessage, policyName, breakBuildOnInvalidPolicy, createIssue, debug); + await pipelineScan(apiId, appKey, appProfileName, filterMitigatedFlaws, breakBuildOnFinding, breakBuildOnError, userErrorMessage, policyName, breakBuildOnInvalidPolicy, createIssue, debug, commitSha, pipelineName, ciPipelineUrl); } if (executeSandbox) { console.log(`Executing sandbox scan on ${projectName} repo for ${sourceBranch} branch`); @@ -55,11 +59,11 @@ async function veracodeScan() { } if (executeSca) { console.log(`Executing sca scan on ${projectName} repo for ${sourceBranch} branch`); - await scaScan(sourceRepoCloneUrl, scaAgenToken, scaUrl, sourceBranch, breakBuildOnFinding, breakBuildOnError, userErrorMessage, createIssue, debug); + await scaScan(sourceRepoCloneUrl, scaAgenToken, scaUrl, sourceBranch, breakBuildOnFinding, breakBuildOnError, userErrorMessage, createIssue, debug, commitSha, pipelineName, ciPipelineUrl); } if (executeIac) { console.log(`Executing iac scan on ${projectName} repo for ${sourceBranch} branch`); - await iacScan(sourceBranch, breakBuildOnFinding, breakBuildOnError, userErrorMessage, debug) + await iacScan(sourceBranch, breakBuildOnFinding, breakBuildOnError, userErrorMessage, debug, commitSha, pipelineName, ciPipelineUrl) } } veracodeScan();