From 157d43fd0c138d91fe3ca37bb61faec9372fe6d8 Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Fri, 12 Apr 2024 10:50:15 +0200 Subject: [PATCH 01/35] SCANNPM-1 Cleanup existing sourcecode and prepare bootstrap foundation (#108) --- .gitignore | 3 + jest.config.js | 2 +- package-lock.json | 2288 ++++++++--------- package.json | 22 +- scripts/ci-analysis.js | 2 +- scripts/file-header.ts | 2 +- scripts/fix-comments.js | 43 + src/bin/sonar-scanner | 21 +- src/config.js | 201 -- src/constants.ts | 35 + src/index.js | 64 - src/{utils/index.js => index.ts} | 7 +- src/logging.ts | 71 + src/{utils/platform.js => scan.ts} | 42 +- src/sonar-scanner-executable.js | 130 - src/sonar-scanner-params.js | 196 -- src/{utils/paths.js => types.ts} | 36 +- .../fake_project_for_integration/src/index.js | 2 +- test/integration/scanner.test.js | 2 +- test/unit/config.test.js | 2 +- .../index.js | 2 +- test/unit/fixtures/webserver/server.js | 2 +- test/unit/index.test.js | 2 +- test/unit/sonar-scanner-executable.test.js | 2 +- test/unit/utils.test.js | 2 +- tools/orchestrator/scripts/full.js | 2 +- tools/orchestrator/scripts/issues.js | 2 +- tools/orchestrator/scripts/sonarqube.js | 2 +- tools/orchestrator/src/download.ts | 2 +- tools/orchestrator/src/index.ts | 2 +- tools/orchestrator/src/sonarqube.ts | 2 +- tsconfig.json | 111 + 32 files changed, 1476 insertions(+), 1828 deletions(-) create mode 100644 scripts/fix-comments.js delete mode 100644 src/config.js create mode 100644 src/constants.ts delete mode 100644 src/index.js rename src/{utils/index.js => index.ts} (82%) create mode 100644 src/logging.ts rename src/{utils/platform.js => scan.ts} (55%) delete mode 100644 src/sonar-scanner-executable.js delete mode 100644 src/sonar-scanner-params.js rename src/{utils/paths.js => types.ts} (57%) create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index bd285f4c..d984fc9a 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,6 @@ xunit.xml # MacOS .DS_Store + +# TS build artifacts +build/ diff --git a/jest.config.js b/jest.config.js index 0fefcbd7..4d53e1d4 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/package-lock.json b/package-lock.json index f189a2e6..2c41f39a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,19 +10,24 @@ "license": "LGPL-3.0-only", "dependencies": { "adm-zip": "0.5.12", - "fancy-log": "2.0.0", - "https-proxy-agent": "7.0.4", + "axios": "1.6.8", + "fs-extra": "11.2.0", + "hpagent": "1.2.0", "jest-sonar-reporter": "2.0.0", - "mkdirp": "3.0.1", - "node-downloader-helper": "2.1.9", - "progress": "2.0.3", - "slugify": "1.6.6" + "proxy-from-env": "1.1.0", + "semver": "7.6.0", + "tar-stream": "3.1.7" }, "bin": { "sonar-scanner": "src/bin/sonar-scanner" }, "devDependencies": { + "@types/adm-zip": "^0.5.5", + "@types/fs-extra": "^11.0.4", "@types/jest": "29.5.12", + "@types/proxy-from-env": "^1.0.4", + "@types/semver": "^7.5.8", + "@types/tar-stream": "^3.1.3", "@typescript-eslint/parser": "7.4.0", "chai": "4.4.1", "eslint": "8.57.0", @@ -36,23 +41,21 @@ "typescript": "5.4.3" }, "engines": { - "node": ">= 16" + "node": ">= 18" } }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/@ampproject/remapping": { "version": "2.3.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -62,113 +65,39 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "version": "7.24.2", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/compat-data": { - "version": "7.23.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", + "version": "7.24.4", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.24.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/core/-/core-7.24.0.tgz", - "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "version": "7.24.4", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.4", "@babel/helper-compilation-targets": "^7.23.6", "@babel/helper-module-transforms": "^7.23.3", - "@babel/helpers": "^7.24.0", - "@babel/parser": "^7.24.0", + "@babel/helpers": "^7.24.4", + "@babel/parser": "^7.24.4", "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", + "@babel/traverse": "^7.24.1", "@babel/types": "^7.24.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -178,26 +107,28 @@ }, "engines": { "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.23.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "version": "7.24.4", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.23.6", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { @@ -206,9 +137,8 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.23.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-validator-option": "^7.23.5", @@ -222,27 +152,24 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/helper-environment-visitor": { "version": "7.22.20", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { "version": "7.23.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.22.15", "@babel/types": "^7.23.0" @@ -253,9 +180,8 @@ }, "node_modules/@babel/helper-hoist-variables": { "version": "7.22.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" }, @@ -264,12 +190,11 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.22.15", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "version": "7.24.3", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.15" + "@babel/types": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -277,9 +202,8 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.23.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -296,18 +220,16 @@ }, "node_modules/@babel/helper-plugin-utils": { "version": "7.24.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz", - "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { "version": "7.22.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" }, @@ -317,9 +239,8 @@ }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.22.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" }, @@ -328,40 +249,36 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.1", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.22.20", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { "version": "7.23.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.24.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/helpers/-/helpers-7.24.0.tgz", - "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==", + "version": "7.24.4", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.0", + "@babel/traverse": "^7.24.1", "@babel/types": "^7.24.0" }, "engines": { @@ -369,14 +286,14 @@ } }, "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "version": "7.24.2", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -384,9 +301,8 @@ }, "node_modules/@babel/highlight/node_modules/ansi-styles": { "version": "3.2.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -396,9 +312,8 @@ }, "node_modules/@babel/highlight/node_modules/chalk": { "version": "2.4.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -410,42 +325,37 @@ }, "node_modules/@babel/highlight/node_modules/color-convert": { "version": "1.9.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "1.1.3" } }, "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@babel/highlight/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/@babel/highlight/node_modules/supports-color": { "version": "5.5.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -454,10 +364,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/parser/-/parser-7.24.0.tgz", - "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "version": "7.24.4", "dev": true, + "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -467,9 +376,8 @@ }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -479,9 +387,8 @@ }, "node_modules/@babel/plugin-syntax-bigint": { "version": "7.8.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -491,9 +398,8 @@ }, "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" }, @@ -503,9 +409,8 @@ }, "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -515,9 +420,8 @@ }, "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -526,12 +430,11 @@ } }, "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.23.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.23.3.tgz", - "integrity": "sha512-EB2MELswq55OHUoRZLGg/zC7QWUKfNLpE57m/S2yr1uEneIgsTgrSzXP3NXEsMkVn76OlaVVnzN+ugObuYGwhg==", + "version": "7.24.1", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -542,9 +445,8 @@ }, "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -554,9 +456,8 @@ }, "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -566,9 +467,8 @@ }, "node_modules/@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, @@ -578,9 +478,8 @@ }, "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -590,9 +489,8 @@ }, "node_modules/@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -602,9 +500,8 @@ }, "node_modules/@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, @@ -614,9 +511,8 @@ }, "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" }, @@ -628,12 +524,11 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.23.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", - "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", + "version": "7.24.1", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-plugin-utils": "^7.24.0" }, "engines": { "node": ">=6.9.0" @@ -644,9 +539,8 @@ }, "node_modules/@babel/template": { "version": "7.24.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/template/-/template-7.24.0.tgz", - "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.23.5", "@babel/parser": "^7.24.0", @@ -657,18 +551,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.24.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/traverse/-/traverse-7.24.0.tgz", - "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==", + "version": "7.24.1", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.23.5", - "@babel/generator": "^7.23.6", + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.24.0", + "@babel/parser": "^7.24.1", "@babel/types": "^7.24.0", "debug": "^4.3.1", "globals": "^11.1.0" @@ -679,18 +572,16 @@ }, "node_modules/@babel/traverse/node_modules/globals": { "version": "11.12.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/@babel/types": { "version": "7.24.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -702,15 +593,13 @@ }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", "dev": true, + "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -723,18 +612,16 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.10.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, "node_modules/@eslint/eslintrc": { "version": "2.1.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -748,22 +635,43 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/@eslint/js": { "version": "8.57.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -773,26 +681,47 @@ "node": ">=10.10.0" } }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "version": "2.0.3", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -807,33 +736,35 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -841,25 +772,29 @@ }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -867,13 +802,15 @@ }, "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", "dev": true, + "license": "ISC", "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -887,18 +824,16 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { "version": "1.0.10", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, + "license": "MIT", "dependencies": { "sprintf-js": "~1.0.2" } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -909,9 +844,8 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { "version": "3.14.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -922,9 +856,8 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -934,21 +867,22 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -958,27 +892,24 @@ }, "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { "version": "5.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jest/console": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -993,9 +924,8 @@ }, "node_modules/@jest/core": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/core/-/core-29.7.0.tgz", - "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", "@jest/reporters": "^29.7.0", @@ -1040,9 +970,8 @@ }, "node_modules/@jest/environment": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", @@ -1055,9 +984,8 @@ }, "node_modules/@jest/expect": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^29.7.0", "jest-snapshot": "^29.7.0" @@ -1068,9 +996,8 @@ }, "node_modules/@jest/expect-utils": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, + "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3" }, @@ -1080,9 +1007,8 @@ }, "node_modules/@jest/fake-timers": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", @@ -1097,9 +1023,8 @@ }, "node_modules/@jest/globals": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/globals/-/globals-29.7.0.tgz", - "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -1112,9 +1037,8 @@ }, "node_modules/@jest/reporters": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/reporters/-/reporters-29.7.0.tgz", - "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, + "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", "@jest/console": "^29.7.0", @@ -1155,9 +1079,8 @@ }, "node_modules/@jest/schemas": { "version": "29.6.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", "dev": true, + "license": "MIT", "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -1167,9 +1090,8 @@ }, "node_modules/@jest/source-map": { "version": "29.6.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/source-map/-/source-map-29.6.3.tgz", - "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "^0.3.18", "callsites": "^3.0.0", @@ -1181,9 +1103,8 @@ }, "node_modules/@jest/test-result": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/test-result/-/test-result-29.7.0.tgz", - "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", "@jest/types": "^29.6.3", @@ -1196,9 +1117,8 @@ }, "node_modules/@jest/test-sequencer": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", - "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", "graceful-fs": "^4.2.9", @@ -1211,9 +1131,8 @@ }, "node_modules/@jest/transform": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/transform/-/transform-29.7.0.tgz", - "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@jest/types": "^29.6.3", @@ -1237,9 +1156,8 @@ }, "node_modules/@jest/types": { "version": "29.6.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -1254,9 +1172,8 @@ }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -1268,33 +1185,29 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { "version": "1.2.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1302,9 +1215,8 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1315,18 +1227,16 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1337,9 +1247,8 @@ }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" @@ -1347,33 +1256,29 @@ }, "node_modules/@sinclair/typebox": { "version": "0.27.8", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@sinonjs/commons": { "version": "3.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { "version": "10.3.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "node_modules/@sinonjs/samsam": { "version": "8.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^2.0.0", "lodash.get": "^4.4.2", @@ -1382,24 +1287,29 @@ }, "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/text-encoding": { "version": "0.7.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true + "dev": true, + "license": "(Unlicense OR Apache-2.0)" + }, + "node_modules/@types/adm-zip": { + "version": "0.5.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/babel__core": { "version": "7.20.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", @@ -1410,18 +1320,16 @@ }, "node_modules/@types/babel__generator": { "version": "7.6.8", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/babel__generator/-/babel__generator-7.6.8.tgz", - "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.0.0" } }, "node_modules/@types/babel__template": { "version": "7.4.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" @@ -1429,91 +1337,118 @@ }, "node_modules/@types/babel__traverse": { "version": "7.20.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/babel__traverse/-/babel__traverse-7.20.5.tgz", - "integrity": "sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.20.7" } }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", - "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" } }, "node_modules/@types/istanbul-reports": { "version": "3.0.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jest": { "version": "29.5.12", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", "dev": true, + "license": "MIT", "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" } }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { - "version": "20.11.26", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/node/-/node-20.11.26.tgz", - "integrity": "sha512-YwOMmyhNnAWijOBQweOJnQPl068Oqd4K3OFbTc6AHJwzweUwwWG3GIFY74OKks2PJUDkQPeddOQES9mLn1CTEQ==", + "version": "20.12.7", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/proxy-from-env": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "dev": true, + "license": "MIT" + }, "node_modules/@types/stack-utils": { "version": "2.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tar-stream": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/yargs": { "version": "17.0.32", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { "version": "21.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@typescript-eslint/parser": { "version": "7.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/parser/-/parser-7.4.0.tgz", - "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "7.4.0", "@typescript-eslint/types": "7.4.0", @@ -1524,6 +1459,10 @@ "engines": { "node": "^18.18.0 || >=20.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, "peerDependencies": { "eslint": "^8.56.0" }, @@ -1535,31 +1474,36 @@ }, "node_modules/@typescript-eslint/scope-manager": { "version": "7.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz", - "integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "7.4.0", "@typescript-eslint/visitor-keys": "7.4.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/types": { "version": "7.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/types/-/types-7.4.0.tgz", - "integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@typescript-eslint/typescript-estree": { "version": "7.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz", - "integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "7.4.0", "@typescript-eslint/visitor-keys": "7.4.0", @@ -1573,57 +1517,41 @@ "engines": { "node": "^18.18.0 || >=20.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "7.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz", - "integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "7.4.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/acorn": { "version": "8.11.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -1633,91 +1561,85 @@ }, "node_modules/acorn-jsx": { "version": "5.3.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/adm-zip": { "version": "0.5.12", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/adm-zip/-/adm-zip-0.5.12.tgz", - "integrity": "sha512-6TVU49mK6KZb4qG6xWaaM4C7sA/sgUMLy/JYMOzkcp3BvVLpW0fXDFQiIzAuxFCt/2+xD7fNIiPFAoLZPhVNLQ==", + "license": "MIT", "engines": { "node": ">=6.0" } }, - "node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "6.12.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, "node_modules/ansi-escapes": { "version": "4.3.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-escapes/node_modules/type-fest": { "version": "0.21.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-styles": { "version": "4.3.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1728,33 +1650,46 @@ }, "node_modules/argparse": { "version": "2.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/array-union": { "version": "2.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/assertion-error": { "version": "1.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.6.8", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.6.6", + "license": "Apache-2.0" + }, "node_modules/babel-jest": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/babel-jest/-/babel-jest-29.7.0.tgz", - "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/transform": "^29.7.0", "@types/babel__core": "^7.1.14", @@ -1773,9 +1708,8 @@ }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -1789,9 +1723,8 @@ }, "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { "version": "5.2.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.12.3", "@babel/parser": "^7.14.7", @@ -1805,18 +1738,16 @@ }, "node_modules/babel-plugin-istanbul/node_modules/semver": { "version": "6.3.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/babel-plugin-jest-hoist": { "version": "29.6.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", - "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.3.3", "@babel/types": "^7.3.3", @@ -1829,9 +1760,8 @@ }, "node_modules/babel-preset-current-node-syntax": { "version": "1.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/plugin-syntax-async-generators": "^7.8.4", "@babel/plugin-syntax-bigint": "^7.8.3", @@ -1852,9 +1782,8 @@ }, "node_modules/babel-preset-jest": { "version": "29.6.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", - "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, + "license": "MIT", "dependencies": { "babel-plugin-jest-hoist": "^29.6.3", "babel-preset-current-node-syntax": "^1.0.0" @@ -1868,25 +1797,26 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.2.2", + "license": "Apache-2.0", + "optional": true }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "2.0.1", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { "version": "3.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.0.1" }, @@ -1896,9 +1826,22 @@ }, "node_modules/browserslist": { "version": "4.23.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001587", "electron-to-chromium": "^1.4.668", @@ -1914,48 +1857,56 @@ }, "node_modules/bser": { "version": "2.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "node-int64": "^0.4.0" } }, "node_modules/buffer-from": { "version": "1.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/callsites": { "version": "3.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/camelcase": { "version": "5.3.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001597", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", - "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", - "dev": true + "version": "1.0.30001609", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" }, "node_modules/chai": { "version": "4.4.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", @@ -1971,31 +1922,31 @@ }, "node_modules/chalk": { "version": "4.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/char-regex": { "version": "1.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/check-error": { "version": "1.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.2" }, @@ -2005,24 +1956,27 @@ }, "node_modules/ci-info": { "version": "3.9.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/cjs-module-lexer": { "version": "1.2.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", - "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -2034,9 +1988,8 @@ }, "node_modules/co": { "version": "4.6.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, + "license": "MIT", "engines": { "iojs": ">= 1.0.0", "node": ">= 0.12.0" @@ -2044,15 +1997,13 @@ }, "node_modules/collect-v8-coverage": { "version": "1.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", - "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2062,35 +2013,33 @@ }, "node_modules/color-name": { "version": "1.1.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "bin": { - "color-support": "bin.js" + "node_modules/combined-stream": { + "version": "1.0.8", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/create-jest": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/create-jest/-/create-jest-29.7.0.tgz", - "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -2109,9 +2058,8 @@ }, "node_modules/cross-spawn": { "version": "7.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2123,8 +2071,8 @@ }, "node_modules/debug": { "version": "4.3.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -2138,10 +2086,9 @@ } }, "node_modules/dedent": { - "version": "1.5.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/dedent/-/dedent-1.5.1.tgz", - "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "version": "1.5.3", "dev": true, + "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, @@ -2153,9 +2100,8 @@ }, "node_modules/deep-eql": { "version": "4.1.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", "dev": true, + "license": "MIT", "dependencies": { "type-detect": "^4.0.0" }, @@ -2165,51 +2111,52 @@ }, "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/deepmerge": { "version": "4.3.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-newline": { "version": "3.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/diff": { "version": "5.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "node_modules/diff-sequences": { "version": "29.6.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/dir-glob": { "version": "3.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -2219,9 +2166,8 @@ }, "node_modules/doctrine": { "version": "3.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -2231,63 +2177,61 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.4.701", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", - "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==", - "dev": true + "version": "1.4.735", + "dev": true, + "license": "ISC" }, "node_modules/emittery": { "version": "0.13.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" } }, "node_modules/emoji-regex": { "version": "8.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/error-ex": { "version": "1.3.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/escalade": { "version": "3.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/escape-string-regexp": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/eslint": { "version": "8.57.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -2333,13 +2277,15 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-plugin-notice": { "version": "0.9.10", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint-plugin-notice/-/eslint-plugin-notice-0.9.10.tgz", - "integrity": "sha512-rF79EuqdJKu9hhTmwUkNeSvLmmq03m/NXq/NHwUENHbdJ0wtoyOjxZBhW4QCug8v5xYE6cGe3AWkGqSIe9KUbQ==", "dev": true, + "license": "MIT", "dependencies": { "find-root": "^1.1.0", "lodash": "^4.17.15", @@ -2351,31 +2297,54 @@ }, "node_modules/eslint-scope": { "version": "7.2.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/espree": { "version": "9.6.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -2383,13 +2352,15 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/esprima": { "version": "4.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, + "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -2400,9 +2371,8 @@ }, "node_modules/esquery": { "version": "1.5.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -2412,9 +2382,8 @@ }, "node_modules/esrecurse": { "version": "4.3.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -2424,27 +2393,24 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/execa": { "version": "5.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -2458,12 +2424,13 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, "node_modules/exit": { "version": "0.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/exit/-/exit-0.1.2.tgz", - "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true, "engines": { "node": ">= 0.8.0" @@ -2471,9 +2438,8 @@ }, "node_modules/expect": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -2485,28 +2451,19 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/fancy-log": { - "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fancy-log/-/fancy-log-2.0.0.tgz", - "integrity": "sha512-9CzxZbACXMUXW13tS0tI8XsGGmxWzO2DmYrGuBJOJ8k8q2K7hwfJA5qHjuPPe8wtsco33YR9wc+Rlr5wYFvhSA==", - "dependencies": { - "color-support": "^1.1.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.3.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2520,9 +2477,8 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -2532,39 +2488,34 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fastq": { "version": "1.17.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fb-watchman": { "version": "2.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "bser": "2.1.1" } }, "node_modules/file-entry-cache": { "version": "6.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -2574,9 +2525,8 @@ }, "node_modules/fill-range": { "version": "7.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2586,28 +2536,28 @@ }, "node_modules/find-root": { "version": "1.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/find-up": { "version": "5.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat-cache": { "version": "3.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -2619,120 +2569,151 @@ }, "node_modules/flat-cache/node_modules/rimraf": { "version": "3.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/flatted": { "version": "3.3.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } }, "node_modules/foreground-child": { "version": "3.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/foreground-child/-/foreground-child-3.1.1.tgz", - "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", "dev": true, + "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^4.0.1" }, "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/foreground-child/node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "node_modules/form-data": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "node_modules/fs-extra": { + "version": "11.2.0", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=14.14" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/gensync": { "version": "1.0.0-beta.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/get-caller-file": { "version": "2.0.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-func-name": { "version": "2.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, "node_modules/get-package-type": { "version": "0.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.0.0" } }, "node_modules/get-stream": { "version": "6.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/glob": { "version": "7.2.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2743,13 +2724,15 @@ }, "engines": { "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob-parent": { "version": "6.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -2757,23 +2740,44 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/globals": { "version": "13.24.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globby": { "version": "11.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -2784,34 +2788,32 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has-flag": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/hasown": { "version": "2.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -2819,72 +2821,67 @@ "node": ">= 0.4" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/https-proxy-agent": { - "version": "7.0.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", - "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, + "node_modules/hpagent": { + "version": "1.2.0", + "license": "MIT", "engines": { - "node": ">= 14" + "node": ">=14" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, "node_modules/human-signals": { "version": "2.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, "node_modules/husky": { "version": "9.0.11", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/husky/-/husky-9.0.11.tgz", - "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", "dev": true, + "license": "MIT", "bin": { "husky": "bin.mjs" }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" } }, "node_modules/ignore": { "version": "5.3.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/import-fresh": { "version": "3.3.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/import-local": { "version": "3.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/import-local/-/import-local-3.1.0.tgz", - "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, + "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -2894,22 +2891,23 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -2917,57 +2915,53 @@ }, "node_modules/inherits": { "version": "2.0.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-core-module": { "version": "2.13.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-generator-fn": { "version": "2.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2977,51 +2971,48 @@ }, "node_modules/is-number": { "version": "7.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-path-inside": { "version": "3.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-stream": { "version": "2.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } }, "node_modules/istanbul-lib-instrument": { "version": "6.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", - "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -3035,9 +3026,8 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -3049,9 +3039,8 @@ }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -3063,9 +3052,8 @@ }, "node_modules/istanbul-reports": { "version": "3.1.7", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -3076,24 +3064,25 @@ }, "node_modules/jackspeak": { "version": "2.3.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, "engines": { "node": ">=14" }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jest": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest/-/jest-29.7.0.tgz", - "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -3117,9 +3106,8 @@ }, "node_modules/jest-changed-files": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-changed-files/-/jest-changed-files-29.7.0.tgz", - "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", "dev": true, + "license": "MIT", "dependencies": { "execa": "^5.0.0", "jest-util": "^29.7.0", @@ -3131,9 +3119,8 @@ }, "node_modules/jest-circus": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-circus/-/jest-circus-29.7.0.tgz", - "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/expect": "^29.7.0", @@ -3162,9 +3149,8 @@ }, "node_modules/jest-cli": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, + "license": "MIT", "dependencies": { "@jest/core": "^29.7.0", "@jest/test-result": "^29.7.0", @@ -3195,9 +3181,8 @@ }, "node_modules/jest-config": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-config/-/jest-config-29.7.0.tgz", - "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@jest/test-sequencer": "^29.7.0", @@ -3240,9 +3225,8 @@ }, "node_modules/jest-diff": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -3255,9 +3239,8 @@ }, "node_modules/jest-docblock": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, + "license": "MIT", "dependencies": { "detect-newline": "^3.0.0" }, @@ -3267,9 +3250,8 @@ }, "node_modules/jest-each": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "chalk": "^4.0.0", @@ -3283,9 +3265,8 @@ }, "node_modules/jest-environment-node": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-environment-node/-/jest-environment-node-29.7.0.tgz", - "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -3300,18 +3281,16 @@ }, "node_modules/jest-get-type": { "version": "29.6.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-haste-map/-/jest-haste-map-29.7.0.tgz", - "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/graceful-fs": "^4.1.3", @@ -3334,9 +3313,8 @@ }, "node_modules/jest-leak-detector": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", - "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", "dev": true, + "license": "MIT", "dependencies": { "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" @@ -3347,9 +3325,8 @@ }, "node_modules/jest-matcher-utils": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", @@ -3362,9 +3339,8 @@ }, "node_modules/jest-message-util": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -3382,9 +3358,8 @@ }, "node_modules/jest-mock": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-mock/-/jest-mock-29.7.0.tgz", - "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -3396,9 +3371,8 @@ }, "node_modules/jest-pnp-resolver": { "version": "1.2.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -3413,18 +3387,16 @@ }, "node_modules/jest-regex-util": { "version": "29.6.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-regex-util/-/jest-regex-util-29.6.3.tgz", - "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", "dev": true, + "license": "MIT", "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-resolve/-/jest-resolve-29.7.0.tgz", - "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", @@ -3442,9 +3414,8 @@ }, "node_modules/jest-resolve-dependencies": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", - "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", "dev": true, + "license": "MIT", "dependencies": { "jest-regex-util": "^29.6.3", "jest-snapshot": "^29.7.0" @@ -3455,9 +3426,8 @@ }, "node_modules/jest-runner": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/console": "^29.7.0", "@jest/environment": "^29.7.0", @@ -3487,9 +3457,8 @@ }, "node_modules/jest-runtime": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-runtime/-/jest-runtime-29.7.0.tgz", - "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", @@ -3520,9 +3489,8 @@ }, "node_modules/jest-snapshot": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-snapshot/-/jest-snapshot-29.7.0.tgz", - "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.11.6", "@babel/generator": "^7.7.2", @@ -3551,8 +3519,7 @@ }, "node_modules/jest-sonar-reporter": { "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-sonar-reporter/-/jest-sonar-reporter-2.0.0.tgz", - "integrity": "sha512-ZervDCgEX5gdUbdtWsjdipLN3bKJwpxbvhkYNXTAYvAckCihobSLr9OT/IuyNIRT1EZMDDwR6DroWtrq+IL64w==", + "license": "MIT", "dependencies": { "xml": "^1.0.1" }, @@ -3562,9 +3529,8 @@ }, "node_modules/jest-util": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -3579,9 +3545,8 @@ }, "node_modules/jest-validate": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-validate/-/jest-validate-29.7.0.tgz", - "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", "dev": true, + "license": "MIT", "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", @@ -3596,18 +3561,19 @@ }, "node_modules/jest-validate/node_modules/camelcase": { "version": "6.3.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/jest-watcher": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, + "license": "MIT", "dependencies": { "@jest/test-result": "^29.7.0", "@jest/types": "^29.6.3", @@ -3624,9 +3590,8 @@ }, "node_modules/jest-worker": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", @@ -3639,27 +3604,27 @@ }, "node_modules/jest-worker/node_modules/supports-color": { "version": "8.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/js-tokens": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3669,9 +3634,8 @@ }, "node_modules/jsesc": { "version": "2.5.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -3681,33 +3645,28 @@ }, "node_modules/json-buffer": { "version": "3.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -3715,44 +3674,49 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/just-extend": { "version": "6.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, "node_modules/kleur": { "version": "3.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/leven": { "version": "3.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/levn": { "version": "0.4.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -3763,105 +3727,98 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/locate-path": { "version": "6.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.get": { "version": "4.4.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/loupe": { "version": "2.3.7", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", "dev": true, + "license": "MIT", "dependencies": { "get-func-name": "^2.0.1" } }, "node_modules/lru-cache": { "version": "5.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, "node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/makeerror": { "version": "1.0.12", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "tmpl": "1.0.5" } }, "node_modules/merge-stream": { "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/metric-lcs": { "version": "0.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/metric-lcs/-/metric-lcs-0.1.2.tgz", - "integrity": "sha512-+TZ5dUDPKPJaU/rscTzxyN8ZkX7eAVLAiQU/e+YINleXPv03SCmJShaMT1If1liTH8OcmWXZs0CmzCBRBLcMpA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/micromatch": { "version": "4.0.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -3870,72 +3827,75 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.3", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { "version": "7.0.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/mri": { "version": "1.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/ms": { "version": "2.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "dev": true, + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/nise": { "version": "5.1.9", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/nise/-/nise-5.1.9.tgz", - "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0", "@sinonjs/fake-timers": "^11.2.2", @@ -3946,50 +3906,34 @@ }, "node_modules/nise/node_modules/@sinonjs/fake-timers": { "version": "11.2.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" } }, - "node_modules/node-downloader-helper": { - "version": "2.1.9", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/node-downloader-helper/-/node-downloader-helper-2.1.9.tgz", - "integrity": "sha512-FSvAol2Z8UP191sZtsUZwHIN0eGoGue3uEXGdWIH5228e9KH1YHXT7fN8Oa33UGf+FbqGTQg3sJfrRGzmVCaJA==", - "bin": { - "ndh": "bin/ndh" - }, - "engines": { - "node": ">=14.18" - } - }, "node_modules/node-int64": { "version": "0.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-releases": { "version": "2.0.14", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/npm-run-path": { "version": "4.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -3999,30 +3943,30 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { "version": "5.1.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/optionator": { "version": "0.9.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", "dev": true, + "license": "MIT", "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -4037,42 +3981,44 @@ }, "node_modules/p-limit": { "version": "3.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-locate": { "version": "5.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-try": { "version": "2.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/parent-module": { "version": "1.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -4082,9 +4028,8 @@ }, "node_modules/parse-json": { "version": "5.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -4093,116 +4038,112 @@ }, "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/path-exists": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/path-key": { "version": "3.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/path-parse": { "version": "1.0.7", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.10.2", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { "version": "10.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", "dev": true, + "license": "ISC", "engines": { "node": "14 || >=16.14" } }, "node_modules/path-to-regexp": { - "version": "6.2.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", - "dev": true + "version": "6.2.2", + "dev": true, + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/pathval": { "version": "1.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "dev": true, + "license": "MIT", "engines": { "node": "*" } }, "node_modules/picocolors": { "version": "1.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/pirates": { "version": "4.0.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6" } }, "node_modules/pkg-dir": { "version": "4.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -4212,9 +4153,8 @@ }, "node_modules/pkg-dir/node_modules/find-up": { "version": "4.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -4225,9 +4165,8 @@ }, "node_modules/pkg-dir/node_modules/locate-path": { "version": "5.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -4237,21 +4176,22 @@ }, "node_modules/pkg-dir/node_modules/p-limit": { "version": "2.3.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, "engines": { "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/pkg-dir/node_modules/p-locate": { "version": "4.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -4261,30 +4201,30 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } }, "node_modules/prettier": { "version": "3.2.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/pretty-format": { "version": "29.7.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, + "license": "MIT", "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -4296,18 +4236,19 @@ }, "node_modules/pretty-format/node_modules/ansi-styles": { "version": "5.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/pretty-quick": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/pretty-quick/-/pretty-quick-4.0.0.tgz", - "integrity": "sha512-M+2MmeufXb/M7Xw3Afh1gxcYpj+sK0AxEfnfF958ktFeAyi5MsKY5brymVURQLgPLV1QaF5P4pb2oFJ54H3yzQ==", "dev": true, + "license": "MIT", "dependencies": { "execa": "^5.1.1", "find-up": "^5.0.0", @@ -4329,26 +4270,19 @@ }, "node_modules/pretty-quick/node_modules/picomatch": { "version": "3.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "engines": { - "node": ">=0.4.0" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/prompts": { "version": "2.4.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", "dev": true, + "license": "MIT", "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" @@ -4357,47 +4291,73 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/pure-rand": { - "version": "6.0.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/pure-rand/-/pure-rand-6.0.4.tgz", - "integrity": "sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==", - "dev": true + "version": "6.1.0", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" }, "node_modules/queue-microtask": { "version": "1.2.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "license": "MIT" }, "node_modules/react-is": { "version": "18.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/resolve": { "version": "1.22.8", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -4405,13 +4365,15 @@ }, "bin": { "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-cwd": { "version": "3.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, + "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -4421,36 +4383,32 @@ }, "node_modules/resolve-cwd/node_modules/resolve-from": { "version": "5.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/resolve-from": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/resolve.exports": { "version": "2.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/reusify": { "version": "1.0.4", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -4458,9 +4416,8 @@ }, "node_modules/rimraf": { "version": "5.0.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", "dev": true, + "license": "ISC", "dependencies": { "glob": "^10.3.7" }, @@ -4469,62 +4426,57 @@ }, "engines": { "node": ">=14" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rimraf/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "version": "10.3.12", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", + "jackspeak": "^2.3.6", "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "minipass": "^7.0.4", + "path-scurry": "^1.10.2" }, "bin": { "glob": "dist/esm/bin.mjs" }, "engines": { "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" }, - "engines": { - "node": ">=16 || 14 >=14.17" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/run-parallel": { "version": "1.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/semver": { "version": "7.6.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -4537,9 +4489,7 @@ }, "node_modules/semver/node_modules/lru-cache": { "version": "6.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -4549,15 +4499,12 @@ }, "node_modules/semver/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4567,24 +4514,21 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/sinon": { "version": "17.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/sinon/-/sinon-17.0.1.tgz", - "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0", "@sinonjs/fake-timers": "^11.2.2", @@ -4592,54 +4536,45 @@ "diff": "^5.1.0", "nise": "^5.1.5", "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" } }, "node_modules/sinon/node_modules/@sinonjs/fake-timers": { "version": "11.2.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "node_modules/sisteransi": { "version": "1.0.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/slash": { "version": "3.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/slugify": { - "version": "1.6.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/slugify/-/slugify-1.6.6.tgz", - "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/source-map": { "version": "0.6.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/source-map-support": { "version": "0.5.13", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -4647,15 +4582,13 @@ }, "node_modules/sprintf-js": { "version": "1.0.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/stack-utils": { "version": "2.0.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -4665,18 +4598,27 @@ }, "node_modules/stack-utils/node_modules/escape-string-regexp": { "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/streamx": { + "version": "2.16.1", + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.1.0", + "queue-tick": "^1.0.1" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, + "license": "MIT", "dependencies": { "char-regex": "^1.0.2", "strip-ansi": "^6.0.0" @@ -4687,9 +4629,8 @@ }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4702,9 +4643,8 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -4716,9 +4656,8 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4729,9 +4668,8 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -4741,36 +4679,35 @@ }, "node_modules/strip-bom": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/strip-final-newline": { "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/strip-json-comments": { "version": "3.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4780,18 +4717,28 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" } }, "node_modules/test-exclude": { "version": "6.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -4801,32 +4748,48 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/text-table": { "version": "0.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tmpl": { "version": "1.0.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/to-fast-properties": { "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/to-regex-range": { "version": "5.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -4836,9 +4799,8 @@ }, "node_modules/ts-api-utils": { "version": "1.3.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -4848,15 +4810,13 @@ }, "node_modules/tslib": { "version": "2.6.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "dev": true, + "license": "0BSD" }, "node_modules/type-check": { "version": "0.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -4866,27 +4826,27 @@ }, "node_modules/type-detect": { "version": "4.0.8", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/type-fest": { "version": "0.20.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/typescript": { "version": "5.4.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/typescript/-/typescript-5.4.3.tgz", - "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4897,15 +4857,34 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } }, "node_modules/update-browserslist-db": { "version": "1.0.13", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" @@ -4919,18 +4898,16 @@ }, "node_modules/uri-js": { "version": "4.4.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, "node_modules/v8-to-istanbul": { "version": "9.2.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", - "integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==", "dev": true, + "license": "ISC", "dependencies": { "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", @@ -4942,18 +4919,16 @@ }, "node_modules/walker": { "version": "1.0.8", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "makeerror": "1.0.12" } }, "node_modules/which": { "version": "2.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -4966,9 +4941,8 @@ }, "node_modules/wrap-ansi": { "version": "7.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -4976,14 +4950,16 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -4991,19 +4967,20 @@ }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/write-file-atomic": { "version": "4.0.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -5014,29 +4991,25 @@ }, "node_modules/xml": { "version": "1.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/xml/-/xml-1.0.1.tgz", - "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + "license": "MIT" }, "node_modules/y18n": { "version": "5.0.8", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -5052,20 +5025,21 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/yocto-queue": { "version": "0.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } } } diff --git a/package.json b/package.json index e4001547..aaf88580 100644 --- a/package.json +++ b/package.json @@ -20,20 +20,25 @@ "sonar-scanner": "src/bin/sonar-scanner" }, "engines": { - "node": ">= 16" + "node": ">= 18" }, "dependencies": { "adm-zip": "0.5.12", - "fancy-log": "2.0.0", - "https-proxy-agent": "7.0.4", + "axios": "1.6.8", + "fs-extra": "11.2.0", + "hpagent": "1.2.0", "jest-sonar-reporter": "2.0.0", - "mkdirp": "3.0.1", - "node-downloader-helper": "2.1.9", - "progress": "2.0.3", - "slugify": "1.6.6" + "proxy-from-env": "1.1.0", + "semver": "7.6.0", + "tar-stream": "3.1.7" }, "devDependencies": { + "@types/adm-zip": "^0.5.5", + "@types/fs-extra": "^11.0.4", "@types/jest": "29.5.12", + "@types/proxy-from-env": "^1.0.4", + "@types/semver": "^7.5.8", + "@types/tar-stream": "^3.1.3", "@typescript-eslint/parser": "7.4.0", "chai": "4.4.1", "eslint": "8.57.0", @@ -55,7 +60,8 @@ "sonar-runner" ], "scripts": { - "build": "npm ci && npm run check-format && npm run license && npm test && cd tools/orchestrator && npm run build", + "build": "npm ci && npm run ts-build && npm run check-format && npm run license && npm test && cd tools/orchestrator && npm run build", + "ts-build": "tsc && node scripts/fix-comments.js", "test": "npx jest --coverage", "test-integration": "cd test/integration && npm test", "format": "prettier --write .", diff --git a/scripts/ci-analysis.js b/scripts/ci-analysis.js index 8536bee1..cc87821a 100644 --- a/scripts/ci-analysis.js +++ b/scripts/ci-analysis.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/scripts/file-header.ts b/scripts/file-header.ts index 6ad37515..fadfa9f5 100644 --- a/scripts/file-header.ts +++ b/scripts/file-header.ts @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/scripts/fix-comments.js b/scripts/fix-comments.js new file mode 100644 index 00000000..3d8e38a1 --- /dev/null +++ b/scripts/fix-comments.js @@ -0,0 +1,43 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +const fs = require('fs'); +const path = require('path'); + +const LICENSE_HEADER = fs.readFileSync(path.resolve(__dirname, 'file-header.ts')).toString().trim(); + +// Read every .js file in the ../build directory +const directoryPath = path.resolve(__dirname, '../build/src'); + +const fileNames = fs.readdirSync(directoryPath); +for (const fileName of fileNames) { + // Skip if not a .js file + if (!fileName.endsWith('.js')) { + continue; + } + + // Read the file, drop the license header, re-prepend it and write the file + const filePath = path.join(directoryPath, fileName); + const fileContent = fs.readFileSync(filePath, 'utf8'); + const fileWithoutHeader = fileContent.replace(LICENSE_HEADER, ''); + const newFileContent = `${LICENSE_HEADER}\n${fileWithoutHeader}`; + + fs.writeFileSync(filePath, newFileContent); +} diff --git a/src/bin/sonar-scanner b/src/bin/sonar-scanner index 5adf20dc..c9fcb83d 100755 --- a/src/bin/sonar-scanner +++ b/src/bin/sonar-scanner @@ -1,5 +1,24 @@ #!/usr/bin/env node -const scan = require('../index').scan; +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +const scan = require('../../build/src/index').scan; const options = process.argv.length > 2 ? process.argv.slice(2) : []; diff --git a/src/config.js b/src/config.js deleted file mode 100644 index 105c6dfe..00000000 --- a/src/config.js +++ /dev/null @@ -1,201 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -const sonarScannerParams = require('./sonar-scanner-params'); -const { findTargetOS, buildInstallFolderPath, buildExecutablePath } = require('./utils'); -const os = require('os'); -const fs = require('fs'); -const log = require('fancy-log'); -const { HttpsProxyAgent } = require('https-proxy-agent'); -const { isWindows } = require('./utils/platform'); - -module.exports.getScannerParams = getScannerParams; -module.exports.extendWithExecParams = extendWithExecParams; -module.exports.getExecutableParams = getExecutableParams; - -const DEFAULT_EXCLUSIONS = - 'node_modules/**,bower_components/**,jspm_packages/**,typings/**,lib-cov/**'; -module.exports.DEFAULT_EXCLUSIONS = DEFAULT_EXCLUSIONS; -const DEFAULT_SCANNER_VERSION = '5.0.1.3006'; -module.exports.DEFAULT_SCANNER_VERSION = DEFAULT_SCANNER_VERSION; -const SONAR_SCANNER_MIRROR = 'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/'; -module.exports.SONAR_SCANNER_MIRROR = SONAR_SCANNER_MIRROR; - -/** - * Build the SONARQUBE_SCANNER_PARAMS which will be passed as an environment - * variable to the scanner. - * - * @returns - */ -function getScannerParams(basePath, params = {}) { - const config = {}; - - const sqScannerParams = sonarScannerParams( - params, - basePath, - process.env.hasOwnProperty('SONARQUBE_SCANNER_PARAMS') && process.env.SONARQUBE_SCANNER_PARAMS, - ); - - // We need to merge the existing env variables (process.env) with the SQ ones - if (sqScannerParams) { - config.SONARQUBE_SCANNER_PARAMS = sqScannerParams; - } - - return config; -} - -/** - * Gather the parameters for sonar-scanner-executable: - * - installFolder - * - platformExecutable - * - downloadUrl - * - fileName - * - httpOptions, if proxy - */ -function getExecutableParams(params = {}) { - const config = { - httpOptions: {}, - }; - - const env = process.env; - - let platformBinariesVersion = DEFAULT_SCANNER_VERSION; - - if (params.hasOwnProperty('version')) { - platformBinariesVersion = params.version; - } else if (env.hasOwnProperty('SONAR_SCANNER_VERSION')) { - platformBinariesVersion = env.SONAR_SCANNER_VERSION; - } else if (env.hasOwnProperty('npm_config_sonar_scanner_version')) { - platformBinariesVersion = env.npm_config_sonar_scanner_version; - } - - if (!/^[\d.]+$/.test(platformBinariesVersion)) { - log( - `Version "${platformBinariesVersion}" does not have a correct format. Will use default version "${DEFAULT_SCANNER_VERSION}"`, - ); - platformBinariesVersion = DEFAULT_SCANNER_VERSION; - } - - const targetOS = (config.targetOS = findTargetOS()); - - let basePath = os.homedir(); - if (params.hasOwnProperty('basePath')) { - basePath = params.basePath; - } else if (env.hasOwnProperty('SONAR_BINARY_CACHE')) { - basePath = env.SONAR_BINARY_CACHE; - } else if (env.hasOwnProperty('npm_config_sonar_binary_cache')) { - basePath = env.npm_config_sonar_binary_cache; - } - - const installFolder = (config.installFolder = buildInstallFolderPath(basePath)); - config.platformExecutable = buildExecutablePath(installFolder, platformBinariesVersion); - - let baseUrl = SONAR_SCANNER_MIRROR; - if (params.hasOwnProperty('baseUrl')) { - baseUrl = params.baseUrl; - } else if (env.hasOwnProperty('SONAR_SCANNER_MIRROR')) { - baseUrl = env.SONAR_SCANNER_MIRROR; - } else if (env.hasOwnProperty('npm_config_sonar_scanner_mirror')) { - baseUrl = env.npm_config_sonar_scanner_mirror; - } - - const fileName = (config.fileName = - 'sonar-scanner-cli-' + platformBinariesVersion + '-' + targetOS + '.zip'); - - let finalUrl; - - try { - finalUrl = new URL(fileName, baseUrl); - } catch (e) { - log(`Invalid URL "${baseUrl}". Will use default mirror "${SONAR_SCANNER_MIRROR}"`); - finalUrl = new URL(fileName, SONAR_SCANNER_MIRROR); - } - - config.downloadUrl = finalUrl.href; - - let proxy = ''; - if (env.hasOwnProperty('http_proxy') && typeof env.http_proxy === 'string') { - proxy = env.http_proxy; - } - // Use https_proxy when available - if ( - env.hasOwnProperty('https_proxy') && - typeof env.https_proxy === 'string' && - finalUrl.protocol === 'https:' - ) { - proxy = env.https_proxy; - } - if (proxy && proxy !== '') { - try { - new URL(proxy); - const proxyAgent = new HttpsProxyAgent(proxy); - config.httpOptions.httpRequestOptions = { agent: proxyAgent }; - config.httpOptions.httpsRequestOptions = { agent: proxyAgent }; - } catch (e) { - log(`Invalid proxy "${proxy}"`); - } - } - - if (finalUrl.username !== '' || finalUrl.password !== '') { - config.httpOptions.headers = { - Authorization: - 'Basic ' + Buffer.from(finalUrl.username + ':' + finalUrl.password).toString('base64'), - }; - } - - if (params.caPath) { - config.httpOptions.ca = extractCa(params.caPath); - } - - log(`Executable parameters built:`); - log(config); - return config; - - function extractCa(caPath) { - if (!fs.existsSync(caPath)) { - throw new Error(`Provided CA certificate path does not exist: ${caPath}`); - } - const ca = fs.readFileSync(caPath, 'utf8'); - if (!ca.startsWith('-----BEGIN CERTIFICATE-----')) { - throw new Error('Invalid CA certificate'); - } - return ca; - } -} - -/** - * Options for child_proces.exec() - * - * @param {*} env the environment variables - * @returns - */ -function extendWithExecParams(env = {}) { - const ONE_MB = 1024 * 1024; - - return { - env: Object.assign({}, process.env, env), - stdio: 'inherit', - // Increase the amount of data allowed on stdout or stderr - // (if this value is exceeded then the child process is killed). - // TODO: make this customizable - maxBuffer: ONE_MB, - shell: isWindows(), //we need to enable shell on windows due to CVE-2024-27980 - }; -} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..a42633e5 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,35 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import path from 'path'; + +export const SCANNER_BOOTSTRAPPER_NAME = 'ScannerNpm'; + +export const SONARCLOUD_ENV_REGEX = + /^(https?:\/\/)?(www\.)?([a-zA-Z0-9-]+\.)?(sc-dev\.io|sc-staging\.io|sonarcloud\.io)/; + +export const SONARQUBE_JRE_PROVISIONING_MIN_VERSION = '10.6'; + +export const SONAR_CACHE_DIR = path.join( + process.env.HOME ?? process.env.USERPROFILE ?? '', + '.sonar', + 'cache', +); + +export const UNARCHIVE_SUFFIX = '_extracted'; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index b29dc706..00000000 --- a/src/index.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -const exec = require('child_process').execFileSync; -const log = require('fancy-log'); -const { getScannerParams, extendWithExecParams } = require('./config'); -const { getScannerExecutable } = require('./sonar-scanner-executable'); -const version = require('../package.json').version; - -/* - * Function used programmatically to trigger an analysis. - */ -async function scan(params, cliArgs = [], localScanner = false) { - log('Starting analysis...'); - - // determine the command to run and execute it - const sqScannerCommand = await getScannerExecutable(localScanner, params); - - // prepare the exec options, most notably with the SQ params - const scannerParams = getScannerParams(process.cwd(), params); - const execOptions = extendWithExecParams(scannerParams); - exec(sqScannerCommand, fromParam().concat(cliArgs), execOptions); - log('Analysis finished.'); -} - -function scanWithCallback(params, cliArgs, localScanner, callback) { - // here we make the code unit-testable - i.e. by making the scan property stub-able - // this is not nice - and anyway when we move to ESM it won't work anymore because the module will be read-only - - // but for now, it allows us to unit test the module - module.exports.scan(params, cliArgs, localScanner).then(() => { - callback(); - }); -} - -function fromParam() { - return [`--from=ScannerNpm/${version}`]; -} - -module.exports = (params, callback) => scanWithCallback(params, [], false, callback); -module.exports.scan = scan; -module.exports.cli = (cliArgs, params, callback) => - scanWithCallback(params, cliArgs, false, callback); -module.exports.customScanner = (params, callback) => scanWithCallback(params, [], true, callback); -module.exports.async = async params => { - await scan(params); -}; -module.exports.fromParam = fromParam; diff --git a/src/utils/index.js b/src/index.ts similarity index 82% rename from src/utils/index.js rename to src/index.ts index 75b5e73a..d1872f1b 100644 --- a/src/utils/index.js +++ b/src/index.ts @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or @@ -17,7 +17,4 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - -const platform = require('./platform'); -const paths = require('./paths'); -module.exports = Object.assign({}, platform, paths); +export { scan } from './scan'; diff --git a/src/logging.ts b/src/logging.ts new file mode 100644 index 00000000..5701a02e --- /dev/null +++ b/src/logging.ts @@ -0,0 +1,71 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +export enum LogLevel { + TRACE = 'TRACE', + DEBUG = 'DEBUG', + INFO = 'INFO', + WARN = 'WARN', + ERROR = 'ERROR', +} + +const logLevelValues = { + ERROR: 0, + WARN: 1, + INFO: 2, + DEBUG: 3, + TRACE: 4, +}; + +const DEFAULT_LOG_LEVEL = LogLevel.INFO; + +let logLevel = DEFAULT_LOG_LEVEL; + +export function log(level: LogLevel, ...message: unknown[]) { + if (logLevelValues[level] <= logLevelValues[logLevel]) { + console.log(`[${level}] Bootstrapper ${message}`); + } +} + +export function getLogLevel() { + return logLevel; +} + +function stringToLogLevel(level: string): LogLevel { + switch (level.toUpperCase()) { + case 'ERROR': + return LogLevel.ERROR; + case 'WARN': + return LogLevel.WARN; + case 'INFO': + return LogLevel.INFO; + case 'DEBUG': + return LogLevel.DEBUG; + case 'TRACE': + return LogLevel.TRACE; + default: + log(LogLevel.WARN, `Invalid log level: ${level}`); + return DEFAULT_LOG_LEVEL; + } +} + +export function setLogLevel(level: string) { + logLevel = stringToLogLevel(level); +} diff --git a/src/utils/platform.js b/src/scan.ts similarity index 55% rename from src/utils/platform.js rename to src/scan.ts index 74ff0b30..4f063bc4 100644 --- a/src/utils/platform.js +++ b/src/scan.ts @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or @@ -18,34 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -function isWindows() { - return /^win/.test(process.platform); -} - -function isMac() { - return /^darwin/.test(process.platform); -} - -function isLinux() { - return /^linux/.test(process.platform); -} - -/* - * Get the target OS based on the platform name - */ -module.exports.findTargetOS = function () { - if (isWindows()) { - return 'windows'; - } - if (isLinux()) { - return 'linux'; - } - if (isMac()) { - return 'macosx'; - } - throw Error(`Your platform '${process.platform}' is currently not supported.`); +export type ScanOptions = { + serverUrl: string; + token: string; + jvmOptions: string[]; + options?: { [key: string]: string }; + caPath: string; + logLevel?: string; + verbose?: boolean; }; -module.exports.isWindows = isWindows; -module.exports.isMac = isMac; -module.exports.isLinux = isLinux; +export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { + // TODO: NPMSCAN-2 new bootstrapper sequence +} diff --git a/src/sonar-scanner-executable.js b/src/sonar-scanner-executable.js deleted file mode 100644 index 185993f6..00000000 --- a/src/sonar-scanner-executable.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -const exec = require('child_process').execFileSync; -const mkdirs = require('mkdirp').sync; -const { DownloaderHelper } = require('node-downloader-helper'); -const AdmZip = require('adm-zip'); -const ProgressBar = require('progress'); -const log = require('fancy-log'); -const logError = log.error; -const path = require('path'); -const { getExecutableParams } = require('./config'); - -module.exports.getScannerExecutable = getScannerExecutable; - -const bar = new ProgressBar('[:bar] :percent :etas', { - complete: '=', - incomplete: ' ', - width: 20, - total: 0, -}); - -/** - * If localScanner is true, returns the command to use the local scanner executable. - * Otherwise, returns a promise to download the scanner executable and the command to use it. - * - * @param {*} localScanner - * @param {*} params - * @returns - */ -function getScannerExecutable(localScanner = false, params = {}) { - if (localScanner) { - return getLocalSonarScannerExecutable(); - } - return getSonarScannerExecutable(params); -} - -/* - * Returns the SQ Scanner executable for the current platform - */ -async function getSonarScannerExecutable(params = {}) { - const config = getExecutableParams(params); - const { downloadUrl, httpOptions, platformExecutable, fileName, targetOS } = config; - - // #1 - Try to execute the scanner - try { - return getLocalSonarScannerExecutable(platformExecutable); - } catch (e) { - // ignore - } - - const installFolder = config.installFolder; - // #2 - Download the binaries and unzip them - // They are located at https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${version}-${os}.zip - log('Proceed with download of the platform binaries for SonarScanner...'); - log('Creating ' + installFolder); - mkdirs(installFolder); - // SQ - - const downloader = new DownloaderHelper(downloadUrl, installFolder, httpOptions); - // node-downloader-helper recommends defining both an onError and a catch because: - // "if on('error') is not defined, an error will be thrown when the error event is emitted and - // not listing, this is because EventEmitter is designed to throw an unhandled error event - // error if not been listened and is too late to change it now." - downloader.on('error', error => { - logError('error in downloader'); - logError(error); - }); - downloader.on('download', downloadInfo => { - bar.total = downloadInfo.totalSize; - }); - downloader.on('progress', stats => { - bar.update(stats.progress / 100); - }); - try { - await downloader.start(); - const tarPath = path.join(installFolder, fileName); - log('decompressing', tarPath, 'into', installFolder); - const zip = new AdmZip(tarPath); - zip.extractAllTo(installFolder, true, true); - log('decompressed', platformExecutable); - return platformExecutable; - } catch (err) { - logError(`ERROR: impossible to download and extract binary: ${err.message}`); - logError(` SonarScanner binaries probably don't exist for your OS (${targetOS}).`); - logError( - ' In such situation, the best solution is to install the standard SonarScanner (requires a JVM).', - ); - logError( - ' Check it out at https://redirect.sonarsource.com/doc/install-configure-scanner.html', - ); - throw err; - } -} - -/** - * Verifies if the provided (or default) command is executable - * Throws otherwise - * - * @param {*} command the command to execute. - * @returns the command to execute - */ -function getLocalSonarScannerExecutable(command = 'sonar-scanner') { - try { - log(`Trying to find a local install of the SonarScanner: ${command}`); - exec(command, ['-v'], { shell: true }); - // TODO: we should check that it's at least v2.8+ - log('Local install of Sonarscanner found.'); - return command; - } catch (e) { - throw Error(`Local install of SonarScanner not found in: ${command}`); - } -} diff --git a/src/sonar-scanner-params.js b/src/sonar-scanner-params.js deleted file mode 100644 index d5f9e9ef..00000000 --- a/src/sonar-scanner-params.js +++ /dev/null @@ -1,196 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -const fs = require('fs'); -const path = require('path'); -const slugify = require('slugify'); -const log = require('fancy-log'); - -module.exports = defineSonarScannerParams; - -const invalidCharacterRegex = /[?$*+~.()'"!:@/]/g; - -/* - * Build the config.SONARQUBE_SCANNER_PARAMS property from: - * 1. params - * serverUrl -> sonar.host.url - * login -> sonar.login - * token -> sonar.token - * options to root - * 2. the 'SONARQUBE_SCANNER_PARAMS' env variable - * all - * 3. sonar-project-properties - * as-is - * OR (TODO: make it hierarchical, not conditionnal) - * 3. package.json (only some other fields) - * slug(name) -> sonar.projectKey - * name -> sonar.projectName - * version -> sonar.projectVersion - * description -> sonar.projectDescription - * homepage -> sonar.links.homepage - * bugs.url -> sonar.links.issue - * repository.url -> sonar.links.scm - * pick up nyc/jest and append them to existing sonar.exclusions - * some logic aroung sonar.javascript.lcov.reportPaths - * same for sonar.testExecutionReportPaths - * same for sonar.testExecutionReportPaths - * 4. default values (same) - * sonar.projectDescription - * sonar.sources - * sonar.exclusions - * - * returns it stringified - * - * Try to be smart and guess most SQ parameters from JS files that - * might exist - like 'package.json'. - */ -function defineSonarScannerParams(params, projectBaseDir, sqScannerParamsFromEnvVariable) { - // #1 - set default values - let sonarScannerParams = {}; - try { - const sqFile = path.join(projectBaseDir, 'sonar-project.properties'); - fs.accessSync(sqFile, fs.F_OK); - // there's a 'sonar-project.properties' file - no need to set default values - } catch (e) { - sonarScannerParams = { - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.exclusions': - 'node_modules/**,bower_components/**,jspm_packages/**,typings/**,lib-cov/**', - }; - // If there's a 'package.json' file, read it to grab info - try { - sonarScannerParams = Object.assign( - {}, - sonarScannerParams, - extractInfoFromPackageFile(projectBaseDir, sonarScannerParams['sonar.exclusions']), - ); - } catch (extractError) { - // No 'package.json' file (or invalid one) - let's remain on the defaults - log(`No 'package.json' file found (or no valid one): ${extractError.message}`); - log('=> Using default settings.'); - } - } - - // #2 - if SONARQUBE_SCANNER_PARAMS exists, extend the current params - if (sqScannerParamsFromEnvVariable) { - sonarScannerParams = Object.assign( - {}, - sonarScannerParams, - JSON.parse(sqScannerParamsFromEnvVariable), - ); - } - - // #3 - check what's passed in the call params - these are prevalent params - if (params.serverUrl) { - sonarScannerParams['sonar.host.url'] = params.serverUrl; - } - if (params.login) { - sonarScannerParams['sonar.login'] = params.login; - } - if (params.token) { - sonarScannerParams['sonar.token'] = params.token; - } - if (params.options) { - sonarScannerParams = Object.assign(sonarScannerParams, params.options); - } - - if (!isEmpty(sonarScannerParams)) { - return JSON.stringify(sonarScannerParams); - } else { - return null; - } -} - -function isEmpty(jsObject) { - return jsObject.constructor === Object && Object.entries(jsObject).length === 0; -} - -/** - * Build the config. - * - * @param {*} projectBaseDir - */ -function extractInfoFromPackageFile(projectBaseDir, exclusions) { - const packageJsonParams = {}; - const packageFile = path.join(projectBaseDir, 'package.json'); - const packageData = fs.readFileSync(packageFile); - const pkg = JSON.parse(packageData); - log('Retrieving info from "package.json" file'); - function fileExistsInProjectSync(file) { - return fs.existsSync(path.resolve(projectBaseDir, file)); - } - function dependenceExists(pkgName) { - return ['devDependencies', 'dependencies', 'peerDependencies'].some(function (prop) { - return pkg[prop] && pkgName in pkg[prop]; - }); - } - if (pkg) { - packageJsonParams['sonar.projectKey'] = slugify(pkg.name, { - remove: invalidCharacterRegex, - }); - packageJsonParams['sonar.projectName'] = pkg.name; - packageJsonParams['sonar.projectVersion'] = pkg.version; - if (pkg.description) { - packageJsonParams['sonar.projectDescription'] = pkg.description; - } - if (pkg.homepage) { - packageJsonParams['sonar.links.homepage'] = pkg.homepage; - } - if (pkg.bugs?.url) { - packageJsonParams['sonar.links.issue'] = pkg.bugs.url; - } - if (pkg.repository?.url) { - packageJsonParams['sonar.links.scm'] = pkg.repository.url; - } - - const potentialCoverageDirs = [ - // jest coverage output directory - // See: http://facebook.github.io/jest/docs/en/configuration.html#coveragedirectory-string - pkg['nyc']?.['report-dir'], - // nyc coverage output directory - // See: https://github.com/istanbuljs/nyc#configuring-nyc - pkg['jest']?.['coverageDirectory'], - ] - .filter(Boolean) - .concat( - // default coverage output directory - 'coverage', - ); - const uniqueCoverageDirs = Array.from(new Set(potentialCoverageDirs)); - packageJsonParams['sonar.exclusions'] = exclusions; - for (const lcovReportDir of uniqueCoverageDirs) { - const lcovReportPath = path.posix.join(lcovReportDir, 'lcov.info'); - if (fileExistsInProjectSync(lcovReportPath)) { - packageJsonParams['sonar.exclusions'] += ',' + path.posix.join(lcovReportDir, '**'); - // https://docs.sonarqube.org/display/PLUG/JavaScript+Coverage+Results+Import - packageJsonParams['sonar.javascript.lcov.reportPaths'] = lcovReportPath; - // TODO: use Generic Test Data to remove dependence of SonarJS, it is need transformation lcov to sonar generic coverage format - } - } - - if (dependenceExists('mocha-sonarqube-reporter') && fileExistsInProjectSync('xunit.xml')) { - // https://docs.sonarqube.org/display/SONAR/Generic+Test+Data - packageJsonParams['sonar.testExecutionReportPaths'] = 'xunit.xml'; - } - // TODO: use `glob` to lookup xunit format files and transformation to sonar generic report format - } - return packageJsonParams; -} diff --git a/src/utils/paths.js b/src/types.ts similarity index 57% rename from src/utils/paths.js rename to src/types.ts index af0b354c..fc7e7c44 100644 --- a/src/utils/paths.js +++ b/src/types.ts @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or @@ -17,27 +17,25 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { LogLevel } from './logging'; -const path = require('path'); -const { isWindows, findTargetOS } = require('./platform'); +export type SupportedOS = 'windows' | 'linux' | 'alpine' | 'macos' | 'aix'; -module.exports.buildExecutablePath = function (installFolder, platformBinariesVersion) { - return path.join( - installFolder, - `sonar-scanner-${platformBinariesVersion}-${findTargetOS()}`, - 'bin', - `sonar-scanner${getBinaryExtension()}`, - ); +export type PlatformInfo = { + os: SupportedOS | null; + arch: string; }; -module.exports.buildInstallFolderPath = function (basePath) { - return path.join(basePath, '.sonar', 'native-sonar-scanner'); +export type JreMetaData = { + filename: string; + md5: string; + javaPath: string; }; -function getBinaryExtension() { - if (isWindows()) { - return '.bat'; - } else { - return ''; - } -} +export type ScannerLogEntry = { + level: LogLevel; + formattedMessage: string; + throwable?: string; +}; + +export type ScannerParams = { [key: string]: string }; diff --git a/test/integration/fixtures/fake_project_for_integration/src/index.js b/test/integration/fixtures/fake_project_for_integration/src/index.js index c0a28ea6..73563266 100644 --- a/test/integration/fixtures/fake_project_for_integration/src/index.js +++ b/test/integration/fixtures/fake_project_for_integration/src/index.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/test/integration/scanner.test.js b/test/integration/scanner.test.js index 9ac5099f..9497baa4 100644 --- a/test/integration/scanner.test.js +++ b/test/integration/scanner.test.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/test/unit/config.test.js b/test/unit/config.test.js index 591e28b8..f98774be 100644 --- a/test/unit/config.test.js +++ b/test/unit/config.test.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/test/unit/fixtures/fake_project_with_no_package_file/index.js b/test/unit/fixtures/fake_project_with_no_package_file/index.js index 77ef5f63..64cf57f0 100644 --- a/test/unit/fixtures/fake_project_with_no_package_file/index.js +++ b/test/unit/fixtures/fake_project_with_no_package_file/index.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/test/unit/fixtures/webserver/server.js b/test/unit/fixtures/webserver/server.js index d043db78..9557b500 100644 --- a/test/unit/fixtures/webserver/server.js +++ b/test/unit/fixtures/webserver/server.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/test/unit/index.test.js b/test/unit/index.test.js index f7f5b816..862b9493 100644 --- a/test/unit/index.test.js +++ b/test/unit/index.test.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/test/unit/sonar-scanner-executable.test.js b/test/unit/sonar-scanner-executable.test.js index c3890939..24a80dab 100644 --- a/test/unit/sonar-scanner-executable.test.js +++ b/test/unit/sonar-scanner-executable.test.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/test/unit/utils.test.js b/test/unit/utils.test.js index 6494da46..42488966 100644 --- a/test/unit/utils.test.js +++ b/test/unit/utils.test.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/tools/orchestrator/scripts/full.js b/tools/orchestrator/scripts/full.js index 4b6b900a..015d5e79 100644 --- a/tools/orchestrator/scripts/full.js +++ b/tools/orchestrator/scripts/full.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/tools/orchestrator/scripts/issues.js b/tools/orchestrator/scripts/issues.js index c9a5b06a..3c2f173c 100644 --- a/tools/orchestrator/scripts/issues.js +++ b/tools/orchestrator/scripts/issues.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/tools/orchestrator/scripts/sonarqube.js b/tools/orchestrator/scripts/sonarqube.js index 1183a409..2c6d71a3 100644 --- a/tools/orchestrator/scripts/sonarqube.js +++ b/tools/orchestrator/scripts/sonarqube.js @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/tools/orchestrator/src/download.ts b/tools/orchestrator/src/download.ts index 01ccec8a..60aabc3f 100644 --- a/tools/orchestrator/src/download.ts +++ b/tools/orchestrator/src/download.ts @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/tools/orchestrator/src/index.ts b/tools/orchestrator/src/index.ts index c73bf656..e6d26619 100644 --- a/tools/orchestrator/src/index.ts +++ b/tools/orchestrator/src/index.ts @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/tools/orchestrator/src/sonarqube.ts b/tools/orchestrator/src/sonarqube.ts index 317b626e..ecf95fd9 100644 --- a/tools/orchestrator/src/sonarqube.ts +++ b/tools/orchestrator/src/sonarqube.ts @@ -1,6 +1,6 @@ /* * sonar-scanner-npm - * Copyright (C) 2022-2023 SonarSource SA + * Copyright (C) 2022-2024 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..3285124e --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,111 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Node" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "build" /* Specify an output folder for all emitted files. */, + "removeComments": false /* Disable emitting comments. */, + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "src/**/types.ts"] +} From 1ed6ea63782b2ab0f9c6c7e3db01c4e168a82ffa Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:02:07 +0200 Subject: [PATCH 02/35] SCANNPM-2 Detect Platform (#110) --- jest.config.js | 6 +- package-lock.json | 94 +++++++++++++++++++++++++++-- package.json | 12 ++-- src/bin/sonar-scanner | 2 +- src/logging.ts | 2 +- src/platform.ts | 65 ++++++++++++++++++++ src/scan.ts | 22 +++---- src/types.ts | 12 +++- test/unit/platform.test.ts | 119 +++++++++++++++++++++++++++++++++++++ 9 files changed, 309 insertions(+), 25 deletions(-) create mode 100644 src/platform.ts create mode 100644 test/unit/platform.test.ts diff --git a/jest.config.js b/jest.config.js index 4d53e1d4..30d74bc8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -19,12 +19,14 @@ */ module.exports = { - collectCoverageFrom: ['src/**/*.js'], + preset: 'ts-jest', + testEnvironment: 'node', + collectCoverageFrom: ['src/**/*.{js,ts}'], coverageReporters: ['lcov', 'text'], coveragePathIgnorePatterns: ['.fixture.', '/fixtures/'], moduleFileExtensions: ['js', 'ts', 'json'], moduleDirectories: ['node_modules'], testResultsProcessor: 'jest-sonar-reporter', - testMatch: ['/test/unit/**/*.test.js'], + testMatch: ['/test/unit/**/*.test.{js,ts}'], testTimeout: 20000, }; diff --git a/package-lock.json b/package-lock.json index 2c41f39a..dc289130 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,12 +22,13 @@ "sonar-scanner": "src/bin/sonar-scanner" }, "devDependencies": { - "@types/adm-zip": "^0.5.5", - "@types/fs-extra": "^11.0.4", + "@types/adm-zip": "0.5.5", + "@types/fs-extra": "11.0.4", "@types/jest": "29.5.12", - "@types/proxy-from-env": "^1.0.4", - "@types/semver": "^7.5.8", - "@types/tar-stream": "^3.1.3", + "@types/proxy-from-env": "1.0.4", + "@types/semver": "7.5.8", + "@types/sinon": "17.0.3", + "@types/tar-stream": "3.1.3", "@typescript-eslint/parser": "7.4.0", "chai": "4.4.1", "eslint": "8.57.0", @@ -38,6 +39,7 @@ "pretty-quick": "4.0.0", "rimraf": "5.0.5", "sinon": "17.0.1", + "ts-jest": "29.1.2", "typescript": "5.4.3" }, "engines": { @@ -1419,6 +1421,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "dev": true, @@ -1855,6 +1872,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "dev": true, @@ -3754,6 +3783,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "dev": true, @@ -3789,6 +3824,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/makeerror": { "version": "1.0.12", "dev": true, @@ -4808,6 +4849,49 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.2.tgz", + "integrity": "sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, "node_modules/tslib": { "version": "2.6.2", "dev": true, diff --git a/package.json b/package.json index aaf88580..c1a9fe8b 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,13 @@ "tar-stream": "3.1.7" }, "devDependencies": { - "@types/adm-zip": "^0.5.5", - "@types/fs-extra": "^11.0.4", + "@types/adm-zip": "0.5.5", + "@types/fs-extra": "11.0.4", "@types/jest": "29.5.12", - "@types/proxy-from-env": "^1.0.4", - "@types/semver": "^7.5.8", - "@types/tar-stream": "^3.1.3", + "@types/proxy-from-env": "1.0.4", + "@types/semver": "7.5.8", + "@types/sinon": "17.0.3", + "@types/tar-stream": "3.1.3", "@typescript-eslint/parser": "7.4.0", "chai": "4.4.1", "eslint": "8.57.0", @@ -49,6 +50,7 @@ "pretty-quick": "4.0.0", "rimraf": "5.0.5", "sinon": "17.0.1", + "ts-jest": "29.1.2", "typescript": "5.4.3" }, "keywords": [ diff --git a/src/bin/sonar-scanner b/src/bin/sonar-scanner index c9fcb83d..aa551baa 100755 --- a/src/bin/sonar-scanner +++ b/src/bin/sonar-scanner @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const scan = require('../../build/src/index').scan; +const scan = require('../../build/index').scan; const options = process.argv.length > 2 ? process.argv.slice(2) : []; diff --git a/src/logging.ts b/src/logging.ts index 5701a02e..f07d7c8a 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -40,7 +40,7 @@ let logLevel = DEFAULT_LOG_LEVEL; export function log(level: LogLevel, ...message: unknown[]) { if (logLevelValues[level] <= logLevelValues[logLevel]) { - console.log(`[${level}] Bootstrapper ${message}`); + console.log(`[${level}] Bootstrapper:: `, ...message); } } diff --git a/src/platform.ts b/src/platform.ts new file mode 100644 index 00000000..a3857621 --- /dev/null +++ b/src/platform.ts @@ -0,0 +1,65 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import fs from 'fs'; +import { LogLevel, log } from './logging'; +import { PlatformInfo, SupportedOS } from './types'; + +export function getArch(): NodeJS.Architecture { + return process.arch; +} + +function isLinux(): boolean { + return process.platform.startsWith('linux'); +} + +/** + * @see https://github.com/microsoft/vscode/blob/64874113ad3c59e8d045f75dc2ef9d33d13f3a03/src/vs/platform/extensionManagement/common/extensionManagementUtil.ts#L171C1-L190C1 + */ + +function isAlpineLinux(): boolean { + if (!isLinux()) { + return false; + } + let content: string | undefined; + try { + const fileContent = fs.readFileSync('/etc/os-release'); + content = fileContent.toString(); + } catch (error) { + try { + const fileContent = fs.readFileSync('/usr/lib/os-release'); + content = fileContent.toString(); + } catch (error) { + log(LogLevel.WARN, 'Failed to read /etc/os-release or /usr/lib/os-release'); + } + } + return !!content && (content.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] === 'alpine'; +} + +function getSupportedOS(): SupportedOS { + return isAlpineLinux() ? 'alpine' : process.platform; +} + +export function getPlatformInfo(): PlatformInfo { + return { + os: getSupportedOS(), + arch: getArch(), + }; +} diff --git a/src/scan.ts b/src/scan.ts index 4f063bc4..968e3fcb 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -18,16 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export type ScanOptions = { - serverUrl: string; - token: string; - jvmOptions: string[]; - options?: { [key: string]: string }; - caPath: string; - logLevel?: string; - verbose?: boolean; -}; +import { log, LogLevel } from './logging'; +import { getPlatformInfo } from './platform'; +import { ScanOptions } from './types'; export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { - // TODO: NPMSCAN-2 new bootstrapper sequence + log(LogLevel.DEBUG, 'Finding platform info'); + const platformInfo = getPlatformInfo(); + log(LogLevel.INFO, 'Platform: ', platformInfo); + + //TODO: verifyJRE based on platform + //TODO: fetchJRE + //TODO: verifyScannerEngine + //TODO: fetchScannerEngine + //TODO: } diff --git a/src/types.ts b/src/types.ts index fc7e7c44..8738fcd1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,7 +19,7 @@ */ import { LogLevel } from './logging'; -export type SupportedOS = 'windows' | 'linux' | 'alpine' | 'macos' | 'aix'; +export type SupportedOS = NodeJS.Platform | 'alpine'; export type PlatformInfo = { os: SupportedOS | null; @@ -39,3 +39,13 @@ export type ScannerLogEntry = { }; export type ScannerParams = { [key: string]: string }; + +export type ScanOptions = { + serverUrl: string; + token: string; + jvmOptions: string[]; + options?: { [key: string]: string }; + caPath: string; + logLevel?: string; + verbose?: boolean; +}; diff --git a/test/unit/platform.test.ts b/test/unit/platform.test.ts new file mode 100644 index 00000000..0696c4cf --- /dev/null +++ b/test/unit/platform.test.ts @@ -0,0 +1,119 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import * as platform from '../../src/platform'; +import * as logging from '../../src/logging'; +import fs from 'fs'; +import sinon from 'sinon'; + +describe('getPlatformInfo', () => { + it('detect macos', () => { + const platformStub = sinon.stub(process, 'platform').value('darwin'); + const archStub = sinon.stub(process, 'arch').value('arm64'); + + expect(platform.getPlatformInfo()).toEqual({ + os: 'darwin', + arch: 'arm64', + }); + + platformStub.restore(); + archStub.restore(); + }); + + it('detect windows', () => { + const platformStub = sinon.stub(process, 'platform').value('win32'); + const archStub = sinon.stub(process, 'arch').value('x64'); + + expect(platform.getPlatformInfo()).toEqual({ + os: 'win32', + arch: 'x64', + }); + + platformStub.restore(); + archStub.restore(); + }); + + it('detect linux flavor', () => { + const platformStub = sinon.stub(process, 'platform').value('openbsd'); + const archStub = sinon.stub(process, 'arch').value('x64'); + + expect(platform.getPlatformInfo()).toEqual({ + os: 'openbsd', + arch: 'x64', + }); + + platformStub.restore(); + archStub.restore(); + }); + + it('detect alpine', () => { + const platformStub = sinon.stub(process, 'platform').value('linux'); + const archStub = sinon.stub(process, 'arch').value('x64'); + const fsReadStub = sinon.stub(fs, 'readFileSync'); + fsReadStub.withArgs('/etc/os-release').returns('NAME="Alpine Linux"\nID=alpine'); + + expect(platform.getPlatformInfo()).toEqual({ + os: 'alpine', + arch: 'x64', + }); + + platformStub.restore(); + archStub.restore(); + fsReadStub.restore(); + }); + + it('detect alpine with fallback', () => { + const platformStub = sinon.stub(process, 'platform').value('linux'); + const archStub = sinon.stub(process, 'arch').value('x64'); + const fsReadStub = sinon.stub(fs, 'readFileSync'); + fsReadStub.withArgs('/usr/lib/os-release').returns('NAME="Alpine Linux"\nID=alpine'); + + expect(platform.getPlatformInfo()).toEqual({ + os: 'alpine', + arch: 'x64', + }); + + platformStub.restore(); + archStub.restore(); + fsReadStub.restore(); + }); + + it('failed to detect alpine', () => { + const logSpy = sinon.spy(logging, 'log'); + const platformStub = sinon.stub(process, 'platform').value('linux'); + const archStub = sinon.stub(process, 'arch').value('x64'); + + expect(platform.getPlatformInfo()).toEqual({ + os: 'linux', + arch: 'x64', + }); + + expect( + logSpy.calledWith( + logging.LogLevel.ERROR, + 'Failed to read /etc/os-release or /usr/lib/os-release', + ), + ).toBe(true); + + platformStub.restore(); + archStub.restore(); + logSpy.restore(); + }); +}); From 1e20e0f71b22add7ebed35069e9b2820e722ce97 Mon Sep 17 00:00:00 2001 From: 7PH Date: Fri, 12 Apr 2024 16:00:22 +0200 Subject: [PATCH 03/35] SCANNPM-2 Handle logic to process scanner properties from various sources with priority --- package-lock.json | 12 +- package.json | 4 +- src/constants.ts | 15 + src/properties.ts | 321 ++++++++++++ src/scan.ts | 15 +- src/types.ts | 23 +- .../sonar-project.properties | 4 +- test/unit/mocks/FakeProjectMock.ts | 62 +++ test/unit/properties.test.ts | 480 ++++++++++++++++++ 9 files changed, 926 insertions(+), 10 deletions(-) create mode 100644 src/properties.ts create mode 100644 test/unit/mocks/FakeProjectMock.ts create mode 100644 test/unit/properties.test.ts diff --git a/package-lock.json b/package-lock.json index dc289130..a31993bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,9 @@ "jest-sonar-reporter": "2.0.0", "proxy-from-env": "1.1.0", "semver": "7.6.0", - "tar-stream": "3.1.7" + "slugify": "1.6.6", + "tar-stream": "3.1.7", + "ts-jest": "^29.1.2" }, "bin": { "sonar-scanner": "src/bin/sonar-scanner" @@ -4604,6 +4606,14 @@ "node": ">=8" } }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "dev": true, diff --git a/package.json b/package.json index c1a9fe8b..0b14efa3 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,9 @@ "jest-sonar-reporter": "2.0.0", "proxy-from-env": "1.1.0", "semver": "7.6.0", - "tar-stream": "3.1.7" + "slugify": "1.6.6", + "tar-stream": "3.1.7", + "ts-jest": "^29.1.2" }, "devDependencies": { "@types/adm-zip": "0.5.5", diff --git a/src/constants.ts b/src/constants.ts index a42633e5..729030b6 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import path from 'path'; +import { ScannerProperty } from './types'; export const SCANNER_BOOTSTRAPPER_NAME = 'ScannerNpm'; @@ -33,3 +34,17 @@ export const SONAR_CACHE_DIR = path.join( ); export const UNARCHIVE_SUFFIX = '_extracted'; + +export const ENV_VAR_PREFIX = 'SONAR_SCANNER_'; + +export const ENV_TO_PROPERTY_NAME: [string, ScannerProperty][] = [ + ['SONAR_TOKEN', ScannerProperty.SonarToken], + ['SONAR_HOST_URL', ScannerProperty.SonarHostUrl], + ['SONAR_USER_HOME', ScannerProperty.SonarUserHome], + ['SONAR_ORGANIZATION', ScannerProperty.SonarOrganization], +]; + +export const SONAR_PROJECT_FILENAME = 'sonar-project.properties'; + +export const DEFAULT_SONAR_EXCLUSIONS = + 'node_modules/**,bower_components/**,jspm_packages/**,typings/**,lib-cov/**'; diff --git a/src/properties.ts b/src/properties.ts new file mode 100644 index 00000000..dbc190da --- /dev/null +++ b/src/properties.ts @@ -0,0 +1,321 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import fs from 'fs'; +import path from 'path'; +import slugify from 'slugify'; +import { version } from '../package.json'; +import { + DEFAULT_SONAR_EXCLUSIONS, + ENV_TO_PROPERTY_NAME, + ENV_VAR_PREFIX, + SCANNER_BOOTSTRAPPER_NAME, + SONAR_PROJECT_FILENAME, +} from './constants'; +import { LogLevel, log } from './logging'; +import { ScanOptions, ScannerProperties, ScannerProperty } from './types'; + +/** + * Convert the name of a sonar property from its environment variable form + * (eg SONAR_SCANNER_FOO_BAR) to its sonar form (eg sonar.scanner.fooBar). + */ +function envNameToSonarPropertyNameMapper(envName: string) { + // Extract the name and convert to camel case + const sonarScannerKey = envName + .substring(ENV_VAR_PREFIX.length) + .toLowerCase() + .replace(/_([a-z])/g, g => g[1].toUpperCase()); + return `sonar.scanner.${sonarScannerKey}`; +} + +/** + * Build the config. + */ +function getPackageJsonProperties( + projectBaseDir: string, + sonarBaseExclusions: string, +): ScannerProperties { + const packageJsonParams: { [key: string]: string } = {}; + const packageFile = path.join(projectBaseDir, 'package.json'); + let packageData; + try { + packageData = fs.readFileSync(packageFile).toString(); + } catch (error) { + log(LogLevel.INFO, `Unable to read "package.json" file`); + return { + 'sonar.exclusions': sonarBaseExclusions, + }; + } + const pkg = JSON.parse(packageData); + log(LogLevel.INFO, 'Retrieving info from "package.json" file'); + + function fileExistsInProjectSync(file: string) { + return fs.existsSync(path.resolve(projectBaseDir, file)); + } + + function dependenceExists(pkgName: string) { + return ['devDependencies', 'dependencies', 'peerDependencies'].some(function (prop) { + return pkg[prop] && pkgName in pkg[prop]; + }); + } + + if (pkg) { + const invalidCharacterRegex = /[?$*+~.()'"!:@/]/g; + packageJsonParams['sonar.projectKey'] = slugify(pkg.name, { + remove: invalidCharacterRegex, + }); + packageJsonParams['sonar.projectName'] = pkg.name; + packageJsonParams['sonar.projectVersion'] = pkg.version; + if (pkg.description) { + packageJsonParams['sonar.projectDescription'] = pkg.description; + } + if (pkg.homepage) { + packageJsonParams['sonar.links.homepage'] = pkg.homepage; + } + if (pkg.bugs?.url) { + packageJsonParams['sonar.links.issue'] = pkg.bugs.url; + } + if (pkg.repository?.url) { + packageJsonParams['sonar.links.scm'] = pkg.repository.url; + } + + const potentialCoverageDirs = [ + // jest coverage output directory + // See: http://facebook.github.io/jest/docs/en/configuration.html#coveragedirectory-string + pkg['nyc']?.['report-dir'], + // nyc coverage output directory + // See: https://github.com/istanbuljs/nyc#configuring-nyc + pkg['jest']?.['coverageDirectory'], + ] + .filter(Boolean) + .concat( + // default coverage output directory + 'coverage', + ); + const uniqueCoverageDirs = Array.from(new Set(potentialCoverageDirs)); + packageJsonParams['sonar.exclusions'] = sonarBaseExclusions; + for (const lcovReportDir of uniqueCoverageDirs) { + const lcovReportPath = path.posix.join(lcovReportDir, 'lcov.info'); + if (fileExistsInProjectSync(lcovReportPath)) { + packageJsonParams['sonar.exclusions'] += + (packageJsonParams['sonar.exclusions'].length > 0 ? ',' : '') + + path.posix.join(lcovReportDir, '**'); + // https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/ + packageJsonParams['sonar.javascript.lcov.reportPaths'] = lcovReportPath; + // TODO: use Generic Test Data to remove dependence of SonarJS, it is need transformation lcov to sonar generic coverage format + } + } + + if (dependenceExists('mocha-sonarqube-reporter') && fileExistsInProjectSync('xunit.xml')) { + // https://docs.sonarqube.org/display/SONAR/Generic+Test+Data + packageJsonParams['sonar.testExecutionReportPaths'] = 'xunit.xml'; + } + // TODO: (SCANNPM-13) use `glob` to lookup xunit format files and transformation to sonar generic report format + } + return packageJsonParams; +} + +/** + * Convert CLI args into scanner properties. + */ +function getCommandLineProperties(cliArgs?: string[]): ScannerProperties { + if (!cliArgs || cliArgs.length === 0) { + return {}; + } + + // Parse CLI args (eg: -Dsonar.token=xxx) + const properties: ScannerProperties = {}; + for (const arg of cliArgs) { + if (!arg.startsWith('-D')) { + continue; + } + const [key, value] = arg.substring(2).split('='); + properties[key] = value; + } + + return properties; +} + +/** + * Parse properties stored in sonar project properties file, if it exists. + */ +function getSonarFileProperties(projectBaseDir: string): ScannerProperties { + // Read sonar project properties file in project base dir + try { + const sonarPropertiesFile = path.join(projectBaseDir, SONAR_PROJECT_FILENAME); + const properties: ScannerProperties = {}; + const data = fs.readFileSync(sonarPropertiesFile).toString(); + const lines = data.split(/\r?\n/); + for (const line of lines) { + const trimmedLine = line.trim(); + if (trimmedLine.length === 0 || trimmedLine.startsWith('#')) { + continue; + } + const [key, value] = trimmedLine.split('='); + properties[key] = value; + } + + return properties; + } catch (error: any) { + log(LogLevel.WARN, `Failed to read ${SONAR_PROJECT_FILENAME} file: ${error.message}`); + throw error; + } +} + +/** + * Get scanner properties from scan option object (JS API). + */ +function getScanOptionsProperties(scanOptions: ScanOptions): ScannerProperties { + const options = { + ...scanOptions.options, + }; + + if (typeof scanOptions.serverUrl !== 'undefined') { + options[ScannerProperty.SonarHostUrl] = scanOptions.serverUrl; + } + + if (typeof scanOptions.token !== 'undefined') { + options[ScannerProperty.SonarToken] = scanOptions.token; + } + + if (typeof scanOptions.verbose !== 'undefined') { + options[ScannerProperty.SonarVerbose] = scanOptions.verbose ? 'true' : 'false'; + } + + return options; +} + +/** + * Automatically parse properties from environment variables. + */ +export function getEnvironmentProperties() { + const { env } = process; + + const jsonEnvVariables = ['SONAR_SCANNER_JSON_PARAMS', 'SONARQUBE_SCANNER_PARAMS']; + + let properties: ScannerProperties = {}; + + // Get known environment variables + for (const [envName, scannerProperty] of ENV_TO_PROPERTY_NAME) { + if (envName in env) { + const envValue = env[envName]; + + if (typeof envValue !== 'undefined') { + properties[scannerProperty] = envValue; + } + } + } + + // Get generic environment variables + properties = { + ...properties, + ...Object.fromEntries( + Object.entries(env) + .filter(([key]) => key.startsWith(ENV_VAR_PREFIX)) + .filter(([key]) => !jsonEnvVariables.includes(key)) + .map(([key, value]) => [envNameToSonarPropertyNameMapper(key), value as string]), + ), + }; + + // Get JSON parameters from env + try { + const jsonParams = env.SONAR_SCANNER_JSON_PARAMS ?? env.SONARQUBE_SCANNER_PARAMS; + if (jsonParams) { + properties = { + ...JSON.parse(jsonParams), + ...properties, + }; + } + if (!env.SONAR_SCANNER_JSON_PARAMS && env.SONARQUBE_SCANNER_PARAMS) { + log( + LogLevel.WARN, + 'SONARQUBE_SCANNER_PARAMS is deprecated, please use SONAR_SCANNER_JSON_PARAMS instead', + ); + } + } catch (e) { + log(LogLevel.WARN, `Failed to parse JSON parameters from ENV: ${e}`); + } + + return properties; +} + +/** + * Get bootstrapper properties, that can not be overridden. + */ +function getBootstrapperProperties(startTimestampMs: number): ScannerProperties { + return { + 'sonar.scanner.app': SCANNER_BOOTSTRAPPER_NAME, + 'sonar.scanner.appVersion': version, + 'sonar.scanner.bootstrapStartTime': startTimestampMs.toString(), + // Bootstrap cache hit/miss is set later after the bootstrapper has run and before scanner engine is started + 'sonar.scanner.wasJreCacheHit': 'false', + 'sonar.scanner.wasEngineCacheHit': 'false', + }; +} + +export function getProperties( + scanOptions: ScanOptions, + startTimestampMs: number, + cliArgs?: string[], +): ScannerProperties { + const bootstrapperProperties = getBootstrapperProperties(startTimestampMs); + const cliProperties = getCommandLineProperties(cliArgs); + const scanOptionsProperties = getScanOptionsProperties(scanOptions); + const envProperties = getEnvironmentProperties(); + + // Compute default base dir respecting order of precedence we use for the final merge + const projectBaseDir = + cliProperties[ScannerProperty.SonarProjectBaseDir] ?? + scanOptionsProperties[ScannerProperty.SonarProjectBaseDir] ?? + envProperties[ScannerProperty.SonarProjectBaseDir] ?? + process.cwd(); + + let inferredProperties: ScannerProperties; + try { + inferredProperties = getSonarFileProperties(projectBaseDir); + } catch (error) { + inferredProperties = { + 'sonar.projectDescription': 'No description.', + 'sonar.sources': '.', + }; + + const baseSonarExclusions = + cliProperties[ScannerProperty.SonarExclusions] ?? + scanOptionsProperties[ScannerProperty.SonarExclusions] ?? + envProperties[ScannerProperty.SonarExclusions] ?? + DEFAULT_SONAR_EXCLUSIONS; + + inferredProperties = { + ...inferredProperties, + ...getPackageJsonProperties(projectBaseDir, baseSonarExclusions), + }; + } + + // Merge properties respecting order of precedence + return [ + { 'sonar.projectBaseDir': projectBaseDir }, // Manually computed, can't be overridden + bootstrapperProperties, // Can't be overridden + cliProperties, // Highest precedence + scanOptionsProperties, + inferredProperties, + envProperties, // Lowest precedence + ] + .reverse() + .reduce((acc, curr) => ({ ...acc, ...curr }), {}); +} diff --git a/src/scan.ts b/src/scan.ts index 968e3fcb..866512f6 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -18,11 +18,19 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { log, LogLevel } from './logging'; +import { log, LogLevel, setLogLevel } from './logging'; import { getPlatformInfo } from './platform'; -import { ScanOptions } from './types'; +import { getProperties } from './properties'; +import { ScannerProperty, ScanOptions } from './types'; export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { + const startTimestampMs = Date.now(); + const properties = getProperties(scanOptions, startTimestampMs, cliArgs); + if (properties[ScannerProperty.SonarVerbose] === 'true') { + setLogLevel(LogLevel.DEBUG); + log(LogLevel.DEBUG, 'Setting the log level to DEBUG due to verbose mode'); + } + log(LogLevel.DEBUG, 'Finding platform info'); const platformInfo = getPlatformInfo(); log(LogLevel.INFO, 'Platform: ', platformInfo); @@ -32,4 +40,7 @@ export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { //TODO: verifyScannerEngine //TODO: fetchScannerEngine //TODO: + // ... + properties[ScannerProperty.SonarScannerWasEngineCacheHit] = 'false'; + // ... } diff --git a/src/types.ts b/src/types.ts index 8738fcd1..5c557598 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,14 +38,27 @@ export type ScannerLogEntry = { throwable?: string; }; -export type ScannerParams = { [key: string]: string }; +export enum ScannerProperty { + SonarVerbose = 'sonar.verbose', + SonarToken = 'sonar.token', + SonarExclusions = 'sonar.exclusions', + SonarHostUrl = 'sonar.host.url', + SonarUserHome = 'sonar.userHome', + SonarOrganization = 'sonar.organization', + SonarProjectBaseDir = 'sonar.projectBaseDir', + SonarScannerWasEngineCacheHit = 'sonar.scanner.wasEngineCacheHit', +} + +export type ScannerProperties = { + [key: string]: string; +}; export type ScanOptions = { - serverUrl: string; - token: string; - jvmOptions: string[]; + serverUrl?: string; + token?: string; + jvmOptions?: string[]; options?: { [key: string]: string }; - caPath: string; + caPath?: string; logLevel?: string; verbose?: boolean; }; diff --git a/test/unit/fixtures/fake_project_with_sonar_properties_file/sonar-project.properties b/test/unit/fixtures/fake_project_with_sonar_properties_file/sonar-project.properties index 1df2ec1b..cd40123a 100644 --- a/test/unit/fixtures/fake_project_with_sonar_properties_file/sonar-project.properties +++ b/test/unit/fixtures/fake_project_with_sonar_properties_file/sonar-project.properties @@ -1,4 +1,6 @@ sonar.projectKey=foo sonar.projectVersion=1.0-SNAPSHOT sonar.projectName=Foo -sonar.sources=. \ No newline at end of file + +#sonar.token=ignore-this +sonar.sources=the-sources diff --git a/test/unit/mocks/FakeProjectMock.ts b/test/unit/mocks/FakeProjectMock.ts new file mode 100644 index 00000000..6a611b53 --- /dev/null +++ b/test/unit/mocks/FakeProjectMock.ts @@ -0,0 +1,62 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import path from 'path'; +import { SCANNER_BOOTSTRAPPER_NAME } from '../../../src/constants'; + +const baseEnvVariables = process.env; + +export class FakeProjectMock { + static getPathForProject(projectName: string) { + return path.join(__dirname, '../', 'fixtures', projectName); + } + + private projectPath: string = ''; + + private startTimeMs = 1713164095650; + + reset(projectName?: string) { + if (projectName) { + this.projectPath = FakeProjectMock.getPathForProject(projectName); + } else { + this.projectPath = ''; + } + process.env = baseEnvVariables; + process.cwd = () => this.projectPath; + } + + setEnvironmentVariables(values: { [key: string]: string }) { + process.env = values; + } + + getStartTime() { + return this.startTimeMs; + } + + getExpectedProperties() { + return { + 'sonar.projectBaseDir': this.projectPath, + 'sonar.scanner.bootstrapStartTime': this.startTimeMs.toString(), + 'sonar.scanner.app': SCANNER_BOOTSTRAPPER_NAME, + 'sonar.scanner.appVersion': '1.2.3', + 'sonar.scanner.wasEngineCacheHit': 'false', + 'sonar.scanner.wasJreCacheHit': 'false', + }; + } +} diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts new file mode 100644 index 00000000..31b8ce1e --- /dev/null +++ b/test/unit/properties.test.ts @@ -0,0 +1,480 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { DEFAULT_SONAR_EXCLUSIONS, SCANNER_BOOTSTRAPPER_NAME } from '../../src/constants'; +import { LogLevel, log } from '../../src/logging'; +import { getProperties } from '../../src/properties'; +import { FakeProjectMock } from './mocks/FakeProjectMock'; + +jest.mock('../../src/logging'); + +jest.mock('../../package.json', () => ({ + version: '1.2.3', +})); + +const projectHandler = new FakeProjectMock(); + +afterEach(() => { + projectHandler.reset(); +}); + +describe('getProperties', () => { + describe('should handle JS API scan options params correctly', () => { + it('should detect and use user-provided scan option params', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + token: 'dummy-token', + verbose: true, + options: { + 'sonar.projectKey': 'use-this-project-key', + }, + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.token': 'dummy-token', + 'sonar.verbose': 'true', + 'sonar.projectKey': 'use-this-project-key', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + }); + }); + + it('should not set verbose mode when explicitly turned off', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + verbose: false, + }, + projectHandler.getStartTime(), + ); + + expect(properties['sonar.verbose']).toBe('false'); + }); + }); + + describe('should handle package.json correctly', () => { + it('should generate default properties with package.json', () => { + projectHandler.reset('fake_project_with_basic_package_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.javascript.lcov.reportPaths': 'coverage/lcov.info', + 'sonar.projectKey': 'fake-basic-project', + 'sonar.projectName': 'fake-basic-project', + 'sonar.projectDescription': 'No description.', + 'sonar.projectVersion': '1.0.0', + 'sonar.sources': '.', + 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS + ',coverage/**', + 'sonar.scanner.app': SCANNER_BOOTSTRAPPER_NAME, + 'sonar.scanner.appVersion': '1.2.3', + }); + }); + + it('should use all available information from package.json', () => { + projectHandler.reset('fake_project_with_complete_package_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.projectKey': 'fake-project', + 'sonar.projectName': 'fake-project', + 'sonar.projectDescription': 'A fake project', + 'sonar.projectVersion': '1.0.0', + 'sonar.links.homepage': 'https://github.com/fake/project', + 'sonar.links.issue': 'https://github.com/fake/project/issues', + 'sonar.links.scm': 'git+https://github.com/fake/project.git', + 'sonar.sources': '.', + 'sonar.testExecutionReportPaths': 'xunit.xml', + 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, + }); + }); + + it('should allow package.json not to exist', () => { + projectHandler.reset('fake_project_with_no_package_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.projectDescription': 'No description.', + 'sonar.sources': '.', + 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, + }); + }); + + it('should slugify scoped package names', () => { + projectHandler.reset('fake_project_with_scoped_package_name'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.projectDescription': 'No description.', + 'sonar.sources': '.', + 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, + 'sonar.projectKey': 'myfake-basic-project', + 'sonar.projectName': '@my/fake-basic-project', + 'sonar.projectVersion': '1.0.0', + }); + }); + + it('should detect jest report file', () => { + projectHandler.reset('fake_project_with_jest_report_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.javascript.lcov.reportPaths': 'jest-coverage/lcov.info', + 'sonar.projectKey': 'fake-basic-project', + 'sonar.projectName': 'fake-basic-project', + 'sonar.projectDescription': 'No description.', + 'sonar.projectVersion': '1.0.0', + 'sonar.sources': '.', + 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS + ',jest-coverage/**', + }); + }); + + it('should detect nyc report file', () => { + projectHandler.reset('fake_project_with_nyc_report_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.javascript.lcov.reportPaths': 'nyc-coverage/lcov.info', + 'sonar.projectKey': 'fake-basic-project', + 'sonar.projectName': 'fake-basic-project', + 'sonar.projectDescription': 'No description.', + 'sonar.projectVersion': '1.0.0', + 'sonar.sources': '.', + 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS + ',nyc-coverage/**', + }); + }); + }); + + describe('should handle sonar-project.properties correctly', () => { + it('should parse sonar-project.properties properly', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + }, + projectHandler.getStartTime(), + ); + + expect(properties['sonar.token']).toBeUndefined(); + }); + it('should not set default values if sonar-project.properties file exists', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + }); + }); + }); + + describe('should handle environment variables', () => { + it('should detect known environment variables', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONAR_TOKEN: 'my-token', + SONAR_HOST_URL: 'https://sonarqube.com/', + SONAR_USER_HOME: '/tmp/.sonar/', + SONAR_ORGANIZATION: 'my-org', + }); + + const properties = getProperties({}, projectHandler.getStartTime()); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + 'sonar.host.url': 'https://sonarqube.com/', + 'sonar.token': 'my-token', + 'sonar.userHome': '/tmp/.sonar/', + 'sonar.organization': 'my-org', + }); + }); + + it('should detect generic environment variables', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONAR_SCANNER_SOME_VAR: 'some-value', + }); + + const properties = getProperties({}, projectHandler.getStartTime()); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + 'sonar.scanner.someVar': 'some-value', + }); + }); + + it('should use SONAR_SCANNER_JSON_PARAMS', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONAR_SCANNER_JSON_PARAMS: JSON.stringify({ + 'sonar.token': 'this-is-another-token', + }), + }); + + const properties = getProperties({}, projectHandler.getStartTime()); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + 'sonar.token': 'this-is-another-token', + }); + }); + + it('should not throw if SONAR_SCANNER_JSON_PARAMS is incorrectly formatted', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONAR_SCANNER_JSON_PARAMS: 'this is def not JSON', + }); + + const properties = getProperties({}, projectHandler.getStartTime()); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + }); + expect(log).toHaveBeenLastCalledWith( + LogLevel.WARN, + expect.stringMatching(/Failed to parse JSON parameters/), + ); + }); + + it('should use deprecated SONARQUBE_SCANNER_PARAMS', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONARQUBE_SCANNER_PARAMS: JSON.stringify({ + 'sonar.token': 'this-is-another-token', + }), + }); + + const properties = getProperties({}, projectHandler.getStartTime()); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + 'sonar.token': 'this-is-another-token', + }); + expect(log).toHaveBeenCalledWith( + LogLevel.WARN, + 'SONARQUBE_SCANNER_PARAMS is deprecated, please use SONAR_SCANNER_JSON_PARAMS instead', + ); + }); + }); + + describe('should handle command line properties', () => { + it('should use command line properties', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + }, + projectHandler.getStartTime(), + ['-Dsonar.token=my-token', '-javaagent:/ignored-value.jar'], + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + 'sonar.token': 'my-token', + }); + }); + }); + + describe('should handle priorities properly', () => { + it('priority should respect CLI > Project conf > Global conf > Env', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONAR_TOKEN: 'ignored', + SONAR_HOST_URL: 'http://ignored', + SONAR_USER_HOME: '/tmp/used', + SONAR_ORGANIZATION: 'ignored', + SONAR_SCANNER_JSON_PARAMS: JSON.stringify({ + 'sonar.userHome': 'ignored', + 'sonar.scanner.someVar': 'used', + }), + }); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + options: { + 'sonar.token': 'ignored', + 'sonar.organization': 'used', + }, + }, + projectHandler.getStartTime(), + ['-Dsonar.token=only-this-will-be-used'], + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + 'sonar.token': 'only-this-will-be-used', + 'sonar.userHome': '/tmp/used', + 'sonar.organization': 'used', + 'sonar.scanner.someVar': 'used', + }); + }); + + it('does not let user override bootstrapper-only properties', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONAR_SCANNER_APP: 'ignored', + SONAR_SCANNER_APP_VERSION: '3.2.1', + SONAR_SCANNER_WAS_JRE_CACHE_HIT: 'true', + SONAR_SCANNER_WAS_ENGINE_CACHE_HIT: 'true', + SONAR_SCANNER_JSON_PARAMS: JSON.stringify({ + 'sonar.scanner.app': 'ignored', + 'sonar.scanner.appVersion': 'ignored', + 'sonar.scanner.bootstrapStartTime': '0000', + 'sonar.scanner.wasJreCacheHit': 'true', + 'sonar.scanner.wasEngineCacheHit': 'true', + }), + }); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + options: { + 'sonar.scanner.app': 'ignored', + 'sonar.scanner.appVersion': 'ignored', + 'sonar.scanner.bootstrapStartTime': '0000', + 'sonar.scanner.wasJreCacheHit': 'true', + 'sonar.scanner.wasEngineCacheHit': 'true', + }, + }, + projectHandler.getStartTime(), + ['-Dsonar.scanner.app=ignored'], + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + }); + }); + }); +}); From 5c6a25552c45c180cc8283a20d7295cc2d6060e2 Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:12:30 +0200 Subject: [PATCH 04/35] SCANNPM-2 Implement logic to fetch JRE (#112) Co-authored-by: 7PH --- .gitignore | 4 + src/bin/sonar-scanner | 2 +- src/constants.ts | 10 ++ src/java.ts | 301 ++++++++++++++++++++++++++++++++++ src/request.ts | 29 ++++ src/scan.ts | 35 +++- src/types.ts | 7 + test/unit/java.test.ts | 136 +++++++++++++++ test/unit/mocks/ServerMock.ts | 68 ++++++++ test/unit/scan.test.ts | 93 +++++++++++ tsconfig.json | 4 +- 11 files changed, 683 insertions(+), 6 deletions(-) create mode 100644 src/java.ts create mode 100644 src/request.ts create mode 100644 test/unit/java.test.ts create mode 100644 test/unit/mocks/ServerMock.ts create mode 100644 test/unit/scan.test.ts diff --git a/.gitignore b/.gitignore index d984fc9a..4643056e 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,10 @@ fabric.properties .idea/ + +### VS Code ### +.vscode/ + ### Node ### # Logs logs diff --git a/src/bin/sonar-scanner b/src/bin/sonar-scanner index aa551baa..c9fcb83d 100755 --- a/src/bin/sonar-scanner +++ b/src/bin/sonar-scanner @@ -18,7 +18,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const scan = require('../../build/index').scan; +const scan = require('../../build/src/index').scan; const options = process.argv.length > 2 ? process.argv.slice(2) : []; diff --git a/src/constants.ts b/src/constants.ts index 729030b6..434438a1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -22,9 +22,15 @@ import { ScannerProperty } from './types'; export const SCANNER_BOOTSTRAPPER_NAME = 'ScannerNpm'; +export const SONARCLOUD_URL = 'https://sonarcloud.io'; + +export const SONARCLOUD_URL_REGEX = /^(https?:\/\/)?(www\.)?(sonarcloud\.io)/; + export const SONARCLOUD_ENV_REGEX = /^(https?:\/\/)?(www\.)?([a-zA-Z0-9-]+\.)?(sc-dev\.io|sc-staging\.io|sonarcloud\.io)/; +export const SONARCLOUD_PRODUCTION_URL = 'https://sonarcloud.io'; + export const SONARQUBE_JRE_PROVISIONING_MIN_VERSION = '10.6'; export const SONAR_CACHE_DIR = path.join( @@ -48,3 +54,7 @@ export const SONAR_PROJECT_FILENAME = 'sonar-project.properties'; export const DEFAULT_SONAR_EXCLUSIONS = 'node_modules/**,bower_components/**,jspm_packages/**,typings/**,lib-cov/**'; + +export const API_V2_VERSION_ENDPOINT = '/api/v2/analysis/version'; +export const API_OLD_VERSION_ENDPOINT = '/api/server/version'; +export const API_V2_JRE_ENDPOINT = '/api/v2/analysis/jres'; diff --git a/src/java.ts b/src/java.ts new file mode 100644 index 00000000..bf584e94 --- /dev/null +++ b/src/java.ts @@ -0,0 +1,301 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import fs from 'fs'; +import * as fsExtra from 'fs-extra'; +import AdmZip from 'adm-zip'; +import zlib from 'zlib'; +import path from 'path'; +import axios from 'axios'; +import crypto from 'crypto'; +import * as stream from 'stream'; +import tarStream from 'tar-stream'; +import { promisify } from 'util'; +import semver, { SemVer } from 'semver'; +import { log, LogLevel } from './logging'; +import { + API_OLD_VERSION_ENDPOINT, + API_V2_JRE_ENDPOINT, + API_V2_VERSION_ENDPOINT, + SONAR_CACHE_DIR, + SONARCLOUD_PRODUCTION_URL, + SONARCLOUD_URL, + SONARCLOUD_URL_REGEX, + SONARQUBE_JRE_PROVISIONING_MIN_VERSION, + UNARCHIVE_SUFFIX, +} from './constants'; +import { + JREFullData, + JreMetaData, + PlatformInfo, + ScannerProperties, + ScannerProperty, +} from './types'; +import { fetch } from './request'; + +const finished = promisify(stream.finished); + +export function getEndpoint(parameters: ScannerProperties): { + isSonarCloud: boolean; + sonarHostUrl: string; +} { + let sonarHostUrl = parameters[ScannerProperty.SonarHostUrl] ?? ''; + if (!sonarHostUrl || SONARCLOUD_URL_REGEX.exec(sonarHostUrl)) { + return { + isSonarCloud: true, + sonarHostUrl: parameters[ScannerProperty.SonarScannerSonarCloudURL] ?? SONARCLOUD_URL, + }; + } + return { + isSonarCloud: false, + sonarHostUrl, + }; +} + +export async function serverSupportsJREProvisioning( + parameters: ScannerProperties, + platformInfo: PlatformInfo, +): Promise { + const { isSonarCloud, sonarHostUrl } = getEndpoint(parameters); + + if (isSonarCloud) { + return true; + } + + // SonarQube + log(LogLevel.DEBUG, 'Detecting SonarQube server version'); + const SQServerInfo = await fetchServerVersion( + sonarHostUrl, + parameters[ScannerProperty.SonarToken], + ); + log(LogLevel.INFO, 'SonarQube server version: ', SQServerInfo.version); + + const supports = semver.satisfies(SQServerInfo, `>=${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`); + log(LogLevel.DEBUG, `SonarQube Server v${SQServerInfo} supports JRE provisioning: ${supports}`); + return supports; +} + +async function fetchLatestSupportedJRE(serverUrl: string, platformInfo: PlatformInfo) { + const jreInfoUrl = `${serverUrl}/api/v2/analysis/jres?os=${platformInfo.os}&arch=${platformInfo.arch}`; + log(LogLevel.DEBUG, `Downloading JRE from: ${jreInfoUrl}`); + + const { data } = await axios.get(jreInfoUrl); + + log(LogLevel.DEBUG, 'file info: ', data); + + return data; +} + +export async function handleJREProvisioning( + properties: ScannerProperties, + platformInfo: PlatformInfo, +): Promise { + // TODO: use correct mapping to SC/SQ + const serverUrl = properties[ScannerProperty.SonarHostUrl] ?? SONARCLOUD_PRODUCTION_URL; + const token = properties[ScannerProperty.SonarToken]; + + log(LogLevel.DEBUG, 'Detecting latest version of JRE'); + const latestJREData = await fetchLatestSupportedJRE(serverUrl, platformInfo); + log(LogLevel.INFO, 'Latest Supported JRE: ', latestJREData); + + log(LogLevel.DEBUG, 'Looking for Cached JRE'); + const cachedJRE = await getCachedFileLocation( + latestJREData.md5, + latestJREData.filename + UNARCHIVE_SUFFIX, + ); + + if (cachedJRE) { + return { + ...latestJREData, + jrePath: path.join(cachedJRE, cachedJRE), + }; + } else { + const archivePath = path.join(SONAR_CACHE_DIR, latestJREData.md5, latestJREData.filename); + const jreDirPath = path.join( + SONAR_CACHE_DIR, + latestJREData.md5, + latestJREData.filename + UNARCHIVE_SUFFIX, + ); + + log(LogLevel.DEBUG, `Extracting JRE from: ${archivePath}`); + log(LogLevel.DEBUG, `Extracting JRE to: ${jreDirPath}`); + // Create destination directory if it doesn't exist + const parentCacheDirectory = jreDirPath.substring(0, jreDirPath.lastIndexOf('/')); + if (!fs.existsSync(parentCacheDirectory)) { + log(LogLevel.DEBUG, `Cache directory doesn't exist: ${parentCacheDirectory}`); + log(LogLevel.DEBUG, `Creating cache directory`); + fs.mkdirSync(parentCacheDirectory, { recursive: true }); + } + const writer = fs.createWriteStream(archivePath); + + const url = serverUrl + API_V2_JRE_ENDPOINT + `/${latestJREData.filename}`; + log(LogLevel.DEBUG, `Downloading ${url} to ${archivePath}`); + + const response = await fetch(token, { + url, + method: 'GET', + responseType: 'stream', + }); + + const totalLength = response.headers['content-length']; + let progress = 0; + + response.data.on('data', (chunk: any) => { + progress += chunk.length; + process.stdout.write( + `\r[INFO] Bootstrapper:: Downloaded ${Math.round((progress / totalLength) * 100)}%`, + ); + }); + + response.data.on('end', () => { + console.log(); + log(LogLevel.INFO, 'JRE Download complete'); + }); + + const streamPipeline = promisify(stream.pipeline); + await streamPipeline(response.data, writer); + + response.data.pipe(writer); + + await finished(writer); + log(LogLevel.INFO, `Downloaded JRE to ${archivePath}`); + + await validateChecksum(archivePath, latestJREData.md5); + + log(LogLevel.INFO, `Extracting JRE to ${jreDirPath}`); + await extractArchive(archivePath, jreDirPath); + + const jreBinPath = path.join(jreDirPath, latestJREData.javaPath); + log(LogLevel.DEBUG, `JRE downloaded to ${jreDirPath}. Allowing execution on ${jreBinPath}`); + + return { + ...latestJREData, + jrePath: jreBinPath, + }; + } +} + +async function generateChecksum(filepath: string) { + return new Promise((resolve, reject) => { + fs.readFile(filepath, (err, data) => { + if (err) { + reject(err); + return; + } + resolve(crypto.createHash('md5').update(data).digest('hex')); + }); + }); +} + +async function validateChecksum(filePath: string, expectedChecksum: string) { + if (expectedChecksum) { + log(LogLevel.INFO, `Verifying checksum ${expectedChecksum}`); + const checksum = await generateChecksum(filePath); + + log(LogLevel.DEBUG, `Checksum Value: ${checksum}`); + if (checksum !== expectedChecksum) { + throw new Error( + `Checksum verification failed for ${filePath}. Expected checksum ${expectedChecksum} but got ${checksum}`, + ); + } + } +} + +async function extractArchive(fromPath: string, toPath: string) { + log(LogLevel.INFO, `Extracting ${fromPath} to ${toPath}`); + if (fromPath.endsWith('.tar.gz')) { + const tarFilePath = fromPath; + const extract = tarStream.extract(); + + const extractionPromise = new Promise((resolve, reject) => { + extract.on('entry', async (header, stream, next) => { + // Create the full path for the file + const filePath = path.join(toPath, header.name); + + // Ensure the directory exists + await fsExtra.ensureDir(path.dirname(filePath)); + + stream.pipe(fs.createWriteStream(filePath, { mode: header.mode })); + + stream.on('end', next); + + stream.resume(); // just auto drain the stream + }); + + extract.on('finish', () => { + resolve(null); + }); + + extract.on('error', err => { + log(LogLevel.ERROR, 'Error extracting tar.gz', err); + reject(err); + }); + }); + + fs.createReadStream(tarFilePath).pipe(zlib.createGunzip()).pipe(extract); + + await extractionPromise; + } else { + const zip = new AdmZip(fromPath); + zip.extractAllTo(toPath, true); + } +} + +async function getCachedFileLocation(md5: string, filename: string) { + const filePath = path.join(SONAR_CACHE_DIR, md5, filename); + if (fs.existsSync(filePath)) { + log(LogLevel.INFO, 'Found Cached JRE: ', filePath); + return filePath; + } else { + log(LogLevel.INFO, 'No Cached JRE found'); + return null; + } +} + +export async function fetchServerVersion(sonarHostUrl: string, token: string): Promise { + let version: SemVer | null = null; + try { + // Try and fetch the new version endpoint first + log(LogLevel.DEBUG, `Fetching API V2 ${API_V2_VERSION_ENDPOINT}`); + const response = await fetch(token, { url: sonarHostUrl + API_V2_VERSION_ENDPOINT }); + version = semver.coerce(response.data); + } catch (error: unknown) { + try { + // If it fails, fallback on deprecated server version endpoint + log( + LogLevel.DEBUG, + `Unable to fetch API V2 ${API_V2_VERSION_ENDPOINT}: ${error}. Falling back on ${API_OLD_VERSION_ENDPOINT}`, + ); + const response = await fetch(token, { url: sonarHostUrl + API_OLD_VERSION_ENDPOINT }); + version = semver.coerce(response.data); + } catch (error: unknown) { + // If it also failed, give up + log(LogLevel.ERROR, `Failed to fetch server version: ${error}`); + throw error; + } + } + + // If we couldn't parse the version + if (!version) { + throw new Error(`Failed to parse server version "${version}"`); + } + + return version; +} diff --git a/src/request.ts b/src/request.ts new file mode 100644 index 00000000..e523ec14 --- /dev/null +++ b/src/request.ts @@ -0,0 +1,29 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import axios, { AxiosRequestConfig } from 'axios'; + +export function fetch(token: string, config: AxiosRequestConfig) { + return axios({ + headers: { + Authorization: `Bearer ${token}`, + }, + ...config, + }); +} diff --git a/src/scan.ts b/src/scan.ts index 866512f6..daad2813 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -18,27 +18,56 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { handleJREProvisioning, serverSupportsJREProvisioning } from './java'; import { log, LogLevel, setLogLevel } from './logging'; import { getPlatformInfo } from './platform'; import { getProperties } from './properties'; -import { ScannerProperty, ScanOptions } from './types'; +import { ScannerProperty, JreMetaData, ScanOptions } from './types'; +import { version } from '../package.json'; export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { const startTimestampMs = Date.now(); const properties = getProperties(scanOptions, startTimestampMs, cliArgs); + + const serverUrl = properties[ScannerProperty.SonarHostUrl]; + const explicitJREPathOverride = properties[ScannerProperty.SonarScannerJavaExePath]; + if (properties[ScannerProperty.SonarVerbose] === 'true') { setLogLevel(LogLevel.DEBUG); log(LogLevel.DEBUG, 'Setting the log level to DEBUG due to verbose mode'); } + if (properties[ScannerProperty.SonarLogLevel]) { + setLogLevel(properties[ScannerProperty.SonarLogLevel]); + log(LogLevel.DEBUG, `Overriding the log level to ${properties[ScannerProperty.SonarLogLevel]}`); + } + + log(LogLevel.INFO, 'Version: ', version); + log(LogLevel.DEBUG, 'Finding platform info'); const platformInfo = getPlatformInfo(); log(LogLevel.INFO, 'Platform: ', platformInfo); - //TODO: verifyJRE based on platform - //TODO: fetchJRE + log(LogLevel.DEBUG, 'Check if Server supports JRE Provisioning'); + const supportsJREProvisioning = await serverSupportsJREProvisioning(properties, platformInfo); + log( + LogLevel.INFO, + `JRE Provisioning ${supportsJREProvisioning ? 'is ' : 'is NOT '}supported on ${serverUrl}`, + ); + + // TODO: also check if JRE is explicitly set by properties + let latestJRE: string | JreMetaData = explicitJREPathOverride || 'java'; + if (!explicitJREPathOverride && supportsJREProvisioning) { + await handleJREProvisioning(properties, platformInfo); + } else { + // TODO: old SQ, support old CLI fetch + // https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${version}-${os}.zip + } + //TODO: verifyScannerEngine + //TODO: fetchScannerEngine + //TODO: // ... properties[ScannerProperty.SonarScannerWasEngineCacheHit] = 'false'; diff --git a/src/types.ts b/src/types.ts index 5c557598..f4e03978 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,6 +32,10 @@ export type JreMetaData = { javaPath: string; }; +export type JREFullData = JreMetaData & { + jrePath: string; +}; + export type ScannerLogEntry = { level: LogLevel; formattedMessage: string; @@ -40,12 +44,15 @@ export type ScannerLogEntry = { export enum ScannerProperty { SonarVerbose = 'sonar.verbose', + SonarLogLevel = 'sonar.log.level', SonarToken = 'sonar.token', SonarExclusions = 'sonar.exclusions', SonarHostUrl = 'sonar.host.url', SonarUserHome = 'sonar.userHome', SonarOrganization = 'sonar.organization', SonarProjectBaseDir = 'sonar.projectBaseDir', + SonarScannerSonarCloudURL = 'sonar.scanner.sonarcloudUrl', + SonarScannerJavaExePath = 'sonar.scanner.javaExePath', SonarScannerWasEngineCacheHit = 'sonar.scanner.wasEngineCacheHit', } diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts new file mode 100644 index 00000000..b81d2758 --- /dev/null +++ b/test/unit/java.test.ts @@ -0,0 +1,136 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { ServerMock } from './mocks/ServerMock'; +import { fetchServerVersion, getEndpoint } from '../../src/java'; +import { fetch } from '../../src/request'; +import { ScannerProperty } from '../../src/types'; + +const serverHandler = new ServerMock(); + +beforeEach(() => { + jest.clearAllMocks(); + serverHandler.reset(); +}); + +describe('java', () => { + describe('endpoint should be detected correctly', () => { + it('should detect SonarCloud', () => { + const expected = { + isSonarCloud: true, + sonarHostUrl: 'https://sonarcloud.io', + }; + + // SonarCloud used by default + expect(getEndpoint({})).toEqual(expected); + + // Backward-compatible use-case + expect( + getEndpoint({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + }), + ).toEqual(expected); + + // Using www. + expect( + getEndpoint({ + [ScannerProperty.SonarHostUrl]: 'https://www.sonarcloud.io', + }), + ).toEqual(expected); + + // Using trailing slash (ensures trailing slash is dropped) + expect( + getEndpoint({ + [ScannerProperty.SonarHostUrl]: 'https://www.sonarcloud.io/', + }), + ).toEqual(expected); + }); + + it('should detect SonarCloud with custom URL', () => { + const endpoint = getEndpoint({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io/', + [ScannerProperty.SonarScannerSonarCloudURL]: 'http://that-is-a-sonarcloud-custom-url.com', + }); + + expect(endpoint).toEqual({ + isSonarCloud: true, + sonarHostUrl: 'http://that-is-a-sonarcloud-custom-url.com', + }); + }); + + it('should detect SonarQube', () => { + const endpoint = getEndpoint({ + [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', + }); + + expect(endpoint).toEqual({ + isSonarCloud: false, + sonarHostUrl: 'https://next.sonarqube.com', + }); + }); + + it('should ignore SonarCloud custom URL if sonar host URL does not match sonarcloud', () => { + const endpoint = getEndpoint({ + [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', + [ScannerProperty.SonarScannerSonarCloudURL]: 'http://that-is-a-sonarcloud-custom-url.com', + }); + + expect(endpoint).toEqual({ + isSonarCloud: false, + sonarHostUrl: 'https://next.sonarqube.com', + }); + }); + }); + + describe('version should be detected correctly', () => { + it('the SonarQube version should be fetched correctly when new endpoint does not exist', async () => { + serverHandler.mockServerErrorResponse(); + serverHandler.mockServerVersionResponse('3.2.2'); + + const serverSemver = await fetchServerVersion('http://sonarqube.com', 'dummy-token'); + expect(serverSemver.toString()).toEqual('3.2.2'); + expect(fetch).toHaveBeenCalledTimes(2); + }); + + it('the SonarQube version should be fetched correctly using the new endpoint', async () => { + serverHandler.mockServerVersionResponse('3.2.1.12313'); + + const serverSemver = await fetchServerVersion('http://sonarqube.com', 'dummy-token'); + expect(serverSemver.toString()).toEqual('3.2.1'); + }); + + it('should fail if both endpoints do not work', async () => { + serverHandler.mockServerErrorResponse(); + serverHandler.mockServerErrorResponse(); + + expect(async () => { + await fetchServerVersion('http://sonarqube.com', 'dummy-token'); + }).rejects.toBeDefined(); + }); + + it('should fail if version can not be parsed', async () => { + serverHandler.mockServerVersionResponse('FORBIDDEN'); + + expect(async () => { + await fetchServerVersion('http://sonarqube.com', 'dummy-token'); + }).rejects.toBeDefined(); + }); + }); +}); diff --git a/test/unit/mocks/ServerMock.ts b/test/unit/mocks/ServerMock.ts new file mode 100644 index 00000000..dd85fe53 --- /dev/null +++ b/test/unit/mocks/ServerMock.ts @@ -0,0 +1,68 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { fetch } from '../../../src/request'; + +jest.mock('../../../src/request'); + +const DEFAULT_AXIOS_RESPONSE: AxiosResponse = { + data: '', + status: 200, + statusText: 'OK', + config: {}, + headers: {}, +} as AxiosResponse; + +export class ServerMock { + responses: (Partial | Error)[] = []; + + constructor() { + jest.mocked(fetch).mockImplementation(this.handleFetch.bind(this)); + } + + mockServerVersionResponse(version: string) { + this.responses.push({ + data: version, + status: 200, + statusText: 'OK', + }); + } + + mockServerErrorResponse() { + this.responses.push(new Error('Not found')); + } + + async handleFetch(_token: string, _config: AxiosRequestConfig) { + if (this.responses.length === 0) { + return { ...DEFAULT_AXIOS_RESPONSE }; + } + + const response = this.responses.shift(); + if (response instanceof Error) { + throw response; + } else { + return { ...DEFAULT_AXIOS_RESPONSE, ...response }; + } + } + + reset() { + this.responses = []; + } +} diff --git a/test/unit/scan.test.ts b/test/unit/scan.test.ts new file mode 100644 index 00000000..b5ff6922 --- /dev/null +++ b/test/unit/scan.test.ts @@ -0,0 +1,93 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { scan } from '../../src/scan'; +import * as java from '../../src/java'; +import * as platform from '../../src/platform'; +import * as logging from '../../src/logging'; + +jest.mock('../../src/java'); +jest.mock('../../src/platform'); +jest.mock('../../package.json', () => ({ + version: 'MOCK.VERSION', +})); + +jest.spyOn(logging, 'setLogLevel'); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('scan', () => { + it('should default the log level to INFO', async () => { + await scan({}, []); + expect(logging.getLogLevel()).toBe('INFO'); + }); + + it('should set the log level to the value provided by the user', async () => { + await scan({ options: { 'sonar.verbose': 'true' } }, []); + expect(logging.getLogLevel()).toBe('DEBUG'); + }); + + it('should output the current version of the scanner', async () => { + (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(false); + jest.spyOn(logging, 'log'); + await scan({}, []); + expect(logging.log).toHaveBeenNthCalledWith(1, 'INFO', 'Version: ', 'MOCK.VERSION'); + }); + + it('should output the current platform', async () => { + (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(false); + jest.spyOn(logging, 'log'); + jest.spyOn(platform, 'getPlatformInfo').mockReturnValue({ os: 'alpine', arch: 'mock-arch' }); + await scan({}, []); + expect(logging.log).toHaveBeenNthCalledWith(3, 'INFO', 'Platform: ', { + os: 'alpine', + arch: 'mock-arch', + }); + }); + + describe('when the SQ version does not support JRE provisioning', () => { + it('should not fetch the JRE version', async () => { + (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(false); + await scan({}, []); + expect(java.handleJREProvisioning).not.toHaveBeenCalled(); + }); + }); + + describe('when the user provides a JRE exe path override', () => { + it('should not fetch the JRE version', async () => { + (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(false); + await scan({ options: { 'sonar.scanner.javaExePath': 'path/to/java' } }, []); + expect(java.handleJREProvisioning).not.toHaveBeenCalled(); + + // TODO: test that the JRE exe path is used when running the scanner engine + }); + }); + + describe('when the user provides a SonarQube URL and the version supports provisioning', () => { + it('should fetch the JRE version', async () => { + (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(true); + jest.spyOn(java, 'handleJREProvisioning'); + await scan({ serverUrl: 'http://localhost:9000' }, []); + expect(java.handleJREProvisioning).toHaveBeenCalled(); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 3285124e..b16944f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -56,7 +56,7 @@ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ "outDir": "build" /* Specify an output folder for all emitted files. */, - "removeComments": false /* Disable emitting comments. */, + // "removeComments": true, /* Disable emitting comments. */ // "noEmit": true, /* Disable emitting files from a compilation. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ @@ -107,5 +107,5 @@ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, "include": ["src/**/*.ts"], - "exclude": ["node_modules", "src/**/types.ts"] + "exclude": ["node_modules"] } From b9a4f8c37d59ad5ccfb39a8fbd9f15401f59b016 Mon Sep 17 00:00:00 2001 From: 7PH Date: Thu, 18 Apr 2024 00:36:22 +0200 Subject: [PATCH 05/35] SCANNPM-2 Handle proxy detection & usage --- src/http-agent.ts | 33 +++++++++ src/java.ts | 64 +++++++++--------- src/properties.ts | 28 +++++++- src/proxy.ts | 65 ++++++++++++++++++ src/types.ts | 5 ++ test/unit/http-agent.test.ts | 41 +++++++++++ test/unit/java.test.ts | 89 ++++-------------------- test/unit/properties.test.ts | 126 +++++++++++++++++++++++++++++++++- test/unit/proxy.test.ts | 127 +++++++++++++++++++++++++++++++++++ 9 files changed, 465 insertions(+), 113 deletions(-) create mode 100644 src/http-agent.ts create mode 100644 src/proxy.ts create mode 100644 test/unit/http-agent.test.ts create mode 100644 test/unit/proxy.test.ts diff --git a/src/http-agent.ts b/src/http-agent.ts new file mode 100644 index 00000000..b23b6e75 --- /dev/null +++ b/src/http-agent.ts @@ -0,0 +1,33 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { AxiosRequestConfig } from 'axios'; +import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; + +export function getHttpAgents( + proxyUrl?: URL, +): Pick { + const agents: Pick = {}; + + if (proxyUrl) { + agents.httpsAgent = new HttpsProxyAgent({ proxy: proxyUrl.toString() }); + agents.httpAgent = new HttpProxyAgent({ proxy: proxyUrl.toString() }); + } + return agents; +} diff --git a/src/java.ts b/src/java.ts index bf584e94..1916f46b 100644 --- a/src/java.ts +++ b/src/java.ts @@ -18,29 +18,30 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import fs from 'fs'; -import * as fsExtra from 'fs-extra'; import AdmZip from 'adm-zip'; -import zlib from 'zlib'; -import path from 'path'; import axios from 'axios'; import crypto from 'crypto'; +import fs from 'fs'; +import * as fsExtra from 'fs-extra'; +import path from 'path'; +import semver, { SemVer } from 'semver'; import * as stream from 'stream'; import tarStream from 'tar-stream'; import { promisify } from 'util'; -import semver, { SemVer } from 'semver'; -import { log, LogLevel } from './logging'; +import zlib from 'zlib'; import { API_OLD_VERSION_ENDPOINT, API_V2_JRE_ENDPOINT, API_V2_VERSION_ENDPOINT, SONAR_CACHE_DIR, SONARCLOUD_PRODUCTION_URL, - SONARCLOUD_URL, - SONARCLOUD_URL_REGEX, SONARQUBE_JRE_PROVISIONING_MIN_VERSION, UNARCHIVE_SUFFIX, } from './constants'; +import { getHttpAgents } from './http-agent'; +import { log, LogLevel } from './logging'; +import { getProxyUrl } from './proxy'; +import { fetch } from './request'; import { JREFullData, JreMetaData, @@ -48,42 +49,22 @@ import { ScannerProperties, ScannerProperty, } from './types'; -import { fetch } from './request'; const finished = promisify(stream.finished); -export function getEndpoint(parameters: ScannerProperties): { - isSonarCloud: boolean; - sonarHostUrl: string; -} { - let sonarHostUrl = parameters[ScannerProperty.SonarHostUrl] ?? ''; - if (!sonarHostUrl || SONARCLOUD_URL_REGEX.exec(sonarHostUrl)) { - return { - isSonarCloud: true, - sonarHostUrl: parameters[ScannerProperty.SonarScannerSonarCloudURL] ?? SONARCLOUD_URL, - }; - } - return { - isSonarCloud: false, - sonarHostUrl, - }; -} - export async function serverSupportsJREProvisioning( parameters: ScannerProperties, platformInfo: PlatformInfo, ): Promise { - const { isSonarCloud, sonarHostUrl } = getEndpoint(parameters); - - if (isSonarCloud) { + if (parameters[ScannerProperty.SonarScannerInternalIsSonarCloud] !== 'true') { return true; } // SonarQube log(LogLevel.DEBUG, 'Detecting SonarQube server version'); const SQServerInfo = await fetchServerVersion( - sonarHostUrl, - parameters[ScannerProperty.SonarToken], + parameters[ScannerProperty.SonarHostUrl], + parameters, ); log(LogLevel.INFO, 'SonarQube server version: ', SQServerInfo.version); @@ -121,6 +102,8 @@ export async function handleJREProvisioning( latestJREData.filename + UNARCHIVE_SUFFIX, ); + const proxyUrl = getProxyUrl(properties); + if (cachedJRE) { return { ...latestJREData, @@ -152,6 +135,7 @@ export async function handleJREProvisioning( url, method: 'GET', responseType: 'stream', + ...getHttpAgents(proxyUrl), }); const totalLength = response.headers['content-length']; @@ -269,12 +253,21 @@ async function getCachedFileLocation(md5: string, filename: string) { } } -export async function fetchServerVersion(sonarHostUrl: string, token: string): Promise { +export async function fetchServerVersion( + sonarHostUrl: string, + parameters: ScannerProperties, +): Promise { + const token = parameters[ScannerProperty.SonarToken]; + const proxyUrl = getProxyUrl(parameters); + let version: SemVer | null = null; try { // Try and fetch the new version endpoint first log(LogLevel.DEBUG, `Fetching API V2 ${API_V2_VERSION_ENDPOINT}`); - const response = await fetch(token, { url: sonarHostUrl + API_V2_VERSION_ENDPOINT }); + const response = await fetch(token, { + url: sonarHostUrl + API_V2_VERSION_ENDPOINT, + ...getHttpAgents(proxyUrl), + }); version = semver.coerce(response.data); } catch (error: unknown) { try { @@ -283,7 +276,10 @@ export async function fetchServerVersion(sonarHostUrl: string, token: string): P LogLevel.DEBUG, `Unable to fetch API V2 ${API_V2_VERSION_ENDPOINT}: ${error}. Falling back on ${API_OLD_VERSION_ENDPOINT}`, ); - const response = await fetch(token, { url: sonarHostUrl + API_OLD_VERSION_ENDPOINT }); + const response = await fetch(token, { + url: sonarHostUrl + API_OLD_VERSION_ENDPOINT, + ...getHttpAgents(proxyUrl), + }); version = semver.coerce(response.data); } catch (error: unknown) { // If it also failed, give up diff --git a/src/properties.ts b/src/properties.ts index dbc190da..fa18440b 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -26,6 +26,8 @@ import { ENV_TO_PROPERTY_NAME, ENV_VAR_PREFIX, SCANNER_BOOTSTRAPPER_NAME, + SONARCLOUD_URL, + SONARCLOUD_URL_REGEX, SONAR_PROJECT_FILENAME, } from './constants'; import { LogLevel, log } from './logging'; @@ -269,6 +271,25 @@ function getBootstrapperProperties(startTimestampMs: number): ScannerProperties }; } +/** + * Get endpoint properties from scanner properties. + */ +export function getHostProperties(properties: ScannerProperties): ScannerProperties { + let sonarHostUrl = properties[ScannerProperty.SonarHostUrl] ?? ''; + + if (!sonarHostUrl || SONARCLOUD_URL_REGEX.exec(sonarHostUrl)) { + return { + [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', + [ScannerProperty.SonarHostUrl]: + properties[ScannerProperty.SonarScannerSonarCloudURL] ?? SONARCLOUD_URL, + }; + } + return { + [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'false', + [ScannerProperty.SonarHostUrl]: sonarHostUrl, + }; +} + export function getProperties( scanOptions: ScanOptions, startTimestampMs: number, @@ -308,7 +329,7 @@ export function getProperties( } // Merge properties respecting order of precedence - return [ + const properties = [ { 'sonar.projectBaseDir': projectBaseDir }, // Manually computed, can't be overridden bootstrapperProperties, // Can't be overridden cliProperties, // Highest precedence @@ -318,4 +339,9 @@ export function getProperties( ] .reverse() .reduce((acc, curr) => ({ ...acc, ...curr }), {}); + + return { + ...properties, + ...getHostProperties(properties), + }; } diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 00000000..45634542 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,65 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { URL } from 'url'; +import { LogLevel, log } from './logging'; +import { ScannerProperties, ScannerProperty } from './types'; + +export function getProxyUrl(properties: ScannerProperties): URL | undefined { + const proxyHost = properties[ScannerProperty.SonarScannerProxyHost]; + const serverUsesHttps = properties[ScannerProperty.SonarHostUrl].startsWith('https'); + + if (proxyHost) { + // We assume that the proxy protocol is the same as the endpoint. + const protocol = serverUsesHttps ? 'https' : 'http'; + const proxyPort = + properties[ScannerProperty.SonarScannerProxyPort] ?? (serverUsesHttps ? 443 : 80); + const proxyUser = properties[ScannerProperty.SonarScannerProxyUser] ?? ''; + const proxyPassword = properties[ScannerProperty.SonarScannerProxyPassword] ?? ''; + const proxyUrl = new URL( + `${protocol}://${proxyUser}:${proxyPassword}@${proxyHost}:${proxyPort}`, + ); + log(LogLevel.DEBUG, `Detecting proxy: ${proxyUrl}`); + return proxyUrl; + } else if ( + properties[ScannerProperty.SonarScannerProxyPort] || + properties[ScannerProperty.SonarScannerProxyUser] || + properties[ScannerProperty.SonarScannerProxyPassword] + ) { + log(LogLevel.WARN, `Detecting proxy: Incomplete proxy configuration. Proxy host is missing.`); + } + + log(LogLevel.DEBUG, `Detecting proxy: No proxy detected'}`); + return undefined; +} + +export function proxyUrlToJavaOptions(properties: ScannerProperties): string[] { + const proxyUrl = getProxyUrl(properties); + if (!proxyUrl) { + return []; + } + + const protocol = properties[ScannerProperty.SonarHostUrl].startsWith('https') ? 'https' : 'http'; + return [ + `-D${protocol}.proxyHost=${proxyUrl.hostname}`, + `-D${protocol}.proxyPort=${proxyUrl.port}`, + `-D${protocol}.proxyUser=${proxyUrl.username}`, + `-D${protocol}.proxyPassword=${proxyUrl.password}`, + ]; +} diff --git a/src/types.ts b/src/types.ts index f4e03978..68005708 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,6 +54,11 @@ export enum ScannerProperty { SonarScannerSonarCloudURL = 'sonar.scanner.sonarcloudUrl', SonarScannerJavaExePath = 'sonar.scanner.javaExePath', SonarScannerWasEngineCacheHit = 'sonar.scanner.wasEngineCacheHit', + SonarScannerProxyHost = 'sonar.scanner.proxyHost', + SonarScannerProxyPort = 'sonar.scanner.proxyPort', + SonarScannerProxyUser = 'sonar.scanner.proxyUser', + SonarScannerProxyPassword = 'sonar.scanner.proxyPassword', + SonarScannerInternalIsSonarCloud = 'sonar.scanner.internal.isSonarCloud', } export type ScannerProperties = { diff --git a/test/unit/http-agent.test.ts b/test/unit/http-agent.test.ts new file mode 100644 index 00000000..6a279fb4 --- /dev/null +++ b/test/unit/http-agent.test.ts @@ -0,0 +1,41 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; +import { getHttpAgents } from '../../src/http-agent'; + +describe('http-agent', () => { + it('should define proxy url correctly', () => { + const proxyUrl = new URL('http://proxy.com'); + + const agents = getHttpAgents(proxyUrl); + expect(agents.httpAgent).toBeInstanceOf(HttpProxyAgent); + expect(agents.httpAgent?.proxy.toString()).toBe(proxyUrl.toString()); + expect(agents.httpsAgent).toBeInstanceOf(HttpsProxyAgent); + expect(agents.httpsAgent?.proxy.toString()).toBe(proxyUrl.toString()); + }); + + it('should not define agents when no proxy is provided', () => { + const agents = getHttpAgents(); + expect(agents.httpAgent).toBeUndefined(); + expect(agents.httpsAgent).toBeUndefined(); + expect(agents).toEqual({}); + }); +}); diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index b81d2758..6e7d8e94 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -18,93 +18,32 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ServerMock } from './mocks/ServerMock'; -import { fetchServerVersion, getEndpoint } from '../../src/java'; +import { fetchServerVersion } from '../../src/java'; import { fetch } from '../../src/request'; -import { ScannerProperty } from '../../src/types'; +import { ScannerProperties, ScannerProperty } from '../../src/types'; +import { ServerMock } from './mocks/ServerMock'; + +jest.mock('../../src/request'); const serverHandler = new ServerMock(); +const MOCKED_PROPERTIES: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'http://sonarqube.com', + [ScannerProperty.SonarToken]: 'dummy-token', +}; + beforeEach(() => { jest.clearAllMocks(); serverHandler.reset(); }); describe('java', () => { - describe('endpoint should be detected correctly', () => { - it('should detect SonarCloud', () => { - const expected = { - isSonarCloud: true, - sonarHostUrl: 'https://sonarcloud.io', - }; - - // SonarCloud used by default - expect(getEndpoint({})).toEqual(expected); - - // Backward-compatible use-case - expect( - getEndpoint({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', - }), - ).toEqual(expected); - - // Using www. - expect( - getEndpoint({ - [ScannerProperty.SonarHostUrl]: 'https://www.sonarcloud.io', - }), - ).toEqual(expected); - - // Using trailing slash (ensures trailing slash is dropped) - expect( - getEndpoint({ - [ScannerProperty.SonarHostUrl]: 'https://www.sonarcloud.io/', - }), - ).toEqual(expected); - }); - - it('should detect SonarCloud with custom URL', () => { - const endpoint = getEndpoint({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io/', - [ScannerProperty.SonarScannerSonarCloudURL]: 'http://that-is-a-sonarcloud-custom-url.com', - }); - - expect(endpoint).toEqual({ - isSonarCloud: true, - sonarHostUrl: 'http://that-is-a-sonarcloud-custom-url.com', - }); - }); - - it('should detect SonarQube', () => { - const endpoint = getEndpoint({ - [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', - }); - - expect(endpoint).toEqual({ - isSonarCloud: false, - sonarHostUrl: 'https://next.sonarqube.com', - }); - }); - - it('should ignore SonarCloud custom URL if sonar host URL does not match sonarcloud', () => { - const endpoint = getEndpoint({ - [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', - [ScannerProperty.SonarScannerSonarCloudURL]: 'http://that-is-a-sonarcloud-custom-url.com', - }); - - expect(endpoint).toEqual({ - isSonarCloud: false, - sonarHostUrl: 'https://next.sonarqube.com', - }); - }); - }); - describe('version should be detected correctly', () => { it('the SonarQube version should be fetched correctly when new endpoint does not exist', async () => { serverHandler.mockServerErrorResponse(); serverHandler.mockServerVersionResponse('3.2.2'); - const serverSemver = await fetchServerVersion('http://sonarqube.com', 'dummy-token'); + const serverSemver = await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); expect(serverSemver.toString()).toEqual('3.2.2'); expect(fetch).toHaveBeenCalledTimes(2); }); @@ -112,7 +51,7 @@ describe('java', () => { it('the SonarQube version should be fetched correctly using the new endpoint', async () => { serverHandler.mockServerVersionResponse('3.2.1.12313'); - const serverSemver = await fetchServerVersion('http://sonarqube.com', 'dummy-token'); + const serverSemver = await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); expect(serverSemver.toString()).toEqual('3.2.1'); }); @@ -121,7 +60,7 @@ describe('java', () => { serverHandler.mockServerErrorResponse(); expect(async () => { - await fetchServerVersion('http://sonarqube.com', 'dummy-token'); + await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); }).rejects.toBeDefined(); }); @@ -129,7 +68,7 @@ describe('java', () => { serverHandler.mockServerVersionResponse('FORBIDDEN'); expect(async () => { - await fetchServerVersion('http://sonarqube.com', 'dummy-token'); + await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); }).rejects.toBeDefined(); }); }); diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index 31b8ce1e..63cf01b1 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -17,9 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { DEFAULT_SONAR_EXCLUSIONS, SCANNER_BOOTSTRAPPER_NAME } from '../../src/constants'; +import { + DEFAULT_SONAR_EXCLUSIONS, + SCANNER_BOOTSTRAPPER_NAME, + SONARCLOUD_URL, +} from '../../src/constants'; import { LogLevel, log } from '../../src/logging'; -import { getProperties } from '../../src/properties'; +import { getHostProperties, getProperties } from '../../src/properties'; +import { ScannerProperty } from '../../src/types'; import { FakeProjectMock } from './mocks/FakeProjectMock'; jest.mock('../../src/logging'); @@ -35,6 +40,33 @@ afterEach(() => { }); describe('getProperties', () => { + describe('should handle JS API scan options params correctly', () => { + it('should detect custom SonarCloud endpoint', () => { + projectHandler.reset('fake_project_with_no_package_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + options: { + 'sonar.projectKey': 'use-this-project-key', + }, + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', + 'sonar.projectKey': 'use-this-project-key', + 'sonar.projectDescription': 'No description.', + 'sonar.sources': '.', + 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, + }); + }); + }); + describe('should handle JS API scan options params correctly', () => { it('should detect and use user-provided scan option params', () => { projectHandler.reset('fake_project_with_sonar_properties_file'); @@ -55,6 +87,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.token': 'dummy-token', 'sonar.verbose': 'true', 'sonar.projectKey': 'use-this-project-key', @@ -95,6 +128,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.javascript.lcov.reportPaths': 'coverage/lcov.info', 'sonar.projectKey': 'fake-basic-project', 'sonar.projectName': 'fake-basic-project', @@ -121,6 +155,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'fake-project', 'sonar.projectName': 'fake-project', 'sonar.projectDescription': 'A fake project', @@ -148,6 +183,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectDescription': 'No description.', 'sonar.sources': '.', 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, @@ -168,6 +204,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectDescription': 'No description.', 'sonar.sources': '.', 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, @@ -191,6 +228,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.javascript.lcov.reportPaths': 'jest-coverage/lcov.info', 'sonar.projectKey': 'fake-basic-project', 'sonar.projectName': 'fake-basic-project', @@ -215,6 +253,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.javascript.lcov.reportPaths': 'nyc-coverage/lcov.info', 'sonar.projectKey': 'fake-basic-project', 'sonar.projectName': 'fake-basic-project', @@ -254,6 +293,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', @@ -276,11 +316,12 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'https://sonarqube.com/', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', 'sonar.sources': 'the-sources', - 'sonar.host.url': 'https://sonarqube.com/', 'sonar.token': 'my-token', 'sonar.userHome': '/tmp/.sonar/', 'sonar.organization': 'my-org', @@ -297,6 +338,8 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), + 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', @@ -317,6 +360,8 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), + 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', @@ -335,6 +380,8 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), + 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', @@ -358,6 +405,8 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), + 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', @@ -387,6 +436,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', @@ -425,6 +475,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', @@ -470,6 +521,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', @@ -478,3 +530,71 @@ describe('getProperties', () => { }); }); }); + +describe('getHostProperties', () => { + it('should detect SonarCloud', () => { + const expected = { + [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + }; + + // SonarCloud used by default + expect(getHostProperties({})).toEqual(expected); + + // Backward-compatible use-case + expect( + getHostProperties({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + }), + ).toEqual(expected); + + // Using www. + expect( + getHostProperties({ + [ScannerProperty.SonarHostUrl]: 'https://www.sonarcloud.io', + }), + ).toEqual(expected); + + // Using trailing slash (ensures trailing slash is dropped) + expect( + getHostProperties({ + [ScannerProperty.SonarHostUrl]: 'https://www.sonarcloud.io/', + }), + ).toEqual(expected); + }); + + it('should detect SonarCloud with custom URL', () => { + const endpoint = getHostProperties({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io/', + [ScannerProperty.SonarScannerSonarCloudURL]: 'http://that-is-a-sonarcloud-custom-url.com', + }); + + expect(endpoint).toEqual({ + [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', + [ScannerProperty.SonarHostUrl]: 'http://that-is-a-sonarcloud-custom-url.com', + }); + }); + + it('should detect SonarQube', () => { + const endpoint = getHostProperties({ + [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', + }); + + expect(endpoint).toEqual({ + [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'false', + [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', + }); + }); + + it('should ignore SonarCloud custom URL if sonar host URL does not match sonarcloud', () => { + const endpoint = getHostProperties({ + [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', + [ScannerProperty.SonarScannerSonarCloudURL]: 'http://that-is-a-sonarcloud-custom-url.com', + }); + + expect(endpoint).toEqual({ + [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'false', + [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', + }); + }); +}); diff --git a/test/unit/proxy.test.ts b/test/unit/proxy.test.ts new file mode 100644 index 00000000..6f2917d0 --- /dev/null +++ b/test/unit/proxy.test.ts @@ -0,0 +1,127 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { log } from '../../src/logging'; +import { getProxyUrl, proxyUrlToJavaOptions } from '../../src/proxy'; +import { ScannerProperties, ScannerProperty } from '../../src/types'; + +jest.mock('../../src/logging'); + +describe('proxy', () => { + describe('getProxyUrl', () => { + it('should not detect proxy when proxy host is not provided', () => { + const properties: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'http://sq.some-company.com', + [ScannerProperty.SonarScannerProxyPort]: '4234', + [ScannerProperty.SonarScannerProxyUser]: 'user', + [ScannerProperty.SonarScannerProxyPassword]: 'password', + }; + getProxyUrl(properties); + + expect(getProxyUrl(properties)).toBeUndefined(); + expect(log).toHaveBeenCalledWith( + 'WARN', + `Detecting proxy: Incomplete proxy configuration. Proxy host is missing.`, + ); + }); + + it('should detect proxy with only host on http endpoint', () => { + const properties: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'http://sq.some-company.com', + [ScannerProperty.SonarScannerProxyHost]: 'some-proxy.io', + }; + getProxyUrl(properties); + + expect(getProxyUrl(properties)?.toString()).toBe('http://some-proxy.io/'); + }); + + it('should detect proxy with only host on https endpoint', () => { + const properties: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'https://sq.some-company.com', + [ScannerProperty.SonarScannerProxyHost]: 'some-proxy.io', + }; + getProxyUrl(properties); + + expect(getProxyUrl(properties)?.toString()).toBe('https://some-proxy.io/'); + }); + + it('should detect proxy with host and port', () => { + const properties: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'http://sq.some-company.com', + [ScannerProperty.SonarScannerProxyHost]: 'some-proxy.io', + [ScannerProperty.SonarScannerProxyPort]: '4234', + }; + getProxyUrl(properties); + + expect(getProxyUrl(properties)?.toString()).toBe('http://some-proxy.io:4234/'); + }); + + it('should detect proxy with host, port and authentication', () => { + const properties: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'http://sq.some-company.com', + [ScannerProperty.SonarScannerProxyHost]: 'some-proxy.io', + [ScannerProperty.SonarScannerProxyPort]: '4234', + [ScannerProperty.SonarScannerProxyUser]: 'user', + [ScannerProperty.SonarScannerProxyPassword]: 'password', + }; + getProxyUrl(properties); + + expect(getProxyUrl(properties)?.toString()).toBe('http://user:password@some-proxy.io:4234/'); + }); + }); + + describe('proxyUrlToJavaOptions', () => { + it('should return empty array when no proxy', () => { + const options = proxyUrlToJavaOptions({ + [ScannerProperty.SonarHostUrl]: 'http://sq.some-company.com', + }); + expect(options).toEqual([]); + }); + + it('should return java options for http proxy', () => { + const options = proxyUrlToJavaOptions({ + [ScannerProperty.SonarHostUrl]: 'http://sq.some-company.com', + [ScannerProperty.SonarScannerProxyHost]: 'some-proxy.io', + [ScannerProperty.SonarScannerProxyPort]: '4234', + }); + expect(options).toEqual([ + '-Dhttp.proxyHost=some-proxy.io', + '-Dhttp.proxyPort=4234', + '-Dhttp.proxyUser=', + '-Dhttp.proxyPassword=', + ]); + }); + + it('should return java options for https proxy', () => { + const options = proxyUrlToJavaOptions({ + [ScannerProperty.SonarHostUrl]: 'https://sq.some-company.com', + [ScannerProperty.SonarScannerProxyHost]: 'some-proxy.io', + [ScannerProperty.SonarScannerProxyPort]: '4234', + [ScannerProperty.SonarScannerProxyUser]: 'user', + [ScannerProperty.SonarScannerProxyPassword]: 'password', + }); + expect(options).toEqual([ + '-Dhttps.proxyHost=some-proxy.io', + '-Dhttps.proxyPort=4234', + '-Dhttps.proxyUser=user', + '-Dhttps.proxyPassword=password', + ]); + }); + }); +}); From d7148a4db5373ab5049a873f2194944ea4668b4e Mon Sep 17 00:00:00 2001 From: 7PH Date: Thu, 18 Apr 2024 11:12:22 +0200 Subject: [PATCH 06/35] SCANNPM-2 Allow to override the SonarQube version for internal testing --- src/java.ts | 7 +++---- src/types.ts | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/java.ts b/src/java.ts index 1916f46b..7c5670de 100644 --- a/src/java.ts +++ b/src/java.ts @@ -62,10 +62,9 @@ export async function serverSupportsJREProvisioning( // SonarQube log(LogLevel.DEBUG, 'Detecting SonarQube server version'); - const SQServerInfo = await fetchServerVersion( - parameters[ScannerProperty.SonarHostUrl], - parameters, - ); + const SQServerInfo = + semver.coerce(parameters[ScannerProperty.SonarScannerInternalSqVersion]) ?? + (await fetchServerVersion(parameters[ScannerProperty.SonarHostUrl], parameters)); log(LogLevel.INFO, 'SonarQube server version: ', SQServerInfo.version); const supports = semver.satisfies(SQServerInfo, `>=${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`); diff --git a/src/types.ts b/src/types.ts index 68005708..73133183 100644 --- a/src/types.ts +++ b/src/types.ts @@ -59,6 +59,7 @@ export enum ScannerProperty { SonarScannerProxyUser = 'sonar.scanner.proxyUser', SonarScannerProxyPassword = 'sonar.scanner.proxyPassword', SonarScannerInternalIsSonarCloud = 'sonar.scanner.internal.isSonarCloud', + SonarScannerInternalSqVersion = 'sonar.scanner.internal.sqVersion', } export type ScannerProperties = { From 48afbceb6c134baf8efce117cbc1763902bdb642 Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:27:35 +0200 Subject: [PATCH 07/35] SCANNPM-2 Support JRE provisioning (#116) --- package-lock.json | 23 +++++ package.json | 1 + src/file.ts | 79 +++++++++++++++++ src/java.ts | 86 +++---------------- src/scan.ts | 2 +- src/types.ts | 3 +- test/unit/file.test.ts | 85 +++++++++++++++++++ test/unit/java.test.ts | 146 ++++++++++++++++++++++++++++---- test/unit/mocks/ServerMock.ts | 68 --------------- test/unit/mocks/mock-jre.tar.gz | 0 test/unit/scan.test.ts | 4 +- 11 files changed, 336 insertions(+), 161 deletions(-) create mode 100644 src/file.ts create mode 100644 test/unit/file.test.ts delete mode 100644 test/unit/mocks/ServerMock.ts create mode 100644 test/unit/mocks/mock-jre.tar.gz diff --git a/package-lock.json b/package-lock.json index a31993bf..bbaf58be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "@types/sinon": "17.0.3", "@types/tar-stream": "3.1.3", "@typescript-eslint/parser": "7.4.0", + "axios-mock-adapter": "1.22.0", "chai": "4.4.1", "eslint": "8.57.0", "eslint-plugin-notice": "0.9.10", @@ -1701,6 +1702,19 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-mock-adapter": { + "version": "1.22.0", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz", + "integrity": "sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, "node_modules/b4a": { "version": "1.6.6", "license": "Apache-2.0" @@ -2954,6 +2968,15 @@ "dev": true, "license": "MIT" }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://repox.jfrog.io/repox/api/npm/npm/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/is-core-module": { "version": "2.13.1", "dev": true, diff --git a/package.json b/package.json index 0b14efa3..2fee5d43 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@types/sinon": "17.0.3", "@types/tar-stream": "3.1.3", "@typescript-eslint/parser": "7.4.0", + "axios-mock-adapter": "1.22.0", "chai": "4.4.1", "eslint": "8.57.0", "eslint-plugin-notice": "0.9.10", diff --git a/src/file.ts b/src/file.ts new file mode 100644 index 00000000..bc660682 --- /dev/null +++ b/src/file.ts @@ -0,0 +1,79 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import * as fsExtra from 'fs-extra'; +import AdmZip from 'adm-zip'; +import zlib from 'zlib'; +import tarStream from 'tar-stream'; +import fs from 'fs'; +import path from 'path'; +import { SONAR_CACHE_DIR } from './constants'; +import { log, LogLevel } from './logging'; + +export async function getCachedFileLocation(md5: string, filename: string) { + const filePath = path.join(SONAR_CACHE_DIR, md5, filename); + if (fs.existsSync(filePath)) { + log(LogLevel.INFO, 'Found Cached JRE: ', filePath); + return filePath; + } else { + log(LogLevel.INFO, 'No Cached JRE found'); + return null; + } +} + +export async function extractArchive(fromPath: string, toPath: string) { + log(LogLevel.INFO, `Extracting ${fromPath} to ${toPath}`); + if (fromPath.endsWith('.tar.gz')) { + const tarFilePath = fromPath; + const extract = tarStream.extract(); + + const extractionPromise = new Promise((resolve, reject) => { + extract.on('entry', async (header, stream, next) => { + // Create the full path for the file + const filePath = path.join(toPath, header.name); + + // Ensure the directory exists + await fsExtra.ensureDir(path.dirname(filePath)); + + stream.pipe(fs.createWriteStream(filePath, { mode: header.mode })); + + stream.on('end', next); + + stream.resume(); // just auto drain the stream + }); + + extract.on('finish', () => { + resolve(null); + }); + + extract.on('error', err => { + log(LogLevel.ERROR, 'Error extracting tar.gz', err); + reject(err); + }); + }); + + fs.createReadStream(tarFilePath).pipe(zlib.createGunzip()).pipe(extract); + + await extractionPromise; + } else { + const zip = new AdmZip(fromPath); + zip.extractAllTo(toPath, true); + } +} diff --git a/src/java.ts b/src/java.ts index 7c5670de..c08a4a3e 100644 --- a/src/java.ts +++ b/src/java.ts @@ -18,23 +18,17 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import AdmZip from 'adm-zip'; -import axios from 'axios'; import crypto from 'crypto'; import fs from 'fs'; -import * as fsExtra from 'fs-extra'; import path from 'path'; import semver, { SemVer } from 'semver'; import * as stream from 'stream'; -import tarStream from 'tar-stream'; import { promisify } from 'util'; -import zlib from 'zlib'; import { API_OLD_VERSION_ENDPOINT, API_V2_JRE_ENDPOINT, API_V2_VERSION_ENDPOINT, SONAR_CACHE_DIR, - SONARCLOUD_PRODUCTION_URL, SONARQUBE_JRE_PROVISIONING_MIN_VERSION, UNARCHIVE_SUFFIX, } from './constants'; @@ -42,21 +36,15 @@ import { getHttpAgents } from './http-agent'; import { log, LogLevel } from './logging'; import { getProxyUrl } from './proxy'; import { fetch } from './request'; -import { - JREFullData, - JreMetaData, - PlatformInfo, - ScannerProperties, - ScannerProperty, -} from './types'; +import { JREFullData, PlatformInfo, ScannerProperties, ScannerProperty } from './types'; +import { extractArchive, getCachedFileLocation } from './file'; const finished = promisify(stream.finished); export async function serverSupportsJREProvisioning( parameters: ScannerProperties, - platformInfo: PlatformInfo, ): Promise { - if (parameters[ScannerProperty.SonarScannerInternalIsSonarCloud] !== 'true') { + if (parameters[ScannerProperty.SonarScannerInternalIsSonarCloud] === 'true') { return true; } @@ -72,11 +60,14 @@ export async function serverSupportsJREProvisioning( return supports; } -async function fetchLatestSupportedJRE(serverUrl: string, platformInfo: PlatformInfo) { +async function fetchLatestSupportedJRE(properties: ScannerProperties, platformInfo: PlatformInfo) { + const serverUrl = properties[ScannerProperty.SonarHostUrl]; + const token = properties[ScannerProperty.SonarToken]; + const jreInfoUrl = `${serverUrl}/api/v2/analysis/jres?os=${platformInfo.os}&arch=${platformInfo.arch}`; log(LogLevel.DEBUG, `Downloading JRE from: ${jreInfoUrl}`); - const { data } = await axios.get(jreInfoUrl); + const { data } = await fetch(token, { url: jreInfoUrl }); log(LogLevel.DEBUG, 'file info: ', data); @@ -87,12 +78,11 @@ export async function handleJREProvisioning( properties: ScannerProperties, platformInfo: PlatformInfo, ): Promise { - // TODO: use correct mapping to SC/SQ - const serverUrl = properties[ScannerProperty.SonarHostUrl] ?? SONARCLOUD_PRODUCTION_URL; + const serverUrl = properties[ScannerProperty.SonarHostUrl]; const token = properties[ScannerProperty.SonarToken]; log(LogLevel.DEBUG, 'Detecting latest version of JRE'); - const latestJREData = await fetchLatestSupportedJRE(serverUrl, platformInfo); + const latestJREData = await fetchLatestSupportedJRE(properties, platformInfo); log(LogLevel.INFO, 'Latest Supported JRE: ', latestJREData); log(LogLevel.DEBUG, 'Looking for Cached JRE'); @@ -104,6 +94,9 @@ export async function handleJREProvisioning( const proxyUrl = getProxyUrl(properties); if (cachedJRE) { + log(LogLevel.INFO, 'Using Cached JRE'); + properties[ScannerProperty.SonarScannerWasJRECacheHit] = 'true'; + return { ...latestJREData, jrePath: path.join(cachedJRE, cachedJRE), @@ -166,7 +159,6 @@ export async function handleJREProvisioning( await extractArchive(archivePath, jreDirPath); const jreBinPath = path.join(jreDirPath, latestJREData.javaPath); - log(LogLevel.DEBUG, `JRE downloaded to ${jreDirPath}. Allowing execution on ${jreBinPath}`); return { ...latestJREData, @@ -201,64 +193,12 @@ async function validateChecksum(filePath: string, expectedChecksum: string) { } } -async function extractArchive(fromPath: string, toPath: string) { - log(LogLevel.INFO, `Extracting ${fromPath} to ${toPath}`); - if (fromPath.endsWith('.tar.gz')) { - const tarFilePath = fromPath; - const extract = tarStream.extract(); - - const extractionPromise = new Promise((resolve, reject) => { - extract.on('entry', async (header, stream, next) => { - // Create the full path for the file - const filePath = path.join(toPath, header.name); - - // Ensure the directory exists - await fsExtra.ensureDir(path.dirname(filePath)); - - stream.pipe(fs.createWriteStream(filePath, { mode: header.mode })); - - stream.on('end', next); - - stream.resume(); // just auto drain the stream - }); - - extract.on('finish', () => { - resolve(null); - }); - - extract.on('error', err => { - log(LogLevel.ERROR, 'Error extracting tar.gz', err); - reject(err); - }); - }); - - fs.createReadStream(tarFilePath).pipe(zlib.createGunzip()).pipe(extract); - - await extractionPromise; - } else { - const zip = new AdmZip(fromPath); - zip.extractAllTo(toPath, true); - } -} - -async function getCachedFileLocation(md5: string, filename: string) { - const filePath = path.join(SONAR_CACHE_DIR, md5, filename); - if (fs.existsSync(filePath)) { - log(LogLevel.INFO, 'Found Cached JRE: ', filePath); - return filePath; - } else { - log(LogLevel.INFO, 'No Cached JRE found'); - return null; - } -} - export async function fetchServerVersion( sonarHostUrl: string, parameters: ScannerProperties, ): Promise { const token = parameters[ScannerProperty.SonarToken]; const proxyUrl = getProxyUrl(parameters); - let version: SemVer | null = null; try { // Try and fetch the new version endpoint first diff --git a/src/scan.ts b/src/scan.ts index daad2813..1c57f896 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -49,7 +49,7 @@ export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { log(LogLevel.INFO, 'Platform: ', platformInfo); log(LogLevel.DEBUG, 'Check if Server supports JRE Provisioning'); - const supportsJREProvisioning = await serverSupportsJREProvisioning(properties, platformInfo); + const supportsJREProvisioning = await serverSupportsJREProvisioning(properties); log( LogLevel.INFO, `JRE Provisioning ${supportsJREProvisioning ? 'is ' : 'is NOT '}supported on ${serverUrl}`, diff --git a/src/types.ts b/src/types.ts index 73133183..78d4d7fb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,7 +23,7 @@ export type SupportedOS = NodeJS.Platform | 'alpine'; export type PlatformInfo = { os: SupportedOS | null; - arch: string; + arch: NodeJS.Architecture | null; }; export type JreMetaData = { @@ -53,6 +53,7 @@ export enum ScannerProperty { SonarProjectBaseDir = 'sonar.projectBaseDir', SonarScannerSonarCloudURL = 'sonar.scanner.sonarcloudUrl', SonarScannerJavaExePath = 'sonar.scanner.javaExePath', + SonarScannerWasJRECacheHit = 'sonar.scanner.wasJRECacheHit', SonarScannerWasEngineCacheHit = 'sonar.scanner.wasEngineCacheHit', SonarScannerProxyHost = 'sonar.scanner.proxyHost', SonarScannerProxyPort = 'sonar.scanner.proxyPort', diff --git a/test/unit/file.test.ts b/test/unit/file.test.ts new file mode 100644 index 00000000..fd8f381a --- /dev/null +++ b/test/unit/file.test.ts @@ -0,0 +1,85 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import fs from 'fs'; +import path from 'path'; +import AdmZip from 'adm-zip'; +import { extractArchive, getCachedFileLocation } from '../../src/file'; +import { SONAR_CACHE_DIR } from '../../src/constants'; +import { Readable } from 'stream'; + +// Mock the filesystem +jest.mock('fs', () => ({ + createReadStream: jest.fn().mockImplementation(() => { + const mockStream = new Readable({ + read() { + process.nextTick(() => this.emit('end')); // emit 'end' on next tick + }, + }); + mockStream.pipe = jest.fn().mockReturnThis(); + return mockStream; + }), + createWriteStream: jest.fn(), + existsSync: jest.fn(), +})); + +jest.mock('fs-extra', () => ({})); + +jest.mock('adm-zip', () => { + const MockAdmZip = jest.fn(); + MockAdmZip.prototype.extractAllTo = jest.fn(); + return MockAdmZip; +}); + +describe('extractArchive', () => { + it('should extract zip files to the specified directory', async () => { + const archivePath = 'path/to/archive.zip'; + const extractPath = 'path/to/extract'; + + await extractArchive(archivePath, extractPath); + + const mockAdmZipInstance = (AdmZip as jest.MockedClass).mock.instances[0]; + expect(mockAdmZipInstance.extractAllTo).toHaveBeenCalledWith(extractPath, true); + }); +}); + +describe('getCachedFileLocation', () => { + it('should return the file path if the file exists', async () => { + const md5 = 'md5hash'; + const filename = 'file.txt'; + const filePath = path.join(SONAR_CACHE_DIR, md5, filename); + + (fs.existsSync as jest.Mock).mockReturnValue(true); + + const result = await getCachedFileLocation(md5, filename); + + expect(result).toEqual(filePath); + }); + + it('should return null if the file does not exist', async () => { + const md5 = 'md5hash'; + const filename = 'file.txt'; + + (fs.existsSync as jest.Mock).mockReturnValue(false); + + const result = await getCachedFileLocation(md5, filename); + + expect(result).toBeNull(); + }); +}); diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index 6e7d8e94..7537673a 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -17,15 +17,21 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import path from 'path'; +import fs from 'fs'; +import MockAdapter from 'axios-mock-adapter'; +import { + fetchServerVersion, + handleJREProvisioning, + serverSupportsJREProvisioning, +} from '../../src/java'; +import * as request from '../../src/request'; +import * as file from '../../src/file'; +import { JreMetaData, PlatformInfo, ScannerProperties, ScannerProperty } from '../../src/types'; +import { SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../../src/constants'; +import axios from 'axios'; -import { fetchServerVersion } from '../../src/java'; -import { fetch } from '../../src/request'; -import { ScannerProperties, ScannerProperty } from '../../src/types'; -import { ServerMock } from './mocks/ServerMock'; - -jest.mock('../../src/request'); - -const serverHandler = new ServerMock(); +const mock = new MockAdapter(axios); const MOCKED_PROPERTIES: ScannerProperties = { [ScannerProperty.SonarHostUrl]: 'http://sonarqube.com', @@ -34,30 +40,33 @@ const MOCKED_PROPERTIES: ScannerProperties = { beforeEach(() => { jest.clearAllMocks(); - serverHandler.reset(); + mock.reset(); + jest.spyOn(request, 'fetch'); }); describe('java', () => { describe('version should be detected correctly', () => { it('the SonarQube version should be fetched correctly when new endpoint does not exist', async () => { - serverHandler.mockServerErrorResponse(); - serverHandler.mockServerVersionResponse('3.2.2'); + const token = 'dummy-token'; + mock.onGet('http://sonarqube.com/api/server/version').reply(200, '3.2.2'); + + mock.onGet('http://sonarqube.com/api/v2/analysis/version').reply(404, 'Not Found'); const serverSemver = await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); expect(serverSemver.toString()).toEqual('3.2.2'); - expect(fetch).toHaveBeenCalledTimes(2); + expect(request.fetch).toHaveBeenCalledTimes(2); }); it('the SonarQube version should be fetched correctly using the new endpoint', async () => { - serverHandler.mockServerVersionResponse('3.2.1.12313'); + mock.onGet('http://sonarqube.com/api/server/version').reply(200, '3.2.1.12313'); const serverSemver = await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); expect(serverSemver.toString()).toEqual('3.2.1'); }); it('should fail if both endpoints do not work', async () => { - serverHandler.mockServerErrorResponse(); - serverHandler.mockServerErrorResponse(); + mock.onGet('http://sonarqube.com/api/server/version').reply(404, 'Not Found'); + mock.onGet('http://sonarqube.com/api/v2/server/version').reply(404, 'Not Found'); expect(async () => { await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); @@ -65,11 +74,116 @@ describe('java', () => { }); it('should fail if version can not be parsed', async () => { - serverHandler.mockServerVersionResponse('FORBIDDEN'); + mock + .onGet('http://sonarqube.com/api/server/version') + .reply(200, 'FORBIDDEN'); expect(async () => { await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); }).rejects.toBeDefined(); }); }); + + describe('JRE provisioning should be detected correctly', () => { + it('should return true for sonarcloud', async () => { + expect( + await serverSupportsJREProvisioning({ + [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', + }), + ).toBe(true); + }); + + it(`should return true for SQ version >= ${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`, async () => { + mock.onGet('https://next.sonarqube.com/api/server/version').reply(200, '10.5.0'); + expect( + await serverSupportsJREProvisioning({ + [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', + }), + ).toBe(true); + }); + + it(`should return false for SQ version < ${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`, async () => { + // Define the behavior of the GET request + mock.onGet('https://next.sonarqube.com/api/server/version').reply(200, '9.9.9'); + expect( + await serverSupportsJREProvisioning({ + [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', + }), + ).toBe(false); + }); + }); + + describe('when JRE provisioning is supported', () => { + const platformInfo: PlatformInfo = { os: 'linux', arch: 'arm64' }; + const serverResponse: JreMetaData = { + filename: 'mock-jre.tar.gz', + javaPath: 'jre/bin/java', + md5: 'd41d8cd98f00b204e9800998ecf8427e', + }; + beforeEach(() => { + jest.spyOn(file, 'getCachedFileLocation').mockImplementation((md5, filename) => { + // Your mock implementation here + return Promise.resolve('mocked/path/to/file'); + }); + + jest.spyOn(file, 'extractArchive').mockImplementation((fromPath, toPath) => { + // Your mock implementation here + return Promise.resolve(); + }); + + mock + .onGet( + `https://sonarcloud.io/api/v2/analysis/jres?os=${platformInfo.os}&arch=${platformInfo.arch}`, + ) + .reply(200, serverResponse); + + mock + .onGet(`https://sonarcloud.io/api/v2/analysis/jres/${serverResponse.filename}`) + .reply(200, fs.createReadStream(path.resolve(__dirname, '../unit/mocks/mock-jre.tar.gz'))); + }); + + describe('when the JRE is cached', () => { + it('should fetch the latest supported JRE and use the cached version', async () => { + await handleJREProvisioning( + { + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarToken]: 'mock-token', + }, + platformInfo, + ); + + expect(request.fetch).toHaveBeenCalledTimes(1); + + // check for the cache + expect(file.getCachedFileLocation).toHaveBeenCalledTimes(1); + + expect(file.extractArchive).not.toHaveBeenCalled(); + }); + }); + + describe('when the JRE is not cached', () => { + beforeEach(() => { + jest.spyOn(file, 'getCachedFileLocation').mockImplementation((md5, filename) => { + // Your mock implementation here + return Promise.resolve(null); + }); + }); + it('should download the JRE', async () => { + await handleJREProvisioning( + { + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarToken]: 'mock-token', + }, + platformInfo, + ); + + expect(request.fetch).toHaveBeenCalledTimes(2); + + // check for the cache + expect(file.getCachedFileLocation).toHaveBeenCalledTimes(1); + + expect(file.extractArchive).toHaveBeenCalledTimes(1); + }); + }); + }); }); diff --git a/test/unit/mocks/ServerMock.ts b/test/unit/mocks/ServerMock.ts deleted file mode 100644 index dd85fe53..00000000 --- a/test/unit/mocks/ServerMock.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { AxiosRequestConfig, AxiosResponse } from 'axios'; -import { fetch } from '../../../src/request'; - -jest.mock('../../../src/request'); - -const DEFAULT_AXIOS_RESPONSE: AxiosResponse = { - data: '', - status: 200, - statusText: 'OK', - config: {}, - headers: {}, -} as AxiosResponse; - -export class ServerMock { - responses: (Partial | Error)[] = []; - - constructor() { - jest.mocked(fetch).mockImplementation(this.handleFetch.bind(this)); - } - - mockServerVersionResponse(version: string) { - this.responses.push({ - data: version, - status: 200, - statusText: 'OK', - }); - } - - mockServerErrorResponse() { - this.responses.push(new Error('Not found')); - } - - async handleFetch(_token: string, _config: AxiosRequestConfig) { - if (this.responses.length === 0) { - return { ...DEFAULT_AXIOS_RESPONSE }; - } - - const response = this.responses.shift(); - if (response instanceof Error) { - throw response; - } else { - return { ...DEFAULT_AXIOS_RESPONSE, ...response }; - } - } - - reset() { - this.responses = []; - } -} diff --git a/test/unit/mocks/mock-jre.tar.gz b/test/unit/mocks/mock-jre.tar.gz new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/scan.test.ts b/test/unit/scan.test.ts index b5ff6922..3363e616 100644 --- a/test/unit/scan.test.ts +++ b/test/unit/scan.test.ts @@ -56,11 +56,11 @@ describe('scan', () => { it('should output the current platform', async () => { (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(false); jest.spyOn(logging, 'log'); - jest.spyOn(platform, 'getPlatformInfo').mockReturnValue({ os: 'alpine', arch: 'mock-arch' }); + jest.spyOn(platform, 'getPlatformInfo').mockReturnValue({ os: 'alpine', arch: 'arm64' }); await scan({}, []); expect(logging.log).toHaveBeenNthCalledWith(3, 'INFO', 'Platform: ', { os: 'alpine', - arch: 'mock-arch', + arch: 'arm64', }); }); From 229c6f1785b58cc99aae9ea1b401275be0febb92 Mon Sep 17 00:00:00 2001 From: Benjamin Raymond <31401273+7PH@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:13:17 +0200 Subject: [PATCH 08/35] SCANNPM-2 Use axios instance to re-use fetching logic (#120) --- src/http-agent.ts | 33 ------------ src/java.ts | 57 ++++++++------------ src/request.ts | 50 ++++++++++++++--- src/scan.ts | 15 +++--- test/unit/http-agent.test.ts | 41 -------------- test/unit/java.test.ts | 61 ++++++++++----------- test/unit/platform.test.ts | 4 +- test/unit/request.test.ts | 101 +++++++++++++++++++++++++++++++++++ test/unit/scan.test.ts | 8 +-- 9 files changed, 209 insertions(+), 161 deletions(-) delete mode 100644 src/http-agent.ts delete mode 100644 test/unit/http-agent.test.ts create mode 100644 test/unit/request.test.ts diff --git a/src/http-agent.ts b/src/http-agent.ts deleted file mode 100644 index b23b6e75..00000000 --- a/src/http-agent.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { AxiosRequestConfig } from 'axios'; -import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; - -export function getHttpAgents( - proxyUrl?: URL, -): Pick { - const agents: Pick = {}; - - if (proxyUrl) { - agents.httpsAgent = new HttpsProxyAgent({ proxy: proxyUrl.toString() }); - agents.httpAgent = new HttpProxyAgent({ proxy: proxyUrl.toString() }); - } - return agents; -} diff --git a/src/java.ts b/src/java.ts index c08a4a3e..878ea5f7 100644 --- a/src/java.ts +++ b/src/java.ts @@ -32,12 +32,10 @@ import { SONARQUBE_JRE_PROVISIONING_MIN_VERSION, UNARCHIVE_SUFFIX, } from './constants'; -import { getHttpAgents } from './http-agent'; +import { extractArchive, getCachedFileLocation } from './file'; import { log, LogLevel } from './logging'; -import { getProxyUrl } from './proxy'; import { fetch } from './request'; import { JREFullData, PlatformInfo, ScannerProperties, ScannerProperty } from './types'; -import { extractArchive, getCachedFileLocation } from './file'; const finished = promisify(stream.finished); @@ -52,7 +50,7 @@ export async function serverSupportsJREProvisioning( log(LogLevel.DEBUG, 'Detecting SonarQube server version'); const SQServerInfo = semver.coerce(parameters[ScannerProperty.SonarScannerInternalSqVersion]) ?? - (await fetchServerVersion(parameters[ScannerProperty.SonarHostUrl], parameters)); + (await fetchServerVersion()); log(LogLevel.INFO, 'SonarQube server version: ', SQServerInfo.version); const supports = semver.satisfies(SQServerInfo, `>=${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`); @@ -60,16 +58,21 @@ export async function serverSupportsJREProvisioning( return supports; } -async function fetchLatestSupportedJRE(properties: ScannerProperties, platformInfo: PlatformInfo) { - const serverUrl = properties[ScannerProperty.SonarHostUrl]; - const token = properties[ScannerProperty.SonarToken]; - - const jreInfoUrl = `${serverUrl}/api/v2/analysis/jres?os=${platformInfo.os}&arch=${platformInfo.arch}`; - log(LogLevel.DEBUG, `Downloading JRE from: ${jreInfoUrl}`); +async function fetchLatestSupportedJRE(platformInfo: PlatformInfo) { + log( + LogLevel.DEBUG, + `Downloading JRE for ${platformInfo.os} ${platformInfo.arch} from ${API_V2_JRE_ENDPOINT}`, + ); - const { data } = await fetch(token, { url: jreInfoUrl }); + const { data } = await fetch({ + url: API_V2_JRE_ENDPOINT, + params: { + os: platformInfo.os, + arch: platformInfo.arch, + }, + }); - log(LogLevel.DEBUG, 'file info: ', data); + log(LogLevel.DEBUG, 'JRE information: ', data); return data; } @@ -78,11 +81,8 @@ export async function handleJREProvisioning( properties: ScannerProperties, platformInfo: PlatformInfo, ): Promise { - const serverUrl = properties[ScannerProperty.SonarHostUrl]; - const token = properties[ScannerProperty.SonarToken]; - log(LogLevel.DEBUG, 'Detecting latest version of JRE'); - const latestJREData = await fetchLatestSupportedJRE(properties, platformInfo); + const latestJREData = await fetchLatestSupportedJRE(platformInfo); log(LogLevel.INFO, 'Latest Supported JRE: ', latestJREData); log(LogLevel.DEBUG, 'Looking for Cached JRE'); @@ -91,8 +91,6 @@ export async function handleJREProvisioning( latestJREData.filename + UNARCHIVE_SUFFIX, ); - const proxyUrl = getProxyUrl(properties); - if (cachedJRE) { log(LogLevel.INFO, 'Using Cached JRE'); properties[ScannerProperty.SonarScannerWasJRECacheHit] = 'true'; @@ -120,14 +118,12 @@ export async function handleJREProvisioning( } const writer = fs.createWriteStream(archivePath); - const url = serverUrl + API_V2_JRE_ENDPOINT + `/${latestJREData.filename}`; + const url = `${API_V2_JRE_ENDPOINT}/${latestJREData.filename}`; log(LogLevel.DEBUG, `Downloading ${url} to ${archivePath}`); - const response = await fetch(token, { + const response = await fetch({ url, - method: 'GET', responseType: 'stream', - ...getHttpAgents(proxyUrl), }); const totalLength = response.headers['content-length']; @@ -193,19 +189,13 @@ async function validateChecksum(filePath: string, expectedChecksum: string) { } } -export async function fetchServerVersion( - sonarHostUrl: string, - parameters: ScannerProperties, -): Promise { - const token = parameters[ScannerProperty.SonarToken]; - const proxyUrl = getProxyUrl(parameters); +export async function fetchServerVersion(): Promise { let version: SemVer | null = null; try { // Try and fetch the new version endpoint first log(LogLevel.DEBUG, `Fetching API V2 ${API_V2_VERSION_ENDPOINT}`); - const response = await fetch(token, { - url: sonarHostUrl + API_V2_VERSION_ENDPOINT, - ...getHttpAgents(proxyUrl), + const response = await fetch({ + url: API_V2_VERSION_ENDPOINT, }); version = semver.coerce(response.data); } catch (error: unknown) { @@ -215,9 +205,8 @@ export async function fetchServerVersion( LogLevel.DEBUG, `Unable to fetch API V2 ${API_V2_VERSION_ENDPOINT}: ${error}. Falling back on ${API_OLD_VERSION_ENDPOINT}`, ); - const response = await fetch(token, { - url: sonarHostUrl + API_OLD_VERSION_ENDPOINT, - ...getHttpAgents(proxyUrl), + const response = await fetch({ + url: API_OLD_VERSION_ENDPOINT, }); version = semver.coerce(response.data); } catch (error: unknown) { diff --git a/src/request.ts b/src/request.ts index e523ec14..c860efa0 100644 --- a/src/request.ts +++ b/src/request.ts @@ -17,13 +17,47 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import axios, { AxiosRequestConfig } from 'axios'; +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; +import { getProxyUrl } from './proxy'; +import { ScannerProperties, ScannerProperty } from './types'; -export function fetch(token: string, config: AxiosRequestConfig) { - return axios({ - headers: { - Authorization: `Bearer ${token}`, - }, - ...config, - }); +// The axios instance is private to this module +let _axiosInstance: AxiosInstance | null = null; + +export function getHttpAgents( + properties: ScannerProperties, +): Pick { + const agents: Pick = {}; + const proxyUrl = getProxyUrl(properties); + + if (proxyUrl) { + agents.httpsAgent = new HttpsProxyAgent({ proxy: proxyUrl.toString() }); + agents.httpAgent = new HttpProxyAgent({ proxy: proxyUrl.toString() }); + } + return agents; +} + +export function initializeAxios(properties: ScannerProperties) { + const token = properties[ScannerProperty.SonarToken]; + const baseURL = properties[ScannerProperty.SonarHostUrl]; + const agents = getHttpAgents(properties); + + if (!_axiosInstance) { + _axiosInstance = axios.create({ + baseURL, + headers: { + Authorization: `Bearer ${token}`, + }, + ...agents, + }); + } +} + +export function fetch(config: AxiosRequestConfig) { + if (!_axiosInstance) { + throw new Error('Axios instance is not initialized'); + } + + return _axiosInstance.request(config); } diff --git a/src/scan.ts b/src/scan.ts index 1c57f896..d7c5b41e 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -18,20 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { version } from '../package.json'; import { handleJREProvisioning, serverSupportsJREProvisioning } from './java'; -import { log, LogLevel, setLogLevel } from './logging'; +import { LogLevel, log, setLogLevel } from './logging'; import { getPlatformInfo } from './platform'; import { getProperties } from './properties'; -import { ScannerProperty, JreMetaData, ScanOptions } from './types'; -import { version } from '../package.json'; +import { initializeAxios } from './request'; +import { JreMetaData, ScanOptions, ScannerProperty } from './types'; export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { const startTimestampMs = Date.now(); const properties = getProperties(scanOptions, startTimestampMs, cliArgs); - const serverUrl = properties[ScannerProperty.SonarHostUrl]; - const explicitJREPathOverride = properties[ScannerProperty.SonarScannerJavaExePath]; - if (properties[ScannerProperty.SonarVerbose] === 'true') { setLogLevel(LogLevel.DEBUG); log(LogLevel.DEBUG, 'Setting the log level to DEBUG due to verbose mode'); @@ -42,6 +40,11 @@ export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { log(LogLevel.DEBUG, `Overriding the log level to ${properties[ScannerProperty.SonarLogLevel]}`); } + initializeAxios(properties); + + const serverUrl = properties[ScannerProperty.SonarHostUrl]; + const explicitJREPathOverride = properties[ScannerProperty.SonarScannerJavaExePath]; + log(LogLevel.INFO, 'Version: ', version); log(LogLevel.DEBUG, 'Finding platform info'); diff --git a/test/unit/http-agent.test.ts b/test/unit/http-agent.test.ts deleted file mode 100644 index 6a279fb4..00000000 --- a/test/unit/http-agent.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; -import { getHttpAgents } from '../../src/http-agent'; - -describe('http-agent', () => { - it('should define proxy url correctly', () => { - const proxyUrl = new URL('http://proxy.com'); - - const agents = getHttpAgents(proxyUrl); - expect(agents.httpAgent).toBeInstanceOf(HttpProxyAgent); - expect(agents.httpAgent?.proxy.toString()).toBe(proxyUrl.toString()); - expect(agents.httpsAgent).toBeInstanceOf(HttpsProxyAgent); - expect(agents.httpsAgent?.proxy.toString()).toBe(proxyUrl.toString()); - }); - - it('should not define agents when no proxy is provided', () => { - const agents = getHttpAgents(); - expect(agents.httpAgent).toBeUndefined(); - expect(agents.httpsAgent).toBeUndefined(); - expect(agents).toEqual({}); - }); -}); diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index 7537673a..f17a0020 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -17,19 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import path from 'path'; -import fs from 'fs'; +import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; +import fs from 'fs'; +import path from 'path'; +import { SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../../src/constants'; +import * as file from '../../src/file'; import { fetchServerVersion, handleJREProvisioning, serverSupportsJREProvisioning, } from '../../src/java'; import * as request from '../../src/request'; -import * as file from '../../src/file'; import { JreMetaData, PlatformInfo, ScannerProperties, ScannerProperty } from '../../src/types'; -import { SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../../src/constants'; -import axios from 'axios'; const mock = new MockAdapter(axios); @@ -40,6 +40,7 @@ const MOCKED_PROPERTIES: ScannerProperties = { beforeEach(() => { jest.clearAllMocks(); + request.initializeAxios(MOCKED_PROPERTIES); mock.reset(); jest.spyOn(request, 'fetch'); }); @@ -47,29 +48,28 @@ beforeEach(() => { describe('java', () => { describe('version should be detected correctly', () => { it('the SonarQube version should be fetched correctly when new endpoint does not exist', async () => { - const token = 'dummy-token'; - mock.onGet('http://sonarqube.com/api/server/version').reply(200, '3.2.2'); + mock.onGet('/api/server/version').reply(200, '3.2.2'); - mock.onGet('http://sonarqube.com/api/v2/analysis/version').reply(404, 'Not Found'); + mock.onGet('/api/v2/analysis/version').reply(404, 'Not Found'); - const serverSemver = await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); + const serverSemver = await fetchServerVersion(); expect(serverSemver.toString()).toEqual('3.2.2'); expect(request.fetch).toHaveBeenCalledTimes(2); }); it('the SonarQube version should be fetched correctly using the new endpoint', async () => { - mock.onGet('http://sonarqube.com/api/server/version').reply(200, '3.2.1.12313'); + mock.onGet('/api/server/version').reply(200, '3.2.1.12313'); - const serverSemver = await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); + const serverSemver = await fetchServerVersion(); expect(serverSemver.toString()).toEqual('3.2.1'); }); it('should fail if both endpoints do not work', async () => { - mock.onGet('http://sonarqube.com/api/server/version').reply(404, 'Not Found'); - mock.onGet('http://sonarqube.com/api/v2/server/version').reply(404, 'Not Found'); + mock.onGet('/api/server/version').reply(404, 'Not Found'); + mock.onGet('/api/v2/server/version').reply(404, 'Not Found'); expect(async () => { - await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); + await fetchServerVersion(); }).rejects.toBeDefined(); }); @@ -79,7 +79,7 @@ describe('java', () => { .reply(200, 'FORBIDDEN'); expect(async () => { - await fetchServerVersion('http://sonarqube.com', MOCKED_PROPERTIES); + await fetchServerVersion(); }).rejects.toBeDefined(); }); }); @@ -94,7 +94,7 @@ describe('java', () => { }); it(`should return true for SQ version >= ${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`, async () => { - mock.onGet('https://next.sonarqube.com/api/server/version').reply(200, '10.5.0'); + mock.onGet('/api/server/version').reply(200, '10.5.0'); expect( await serverSupportsJREProvisioning({ [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', @@ -104,7 +104,7 @@ describe('java', () => { it(`should return false for SQ version < ${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`, async () => { // Define the behavior of the GET request - mock.onGet('https://next.sonarqube.com/api/server/version').reply(200, '9.9.9'); + mock.onGet('/api/server/version').reply(200, '9.9.9'); expect( await serverSupportsJREProvisioning({ [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', @@ -121,24 +121,21 @@ describe('java', () => { md5: 'd41d8cd98f00b204e9800998ecf8427e', }; beforeEach(() => { - jest.spyOn(file, 'getCachedFileLocation').mockImplementation((md5, filename) => { - // Your mock implementation here - return Promise.resolve('mocked/path/to/file'); - }); + jest.spyOn(file, 'getCachedFileLocation').mockResolvedValue('mocked/path/to/file'); - jest.spyOn(file, 'extractArchive').mockImplementation((fromPath, toPath) => { - // Your mock implementation here - return Promise.resolve(); - }); + jest.spyOn(file, 'extractArchive').mockResolvedValue(undefined); mock - .onGet( - `https://sonarcloud.io/api/v2/analysis/jres?os=${platformInfo.os}&arch=${platformInfo.arch}`, - ) + .onGet(`/api/v2/analysis/jres`, { + params: { + os: platformInfo.os, + arch: platformInfo.arch, + }, + }) .reply(200, serverResponse); mock - .onGet(`https://sonarcloud.io/api/v2/analysis/jres/${serverResponse.filename}`) + .onGet(`/api/v2/analysis/jres/${serverResponse.filename}`) .reply(200, fs.createReadStream(path.resolve(__dirname, '../unit/mocks/mock-jre.tar.gz'))); }); @@ -163,11 +160,9 @@ describe('java', () => { describe('when the JRE is not cached', () => { beforeEach(() => { - jest.spyOn(file, 'getCachedFileLocation').mockImplementation((md5, filename) => { - // Your mock implementation here - return Promise.resolve(null); - }); + jest.spyOn(file, 'getCachedFileLocation').mockResolvedValue(null); }); + it('should download the JRE', async () => { await handleJREProvisioning( { diff --git a/test/unit/platform.test.ts b/test/unit/platform.test.ts index 0696c4cf..220fe2f7 100644 --- a/test/unit/platform.test.ts +++ b/test/unit/platform.test.ts @@ -18,10 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import * as platform from '../../src/platform'; -import * as logging from '../../src/logging'; import fs from 'fs'; import sinon from 'sinon'; +import * as logging from '../../src/logging'; +import * as platform from '../../src/platform'; describe('getPlatformInfo', () => { it('detect macos', () => { diff --git a/test/unit/request.test.ts b/test/unit/request.test.ts new file mode 100644 index 00000000..a9c4bcb7 --- /dev/null +++ b/test/unit/request.test.ts @@ -0,0 +1,101 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import axios from 'axios'; +import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; +import { fetch, getHttpAgents, initializeAxios } from '../../src/request'; +import { ScannerProperties, ScannerProperty } from '../../src/types'; + +jest.mock('axios', () => ({ + create: jest.fn(), +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('request', () => { + describe('http-agent', () => { + it('should define proxy url correctly', () => { + const agents = getHttpAgents({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarScannerProxyHost]: 'proxy.com', + }); + expect(agents.httpAgent).toBeInstanceOf(HttpProxyAgent); + expect(agents.httpAgent?.proxy.toString()).toBe('https://proxy.com/'); + expect(agents.httpsAgent).toBeInstanceOf(HttpsProxyAgent); + expect(agents.httpsAgent?.proxy.toString()).toBe('https://proxy.com/'); + }); + + it('should not define agents when no proxy is provided', () => { + const agents = getHttpAgents({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + }); + expect(agents.httpAgent).toBeUndefined(); + expect(agents.httpsAgent).toBeUndefined(); + expect(agents).toEqual({}); + }); + }); + + describe('fetch', () => { + it('should initialize axios', () => { + jest.spyOn(axios, 'create'); + + const properties: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarToken]: 'testToken', + }; + + initializeAxios(properties); + + expect(axios.create).toHaveBeenCalledWith({ + baseURL: 'https://sonarcloud.io', + headers: { + Authorization: `Bearer testToken`, + }, + }); + }); + + it('should throw error if axios is not initialized', () => { + expect(() => fetch({})).toThrow('Axios instance is not initialized'); + }); + + it('should call axios request if axios is initialized', () => { + const mockedRequest = jest.fn(); + jest.spyOn(axios, 'create').mockImplementation( + () => + ({ + request: mockedRequest, + }) as any, + ); + + const properties: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarToken]: 'testToken', + }; + + initializeAxios(properties); + + const config = { url: 'https://sonarcloud.io/api/issues/search' }; + + fetch(config); + expect(mockedRequest).toHaveBeenCalledWith(config); + }); + }); +}); diff --git a/test/unit/scan.test.ts b/test/unit/scan.test.ts index 3363e616..242db2c3 100644 --- a/test/unit/scan.test.ts +++ b/test/unit/scan.test.ts @@ -18,10 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { scan } from '../../src/scan'; import * as java from '../../src/java'; -import * as platform from '../../src/platform'; import * as logging from '../../src/logging'; +import * as platform from '../../src/platform'; +import { scan } from '../../src/scan'; jest.mock('../../src/java'); jest.mock('../../src/platform'); @@ -50,7 +50,7 @@ describe('scan', () => { (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(false); jest.spyOn(logging, 'log'); await scan({}, []); - expect(logging.log).toHaveBeenNthCalledWith(1, 'INFO', 'Version: ', 'MOCK.VERSION'); + expect(logging.log).toHaveBeenCalledWith('INFO', 'Version: ', 'MOCK.VERSION'); }); it('should output the current platform', async () => { @@ -58,7 +58,7 @@ describe('scan', () => { jest.spyOn(logging, 'log'); jest.spyOn(platform, 'getPlatformInfo').mockReturnValue({ os: 'alpine', arch: 'arm64' }); await scan({}, []); - expect(logging.log).toHaveBeenNthCalledWith(3, 'INFO', 'Platform: ', { + expect(logging.log).toHaveBeenCalledWith('INFO', 'Platform: ', { os: 'alpine', arch: 'arm64', }); From cf45c7eb5151b7add357fbdafb9f91baee3b9548 Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Tue, 23 Apr 2024 14:52:29 +0200 Subject: [PATCH 09/35] SCANNPM-2 Fetch scanner engine --- jest.config.js | 1 + src/constants.ts | 1 + src/file.ts | 33 +++++- src/java.ts | 176 ++++++++++--------------------- src/proxy.ts | 2 +- src/request.ts | 42 ++++++++ src/scan.ts | 28 ++--- src/scanner-engine.ts | 72 +++++++++++++ test/setup.ts | 27 +++++ test/unit/file.test.ts | 46 +++++++- test/unit/java.test.ts | 14 +-- test/unit/platform.test.ts | 14 +-- test/unit/scan.test.ts | 28 ++--- test/unit/scanner-engine.test.ts | 142 +++++++++++++++++++++++++ 14 files changed, 453 insertions(+), 173 deletions(-) create mode 100644 src/scanner-engine.ts create mode 100644 test/setup.ts create mode 100644 test/unit/scanner-engine.test.ts diff --git a/jest.config.js b/jest.config.js index 30d74bc8..7fae4cab 100644 --- a/jest.config.js +++ b/jest.config.js @@ -29,4 +29,5 @@ module.exports = { testResultsProcessor: 'jest-sonar-reporter', testMatch: ['/test/unit/**/*.test.{js,ts}'], testTimeout: 20000, + setupFilesAfterEnv: ['/test/setup.ts'], }; diff --git a/src/constants.ts b/src/constants.ts index 434438a1..629c098e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -58,3 +58,4 @@ export const DEFAULT_SONAR_EXCLUSIONS = export const API_V2_VERSION_ENDPOINT = '/api/v2/analysis/version'; export const API_OLD_VERSION_ENDPOINT = '/api/server/version'; export const API_V2_JRE_ENDPOINT = '/api/v2/analysis/jres'; +export const API_V2_SCANNER_ENGINE_ENDPOINT = '/api/v2/analysis/engine'; diff --git a/src/file.ts b/src/file.ts index bc660682..c7627574 100644 --- a/src/file.ts +++ b/src/file.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import crypto from 'crypto'; import * as fsExtra from 'fs-extra'; import AdmZip from 'adm-zip'; import zlib from 'zlib'; @@ -30,10 +31,10 @@ import { log, LogLevel } from './logging'; export async function getCachedFileLocation(md5: string, filename: string) { const filePath = path.join(SONAR_CACHE_DIR, md5, filename); if (fs.existsSync(filePath)) { - log(LogLevel.INFO, 'Found Cached JRE: ', filePath); + log(LogLevel.INFO, 'Found Cached: ', filePath); return filePath; } else { - log(LogLevel.INFO, 'No Cached JRE found'); + log(LogLevel.INFO, `No Cache found for ${filePath}`); return null; } } @@ -77,3 +78,31 @@ export async function extractArchive(fromPath: string, toPath: string) { zip.extractAllTo(toPath, true); } } + +async function generateChecksum(filepath: string) { + return new Promise((resolve, reject) => { + fs.readFile(filepath, (err, data) => { + if (err) { + reject(err); + return; + } + resolve(crypto.createHash('md5').update(data).digest('hex')); + }); + }); +} + +export async function validateChecksum(filePath: string, expectedChecksum: string) { + if (expectedChecksum) { + log(LogLevel.INFO, `Verifying checksum ${expectedChecksum}`); + const checksum = await generateChecksum(filePath); + + log(LogLevel.DEBUG, `Checksum Value: ${checksum}`); + if (checksum !== expectedChecksum) { + throw new Error( + `Checksum verification failed for ${filePath}. Expected checksum ${expectedChecksum} but got ${checksum}`, + ); + } + } else { + throw new Error('Checksum not provided'); + } +} diff --git a/src/java.ts b/src/java.ts index 878ea5f7..f3ffd88c 100644 --- a/src/java.ts +++ b/src/java.ts @@ -18,12 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import semver, { SemVer } from 'semver'; -import * as stream from 'stream'; -import { promisify } from 'util'; import { API_OLD_VERSION_ENDPOINT, API_V2_JRE_ENDPOINT, @@ -32,12 +29,45 @@ import { SONARQUBE_JRE_PROVISIONING_MIN_VERSION, UNARCHIVE_SUFFIX, } from './constants'; -import { extractArchive, getCachedFileLocation } from './file'; import { log, LogLevel } from './logging'; -import { fetch } from './request'; +import { fetch, download } from './request'; import { JREFullData, PlatformInfo, ScannerProperties, ScannerProperty } from './types'; +import { extractArchive, getCachedFileLocation, validateChecksum } from './file'; -const finished = promisify(stream.finished); +export async function fetchServerVersion(): Promise { + let version: SemVer | null = null; + try { + // Try and fetch the new version endpoint first + log(LogLevel.DEBUG, `Fetching API V2 ${API_V2_VERSION_ENDPOINT}`); + const response = await fetch({ + url: API_V2_VERSION_ENDPOINT, + }); + version = semver.coerce(response.data); + } catch (error: unknown) { + try { + // If it fails, fallback on deprecated server version endpoint + log( + LogLevel.DEBUG, + `Unable to fetch API V2 ${API_V2_VERSION_ENDPOINT}: ${error}. Falling back on ${API_OLD_VERSION_ENDPOINT}`, + ); + const response = await fetch({ + url: API_OLD_VERSION_ENDPOINT, + }); + version = semver.coerce(response.data); + } catch (error: unknown) { + // If it also failed, give up + log(LogLevel.ERROR, `Failed to fetch server version: ${error}`); + throw error; + } + } + + // If we couldn't parse the version + if (!version) { + throw new Error(`Failed to parse server version "${version}"`); + } + + return version; +} export async function serverSupportsJREProvisioning( parameters: ScannerProperties, @@ -58,29 +88,10 @@ export async function serverSupportsJREProvisioning( return supports; } -async function fetchLatestSupportedJRE(platformInfo: PlatformInfo) { - log( - LogLevel.DEBUG, - `Downloading JRE for ${platformInfo.os} ${platformInfo.arch} from ${API_V2_JRE_ENDPOINT}`, - ); - - const { data } = await fetch({ - url: API_V2_JRE_ENDPOINT, - params: { - os: platformInfo.os, - arch: platformInfo.arch, - }, - }); - - log(LogLevel.DEBUG, 'JRE information: ', data); - - return data; -} - -export async function handleJREProvisioning( +export async function fetchJRE( properties: ScannerProperties, platformInfo: PlatformInfo, -): Promise { +): Promise { log(LogLevel.DEBUG, 'Detecting latest version of JRE'); const latestJREData = await fetchLatestSupportedJRE(platformInfo); log(LogLevel.INFO, 'Latest Supported JRE: ', latestJREData); @@ -107,51 +118,19 @@ export async function handleJREProvisioning( latestJREData.filename + UNARCHIVE_SUFFIX, ); - log(LogLevel.DEBUG, `Extracting JRE from: ${archivePath}`); - log(LogLevel.DEBUG, `Extracting JRE to: ${jreDirPath}`); // Create destination directory if it doesn't exist - const parentCacheDirectory = jreDirPath.substring(0, jreDirPath.lastIndexOf('/')); + const parentCacheDirectory = path.dirname(jreDirPath); if (!fs.existsSync(parentCacheDirectory)) { - log(LogLevel.DEBUG, `Cache directory doesn't exist: ${parentCacheDirectory}`); - log(LogLevel.DEBUG, `Creating cache directory`); + log(LogLevel.DEBUG, `Creating Cache directory as it doesn't exist: ${parentCacheDirectory}`); fs.mkdirSync(parentCacheDirectory, { recursive: true }); } - const writer = fs.createWriteStream(archivePath); - const url = `${API_V2_JRE_ENDPOINT}/${latestJREData.filename}`; - log(LogLevel.DEBUG, `Downloading ${url} to ${archivePath}`); + const url = API_V2_JRE_ENDPOINT + `/${latestJREData.filename}`; - const response = await fetch({ - url, - responseType: 'stream', - }); - - const totalLength = response.headers['content-length']; - let progress = 0; - - response.data.on('data', (chunk: any) => { - progress += chunk.length; - process.stdout.write( - `\r[INFO] Bootstrapper:: Downloaded ${Math.round((progress / totalLength) * 100)}%`, - ); - }); - - response.data.on('end', () => { - console.log(); - log(LogLevel.INFO, 'JRE Download complete'); - }); - - const streamPipeline = promisify(stream.pipeline); - await streamPipeline(response.data, writer); - - response.data.pipe(writer); - - await finished(writer); - log(LogLevel.INFO, `Downloaded JRE to ${archivePath}`); + await download(url, archivePath); await validateChecksum(archivePath, latestJREData.md5); - log(LogLevel.INFO, `Extracting JRE to ${jreDirPath}`); await extractArchive(archivePath, jreDirPath); const jreBinPath = path.join(jreDirPath, latestJREData.javaPath); @@ -163,63 +142,20 @@ export async function handleJREProvisioning( } } -async function generateChecksum(filepath: string) { - return new Promise((resolve, reject) => { - fs.readFile(filepath, (err, data) => { - if (err) { - reject(err); - return; - } - resolve(crypto.createHash('md5').update(data).digest('hex')); - }); - }); -} - -async function validateChecksum(filePath: string, expectedChecksum: string) { - if (expectedChecksum) { - log(LogLevel.INFO, `Verifying checksum ${expectedChecksum}`); - const checksum = await generateChecksum(filePath); - - log(LogLevel.DEBUG, `Checksum Value: ${checksum}`); - if (checksum !== expectedChecksum) { - throw new Error( - `Checksum verification failed for ${filePath}. Expected checksum ${expectedChecksum} but got ${checksum}`, - ); - } - } -} - -export async function fetchServerVersion(): Promise { - let version: SemVer | null = null; - try { - // Try and fetch the new version endpoint first - log(LogLevel.DEBUG, `Fetching API V2 ${API_V2_VERSION_ENDPOINT}`); - const response = await fetch({ - url: API_V2_VERSION_ENDPOINT, - }); - version = semver.coerce(response.data); - } catch (error: unknown) { - try { - // If it fails, fallback on deprecated server version endpoint - log( - LogLevel.DEBUG, - `Unable to fetch API V2 ${API_V2_VERSION_ENDPOINT}: ${error}. Falling back on ${API_OLD_VERSION_ENDPOINT}`, - ); - const response = await fetch({ - url: API_OLD_VERSION_ENDPOINT, - }); - version = semver.coerce(response.data); - } catch (error: unknown) { - // If it also failed, give up - log(LogLevel.ERROR, `Failed to fetch server version: ${error}`); - throw error; - } - } +async function fetchLatestSupportedJRE(platformInfo: PlatformInfo) { + log( + LogLevel.DEBUG, + `Downloading JRE for ${platformInfo.os} ${platformInfo.arch} from ${API_V2_JRE_ENDPOINT}`, + ); - // If we couldn't parse the version - if (!version) { - throw new Error(`Failed to parse server version "${version}"`); - } + const { data } = await fetch({ + url: API_V2_JRE_ENDPOINT, + params: { + os: platformInfo.os, + arch: platformInfo.arch, + }, + }); - return version; + log(LogLevel.DEBUG, 'JRE information: ', data); + return data; } diff --git a/src/proxy.ts b/src/proxy.ts index 45634542..2c0e4996 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -45,7 +45,7 @@ export function getProxyUrl(properties: ScannerProperties): URL | undefined { log(LogLevel.WARN, `Detecting proxy: Incomplete proxy configuration. Proxy host is missing.`); } - log(LogLevel.DEBUG, `Detecting proxy: No proxy detected'}`); + log(LogLevel.DEBUG, 'Detecting proxy: No proxy detected'); return undefined; } diff --git a/src/request.ts b/src/request.ts index c860efa0..a600dcce 100644 --- a/src/request.ts +++ b/src/request.ts @@ -19,8 +19,14 @@ */ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; +import { promisify } from 'util'; +import * as stream from 'stream'; +import fs from 'fs'; import { getProxyUrl } from './proxy'; import { ScannerProperties, ScannerProperty } from './types'; +import { log, LogLevel } from './logging'; + +const finished = promisify(stream.finished); // The axios instance is private to this module let _axiosInstance: AxiosInstance | null = null; @@ -61,3 +67,39 @@ export function fetch(config: AxiosRequestConfig) { return _axiosInstance.request(config); } + +export async function download(url: string, destPath: string) { + log(LogLevel.DEBUG, `Downloading ${url} to ${destPath}`); + + const response = await fetch({ + url, + method: 'GET', + responseType: 'stream', + }); + + const totalLength = response.headers['content-length']; + + if (totalLength) { + let progress = 0; + + response.data.on('data', (chunk: any) => { + progress += chunk.length; + process.stdout.write( + `\r[INFO] Bootstrapper:: Downloaded ${Math.round((progress / totalLength) * 100)}%`, + ); + }); + } else { + log(LogLevel.INFO, 'Download started'); + } + + response.data.on('end', () => { + totalLength && process.stdout.write('\n'); + log(LogLevel.INFO, 'Download complete'); + }); + + const writer = fs.createWriteStream(destPath); + const streamPipeline = promisify(stream.pipeline); + await streamPipeline(response.data, writer); + response.data.pipe(writer); + await finished(writer); +} diff --git a/src/scan.ts b/src/scan.ts index d7c5b41e..0a25a0ac 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -18,13 +18,14 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { version } from '../package.json'; -import { handleJREProvisioning, serverSupportsJREProvisioning } from './java'; -import { LogLevel, log, setLogLevel } from './logging'; +import { fetchJRE, serverSupportsJREProvisioning } from './java'; +import { fetchScannerEngine } from './scanner-engine'; +import { log, LogLevel, setLogLevel } from './logging'; import { getPlatformInfo } from './platform'; import { getProperties } from './properties'; +import { ScannerProperty, ScanOptions, JREFullData } from './types'; +import { version } from '../package.json'; import { initializeAxios } from './request'; -import { JreMetaData, ScanOptions, ScannerProperty } from './types'; export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { const startTimestampMs = Date.now(); @@ -59,20 +60,19 @@ export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { ); // TODO: also check if JRE is explicitly set by properties - let latestJRE: string | JreMetaData = explicitJREPathOverride || 'java'; - if (!explicitJREPathOverride && supportsJREProvisioning) { - await handleJREProvisioning(properties, platformInfo); - } else { + let latestJRE: string | JREFullData = explicitJREPathOverride || 'java'; + let latestScannerEngine; + if (!supportsJREProvisioning) { // TODO: old SQ, support old CLI fetch // https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${version}-${os}.zip } - //TODO: verifyScannerEngine + if (!explicitJREPathOverride) { + latestJRE = await fetchJRE(properties, platformInfo); + } - //TODO: fetchScannerEngine + latestScannerEngine = await fetchScannerEngine(properties); - //TODO: - // ... - properties[ScannerProperty.SonarScannerWasEngineCacheHit] = 'false'; - // ... + //TODO: run the scanner.. + log(LogLevel.INFO, 'Running the scanner ...'); } diff --git a/src/scanner-engine.ts b/src/scanner-engine.ts new file mode 100644 index 00000000..e2eec00d --- /dev/null +++ b/src/scanner-engine.ts @@ -0,0 +1,72 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import fs from 'fs'; +import path from 'path'; +import { log, LogLevel } from './logging'; +import { fetch, download } from './request'; +import { ScannerProperties, ScannerProperty } from './types'; +import { extractArchive, getCachedFileLocation, validateChecksum } from './file'; +import { SONAR_CACHE_DIR, UNARCHIVE_SUFFIX } from './constants'; + +export async function fetchScannerEngine(properties: ScannerProperties) { + log(LogLevel.DEBUG, 'Detecting latest version of Scanner Engine'); + const { data } = await fetch({ + // TODO: replace with /api/v2/analysis/engine + url: '/batch/index', + }); + const [filename, md5] = data.trim().split('|'); + log(LogLevel.INFO, 'Latest Supported Scanner Engine: ', filename); + + log(LogLevel.DEBUG, 'Looking for Cached Scanner Engine'); + + const cachedScannerEngine = await getCachedFileLocation( + md5, // TODO: use sha256 + filename, + ); + + if (cachedScannerEngine) { + log(LogLevel.INFO, 'Using Cached Scanner Engine'); + properties[ScannerProperty.SonarScannerWasEngineCacheHit] = 'true'; + + return cachedScannerEngine; + } + + properties[ScannerProperty.SonarScannerWasEngineCacheHit] = 'false'; + const archivePath = path.join(SONAR_CACHE_DIR, md5, filename); + const scannerEnginePath = path.join(SONAR_CACHE_DIR, md5, filename + UNARCHIVE_SUFFIX); + + // Create destination directory if it doesn't exist + const parentCacheDirectory = path.dirname(scannerEnginePath); + if (!fs.existsSync(parentCacheDirectory)) { + log(LogLevel.DEBUG, `Creating Cache directory as it doesn't exist: ${parentCacheDirectory}`); + fs.mkdirSync(parentCacheDirectory, { recursive: true }); + } + + // TODO: replace with /api/v2/analysis/engine/ + log(LogLevel.DEBUG, `Starting download of Scanner Engine`); + await download(`/batch/file?name=${filename}`, archivePath); + log(LogLevel.INFO, `Downloaded Scanner Engine to ${scannerEnginePath}`); + + await validateChecksum(archivePath, md5); + + log(LogLevel.INFO, `Extracting Scanner Engine to ${scannerEnginePath}`); + await extractArchive(archivePath, scannerEnginePath); + return scannerEnginePath; +} diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 00000000..b5f118a2 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,27 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +// this is to avoid log outputs throughout the tests +jest.mock('../src/logging', () => ({ + ...jest.requireActual('../src/logging'), + log: jest.fn(), + getLogLevel: jest.fn(), + stringToLogLevel: jest.fn(), +})); diff --git a/test/unit/file.test.ts b/test/unit/file.test.ts index fd8f381a..17a68491 100644 --- a/test/unit/file.test.ts +++ b/test/unit/file.test.ts @@ -20,9 +20,10 @@ import fs from 'fs'; import path from 'path'; import AdmZip from 'adm-zip'; -import { extractArchive, getCachedFileLocation } from '../../src/file'; +import { extractArchive, getCachedFileLocation, validateChecksum } from '../../src/file'; import { SONAR_CACHE_DIR } from '../../src/constants'; import { Readable } from 'stream'; +import { readFile } from 'fs-extra'; // Mock the filesystem jest.mock('fs', () => ({ @@ -37,6 +38,7 @@ jest.mock('fs', () => ({ }), createWriteStream: jest.fn(), existsSync: jest.fn(), + readFile: jest.fn(), })); jest.mock('fs-extra', () => ({})); @@ -47,6 +49,10 @@ jest.mock('adm-zip', () => { return MockAdmZip; }); +afterEach(() => { + jest.resetAllMocks(); +}); + describe('extractArchive', () => { it('should extract zip files to the specified directory', async () => { const archivePath = 'path/to/archive.zip'; @@ -65,7 +71,7 @@ describe('getCachedFileLocation', () => { const filename = 'file.txt'; const filePath = path.join(SONAR_CACHE_DIR, md5, filename); - (fs.existsSync as jest.Mock).mockReturnValue(true); + jest.spyOn(fs, 'existsSync').mockReturnValue(true); const result = await getCachedFileLocation(md5, filename); @@ -76,10 +82,44 @@ describe('getCachedFileLocation', () => { const md5 = 'md5hash'; const filename = 'file.txt'; - (fs.existsSync as jest.Mock).mockReturnValue(false); + jest.spyOn(fs, 'existsSync').mockReturnValue(false); const result = await getCachedFileLocation(md5, filename); expect(result).toBeNull(); }); }); + +describe('validateChecksum', () => { + it('should read the file of the path provided', async () => { + jest + .spyOn(fs, 'readFile') + .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); + + await validateChecksum('path/to/file', 'd10b4c3ff123b26dc068d43a8bef2d23'); + + expect(fs.readFile).toHaveBeenCalledWith('path/to/file', expect.any(Function)); + }); + + it('should throw an error if the checksum does not match', async () => { + jest + .spyOn(fs, 'readFile') + .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); + + await expect(validateChecksum('path/to/file', 'invalidchecksum')).rejects.toThrow( + 'Checksum verification failed for path/to/file. Expected checksum invalidchecksum but got d10b4c3ff123b26dc068d43a8bef2d23', + ); + }); + + it('should throw an error if the checksum is not provided', async () => { + await expect(validateChecksum('path/to/file', '')).rejects.toThrow('Checksum not provided'); + }); + + it('should throw an error if the file cannot be read', async () => { + jest + .spyOn(fs, 'readFile') + .mockImplementation((path, cb) => cb(new Error('File not found'), Buffer.from(''))); + + await expect(validateChecksum('path/to/file', 'checksum')).rejects.toThrow('File not found'); + }); +}); diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index f17a0020..a8e7a4b8 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -19,15 +19,7 @@ */ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import fs from 'fs'; -import path from 'path'; -import { SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../../src/constants'; -import * as file from '../../src/file'; -import { - fetchServerVersion, - handleJREProvisioning, - serverSupportsJREProvisioning, -} from '../../src/java'; +import { fetchServerVersion, fetchJRE, serverSupportsJREProvisioning } from '../../src/java'; import * as request from '../../src/request'; import { JreMetaData, PlatformInfo, ScannerProperties, ScannerProperty } from '../../src/types'; @@ -141,7 +133,7 @@ describe('java', () => { describe('when the JRE is cached', () => { it('should fetch the latest supported JRE and use the cached version', async () => { - await handleJREProvisioning( + await fetchJRE( { [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', [ScannerProperty.SonarToken]: 'mock-token', @@ -164,7 +156,7 @@ describe('java', () => { }); it('should download the JRE', async () => { - await handleJREProvisioning( + await fetchJRE( { [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', [ScannerProperty.SonarToken]: 'mock-token', diff --git a/test/unit/platform.test.ts b/test/unit/platform.test.ts index 220fe2f7..fb088b00 100644 --- a/test/unit/platform.test.ts +++ b/test/unit/platform.test.ts @@ -20,7 +20,7 @@ import fs from 'fs'; import sinon from 'sinon'; -import * as logging from '../../src/logging'; +import { log, LogLevel } from '../../src/logging'; import * as platform from '../../src/platform'; describe('getPlatformInfo', () => { @@ -96,7 +96,6 @@ describe('getPlatformInfo', () => { }); it('failed to detect alpine', () => { - const logSpy = sinon.spy(logging, 'log'); const platformStub = sinon.stub(process, 'platform').value('linux'); const archStub = sinon.stub(process, 'arch').value('x64'); @@ -105,15 +104,12 @@ describe('getPlatformInfo', () => { arch: 'x64', }); - expect( - logSpy.calledWith( - logging.LogLevel.ERROR, - 'Failed to read /etc/os-release or /usr/lib/os-release', - ), - ).toBe(true); + expect(log).toHaveBeenCalledWith( + LogLevel.WARN, + 'Failed to read /etc/os-release or /usr/lib/os-release', + ); platformStub.restore(); archStub.restore(); - logSpy.restore(); }); }); diff --git a/test/unit/scan.test.ts b/test/unit/scan.test.ts index 242db2c3..02c4e6d9 100644 --- a/test/unit/scan.test.ts +++ b/test/unit/scan.test.ts @@ -18,6 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +// logging is globally mocked, but in this case the true log output is needed +jest.mock('../../src/logging', () => ({ + ...jest.requireActual('../../src/logging'), + log: jest.fn(), +})); + import * as java from '../../src/java'; import * as logging from '../../src/logging'; import * as platform from '../../src/platform'; @@ -29,8 +35,6 @@ jest.mock('../../package.json', () => ({ version: 'MOCK.VERSION', })); -jest.spyOn(logging, 'setLogLevel'); - beforeEach(() => { jest.clearAllMocks(); }); @@ -47,15 +51,13 @@ describe('scan', () => { }); it('should output the current version of the scanner', async () => { - (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(false); - jest.spyOn(logging, 'log'); + jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); await scan({}, []); expect(logging.log).toHaveBeenCalledWith('INFO', 'Version: ', 'MOCK.VERSION'); }); it('should output the current platform', async () => { - (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(false); - jest.spyOn(logging, 'log'); + jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); jest.spyOn(platform, 'getPlatformInfo').mockReturnValue({ os: 'alpine', arch: 'arm64' }); await scan({}, []); expect(logging.log).toHaveBeenCalledWith('INFO', 'Platform: ', { @@ -66,17 +68,17 @@ describe('scan', () => { describe('when the SQ version does not support JRE provisioning', () => { it('should not fetch the JRE version', async () => { - (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(false); + jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); await scan({}, []); - expect(java.handleJREProvisioning).not.toHaveBeenCalled(); + expect(java.fetchJRE).not.toHaveBeenCalled(); }); }); describe('when the user provides a JRE exe path override', () => { it('should not fetch the JRE version', async () => { - (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(false); + jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); await scan({ options: { 'sonar.scanner.javaExePath': 'path/to/java' } }, []); - expect(java.handleJREProvisioning).not.toHaveBeenCalled(); + expect(java.fetchJRE).not.toHaveBeenCalled(); // TODO: test that the JRE exe path is used when running the scanner engine }); @@ -84,10 +86,10 @@ describe('scan', () => { describe('when the user provides a SonarQube URL and the version supports provisioning', () => { it('should fetch the JRE version', async () => { - (java.serverSupportsJREProvisioning as jest.Mock).mockResolvedValue(true); - jest.spyOn(java, 'handleJREProvisioning'); + jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(true); + jest.spyOn(java, 'fetchJRE'); await scan({ serverUrl: 'http://localhost:9000' }, []); - expect(java.handleJREProvisioning).toHaveBeenCalled(); + expect(java.fetchJRE).toHaveBeenCalled(); }); }); }); diff --git a/test/unit/scanner-engine.test.ts b/test/unit/scanner-engine.test.ts new file mode 100644 index 00000000..50f7baaf --- /dev/null +++ b/test/unit/scanner-engine.test.ts @@ -0,0 +1,142 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import axios from 'axios'; +import fs from 'fs'; +import MockAdapter from 'axios-mock-adapter'; +import { ScannerProperties, ScannerProperty } from '../../src/types'; +import { fetchScannerEngine } from '../../src/scanner-engine'; +import * as file from '../../src/file'; +import * as request from '../../src/request'; +import { Readable } from 'stream'; +const mock = new MockAdapter(axios); + +const MOCKED_PROPERTIES: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'http://sonarqube.com', + [ScannerProperty.SonarToken]: 'dummy-token', +}; + +jest.mock('../../src/constants', () => ({ + ...jest.requireActual('../../src/constants'), + SONAR_CACHE_DIR: 'mocked/path/to/sonar/cache', +})); + +beforeEach(() => { + jest.clearAllMocks(); + mock.reset(); +}); + +describe('scanner-engine', () => { + beforeEach(async () => { + await request.initializeAxios(MOCKED_PROPERTIES); + mock.onGet('/batch/index').reply(200, 'scanner-engine-1.2.3.zip|md5_test'); + mock.onGet('/batch/file?name=scanner-engine-1.2.3.zip').reply(() => { + const readable = new Readable({ + read() { + this.push('md5_test'); + this.push(null); // Indicates end of stream + }, + }); + + return [200, readable]; + }); + + jest.spyOn(file, 'getCachedFileLocation').mockImplementation((md5, filename) => { + return Promise.resolve(null); + }); + + jest.spyOn(file, 'extractArchive').mockImplementation((fromPath, toPath) => { + return Promise.resolve(); + }); + + jest.spyOn(file, 'validateChecksum').mockImplementation(() => { + return Promise.resolve(); + }); + + jest.spyOn(request, 'download').mockImplementation(() => { + return Promise.resolve(); + }); + }); + + describe('fetchScannerEngine', () => { + it('should fetch the latest version of the scanner engine', async () => { + jest.spyOn(file, 'getCachedFileLocation'); + + await fetchScannerEngine(MOCKED_PROPERTIES); + + expect(file.getCachedFileLocation).toHaveBeenCalledWith( + 'md5_test', + 'scanner-engine-1.2.3.zip', + ); + }); + + describe('when the scanner engine is cached', () => { + beforeEach(() => { + jest.spyOn(file, 'getCachedFileLocation').mockImplementation((md5, filename) => { + return Promise.resolve('mocked/path/to/scanner-engine'); + }); + + jest.spyOn(file, 'extractArchive'); + + jest.spyOn(request, 'download'); + }); + + it('should use the cached scanner engine', async () => { + const scannerEngine = await fetchScannerEngine(MOCKED_PROPERTIES); + + expect(file.getCachedFileLocation).toHaveBeenCalledWith( + 'md5_test', + 'scanner-engine-1.2.3.zip', + ); + expect(request.download).not.toHaveBeenCalled(); + expect(file.extractArchive).not.toHaveBeenCalled(); + + expect(scannerEngine).toEqual('mocked/path/to/scanner-engine'); + }); + }); + describe('when the scanner engine is not cached', () => { + it('should create the parent cache directory if it does not exist', async () => { + jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => false); + jest.spyOn(fs, 'mkdirSync').mockImplementationOnce(() => undefined); + await fetchScannerEngine(MOCKED_PROPERTIES); + + expect(fs.existsSync).toHaveBeenCalledWith('mocked/path/to/sonar/cache/md5_test'); + expect(fs.mkdirSync).toHaveBeenCalledWith('mocked/path/to/sonar/cache/md5_test', { + recursive: true, + }); + }); + + it('should download and extract the scanner engine', async () => { + const scannerEngine = await fetchScannerEngine(MOCKED_PROPERTIES); + + expect(file.getCachedFileLocation).toHaveBeenCalledWith( + 'md5_test', + 'scanner-engine-1.2.3.zip', + ); + expect(request.download).toHaveBeenCalledTimes(1); + expect(file.extractArchive).toHaveBeenCalledTimes(1); + + expect(scannerEngine).toEqual( + 'mocked/path/to/sonar/cache/md5_test/scanner-engine-1.2.3.zip_extracted', + ); + }); + }); + }); +}); From d671b390d943e4c2942f6d6861a79aedee0e3260 Mon Sep 17 00:00:00 2001 From: 7PH Date: Thu, 18 Apr 2024 11:05:02 +0200 Subject: [PATCH 10/35] SCANNPM-2 Implement fallback on old bootstrapping logic --- src/constants.ts | 14 +- src/file.ts | 10 +- src/index.ts | 16 +- src/java.ts | 9 +- src/request.ts | 6 +- src/scan.ts | 45 ++-- src/scanner-cli.ts | 148 +++++++++++++ src/types.ts | 3 + test/unit/file.test.ts | 9 +- test/unit/java.test.ts | 11 +- test/unit/mocks/ChildProcessMock.ts | 60 ++++++ test/unit/scanner-cli.test.ts | 322 ++++++++++++++++++++++++++++ 12 files changed, 606 insertions(+), 47 deletions(-) create mode 100644 src/scanner-cli.ts create mode 100644 test/unit/mocks/ChildProcessMock.ts create mode 100644 test/unit/scanner-cli.test.ts diff --git a/src/constants.ts b/src/constants.ts index 629c098e..af3aa7fa 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -33,11 +33,9 @@ export const SONARCLOUD_PRODUCTION_URL = 'https://sonarcloud.io'; export const SONARQUBE_JRE_PROVISIONING_MIN_VERSION = '10.6'; -export const SONAR_CACHE_DIR = path.join( - process.env.HOME ?? process.env.USERPROFILE ?? '', - '.sonar', - 'cache', -); +export const SONAR_DIR = path.join(process.env.HOME ?? process.env.USERPROFILE ?? '', '.sonar'); + +export const SONAR_CACHE_DIR = path.join(SONAR_DIR, 'cache'); export const UNARCHIVE_SUFFIX = '_extracted'; @@ -59,3 +57,9 @@ export const API_V2_VERSION_ENDPOINT = '/api/v2/analysis/version'; export const API_OLD_VERSION_ENDPOINT = '/api/server/version'; export const API_V2_JRE_ENDPOINT = '/api/v2/analysis/jres'; export const API_V2_SCANNER_ENGINE_ENDPOINT = '/api/v2/analysis/engine'; + +export const SCANNER_CLI_DEFAULT_BIN_NAME = 'sonar-scanner'; +export const SCANNER_CLI_VERSION = '5.0.1.3006'; +export const SCANNER_CLI_MIRROR = + 'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/'; +export const SCANNER_CLI_INSTALL_PATH = 'native-sonar-scanner'; diff --git a/src/file.ts b/src/file.ts index c7627574..38d93276 100644 --- a/src/file.ts +++ b/src/file.ts @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import crypto from 'crypto'; -import * as fsExtra from 'fs-extra'; import AdmZip from 'adm-zip'; -import zlib from 'zlib'; -import tarStream from 'tar-stream'; +import crypto from 'crypto'; import fs from 'fs'; +import * as fsExtra from 'fs-extra'; import path from 'path'; +import tarStream from 'tar-stream'; +import zlib from 'zlib'; import { SONAR_CACHE_DIR } from './constants'; import { log, LogLevel } from './logging'; @@ -75,7 +75,7 @@ export async function extractArchive(fromPath: string, toPath: string) { await extractionPromise; } else { const zip = new AdmZip(fromPath); - zip.extractAllTo(toPath, true); + zip.extractAllTo(toPath, true, true); } } diff --git a/src/index.ts b/src/index.ts index d1872f1b..926a29da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,4 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -export { scan } from './scan'; +import { scan } from './scan'; +import { ScanOptions } from './types'; + +export { scan }; + +/** + * TODO: SCANNPM-8 Ensure backwards compatibility and assess what to re-export + */ + +export function customScanner(scanOptions: ScanOptions) { + return scan({ + ...scanOptions, + localScannerCli: true, + }); +} diff --git a/src/java.ts b/src/java.ts index f3ffd88c..4c4abe19 100644 --- a/src/java.ts +++ b/src/java.ts @@ -29,10 +29,10 @@ import { SONARQUBE_JRE_PROVISIONING_MIN_VERSION, UNARCHIVE_SUFFIX, } from './constants'; +import { extractArchive, getCachedFileLocation, validateChecksum } from './file'; import { log, LogLevel } from './logging'; -import { fetch, download } from './request'; +import { download, fetch } from './request'; import { JREFullData, PlatformInfo, ScannerProperties, ScannerProperty } from './types'; -import { extractArchive, getCachedFileLocation, validateChecksum } from './file'; export async function fetchServerVersion(): Promise { let version: SemVer | null = null; @@ -125,9 +125,8 @@ export async function fetchJRE( fs.mkdirSync(parentCacheDirectory, { recursive: true }); } - const url = API_V2_JRE_ENDPOINT + `/${latestJREData.filename}`; - - await download(url, archivePath); + await download(`${API_V2_JRE_ENDPOINT}/${latestJREData.filename}`, archivePath); + log(LogLevel.INFO, `Downloaded JRE to ${archivePath}`); await validateChecksum(archivePath, latestJREData.md5); diff --git a/src/request.ts b/src/request.ts index a600dcce..ac81bf8b 100644 --- a/src/request.ts +++ b/src/request.ts @@ -18,13 +18,13 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import fs from 'fs'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; -import { promisify } from 'util'; import * as stream from 'stream'; -import fs from 'fs'; +import { promisify } from 'util'; +import { LogLevel, log } from './logging'; import { getProxyUrl } from './proxy'; import { ScannerProperties, ScannerProperty } from './types'; -import { log, LogLevel } from './logging'; const finished = promisify(stream.finished); diff --git a/src/scan.ts b/src/scan.ts index 0a25a0ac..abcc02b1 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -18,14 +18,16 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { version } from '../package.json'; +import { SCANNER_CLI_DEFAULT_BIN_NAME } from './constants'; import { fetchJRE, serverSupportsJREProvisioning } from './java'; -import { fetchScannerEngine } from './scanner-engine'; -import { log, LogLevel, setLogLevel } from './logging'; +import { LogLevel, log, setLogLevel } from './logging'; import { getPlatformInfo } from './platform'; import { getProperties } from './properties'; -import { ScannerProperty, ScanOptions, JREFullData } from './types'; -import { version } from '../package.json'; import { initializeAxios } from './request'; +import { downloadScannerCli, runScannerCli, tryLocalSonarScannerExecutable } from './scanner-cli'; +import { fetchScannerEngine } from './scanner-engine'; +import { ScanOptions, ScannerProperty } from './types'; export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { const startTimestampMs = Date.now(); @@ -43,9 +45,6 @@ export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { initializeAxios(properties); - const serverUrl = properties[ScannerProperty.SonarHostUrl]; - const explicitJREPathOverride = properties[ScannerProperty.SonarScannerJavaExePath]; - log(LogLevel.INFO, 'Version: ', version); log(LogLevel.DEBUG, 'Finding platform info'); @@ -54,25 +53,29 @@ export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { log(LogLevel.DEBUG, 'Check if Server supports JRE Provisioning'); const supportsJREProvisioning = await serverSupportsJREProvisioning(properties); - log( - LogLevel.INFO, - `JRE Provisioning ${supportsJREProvisioning ? 'is ' : 'is NOT '}supported on ${serverUrl}`, - ); + log(LogLevel.INFO, `JRE Provisioning ${supportsJREProvisioning ? 'is' : 'is NOT'} supported`); - // TODO: also check if JRE is explicitly set by properties - let latestJRE: string | JREFullData = explicitJREPathOverride || 'java'; - let latestScannerEngine; if (!supportsJREProvisioning) { - // TODO: old SQ, support old CLI fetch - // https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-${version}-${os}.zip - } - - if (!explicitJREPathOverride) { - latestJRE = await fetchJRE(properties, platformInfo); + log(LogLevel.INFO, 'Will download and use sonar-scanner-cli'); + if (scanOptions.localScannerCli) { + log(LogLevel.INFO, 'Local scanner is requested, will not download sonar-scanner-cli'); + if (!(await tryLocalSonarScannerExecutable(SCANNER_CLI_DEFAULT_BIN_NAME))) { + throw new Error('Local scanner is requested but not found'); + } + await runScannerCli(scanOptions, properties, SCANNER_CLI_DEFAULT_BIN_NAME); + } else { + const binPath = await downloadScannerCli(properties); + await runScannerCli(scanOptions, properties, binPath); + } + return; } - latestScannerEngine = await fetchScannerEngine(properties); + // TODO: also check if JRE is explicitly set by properties + const explicitJREPathOverride = properties[ScannerProperty.SonarScannerJavaExePath]; + const latestJRE = explicitJREPathOverride ?? (await fetchJRE(properties, platformInfo)); + const latestScannerEngine = await fetchScannerEngine(properties); //TODO: run the scanner.. + log(LogLevel.INFO, 'Running the scanner ...'); } diff --git a/src/scanner-cli.ts b/src/scanner-cli.ts new file mode 100644 index 00000000..4a315205 --- /dev/null +++ b/src/scanner-cli.ts @@ -0,0 +1,148 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { spawn } from 'child_process'; +import * as fsExtra from 'fs-extra'; +import path from 'path'; +import { + SCANNER_CLI_INSTALL_PATH, + SCANNER_CLI_MIRROR, + SCANNER_CLI_VERSION, + SONAR_DIR, +} from './constants'; +import { extractArchive } from './file'; +import { LogLevel, log } from './logging'; +import { getProxyUrl, proxyUrlToJavaOptions } from './proxy'; +import { download } from './request'; +import { ScanOptions, ScannerProperties, ScannerProperty } from './types'; + +export function normalizePlatformName(): 'windows' | 'linux' | 'macosx' { + if (process.platform.startsWith('win')) { + return 'windows'; + } + if (process.platform.startsWith('linux')) { + return 'linux'; + } + if (process.platform.startsWith('darwin')) { + return 'macosx'; + } + throw Error(`Your platform '${process.platform}' is currently not supported.`); +} + +/** + * Verifies if the provided (or default) command is executable + */ +export async function tryLocalSonarScannerExecutable(command: string): Promise { + return new Promise(resolve => { + log(LogLevel.INFO, `Trying to find a local install of the SonarScanner: ${command}`); + const scannerProcess = spawn(command, ['-v']); + + scannerProcess.on('exit', code => { + if (code === 0) { + log(LogLevel.INFO, 'Local install of SonarScanner CLI found.'); + resolve(true); + } else { + log(LogLevel.INFO, `Local install of SonarScanner CLI (${command}) not found`); + resolve(false); + } + }); + }); +} + +/** + * Where to download the SonarScanner CLI + */ +function getScannerCliUrl(properties: ScannerProperties, version: string): URL { + // Get location to download scanner-cli from + const scannerCliMirror = properties[ScannerProperty.SonarScannerCliMirror] ?? SCANNER_CLI_MIRROR; + const scannerCliFileName = + 'sonar-scanner-cli-' + version + '-' + normalizePlatformName() + '.zip'; + return new URL(scannerCliFileName, scannerCliMirror); +} + +export async function downloadScannerCli(properties: ScannerProperties): Promise { + const token = properties[ScannerProperty.SonarToken]; + const version = properties[ScannerProperty.SonarScannerCliVersion] ?? SCANNER_CLI_VERSION; + if (!/^[\d.]+$/.test(version)) { + throw new Error(`Version "${version}" does not have a correct format."`); + } + + const proxyUrl = getProxyUrl(properties); + const scannerCliUrl = getScannerCliUrl(properties, version); + + // Build paths + const binExt = normalizePlatformName() === 'windows' ? '.bat' : ''; + const dirName = `sonar-scanner-${version}-${normalizePlatformName()}`; + const installDir = path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH); + const archivePath = path.join(installDir, `${dirName}.zip`); + const binPath = path.join(installDir, dirName, 'bin', `sonar-scanner${binExt}`); + + // Try and execute an already downloaded scanner, which should be at the same location + if (await tryLocalSonarScannerExecutable(binPath)) { + return binPath; + } + + // Create parent directory if needed + await fsExtra.ensureDir(installDir); + + // Download SonarScanner CLI + log(LogLevel.INFO, 'Downloading SonarScanner CLI'); + await download(scannerCliUrl.href, archivePath); + + log(LogLevel.INFO, `Extracting SonarScanner CLI archive`); + extractArchive(archivePath, installDir); + + return binPath; +} + +export async function runScannerCli( + scanOptions: ScanOptions, + properties: ScannerProperties, + binPath: string, +) { + log(LogLevel.INFO, 'Starting analysis'); + + const options = [...(scanOptions.jvmOptions ?? []), ...proxyUrlToJavaOptions(properties)]; + const scannerProcess = spawn(binPath, options, { + env: { + ...process.env, + SONARQUBE_SCANNER_PARAMS: JSON.stringify(properties), + }, + }); + + return new Promise((resolve, reject) => { + scannerProcess.stdout.on('data', data => { + for (const line of data.toString().trim().split('\n')) { + log(LogLevel.INFO, line); + } + }); + scannerProcess.stderr.on('data', data => { + log(LogLevel.ERROR, data.toString().trim()); + }); + scannerProcess.on('exit', code => { + if (code === 0) { + log(LogLevel.INFO, 'SonarScanner CLI finished successfully'); + resolve(); + } else { + log(LogLevel.ERROR, `SonarScanner CLI failed with code ${code}`); + reject(new Error(`SonarScanner CLI failed with code ${code}`)); + } + }); + }); +} diff --git a/src/types.ts b/src/types.ts index 78d4d7fb..d303d0ff 100644 --- a/src/types.ts +++ b/src/types.ts @@ -61,6 +61,8 @@ export enum ScannerProperty { SonarScannerProxyPassword = 'sonar.scanner.proxyPassword', SonarScannerInternalIsSonarCloud = 'sonar.scanner.internal.isSonarCloud', SonarScannerInternalSqVersion = 'sonar.scanner.internal.sqVersion', + SonarScannerCliVersion = 'sonar.scanner.version', + SonarScannerCliMirror = 'sonar.scanner.mirror', } export type ScannerProperties = { @@ -71,6 +73,7 @@ export type ScanOptions = { serverUrl?: string; token?: string; jvmOptions?: string[]; + localScannerCli?: boolean; options?: { [key: string]: string }; caPath?: string; logLevel?: string; diff --git a/test/unit/file.test.ts b/test/unit/file.test.ts index 17a68491..f6f681ed 100644 --- a/test/unit/file.test.ts +++ b/test/unit/file.test.ts @@ -17,13 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import AdmZip from 'adm-zip'; import fs from 'fs'; import path from 'path'; -import AdmZip from 'adm-zip'; -import { extractArchive, getCachedFileLocation, validateChecksum } from '../../src/file'; -import { SONAR_CACHE_DIR } from '../../src/constants'; import { Readable } from 'stream'; -import { readFile } from 'fs-extra'; +import { SONAR_CACHE_DIR } from '../../src/constants'; +import { extractArchive, getCachedFileLocation, validateChecksum } from '../../src/file'; // Mock the filesystem jest.mock('fs', () => ({ @@ -61,7 +60,7 @@ describe('extractArchive', () => { await extractArchive(archivePath, extractPath); const mockAdmZipInstance = (AdmZip as jest.MockedClass).mock.instances[0]; - expect(mockAdmZipInstance.extractAllTo).toHaveBeenCalledWith(extractPath, true); + expect(mockAdmZipInstance.extractAllTo).toHaveBeenCalledWith(extractPath, true, true); }); }); diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index a8e7a4b8..3b5552e2 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -19,7 +19,11 @@ */ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { fetchServerVersion, fetchJRE, serverSupportsJREProvisioning } from '../../src/java'; +import fs from 'fs'; +import path from 'path'; +import { SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../../src/constants'; +import * as file from '../../src/file'; +import { fetchJRE, fetchServerVersion, serverSupportsJREProvisioning } from '../../src/java'; import * as request from '../../src/request'; import { JreMetaData, PlatformInfo, ScannerProperties, ScannerProperty } from '../../src/types'; @@ -35,6 +39,7 @@ beforeEach(() => { request.initializeAxios(MOCKED_PROPERTIES); mock.reset(); jest.spyOn(request, 'fetch'); + jest.spyOn(request, 'download'); }); describe('java', () => { @@ -142,6 +147,7 @@ describe('java', () => { ); expect(request.fetch).toHaveBeenCalledTimes(1); + expect(request.download).not.toHaveBeenCalled(); // check for the cache expect(file.getCachedFileLocation).toHaveBeenCalledTimes(1); @@ -164,7 +170,8 @@ describe('java', () => { platformInfo, ); - expect(request.fetch).toHaveBeenCalledTimes(2); + expect(request.fetch).toHaveBeenCalledTimes(1); + expect(request.download).toHaveBeenCalledTimes(1); // check for the cache expect(file.getCachedFileLocation).toHaveBeenCalledTimes(1); diff --git a/test/unit/mocks/ChildProcessMock.ts b/test/unit/mocks/ChildProcessMock.ts new file mode 100644 index 00000000..cb14fa4a --- /dev/null +++ b/test/unit/mocks/ChildProcessMock.ts @@ -0,0 +1,60 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { spawn } from 'child_process'; + +export class ChildProcessMock { + private exitCode: number = 0; + + private stdout: string = ''; + + private stderr: string = ''; + + constructor() { + jest.mocked(spawn).mockImplementation((this.handleSpawn as any).bind(this)); + } + + setExitCode(exitCode: number) { + this.exitCode = exitCode; + } + + setOutput(stdout?: string, stderr?: string) { + this.stdout = stdout ?? ''; + this.stderr = stderr ?? ''; + } + + handleSpawn() { + return { + on: jest.fn().mockImplementation((event, callback) => { + if (event === 'exit') { + callback(this.exitCode); + } + }), + stdout: { on: jest.fn().mockImplementation((event, callback) => callback(this.stdout)) }, + stderr: { on: jest.fn().mockImplementation((event, callback) => callback(this.stderr)) }, + }; + } + + reset() { + this.exitCode = 0; + this.stdout = ''; + this.stderr = ''; + jest.clearAllMocks(); + } +} diff --git a/test/unit/scanner-cli.test.ts b/test/unit/scanner-cli.test.ts new file mode 100644 index 00000000..9a1f4169 --- /dev/null +++ b/test/unit/scanner-cli.test.ts @@ -0,0 +1,322 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { spawn } from 'child_process'; +import path from 'path'; +import sinon from 'sinon'; +import { + SCANNER_CLI_DEFAULT_BIN_NAME, + SCANNER_CLI_INSTALL_PATH, + SONAR_DIR, +} from '../../src/constants'; +import { extractArchive } from '../../src/file'; +import { LogLevel, log } from '../../src/logging'; +import { download } from '../../src/request'; +import { + downloadScannerCli, + normalizePlatformName, + runScannerCli, + tryLocalSonarScannerExecutable, +} from '../../src/scanner-cli'; +import { ScannerProperty } from '../../src/types'; +import { ChildProcessMock } from './mocks/ChildProcessMock'; + +jest.mock('child_process'); +jest.mock('../../src/request'); +jest.mock('../../src/file'); +jest.mock('../../src/logging'); + +const childProcessHandler = new ChildProcessMock(); + +beforeEach(() => { + childProcessHandler.reset(); +}); + +describe('scanner-cli', () => { + describe('tryLocalSonarScannerExecutable', () => { + it('should detect locally installed scanner-cli', async () => { + expect(await tryLocalSonarScannerExecutable(SCANNER_CLI_DEFAULT_BIN_NAME)).toBe(true); + }); + + it('should not detect locally installed scanner-cli', async () => { + childProcessHandler.setExitCode(1); + + expect(await tryLocalSonarScannerExecutable(SCANNER_CLI_DEFAULT_BIN_NAME)).toBe(false); + }); + }); + + describe('downloadScannerCli', function () { + it('should reject invalid versions', () => { + expect( + downloadScannerCli({ + [ScannerProperty.SonarScannerCliVersion]: 'not a version', + }), + ).rejects.toBeDefined(); + }); + + it('should use already downloaded version', async () => { + const stub = sinon.stub(process, 'platform').value('linux'); + + expect( + await downloadScannerCli({ + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + }), + ).toBe( + path.join( + SONAR_DIR, + SCANNER_CLI_INSTALL_PATH, + 'sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner', + ), + ); + expect(download).not.toHaveBeenCalled(); + + stub.restore(); + }); + + it('should download SonarScanner CLI if it does not exist on Unix', async () => { + childProcessHandler.setExitCode(1); + const stub = sinon.stub(process, 'platform').value('linux'); + + const binPath = await downloadScannerCli({ + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + }); + + expect( + await downloadScannerCli({ + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + }), + ).toBe( + path.join( + SONAR_DIR, + SCANNER_CLI_INSTALL_PATH, + 'sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner', + ), + ); + expect(download).toHaveBeenLastCalledWith( + 'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-linux.zip', + path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-linux.zip'), + ); + expect(extractArchive).toHaveBeenLastCalledWith( + path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-linux.zip'), + path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH), + ); + expect(binPath).toBe( + path.join( + SONAR_DIR, + SCANNER_CLI_INSTALL_PATH, + 'sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner', + ), + ); + + stub.restore(); + }); + + it('should download SonarScanner CLI if it does not exist on Windows', async () => { + childProcessHandler.setExitCode(1); + const stub = sinon.stub(process, 'platform').value('win32'); + + const binPath = await downloadScannerCli({ + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + }); + + expect(download).toHaveBeenLastCalledWith( + 'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-windows.zip', + path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-windows.zip'), + ); + expect(extractArchive).toHaveBeenLastCalledWith( + path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-windows.zip'), + path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH), + ); + expect(binPath).toBe( + path.join( + SONAR_DIR, + SCANNER_CLI_INSTALL_PATH, + 'sonar-scanner-5.0.1.3006-windows/bin/sonar-scanner.bat', + ), + ); + + stub.restore(); + }); + }); + + describe('runScannerCli', function () { + it('should pass jvmOptions and scanner properties to scanner', async () => { + await runScannerCli( + { + jvmOptions: ['-Xmx512m'], + }, + { + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + }, + 'sonar-scanner', + ); + + expect(spawn).toHaveBeenCalledTimes(1); + const [command, args, options] = (spawn as jest.Mock).mock.calls.pop(); + expect(command).toBe('sonar-scanner'); + expect(args).toEqual(['-Xmx512m']); + expect(options.env.SONARQUBE_SCANNER_PARAMS).toBe( + JSON.stringify({ + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + }), + ); + }); + + it('should display SonarScanner CLI output', async () => { + childProcessHandler.setOutput('the output', 'some error'); + + await runScannerCli( + {}, + { + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'https://localhost:9000', + }, + 'sonar-scanner', + ); + + expect(log).toHaveBeenCalledWith(LogLevel.ERROR, 'some error'); + expect(log).toHaveBeenCalledWith(LogLevel.INFO, 'the output'); + }); + + it('should reject if SonarScanner CLI fails', async () => { + childProcessHandler.setExitCode(1); + + await expect( + runScannerCli( + {}, + { + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + }, + 'sonar-scanner', + ), + ).rejects.toBeDefined(); + + expect(log).toHaveBeenCalledWith(LogLevel.ERROR, 'SonarScanner CLI failed with code 1'); + }); + + it('should pass proxy options to scanner', async () => { + await runScannerCli( + {}, + { + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + [ScannerProperty.SonarScannerProxyHost]: 'proxy', + [ScannerProperty.SonarScannerProxyPort]: '9000', + [ScannerProperty.SonarScannerProxyUser]: 'some-user', + [ScannerProperty.SonarScannerProxyPassword]: 'password', + }, + 'sonar-scanner', + ); + + expect(spawn).toHaveBeenCalledTimes(1); + const [command, args, options] = (spawn as jest.Mock).mock.calls.pop(); + expect(command).toBe('sonar-scanner'); + expect(args).toEqual([ + '-Dhttp.proxyHost=proxy', + '-Dhttp.proxyPort=9000', + '-Dhttp.proxyUser=some-user', + '-Dhttp.proxyPassword=password', + ]); + expect(options.env.SONARQUBE_SCANNER_PARAMS).toBe( + JSON.stringify({ + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + [ScannerProperty.SonarScannerProxyHost]: 'proxy', + [ScannerProperty.SonarScannerProxyPort]: '9000', + [ScannerProperty.SonarScannerProxyUser]: 'some-user', + [ScannerProperty.SonarScannerProxyPassword]: 'password', + }), + ); + }); + + it('should pass https proxy options to scanner', async () => { + await runScannerCli( + {}, + { + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'https://localhost:9000', + [ScannerProperty.SonarScannerProxyHost]: 'proxy', + [ScannerProperty.SonarScannerProxyPort]: '9000', + [ScannerProperty.SonarScannerProxyUser]: 'some-user', + [ScannerProperty.SonarScannerProxyPassword]: 'password', + }, + 'sonar-scanner', + ); + + expect(spawn).toHaveBeenCalledTimes(1); + const [command, args, options] = (spawn as jest.Mock).mock.calls.pop(); + expect(command).toBe('sonar-scanner'); + expect(args).toEqual([ + '-Dhttps.proxyHost=proxy', + '-Dhttps.proxyPort=9000', + '-Dhttps.proxyUser=some-user', + '-Dhttps.proxyPassword=password', + ]); + expect(options.env.SONARQUBE_SCANNER_PARAMS).toBe( + JSON.stringify({ + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'https://localhost:9000', + [ScannerProperty.SonarScannerProxyHost]: 'proxy', + [ScannerProperty.SonarScannerProxyPort]: '9000', + [ScannerProperty.SonarScannerProxyUser]: 'some-user', + [ScannerProperty.SonarScannerProxyPassword]: 'password', + }), + ); + }); + }); + + describe('normalizePlatformName', function () { + it('detect Windows', function () { + const stub = sinon.stub(process, 'platform').value('windows10'); + + expect(normalizePlatformName()).toEqual('windows'); + stub.restore(); + }); + + it('detect Mac', function () { + const stub = sinon.stub(process, 'platform').value('darwin'); + + expect(normalizePlatformName()).toEqual('macosx'); + stub.restore(); + }); + + it('detect Linux', function () { + const stub = sinon.stub(process, 'platform').value('linux'); + + expect(normalizePlatformName()).toEqual('linux'); + stub.restore(); + }); + + it('throw if something else', function () { + const stub = sinon.stub(process, 'platform').value('non-existing-os'); + + expect(normalizePlatformName).toThrow( + new Error(`Your platform 'non-existing-os' is currently not supported.`), + ); + stub.restore(); + }); + }); +}); From 63fcba243fad9cadb831a38ad230f874817cb393 Mon Sep 17 00:00:00 2001 From: 7PH Date: Tue, 23 Apr 2024 14:40:39 +0200 Subject: [PATCH 11/35] SCANNPM-2 Log top-level errors & Adjust scan method --- src/scan.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/scan.ts b/src/scan.ts index abcc02b1..2f53ef79 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -30,6 +30,14 @@ import { fetchScannerEngine } from './scanner-engine'; import { ScanOptions, ScannerProperty } from './types'; export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { + try { + await runScan(scanOptions, cliArgs); + } catch (error: any) { + log(LogLevel.ERROR, `An error occurred: ${error?.message ?? error}`); + } +} + +async function runScan(scanOptions: ScanOptions, cliArgs?: string[]) { const startTimestampMs = Date.now(); const properties = getProperties(scanOptions, startTimestampMs, cliArgs); @@ -45,7 +53,8 @@ export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { initializeAxios(properties); - log(LogLevel.INFO, 'Version: ', version); + log(LogLevel.INFO, `Server URL: ${properties[ScannerProperty.SonarHostUrl]}`); + log(LogLevel.INFO, `Version: ${version}`); log(LogLevel.DEBUG, 'Finding platform info'); const platformInfo = getPlatformInfo(); From 4e166181236d2b75996f193c5553d263c90a6342 Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Wed, 24 Apr 2024 17:07:57 +0200 Subject: [PATCH 12/35] SCANNPM-2 Support custom cache location (#123) --- src/constants.ts | 4 +- src/file.ts | 35 ++++++- src/java.ts | 33 +++---- src/properties.ts | 9 ++ src/scan.ts | 2 + src/scanner-cli.ts | 6 +- src/scanner-engine.ts | 31 +++---- src/types.ts | 2 + test/unit/file.test.ts | 141 +++++++++++++++++++---------- test/unit/java.test.ts | 69 +++++++------- test/unit/mocks/FakeProjectMock.ts | 1 + test/unit/scan.test.ts | 9 +- test/unit/scanner-cli.test.ts | 110 +++++++++------------- test/unit/scanner-engine.test.ts | 70 +++++--------- 14 files changed, 275 insertions(+), 247 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index af3aa7fa..40065eab 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -33,9 +33,9 @@ export const SONARCLOUD_PRODUCTION_URL = 'https://sonarcloud.io'; export const SONARQUBE_JRE_PROVISIONING_MIN_VERSION = '10.6'; -export const SONAR_DIR = path.join(process.env.HOME ?? process.env.USERPROFILE ?? '', '.sonar'); +export const SONAR_DIR_DEFAULT = '.sonar'; -export const SONAR_CACHE_DIR = path.join(SONAR_DIR, 'cache'); +export const SONAR_CACHE_DIR = 'cache'; export const UNARCHIVE_SUFFIX = '_extracted'; diff --git a/src/file.ts b/src/file.ts index 38d93276..77232f17 100644 --- a/src/file.ts +++ b/src/file.ts @@ -25,11 +25,15 @@ import * as fsExtra from 'fs-extra'; import path from 'path'; import tarStream from 'tar-stream'; import zlib from 'zlib'; -import { SONAR_CACHE_DIR } from './constants'; +import { SONAR_CACHE_DIR, UNARCHIVE_SUFFIX } from './constants'; import { log, LogLevel } from './logging'; +import { CacheFileData, ScannerProperties, ScannerProperty } from './types'; -export async function getCachedFileLocation(md5: string, filename: string) { - const filePath = path.join(SONAR_CACHE_DIR, md5, filename); +export async function getCacheFileLocation( + properties: ScannerProperties, + { md5, filename }: CacheFileData, +) { + const filePath = path.join(getParentCacheDirectory(properties), md5, filename); if (fs.existsSync(filePath)) { log(LogLevel.INFO, 'Found Cached: ', filePath); return filePath; @@ -106,3 +110,28 @@ export async function validateChecksum(filePath: string, expectedChecksum: strin throw new Error('Checksum not provided'); } } + +export async function getCacheDirectories( + properties: ScannerProperties, + { md5, filename }: CacheFileData, +) { + const archivePath = path.join(getParentCacheDirectory(properties), md5, filename); + const unarchivePath = path.join( + getParentCacheDirectory(properties), + md5, + filename + UNARCHIVE_SUFFIX, + ); + + // Create destination directory if it doesn't exist + const parentCacheDirectory = path.dirname(unarchivePath); + if (!fs.existsSync(parentCacheDirectory)) { + log(LogLevel.DEBUG, `Creating Cache directory as it doesn't exist: ${parentCacheDirectory}`); + fs.mkdirSync(parentCacheDirectory, { recursive: true }); + } + + return { archivePath, unarchivePath }; +} + +function getParentCacheDirectory(properties: ScannerProperties) { + return path.join(properties[ScannerProperty.SonarUserHome], SONAR_CACHE_DIR); +} diff --git a/src/java.ts b/src/java.ts index 4c4abe19..ef5303b9 100644 --- a/src/java.ts +++ b/src/java.ts @@ -18,18 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import fs from 'fs'; import path from 'path'; import semver, { SemVer } from 'semver'; import { API_OLD_VERSION_ENDPOINT, API_V2_JRE_ENDPOINT, API_V2_VERSION_ENDPOINT, - SONAR_CACHE_DIR, SONARQUBE_JRE_PROVISIONING_MIN_VERSION, UNARCHIVE_SUFFIX, } from './constants'; -import { extractArchive, getCachedFileLocation, validateChecksum } from './file'; +import { + extractArchive, + getCacheDirectories, + getCacheFileLocation, + validateChecksum, +} from './file'; import { log, LogLevel } from './logging'; import { download, fetch } from './request'; import { JREFullData, PlatformInfo, ScannerProperties, ScannerProperty } from './types'; @@ -97,11 +100,10 @@ export async function fetchJRE( log(LogLevel.INFO, 'Latest Supported JRE: ', latestJREData); log(LogLevel.DEBUG, 'Looking for Cached JRE'); - const cachedJRE = await getCachedFileLocation( - latestJREData.md5, - latestJREData.filename + UNARCHIVE_SUFFIX, - ); - + const cachedJRE = await getCacheFileLocation(properties, { + md5: latestJREData.md5, + filename: latestJREData.filename + UNARCHIVE_SUFFIX, + }); if (cachedJRE) { log(LogLevel.INFO, 'Using Cached JRE'); properties[ScannerProperty.SonarScannerWasJRECacheHit] = 'true'; @@ -111,20 +113,11 @@ export async function fetchJRE( jrePath: path.join(cachedJRE, cachedJRE), }; } else { - const archivePath = path.join(SONAR_CACHE_DIR, latestJREData.md5, latestJREData.filename); - const jreDirPath = path.join( - SONAR_CACHE_DIR, - latestJREData.md5, - latestJREData.filename + UNARCHIVE_SUFFIX, + const { archivePath, unarchivePath: jreDirPath } = await getCacheDirectories( + properties, + latestJREData, ); - // Create destination directory if it doesn't exist - const parentCacheDirectory = path.dirname(jreDirPath); - if (!fs.existsSync(parentCacheDirectory)) { - log(LogLevel.DEBUG, `Creating Cache directory as it doesn't exist: ${parentCacheDirectory}`); - fs.mkdirSync(parentCacheDirectory, { recursive: true }); - } - await download(`${API_V2_JRE_ENDPOINT}/${latestJREData.filename}`, archivePath); log(LogLevel.INFO, `Downloaded JRE to ${archivePath}`); diff --git a/src/properties.ts b/src/properties.ts index fa18440b..1a2a9097 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -28,11 +28,19 @@ import { SCANNER_BOOTSTRAPPER_NAME, SONARCLOUD_URL, SONARCLOUD_URL_REGEX, + SONAR_DIR_DEFAULT, SONAR_PROJECT_FILENAME, } from './constants'; import { LogLevel, log } from './logging'; import { ScanOptions, ScannerProperties, ScannerProperty } from './types'; +const DEFAULT_PROPERTIES = { + [ScannerProperty.SonarUserHome]: path.join( + process.env.HOME ?? process.env.USERPROFILE ?? '', + SONAR_DIR_DEFAULT, + ), +}; + /** * Convert the name of a sonar property from its environment variable form * (eg SONAR_SCANNER_FOO_BAR) to its sonar form (eg sonar.scanner.fooBar). @@ -336,6 +344,7 @@ export function getProperties( scanOptionsProperties, inferredProperties, envProperties, // Lowest precedence + DEFAULT_PROPERTIES, // fallback to default if nothing was provided for these properties ] .reverse() .reduce((acc, curr) => ({ ...acc, ...curr }), {}); diff --git a/src/scan.ts b/src/scan.ts index 2f53ef79..2006d26d 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -51,6 +51,8 @@ async function runScan(scanOptions: ScanOptions, cliArgs?: string[]) { log(LogLevel.DEBUG, `Overriding the log level to ${properties[ScannerProperty.SonarLogLevel]}`); } + log(LogLevel.DEBUG, 'Properties: ', properties); + initializeAxios(properties); log(LogLevel.INFO, `Server URL: ${properties[ScannerProperty.SonarHostUrl]}`); diff --git a/src/scanner-cli.ts b/src/scanner-cli.ts index 4a315205..1e6bd77d 100644 --- a/src/scanner-cli.ts +++ b/src/scanner-cli.ts @@ -24,7 +24,7 @@ import { SCANNER_CLI_INSTALL_PATH, SCANNER_CLI_MIRROR, SCANNER_CLI_VERSION, - SONAR_DIR, + SONAR_CACHE_DIR, } from './constants'; import { extractArchive } from './file'; import { LogLevel, log } from './logging'; @@ -77,19 +77,17 @@ function getScannerCliUrl(properties: ScannerProperties, version: string): URL { } export async function downloadScannerCli(properties: ScannerProperties): Promise { - const token = properties[ScannerProperty.SonarToken]; const version = properties[ScannerProperty.SonarScannerCliVersion] ?? SCANNER_CLI_VERSION; if (!/^[\d.]+$/.test(version)) { throw new Error(`Version "${version}" does not have a correct format."`); } - const proxyUrl = getProxyUrl(properties); const scannerCliUrl = getScannerCliUrl(properties, version); // Build paths const binExt = normalizePlatformName() === 'windows' ? '.bat' : ''; const dirName = `sonar-scanner-${version}-${normalizePlatformName()}`; - const installDir = path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH); + const installDir = path.join(properties[ScannerProperty.SonarUserHome], SCANNER_CLI_INSTALL_PATH); const archivePath = path.join(installDir, `${dirName}.zip`); const binPath = path.join(installDir, dirName, 'bin', `sonar-scanner${binExt}`); diff --git a/src/scanner-engine.ts b/src/scanner-engine.ts index e2eec00d..8a3e9fc6 100644 --- a/src/scanner-engine.ts +++ b/src/scanner-engine.ts @@ -17,13 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import fs from 'fs'; -import path from 'path'; import { log, LogLevel } from './logging'; import { fetch, download } from './request'; -import { ScannerProperties, ScannerProperty } from './types'; -import { extractArchive, getCachedFileLocation, validateChecksum } from './file'; -import { SONAR_CACHE_DIR, UNARCHIVE_SUFFIX } from './constants'; +import { CacheFileData, ScannerProperties, ScannerProperty } from './types'; +import { + extractArchive, + getCacheDirectories, + getCacheFileLocation, + validateChecksum, +} from './file'; export async function fetchScannerEngine(properties: ScannerProperties) { log(LogLevel.DEBUG, 'Detecting latest version of Scanner Engine'); @@ -36,10 +38,9 @@ export async function fetchScannerEngine(properties: ScannerProperties) { log(LogLevel.DEBUG, 'Looking for Cached Scanner Engine'); - const cachedScannerEngine = await getCachedFileLocation( - md5, // TODO: use sha256 - filename, - ); + // TODO: use sha256 instead of md5 + const cacheFileData: CacheFileData = { md5, filename }; + const cachedScannerEngine = await getCacheFileLocation(properties, cacheFileData); if (cachedScannerEngine) { log(LogLevel.INFO, 'Using Cached Scanner Engine'); @@ -49,15 +50,11 @@ export async function fetchScannerEngine(properties: ScannerProperties) { } properties[ScannerProperty.SonarScannerWasEngineCacheHit] = 'false'; - const archivePath = path.join(SONAR_CACHE_DIR, md5, filename); - const scannerEnginePath = path.join(SONAR_CACHE_DIR, md5, filename + UNARCHIVE_SUFFIX); - // Create destination directory if it doesn't exist - const parentCacheDirectory = path.dirname(scannerEnginePath); - if (!fs.existsSync(parentCacheDirectory)) { - log(LogLevel.DEBUG, `Creating Cache directory as it doesn't exist: ${parentCacheDirectory}`); - fs.mkdirSync(parentCacheDirectory, { recursive: true }); - } + const { archivePath, unarchivePath: scannerEnginePath } = await getCacheDirectories(properties, { + md5, + filename, + }); // TODO: replace with /api/v2/analysis/engine/ log(LogLevel.DEBUG, `Starting download of Scanner Engine`); diff --git a/src/types.ts b/src/types.ts index d303d0ff..7ae8e500 100644 --- a/src/types.ts +++ b/src/types.ts @@ -32,6 +32,8 @@ export type JreMetaData = { javaPath: string; }; +export type CacheFileData = { md5: string; filename: string }; + export type JREFullData = JreMetaData & { jrePath: string; }; diff --git a/test/unit/file.test.ts b/test/unit/file.test.ts index f6f681ed..b5a12f49 100644 --- a/test/unit/file.test.ts +++ b/test/unit/file.test.ts @@ -21,8 +21,18 @@ import AdmZip from 'adm-zip'; import fs from 'fs'; import path from 'path'; import { Readable } from 'stream'; +import { + extractArchive, + getCacheDirectories, + getCacheFileLocation, + validateChecksum, +} from '../../src/file'; +import { ScannerProperty } from '../../src/types'; import { SONAR_CACHE_DIR } from '../../src/constants'; -import { extractArchive, getCachedFileLocation, validateChecksum } from '../../src/file'; + +const MOCKED_PROPERTIES = { + [ScannerProperty.SonarUserHome]: '/path/to/sonar/user/home', +}; // Mock the filesystem jest.mock('fs', () => ({ @@ -38,6 +48,7 @@ jest.mock('fs', () => ({ createWriteStream: jest.fn(), existsSync: jest.fn(), readFile: jest.fn(), + mkdirSync: jest.fn(), })); jest.mock('fs-extra', () => ({})); @@ -52,73 +63,107 @@ afterEach(() => { jest.resetAllMocks(); }); -describe('extractArchive', () => { - it('should extract zip files to the specified directory', async () => { - const archivePath = 'path/to/archive.zip'; - const extractPath = 'path/to/extract'; +describe('file', () => { + describe('extractArchive', () => { + it('should extract zip files to the specified directory', async () => { + const archivePath = 'path/to/archive.zip'; + const extractPath = 'path/to/extract'; - await extractArchive(archivePath, extractPath); + await extractArchive(archivePath, extractPath); - const mockAdmZipInstance = (AdmZip as jest.MockedClass).mock.instances[0]; - expect(mockAdmZipInstance.extractAllTo).toHaveBeenCalledWith(extractPath, true, true); + const mockAdmZipInstance = (AdmZip as jest.MockedClass).mock.instances[0]; + expect(mockAdmZipInstance.extractAllTo).toHaveBeenCalledWith(extractPath, true, true); + }); }); -}); -describe('getCachedFileLocation', () => { - it('should return the file path if the file exists', async () => { - const md5 = 'md5hash'; - const filename = 'file.txt'; - const filePath = path.join(SONAR_CACHE_DIR, md5, filename); + describe('getCacheFileLocation', () => { + it('should return the file path if the file exists', async () => { + const md5 = 'md5hash'; + const filename = 'file.txt'; + const filePath = path.join( + MOCKED_PROPERTIES[ScannerProperty.SonarUserHome], + SONAR_CACHE_DIR, + md5, + filename, + ); - jest.spyOn(fs, 'existsSync').mockReturnValue(true); + jest.spyOn(fs, 'existsSync').mockReturnValue(true); - const result = await getCachedFileLocation(md5, filename); + const result = await getCacheFileLocation(MOCKED_PROPERTIES, { md5, filename }); - expect(result).toEqual(filePath); - }); + expect(result).toEqual(filePath); + }); - it('should return null if the file does not exist', async () => { - const md5 = 'md5hash'; - const filename = 'file.txt'; + it('should return null if the file does not exist', async () => { + const md5 = 'md5hash'; + const filename = 'file.txt'; - jest.spyOn(fs, 'existsSync').mockReturnValue(false); + jest.spyOn(fs, 'existsSync').mockReturnValue(false); - const result = await getCachedFileLocation(md5, filename); + const result = await getCacheFileLocation(MOCKED_PROPERTIES, { md5, filename }); - expect(result).toBeNull(); + expect(result).toBeNull(); + }); }); -}); -describe('validateChecksum', () => { - it('should read the file of the path provided', async () => { - jest - .spyOn(fs, 'readFile') - .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); + describe('validateChecksum', () => { + it('should read the file of the path provided', async () => { + jest + .spyOn(fs, 'readFile') + .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); - await validateChecksum('path/to/file', 'd10b4c3ff123b26dc068d43a8bef2d23'); + await validateChecksum('path/to/file', 'd10b4c3ff123b26dc068d43a8bef2d23'); - expect(fs.readFile).toHaveBeenCalledWith('path/to/file', expect.any(Function)); - }); + expect(fs.readFile).toHaveBeenCalledWith('path/to/file', expect.any(Function)); + }); - it('should throw an error if the checksum does not match', async () => { - jest - .spyOn(fs, 'readFile') - .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); + it('should throw an error if the checksum does not match', async () => { + jest + .spyOn(fs, 'readFile') + .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); - await expect(validateChecksum('path/to/file', 'invalidchecksum')).rejects.toThrow( - 'Checksum verification failed for path/to/file. Expected checksum invalidchecksum but got d10b4c3ff123b26dc068d43a8bef2d23', - ); - }); + await expect(validateChecksum('path/to/file', 'invalidchecksum')).rejects.toThrow( + 'Checksum verification failed for path/to/file. Expected checksum invalidchecksum but got d10b4c3ff123b26dc068d43a8bef2d23', + ); + }); + + it('should throw an error if the checksum is not provided', async () => { + await expect(validateChecksum('path/to/file', '')).rejects.toThrow('Checksum not provided'); + }); + + it('should throw an error if the file cannot be read', async () => { + jest + .spyOn(fs, 'readFile') + .mockImplementation((path, cb) => cb(new Error('File not found'), Buffer.from(''))); - it('should throw an error if the checksum is not provided', async () => { - await expect(validateChecksum('path/to/file', '')).rejects.toThrow('Checksum not provided'); + await expect(validateChecksum('path/to/file', 'checksum')).rejects.toThrow('File not found'); + }); }); - it('should throw an error if the file cannot be read', async () => { - jest - .spyOn(fs, 'readFile') - .mockImplementation((path, cb) => cb(new Error('File not found'), Buffer.from(''))); + describe('getCacheDirectories', () => { + it('should return the cache directories', async () => { + jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => true); + jest.spyOn(fs, 'mkdirSync'); + const { archivePath, unarchivePath } = await getCacheDirectories(MOCKED_PROPERTIES, { + md5: 'md5_test', + filename: 'file.txt', + }); + + expect(fs.existsSync).toHaveBeenCalledWith('/path/to/sonar/user/home/cache/md5_test'); + expect(fs.mkdirSync).not.toHaveBeenCalled(); - await expect(validateChecksum('path/to/file', 'checksum')).rejects.toThrow('File not found'); + expect(archivePath).toEqual('/path/to/sonar/user/home/cache/md5_test/file.txt'); + expect(unarchivePath).toEqual('/path/to/sonar/user/home/cache/md5_test/file.txt_extracted'); + }); + it('should create the parent cache directory if it does not exist', async () => { + jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => false); + jest.spyOn(fs, 'mkdirSync').mockImplementationOnce(() => undefined); + await getCacheDirectories(MOCKED_PROPERTIES, { md5: 'md5_test', filename: 'file.txt' }); + + expect(fs.existsSync).toHaveBeenCalledWith('/path/to/sonar/user/home/cache/md5_test'); + expect(fs.mkdirSync).toHaveBeenCalledWith('/path/to/sonar/user/home/cache/md5_test', { + recursive: true, + }); + }); }); }); diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index 3b5552e2..bf6b0000 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -21,7 +21,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import fs from 'fs'; import path from 'path'; -import { SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../../src/constants'; +import { API_V2_JRE_ENDPOINT, SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../../src/constants'; import * as file from '../../src/file'; import { fetchJRE, fetchServerVersion, serverSupportsJREProvisioning } from '../../src/java'; import * as request from '../../src/request'; @@ -31,7 +31,6 @@ const mock = new MockAdapter(axios); const MOCKED_PROPERTIES: ScannerProperties = { [ScannerProperty.SonarHostUrl]: 'http://sonarqube.com', - [ScannerProperty.SonarToken]: 'dummy-token', }; beforeEach(() => { @@ -71,9 +70,7 @@ describe('java', () => { }); it('should fail if version can not be parsed', async () => { - mock - .onGet('http://sonarqube.com/api/server/version') - .reply(200, 'FORBIDDEN'); + mock.onGet('/api/server/version').reply(200, 'FORBIDDEN'); expect(async () => { await fetchServerVersion(); @@ -85,6 +82,7 @@ describe('java', () => { it('should return true for sonarcloud', async () => { expect( await serverSupportsJREProvisioning({ + ...MOCKED_PROPERTIES, [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', }), ).toBe(true); @@ -92,21 +90,13 @@ describe('java', () => { it(`should return true for SQ version >= ${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`, async () => { mock.onGet('/api/server/version').reply(200, '10.5.0'); - expect( - await serverSupportsJREProvisioning({ - [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', - }), - ).toBe(true); + expect(await serverSupportsJREProvisioning(MOCKED_PROPERTIES)).toBe(true); }); it(`should return false for SQ version < ${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`, async () => { // Define the behavior of the GET request mock.onGet('/api/server/version').reply(200, '9.9.9'); - expect( - await serverSupportsJREProvisioning({ - [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', - }), - ).toBe(false); + expect(await serverSupportsJREProvisioning(MOCKED_PROPERTIES)).toBe(false); }); }); @@ -118,12 +108,12 @@ describe('java', () => { md5: 'd41d8cd98f00b204e9800998ecf8427e', }; beforeEach(() => { - jest.spyOn(file, 'getCachedFileLocation').mockResolvedValue('mocked/path/to/file'); + jest.spyOn(file, 'getCacheFileLocation').mockResolvedValue('mocked/path/to/file'); jest.spyOn(file, 'extractArchive').mockResolvedValue(undefined); mock - .onGet(`/api/v2/analysis/jres`, { + .onGet(API_V2_JRE_ENDPOINT, { params: { os: platformInfo.os, arch: platformInfo.arch, @@ -132,49 +122,52 @@ describe('java', () => { .reply(200, serverResponse); mock - .onGet(`/api/v2/analysis/jres/${serverResponse.filename}`) + .onGet(`${API_V2_JRE_ENDPOINT}/${serverResponse.filename}`) .reply(200, fs.createReadStream(path.resolve(__dirname, '../unit/mocks/mock-jre.tar.gz'))); }); describe('when the JRE is cached', () => { it('should fetch the latest supported JRE and use the cached version', async () => { - await fetchJRE( - { - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', - [ScannerProperty.SonarToken]: 'mock-token', - }, - platformInfo, - ); + await fetchJRE(MOCKED_PROPERTIES, platformInfo); expect(request.fetch).toHaveBeenCalledTimes(1); expect(request.download).not.toHaveBeenCalled(); // check for the cache - expect(file.getCachedFileLocation).toHaveBeenCalledTimes(1); + expect(file.getCacheFileLocation).toHaveBeenCalledTimes(1); expect(file.extractArchive).not.toHaveBeenCalled(); }); }); describe('when the JRE is not cached', () => { + const mockCacheDirectories = { + archivePath: '/mocked-archive-path', + unarchivePath: '/mocked-archive-path_extracted', + }; beforeEach(() => { - jest.spyOn(file, 'getCachedFileLocation').mockResolvedValue(null); + jest.spyOn(file, 'getCacheFileLocation').mockResolvedValue(null); + jest.spyOn(file, 'getCacheDirectories').mockResolvedValue(mockCacheDirectories); + jest.spyOn(file, 'validateChecksum').mockResolvedValue(undefined); + jest.spyOn(request, 'download').mockResolvedValue(undefined); }); it('should download the JRE', async () => { - await fetchJRE( - { - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', - [ScannerProperty.SonarToken]: 'mock-token', - }, - platformInfo, - ); + await fetchJRE({ ...MOCKED_PROPERTIES }, platformInfo); - expect(request.fetch).toHaveBeenCalledTimes(1); - expect(request.download).toHaveBeenCalledTimes(1); + expect(request.fetch).toHaveBeenCalledWith({ + url: API_V2_JRE_ENDPOINT, + params: platformInfo, + }); - // check for the cache - expect(file.getCachedFileLocation).toHaveBeenCalledTimes(1); + expect(file.getCacheFileLocation).toHaveBeenCalledTimes(1); + + expect(request.download).toHaveBeenCalledWith( + `${API_V2_JRE_ENDPOINT}/${serverResponse.filename}`, + mockCacheDirectories.archivePath, + ); + + expect(file.validateChecksum).toHaveBeenCalledTimes(1); expect(file.extractArchive).toHaveBeenCalledTimes(1); }); diff --git a/test/unit/mocks/FakeProjectMock.ts b/test/unit/mocks/FakeProjectMock.ts index 6a611b53..ad28b889 100644 --- a/test/unit/mocks/FakeProjectMock.ts +++ b/test/unit/mocks/FakeProjectMock.ts @@ -57,6 +57,7 @@ export class FakeProjectMock { 'sonar.scanner.appVersion': '1.2.3', 'sonar.scanner.wasEngineCacheHit': 'false', 'sonar.scanner.wasJreCacheHit': 'false', + 'sonar.userHome': expect.stringMatching(/\.sonar$/), }; } } diff --git a/test/unit/scan.test.ts b/test/unit/scan.test.ts index 02c4e6d9..43904b24 100644 --- a/test/unit/scan.test.ts +++ b/test/unit/scan.test.ts @@ -30,6 +30,8 @@ import * as platform from '../../src/platform'; import { scan } from '../../src/scan'; jest.mock('../../src/java'); +jest.mock('../../src/scanner-cli'); +jest.mock('../../src/scanner-engine'); jest.mock('../../src/platform'); jest.mock('../../package.json', () => ({ version: 'MOCK.VERSION', @@ -50,10 +52,15 @@ describe('scan', () => { expect(logging.getLogLevel()).toBe('DEBUG'); }); + it('should set the log level to the value provided by the user', async () => { + await scan({ options: { 'sonar.log.level': 'DEBUG' } }, []); + expect(logging.getLogLevel()).toBe('DEBUG'); + }); + it('should output the current version of the scanner', async () => { jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); await scan({}, []); - expect(logging.log).toHaveBeenCalledWith('INFO', 'Version: ', 'MOCK.VERSION'); + expect(logging.log).toHaveBeenCalledWith('INFO', 'Version: MOCK.VERSION'); }); it('should output the current platform', async () => { diff --git a/test/unit/scanner-cli.test.ts b/test/unit/scanner-cli.test.ts index 9a1f4169..2e4e520c 100644 --- a/test/unit/scanner-cli.test.ts +++ b/test/unit/scanner-cli.test.ts @@ -20,11 +20,7 @@ import { spawn } from 'child_process'; import path from 'path'; import sinon from 'sinon'; -import { - SCANNER_CLI_DEFAULT_BIN_NAME, - SCANNER_CLI_INSTALL_PATH, - SONAR_DIR, -} from '../../src/constants'; +import { SCANNER_CLI_DEFAULT_BIN_NAME, SCANNER_CLI_INSTALL_PATH } from '../../src/constants'; import { extractArchive } from '../../src/file'; import { LogLevel, log } from '../../src/logging'; import { download } from '../../src/request'; @@ -44,6 +40,12 @@ jest.mock('../../src/logging'); const childProcessHandler = new ChildProcessMock(); +const MOCK_PROPERTIES = { + [ScannerProperty.SonarToken]: 'token', + [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + [ScannerProperty.SonarUserHome]: 'path/to/user/home', +}; + beforeEach(() => { childProcessHandler.reset(); }); @@ -73,14 +75,9 @@ describe('scanner-cli', () => { it('should use already downloaded version', async () => { const stub = sinon.stub(process, 'platform').value('linux'); - expect( - await downloadScannerCli({ - [ScannerProperty.SonarToken]: 'token', - [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', - }), - ).toBe( + expect(await downloadScannerCli(MOCK_PROPERTIES)).toBe( path.join( - SONAR_DIR, + MOCK_PROPERTIES[ScannerProperty.SonarUserHome], SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner', ), @@ -94,34 +91,34 @@ describe('scanner-cli', () => { childProcessHandler.setExitCode(1); const stub = sinon.stub(process, 'platform').value('linux'); - const binPath = await downloadScannerCli({ - [ScannerProperty.SonarToken]: 'token', - [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', - }); + const binPath = await downloadScannerCli(MOCK_PROPERTIES); - expect( - await downloadScannerCli({ - [ScannerProperty.SonarToken]: 'token', - [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', - }), - ).toBe( + expect(await downloadScannerCli(MOCK_PROPERTIES)).toBe( path.join( - SONAR_DIR, + MOCK_PROPERTIES[ScannerProperty.SonarUserHome], SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner', ), ); expect(download).toHaveBeenLastCalledWith( 'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-linux.zip', - path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-linux.zip'), + path.join( + MOCK_PROPERTIES[ScannerProperty.SonarUserHome], + SCANNER_CLI_INSTALL_PATH, + 'sonar-scanner-5.0.1.3006-linux.zip', + ), ); expect(extractArchive).toHaveBeenLastCalledWith( - path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-linux.zip'), - path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH), + path.join( + MOCK_PROPERTIES[ScannerProperty.SonarUserHome], + SCANNER_CLI_INSTALL_PATH, + 'sonar-scanner-5.0.1.3006-linux.zip', + ), + path.join(MOCK_PROPERTIES[ScannerProperty.SonarUserHome], SCANNER_CLI_INSTALL_PATH), ); expect(binPath).toBe( path.join( - SONAR_DIR, + MOCK_PROPERTIES[ScannerProperty.SonarUserHome], SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner', ), @@ -134,22 +131,27 @@ describe('scanner-cli', () => { childProcessHandler.setExitCode(1); const stub = sinon.stub(process, 'platform').value('win32'); - const binPath = await downloadScannerCli({ - [ScannerProperty.SonarToken]: 'token', - [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', - }); + const binPath = await downloadScannerCli(MOCK_PROPERTIES); expect(download).toHaveBeenLastCalledWith( 'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-windows.zip', - path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-windows.zip'), + path.join( + MOCK_PROPERTIES[ScannerProperty.SonarUserHome], + SCANNER_CLI_INSTALL_PATH, + 'sonar-scanner-5.0.1.3006-windows.zip', + ), ); expect(extractArchive).toHaveBeenLastCalledWith( - path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-windows.zip'), - path.join(SONAR_DIR, SCANNER_CLI_INSTALL_PATH), + path.join( + MOCK_PROPERTIES[ScannerProperty.SonarUserHome], + SCANNER_CLI_INSTALL_PATH, + 'sonar-scanner-5.0.1.3006-windows.zip', + ), + path.join(MOCK_PROPERTIES[ScannerProperty.SonarUserHome], SCANNER_CLI_INSTALL_PATH), ); expect(binPath).toBe( path.join( - SONAR_DIR, + MOCK_PROPERTIES[ScannerProperty.SonarUserHome], SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-windows/bin/sonar-scanner.bat', ), @@ -165,10 +167,7 @@ describe('scanner-cli', () => { { jvmOptions: ['-Xmx512m'], }, - { - [ScannerProperty.SonarToken]: 'token', - [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', - }, + MOCK_PROPERTIES, 'sonar-scanner', ); @@ -176,25 +175,13 @@ describe('scanner-cli', () => { const [command, args, options] = (spawn as jest.Mock).mock.calls.pop(); expect(command).toBe('sonar-scanner'); expect(args).toEqual(['-Xmx512m']); - expect(options.env.SONARQUBE_SCANNER_PARAMS).toBe( - JSON.stringify({ - [ScannerProperty.SonarToken]: 'token', - [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', - }), - ); + expect(options.env.SONARQUBE_SCANNER_PARAMS).toBe(JSON.stringify(MOCK_PROPERTIES)); }); it('should display SonarScanner CLI output', async () => { childProcessHandler.setOutput('the output', 'some error'); - await runScannerCli( - {}, - { - [ScannerProperty.SonarToken]: 'token', - [ScannerProperty.SonarHostUrl]: 'https://localhost:9000', - }, - 'sonar-scanner', - ); + await runScannerCli({}, MOCK_PROPERTIES, 'sonar-scanner'); expect(log).toHaveBeenCalledWith(LogLevel.ERROR, 'some error'); expect(log).toHaveBeenCalledWith(LogLevel.INFO, 'the output'); @@ -203,16 +190,7 @@ describe('scanner-cli', () => { it('should reject if SonarScanner CLI fails', async () => { childProcessHandler.setExitCode(1); - await expect( - runScannerCli( - {}, - { - [ScannerProperty.SonarToken]: 'token', - [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', - }, - 'sonar-scanner', - ), - ).rejects.toBeDefined(); + await expect(runScannerCli({}, MOCK_PROPERTIES, 'sonar-scanner')).rejects.toBeDefined(); expect(log).toHaveBeenCalledWith(LogLevel.ERROR, 'SonarScanner CLI failed with code 1'); }); @@ -221,8 +199,7 @@ describe('scanner-cli', () => { await runScannerCli( {}, { - [ScannerProperty.SonarToken]: 'token', - [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + ...MOCK_PROPERTIES, [ScannerProperty.SonarScannerProxyHost]: 'proxy', [ScannerProperty.SonarScannerProxyPort]: '9000', [ScannerProperty.SonarScannerProxyUser]: 'some-user', @@ -242,8 +219,7 @@ describe('scanner-cli', () => { ]); expect(options.env.SONARQUBE_SCANNER_PARAMS).toBe( JSON.stringify({ - [ScannerProperty.SonarToken]: 'token', - [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', + ...MOCK_PROPERTIES, [ScannerProperty.SonarScannerProxyHost]: 'proxy', [ScannerProperty.SonarScannerProxyPort]: '9000', [ScannerProperty.SonarScannerProxyUser]: 'some-user', diff --git a/test/unit/scanner-engine.test.ts b/test/unit/scanner-engine.test.ts index 50f7baaf..63f7ce69 100644 --- a/test/unit/scanner-engine.test.ts +++ b/test/unit/scanner-engine.test.ts @@ -19,7 +19,6 @@ */ import axios from 'axios'; -import fs from 'fs'; import MockAdapter from 'axios-mock-adapter'; import { ScannerProperties, ScannerProperty } from '../../src/types'; import { fetchScannerEngine } from '../../src/scanner-engine'; @@ -33,10 +32,10 @@ const MOCKED_PROPERTIES: ScannerProperties = { [ScannerProperty.SonarToken]: 'dummy-token', }; -jest.mock('../../src/constants', () => ({ - ...jest.requireActual('../../src/constants'), - SONAR_CACHE_DIR: 'mocked/path/to/sonar/cache', -})); +const MOCK_CACHE_DIRECTORIES = { + archivePath: 'mocked/path/to/sonar/cache/md5_test/scanner-engine-1.2.3.zip', + unarchivePath: 'mocked/path/to/sonar/cache/md5_test/scanner-engine-1.2.3.zip_extracted', +}; beforeEach(() => { jest.clearAllMocks(); @@ -58,40 +57,28 @@ describe('scanner-engine', () => { return [200, readable]; }); - jest.spyOn(file, 'getCachedFileLocation').mockImplementation((md5, filename) => { - return Promise.resolve(null); - }); - - jest.spyOn(file, 'extractArchive').mockImplementation((fromPath, toPath) => { - return Promise.resolve(); - }); - - jest.spyOn(file, 'validateChecksum').mockImplementation(() => { - return Promise.resolve(); - }); - - jest.spyOn(request, 'download').mockImplementation(() => { - return Promise.resolve(); - }); + jest.spyOn(file, 'getCacheFileLocation').mockResolvedValue(null); + jest.spyOn(file, 'extractArchive').mockResolvedValue(); + jest.spyOn(file, 'validateChecksum').mockResolvedValue(); + jest.spyOn(file, 'getCacheDirectories').mockResolvedValue(MOCK_CACHE_DIRECTORIES); + jest.spyOn(request, 'download').mockResolvedValue(); }); describe('fetchScannerEngine', () => { it('should fetch the latest version of the scanner engine', async () => { - jest.spyOn(file, 'getCachedFileLocation'); + jest.spyOn(file, 'getCacheFileLocation'); await fetchScannerEngine(MOCKED_PROPERTIES); - expect(file.getCachedFileLocation).toHaveBeenCalledWith( - 'md5_test', - 'scanner-engine-1.2.3.zip', - ); + expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { + md5: 'md5_test', + filename: 'scanner-engine-1.2.3.zip', + }); }); describe('when the scanner engine is cached', () => { beforeEach(() => { - jest.spyOn(file, 'getCachedFileLocation').mockImplementation((md5, filename) => { - return Promise.resolve('mocked/path/to/scanner-engine'); - }); + jest.spyOn(file, 'getCacheFileLocation').mockResolvedValue('mocked/path/to/scanner-engine'); jest.spyOn(file, 'extractArchive'); @@ -101,10 +88,10 @@ describe('scanner-engine', () => { it('should use the cached scanner engine', async () => { const scannerEngine = await fetchScannerEngine(MOCKED_PROPERTIES); - expect(file.getCachedFileLocation).toHaveBeenCalledWith( - 'md5_test', - 'scanner-engine-1.2.3.zip', - ); + expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { + md5: 'md5_test', + filename: 'scanner-engine-1.2.3.zip', + }); expect(request.download).not.toHaveBeenCalled(); expect(file.extractArchive).not.toHaveBeenCalled(); @@ -112,24 +99,13 @@ describe('scanner-engine', () => { }); }); describe('when the scanner engine is not cached', () => { - it('should create the parent cache directory if it does not exist', async () => { - jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => false); - jest.spyOn(fs, 'mkdirSync').mockImplementationOnce(() => undefined); - await fetchScannerEngine(MOCKED_PROPERTIES); - - expect(fs.existsSync).toHaveBeenCalledWith('mocked/path/to/sonar/cache/md5_test'); - expect(fs.mkdirSync).toHaveBeenCalledWith('mocked/path/to/sonar/cache/md5_test', { - recursive: true, - }); - }); - it('should download and extract the scanner engine', async () => { const scannerEngine = await fetchScannerEngine(MOCKED_PROPERTIES); - expect(file.getCachedFileLocation).toHaveBeenCalledWith( - 'md5_test', - 'scanner-engine-1.2.3.zip', - ); + expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { + md5: 'md5_test', + filename: 'scanner-engine-1.2.3.zip', + }); expect(request.download).toHaveBeenCalledTimes(1); expect(file.extractArchive).toHaveBeenCalledTimes(1); From 2786c56e8f48b844f0ea8cc91b7a491b0653837f Mon Sep 17 00:00:00 2001 From: 7PH Date: Tue, 23 Apr 2024 17:07:59 +0200 Subject: [PATCH 13/35] SCANNPM-2 Implement running the Scanner Engine --- src/java.ts | 16 +--- src/logging.ts | 11 ++- src/properties.ts | 2 + src/scan.ts | 7 +- src/scanner-cli.ts | 41 ++++----- src/scanner-engine.ts | 66 +++++++++++++- test/setup.ts | 1 + test/unit/mocks/ChildProcessMock.ts | 16 +++- test/unit/mocks/FakeProjectMock.ts | 3 +- test/unit/scanner-engine.test.ts | 134 +++++++++++++++++++++++++--- 10 files changed, 236 insertions(+), 61 deletions(-) diff --git a/src/java.ts b/src/java.ts index ef5303b9..cc5951f8 100644 --- a/src/java.ts +++ b/src/java.ts @@ -35,7 +35,7 @@ import { } from './file'; import { log, LogLevel } from './logging'; import { download, fetch } from './request'; -import { JREFullData, PlatformInfo, ScannerProperties, ScannerProperty } from './types'; +import { PlatformInfo, ScannerProperties, ScannerProperty } from './types'; export async function fetchServerVersion(): Promise { let version: SemVer | null = null; @@ -94,7 +94,7 @@ export async function serverSupportsJREProvisioning( export async function fetchJRE( properties: ScannerProperties, platformInfo: PlatformInfo, -): Promise { +): Promise { log(LogLevel.DEBUG, 'Detecting latest version of JRE'); const latestJREData = await fetchLatestSupportedJRE(platformInfo); log(LogLevel.INFO, 'Latest Supported JRE: ', latestJREData); @@ -108,10 +108,7 @@ export async function fetchJRE( log(LogLevel.INFO, 'Using Cached JRE'); properties[ScannerProperty.SonarScannerWasJRECacheHit] = 'true'; - return { - ...latestJREData, - jrePath: path.join(cachedJRE, cachedJRE), - }; + return path.join(cachedJRE, latestJREData.javaPath); } else { const { archivePath, unarchivePath: jreDirPath } = await getCacheDirectories( properties, @@ -125,12 +122,7 @@ export async function fetchJRE( await extractArchive(archivePath, jreDirPath); - const jreBinPath = path.join(jreDirPath, latestJREData.javaPath); - - return { - ...latestJREData, - jrePath: jreBinPath, - }; + return path.join(jreDirPath, latestJREData.javaPath); } } diff --git a/src/logging.ts b/src/logging.ts index f07d7c8a..3a087111 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -39,9 +39,16 @@ const DEFAULT_LOG_LEVEL = LogLevel.INFO; let logLevel = DEFAULT_LOG_LEVEL; export function log(level: LogLevel, ...message: unknown[]) { - if (logLevelValues[level] <= logLevelValues[logLevel]) { - console.log(`[${level}] Bootstrapper:: `, ...message); + logWithPrefix(level, 'Bootstrapper', ...message); +} + +export function logWithPrefix(level: LogLevel, prefix: string, ...message: unknown[]) { + if (logLevelValues[level] > logLevelValues[logLevel]) { + return; } + + const levelStr = `[${level}]`.padEnd(7); + console.log(levelStr, `${prefix}:`, ...message); } export function getLogLevel() { diff --git a/src/properties.ts b/src/properties.ts index 1a2a9097..4e43b956 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -26,6 +26,7 @@ import { ENV_TO_PROPERTY_NAME, ENV_VAR_PREFIX, SCANNER_BOOTSTRAPPER_NAME, + SCANNER_CLI_VERSION, SONARCLOUD_URL, SONARCLOUD_URL_REGEX, SONAR_DIR_DEFAULT, @@ -39,6 +40,7 @@ const DEFAULT_PROPERTIES = { process.env.HOME ?? process.env.USERPROFILE ?? '', SONAR_DIR_DEFAULT, ), + [ScannerProperty.SonarScannerCliVersion]: SCANNER_CLI_VERSION, }; /** diff --git a/src/scan.ts b/src/scan.ts index 2006d26d..8e9a0117 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -26,7 +26,7 @@ import { getPlatformInfo } from './platform'; import { getProperties } from './properties'; import { initializeAxios } from './request'; import { downloadScannerCli, runScannerCli, tryLocalSonarScannerExecutable } from './scanner-cli'; -import { fetchScannerEngine } from './scanner-engine'; +import { fetchScannerEngine, runScannerEngine } from './scanner-engine'; import { ScanOptions, ScannerProperty } from './types'; export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { @@ -86,7 +86,6 @@ async function runScan(scanOptions: ScanOptions, cliArgs?: string[]) { const latestJRE = explicitJREPathOverride ?? (await fetchJRE(properties, platformInfo)); const latestScannerEngine = await fetchScannerEngine(properties); - //TODO: run the scanner.. - - log(LogLevel.INFO, 'Running the scanner ...'); + log(LogLevel.INFO, 'Running the Scanner Engine'); + await runScannerEngine(latestJRE, latestScannerEngine, scanOptions, properties); } diff --git a/src/scanner-cli.ts b/src/scanner-cli.ts index 1e6bd77d..a9a082fc 100644 --- a/src/scanner-cli.ts +++ b/src/scanner-cli.ts @@ -20,15 +20,10 @@ import { spawn } from 'child_process'; import * as fsExtra from 'fs-extra'; import path from 'path'; -import { - SCANNER_CLI_INSTALL_PATH, - SCANNER_CLI_MIRROR, - SCANNER_CLI_VERSION, - SONAR_CACHE_DIR, -} from './constants'; +import { SCANNER_CLI_INSTALL_PATH, SCANNER_CLI_MIRROR } from './constants'; import { extractArchive } from './file'; import { LogLevel, log } from './logging'; -import { getProxyUrl, proxyUrlToJavaOptions } from './proxy'; +import { proxyUrlToJavaOptions } from './proxy'; import { download } from './request'; import { ScanOptions, ScannerProperties, ScannerProperty } from './types'; @@ -77,7 +72,7 @@ function getScannerCliUrl(properties: ScannerProperties, version: string): URL { } export async function downloadScannerCli(properties: ScannerProperties): Promise { - const version = properties[ScannerProperty.SonarScannerCliVersion] ?? SCANNER_CLI_VERSION; + const version = properties[ScannerProperty.SonarScannerCliVersion]; if (!/^[\d.]+$/.test(version)) { throw new Error(`Version "${version}" does not have a correct format."`); } @@ -115,30 +110,26 @@ export async function runScannerCli( binPath: string, ) { log(LogLevel.INFO, 'Starting analysis'); - - const options = [...(scanOptions.jvmOptions ?? []), ...proxyUrlToJavaOptions(properties)]; - const scannerProcess = spawn(binPath, options, { - env: { - ...process.env, - SONARQUBE_SCANNER_PARAMS: JSON.stringify(properties), + const child = spawn( + binPath, + [...(scanOptions.jvmOptions ?? []), ...proxyUrlToJavaOptions(properties)], + { + env: { + ...process.env, + SONARQUBE_SCANNER_PARAMS: JSON.stringify(properties), + }, }, - }); + ); + + child.stdout.on('data', buffer => process.stdout.write(buffer)); + child.stderr.on('data', buffer => log(LogLevel.ERROR, buffer.toString())); return new Promise((resolve, reject) => { - scannerProcess.stdout.on('data', data => { - for (const line of data.toString().trim().split('\n')) { - log(LogLevel.INFO, line); - } - }); - scannerProcess.stderr.on('data', data => { - log(LogLevel.ERROR, data.toString().trim()); - }); - scannerProcess.on('exit', code => { + process.on('exit', code => { if (code === 0) { log(LogLevel.INFO, 'SonarScanner CLI finished successfully'); resolve(); } else { - log(LogLevel.ERROR, `SonarScanner CLI failed with code ${code}`); reject(new Error(`SonarScanner CLI failed with code ${code}`)); } }); diff --git a/src/scanner-engine.ts b/src/scanner-engine.ts index 8a3e9fc6..47d602e9 100644 --- a/src/scanner-engine.ts +++ b/src/scanner-engine.ts @@ -17,15 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { log, LogLevel } from './logging'; -import { fetch, download } from './request'; -import { CacheFileData, ScannerProperties, ScannerProperty } from './types'; +import { spawn } from 'child_process'; import { extractArchive, getCacheDirectories, getCacheFileLocation, validateChecksum, } from './file'; +import { LogLevel, log, logWithPrefix } from './logging'; +import { proxyUrlToJavaOptions } from './proxy'; +import { download, fetch } from './request'; +import { + CacheFileData, + ScanOptions, + ScannerLogEntry, + ScannerProperties, + ScannerProperty, +} from './types'; export async function fetchScannerEngine(properties: ScannerProperties) { log(LogLevel.DEBUG, 'Detecting latest version of Scanner Engine'); @@ -67,3 +75,55 @@ export async function fetchScannerEngine(properties: ScannerProperties) { await extractArchive(archivePath, scannerEnginePath); return scannerEnginePath; } + +async function logOutput(message: string) { + try { + // Try and assume the log comes from the scanner engine + const parsed = JSON.parse(message) as ScannerLogEntry; + logWithPrefix(parsed.level, 'ScannerEngine', parsed.formattedMessage); + if (parsed.throwable) { + // Console.log without newline + process.stdout.write(parsed.throwable); + } + } catch (e) { + process.stdout.write(message); + } +} + +export async function runScannerEngine( + javaBinPath: string, + scannerEnginePath: string, + scanOptions: ScanOptions, + properties: ScannerProperties, +) { + // The scanner engine expects a JSON object of properties attached to a key name "scannerProperties" + const propertiesJSON = JSON.stringify({ scannerProperties: properties }); + + // Run the scanner-engine + const args = [ + ...proxyUrlToJavaOptions(properties), + ...(scanOptions.jvmOptions ?? []), + '-jar', + scannerEnginePath, + ]; + log(LogLevel.DEBUG, 'Running scanner engine', javaBinPath, ...args); + const child = spawn(javaBinPath, args); + + log(LogLevel.DEBUG, 'Writing properties to scanner engine', propertiesJSON); + child.stdin.write(propertiesJSON); + child.stdin.end(); + + child.stdout.on('data', buffer => buffer.toString().trim().split('\n').forEach(logOutput)); + child.stderr.on('data', buffer => log(LogLevel.ERROR, buffer.toString())); + + return new Promise((resolve, reject) => { + child.on('exit', code => { + if (code === 0) { + log(LogLevel.INFO, 'Scanner engine finished successfully'); + resolve(); + } else { + reject(new Error(`Scanner engine failed with code ${code}`)); + } + }); + }); +} diff --git a/test/setup.ts b/test/setup.ts index b5f118a2..8ce8965f 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -22,6 +22,7 @@ jest.mock('../src/logging', () => ({ ...jest.requireActual('../src/logging'), log: jest.fn(), + logWithPrefix: jest.fn(), getLogLevel: jest.fn(), stringToLogLevel: jest.fn(), })); diff --git a/test/unit/mocks/ChildProcessMock.ts b/test/unit/mocks/ChildProcessMock.ts index cb14fa4a..a57fb9e1 100644 --- a/test/unit/mocks/ChildProcessMock.ts +++ b/test/unit/mocks/ChildProcessMock.ts @@ -17,15 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { spawn } from 'child_process'; +import { spawn, ChildProcess } from 'child_process'; export class ChildProcessMock { private exitCode: number = 0; private stdout: string = ''; - private stderr: string = ''; + private mock: Partial | null = null; + constructor() { jest.mocked(spawn).mockImplementation((this.handleSpawn as any).bind(this)); } @@ -39,6 +40,10 @@ export class ChildProcessMock { this.stderr = stderr ?? ''; } + setChildProcessMock(mock: Partial | null) { + this.mock = mock; + } + handleSpawn() { return { on: jest.fn().mockImplementation((event, callback) => { @@ -46,8 +51,10 @@ export class ChildProcessMock { callback(this.exitCode); } }), - stdout: { on: jest.fn().mockImplementation((event, callback) => callback(this.stdout)) }, - stderr: { on: jest.fn().mockImplementation((event, callback) => callback(this.stderr)) }, + stdin: { write: jest.fn(), end: jest.fn() }, + stdout: { on: jest.fn().mockImplementation((_event, callback) => callback(this.stdout)) }, + stderr: { on: jest.fn().mockImplementation((_event, callback) => callback(this.stderr)) }, + ...this.mock, }; } @@ -55,6 +62,7 @@ export class ChildProcessMock { this.exitCode = 0; this.stdout = ''; this.stderr = ''; + this.mock = null; jest.clearAllMocks(); } } diff --git a/test/unit/mocks/FakeProjectMock.ts b/test/unit/mocks/FakeProjectMock.ts index ad28b889..3728e9bd 100644 --- a/test/unit/mocks/FakeProjectMock.ts +++ b/test/unit/mocks/FakeProjectMock.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import path from 'path'; -import { SCANNER_BOOTSTRAPPER_NAME } from '../../../src/constants'; +import { SCANNER_BOOTSTRAPPER_NAME, SCANNER_CLI_VERSION } from '../../../src/constants'; const baseEnvVariables = process.env; @@ -57,6 +57,7 @@ export class FakeProjectMock { 'sonar.scanner.appVersion': '1.2.3', 'sonar.scanner.wasEngineCacheHit': 'false', 'sonar.scanner.wasJreCacheHit': 'false', + 'sonar.scanner.version': SCANNER_CLI_VERSION, 'sonar.userHome': expect.stringMatching(/\.sonar$/), }; } diff --git a/test/unit/scanner-engine.test.ts b/test/unit/scanner-engine.test.ts index 63f7ce69..37399891 100644 --- a/test/unit/scanner-engine.test.ts +++ b/test/unit/scanner-engine.test.ts @@ -20,11 +20,16 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { ScannerProperties, ScannerProperty } from '../../src/types'; -import { fetchScannerEngine } from '../../src/scanner-engine'; +import { ChildProcess, spawn } from 'child_process'; +import sinon from 'sinon'; +import { Readable } from 'stream'; import * as file from '../../src/file'; import * as request from '../../src/request'; -import { Readable } from 'stream'; +import { fetchScannerEngine, runScannerEngine } from '../../src/scanner-engine'; +import { ScannerProperties, ScannerProperty } from '../../src/types'; +import { ChildProcessMock } from './mocks/ChildProcessMock'; +import { logWithPrefix } from '../../src/logging'; + const mock = new MockAdapter(axios); const MOCKED_PROPERTIES: ScannerProperties = { @@ -36,15 +41,24 @@ const MOCK_CACHE_DIRECTORIES = { archivePath: 'mocked/path/to/sonar/cache/md5_test/scanner-engine-1.2.3.zip', unarchivePath: 'mocked/path/to/sonar/cache/md5_test/scanner-engine-1.2.3.zip_extracted', }; +jest.mock('../../src/constants', () => ({ + ...jest.requireActual('../../src/constants'), + SONAR_CACHE_DIR: 'mocked/path/to/sonar/cache', +})); + +jest.mock('child_process'); + +let childProcessHandler = new ChildProcessMock(); beforeEach(() => { + childProcessHandler.reset(); jest.clearAllMocks(); mock.reset(); }); describe('scanner-engine', () => { beforeEach(async () => { - await request.initializeAxios(MOCKED_PROPERTIES); + request.initializeAxios(MOCKED_PROPERTIES); mock.onGet('/batch/index').reply(200, 'scanner-engine-1.2.3.zip|md5_test'); mock.onGet('/batch/file?name=scanner-engine-1.2.3.zip').reply(() => { const readable = new Readable({ @@ -66,8 +80,6 @@ describe('scanner-engine', () => { describe('fetchScannerEngine', () => { it('should fetch the latest version of the scanner engine', async () => { - jest.spyOn(file, 'getCacheFileLocation'); - await fetchScannerEngine(MOCKED_PROPERTIES); expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { @@ -79,10 +91,6 @@ describe('scanner-engine', () => { describe('when the scanner engine is cached', () => { beforeEach(() => { jest.spyOn(file, 'getCacheFileLocation').mockResolvedValue('mocked/path/to/scanner-engine'); - - jest.spyOn(file, 'extractArchive'); - - jest.spyOn(request, 'download'); }); it('should use the cached scanner engine', async () => { @@ -98,6 +106,7 @@ describe('scanner-engine', () => { expect(scannerEngine).toEqual('mocked/path/to/scanner-engine'); }); }); + describe('when the scanner engine is not cached', () => { it('should download and extract the scanner engine', async () => { const scannerEngine = await fetchScannerEngine(MOCKED_PROPERTIES); @@ -115,4 +124,109 @@ describe('scanner-engine', () => { }); }); }); + + describe('runScannerEngine', () => { + it('should launch scanner engine and write properties to stdin', async () => { + const write = jest.fn(); + childProcessHandler.setChildProcessMock({ + stdin: { + write, + end: jest.fn(), + } as unknown as ChildProcess['stdin'], + }); + + await runScannerEngine( + 'java', + '/some/path/to/scanner-engine', + { + jvmOptions: ['-Dsome.custom.opt=123'], + }, + MOCKED_PROPERTIES, + ); + + expect(write).toHaveBeenCalledTimes(1); + expect(write).toHaveBeenCalledWith( + JSON.stringify({ + scannerProperties: MOCKED_PROPERTIES, + }), + ); + expect(spawn).toHaveBeenCalledWith('java', [ + '-Dsome.custom.opt=123', + '-jar', + '/some/path/to/scanner-engine', + ]); + }); + + it('should reject when child process exits with code 1', async () => { + childProcessHandler.setExitCode(1); + + expect( + runScannerEngine( + '/some/path/to/java', + '/some/path/to/scanner-engine', + {}, + MOCKED_PROPERTIES, + ), + ).rejects.toBeInstanceOf(Error); + }); + + it('should output scanner engine output', async () => { + const stdoutStub = sinon.stub(process.stdout, 'write').value(jest.fn()); + + const output = [ + JSON.stringify({ level: 'DEBUG', formattedMessage: 'the message' }), + JSON.stringify({ level: 'INFO', formattedMessage: 'another message' }), + "some non-JSON message which shouldn't crash the bootstrapper", + JSON.stringify({ + level: 'ERROR', + formattedMessage: 'final message', + throwable: 'this is a throwable', + }), + ]; + childProcessHandler.setOutput(output.join('\n')); + + await runScannerEngine( + '/some/path/to/java', + '/some/path/to/scanner-engine', + {}, + MOCKED_PROPERTIES, + ); + + expect(logWithPrefix).toHaveBeenCalledWith('DEBUG', 'ScannerEngine', 'the message'); + expect(logWithPrefix).toHaveBeenCalledWith('INFO', 'ScannerEngine', 'another message'); + expect(logWithPrefix).toHaveBeenCalledWith('ERROR', 'ScannerEngine', 'final message'); + expect(process.stdout.write).toHaveBeenCalledWith( + "some non-JSON message which shouldn't crash the bootstrapper", + ); + + stdoutStub.restore(); + }); + + it.each([['http'], ['https']])( + 'should forward proxy %s properties to JVM', + async (protocol: string) => { + await runScannerEngine( + '/some/path/to/java', + '/some/path/to/scanner-engine', + {}, + { + [ScannerProperty.SonarHostUrl]: `${protocol}://my-sonarqube.comp.org`, + [ScannerProperty.SonarScannerProxyHost]: 'some-proxy.io', + [ScannerProperty.SonarScannerProxyPort]: '4244', + [ScannerProperty.SonarScannerProxyUser]: 'the-user', + [ScannerProperty.SonarScannerProxyPassword]: 'the-pass', + }, + ); + + expect(spawn).toHaveBeenCalledWith('/some/path/to/java', [ + `-D${protocol}.proxyHost=some-proxy.io`, + `-D${protocol}.proxyPort=4244`, + `-D${protocol}.proxyUser=the-user`, + `-D${protocol}.proxyPassword=the-pass`, + '-jar', + '/some/path/to/scanner-engine', + ]); + }, + ); + }); }); From 01df4d4e96bca32a745917d683adca1d0ed5fc1b Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Thu, 25 Apr 2024 08:48:33 +0200 Subject: [PATCH 14/35] SCANNPM-2 Write targz test (#125) --- src/file.ts | 6 ++- test/unit/file.test.ts | 103 ++++++++++++++++++++++++++++++++++------- 2 files changed, 90 insertions(+), 19 deletions(-) diff --git a/src/file.ts b/src/file.ts index 77232f17..dbf04d58 100644 --- a/src/file.ts +++ b/src/file.ts @@ -59,6 +59,7 @@ export async function extractArchive(fromPath: string, toPath: string) { stream.pipe(fs.createWriteStream(filePath, { mode: header.mode })); + // end of file, move onto next file stream.on('end', next); stream.resume(); // just auto drain the stream @@ -74,7 +75,10 @@ export async function extractArchive(fromPath: string, toPath: string) { }); }); - fs.createReadStream(tarFilePath).pipe(zlib.createGunzip()).pipe(extract); + const readStream = fs.createReadStream(tarFilePath); + const gunzip = zlib.createGunzip(); + const nextStep = readStream.pipe(gunzip); + nextStep.pipe(extract); await extractionPromise; } else { diff --git a/test/unit/file.test.ts b/test/unit/file.test.ts index b5a12f49..0e4841b3 100644 --- a/test/unit/file.test.ts +++ b/test/unit/file.test.ts @@ -20,7 +20,9 @@ import AdmZip from 'adm-zip'; import fs from 'fs'; import path from 'path'; -import { Readable } from 'stream'; +import * as tarStream from 'tar-stream'; +import * as zlib from 'zlib'; +import { PassThrough } from 'stream'; import { extractArchive, getCacheDirectories, @@ -34,24 +36,21 @@ const MOCKED_PROPERTIES = { [ScannerProperty.SonarUserHome]: '/path/to/sonar/user/home', }; -// Mock the filesystem +jest.mock('fs'); +jest.mock('tar-stream'); +jest.mock('zlib'); + jest.mock('fs', () => ({ - createReadStream: jest.fn().mockImplementation(() => { - const mockStream = new Readable({ - read() { - process.nextTick(() => this.emit('end')); // emit 'end' on next tick - }, - }); - mockStream.pipe = jest.fn().mockReturnThis(); - return mockStream; - }), + createReadStream: jest.fn(), createWriteStream: jest.fn(), existsSync: jest.fn(), readFile: jest.fn(), mkdirSync: jest.fn(), })); -jest.mock('fs-extra', () => ({})); +jest.mock('fs-extra', () => ({ + ensureDir: jest.fn(), +})); jest.mock('adm-zip', () => { const MockAdmZip = jest.fn(); @@ -65,14 +64,82 @@ afterEach(() => { describe('file', () => { describe('extractArchive', () => { - it('should extract zip files to the specified directory', async () => { - const archivePath = 'path/to/archive.zip'; - const extractPath = 'path/to/extract'; + describe('zip', () => { + it('should extract zip files to the specified directory', async () => { + const archivePath = 'path/to/archive.zip'; + const extractPath = 'path/to/extract'; + + await extractArchive(archivePath, extractPath); + + const mockAdmZipInstance = (AdmZip as jest.MockedClass).mock.instances[0]; + expect(mockAdmZipInstance.extractAllTo).toHaveBeenCalledWith(extractPath, true, true); + }); + }); + + describe('tar.gz', () => { + const mockFilePath = 'path/to/file.tar.gz'; + const mockDestDir = 'path/to/dest'; + const mockFileHeader = { name: 'file.txt', mode: 0o777 }; + const mockOn = jest.fn(); + const mockPassThroughStream = new PassThrough(); + beforeEach(() => { + mockPassThroughStream.on = jest.fn().mockImplementation((event, callback) => { + if (event === 'data') { + callback('mock data'); + } else if (event === 'end') { + callback(); + } + }); + + mockPassThroughStream.resume = jest.fn(); + mockPassThroughStream.end = jest.fn(); + jest.spyOn(fs, 'createWriteStream').mockReturnValue({ + on: jest.fn(), + once: jest.fn(), + emit: jest.fn(), + end: jest.fn(), + write: jest.fn(), + } as unknown as fs.WriteStream); + jest + .spyOn(fs, 'createReadStream') + .mockReturnValue({ pipe: jest.fn().mockReturnThis() } as unknown as fs.ReadStream); + jest + .spyOn(tarStream, 'extract') + .mockReturnValue({ on: mockOn } as unknown as tarStream.Extract); + jest + .spyOn(zlib, 'createGunzip') + .mockReturnValue({ pipe: jest.fn().mockReturnThis() } as unknown as zlib.Gunzip); + }); - await extractArchive(archivePath, extractPath); + it('should extract a .tar.gz file to the specified directory', async () => { + mockOn.mockImplementation((event, callback) => { + if (event === 'entry') { + callback(mockFileHeader, mockPassThroughStream, jest.fn()); + } + if (event === 'finish') { + callback(); + } + }); + + await extractArchive(mockFilePath, mockDestDir); + + expect(fs.createReadStream).toHaveBeenCalledWith(mockFilePath); + expect(zlib.createGunzip).toHaveBeenCalled(); + expect(tarStream.extract).toHaveBeenCalled(); + expect(fs.createWriteStream).toHaveBeenCalledWith(`${mockDestDir}/${mockFileHeader.name}`, { + mode: 511, + }); + }); + + it('should throw if extract fails', async () => { + mockOn.mockImplementation((event, callback) => { + if (event === 'error') { + callback(new Error('mock error')); + } + }); - const mockAdmZipInstance = (AdmZip as jest.MockedClass).mock.instances[0]; - expect(mockAdmZipInstance.extractAllTo).toHaveBeenCalledWith(extractPath, true, true); + await expect(extractArchive(mockFilePath, mockDestDir)).rejects.toThrow('mock error'); + }); }); }); From 0affbae9dfd3d40d740e9b9915dd23037076fc1d Mon Sep 17 00:00:00 2001 From: 7PH Date: Thu, 25 Apr 2024 11:04:54 +0200 Subject: [PATCH 15/35] SCANNPM-2 Allow sending arbitrary os/arch using default properties --- src/java.ts | 25 +++++++++---------- src/platform.ts | 10 +------- src/properties.ts | 39 ++++++++++++++++-------------- src/scan.ts | 15 ++++++------ src/types.ts | 9 ++----- test/unit/java.test.ts | 18 ++++++++------ test/unit/mocks/FakeProjectMock.ts | 11 ++++++--- test/unit/platform.test.ts | 38 +++++++++++------------------ test/unit/properties.test.ts | 4 +++ test/unit/scan.test.ts | 8 +++--- 10 files changed, 83 insertions(+), 94 deletions(-) diff --git a/src/java.ts b/src/java.ts index cc5951f8..38f9116a 100644 --- a/src/java.ts +++ b/src/java.ts @@ -33,9 +33,9 @@ import { getCacheFileLocation, validateChecksum, } from './file'; -import { log, LogLevel } from './logging'; +import { LogLevel, log } from './logging'; import { download, fetch } from './request'; -import { PlatformInfo, ScannerProperties, ScannerProperty } from './types'; +import { ScannerProperties, ScannerProperty } from './types'; export async function fetchServerVersion(): Promise { let version: SemVer | null = null; @@ -91,12 +91,9 @@ export async function serverSupportsJREProvisioning( return supports; } -export async function fetchJRE( - properties: ScannerProperties, - platformInfo: PlatformInfo, -): Promise { +export async function fetchJRE(properties: ScannerProperties): Promise { log(LogLevel.DEBUG, 'Detecting latest version of JRE'); - const latestJREData = await fetchLatestSupportedJRE(platformInfo); + const latestJREData = await fetchLatestSupportedJRE(properties); log(LogLevel.INFO, 'Latest Supported JRE: ', latestJREData); log(LogLevel.DEBUG, 'Looking for Cached JRE'); @@ -126,17 +123,17 @@ export async function fetchJRE( } } -async function fetchLatestSupportedJRE(platformInfo: PlatformInfo) { - log( - LogLevel.DEBUG, - `Downloading JRE for ${platformInfo.os} ${platformInfo.arch} from ${API_V2_JRE_ENDPOINT}`, - ); +async function fetchLatestSupportedJRE(properties: ScannerProperties) { + const os = properties[ScannerProperty.SonarScannerOs]; + const arch = properties[ScannerProperty.SonarScannerArch]; + + log(LogLevel.DEBUG, `Downloading JRE for ${os} ${arch} from ${API_V2_JRE_ENDPOINT}`); const { data } = await fetch({ url: API_V2_JRE_ENDPOINT, params: { - os: platformInfo.os, - arch: platformInfo.arch, + os, + arch, }, }); diff --git a/src/platform.ts b/src/platform.ts index a3857621..71167f27 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -20,7 +20,6 @@ import fs from 'fs'; import { LogLevel, log } from './logging'; -import { PlatformInfo, SupportedOS } from './types'; export function getArch(): NodeJS.Architecture { return process.arch; @@ -53,13 +52,6 @@ function isAlpineLinux(): boolean { return !!content && (content.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] === 'alpine'; } -function getSupportedOS(): SupportedOS { +export function getSupportedOS(): NodeJS.Platform | 'alpine' { return isAlpineLinux() ? 'alpine' : process.platform; } - -export function getPlatformInfo(): PlatformInfo { - return { - os: getSupportedOS(), - arch: getArch(), - }; -} diff --git a/src/properties.ts b/src/properties.ts index 4e43b956..d9f36372 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -33,15 +33,20 @@ import { SONAR_PROJECT_FILENAME, } from './constants'; import { LogLevel, log } from './logging'; +import { getArch, getSupportedOS } from './platform'; import { ScanOptions, ScannerProperties, ScannerProperty } from './types'; -const DEFAULT_PROPERTIES = { - [ScannerProperty.SonarUserHome]: path.join( - process.env.HOME ?? process.env.USERPROFILE ?? '', - SONAR_DIR_DEFAULT, - ), - [ScannerProperty.SonarScannerCliVersion]: SCANNER_CLI_VERSION, -}; +function getDefaultProperties(): ScannerProperties { + return { + [ScannerProperty.SonarUserHome]: path.join( + process.env.HOME ?? process.env.USERPROFILE ?? '', + SONAR_DIR_DEFAULT, + ), + [ScannerProperty.SonarScannerCliVersion]: SCANNER_CLI_VERSION, + [ScannerProperty.SonarScannerOs]: getSupportedOS(), + [ScannerProperty.SonarScannerArch]: getArch(), + }; +} /** * Convert the name of a sonar property from its environment variable form @@ -339,17 +344,15 @@ export function getProperties( } // Merge properties respecting order of precedence - const properties = [ - { 'sonar.projectBaseDir': projectBaseDir }, // Manually computed, can't be overridden - bootstrapperProperties, // Can't be overridden - cliProperties, // Highest precedence - scanOptionsProperties, - inferredProperties, - envProperties, // Lowest precedence - DEFAULT_PROPERTIES, // fallback to default if nothing was provided for these properties - ] - .reverse() - .reduce((acc, curr) => ({ ...acc, ...curr }), {}); + const properties = { + ...getDefaultProperties(), // fallback to default if nothing was provided for these properties + ...envProperties, // Lowest precedence + ...inferredProperties, + ...scanOptionsProperties, + ...cliProperties, // Highest precedence + ...bootstrapperProperties, // Can't be overridden + ...{ 'sonar.projectBaseDir': projectBaseDir }, // Manually computed, can't be overridden + }; return { ...properties, diff --git a/src/scan.ts b/src/scan.ts index 8e9a0117..1f537e72 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -22,7 +22,6 @@ import { version } from '../package.json'; import { SCANNER_CLI_DEFAULT_BIN_NAME } from './constants'; import { fetchJRE, serverSupportsJREProvisioning } from './java'; import { LogLevel, log, setLogLevel } from './logging'; -import { getPlatformInfo } from './platform'; import { getProperties } from './properties'; import { initializeAxios } from './request'; import { downloadScannerCli, runScannerCli, tryLocalSonarScannerExecutable } from './scanner-cli'; @@ -51,17 +50,19 @@ async function runScan(scanOptions: ScanOptions, cliArgs?: string[]) { log(LogLevel.DEBUG, `Overriding the log level to ${properties[ScannerProperty.SonarLogLevel]}`); } - log(LogLevel.DEBUG, 'Properties: ', properties); + log(LogLevel.DEBUG, 'Properties:', properties); + log( + LogLevel.INFO, + 'Platform:', + properties[ScannerProperty.SonarScannerOs], + properties[ScannerProperty.SonarScannerArch], + ); initializeAxios(properties); log(LogLevel.INFO, `Server URL: ${properties[ScannerProperty.SonarHostUrl]}`); log(LogLevel.INFO, `Version: ${version}`); - log(LogLevel.DEBUG, 'Finding platform info'); - const platformInfo = getPlatformInfo(); - log(LogLevel.INFO, 'Platform: ', platformInfo); - log(LogLevel.DEBUG, 'Check if Server supports JRE Provisioning'); const supportsJREProvisioning = await serverSupportsJREProvisioning(properties); log(LogLevel.INFO, `JRE Provisioning ${supportsJREProvisioning ? 'is' : 'is NOT'} supported`); @@ -83,7 +84,7 @@ async function runScan(scanOptions: ScanOptions, cliArgs?: string[]) { // TODO: also check if JRE is explicitly set by properties const explicitJREPathOverride = properties[ScannerProperty.SonarScannerJavaExePath]; - const latestJRE = explicitJREPathOverride ?? (await fetchJRE(properties, platformInfo)); + const latestJRE = explicitJREPathOverride ?? (await fetchJRE(properties)); const latestScannerEngine = await fetchScannerEngine(properties); log(LogLevel.INFO, 'Running the Scanner Engine'); diff --git a/src/types.ts b/src/types.ts index 7ae8e500..16220711 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,13 +19,6 @@ */ import { LogLevel } from './logging'; -export type SupportedOS = NodeJS.Platform | 'alpine'; - -export type PlatformInfo = { - os: SupportedOS | null; - arch: NodeJS.Architecture | null; -}; - export type JreMetaData = { filename: string; md5: string; @@ -51,6 +44,8 @@ export enum ScannerProperty { SonarExclusions = 'sonar.exclusions', SonarHostUrl = 'sonar.host.url', SonarUserHome = 'sonar.userHome', + SonarScannerOs = 'sonar.scanner.os', + SonarScannerArch = 'sonar.scanner.arch', SonarOrganization = 'sonar.organization', SonarProjectBaseDir = 'sonar.projectBaseDir', SonarScannerSonarCloudURL = 'sonar.scanner.sonarcloudUrl', diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index bf6b0000..fdcc9a28 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -25,12 +25,14 @@ import { API_V2_JRE_ENDPOINT, SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../ import * as file from '../../src/file'; import { fetchJRE, fetchServerVersion, serverSupportsJREProvisioning } from '../../src/java'; import * as request from '../../src/request'; -import { JreMetaData, PlatformInfo, ScannerProperties, ScannerProperty } from '../../src/types'; +import { JreMetaData, ScannerProperties, ScannerProperty } from '../../src/types'; const mock = new MockAdapter(axios); const MOCKED_PROPERTIES: ScannerProperties = { [ScannerProperty.SonarHostUrl]: 'http://sonarqube.com', + [ScannerProperty.SonarScannerOs]: 'linux', + [ScannerProperty.SonarScannerArch]: 'arm64', }; beforeEach(() => { @@ -101,7 +103,6 @@ describe('java', () => { }); describe('when JRE provisioning is supported', () => { - const platformInfo: PlatformInfo = { os: 'linux', arch: 'arm64' }; const serverResponse: JreMetaData = { filename: 'mock-jre.tar.gz', javaPath: 'jre/bin/java', @@ -115,8 +116,8 @@ describe('java', () => { mock .onGet(API_V2_JRE_ENDPOINT, { params: { - os: platformInfo.os, - arch: platformInfo.arch, + os: MOCKED_PROPERTIES[ScannerProperty.SonarScannerOs], + arch: MOCKED_PROPERTIES[ScannerProperty.SonarScannerArch], }, }) .reply(200, serverResponse); @@ -128,7 +129,7 @@ describe('java', () => { describe('when the JRE is cached', () => { it('should fetch the latest supported JRE and use the cached version', async () => { - await fetchJRE(MOCKED_PROPERTIES, platformInfo); + await fetchJRE(MOCKED_PROPERTIES); expect(request.fetch).toHaveBeenCalledTimes(1); expect(request.download).not.toHaveBeenCalled(); @@ -153,11 +154,14 @@ describe('java', () => { }); it('should download the JRE', async () => { - await fetchJRE({ ...MOCKED_PROPERTIES }, platformInfo); + await fetchJRE({ ...MOCKED_PROPERTIES }); expect(request.fetch).toHaveBeenCalledWith({ url: API_V2_JRE_ENDPOINT, - params: platformInfo, + params: { + os: MOCKED_PROPERTIES[ScannerProperty.SonarScannerOs], + arch: MOCKED_PROPERTIES[ScannerProperty.SonarScannerArch], + }, }); expect(file.getCacheFileLocation).toHaveBeenCalledTimes(1); diff --git a/test/unit/mocks/FakeProjectMock.ts b/test/unit/mocks/FakeProjectMock.ts index 3728e9bd..57a99f48 100644 --- a/test/unit/mocks/FakeProjectMock.ts +++ b/test/unit/mocks/FakeProjectMock.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import path from 'path'; +import sinon from 'sinon'; import { SCANNER_BOOTSTRAPPER_NAME, SCANNER_CLI_VERSION } from '../../../src/constants'; const baseEnvVariables = process.env; @@ -37,12 +38,14 @@ export class FakeProjectMock { } else { this.projectPath = ''; } - process.env = baseEnvVariables; - process.cwd = () => this.projectPath; + sinon.stub(process, 'platform').value('windows'); + sinon.stub(process, 'arch').value('aarch64'); + sinon.stub(process, 'env').value(baseEnvVariables); + sinon.stub(process, 'cwd').value(() => this.projectPath); } setEnvironmentVariables(values: { [key: string]: string }) { - process.env = values; + sinon.stub(process, 'env').value(values); } getStartTime() { @@ -59,6 +62,8 @@ export class FakeProjectMock { 'sonar.scanner.wasJreCacheHit': 'false', 'sonar.scanner.version': SCANNER_CLI_VERSION, 'sonar.userHome': expect.stringMatching(/\.sonar$/), + 'sonar.scanner.os': 'windows', + 'sonar.scanner.arch': 'aarch64', }; } } diff --git a/test/unit/platform.test.ts b/test/unit/platform.test.ts index fb088b00..6c000602 100644 --- a/test/unit/platform.test.ts +++ b/test/unit/platform.test.ts @@ -28,10 +28,8 @@ describe('getPlatformInfo', () => { const platformStub = sinon.stub(process, 'platform').value('darwin'); const archStub = sinon.stub(process, 'arch').value('arm64'); - expect(platform.getPlatformInfo()).toEqual({ - os: 'darwin', - arch: 'arm64', - }); + expect(platform.getSupportedOS()).toEqual('darwin'); + expect(platform.getArch()).toEqual('arm64'); platformStub.restore(); archStub.restore(); @@ -41,10 +39,8 @@ describe('getPlatformInfo', () => { const platformStub = sinon.stub(process, 'platform').value('win32'); const archStub = sinon.stub(process, 'arch').value('x64'); - expect(platform.getPlatformInfo()).toEqual({ - os: 'win32', - arch: 'x64', - }); + expect(platform.getSupportedOS()).toEqual('win32'); + expect(platform.getArch()).toEqual('x64'); platformStub.restore(); archStub.restore(); @@ -54,10 +50,8 @@ describe('getPlatformInfo', () => { const platformStub = sinon.stub(process, 'platform').value('openbsd'); const archStub = sinon.stub(process, 'arch').value('x64'); - expect(platform.getPlatformInfo()).toEqual({ - os: 'openbsd', - arch: 'x64', - }); + expect(platform.getSupportedOS()).toEqual('openbsd'); + expect(platform.getArch()).toEqual('x64'); platformStub.restore(); archStub.restore(); @@ -69,10 +63,8 @@ describe('getPlatformInfo', () => { const fsReadStub = sinon.stub(fs, 'readFileSync'); fsReadStub.withArgs('/etc/os-release').returns('NAME="Alpine Linux"\nID=alpine'); - expect(platform.getPlatformInfo()).toEqual({ - os: 'alpine', - arch: 'x64', - }); + expect(platform.getSupportedOS()).toEqual('alpine'); + expect(platform.getArch()).toEqual('x64'); platformStub.restore(); archStub.restore(); @@ -85,10 +77,8 @@ describe('getPlatformInfo', () => { const fsReadStub = sinon.stub(fs, 'readFileSync'); fsReadStub.withArgs('/usr/lib/os-release').returns('NAME="Alpine Linux"\nID=alpine'); - expect(platform.getPlatformInfo()).toEqual({ - os: 'alpine', - arch: 'x64', - }); + expect(platform.getSupportedOS()).toEqual('alpine'); + expect(platform.getArch()).toEqual('x64'); platformStub.restore(); archStub.restore(); @@ -98,11 +88,10 @@ describe('getPlatformInfo', () => { it('failed to detect alpine', () => { const platformStub = sinon.stub(process, 'platform').value('linux'); const archStub = sinon.stub(process, 'arch').value('x64'); + const fsReadStub = sinon.stub(fs, 'readFileSync'); - expect(platform.getPlatformInfo()).toEqual({ - os: 'linux', - arch: 'x64', - }); + expect(platform.getSupportedOS()).toEqual('linux'); + expect(platform.getArch()).toEqual('x64'); expect(log).toHaveBeenCalledWith( LogLevel.WARN, @@ -111,5 +100,6 @@ describe('getPlatformInfo', () => { platformStub.restore(); archStub.restore(); + fsReadStub.restore(); }); }); diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index 63cf01b1..03dc351a 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import sinon from 'sinon'; import { DEFAULT_SONAR_EXCLUSIONS, SCANNER_BOOTSTRAPPER_NAME, @@ -36,6 +37,7 @@ jest.mock('../../package.json', () => ({ const projectHandler = new FakeProjectMock(); afterEach(() => { + sinon.restore(); projectHandler.reset(); }); @@ -79,6 +81,7 @@ describe('getProperties', () => { verbose: true, options: { 'sonar.projectKey': 'use-this-project-key', + 'sonar.scanner.os': 'some-os', }, }, projectHandler.getStartTime(), @@ -94,6 +97,7 @@ describe('getProperties', () => { 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', 'sonar.sources': 'the-sources', + 'sonar.scanner.os': 'some-os', }); }); diff --git a/test/unit/scan.test.ts b/test/unit/scan.test.ts index 43904b24..0a7f1ee1 100644 --- a/test/unit/scan.test.ts +++ b/test/unit/scan.test.ts @@ -65,12 +65,10 @@ describe('scan', () => { it('should output the current platform', async () => { jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); - jest.spyOn(platform, 'getPlatformInfo').mockReturnValue({ os: 'alpine', arch: 'arm64' }); + jest.spyOn(platform, 'getSupportedOS').mockReturnValue('alpine'); + jest.spyOn(platform, 'getArch').mockReturnValue('arm64'); await scan({}, []); - expect(logging.log).toHaveBeenCalledWith('INFO', 'Platform: ', { - os: 'alpine', - arch: 'arm64', - }); + expect(logging.log).toHaveBeenCalledWith('INFO', 'Platform:', 'alpine', 'arm64'); }); describe('when the SQ version does not support JRE provisioning', () => { From d805adb98e7d7dd3ff66cacc00fe3705fa689c50 Mon Sep 17 00:00:00 2001 From: 7PH Date: Mon, 29 Apr 2024 00:28:20 +0200 Subject: [PATCH 16/35] SCANNPM-2 Add support for skipping JRE provisioning --- src/scan.ts | 18 +++++--- src/scanner-engine.ts | 2 + src/types.ts | 1 + test/unit/scan.test.ts | 95 ++++++++++++++++++++++++++++++++++++------ 4 files changed, 98 insertions(+), 18 deletions(-) diff --git a/src/scan.ts b/src/scan.ts index 1f537e72..3827a287 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -82,11 +82,19 @@ async function runScan(scanOptions: ScanOptions, cliArgs?: string[]) { return; } - // TODO: also check if JRE is explicitly set by properties - const explicitJREPathOverride = properties[ScannerProperty.SonarScannerJavaExePath]; - const latestJRE = explicitJREPathOverride ?? (await fetchJRE(properties)); + // Detect what Java to use (in path, specified from properties or provisioned) + let javaPath: string; + if (properties[ScannerProperty.SonarScannerJavaExePath]) { + javaPath = properties[ScannerProperty.SonarScannerJavaExePath]; + } else if (properties[ScannerProperty.SonarScannerSkipJreProvisioning] === 'true') { + javaPath = 'java'; + } else { + javaPath = await fetchJRE(properties); + } + + // Fetch the Scanner Engine const latestScannerEngine = await fetchScannerEngine(properties); - log(LogLevel.INFO, 'Running the Scanner Engine'); - await runScannerEngine(latestJRE, latestScannerEngine, scanOptions, properties); + // Run the Scanner Engine + await runScannerEngine(javaPath, latestScannerEngine, scanOptions, properties); } diff --git a/src/scanner-engine.ts b/src/scanner-engine.ts index 47d602e9..2b59b705 100644 --- a/src/scanner-engine.ts +++ b/src/scanner-engine.ts @@ -96,6 +96,8 @@ export async function runScannerEngine( scanOptions: ScanOptions, properties: ScannerProperties, ) { + log(LogLevel.INFO, 'Running the Scanner Engine'); + // The scanner engine expects a JSON object of properties attached to a key name "scannerProperties" const propertiesJSON = JSON.stringify({ scannerProperties: properties }); diff --git a/src/types.ts b/src/types.ts index 16220711..a045bdda 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,6 +56,7 @@ export enum ScannerProperty { SonarScannerProxyPort = 'sonar.scanner.proxyPort', SonarScannerProxyUser = 'sonar.scanner.proxyUser', SonarScannerProxyPassword = 'sonar.scanner.proxyPassword', + SonarScannerSkipJreProvisioning = 'sonar.scanner.skipJreProvisioning', SonarScannerInternalIsSonarCloud = 'sonar.scanner.internal.isSonarCloud', SonarScannerInternalSqVersion = 'sonar.scanner.internal.sqVersion', SonarScannerCliVersion = 'sonar.scanner.version', diff --git a/test/unit/scan.test.ts b/test/unit/scan.test.ts index 0a7f1ee1..b4dae5c0 100644 --- a/test/unit/scan.test.ts +++ b/test/unit/scan.test.ts @@ -27,7 +27,10 @@ jest.mock('../../src/logging', () => ({ import * as java from '../../src/java'; import * as logging from '../../src/logging'; import * as platform from '../../src/platform'; +import * as scannerEngine from '../../src/scanner-engine'; +import * as scannerCli from '../../src/scanner-cli'; import { scan } from '../../src/scan'; +import { ScannerProperty } from '../../src/types'; jest.mock('../../src/java'); jest.mock('../../src/scanner-cli'); @@ -71,30 +74,96 @@ describe('scan', () => { expect(logging.log).toHaveBeenCalledWith('INFO', 'Platform:', 'alpine', 'arm64'); }); - describe('when the SQ version does not support JRE provisioning', () => { - it('should not fetch the JRE version', async () => { + describe('when server does not support JRE provisioning', () => { + it('should download and run SonarScanner CLI', async () => { jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); - await scan({}, []); + jest.spyOn(scannerEngine, 'runScannerEngine'); + jest.spyOn(scannerCli, 'downloadScannerCli').mockResolvedValue('/path/to/scanner-cli'); + jest.spyOn(scannerCli, 'runScannerCli'); + + await scan({ serverUrl: 'http://localhost:9000' }); + expect(java.fetchJRE).not.toHaveBeenCalled(); + expect(scannerEngine.runScannerEngine).not.toHaveBeenCalled(); + expect(scannerCli.runScannerCli).toHaveBeenCalled(); + const [, , scannerPath] = (scannerCli.runScannerCli as jest.Mock).mock.calls.pop(); + expect(scannerPath).toBe('/path/to/scanner-cli'); }); - }); - describe('when the user provides a JRE exe path override', () => { - it('should not fetch the JRE version', async () => { + it('should use local scanner if requested', async () => { jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); - await scan({ options: { 'sonar.scanner.javaExePath': 'path/to/java' } }, []); - expect(java.fetchJRE).not.toHaveBeenCalled(); + jest.spyOn(scannerEngine, 'runScannerEngine'); + jest.spyOn(scannerCli, 'runScannerCli'); + jest.spyOn(scannerCli, 'tryLocalSonarScannerExecutable').mockResolvedValue(true); + + await scan({ serverUrl: 'http://localhost:9000', localScannerCli: true }); + + expect(scannerCli.downloadScannerCli).not.toHaveBeenCalled(); + expect(scannerCli.runScannerCli).toHaveBeenCalled(); + const [, , scannerPath] = (scannerCli.runScannerCli as jest.Mock).mock.calls.pop(); + expect(scannerPath).toBe('sonar-scanner'); + }); - // TODO: test that the JRE exe path is used when running the scanner engine + it('should fail if local scanner is requested but not found', async () => { + jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); + jest.spyOn(scannerEngine, 'runScannerEngine'); + jest.spyOn(scannerCli, 'runScannerCli'); + jest.spyOn(scannerCli, 'tryLocalSonarScannerExecutable').mockResolvedValue(false); + + await scan({ serverUrl: 'http://localhost:9000', localScannerCli: true }); + + expect(scannerCli.downloadScannerCli).not.toHaveBeenCalled(); + expect(scannerCli.runScannerCli).not.toHaveBeenCalled(); + expect(logging.log).toHaveBeenCalledWith( + logging.LogLevel.ERROR, + expect.stringMatching(/Local scanner is requested but not found/), + ); }); }); - describe('when the user provides a SonarQube URL and the version supports provisioning', () => { - it('should fetch the JRE version', async () => { + describe('when server supports provisioning', () => { + it('should fetch the JRE', async () => { jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(true); - jest.spyOn(java, 'fetchJRE'); - await scan({ serverUrl: 'http://localhost:9000' }, []); + jest.spyOn(java, 'fetchJRE').mockResolvedValue('/some-provisioned-jre'); + jest.spyOn(scannerEngine, 'runScannerEngine'); + + await scan({ serverUrl: 'http://localhost:9000' }); + expect(java.fetchJRE).toHaveBeenCalled(); + const [javaPath] = (scannerEngine.runScannerEngine as jest.Mock).mock.calls.pop(); + expect(javaPath).toBe('/some-provisioned-jre'); + }); + + it('should not fetch the JRE if the JRE path is explicitly specified', async () => { + jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(true); + jest.spyOn(java, 'fetchJRE'); + jest.spyOn(scannerEngine, 'runScannerEngine'); + + await scan({ + serverUrl: 'http://localhost:9000', + options: { [ScannerProperty.SonarScannerJavaExePath]: 'path/to/java' }, + }); + + expect(java.fetchJRE).not.toHaveBeenCalled(); + const [javaPath] = (scannerEngine.runScannerEngine as jest.Mock).mock.calls.pop(); + expect(javaPath).toBe('path/to/java'); + }); + + it('should not fetch the JRE if skipping JRE provisioning explicitly', async () => { + jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(true); + jest.spyOn(java, 'fetchJRE'); + jest.spyOn(scannerEngine, 'runScannerEngine'); + + await scan({ + serverUrl: 'http://localhost:9000', + options: { + [ScannerProperty.SonarScannerSkipJreProvisioning]: 'true', + }, + }); + + expect(java.fetchJRE).not.toHaveBeenCalled(); + const [javaPath] = (scannerEngine.runScannerEngine as jest.Mock).mock.calls.pop(); + expect(javaPath).toBe('java'); }); }); }); From e50b7def7a4fed49b6dd99dc28f3a3c06d5f7620 Mon Sep 17 00:00:00 2001 From: 7PH Date: Mon, 29 Apr 2024 00:37:19 +0200 Subject: [PATCH 17/35] SCANNPM-2 Add support for dumping data to file instead of running the Scanner Engine --- src/scanner-engine.ts | 17 ++++++++++++++++- src/types.ts | 1 + test/unit/scanner-engine.test.ts | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/scanner-engine.ts b/src/scanner-engine.ts index 2b59b705..0113ddb0 100644 --- a/src/scanner-engine.ts +++ b/src/scanner-engine.ts @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { spawn } from 'child_process'; +import fs from 'fs'; import { extractArchive, getCacheDirectories, @@ -90,7 +91,7 @@ async function logOutput(message: string) { } } -export async function runScannerEngine( +export function runScannerEngine( javaBinPath: string, scannerEnginePath: string, scanOptions: ScanOptions, @@ -108,6 +109,20 @@ export async function runScannerEngine( '-jar', scannerEnginePath, ]; + + // If debugging with dumpToFile, write the properties to a file and exit + const dumpToFile = properties[ScannerProperty.SonarScannerInternalDumpToFile]; + if (dumpToFile) { + const data = { + propertiesJSON, + javaBinPath, + scannerEnginePath, + args, + }; + log(LogLevel.INFO, 'Dumping data to file and exiting'); + return fs.promises.writeFile(dumpToFile, JSON.stringify(data, null, 2)); + } + log(LogLevel.DEBUG, 'Running scanner engine', javaBinPath, ...args); const child = spawn(javaBinPath, args); diff --git a/src/types.ts b/src/types.ts index a045bdda..84692b39 100644 --- a/src/types.ts +++ b/src/types.ts @@ -57,6 +57,7 @@ export enum ScannerProperty { SonarScannerProxyUser = 'sonar.scanner.proxyUser', SonarScannerProxyPassword = 'sonar.scanner.proxyPassword', SonarScannerSkipJreProvisioning = 'sonar.scanner.skipJreProvisioning', + SonarScannerInternalDumpToFile = 'sonar.scanner.internal.dumpToFile', SonarScannerInternalIsSonarCloud = 'sonar.scanner.internal.isSonarCloud', SonarScannerInternalSqVersion = 'sonar.scanner.internal.sqVersion', SonarScannerCliVersion = 'sonar.scanner.version', diff --git a/test/unit/scanner-engine.test.ts b/test/unit/scanner-engine.test.ts index 37399891..361614ae 100644 --- a/test/unit/scanner-engine.test.ts +++ b/test/unit/scanner-engine.test.ts @@ -29,6 +29,7 @@ import { fetchScannerEngine, runScannerEngine } from '../../src/scanner-engine'; import { ScannerProperties, ScannerProperty } from '../../src/types'; import { ChildProcessMock } from './mocks/ChildProcessMock'; import { logWithPrefix } from '../../src/logging'; +import fs from 'fs'; const mock = new MockAdapter(axios); @@ -202,6 +203,23 @@ describe('scanner-engine', () => { stdoutStub.restore(); }); + it('should dump data to file when dumpToFile property is set', async () => { + childProcessHandler.setExitCode(1); // Make it so the scanner would fail + const writeFile = jest.spyOn(fs.promises, 'writeFile').mockResolvedValue(); + + await runScannerEngine( + '/some/path/to/java', + '/some/path/to/scanner-engine', + {}, + { + ...MOCKED_PROPERTIES, + [ScannerProperty.SonarScannerInternalDumpToFile]: '/path/to/dump.json', + }, + ); + + expect(writeFile).toHaveBeenCalledWith('/path/to/dump.json', expect.any(String)); + }); + it.each([['http'], ['https']])( 'should forward proxy %s properties to JVM', async (protocol: string) => { From fc126a1003a6bb025494776adc965cdf9050ffa2 Mon Sep 17 00:00:00 2001 From: 7PH Date: Mon, 29 Apr 2024 00:44:29 +0200 Subject: [PATCH 18/35] SCANNPM-2 Add support for sonar.scanner.responseTimeout --- src/request.ts | 3 +++ src/types.ts | 1 + test/unit/request.test.ts | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/request.ts b/src/request.ts index ac81bf8b..58158b66 100644 --- a/src/request.ts +++ b/src/request.ts @@ -48,6 +48,8 @@ export function initializeAxios(properties: ScannerProperties) { const token = properties[ScannerProperty.SonarToken]; const baseURL = properties[ScannerProperty.SonarHostUrl]; const agents = getHttpAgents(properties); + const timeout = + Math.floor(parseInt(properties[ScannerProperty.SonarScannerResponseTimeout], 10) || 0) * 1000; if (!_axiosInstance) { _axiosInstance = axios.create({ @@ -55,6 +57,7 @@ export function initializeAxios(properties: ScannerProperties) { headers: { Authorization: `Bearer ${token}`, }, + timeout, ...agents, }); } diff --git a/src/types.ts b/src/types.ts index 84692b39..c29ae0f4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,6 +56,7 @@ export enum ScannerProperty { SonarScannerProxyPort = 'sonar.scanner.proxyPort', SonarScannerProxyUser = 'sonar.scanner.proxyUser', SonarScannerProxyPassword = 'sonar.scanner.proxyPassword', + SonarScannerResponseTimeout = 'sonar.scanner.responseTimeout', SonarScannerSkipJreProvisioning = 'sonar.scanner.skipJreProvisioning', SonarScannerInternalDumpToFile = 'sonar.scanner.internal.dumpToFile', SonarScannerInternalIsSonarCloud = 'sonar.scanner.internal.isSonarCloud', diff --git a/test/unit/request.test.ts b/test/unit/request.test.ts index a9c4bcb7..fd850a40 100644 --- a/test/unit/request.test.ts +++ b/test/unit/request.test.ts @@ -69,6 +69,27 @@ describe('request', () => { headers: { Authorization: `Bearer testToken`, }, + timeout: 0, + }); + }); + + it('should initialize axios with timeout', () => { + jest.spyOn(axios, 'create'); + + const properties: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarToken]: 'testToken', + [ScannerProperty.SonarScannerResponseTimeout]: '23', + }; + + initializeAxios(properties); + + expect(axios.create).toHaveBeenCalledWith({ + baseURL: 'https://sonarcloud.io', + headers: { + Authorization: `Bearer testToken`, + }, + timeout: 23000, }); }); From 6dd8720dfb60623e024993ee94df3be6cd7df48d Mon Sep 17 00:00:00 2001 From: Victor Date: Wed, 1 May 2024 11:04:46 +0200 Subject: [PATCH 19/35] SCANNPM-2 Sanitize input passed to child processes (#135) --- .gitignore | 1 + bin/sonar-scanner | 2 ++ package-lock.json | 9 +++++++++ package.json | 3 ++- src/platform.ts | 12 ++++++++++-- src/properties.ts | 25 ++++++++++++++----------- src/{bin/sonar-scanner => runner.ts} | 17 +++++++++++++---- src/scan.ts | 6 +++--- src/scanner-cli.ts | 12 +++++++----- src/types.ts | 5 +++++ test/unit/properties.test.ts | 6 +++--- test/unit/scan.test.ts | 10 +++++----- 12 files changed, 74 insertions(+), 34 deletions(-) create mode 100755 bin/sonar-scanner rename src/{bin/sonar-scanner => runner.ts} (67%) diff --git a/.gitignore b/.gitignore index 4643056e..c82680f4 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ cmake-build-debug/ # IntelliJ /out/ +*.iml # mpeltonen/sbt-idea plugin .idea_modules/ diff --git a/bin/sonar-scanner b/bin/sonar-scanner new file mode 100755 index 00000000..086c0709 --- /dev/null +++ b/bin/sonar-scanner @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('../build/src/runner'); diff --git a/package-lock.json b/package-lock.json index bbaf58be..9ee2e40c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "adm-zip": "0.5.12", "axios": "1.6.8", + "commander": "12.0.0", "fs-extra": "11.2.0", "hpagent": "1.2.0", "jest-sonar-reporter": "2.0.0", @@ -2071,6 +2072,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "12.0.0", + "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "engines": { + "node": ">=18" + } + }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, diff --git a/package.json b/package.json index 2fee5d43..a1b5999e 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "license": "LGPL-3.0-only", "main": "src/index.js", "bin": { - "sonar-scanner": "src/bin/sonar-scanner" + "sonar-scanner": "bin/sonar-scanner" }, "engines": { "node": ">= 18" @@ -25,6 +25,7 @@ "dependencies": { "adm-zip": "0.5.12", "axios": "1.6.8", + "commander": "12.0.0", "fs-extra": "11.2.0", "hpagent": "1.2.0", "jest-sonar-reporter": "2.0.0", diff --git a/src/platform.ts b/src/platform.ts index 71167f27..559f4f7d 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -25,8 +25,16 @@ export function getArch(): NodeJS.Architecture { return process.arch; } -function isLinux(): boolean { - return process.platform.startsWith('linux'); +export function isLinux(): boolean { + return process.platform === 'linux'; +} + +export function isWindows() { + return process.platform === 'win32'; +} + +export function isMac() { + return process.platform === 'darwin'; } /** diff --git a/src/properties.ts b/src/properties.ts index d9f36372..e174c08a 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -34,7 +34,7 @@ import { } from './constants'; import { LogLevel, log } from './logging'; import { getArch, getSupportedOS } from './platform'; -import { ScanOptions, ScannerProperties, ScannerProperty } from './types'; +import { ScanOptions, ScannerProperties, ScannerProperty, CliArgs } from './types'; function getDefaultProperties(): ScannerProperties { return { @@ -151,18 +151,21 @@ function getPackageJsonProperties( /** * Convert CLI args into scanner properties. */ -function getCommandLineProperties(cliArgs?: string[]): ScannerProperties { - if (!cliArgs || cliArgs.length === 0) { - return {}; +function getCommandLineProperties(cliArgs?: CliArgs): ScannerProperties { + const properties: ScannerProperties = {}; + + if (cliArgs?.debug) { + properties[ScannerProperty.SonarVerbose] = 'true'; + } + + const { define } = cliArgs ?? {}; + if (!define || define.length === 0) { + return properties; } // Parse CLI args (eg: -Dsonar.token=xxx) - const properties: ScannerProperties = {}; - for (const arg of cliArgs) { - if (!arg.startsWith('-D')) { - continue; - } - const [key, value] = arg.substring(2).split('='); + for (const arg of define) { + const [key, value] = arg.split('='); properties[key] = value; } @@ -308,7 +311,7 @@ export function getHostProperties(properties: ScannerProperties): ScannerPropert export function getProperties( scanOptions: ScanOptions, startTimestampMs: number, - cliArgs?: string[], + cliArgs?: CliArgs, ): ScannerProperties { const bootstrapperProperties = getBootstrapperProperties(startTimestampMs); const cliProperties = getCommandLineProperties(cliArgs); diff --git a/src/bin/sonar-scanner b/src/runner.ts similarity index 67% rename from src/bin/sonar-scanner rename to src/runner.ts index c9fcb83d..8e54bc53 100755 --- a/src/bin/sonar-scanner +++ b/src/runner.ts @@ -1,4 +1,3 @@ -#!/usr/bin/env node /* * sonar-scanner-npm * Copyright (C) 2022-2024 SonarSource SA @@ -18,10 +17,20 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -const scan = require('../../build/src/index').scan; -const options = process.argv.length > 2 ? process.argv.slice(2) : []; +import { scan } from './scan'; +import { program } from 'commander'; +import { version } from '../package.json'; -scan({}, options).catch(() => { +program + .option('-D, --define ', 'Define property') + .version(version, '-v, --version', 'Display version information') + .option('-X, --debug', 'Produce execution debug output'); + +export function parseArgs() { + return program.parse().opts(); +} + +scan({}, parseArgs()).catch(() => { process.exitCode = 1; }); diff --git a/src/scan.ts b/src/scan.ts index 3827a287..9d7e5de9 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -26,9 +26,9 @@ import { getProperties } from './properties'; import { initializeAxios } from './request'; import { downloadScannerCli, runScannerCli, tryLocalSonarScannerExecutable } from './scanner-cli'; import { fetchScannerEngine, runScannerEngine } from './scanner-engine'; -import { ScanOptions, ScannerProperty } from './types'; +import { ScanOptions, ScannerProperty, CliArgs } from './types'; -export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { +export async function scan(scanOptions: ScanOptions, cliArgs?: CliArgs) { try { await runScan(scanOptions, cliArgs); } catch (error: any) { @@ -36,7 +36,7 @@ export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { } } -async function runScan(scanOptions: ScanOptions, cliArgs?: string[]) { +async function runScan(scanOptions: ScanOptions, cliArgs?: CliArgs) { const startTimestampMs = Date.now(); const properties = getProperties(scanOptions, startTimestampMs, cliArgs); diff --git a/src/scanner-cli.ts b/src/scanner-cli.ts index a9a082fc..402282fb 100644 --- a/src/scanner-cli.ts +++ b/src/scanner-cli.ts @@ -26,15 +26,16 @@ import { LogLevel, log } from './logging'; import { proxyUrlToJavaOptions } from './proxy'; import { download } from './request'; import { ScanOptions, ScannerProperties, ScannerProperty } from './types'; +import { isMac, isWindows, isLinux } from './platform'; export function normalizePlatformName(): 'windows' | 'linux' | 'macosx' { - if (process.platform.startsWith('win')) { + if (isWindows()) { return 'windows'; } - if (process.platform.startsWith('linux')) { + if (isLinux()) { return 'linux'; } - if (process.platform.startsWith('darwin')) { + if (isMac()) { return 'macosx'; } throw Error(`Your platform '${process.platform}' is currently not supported.`); @@ -46,7 +47,7 @@ export function normalizePlatformName(): 'windows' | 'linux' | 'macosx' { export async function tryLocalSonarScannerExecutable(command: string): Promise { return new Promise(resolve => { log(LogLevel.INFO, `Trying to find a local install of the SonarScanner: ${command}`); - const scannerProcess = spawn(command, ['-v']); + const scannerProcess = spawn(command, ['-v'], { shell: isWindows() }); scannerProcess.on('exit', code => { if (code === 0) { @@ -99,7 +100,7 @@ export async function downloadScannerCli(properties: ScannerProperties): Promise await download(scannerCliUrl.href, archivePath); log(LogLevel.INFO, `Extracting SonarScanner CLI archive`); - extractArchive(archivePath, installDir); + await extractArchive(archivePath, installDir); return binPath; } @@ -118,6 +119,7 @@ export async function runScannerCli( ...process.env, SONARQUBE_SCANNER_PARAMS: JSON.stringify(properties), }, + shell: isWindows(), }, ); diff --git a/src/types.ts b/src/types.ts index c29ae0f4..531cf694 100644 --- a/src/types.ts +++ b/src/types.ts @@ -79,3 +79,8 @@ export type ScanOptions = { logLevel?: string; verbose?: boolean; }; + +export type CliArgs = { + debug?: boolean; + define?: string[]; +}; diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index 03dc351a..6da194ac 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -434,7 +434,7 @@ describe('getProperties', () => { serverUrl: 'http://localhost/sonarqube', }, projectHandler.getStartTime(), - ['-Dsonar.token=my-token', '-javaagent:/ignored-value.jar'], + { define: ['sonar.token=my-token', '-javaagent:/ignored-value.jar'] }, ); expect(properties).toEqual({ @@ -473,7 +473,7 @@ describe('getProperties', () => { }, }, projectHandler.getStartTime(), - ['-Dsonar.token=only-this-will-be-used'], + { define: ['sonar.token=only-this-will-be-used'] }, ); expect(properties).toEqual({ @@ -519,7 +519,7 @@ describe('getProperties', () => { }, }, projectHandler.getStartTime(), - ['-Dsonar.scanner.app=ignored'], + { define: ['sonar.scanner.app=ignored'] }, ); expect(properties).toEqual({ diff --git a/test/unit/scan.test.ts b/test/unit/scan.test.ts index b4dae5c0..4b28badc 100644 --- a/test/unit/scan.test.ts +++ b/test/unit/scan.test.ts @@ -46,23 +46,23 @@ beforeEach(() => { describe('scan', () => { it('should default the log level to INFO', async () => { - await scan({}, []); + await scan({}); expect(logging.getLogLevel()).toBe('INFO'); }); it('should set the log level to the value provided by the user', async () => { - await scan({ options: { 'sonar.verbose': 'true' } }, []); + await scan({ options: { 'sonar.verbose': 'true' } }); expect(logging.getLogLevel()).toBe('DEBUG'); }); it('should set the log level to the value provided by the user', async () => { - await scan({ options: { 'sonar.log.level': 'DEBUG' } }, []); + await scan({ options: { 'sonar.log.level': 'DEBUG' } }); expect(logging.getLogLevel()).toBe('DEBUG'); }); it('should output the current version of the scanner', async () => { jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); - await scan({}, []); + await scan({}); expect(logging.log).toHaveBeenCalledWith('INFO', 'Version: MOCK.VERSION'); }); @@ -70,7 +70,7 @@ describe('scan', () => { jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); jest.spyOn(platform, 'getSupportedOS').mockReturnValue('alpine'); jest.spyOn(platform, 'getArch').mockReturnValue('arm64'); - await scan({}, []); + await scan({}); expect(logging.log).toHaveBeenCalledWith('INFO', 'Platform:', 'alpine', 'arm64'); }); From 6ebaa086c5e48356f7ba2e225cb41e11d89d4866 Mon Sep 17 00:00:00 2001 From: 7PH Date: Tue, 30 Apr 2024 12:10:35 +0200 Subject: [PATCH 20/35] SCANNPM-2 Add support to parse proxy configuration from HTTP[S]_PROXY environment variables --- package-lock.json | 24 +- package.json | 2 +- src/properties.ts | 83 ++++-- src/scanner-cli.ts | 4 +- test/unit/config.test.js | 463 ----------------------------- test/unit/mocks/FakeProjectMock.ts | 1 - test/unit/properties.test.ts | 75 ++++- 7 files changed, 148 insertions(+), 504 deletions(-) delete mode 100644 test/unit/config.test.js diff --git a/package-lock.json b/package-lock.json index 9ee2e40c..51b58fe3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,14 +15,14 @@ "fs-extra": "11.2.0", "hpagent": "1.2.0", "jest-sonar-reporter": "2.0.0", - "proxy-from-env": "1.1.0", + "proxy-from-env": "^1.1.0", "semver": "7.6.0", "slugify": "1.6.6", "tar-stream": "3.1.7", "ts-jest": "^29.1.2" }, "bin": { - "sonar-scanner": "src/bin/sonar-scanner" + "sonar-scanner": "bin/sonar-scanner" }, "devDependencies": { "@types/adm-zip": "0.5.5", @@ -1705,7 +1705,7 @@ }, "node_modules/axios-mock-adapter": { "version": "1.22.0", - "resolved": "https://repox.jfrog.io/repox/api/npm/npm/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz", "integrity": "sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==", "dev": true, "dependencies": { @@ -2074,7 +2074,7 @@ }, "node_modules/commander": { "version": "12.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/commander/-/commander-12.0.0.tgz", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", "engines": { "node": ">=18" @@ -2979,9 +2979,23 @@ }, "node_modules/is-buffer": { "version": "2.0.5", - "resolved": "https://repox.jfrog.io/repox/api/npm/npm/is-buffer/-/is-buffer-2.0.5.tgz", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "engines": { "node": ">=4" } diff --git a/package.json b/package.json index a1b5999e..9b4961d9 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "fs-extra": "11.2.0", "hpagent": "1.2.0", "jest-sonar-reporter": "2.0.0", - "proxy-from-env": "1.1.0", + "proxy-from-env": "^1.1.0", "semver": "7.6.0", "slugify": "1.6.6", "tar-stream": "3.1.7", diff --git a/src/properties.ts b/src/properties.ts index e174c08a..f00b35eb 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -19,6 +19,7 @@ */ import fs from 'fs'; import path from 'path'; +import { getProxyForUrl } from 'proxy-from-env'; import slugify from 'slugify'; import { version } from '../package.json'; import { @@ -26,7 +27,6 @@ import { ENV_TO_PROPERTY_NAME, ENV_VAR_PREFIX, SCANNER_BOOTSTRAPPER_NAME, - SCANNER_CLI_VERSION, SONARCLOUD_URL, SONARCLOUD_URL_REGEX, SONAR_DIR_DEFAULT, @@ -42,7 +42,6 @@ function getDefaultProperties(): ScannerProperties { process.env.HOME ?? process.env.USERPROFILE ?? '', SONAR_DIR_DEFAULT, ), - [ScannerProperty.SonarScannerCliVersion]: SCANNER_CLI_VERSION, [ScannerProperty.SonarScannerOs]: getSupportedOS(), [ScannerProperty.SonarScannerArch]: getArch(), }; @@ -202,29 +201,29 @@ function getSonarFileProperties(projectBaseDir: string): ScannerProperties { * Get scanner properties from scan option object (JS API). */ function getScanOptionsProperties(scanOptions: ScanOptions): ScannerProperties { - const options = { + const properties = { ...scanOptions.options, }; if (typeof scanOptions.serverUrl !== 'undefined') { - options[ScannerProperty.SonarHostUrl] = scanOptions.serverUrl; + properties[ScannerProperty.SonarHostUrl] = scanOptions.serverUrl; } if (typeof scanOptions.token !== 'undefined') { - options[ScannerProperty.SonarToken] = scanOptions.token; + properties[ScannerProperty.SonarToken] = scanOptions.token; } if (typeof scanOptions.verbose !== 'undefined') { - options[ScannerProperty.SonarVerbose] = scanOptions.verbose ? 'true' : 'false'; + properties[ScannerProperty.SonarVerbose] = scanOptions.verbose ? 'true' : 'false'; } - return options; + return properties; } /** * Automatically parse properties from environment variables. */ -export function getEnvironmentProperties() { +function getEnvironmentProperties() { const { env } = process; const jsonEnvVariables = ['SONAR_SCANNER_JSON_PARAMS', 'SONARQUBE_SCANNER_PARAMS']; @@ -293,13 +292,13 @@ function getBootstrapperProperties(startTimestampMs: number): ScannerProperties * Get endpoint properties from scanner properties. */ export function getHostProperties(properties: ScannerProperties): ScannerProperties { - let sonarHostUrl = properties[ScannerProperty.SonarHostUrl] ?? ''; + const sonarHostUrl = properties[ScannerProperty.SonarHostUrl]; + const sonarCloudUrl = properties[ScannerProperty.SonarScannerSonarCloudURL]; if (!sonarHostUrl || SONARCLOUD_URL_REGEX.exec(sonarHostUrl)) { return { [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', - [ScannerProperty.SonarHostUrl]: - properties[ScannerProperty.SonarScannerSonarCloudURL] ?? SONARCLOUD_URL, + [ScannerProperty.SonarHostUrl]: sonarCloudUrl ? sonarCloudUrl : SONARCLOUD_URL, }; } return { @@ -308,23 +307,48 @@ export function getHostProperties(properties: ScannerProperties): ScannerPropert }; } +function getHttpProxyEnvProperties(serverUrl: string): ScannerProperties { + const proxyUrl = getProxyForUrl(serverUrl); + // If no proxy is set, return the properties as is + if (!proxyUrl) { + return {}; + } + + // Parse the proxy URL + const url = new URL(proxyUrl); + const properties: ScannerProperties = {}; + properties[ScannerProperty.SonarScannerProxyHost] = url.hostname; + if (url.port) { + properties[ScannerProperty.SonarScannerProxyPort] = url.port; + } + if (url.username) { + properties[ScannerProperty.SonarScannerProxyUser] = url.username; + } + if (url.password) { + properties[ScannerProperty.SonarScannerProxyPassword] = url.password; + } + return properties; +} + export function getProperties( scanOptions: ScanOptions, startTimestampMs: number, cliArgs?: CliArgs, ): ScannerProperties { - const bootstrapperProperties = getBootstrapperProperties(startTimestampMs); const cliProperties = getCommandLineProperties(cliArgs); - const scanOptionsProperties = getScanOptionsProperties(scanOptions); const envProperties = getEnvironmentProperties(); + const scanOptionsProperties = getScanOptionsProperties(scanOptions); + + const userProperties: ScannerProperties = { + ...scanOptionsProperties, + ...envProperties, + ...cliProperties, + }; // Compute default base dir respecting order of precedence we use for the final merge - const projectBaseDir = - cliProperties[ScannerProperty.SonarProjectBaseDir] ?? - scanOptionsProperties[ScannerProperty.SonarProjectBaseDir] ?? - envProperties[ScannerProperty.SonarProjectBaseDir] ?? - process.cwd(); + const projectBaseDir = userProperties[ScannerProperty.SonarProjectBaseDir] ?? process.cwd(); + // Infer specific properties from project files let inferredProperties: ScannerProperties; try { inferredProperties = getSonarFileProperties(projectBaseDir); @@ -335,10 +359,7 @@ export function getProperties( }; const baseSonarExclusions = - cliProperties[ScannerProperty.SonarExclusions] ?? - scanOptionsProperties[ScannerProperty.SonarExclusions] ?? - envProperties[ScannerProperty.SonarExclusions] ?? - DEFAULT_SONAR_EXCLUSIONS; + userProperties[ScannerProperty.SonarExclusions] ?? DEFAULT_SONAR_EXCLUSIONS; inferredProperties = { ...inferredProperties, @@ -346,19 +367,29 @@ export function getProperties( }; } + // Generate proxy properties from HTTP[S]_PROXY env variables, if not already set + const httpProxyProperties = getHttpProxyEnvProperties( + userProperties[ScannerProperty.SonarHostUrl], + ); + // Merge properties respecting order of precedence const properties = { ...getDefaultProperties(), // fallback to default if nothing was provided for these properties - ...envProperties, // Lowest precedence ...inferredProperties, ...scanOptionsProperties, + ...httpProxyProperties, + ...envProperties, ...cliProperties, // Highest precedence - ...bootstrapperProperties, // Can't be overridden - ...{ 'sonar.projectBaseDir': projectBaseDir }, // Manually computed, can't be overridden }; + // Hotfix host properties with custom SonarCloud URL + const hostProperties = getHostProperties(properties); + return { ...properties, - ...getHostProperties(properties), + // Can't be overridden: + ...hostProperties, + ...getBootstrapperProperties(startTimestampMs), + 'sonar.projectBaseDir': projectBaseDir, }; } diff --git a/src/scanner-cli.ts b/src/scanner-cli.ts index 402282fb..ebe53aa0 100644 --- a/src/scanner-cli.ts +++ b/src/scanner-cli.ts @@ -20,7 +20,7 @@ import { spawn } from 'child_process'; import * as fsExtra from 'fs-extra'; import path from 'path'; -import { SCANNER_CLI_INSTALL_PATH, SCANNER_CLI_MIRROR } from './constants'; +import { SCANNER_CLI_INSTALL_PATH, SCANNER_CLI_MIRROR, SCANNER_CLI_VERSION } from './constants'; import { extractArchive } from './file'; import { LogLevel, log } from './logging'; import { proxyUrlToJavaOptions } from './proxy'; @@ -73,7 +73,7 @@ function getScannerCliUrl(properties: ScannerProperties, version: string): URL { } export async function downloadScannerCli(properties: ScannerProperties): Promise { - const version = properties[ScannerProperty.SonarScannerCliVersion]; + const version = properties[ScannerProperty.SonarScannerCliVersion] ?? SCANNER_CLI_VERSION; if (!/^[\d.]+$/.test(version)) { throw new Error(`Version "${version}" does not have a correct format."`); } diff --git a/test/unit/config.test.js b/test/unit/config.test.js deleted file mode 100644 index f98774be..00000000 --- a/test/unit/config.test.js +++ /dev/null @@ -1,463 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -const { assert } = require('chai'); -const path = require('path'); -const os = require('os'); -const { - getScannerParams, - extendWithExecParams, - DEFAULT_EXCLUSIONS, - getExecutableParams, - DEFAULT_SCANNER_VERSION, - SONAR_SCANNER_MIRROR, -} = require('../../src/config'); -const { buildInstallFolderPath, buildExecutablePath } = require('../../src/utils/paths'); -const { findTargetOS, isWindows } = require('../../src/utils/platform'); - -function pathForProject(projectFolder) { - return path.join(__dirname, 'fixtures', projectFolder); -} - -describe('config', function () { - let envBackup = {}; - beforeEach(function () { - envBackup = Object.assign({}, process.env); - }); - afterEach(function () { - process.env = Object.assign({}, envBackup); - }); - - describe('getScannerParams()', function () { - it('should provide default values', function () { - const expectedResult = { - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.exclusions': DEFAULT_EXCLUSIONS, - }; - - assert.deepEqual( - JSON.parse( - getScannerParams(pathForProject('fake_project_with_no_package_file')) - .SONARQUBE_SCANNER_PARAMS, - ), - expectedResult, - ); - }); - - it('should not set default values if sonar-project.properties file exists', function () { - const expectedResult = {}; - - assert.deepEqual( - getScannerParams(pathForProject('fake_project_with_sonar_properties_file')), - expectedResult, - ); - }); - - it('should propagate custom server and token into "SONARQUBE_SCANNER_PARAMS"', function () { - const expectedResult = { - 'sonar.host.url': 'https://sonarcloud.io', - 'sonar.token': 'my_token', - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.exclusions': DEFAULT_EXCLUSIONS, - }; - - const sqParams = getScannerParams(pathForProject('fake_project_with_no_package_file'), { - serverUrl: 'https://sonarcloud.io', - token: 'my_token', - }).SONARQUBE_SCANNER_PARAMS; - - assert.deepEqual(JSON.parse(sqParams), expectedResult); - }); - - it('should allow to override default settings and add new ones', function () { - const expectedResult = { - 'sonar.projectName': 'Foo', - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.tests': 'specs', - 'sonar.exclusions': DEFAULT_EXCLUSIONS, - }; - - const sqParams = getScannerParams(pathForProject('fake_project_with_no_package_file'), { - options: { 'sonar.projectName': 'Foo', 'sonar.tests': 'specs' }, - }).SONARQUBE_SCANNER_PARAMS; - - assert.deepEqual(JSON.parse(sqParams), expectedResult); - }); - - it('should get mandatory information from basic package.json file', function () { - const expectedResult = { - 'sonar.javascript.lcov.reportPaths': 'coverage/lcov.info', - 'sonar.projectKey': 'fake-basic-project', - 'sonar.projectName': 'fake-basic-project', - 'sonar.projectDescription': 'No description.', - 'sonar.projectVersion': '1.0.0', - 'sonar.sources': '.', - 'sonar.exclusions': - 'node_modules/**,bower_components/**,jspm_packages/**,typings/**,lib-cov/**,coverage/**', - }; - - const sqParams = getScannerParams( - pathForProject('fake_project_with_basic_package_file'), - ).SONARQUBE_SCANNER_PARAMS; - - assert.deepEqual(JSON.parse(sqParams), expectedResult); - }); - - it('should get mandatory information from scoped packages package.json file', function () { - const expectedResult = { - 'sonar.projectKey': 'myfake-basic-project', - 'sonar.projectName': '@my/fake-basic-project', - 'sonar.projectDescription': 'No description.', - 'sonar.projectVersion': '1.0.0', - 'sonar.sources': '.', - 'sonar.exclusions': - 'node_modules/**,bower_components/**,jspm_packages/**,typings/**,lib-cov/**', - }; - - const sqParams = getScannerParams( - pathForProject('fake_project_with_scoped_package_name'), - ).SONARQUBE_SCANNER_PARAMS; - - assert.deepEqual(JSON.parse(sqParams), expectedResult); - }); - - it('should get all information from package.json file', function () { - const expectedResult = { - 'sonar.projectKey': 'fake-project', - 'sonar.projectName': 'fake-project', - 'sonar.projectDescription': 'A fake project', - 'sonar.projectVersion': '1.0.0', - 'sonar.links.homepage': 'https://github.com/fake/project', - 'sonar.links.issue': 'https://github.com/fake/project/issues', - 'sonar.links.scm': 'git+https://github.com/fake/project.git', - 'sonar.sources': '.', - 'sonar.testExecutionReportPaths': 'xunit.xml', - 'sonar.exclusions': DEFAULT_EXCLUSIONS, - }; - - const sqParams = getScannerParams( - pathForProject('fake_project_with_complete_package_file'), - ).SONARQUBE_SCANNER_PARAMS; - - assert.deepEqual(JSON.parse(sqParams), expectedResult); - }); - - it('should take into account SONARQUBE_SCANNER_PARAMS env variable', function () { - const expectedResult = { - 'sonar.host.url': 'https://sonarcloud.io', - 'sonar.token': 'my_token', - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.exclusions': DEFAULT_EXCLUSIONS, - }; - - process.env = { - SONARQUBE_SCANNER_PARAMS: JSON.stringify({ - 'sonar.host.url': 'https://sonarcloud.io', - 'sonar.token': 'my_token', - }), - }; - - const sqParams = getScannerParams( - pathForProject('fake_project_with_no_package_file'), - ).SONARQUBE_SCANNER_PARAMS; - - assert.deepEqual(JSON.parse(sqParams), expectedResult); - }); - - it('should make priority to user options over SONARQUBE_SCANNER_PARAMS env variable', function () { - const expectedResult = { - 'sonar.host.url': 'https://sonarcloud.io', - 'sonar.login': 'my_token', - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.exclusions': DEFAULT_EXCLUSIONS, - }; - - process.env = { - SONARQUBE_SCANNER_PARAMS: JSON.stringify({ - 'sonar.host.url': 'https://another.server.com', - 'sonar.login': 'another_token', - }), - }; - - const sqParams = getScannerParams(pathForProject('fake_project_with_no_package_file'), { - serverUrl: 'https://sonarcloud.io', - login: 'my_token', - }).SONARQUBE_SCANNER_PARAMS; - - assert.deepEqual(JSON.parse(sqParams), expectedResult); - }); - - it('should get nyc lcov file path from package.json file', function () { - const expectedResult = { - 'sonar.javascript.lcov.reportPaths': 'nyc-coverage/lcov.info', - 'sonar.projectKey': 'fake-basic-project', - 'sonar.projectName': 'fake-basic-project', - 'sonar.projectDescription': 'No description.', - 'sonar.projectVersion': '1.0.0', - 'sonar.sources': '.', - 'sonar.exclusions': - 'node_modules/**,bower_components/**,jspm_packages/**,typings/**,lib-cov/**,nyc-coverage/**', - }; - - const sqParams = getScannerParams( - pathForProject('fake_project_with_nyc_report_file'), - ).SONARQUBE_SCANNER_PARAMS; - assert.deepEqual(JSON.parse(sqParams), expectedResult); - }); - - it('should get jest lcov file path from package.json file', function () { - const expectedResult = { - 'sonar.javascript.lcov.reportPaths': 'jest-coverage/lcov.info', - 'sonar.projectKey': 'fake-basic-project', - 'sonar.projectName': 'fake-basic-project', - 'sonar.projectDescription': 'No description.', - 'sonar.projectVersion': '1.0.0', - 'sonar.sources': '.', - 'sonar.exclusions': - 'node_modules/**,bower_components/**,jspm_packages/**,typings/**,lib-cov/**,jest-coverage/**', - }; - - const sqParams = getScannerParams( - pathForProject('fake_project_with_jest_report_file'), - ).SONARQUBE_SCANNER_PARAMS; - assert.deepEqual(JSON.parse(sqParams), expectedResult); - }); - - it('should read SONARQUBE_SCANNER_PARAMS provided by environment if it exists', function () { - const expectedResult = { - SONARQUBE_SCANNER_PARAMS: JSON.stringify({ - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.exclusions': DEFAULT_EXCLUSIONS, - 'sonar.host.url': 'https://sonarcloud.io', - 'sonar.branch.name': 'dev', - }), - }; - - process.env = { - SONARQUBE_SCANNER_PARAMS: JSON.stringify({ - 'sonar.host.url': 'https://sonarcloud.io', - 'sonar.branch.name': 'dev', - }), - }; - - assert.ownInclude( - getScannerParams(pathForProject('fake_project_with_no_package_file')), - expectedResult, - ); - }); - }); - - describe('extendWithExecParams()', function () { - it('should put the provided config in the "env" property of the exec params', function () { - process.env = { - whatsup: 'dog', - }; - - assert.deepEqual(extendWithExecParams({ hello: 2 }), { - maxBuffer: 1024 * 1024, - stdio: 'inherit', - shell: isWindows(), - env: { - hello: 2, - whatsup: 'dog', - }, - }); - }); - - it('should set default empty object if no params are provided', function () { - process.env = {}; - - assert.deepEqual(extendWithExecParams(), { - env: {}, - maxBuffer: 1024 * 1024, - shell: isWindows(), - stdio: 'inherit', - }); - }); - }); - - describe('getExecutableParams()', function () { - it('should set default values if no params are provided', function () { - process.env = {}; - const targetOS = findTargetOS(); - const fileName = 'sonar-scanner-cli-' + DEFAULT_SCANNER_VERSION + '-' + targetOS + '.zip'; - const installFolder = buildInstallFolderPath(os.homedir()); - assert.deepEqual(getExecutableParams(), { - installFolder, - fileName, - platformExecutable: buildExecutablePath(installFolder, DEFAULT_SCANNER_VERSION), - targetOS, - downloadUrl: new URL(fileName, SONAR_SCANNER_MIRROR).href, - httpOptions: {}, - }); - }); - - it('should set http proxy configuration if proxy configuration is provided', function () { - process.env = { - http_proxy: 'http://user:password@proxy:3128', - }; - const config = getExecutableParams(); - assert.exists(config.httpOptions.httpRequestOptions.agent); - assert.exists(config.httpOptions.httpsRequestOptions.agent); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.username, 'user'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.password, 'password'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.hostname, 'proxy'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.port, 3128); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.protocol, 'http:'); - assert.deepEqual( - config.httpOptions.httpRequestOptions.agent, - config.httpOptions.httpsRequestOptions.agent, - ); - }); - - it('should set https proxy configuration if proxy configuration is provided', function () { - process.env = { - https_proxy: 'https://user:password@proxy:3128', - }; - const config = getExecutableParams(); - assert.exists(config.httpOptions.httpRequestOptions.agent); - assert.exists(config.httpOptions.httpsRequestOptions.agent); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.username, 'user'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.password, 'password'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.hostname, 'proxy'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.port, 3128); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.protocol, 'https:'); - assert.deepEqual( - config.httpOptions.httpRequestOptions.agent, - config.httpOptions.httpsRequestOptions.agent, - ); - }); - - it('should prefer https over http proxy configuration if proxy configuration is provided on a HTTPS url', function () { - process.env = { - http_proxy: 'http://user:password@httpproxy:3128', - https_proxy: 'https://user:password@httpsproxy:3128', - }; - const config = getExecutableParams(); - assert.exists(config.httpOptions.httpRequestOptions.agent); - assert.exists(config.httpOptions.httpsRequestOptions.agent); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.username, 'user'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.password, 'password'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.hostname, 'httpsproxy'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.port, 3128); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.protocol, 'https:'); - assert.deepEqual( - config.httpOptions.httpRequestOptions.agent, - config.httpOptions.httpsRequestOptions.agent, - ); - }); - - it('should prefer http over https proxy configuration if proxy configuration is provided on a HTTP url', function () { - process.env = { - http_proxy: 'http://user:password@httpproxy:3128', - https_proxy: 'https://user:password@httpsproxy:3128', - }; - const config = getExecutableParams({ - baseUrl: 'http://example.com/sonarqube-repository/', - }); - assert.exists(config.httpOptions.httpRequestOptions.agent); - assert.exists(config.httpOptions.httpsRequestOptions.agent); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.username, 'user'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.password, 'password'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.hostname, 'httpproxy'); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.port, 3128); - assert.equal(config.httpOptions.httpRequestOptions.agent.proxy.protocol, 'http:'); - assert.deepEqual( - config.httpOptions.httpRequestOptions.agent, - config.httpOptions.httpsRequestOptions.agent, - ); - }); - - it('should not set the http proxy if url is invalid', function () { - process.env = { - http_proxy: 'http://user:password@httpp:roxy:3128', - }; - const config = getExecutableParams(); - assert.notExists(config.httpOptions.httpRequestOptions); - }); - - it('should not set baseURL if url is invalid', function () { - const config = getExecutableParams({ - baseUrl: 'http://example.com:80:80/sonarqube-repository/', - }); - assert.equal( - config.downloadUrl, - new URL( - 'sonar-scanner-cli-' + DEFAULT_SCANNER_VERSION + '-' + config.targetOS + '.zip', - SONAR_SCANNER_MIRROR, - ), - ); - }); - - it('should take the version from env or params', function () { - process.env.npm_config_sonar_scanner_version = '4.8.1.3023'; - assert.equal( - getExecutableParams().downloadUrl, - new URL( - 'sonar-scanner-cli-' + '4.8.1.3023' + '-' + findTargetOS() + '.zip', - SONAR_SCANNER_MIRROR, - ), - ); - - process.env.SONAR_SCANNER_VERSION = '5.0.0.2966'; - assert.equal( - getExecutableParams().downloadUrl, - new URL( - 'sonar-scanner-cli-' + '5.0.0.2966' + '-' + findTargetOS() + '.zip', - SONAR_SCANNER_MIRROR, - ), - ); - - assert.equal( - getExecutableParams({ version: '4.7.0.2747' }).downloadUrl, - new URL( - 'sonar-scanner-cli-' + '4.7.0.2747' + '-' + findTargetOS() + '.zip', - SONAR_SCANNER_MIRROR, - ), - ); - }); - - it('should not set the scanner version if invalid', function () { - process.env.npm_config_sonar_scanner_version = '4 && rm -rf'; - assert.equal( - getExecutableParams().downloadUrl, - new URL( - 'sonar-scanner-cli-' + DEFAULT_SCANNER_VERSION + '-' + findTargetOS() + '.zip', - SONAR_SCANNER_MIRROR, - ), - ); - }); - - it('should consume and preserve username and password for sonar-scanner mirror server', function () { - process.env = {}; - const config = getExecutableParams({ - baseUrl: 'https://user:password@example.com/sonarqube-repository/', - }); - assert.exists(config.httpOptions.headers['Authorization']); - assert.equal(config.httpOptions.headers['Authorization'], 'Basic dXNlcjpwYXNzd29yZA=='); - }); - }); -}); diff --git a/test/unit/mocks/FakeProjectMock.ts b/test/unit/mocks/FakeProjectMock.ts index 57a99f48..3a77ede6 100644 --- a/test/unit/mocks/FakeProjectMock.ts +++ b/test/unit/mocks/FakeProjectMock.ts @@ -60,7 +60,6 @@ export class FakeProjectMock { 'sonar.scanner.appVersion': '1.2.3', 'sonar.scanner.wasEngineCacheHit': 'false', 'sonar.scanner.wasJreCacheHit': 'false', - 'sonar.scanner.version': SCANNER_CLI_VERSION, 'sonar.userHome': expect.stringMatching(/\.sonar$/), 'sonar.scanner.os': 'windows', 'sonar.scanner.arch': 'aarch64', diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index 6da194ac..b369c3ba 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -352,6 +352,33 @@ describe('getProperties', () => { }); }); + it.each([ + ['http', 'HTTP_PROXY'], + ['https', 'HTTPS_PROXY'], + ])('should detect %s_proxy env variable', (protocol: string, envName: string) => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + [envName]: `${protocol}://user:pass@my-proxy.io:1234`, + SONAR_HOST_URL: `${protocol}://localhost/sonarqube`, + }); + + const properties = getProperties({}, projectHandler.getStartTime()); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': `${protocol}://localhost/sonarqube`, + 'sonar.scanner.internal.isSonarCloud': 'false', + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + 'sonar.scanner.proxyHost': 'my-proxy.io', + 'sonar.scanner.proxyPort': '1234', + 'sonar.scanner.proxyUser': 'user', + 'sonar.scanner.proxyPassword': 'pass', + }); + }); + it('should use SONAR_SCANNER_JSON_PARAMS', () => { projectHandler.reset('fake_project_with_sonar_properties_file'); projectHandler.setEnvironmentVariables({ @@ -455,9 +482,9 @@ describe('getProperties', () => { projectHandler.reset('fake_project_with_sonar_properties_file'); projectHandler.setEnvironmentVariables({ SONAR_TOKEN: 'ignored', - SONAR_HOST_URL: 'http://ignored', + SONAR_HOST_URL: 'http://localhost/sonarqube', SONAR_USER_HOME: '/tmp/used', - SONAR_ORGANIZATION: 'ignored', + SONAR_ORGANIZATION: 'used', SONAR_SCANNER_JSON_PARAMS: JSON.stringify({ 'sonar.userHome': 'ignored', 'sonar.scanner.someVar': 'used', @@ -466,10 +493,11 @@ describe('getProperties', () => { const properties = getProperties( { - serverUrl: 'http://localhost/sonarqube', + serverUrl: 'http://ignored', options: { + 'sonar.projectKey': 'used', 'sonar.token': 'ignored', - 'sonar.organization': 'used', + 'sonar.organization': 'ignored', }, }, projectHandler.getStartTime(), @@ -480,7 +508,7 @@ describe('getProperties', () => { ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', 'sonar.scanner.internal.isSonarCloud': 'false', - 'sonar.projectKey': 'foo', + 'sonar.projectKey': 'used', 'sonar.projectName': 'Foo', 'sonar.projectVersion': '1.0-SNAPSHOT', 'sonar.sources': 'the-sources', @@ -532,10 +560,45 @@ describe('getProperties', () => { 'sonar.sources': 'the-sources', }); }); + + it.each([ + ['http', 'HTTP_PROXY'], + ['https', 'HTTPS_PROXY'], + ])( + 'should not use HTTP_PROXY if proxy is passed through CLI', + (protocol: string, envName: string) => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + [envName]: `${protocol}://ignore-this-proxy.io`, + }); + + const properties = getProperties( + { + serverUrl: `${protocol}://localhost/sonarqube`, + options: { + [ScannerProperty.SonarScannerProxyHost]: 'ignore-this-proxy.io', + }, + }, + projectHandler.getStartTime(), + ['-Dsonar.scanner.proxyHost=use-this-proxy.io'], + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': `${protocol}://localhost/sonarqube`, + 'sonar.scanner.internal.isSonarCloud': 'false', + 'sonar.projectKey': 'foo', + 'sonar.projectName': 'Foo', + 'sonar.projectVersion': '1.0-SNAPSHOT', + 'sonar.sources': 'the-sources', + [ScannerProperty.SonarScannerProxyHost]: 'use-this-proxy.io', + }); + }, + ); }); }); -describe('getHostProperties', () => { +describe('addHostProperties', () => { it('should detect SonarCloud', () => { const expected = { [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', From 6bbf714dc5d55c17b849e117f0a8e053e6825fae Mon Sep 17 00:00:00 2001 From: 7PH Date: Mon, 29 Apr 2024 11:27:48 +0200 Subject: [PATCH 21/35] SCANNPM-2 Add support for reading PKCS12 truststore and keystores --- ca.pem | 0 package-lock.json | 22 ++- package.json | 5 +- src/request.ts | 61 +++++++- src/scan.ts | 3 +- src/types.ts | 4 + test/unit/fixtures/ssl/README.md | 33 ++++ test/unit/fixtures/ssl/ca-client-auth.crt | 33 ++++ test/unit/fixtures/ssl/ca-client-auth.key | 52 +++++++ test/unit/fixtures/ssl/ca-client-auth.srl | 1 + test/unit/fixtures/ssl/ca.crt | 33 ++++ test/unit/fixtures/ssl/ca.key | 51 ++++++ test/unit/fixtures/ssl/ca.pem | 33 ++++ test/unit/fixtures/ssl/ca.srl | 1 + test/unit/fixtures/ssl/client.csr | 29 ++++ test/unit/fixtures/ssl/client.key | 52 +++++++ test/unit/fixtures/ssl/client.pem | 31 ++++ .../ssl/conf/openssl-client-auth.conf | 33 ++++ test/unit/fixtures/ssl/conf/openssl.conf | 34 ++++ test/unit/fixtures/ssl/conf/v3.ext | 7 + test/unit/fixtures/ssl/keystore.p12 | Bin 0 -> 4176 bytes test/unit/fixtures/ssl/server.csr | 29 ++++ test/unit/fixtures/ssl/server.key | 52 +++++++ test/unit/fixtures/ssl/server.pem | 32 ++++ test/unit/fixtures/ssl/truststore-empty.p12 | Bin 0 -> 103 bytes test/unit/fixtures/ssl/truststore-invalid.p12 | Bin 0 -> 1587 bytes test/unit/fixtures/ssl/truststore.p12 | Bin 0 -> 1846 bytes test/unit/request.test.ts | 145 +++++++++++++++--- 28 files changed, 745 insertions(+), 31 deletions(-) create mode 100644 ca.pem create mode 100644 test/unit/fixtures/ssl/README.md create mode 100644 test/unit/fixtures/ssl/ca-client-auth.crt create mode 100644 test/unit/fixtures/ssl/ca-client-auth.key create mode 100644 test/unit/fixtures/ssl/ca-client-auth.srl create mode 100644 test/unit/fixtures/ssl/ca.crt create mode 100644 test/unit/fixtures/ssl/ca.key create mode 100644 test/unit/fixtures/ssl/ca.pem create mode 100644 test/unit/fixtures/ssl/ca.srl create mode 100644 test/unit/fixtures/ssl/client.csr create mode 100644 test/unit/fixtures/ssl/client.key create mode 100644 test/unit/fixtures/ssl/client.pem create mode 100644 test/unit/fixtures/ssl/conf/openssl-client-auth.conf create mode 100644 test/unit/fixtures/ssl/conf/openssl.conf create mode 100644 test/unit/fixtures/ssl/conf/v3.ext create mode 100644 test/unit/fixtures/ssl/keystore.p12 create mode 100644 test/unit/fixtures/ssl/server.csr create mode 100644 test/unit/fixtures/ssl/server.key create mode 100644 test/unit/fixtures/ssl/server.pem create mode 100755 test/unit/fixtures/ssl/truststore-empty.p12 create mode 100755 test/unit/fixtures/ssl/truststore-invalid.p12 create mode 100644 test/unit/fixtures/ssl/truststore.p12 diff --git a/ca.pem b/ca.pem new file mode 100644 index 00000000..e69de29b diff --git a/package-lock.json b/package-lock.json index 51b58fe3..56633770 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,11 +15,11 @@ "fs-extra": "11.2.0", "hpagent": "1.2.0", "jest-sonar-reporter": "2.0.0", + "node-forge": "^1.3.1", "proxy-from-env": "^1.1.0", "semver": "7.6.0", "slugify": "1.6.6", - "tar-stream": "3.1.7", - "ts-jest": "^29.1.2" + "tar-stream": "3.1.7" }, "bin": { "sonar-scanner": "bin/sonar-scanner" @@ -28,6 +28,7 @@ "@types/adm-zip": "0.5.5", "@types/fs-extra": "11.0.4", "@types/jest": "29.5.12", + "@types/node-forge": "^1.3.11", "@types/proxy-from-env": "1.0.4", "@types/semver": "7.5.8", "@types/sinon": "17.0.3", @@ -1412,6 +1413,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/proxy-from-env": { "version": "1.0.4", "dev": true, @@ -4001,6 +4011,14 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "dev": true, diff --git a/package.json b/package.json index 9b4961d9..d652d745 100644 --- a/package.json +++ b/package.json @@ -29,16 +29,17 @@ "fs-extra": "11.2.0", "hpagent": "1.2.0", "jest-sonar-reporter": "2.0.0", + "node-forge": "^1.3.1", "proxy-from-env": "^1.1.0", "semver": "7.6.0", "slugify": "1.6.6", - "tar-stream": "3.1.7", - "ts-jest": "^29.1.2" + "tar-stream": "3.1.7" }, "devDependencies": { "@types/adm-zip": "0.5.5", "@types/fs-extra": "11.0.4", "@types/jest": "29.5.12", + "@types/node-forge": "^1.3.11", "@types/proxy-from-env": "1.0.4", "@types/semver": "7.5.8", "@types/sinon": "17.0.3", diff --git a/src/request.ts b/src/request.ts index 58158b66..8800b47e 100644 --- a/src/request.ts +++ b/src/request.ts @@ -20,6 +20,8 @@ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import fs from 'fs'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; +import https from 'https'; +import forge from 'node-forge'; import * as stream from 'stream'; import { promisify } from 'util'; import { LogLevel, log } from './logging'; @@ -31,23 +33,72 @@ const finished = promisify(stream.finished); // The axios instance is private to this module let _axiosInstance: AxiosInstance | null = null; -export function getHttpAgents( +async function extractTruststoreCerts(p12Base64: string, password: string = ''): Promise { + // P12/PFX file -> DER -> ASN.1 -> PKCS12 + const der = forge.util.decode64(p12Base64); + const asn1 = forge.asn1.fromDer(der); + const p12 = forge.pkcs12.pkcs12FromAsn1(asn1, password); + + // Extract the CA certificates as PEM for Node + const bags = p12.getBags({ bagType: forge.pki.oids.certBag }); + const ca: string[] = []; + for (const entry of bags[forge.pki.oids.certBag] ?? []) { + if (entry.cert) { + ca.push(forge.pki.certificateToPem(entry.cert)); + } + } + + log(LogLevel.DEBUG, `${ca.length} CA certificates found in truststore`); + return ca; +} + +export async function getHttpAgents( properties: ScannerProperties, -): Pick { +): Promise> { const agents: Pick = {}; const proxyUrl = getProxyUrl(properties); + // Accumulate https agent options + const httpsAgentOptions: https.AgentOptions = {}; + + // Truststore + const truststorePath = properties[ScannerProperty.SonarScannerTruststorePath]; + if (truststorePath) { + log(LogLevel.DEBUG, `Using truststore at ${truststorePath}`); + const p12Base64 = await fs.promises.readFile(truststorePath, { encoding: 'base64' }); + try { + const certs = await extractTruststoreCerts( + p12Base64, + properties[ScannerProperty.SonarScannerTruststorePassword], + ); + httpsAgentOptions.ca = certs; + } catch (e) { + log(LogLevel.WARN, `Failed to load truststore: ${e}`); + } + } + + // Key store + const keystorePath = properties[ScannerProperty.SonarScannerKeystorePath]; + if (keystorePath) { + log(LogLevel.DEBUG, `Using keystore at ${keystorePath}`); + httpsAgentOptions.pfx = await fs.promises.readFile(keystorePath); + httpsAgentOptions.passphrase = properties[ScannerProperty.SonarScannerKeystorePassword] ?? ''; + } + if (proxyUrl) { - agents.httpsAgent = new HttpsProxyAgent({ proxy: proxyUrl.toString() }); + agents.httpsAgent = new HttpsProxyAgent({ proxy: proxyUrl.toString(), ...httpsAgentOptions }); agents.httpAgent = new HttpProxyAgent({ proxy: proxyUrl.toString() }); + } else if (Object.keys(httpsAgentOptions).length > 0) { + // Only create an agent if there are options + agents.httpsAgent = new https.Agent({ ...httpsAgentOptions }); } return agents; } -export function initializeAxios(properties: ScannerProperties) { +export async function initializeAxios(properties: ScannerProperties) { const token = properties[ScannerProperty.SonarToken]; const baseURL = properties[ScannerProperty.SonarHostUrl]; - const agents = getHttpAgents(properties); + const agents = await getHttpAgents(properties); const timeout = Math.floor(parseInt(properties[ScannerProperty.SonarScannerResponseTimeout], 10) || 0) * 1000; diff --git a/src/scan.ts b/src/scan.ts index 9d7e5de9..f45ae673 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - import { version } from '../package.json'; import { SCANNER_CLI_DEFAULT_BIN_NAME } from './constants'; import { fetchJRE, serverSupportsJREProvisioning } from './java'; @@ -58,7 +57,7 @@ async function runScan(scanOptions: ScanOptions, cliArgs?: CliArgs) { properties[ScannerProperty.SonarScannerArch], ); - initializeAxios(properties); + await initializeAxios(properties); log(LogLevel.INFO, `Server URL: ${properties[ScannerProperty.SonarHostUrl]}`); log(LogLevel.INFO, `Version: ${version}`); diff --git a/src/types.ts b/src/types.ts index 531cf694..e6273d03 100644 --- a/src/types.ts +++ b/src/types.ts @@ -59,6 +59,10 @@ export enum ScannerProperty { SonarScannerResponseTimeout = 'sonar.scanner.responseTimeout', SonarScannerSkipJreProvisioning = 'sonar.scanner.skipJreProvisioning', SonarScannerInternalDumpToFile = 'sonar.scanner.internal.dumpToFile', + SonarScannerTruststorePath = 'sonar.scanner.truststorePath', + SonarScannerKeystorePath = 'sonar.scanner.keystorePath', + SonarScannerKeystorePassword = 'sonar.scanner.keystorePassword', + SonarScannerTruststorePassword = 'sonar.scanner.truststorePassword', SonarScannerInternalIsSonarCloud = 'sonar.scanner.internal.isSonarCloud', SonarScannerInternalSqVersion = 'sonar.scanner.internal.sqVersion', SonarScannerCliVersion = 'sonar.scanner.version', diff --git a/test/unit/fixtures/ssl/README.md b/test/unit/fixtures/ssl/README.md new file mode 100644 index 00000000..4c8fb432 --- /dev/null +++ b/test/unit/fixtures/ssl/README.md @@ -0,0 +1,33 @@ +Use this to generate certificates: + +```bash +# Cleanup +rm *.crt *.csr *.p12 *.key *.srl *.pem + +# Create CA (private key) +openssl genrsa -out ca.key 4096 + +# Create the X.509 CA certificate +openssl req -key ca.key -new -x509 -days 3650 -sha256 -extensions ca_extensions -out ca.crt -config ./conf/openssl.conf + +# Create self-signed certificate +openssl req -new -keyout server.key -out server.csr -nodes -newkey rsa:4096 -config ./conf/openssl.conf + +# Sign with CA +openssl x509 -req -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.pem -sha256 -extfile conf/v3.ext +openssl x509 -in server.pem -text -noout # Verify + +# Create truststores +keytool -import -trustcacerts -alias server-ca -keystore truststore.p12 -file ca.crt -noprompt -storepass password + +# Export CA cert as PEM for test environment (use password "password") +openssl pkcs12 -storepass password -in truststore.p12 | sed -n -e '/BEGIN\ CERTIFICATE/,/END\ CERTIFICATE/ p' > ca.pem + +# Create client certificate +openssl req -newkey rsa:4096 -nodes -keyout ca-client-auth.key -new -x509 -days 3650 -sha256 -extensions ca_extensions -out ca-client-auth.crt -subj '/C=CH/ST=Geneva/L=Geneva/O=SonarSource SA/CN=SonarSource/' -config ./conf/openssl-client-auth.conf +openssl req -new -keyout client.key -out client.csr -nodes -newkey rsa:4096 -subj '/C=CH/ST=Geneva/L=Geneva/O=SonarSource SA/CN=Julien Henry/' -config ./conf/openssl-client-auth.conf +openssl x509 -req -days 3650 -in client.csr -CA ca-client-auth.crt -CAkey ca-client-auth.key -CAcreateserial -out client.pem -sha256 + +# Create PKCS12 store containing the client certificate +openssl pkcs12 -export -in client.pem -inkey client.key -name theclient -out keystore.p12 +``` diff --git a/test/unit/fixtures/ssl/ca-client-auth.crt b/test/unit/fixtures/ssl/ca-client-auth.crt new file mode 100644 index 00000000..20d9f540 --- /dev/null +++ b/test/unit/fixtures/ssl/ca-client-auth.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFvzCCA6egAwIBAgIUJuEZC709+KrG23WmjO4uMyxjhUYwDQYJKoZIhvcNAQEL +BQAwXjELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu +ZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTEUMBIGA1UEAwwLU29uYXJTb3Vy +Y2UwHhcNMjQwNTAxMDkyNzU5WhcNMzQwNDI5MDkyNzU5WjBeMQswCQYDVQQGEwJD +SDEPMA0GA1UECAwGR2VuZXZhMQ8wDQYDVQQHDAZHZW5ldmExFzAVBgNVBAoMDlNv +bmFyU291cmNlIFNBMRQwEgYDVQQDDAtTb25hclNvdXJjZTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAM+XuHXdaNZbnL2qL/GMcaBVCBdCunlM6bF0x7oj +3vJvNXK6erwOen6g6ta5qPBmQ0kYiWiLaArvtaKj6UF/IVURToC7jPm1V43ZSG/U +O4EhwaxdJMKdIL27hD7kIEayslfeHw2gqhWqE8u4K5EruxeK6jmj+7Bok9DYkeoI +WXAziHEE9WsyJ7L56UnQbcbbKcviO5f+Sl75guLa57Z7qd0aRRMX0/GfAhFuae+w +PqJSlw/Y6VUpMHHL8VjKpLTzf9ACgFfYSVwLT8sv5HVZVgz0ULVz78+MLrHGGAbC +CDmY+d264QzzEobutUZzX5HElCyvKsQ2JqToetGn/TBV5FhFZsUgKmT4OOyNyEeP +XCt6yML7Qjxc6Mmg81pbLTOyUWwJWATAu+QLV+vPRttAoueBRw7488uZ7INrrdwQ +6KcusabegS9HRtN5Qc7AzNZrsLCiKJRswWcz8h61Hcz/189rq9GGqg56eReBc7Ol ++fKXqNZEEVd3wgVFnDo0ttXq5QqdKZpVAZRIzbcnIKPD8hqDtfB7rDk5zK56QUhV +NlnSLI/dgeOEYHTPgAURbw13JbGITcq24yxdoEdKpXL950xSInRehzOohUSCoVat +OESKGU/CvVw96HA5O7h/DIznL3rk8nxvL9aYGIRONhuztR4r0f0njl0ysEp9NRt3 +Hw7NAgMBAAGjdTBzMAsGA1UdDwQEAwIBtjATBgNVHSUEDDAKBggrBgEFBQcDATAd +BgNVHQ4EFgQUtK5cNvYXhfpaIQl62SYrGrXT/qowHwYDVR0jBBgwFoAUtK5cNvYX +hfpaIQl62SYrGrXT/qowDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEAJ5Vk526Y67c2VDGJRJrl7qd0pIFn/tBmvlo491H5ZTZ7sEWDfDPylUZooLzW +XBDkHpejLehPqNAgH9SJSH9UKYywSM/Ad3NeDH1hBtb2K6SUQoycSyWt+lt4vSc4 +cOKZYKrxYaqDHLA+0X7rkONSVY7KPuby8QixhhY9r90Lk59znq4KSczjZgtKBrQy +jKKNEPTn1uYtmjUP3FM/ly+Mi6S8xPpEJmDvCCsJlFA1j530i1weocLpMVwxUgsP +BUVFU67pSEUwlm7NawG90uCOFdKzJooI+jQ2nl9OjsJO+N5WECz0lx22P31/sk3Q +uzbGZ5r/lI14SDrsL9jmKcUr9mbYvPPYxKw7qCN/F7nsmvlu5dQ+Q4AMKbUYmyUN +l+uTC6qjAecoDqKbkIDkYCBsDEoVx2QPPckJr+XjVTmQJJQXOC9slUwk6JPC21QA +KbKQlDORd3medvKSsuL4gMfn4OXgihGcvRRGJIo81hn52ysgE+L8+XMmlcyAlfAv +tKEK9ekFp8kO0VAz3SkZKDXfPXcAJ8WlTQkwQ5PhsohQhOUl2KFRjRPXJ9Fsu+j0 +JDotth86c44r5Kk5X80ZAxQA46wqLoKIOjxKPw9NL/7Im2bfJQOLzXmASfvr039N +LVoM+k5cBS1v2DXl+kwAH5KmKB7PdmnhMXqmNsc6DFL2QEc= +-----END CERTIFICATE----- diff --git a/test/unit/fixtures/ssl/ca-client-auth.key b/test/unit/fixtures/ssl/ca-client-auth.key new file mode 100644 index 00000000..f538a6ee --- /dev/null +++ b/test/unit/fixtures/ssl/ca-client-auth.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQDPl7h13WjWW5y9 +qi/xjHGgVQgXQrp5TOmxdMe6I97ybzVyunq8Dnp+oOrWuajwZkNJGIloi2gK77Wi +o+lBfyFVEU6Au4z5tVeN2Uhv1DuBIcGsXSTCnSC9u4Q+5CBGsrJX3h8NoKoVqhPL +uCuRK7sXiuo5o/uwaJPQ2JHqCFlwM4hxBPVrMiey+elJ0G3G2ynL4juX/kpe+YLi +2ue2e6ndGkUTF9PxnwIRbmnvsD6iUpcP2OlVKTBxy/FYyqS083/QAoBX2ElcC0/L +L+R1WVYM9FC1c+/PjC6xxhgGwgg5mPnduuEM8xKG7rVGc1+RxJQsryrENiak6HrR +p/0wVeRYRWbFICpk+DjsjchHj1wresjC+0I8XOjJoPNaWy0zslFsCVgEwLvkC1fr +z0bbQKLngUcO+PPLmeyDa63cEOinLrGm3oEvR0bTeUHOwMzWa7CwoiiUbMFnM/Ie +tR3M/9fPa6vRhqoOenkXgXOzpfnyl6jWRBFXd8IFRZw6NLbV6uUKnSmaVQGUSM23 +JyCjw/Iag7Xwe6w5OcyuekFIVTZZ0iyP3YHjhGB0z4AFEW8NdyWxiE3KtuMsXaBH +SqVy/edMUiJ0XoczqIVEgqFWrThEihlPwr1cPehwOTu4fwyM5y965PJ8by/WmBiE +TjYbs7UeK9H9J45dMrBKfTUbdx8OzQIDAQABAoICAQC3CNcjVSzyk6QHtt6+403s +SAzyNWulOCN0y7qubKJOr684kSNWXI20yL1GxjwmeoQpFvFQtFnwCprj5BHuJeGF +19SXvMX4BeREtaggscgle1YAW7/luBT+NS/NI+cxbq3Au6A1q8tLfsIlhSUkwqIb +h+gtGmD4kbyDD/DXoLT7MPTEcdLRyU8nhyIiaxvfka2wjrBsu1FnnCfDTa+wPijv +QhJVW1UMXV69b9UH+SXAiYGX/3D8HW0RaPhLiaDfyzKOfSYcTh+ggHjCdl/A+Bvf +ICtpUefH35nsNPVKQBpwbmkhD30OpeNYBXDfxSompGThYTEb/4LjM/fWk3+x9ol1 +kx/jGccPFqAQr9oMJA9O/l1pz/h+vyajxqRZOKjPaMwu6DuStkqimVLWmS38KxSQ +3A/nUkMp+Y8A2bVwececTN14b6PIxL9iYVQX1HMvLJTEVq0FLpal0A4EWO/GIMsz +6hDcwrN/n2FdDUKiyW0PjTI0l2vsTQRaD30dLtPDJfcmW5TnQAamyzwOc6Zml1Q6 +P1xyg2uwt12z5fQrdUDEIinjP73EkheUF/BGqqM/tXmqxUZT0BNvadrob6HeXhmF +MqDuyt7lb4uHrPpJOEXcXHYAqczYs0/o9venRSx36TVVQV+5GlJUQE8MTlKLaQYt +XSPtNWlopkprf9FqhkesIQKCAQEA+f9nu9ByfEd7ZsPDBbJzYXnsjp9pGGtkUjKw +cPtoEuQfXAJLQtZE1MMcZhsfRH8uRH221zV2ef8I7Yrg+u5ajMJXOz323tnGRyVX +ARi4dgbGM+87wM1dbZqWi7TRr6QKRRDYbtqxfIIUtDvwxTyP/4jhLB4BcXxEN3+H +gDFDYQ9MwvsDxD8gXy8ovfoMhmZxVmN96VOJLwt4KZd/LiyMcEmINLkgN7wPfYXQ +8FEOHl4l3MLCwpxKtNt3pVmExwDVszQsQvmPAvfLKi7//CJaKxKhy7HZhq0rpKei +acI01sTMt2xDeJ3JIA2PsHDc8+1Fk/dmKPAHuPT3Y6xkoJYdPwKCAQEA1JOs8+Ic +4imfA+HWYeq09G2r8MpQT0af1cB8tCLNkKGvQhNK50deMcXBpDKNDfa61F63/mV+ +yLMhNdeDrx+qANdvVgPY/qpV0F41SsQV4TtCvNQ/a7b8qrcixA/rU4clJAhISnS6 +FhjEkiwZuqPTrkS9FmOHrNApGMit1QSo7il5Atr3lbIDRLqegLvdLZ6Uf43XMWtr +F9cHukf1aeY2R1lrNam7U1KIMpdumKfblQ+QNh4lu5ugWd0mXKEtnvWeOhsJ/u0W +h6KQzlrHCFlFV2VAoFQe8eLI7VLGGpcW7nCSE+myu+XzcUtMYhc3LyIpCFeVpFFK +dEujS7vrAq+08wKCAQB9PbZ6ILM6D5WCpg/NitjCvJIF4VaFJUfc5gf+kfRRgncz +YPLTSQSykgxoGq5PYmeLaG9w4Re5hkqytiB/lWlHmxSYWTKT8gWjHtG3euruNfaV +jgQhUsC7Z/aDhtKFa2i5sPa8klLYTVKR+HVmWjDJk4k60M1oTRjftMPtNMDMnx2V +kKsSZY2SIc4HXn1n12pwHOe3PGI9b0GDlKHiP+8bUbsqrpO1WEFqYN+LhQ/NptzQ ++8EWPbYvZMNL0szx5Tkpzble1CcRFZJyT5ludsc1TOBBa5fOIHL8yf5TfTd7YJwu +R86FXoajyCdz/Ra0HOn+drJ3T8iOoCpPhM3kpU+BAoIBACbk6jUpPu2mfeDA9m+t ++PPsCRSif7Uxj9cVQ/vVjlUTMDTfwMm2RibHLxny4doXNbHbrsCOI3dnRwFJ8F8f +ZQSIZmePhql50v+v7QJEBFjUde6EyyHTNkGqBmNnIkCDLql8FnYBC3c1iunPxdlf +VkDBdPNevJlC8PIG7b9W/e2tiuWZ2Mj77BssJgoZ1WseY78+3Yu+Qrb28gQEXIPG +ylGdq78C0jJ5nE/dYy/tLoEEevdb5r1/yQQIMZerKeS2vf+VqOuKx5+DgAkxlM8T +PluyO/PZ0Fujie3aQkLlOB3iXOflz30Pos4s38nmw4MNNgK/u7J36S6EFFmsBWDV +cz8CggEBAMChd/FE+idQQysFv+NIUVgr7p8Pbyhlz2//0mQLOgVIiNF5ZTr7xbcz +aBhKt9tvNsiv6q0aNW1NNdIhk6SD2nKGz/5EE6j+afkUQovEv0t80qqzED2EMRJX +5n79QPok5MPFLjVWVLTMi3lHS6FzFSzV3d5ntEDRuM8/f8b6ReIj1lskt72OrQlg +LAEtokXRtTjYnRR1OfrXRtVurrd+3sfZtR6HfmrCw/jdhvHSB/3OLKQ31qIxJRK0 +aAc94x+Zp50BEkxXYW0kdIe2mle0C4rEi3S1VXMxdYt56dEc0D0UVPOt83p7QOuI +RsQHMIu1cfcomRWz0jsjc7Jar1IiUNQ= +-----END PRIVATE KEY----- diff --git a/test/unit/fixtures/ssl/ca-client-auth.srl b/test/unit/fixtures/ssl/ca-client-auth.srl new file mode 100644 index 00000000..f15a7784 --- /dev/null +++ b/test/unit/fixtures/ssl/ca-client-auth.srl @@ -0,0 +1 @@ +0618B577F5014F4ECF5B9AD03E7483BD0008C353 diff --git a/test/unit/fixtures/ssl/ca.crt b/test/unit/fixtures/ssl/ca.crt new file mode 100644 index 00000000..d1327fa5 --- /dev/null +++ b/test/unit/fixtures/ssl/ca.crt @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIUVCekmTPwf/93kTiGCMu5YJf1J4cwDQYJKoZIhvcNAQEL +BQAwXDELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu +ZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTESMBAGA1UEAwwJbG9jYWxob3N0 +MB4XDTI0MDUwMTA5MDEwMFoXDTM0MDQyOTA5MDEwMFowXDELMAkGA1UEBhMCQ0gx +DzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25h +clNvdXJjZSBTQTESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA5CBdLFWEJ7xeabHdQWUjMRZW3mY1eTl/gL81GS2bsjmK +Itd+7uzqjcU8US7YlMhSWMCVG7wCVnaMtuCqfL0qa7VHtaP6YMh84V1Iw38tptEl +VO4HhZgdsb+wONH2Ut386x+jrmDLH9OUghYwy0gdo+AhzL92mDQBVDBdjBA/xaqv +eq5KiLSxGP29l7c99ykxOQe9EzvkR5wB7AuqxaJWiTteq8/0HQLFn0b0WQ5CMrXN +aBBhioiM567ew2n2Ul/NO62dkkzREEyRsh1DfvgeaayILFpTNA8oMXi7/Qx/wZK1 +QifvP2bqiqnTU93iwFU09NH0Ku+LIxRfDAx8exBfa2e49mCiZblNRGHHyW7GBys+ +nPjA/mbxALCyIl4hRjmJqjqpovam7TjcuLXiA2P0PMNmi/zU2KVQPovHpT7SIkhI +xYm+WGJT/Ak+GTC61v0gigKpPrbL6pOsZls19G6EoTvr612RbjQeT4Wsb5WP5y9x +X7yaUKTMEuWt13Cf3149USbkcFCSRC2v7jxMZjij1Ten0TYCjq4ZaL/zXiQtAfgT +sF72s1p5vqClCS9mPOxInUUHepKdIqRktjM8qCPMa6diCwBPYpHsxR4er2Dwndv3 +KSxjtVeHXn1q3/CO3nqWSi1px9+vaBe10haZU/FrSF7NQfTRiGo2tiigNFuoLUEC +AwEAAaN1MHMwCwYDVR0PBAQDAgG2MBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1Ud +DgQWBBQqt5GwIF516GylOoddDFotwTlgFTAfBgNVHSMEGDAWgBQqt5GwIF516Gyl +OoddDFotwTlgFTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQDL +7M0cAWKfllnmGxY0D2+qocDjU3YC937awHgUXr720GgtmB2EKAcfx8AJyKm3TWUV +ssyO3K3Bkrs6ZrNxlAgYQPmPmgzKMl57menyFnT+pkNKw3c+pDjZOFiMH2XrUR9m +cUpPfnq9j/3hp9F+VTeGVtjmBJHlfUOD7mGrMbwE2O1gUuVHKw0yaPtABvTva5V0 +ZrrhxNakKhhscUjxD6sxxmdrI2speCh4b7ITPQ1MuI8WsA9ij9NTneugxT2DNaxk +W7o1HXuLB9VHQ1VOWsuxYB0hjF0HlVhQ8mZBrqfOSmvPdEfanHEyMkUTyYjuDjXA +OspMBCUZg3tGEWwZm277jOwmbl3F52SQ2f5rowioo7Zh0XDRMLp008TmbcZ/ZGvk +5OhgikmBWrB9iITbXsD3Pi2GBTyikzZlgXOHZRQnuTIJ2hg6cLdu4TWlZ/QQID2i +Bg5ZFEKsUEjcfO0HEWwhErXfV9bInO9sNO8FPaARZ31sNmuWU7+a67qtGMO3AF4G +JplDgu50tY8YY1B6Fpv1erMd1ATEiTW78CihckBdfPHs16iLML3Q8zdiwcaZIqiN +i1b7x/ndZKaWxcIS77upJaFZU7C+va3yYZ+Bko+vEBN7+ro1cs8lRe8YF3/jUHMl +YHoeVt2rx+8yL3AQojG48THMpeWy/E8gdqoF55qBgQ== +-----END CERTIFICATE----- diff --git a/test/unit/fixtures/ssl/ca.key b/test/unit/fixtures/ssl/ca.key new file mode 100644 index 00000000..70dfd9bd --- /dev/null +++ b/test/unit/fixtures/ssl/ca.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJKQIBAAKCAgEA5CBdLFWEJ7xeabHdQWUjMRZW3mY1eTl/gL81GS2bsjmKItd+ +7uzqjcU8US7YlMhSWMCVG7wCVnaMtuCqfL0qa7VHtaP6YMh84V1Iw38tptElVO4H +hZgdsb+wONH2Ut386x+jrmDLH9OUghYwy0gdo+AhzL92mDQBVDBdjBA/xaqveq5K +iLSxGP29l7c99ykxOQe9EzvkR5wB7AuqxaJWiTteq8/0HQLFn0b0WQ5CMrXNaBBh +ioiM567ew2n2Ul/NO62dkkzREEyRsh1DfvgeaayILFpTNA8oMXi7/Qx/wZK1Qifv +P2bqiqnTU93iwFU09NH0Ku+LIxRfDAx8exBfa2e49mCiZblNRGHHyW7GBys+nPjA +/mbxALCyIl4hRjmJqjqpovam7TjcuLXiA2P0PMNmi/zU2KVQPovHpT7SIkhIxYm+ +WGJT/Ak+GTC61v0gigKpPrbL6pOsZls19G6EoTvr612RbjQeT4Wsb5WP5y9xX7ya +UKTMEuWt13Cf3149USbkcFCSRC2v7jxMZjij1Ten0TYCjq4ZaL/zXiQtAfgTsF72 +s1p5vqClCS9mPOxInUUHepKdIqRktjM8qCPMa6diCwBPYpHsxR4er2Dwndv3KSxj +tVeHXn1q3/CO3nqWSi1px9+vaBe10haZU/FrSF7NQfTRiGo2tiigNFuoLUECAwEA +AQKCAgBOYCAivy6sSDdXsNgHQ6wXjUlDF3J/t5VqskaX4+d+D+65kbf2dkcPdhgG +/EVEuJ4yB9gysyFKe2hU3FM2j/cnEh0U9sVqwvbEprv9DpCso2ZkC3NiHqT1EJqG +qvwp9EKUtUYS/wZKZPK8zsrszFYCm1qBcbZZDGT4e7VoDZ0bWEz5pS/OT+YYY/Tj +Tv1nESvsIBCBry36vEqcwlVlmSSJ+W/JL6T64pzq4AHLJu7vZS6w9g/M/KUMZDP8 +h0ctfeSRAFEGloWtR+E2hH9P/AbW34PZWR8E38A7XvOXONgbtT+4/udfrQgfo8EL +K0xgL+YFxqxQpAP1hWYySYfq7/EX23ZixZPmW+D+awWP2+r7o53H7Mak9Mdne7Tu +OrT+qpviAS8nsI4nOQ+M7iHf90gMJHdXO2SJPlKTBe3wTjpfYSYimQGZnBUSW86l +bN9D9/AJUgsZbQKCTzbI7zkC5NUcSRLXX8pq2k6GAhcqX4zw3H7QTUK93Tx5h1jr +DtW+7vF2uZYi5C+i++aCefjj5Qcrgu/yJ5Ur+LSIFXPSKqjnm/2DPmgKwmnjfNby +fYQQvSAdl4rDcurQ/tdBgn1E9chstz/9hYXy5syPceHAPM9quvCA3Z/TGK45C54a +wiiCvqIGw0DrRf93/2sJ08ErGjK8GWfQjjbBKwcd1oU9mYlcAQKCAQEA9GObkh7Q +CXs0HK6PV9MtIbfBvfM/l8CRX0dCzD7NUTtbF+xxwWil9162oA0D0f79v8mWkLO4 +cz4ECnn5FsaM0gfGYGNp2Kg6+Tqjkl6MdGt5MQ8NKkqNmLoqI7nAsNZ2GP7pIQ+i +jWYOoeoM5WibYWAYMKrTFnctR2btO2lDQqotbnnPYxnCBSQT2+DTFaUEZRM8OuMv +zaTTD3Qffjzx+qEUE1BpfB8CoySd1cZS6D1XdJOM5oEddx6HwbzlZDngi16l+CgY ++0Wg36aYbc0ZNd0jN2rG2n7gWcnS43qpM5cI5AOMLcyFluKGP1dduDAz1ucuStDI +RWxbCl9yz/mmMQKCAQEA7vb1+w2FxZEYAEoVdmBS+y9QoHcLCc6nCiXE4MDwVtX3 +H0T7XUoDru2XJUNOU4P3D5R1JCOer14GvpkNBjlc2KcZdUHMOkesUHIMwAHHYd5M +9qmpkOrhtpRGPqYQYgpjAUgNWUZTH9BmLOA23HUmrY3KkfMX3Z65GLk4nbbiozAd +ABTOBCLKVqURFQYLf0hNo0nktelYzofzIDGpimNe7lYDWfAiAFHYJ/N8/ZeQ3SZk +hC5P14QTnslK8E7JTtw8b1KE2YszEWf91QkkkHQoQTFmzXGIeUn2aNuCeWrtBXQS ++YrQmswfaXXSvp6ddMl+W71lgLdPMxzvfy2+mnlkEQKCAQADjcNAX7RUvvbmB9/L +viVk3SAzG+tr0IAMq6OcBrnDmaJcebK5xkTLkRQExcutbRDRjiPjXMms21UBtf5a +R27aywQmeKucW+3nm+OvCDLwqnNrtDVTzRu8AdEFDflwWN4ExQgs0+ZgOgCyeA3R +9DB2PQh9BK7nH7qH1EZU29X/jSv19E2Aumoo2vpy8xT+tpSWx63TiWQzkFcFXYHr +uwUlyNva4At4o5bNOoYVCro/6ExyRIcC/xOnnMkKly2axICwZiLxtduPI9cQCYMj +7ZyVPO77KlFT8g5fH+EyL6FwP50Ae9C5BcVXiVm8aA/T0teeI2R38AtJfybfwr2P +qqixAoIBAQDMhprqERzZh+He/Yl5E+ByFIERllHgRvs4+DKVmw0ZhXBJVyU9J0gX +xEqFHiI/4Mboksvf7oy20+c54oz/MsGAvSRQ88v0ZbvZ7oNzIxKfdUCyLWxwGFiv +iCDHJiMHhpROWHj0W/hxVlDdP9o0viAokx8547IdgOgzfPQ0KH+55Egt9aCOcah0 +mDsSn2XfvuaUR351JA9aVYmFmHvfckWiAARGSf0QAPzc1M25zquyXFXTvD+h2e1h +DjARlZ0+3cjsDtidyUIgN71NRNICwShjBAFU/UMtbFx2SspVWWscK8jBxEne864+ ++RUzCVcCyiJYKrZhgINM4Asr8t8cH6HBAoIBAQDIKyxrEfyrwKDPLSANRmgKuYEY +r0Nl2bAho/eJlhaS4HGFmrsAdSKyGDI8HN00Q9s2CTBWf4SXD5rlncsOEVEb0716 +QriZWgrCiZD1eSlnBabpbuHvbWHUBey4xEytI7hbVRBW+3mO75RuBHJDqH07ul+v +TU2yfpv8up7MzxsZ+IjCaDetf+H1VQKPYz+E9eR4fFv1lDP1HZYqTQC8Hu9sBWdd +z6+jFg0kE4koNTE1E524ZGwF2OszythG/vyeLe7O4JOhQbLLu+Pth7k5eCbxpAM1 +y4eyjqfDocTamS/X9gKZsLbPuW7zp5uZx7o6eQt8dYINn0tjiLEfY4ramnSQ +-----END RSA PRIVATE KEY----- diff --git a/test/unit/fixtures/ssl/ca.pem b/test/unit/fixtures/ssl/ca.pem new file mode 100644 index 00000000..d1327fa5 --- /dev/null +++ b/test/unit/fixtures/ssl/ca.pem @@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIUVCekmTPwf/93kTiGCMu5YJf1J4cwDQYJKoZIhvcNAQEL +BQAwXDELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu +ZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTESMBAGA1UEAwwJbG9jYWxob3N0 +MB4XDTI0MDUwMTA5MDEwMFoXDTM0MDQyOTA5MDEwMFowXDELMAkGA1UEBhMCQ0gx +DzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25h +clNvdXJjZSBTQTESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA5CBdLFWEJ7xeabHdQWUjMRZW3mY1eTl/gL81GS2bsjmK +Itd+7uzqjcU8US7YlMhSWMCVG7wCVnaMtuCqfL0qa7VHtaP6YMh84V1Iw38tptEl +VO4HhZgdsb+wONH2Ut386x+jrmDLH9OUghYwy0gdo+AhzL92mDQBVDBdjBA/xaqv +eq5KiLSxGP29l7c99ykxOQe9EzvkR5wB7AuqxaJWiTteq8/0HQLFn0b0WQ5CMrXN +aBBhioiM567ew2n2Ul/NO62dkkzREEyRsh1DfvgeaayILFpTNA8oMXi7/Qx/wZK1 +QifvP2bqiqnTU93iwFU09NH0Ku+LIxRfDAx8exBfa2e49mCiZblNRGHHyW7GBys+ +nPjA/mbxALCyIl4hRjmJqjqpovam7TjcuLXiA2P0PMNmi/zU2KVQPovHpT7SIkhI +xYm+WGJT/Ak+GTC61v0gigKpPrbL6pOsZls19G6EoTvr612RbjQeT4Wsb5WP5y9x +X7yaUKTMEuWt13Cf3149USbkcFCSRC2v7jxMZjij1Ten0TYCjq4ZaL/zXiQtAfgT +sF72s1p5vqClCS9mPOxInUUHepKdIqRktjM8qCPMa6diCwBPYpHsxR4er2Dwndv3 +KSxjtVeHXn1q3/CO3nqWSi1px9+vaBe10haZU/FrSF7NQfTRiGo2tiigNFuoLUEC +AwEAAaN1MHMwCwYDVR0PBAQDAgG2MBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1Ud +DgQWBBQqt5GwIF516GylOoddDFotwTlgFTAfBgNVHSMEGDAWgBQqt5GwIF516Gyl +OoddDFotwTlgFTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQDL +7M0cAWKfllnmGxY0D2+qocDjU3YC937awHgUXr720GgtmB2EKAcfx8AJyKm3TWUV +ssyO3K3Bkrs6ZrNxlAgYQPmPmgzKMl57menyFnT+pkNKw3c+pDjZOFiMH2XrUR9m +cUpPfnq9j/3hp9F+VTeGVtjmBJHlfUOD7mGrMbwE2O1gUuVHKw0yaPtABvTva5V0 +ZrrhxNakKhhscUjxD6sxxmdrI2speCh4b7ITPQ1MuI8WsA9ij9NTneugxT2DNaxk +W7o1HXuLB9VHQ1VOWsuxYB0hjF0HlVhQ8mZBrqfOSmvPdEfanHEyMkUTyYjuDjXA +OspMBCUZg3tGEWwZm277jOwmbl3F52SQ2f5rowioo7Zh0XDRMLp008TmbcZ/ZGvk +5OhgikmBWrB9iITbXsD3Pi2GBTyikzZlgXOHZRQnuTIJ2hg6cLdu4TWlZ/QQID2i +Bg5ZFEKsUEjcfO0HEWwhErXfV9bInO9sNO8FPaARZ31sNmuWU7+a67qtGMO3AF4G +JplDgu50tY8YY1B6Fpv1erMd1ATEiTW78CihckBdfPHs16iLML3Q8zdiwcaZIqiN +i1b7x/ndZKaWxcIS77upJaFZU7C+va3yYZ+Bko+vEBN7+ro1cs8lRe8YF3/jUHMl +YHoeVt2rx+8yL3AQojG48THMpeWy/E8gdqoF55qBgQ== +-----END CERTIFICATE----- diff --git a/test/unit/fixtures/ssl/ca.srl b/test/unit/fixtures/ssl/ca.srl new file mode 100644 index 00000000..02dde2f7 --- /dev/null +++ b/test/unit/fixtures/ssl/ca.srl @@ -0,0 +1 @@ +5EFA5ACE6F460DAE89B4763C7AB97833DA71A294 diff --git a/test/unit/fixtures/ssl/client.csr b/test/unit/fixtures/ssl/client.csr new file mode 100644 index 00000000..0b845288 --- /dev/null +++ b/test/unit/fixtures/ssl/client.csr @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIE9TCCAt0CAQAwXzELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0G +A1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTEVMBMGA1UEAwwM +SnVsaWVuIEhlbnJ5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEApbdL +Mv3raPqeJC3YGAj8kOiI4qN+xoitERxFRVczmwVPcPcMlfNHYHfVftqoIznGWVeu +gRa5uPKJEFQjUmjyGg/i5tOLx+pf8bRfyoHgSyMzju86Vmskwq/l/psL0d8ni87C +ygH3//lSgAucNkvPcPbHmQKt1w+9MfCJdtaf8EuJjmngl191x5w9dxtt0XJV1hBR +nlo0iQT63iv8uJAkaM1e2ljHoxEKMUmHYJcikWRYJYhlWa0vGqs9tIAaE0iBKSW0 +0MafNmiSZSHoi8vMxcbmdtF3d9aiVOFnXeHwfqGgH0Sv2txm03w1TCPpt7rykeM4 +Szn9omsFW/hILD4iy0+LQtEz1oanlu+CI+u5L6jwZZTeY+Jwb4QIHKB7z1sG8vsb +C98LTQ/OTT/OdY7oDdsaBuS9EzL+end/u5LN1xtiCX35o16SvQmrK6uJHIynRLbY +rM+n9MCH7RtAkB/pcZ4iDI6zJlYui+v3xdq6Z7p74HOhlPcn/+o61Sxh0ht5+aG3 +jMnSCn9iMEncZKC8QDRruuzPkymNMyvkiovqnSOz6imc/dYg3Tz6IzdXb/f14/Sq +1VIEtTObC10DhOQ7CddsbF8U5GgUJp/GDNWwMAiTBYsZ9O0+Zl3uM9CTbrjpILc4 +8qHqdlIhDZRtJi67WYr8UR62dwiqfvXoCD5uBNECAwEAAaBRME8GCSqGSIb3DQEJ +DjFCMEAwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwEwYDVR0lBAwwCgYIKwYBBQUH +AwIwEQYJYIZIAYb4QgEBBAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQBeZ5TahMBg +yrOL4JQ5/lh4UF/jMQuMTc+wx9+uLFYKhzl6J/SMQTZXcOy0waDctl8QVmPzrRWr +FM969k/eiBxeMpVaEfdI/Hk71ICzzMdSr8dp/DvyeptcjbAv1etLBFX9N+3827bJ +mo2xEmNjxM3txWm178XYh4gzLBpPaGEjbCogjRd1W4nqgY312o5fA6kqWCwzHBtU +e0tVmHeEwnY3hgwltaHYXPVlw9O0vJOkyYX6tzdWDRPK8O0XWfDOK0ZxQKEWYdvH +/aR3XzGWFL/up610sBlbTM1ow84mLWvcB+0/GEv/CfYYl/sTyY9DIXLJ6jZBqFmN +IxK5dlYlkefvDHMA68z0WNnw2rhOq1dHK4vkxuW4BSxQBbbblwFvVt4zCl8VzNNc +V3pst7+r2HrauVrsUcIbA0Ix5Jtp/dNEobEFr+ILTTIwLEFXoA3nbUhes5z6JTdR +HKrGCYByxo03jG8nRxoC30eJrNIaSjAQTKwoqEC2QakdOlDpxM5lI4RM8r89RpFW +fr3H66j5M8CyJDK4gq+TCWwFDM5+OsptIJSD+gct5rFriG7MZMv+E7+tuWwsF8OL +n1NjdbFKjoIoaXGe1MEN5KRRUr+JmHfAqW6m40vptRjvoqQA0UpgPOpOjSnEUSL3 +WXtSJskWLJkEODFtLHJoKQTB72LLLdRM9A== +-----END CERTIFICATE REQUEST----- diff --git a/test/unit/fixtures/ssl/client.key b/test/unit/fixtures/ssl/client.key new file mode 100644 index 00000000..65befbf6 --- /dev/null +++ b/test/unit/fixtures/ssl/client.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQClt0sy/eto+p4k +LdgYCPyQ6Ijio37GiK0RHEVFVzObBU9w9wyV80dgd9V+2qgjOcZZV66BFrm48okQ +VCNSaPIaD+Lm04vH6l/xtF/KgeBLIzOO7zpWayTCr+X+mwvR3yeLzsLKAff/+VKA +C5w2S89w9seZAq3XD70x8Il21p/wS4mOaeCXX3XHnD13G23RclXWEFGeWjSJBPre +K/y4kCRozV7aWMejEQoxSYdglyKRZFgliGVZrS8aqz20gBoTSIEpJbTQxp82aJJl +IeiLy8zFxuZ20Xd31qJU4Wdd4fB+oaAfRK/a3GbTfDVMI+m3uvKR4zhLOf2iawVb ++EgsPiLLT4tC0TPWhqeW74Ij67kvqPBllN5j4nBvhAgcoHvPWwby+xsL3wtND85N +P851jugN2xoG5L0TMv56d3+7ks3XG2IJffmjXpK9Casrq4kcjKdEttisz6f0wIft +G0CQH+lxniIMjrMmVi6L6/fF2rpnunvgc6GU9yf/6jrVLGHSG3n5obeMydIKf2Iw +SdxkoLxANGu67M+TKY0zK+SKi+qdI7PqKZz91iDdPPojN1dv9/Xj9KrVUgS1M5sL +XQOE5DsJ12xsXxTkaBQmn8YM1bAwCJMFixn07T5mXe4z0JNuuOkgtzjyoep2UiEN +lG0mLrtZivxRHrZ3CKp+9egIPm4E0QIDAQABAoICACE1OueMBLmzxy7+1Nf0LRCo +2I16L/R+/Rd5r9P2ZowBI2tCxo3iA4KsYOcb0CfG8x2COaD6udr7F0ZjZfSkvSdF +2bVh3RgBuppICA4fup+z/Sf+fpVEwUgWUaOU1GiJLHaSx2wNuvHbt5GbQgGLbZV4 +joT2CXoYWFSCnDlpRwGzUWxtgSk0YvjOMW8F2xrmq5bLDGAMOYzfp5oP/IPLttAd +n41nzxG5X26DCpLrlmzGS/exfoXa856HhEUAirkkhWWGfdZ8hvkzOWr0wZIKFA3q +DtLupN8p7rvNs5YXqcbmgpzhedAE7MIimNeaNsKvvt5HR0ej5lS+14MXnPbouLXs +kDkzXn/5pagYZcomWJcwW9j+XORvCP2jhB62zMYCoueC2rHJVF+krtbAbhKz2o+e +4vpv7gmcpUFrAfyTYoMeepgyVkaxryPAySfRJVeBmz03yOzDnzWj02x/h9uJy28q +ox/DkpDTlzrFuwh37h0DOl3cBfQJ+Wr63rnCidCQ5nWW08J+OFTIMoznBSGRZyPd +FpXNaBRev5ZW+IZYFBUYumPMobj1qKOaeYKgAesfkWiRdkbivwV5Dd3zZ97OFfIC +k5hg4lXDxKjzvBrW3sLj5hz53IJu2TSi83jst5/gpgZlpS25ljn+93naG+BA7ciN +JSmScmxuUn8LxUZ6APZVAoIBAQDb0ZN6q6QdFYrvt26uO4lfR7/B5WLeG2cXaXlk +nEUIUajQnDr0LG2nbee+ft8fd+ousB572nm1EIuyntQoVeEKEucVh6W7V2pgxNpb +PSSPbQlDSIX0vTngGDJDlWrOTt2pscl4xf22LSpVOMhGGLn9GeSqCPCHr2UgOtLR +ERpx6WTHFPAmsN5gVK3aEMAEqoJoQLVPJfkL3rpwoguojLPtWgSeMcTxoTz/8AD7 +Kn8KAdU6WifbCm6wFH3VWl3XgX3q+MiFj4TANyVujFf2ykpQBtrw2JifERic1w8c +fbNQ4zZQvsAXGU8mm8WCoW4TVGQNCeopHTEjSgprV0wGWLfvAoIBAQDA/gMWXgbw +vcZAqpevtGUGzCgrPRi4v/8nwYt+PKlZBoh9TBBJ0TprO4vmnmoNK1b4U6kAS+21 +E85d48cM66k/EIKWUoJYjshq7v/szU+NXN/flwCobk1Eff4JCX1u8O6iYyevQD/C +kK+Pp4lnW5VY96/p4U+BoSuTmeSp9Tc/ss0p0/YrHYbypDn9cxhKG4CL5Crd7s5c +Nyxn7inPOO345m4XgQXzQzFhVUbfIBy9cdeptdHCkqWjNuKmZ44NCqKVbHewYIn5 +Bfvw5dZDMlpkdIYkQWTRGSN9mpsUfGixVnWV/WCWyV0uVoCMaUrkaXvRnawRuh70 +RVBgdFlJik8/AoIBAE3rYUrHkvoYS9KjhCFQy2Yx6cBSjpRKxGVhJv3KAxJq19ty +tdcd9JS3+cDl+jOObz2zgmrGzAOp5MshT/UoVAgdITrZhZ794qCAxyI2b0JEFVd7 +WrihZuWPzil0ypJtFFf1xIQCMugj5HCnGx713t/gENVRK+n8+2zMTTR8ypH8eJO1 +UVd1tK4S9jlpXJeK1YUdAugWfsx9XDtWxakujw0grqhg3f4E+LmEmuRtcPDcK3hu +wtf7P7c83EoHqWId7cOgAnyNnjIAmk0whHHfzS3G8E8ViCxChCX8ecfQqwKOOA+x +Pigx+YOnDgE7Nei2Lm0MyatfRK7MrRrVrAZH5pMCggEALlzXYQg1op/0gJR63Dr6 +CigBfmGvDrMRGPvmBu2LwVdQcslTIGijIB+t/DkSQReoTP/MGcYj1NxtNyEBMJls +jznoJTStG5kxjH7d/IVWFx+4qH4eKlhVN58M6B7fg0deDKTFY9SLfLJFer1ExQ+7 +USQvnoACGaIeVdcil7HRE/xgTSwedz0grinFxJ1huGvi9bhak/ZKnNykTlNot991 +S9YPnJXiWA5MTpWt8OxF+zzeEmcbfSK9p/gHevJlrbxgUoU8O6L1gl1tqPGOB5aR +IJDdqNgM4C6p2ALMPp+khvW+ScoU6iR1viwJtbGVzEmK9VBrhdawmP4N0R4iVQhl +AQKCAQEAlS6ETo5eGEdivRW+PBkkoWKsAJ/QAcXP+ZlNHaQet7KZf/H0IkO60ylD +Uo/5sYdavnC+2I5Ffk6q+C+fNSefxo4I9CdpqluhML6q0xwV9toCIJEeX785Abh2 +EYVlDc2XCSjyqJT8q0KamTpNsyA65E9eWKKcDf/3mV1s3SXoOeaSs4OreuCZ0NKt +XcolPtSRs4mRpvH6biXd5XmHyFJHHQvUxnN6PhJMpGdEeoLv1WmwRvHWYMnMXlfr +MX0KBYFBLdMj809hQDZL55PZAGAKAA2ZCu8dk3DAUK2yJ/qdcdsNTNNqyPROH+oo +lO8WfnSDJJ0X6Cd6p3Umds2FN+4FvA== +-----END PRIVATE KEY----- diff --git a/test/unit/fixtures/ssl/client.pem b/test/unit/fixtures/ssl/client.pem new file mode 100644 index 00000000..f5032067 --- /dev/null +++ b/test/unit/fixtures/ssl/client.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFRDCCAywCFAYYtXf1AU9Oz1ua0D50g70ACMNTMA0GCSqGSIb3DQEBCwUAMF4x +CzAJBgNVBAYTAkNIMQ8wDQYDVQQIDAZHZW5ldmExDzANBgNVBAcMBkdlbmV2YTEX +MBUGA1UECgwOU29uYXJTb3VyY2UgU0ExFDASBgNVBAMMC1NvbmFyU291cmNlMB4X +DTI0MDUwMTA5Mjc1OVoXDTM0MDQyOTA5Mjc1OVowXzELMAkGA1UEBhMCQ0gxDzAN +BgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25hclNv +dXJjZSBTQTEVMBMGA1UEAwwMSnVsaWVuIEhlbnJ5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEApbdLMv3raPqeJC3YGAj8kOiI4qN+xoitERxFRVczmwVP +cPcMlfNHYHfVftqoIznGWVeugRa5uPKJEFQjUmjyGg/i5tOLx+pf8bRfyoHgSyMz +ju86Vmskwq/l/psL0d8ni87CygH3//lSgAucNkvPcPbHmQKt1w+9MfCJdtaf8EuJ +jmngl191x5w9dxtt0XJV1hBRnlo0iQT63iv8uJAkaM1e2ljHoxEKMUmHYJcikWRY +JYhlWa0vGqs9tIAaE0iBKSW00MafNmiSZSHoi8vMxcbmdtF3d9aiVOFnXeHwfqGg +H0Sv2txm03w1TCPpt7rykeM4Szn9omsFW/hILD4iy0+LQtEz1oanlu+CI+u5L6jw +ZZTeY+Jwb4QIHKB7z1sG8vsbC98LTQ/OTT/OdY7oDdsaBuS9EzL+end/u5LN1xti +CX35o16SvQmrK6uJHIynRLbYrM+n9MCH7RtAkB/pcZ4iDI6zJlYui+v3xdq6Z7p7 +4HOhlPcn/+o61Sxh0ht5+aG3jMnSCn9iMEncZKC8QDRruuzPkymNMyvkiovqnSOz +6imc/dYg3Tz6IzdXb/f14/Sq1VIEtTObC10DhOQ7CddsbF8U5GgUJp/GDNWwMAiT +BYsZ9O0+Zl3uM9CTbrjpILc48qHqdlIhDZRtJi67WYr8UR62dwiqfvXoCD5uBNEC +AwEAATANBgkqhkiG9w0BAQsFAAOCAgEAJ+NDZzqTUUHpyAsQXk2Anm8FtgN6A3zy +9pgCZ/crMc3nSgSsrVMVtYGKhHIEalixYXUSV4ladZGXH0A/+GA/4C0rFVqwTE7W +/eW35i0JQAgkmiiYDN13/gsYKgIjoqv6/ChZwll6hsvsJ1JMBsDQuEQyt6ahHj2N +MfGZrLizTJsxENvBB4Szi9laQs9EZRGb5/lvnJMq8vyF9CYqDNkzN20+6lQaD4/k +KklrUM1/2HnwNQmZ7ALNnPU1hingGqVFj3gUA15o0HlZFRwStDWyxZO9iuIm4sh3 +1E1IUen2fwXcD3PIWzEplvbxlIgtn3dr/f7zvT8MtfjLvpg3gKwzGf5Ot9/ZVIEI +SLAnhp0CRCKcZ+iNBoeF19Kiwe+ilpJFrAGhU7UEsHUt1lX00BYeERvSfux2gyst +1vluuofWAJf1bqWWf9rTnQ5uYhaf/el7BQ81lK4sLGZI8aPfbQOJue30zCWPBxQ9 +3AoKGaQ7BRDjdCDXCymgieHAaCdO0kEEcHBP75M2XmD4hbZFJxPfc1s9js9Mno4S +QDPkYscmAT6GKojcmxRU3Z57CzXoiN89b371u/UOpGfR/ueD3Ev0shf0T/nb8cJf +WZohSBNB3ABYQWMRR6lLNnxdUVts1xEgFSxRG5Ch6Ip80ynhHnZ7KMQMcXTNlK4r +eM/OGl3jeFY= +-----END CERTIFICATE----- diff --git a/test/unit/fixtures/ssl/conf/openssl-client-auth.conf b/test/unit/fixtures/ssl/conf/openssl-client-auth.conf new file mode 100644 index 00000000..0600d431 --- /dev/null +++ b/test/unit/fixtures/ssl/conf/openssl-client-auth.conf @@ -0,0 +1,33 @@ +HOME = . +RANDFILE = $ENV::HOME/.rnd + +[ req ] +default_bits = 4096 +distinguished_name = req_distinguished_name +req_extensions = client_extensions + +[ req_distinguished_name ] +countryName = Country Name (2-letter code) +countryName_default = CH +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Geneva +localityName = Locality (e.g. city name) +localityName_default = Geneva +organizationName = Organization (e.g. company name) +organizationName_default = SonarSource SA +commonName = Common Name (your.domain.com) +commonName_default = Julien Henry + +[ client_extensions ] +basicConstraints = CA:FALSE +keyUsage = digitalSignature, keyEncipherment, dataEncipherment +extendedKeyUsage = clientAuth +nsCertType = client + +[ ca_extensions ] +basicConstraints = CA:FALSE +keyUsage = keyEncipherment, dataEncipherment, keyCertSign, cRLSign, digitalSignature +extendedKeyUsage = serverAuth +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always, issuer +basicConstraints = critical, CA:true diff --git a/test/unit/fixtures/ssl/conf/openssl.conf b/test/unit/fixtures/ssl/conf/openssl.conf new file mode 100644 index 00000000..a6e2e280 --- /dev/null +++ b/test/unit/fixtures/ssl/conf/openssl.conf @@ -0,0 +1,34 @@ +HOME = . + +[ req ] +default_bits = 4096 +distinguished_name = req_distinguished_name +req_extensions = req_extensions + +[ req_distinguished_name ] +countryName = Country Name (2-letter code) +countryName_default = CH +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = Geneva +localityName = Locality (e.g. city name) +localityName_default = Geneva +organizationName = Organization (e.g. company name) +organizationName_default = SonarSource SA +commonName = Common Name (your.domain.com) +commonName_default = localhost + +[ req_extensions ] +subjectAltName = @alt_names +keyUsage = keyEncipherment, dataEncipherment, digitalSignature +extendedKeyUsage = serverAuth + +[ ca_extensions ] +basicConstraints = CA:FALSE +keyUsage = keyEncipherment, dataEncipherment, keyCertSign, cRLSign, digitalSignature +extendedKeyUsage = serverAuth +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid:always, issuer +basicConstraints = critical, CA:true + +[ alt_names ] +DNS.1 = localhost diff --git a/test/unit/fixtures/ssl/conf/v3.ext b/test/unit/fixtures/ssl/conf/v3.ext new file mode 100644 index 00000000..8027d8f1 --- /dev/null +++ b/test/unit/fixtures/ssl/conf/v3.ext @@ -0,0 +1,7 @@ +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost \ No newline at end of file diff --git a/test/unit/fixtures/ssl/keystore.p12 b/test/unit/fixtures/ssl/keystore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..1441aba67abb8125e37e813a716c95f1efdc4484 GIT binary patch literal 4176 zcmY*cXE+;|L{3yS1WL`KeJWh&@siwJH>8>^)MeYSh+N6(wd7RIN~>wfEk; z)Fx83Z-38o@BQEV;XLp2yze>Z<9Xi$#n8cTkdQ(#bPQm~y(n1J2{j2BNe+gN6o{d{ zhGJ;Xp%^gjeVi{srQH6cQ*WnBu=TP?3P4G(d781->u(N2+``Zjb}Q zFrcna+Zh-iu)fN(R1sv8eqf^g9AlQc@D@qA0Ky*!ADr$sTJqlWM@02$n5HSGc``M$ z$;KLD9b|M5ZNW_Npg;-sX>425TXJ;IQzbNDd_10!s5&wceY4AY!@uFhg62BKqG6h> zZ-*XIvFYrODoyiRDi}?)KGhD++13zB{;A+`tn1g&oL76S#R2RJwXmBNi<{XysD$yF zRf*{`$?{g_xDL9rEwZ*c)pS~7H)P_@yR>8^|0Mm z?rWiphDb`A;(&Q?&Fe25a0@Gbr9bC3q!*|nZCngD@A6*I(`I&u%Yl(>pGax1mrd-3 z;XC5Ld8DkW9@B|&S6NXLOkd%D@<6tW+CL;n3AbmsF`Ae}CFxm9I~w239muM6R@V|g zKRTx0c$092u*|(M4Z>Ojw-mt@{ zz?GaUU_3Z2)F)lfH9dG)+)m-x%DJd>9u_EuTWM9e?ZH@x8xNy7j~?C zQp*S9VydC%h!UjS23SvX`gOZCMu&1dTA2!YXeQ#f&sctcIJ$-O@E<;slH08P!ke+o z2|#1UJI7BKv}ZrIe{+1$`JSspDkIdle5|_eT?xqfE*Dwf&XGn3aNWftS7tx6KUg8x zFktgQFqiJ0$gF3qLfU-^)Af_ID2HTSTsb%}fvh?Wd9N!K~9H6aQq(_SzBJ;wQ)s z)DlRQV*VNC)kOvB?%Ra9{Zi*s*GsR$Vz_9WE+zX_lU-4wKsevzSb3$;riiemvL7&a zA#-Xf*ji|$%5nVIo2^|PsA}gB{|%P;hr4H|3#uhL3(IOnsh2T~X{x!Y3kZZaAgFA^ z$F`)~emwiqQ^bwk`j%*Z3$MM`9PHlMyWF`$Rq7O>|g<-?hio&&qU(-=FNthoW< zf<~Yn$Xg?w(>#n?B3JA2S9S+epbM2YYD7`Y^araD*0tN93r8mX%ArzsRc3Xt=RuR} zJKTAL^dFAyC{)?QKNj zn@8_yFGmr~LW*^s^^%cyxSBb5J z|8!=XGrtTNm*)b~li&CGcn_@;UQttr;xuQ}`BD7B=xs0MC#}-1&(Ywz+d~?fOq57b zzV-OHkUF-<;K^lPWehoGbWM@-=RWJL(nnXFr-`5D;6(ff&f}U%LNqb%QDY#|9`! zZu}M9|B^2M&mBnrb%$bU9P5%=pwPeW@OK<$>D-Bs^0qO3P@{-KL8(8sv!KcvZuUpZ zhCwq-6$l=FvOENdw>4|IJ7GoS8C`i(k)cDYpf0MoQwir`x5rHJXJKSY3`U9NY+nVx zbx`J(^LAN%*nKi)^SNb>*Q{(`CH30vq8sC%<@KS8^z~4t!Zzr=i&cq3#JI`4zt(td1olXqj)-SU8cjR=5-rnZz_2+@u`)(W#UzT}iiB zMfe*k9!Nmkw0YI^m09kaj;*A)tL@GTl@u;a>rsA}LlLXHuu6Y&boD$XDxxlYh;~-X z9BwQsvB5X!!jRtAxJb@fF4W;i7Wl=$<4vgRd?9`9Oqq=0$uR=@e5V`Vf~0a_@2v#WB=U5GyPFpE*&$RZ68#I5`6U4@`;hjn=WD3z& zqgfMi_k%I|9;r{;aa}I2+a@@rb^TCe(r+>zvppN^~1H$ z9&%yPV#K&DY9qjydV!=|N8bFh<2+!>*7h|C!T;1yq+QJTFCz z{TZ!@@H}Z)eJS!9G)#}nnrv5j{Up(0SfjP>6`fJTZwDQky?5eP62iap%0>hj@CEcZ zwyDPL_0~z{yk^dmu^?|B@BT}emra_}7DY?;ccmjhPG#XGzrIo-wF;ZwAaWsAw#y7Q zsWOqaQyUb%+Z;;rMp@lTB>-8TfgM?FV>Nt}+1@+2L^RGQC&hkD)C8*(NSf^4=V|P$Akc`IIg1S(=t-`ACXT-z zr|~1HkH-%Yab7t$EOeadjnF}Lc+qCec}C`_ z`7RAf_v0pKcZ286GkzaPb@gO4j?V z?QwaeIqr|5bU^SL2TJ&&M>&U&)MEG>>J{5PYpY;adP9>OfHK6nU9GJ{Pv)QQSIH48 zWHa3BuwT_P-&+H?m=u^^a20>t9Mr!{5?;TyuS=G->~Fl=7-{?>onr{l48A_g}k+ zUZ`6Xo?kTx^k`m`1aGhNgW%{aFTlk+aX;7Dc%;1aO-R59>xUA##W%$nXh`md$lz!o z?~mz50_CIy?*#fKwvc?uzHZd2gb0GuQrBZW<(u!1wWu*Pm{iX)aX;3D4UMw(uH517 zr2U!f9gbMm&NOt<&fHFI*d2LKSAjuolLkNS-1Iv9IUY7H@cFIXM4pOmNOn;htCXRQ zfw`QKMXmt@Wjidfa;*m<{q*4!2A3~Ujo?b)6EW$GYQvcsnNXWft0oQbLTHe?9gQ3N zbupKCazXmrdw+^J@^>w$GJw+Dl?zt8E670)d1uc7Hguf(vwg+ndtD$k(aL`fs_t@S=MnqRUIc_qAcgpSpJ$%G5}qD^B7VKHXuNx5U3JS9 z5*HQkQi-+`ePB!@GK24;AdSg4?^8LRaC_b|i!>KBttJMRxk^-wh;6UswN?&m(5fmA z;;2nG9@bzhC5Q_R&$A}nCwC2^U-1&}(7PKDmz;a;ZO^SCi|SR`b+ZXB?~*Qm+Vm`A z&-`wJo6D<%sg|02d=d|)zRP#47}i{q@I|rgS5P}vY;ITt($T-pFf-FhI$RMe^G-p< z07+E4vwtOi3b?59KkFevNgwJ~^JuZ}20AR@)?UDPCQ7;idi6naSz8J}W;Wd;-0Yg= zhmKo_D1ND+_P5%z>#Gu)!}xd`Q)>SwzEsBcvZfa_X(2211}_dP;acW{Uw?H z;P_xa(zHlpTc;m5enCg-h*;n#n(?~|;ZgeNu9VW&0fc$lJ_=7~)bG(&F| zS-Io&_iMZq?854;`KSGMdL2-t`!pRVQBZrCVUZ!bdPfF0@sa+u)yq^^(r9z0@3fdK z1BGvA;XrHOu7{Jz$r`JfZYEQun&8^nxJJy|SQpkfEp_ zQ;Kx|pps7gl1UqPf=G7-6m-VU-Vg53HM#yUWA(<-%{t-%@UDGCV7ch%_Fc_&BhcgU zqvX~qAXA~$vdG5Pj{-$eqESNnfGj9Fnu}LsAOP7f&zpUzLqM)GD!3*EnHdn$Yl}Ga zN6pVVS{ADD9TG6;o&QZ|5GDzxTMPg%021H`um`vTod2RbzzfRzpB82b79f*Lgdr_n zQ3G-gM6R}m^S{-umsWyGK<_{yV305k=?!)e0LY~05N~t1O&5DB_f^gp+_qTKx7&O` R@@L#RoSH|utN#YM{{r0b*s=fs literal 0 HcmV?d00001 diff --git a/test/unit/fixtures/ssl/server.csr b/test/unit/fixtures/ssl/server.csr new file mode 100644 index 00000000..a8d90805 --- /dev/null +++ b/test/unit/fixtures/ssl/server.csr @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIE6jCCAtICAQAwXDELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0G +A1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTESMBAGA1UEAwwJ +bG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9fROFMka +ZzcE9KA+zGVoDZf2gPjd122oLV8CaiLTafqu0tgCI95+ffVXLo/+nTnLwR6VA3qz +uQMIZ6mMAZ442kr9DUCqJC6c6wfji4gQgOhsNp+RV7GWk2ZioJFhFageacrlJFgc +gJHrFGWSF8qtmb1snQ98l1OwEtkSCit0nIWOFS2sydoVl5AD70QdO6TJtP53Kdvf +SgtTAbR56UiEsF2aznzVrLbhD7WWSq+Y/rb+iYuNzkoM/5m/vdtF159MbbDF6WEU +CMVijOTepUfH0t8BI8L4zfS2PaZGLexq2DbpFvQVkkphpR9A1yg5WSZHb8K9dYrK +WNCqzE9hQLKOIRt6kD4ngLlluqOqkeh/Y1Y2eFdwI6RV+Yle4dhbXc08okic4hiy +8WB8vlSU7ErfHLcgPoN1z2NOFp24/YH6YeBpi2+KZbr9sF4Ud+cBATivM46gWtcj +8Q2yun8BVduO+koqprqZDl/vSYqRVFrkTQtYpys++DnJAIAZZK0luEES9baRnLpp +HKjb7QNhc3uXd7rU16fxNVVA9CpepeiKVif/7Zjfe+cgaljLJcP0Sg9IbJosRvFZ +QPMb/573UCyzaac7yNEloWPstG1g7i8/mbS7jBirMUHEUBQJVYQ8xSoh+lUvjC2A +pV8PzY1JHV+WOCZLuuz8bNu5dF5I7OCM/L0CAwEAAaBJMEcGCSqGSIb3DQEJDjE6 +MDgwFAYDVR0RBA0wC4IJbG9jYWxob3N0MAsGA1UdDwQEAwIEsDATBgNVHSUEDDAK +BggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAgEAyALtkSem2WuYu6smEfq4RptU +bhsu7Z1nlqe9fG5TotzPBkmRgMRCtXEd+KVwjw69g4VnWJlKPd7+DCt+3GxNLD/+ +3GWmX7sWnsB0LpmYqoE3ZZx5oJ8oIakKKfiMt5M4Hvi8hXFwBRTiKvGSkPvwLhER +nxsr4zsG7iZUDlRURtJqw7BCwUKHKsy54cxy8unaA4LS9LOAlnJhtyc0XROXEnRS +KeuFnAMRZi+ghHncCWApvxJ4OxXCdXj8PK/jHQnxhOruohaWpsMQHtJYwUlJnHxD +xVXcjcEAWHVjwnkKKYvGWusBOO15mQYpK3uS5PrV7cHBVtFmlmuIBB/1/7GOyZi1 +DgJ9hKyCcS0aEhoO0JvHxMHvcvzbCh7A40iN/nZ+oOIh25sEWaek9KXTl2H4X4iY +MWW76aWKhFKbZEtCqQSP4oZAFJ9ZKI8vooXQukqOZt4kNAe17ceAZ9B/TRVb13oc +evsafdZ9hiWebFPMR5KJ5l5yYX9Cr0MUtSPCqcr1xHiVkOwC/hSNJSErSRfeT+S4 +lBz17ZIAFtrAzp17PPHpNSruOEtCK6Giguhp1b6ANP+ycnDXyWnV6krKm1kXZXxG +KxROfnMqODe8+IR2C6yhyK3lt9nwELgHfxqKSKIpKay9W70d00n/w4vLxPopV3LH +T0AgHjsYkUCXI0Sa4iY= +-----END CERTIFICATE REQUEST----- diff --git a/test/unit/fixtures/ssl/server.key b/test/unit/fixtures/ssl/server.key new file mode 100644 index 00000000..f8656f0e --- /dev/null +++ b/test/unit/fixtures/ssl/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQD19E4UyRpnNwT0 +oD7MZWgNl/aA+N3XbagtXwJqItNp+q7S2AIj3n599Vcuj/6dOcvBHpUDerO5Awhn +qYwBnjjaSv0NQKokLpzrB+OLiBCA6Gw2n5FXsZaTZmKgkWEVqB5pyuUkWByAkesU +ZZIXyq2ZvWydD3yXU7AS2RIKK3SchY4VLazJ2hWXkAPvRB07pMm0/ncp299KC1MB +tHnpSISwXZrOfNWstuEPtZZKr5j+tv6Ji43OSgz/mb+920XXn0xtsMXpYRQIxWKM +5N6lR8fS3wEjwvjN9LY9pkYt7GrYNukW9BWSSmGlH0DXKDlZJkdvwr11ispY0KrM +T2FAso4hG3qQPieAuWW6o6qR6H9jVjZ4V3AjpFX5iV7h2FtdzTyiSJziGLLxYHy+ +VJTsSt8ctyA+g3XPY04Wnbj9gfph4GmLb4pluv2wXhR35wEBOK8zjqBa1yPxDbK6 +fwFV2476SiqmupkOX+9JipFUWuRNC1inKz74OckAgBlkrSW4QRL1tpGcumkcqNvt +A2Fze5d3utTXp/E1VUD0Kl6l6IpWJ//tmN975yBqWMslw/RKD0hsmixG8VlA8xv/ +nvdQLLNppzvI0SWhY+y0bWDuLz+ZtLuMGKsxQcRQFAlVhDzFKiH6VS+MLYClXw/N +jUkdX5Y4Jku67Pxs27l0Xkjs4Iz8vQIDAQABAoICAB9WNzSSwthvvCPm3tlv+ifx +OqkIDEvMXucY+dfIBCO2mtumRe+IA5nMzoTSN+CUYo+Cc/3zfj6OUl3SzlHOdPPr +Jf6wRH1Dqx6O7MD0XxXthwwWnJANwl+ZZeuLWlFGEEnuXe+ZglgnP0pj/o8ldaTm +65W/SWKGeSKNoazGCJ+ArK+qGB/Ht4SOBtJPXWIiBskWutwMdZCbjMHk2ruMT8ug +wX6ZjSfqTRaRTkrJwLaDXj7sFu83pBxU3Ic2DtoAI9697RllEwZjD8Ffz7ZDRQRr +AVwrFUQ4b5e/PaXQP3S42k3gX3c6HuLI7pv7NgNTyzpEF5uISWuzem95layGk3EI +fCHBm2K8klOpS7AWjTBEBiDNg9neNNYi6WYkOiTihTdIBqOR7mZE/lbDMr09bVuh +xZTiTxU0egMobPVi/OW/AaSwCiDHqd/vOmvLn3wKWFxJwSRG2Cmf2y5zZtTz1XNN +0Jm3df6/eXhMDihoppmFFdduRCwBCdKX0nkwYIflg8bwW5wsxCXWd0um4ql+vQlq +xFO0j+bm3BSsul0Y0UCATsnk7Xkw4OS4ybB+fYhGZmtYT4HaSrjtxdP3n8C8jEUp +xWc2Gv69uko4PWSVs1FY1KzCx8Bp+tDrbNL8lxbtO2DhSW1LRNiVsLfZqnh7tXIj +wxv67YtnNXfyDZaHS/+JAoIBAQD7lZDWnQQKehzH1QtB4Qo4HxjB7CK1zYrJqw/Y +fD7n0DDccPi20ugu5zSFLsMc8DXtKf4HJlqF7XRejegxFT9S07BiKiVxPHQYXPeO +AZFsfE+eBmxbUlXYOkJyChf1dKlZCT28j9C7VZOCGqc8A6InszuiPpjjwdvyEXQD +YNJly6/XidMquzhuSxAW+RUUh3YyEtdwCgMa1xC3hto65pgDQRApHCdme3CaV5Dv +EVj4dAq8wmje/u++8oVRsxRTrCr8HHHHmtYrw5SleyFeETvGNZ8o40cQAg+wmEXy +4Kdz+nfpesxIPXyUQt+Uw4Ggg9HZNTOWiKiY7ITq/J6rqRWfAoIBAQD6RXFHYCrj +IVLRXme9VAGkTY112xfc2hjKI/+xQQKrecFH73cRu/mjP3TK4Hm+zszRKQCoVlbF +fNnZIfPmURekOu5wMc8Kk+8pn19L1i4xe5fnHVVvmmgCH4yiqdkyAFgTxxqgeYGp +AjPuopC0tNIE9glc3Xl7QLspQP7c2ucFKw2T5lphnrpE1RsITCau+LNcT4HBNGlE +j+tlIyj3k+Hnsftt8WA0DnQkp6ltv/em9yg0Riz+SM3oeSMQYxC7gPf339itYDav +wlucQGbfxxOMmUDGMSj92Lo3+bl/BgqSFyJgmSOThaHBQaFvWFoCN3CgcPrMEtBR +PqTHzsVhY/gjAoIBAQD6hF18h0+dyyjbh/Y0vIz7g2OYvrV1iV3ZIQCfVmEhXjs5 +Vzkie/N9uPagZAcfysY4CieNIRDk6aWF/hKmxXyP0oGBzmwITOVh2Tkc68zOVR7G +waimat9Wd/TwL8LZxThYk44pNJ/p2vYOiNHcPdX7aEtKbMC7kq+cZOq77m6ztNa9 +bt2aYGF52j8EUTU/gwAcLozeYOnkkSFxTtQB8NqP6vrXpNRLBUIEPovwsrqAdLS4 +b3IUE5HR9xbwWr0z8G3BK+XUmAcJ/zAGdAyu6cQ2w/Bfu6lodFUBSS/mAPRd1ZwQ +HxpKGQfzbn/KV7+9gWW1v3dGP4B6/pIAmFq7npwLAoIBAQCs9sM0Lf1V3djrw8/0 +ZBOCZuqmEhYq1zwcr8ZtzV03/zyaJ6BlzEDaFufzsjHRsgCRaUIAFTOA5ylzy+hR +O6gYI3ZYacQKLnUyked9dPeV0TIJUxeRuue41+8NGE94JA67FHaNg4wdrt0PRqC1 +kuY28YdE+/eSPAldmILLRio1QyzuE1xRbS6UladKE78EW/Mxj+1ABqXd8Y0g56zP +dg/BXhtDP3daYsbX8lvA8tQIO3Y4sms0DkLoMJgQIjcVLyuwzq5kHEOPMsa3dTbj +3yTenafLkXwf++Gu/9K4PAegMYbbtdqFgOxqsJ4OYsRKFeCrsYlS8omwLJgbUwbM +qRd7AoIBAQDudD3EPQGTeR38dy+IvIrvYxUfM+gm/7d8x886/HKMgC2I8rWApHJZ ++tAWyzOivWsy+2uZfi0wWAgQ1uRuIsR4+HVce0KqQQge30qqvokNhtwOkl/fKjfC +Y3G7DboXgjmZW6sOzUthmq99cLUAo7Zb+pKADFjWlC/Nx3vGdjyrV0QwcFsbs/gy +/irk79HH88KhqWEUKXTYkZgwJ1VHXyThh7F4ZujoL7Bu+cU9RkLG/6E6b2p1XOEB ++zJD7p+fuRDTRtve8ch/t4OHed1VGwbQq6XwHCzRZvjigPlDZIuEe5Sipm7yoLnL +utJZfnp1WnKTVF2uF0hm8LyYxTZGb4CI +-----END PRIVATE KEY----- diff --git a/test/unit/fixtures/ssl/server.pem b/test/unit/fixtures/ssl/server.pem new file mode 100644 index 00000000..80838b2b --- /dev/null +++ b/test/unit/fixtures/ssl/server.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFlzCCA3+gAwIBAgIUXvpazm9GDa6JtHY8erl4M9pxopQwDQYJKoZIhvcNAQEL +BQAwXDELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu +ZXZhMRcwFQYDVQQKDA5Tb25hclNvdXJjZSBTQTESMBAGA1UEAwwJbG9jYWxob3N0 +MB4XDTI0MDUwMTA5MDEwMloXDTM0MDQyOTA5MDEwMlowXDELMAkGA1UEBhMCQ0gx +DzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRcwFQYDVQQKDA5Tb25h +clNvdXJjZSBTQTESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA9fROFMkaZzcE9KA+zGVoDZf2gPjd122oLV8CaiLTafqu +0tgCI95+ffVXLo/+nTnLwR6VA3qzuQMIZ6mMAZ442kr9DUCqJC6c6wfji4gQgOhs +Np+RV7GWk2ZioJFhFageacrlJFgcgJHrFGWSF8qtmb1snQ98l1OwEtkSCit0nIWO +FS2sydoVl5AD70QdO6TJtP53KdvfSgtTAbR56UiEsF2aznzVrLbhD7WWSq+Y/rb+ +iYuNzkoM/5m/vdtF159MbbDF6WEUCMVijOTepUfH0t8BI8L4zfS2PaZGLexq2Dbp +FvQVkkphpR9A1yg5WSZHb8K9dYrKWNCqzE9hQLKOIRt6kD4ngLlluqOqkeh/Y1Y2 +eFdwI6RV+Yle4dhbXc08okic4hiy8WB8vlSU7ErfHLcgPoN1z2NOFp24/YH6YeBp +i2+KZbr9sF4Ud+cBATivM46gWtcj8Q2yun8BVduO+koqprqZDl/vSYqRVFrkTQtY +pys++DnJAIAZZK0luEES9baRnLppHKjb7QNhc3uXd7rU16fxNVVA9CpepeiKVif/ +7Zjfe+cgaljLJcP0Sg9IbJosRvFZQPMb/573UCyzaac7yNEloWPstG1g7i8/mbS7 +jBirMUHEUBQJVYQ8xSoh+lUvjC2ApV8PzY1JHV+WOCZLuuz8bNu5dF5I7OCM/L0C +AwEAAaNRME8wHwYDVR0jBBgwFoAUKreRsCBedehspTqHXQxaLcE5YBUwCQYDVR0T +BAIwADALBgNVHQ8EBAMCBPAwFAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3 +DQEBCwUAA4ICAQAcYAPZJ7qaObsKYcRHfJYW10tOq1W79jkfrS0e0MBgFyLxYUnm +Vyvezy+uaZ4uAj9Vv+2jnYpV1xMyHUWFEBpgCU7ZHHw9cigITm94FD5Pv8TdPst8 +NOg+2pRU8h9YOuPkcVsA8M5UN5p/xFKvnUKL7rlEe58R1Z4cHEUpTchzvs1DKnD7 +we5NoAXDxT3D9WBe8OPNy9gALn7/vtDdPyT6VoKJvHCobU5y1d5gw3zXYHjBK0y/ +KWDIxIwm+jaK6vgmXtpTX0qCqmw0O5rQr26z4hu+a00jK2X2OCvxHJVlYfOIC3n4 +jCqa8Fjs1BPCI/ZHPb4aXSgATb8izAzUe7PwHdw7MrPmJedFdu1cyBpKPRrYeXhb +0S3J5umxBiNevCoJLI8NsyOTpHYh5VDjg/9UoARdwAtB/Qjp8mAQDMa8zAFpoKij +7mNziDmCSBs1ks/qrxS4uhx0NUs2wzT8dboC96E92psTwl1bbVOrJNPp/6UJfiSA +9+/UenYUirkQU53vzvMosMRz0sdLcnZI0AVWkbZaP+tGM9nwCd3cOs9az47Otyzp +M5tDlsicEQ/O1jpnKJ8LzU1g+Qp60SiIp4Tr8aYCTtkSlACHZcdGMVGrSfCRc0YE +ypd9cp2WLfx2tZlMVw+7k12dN0tU5kEhh4S3OdP4bFEZg1c2sBiLx8m4xw== +-----END CERTIFICATE----- diff --git a/test/unit/fixtures/ssl/truststore-empty.p12 b/test/unit/fixtures/ssl/truststore-empty.p12 new file mode 100755 index 0000000000000000000000000000000000000000..ef6bfbf7eb94a2b071d4fd084fa5b1d57db4c440 GIT binary patch literal 103 zcmV-t0GR(UWdZ>MFcAg`Duzgg_YDCD0iXl~0x$qDO)xPq4F(BdhDZTr0|WvA1povf zqE2UcJXj*D7c#2(1U7KFC^>9<|E1eW{<;+7pE8@p1Qd9+c_IctVer6rR9a~h2hx8( JnDYVxClFLWALal6 literal 0 HcmV?d00001 diff --git a/test/unit/fixtures/ssl/truststore-invalid.p12 b/test/unit/fixtures/ssl/truststore-invalid.p12 new file mode 100755 index 0000000000000000000000000000000000000000..5436a867f3edb5665b077139d8c291ccf283ac0c GIT binary patch literal 1587 zcma)+Yc!Mz7{}**=f*^&nHG&2xh;}+G>llY+WqvL=l6eZ|1S?DK$(L891p2msVq3_|=bs5O5#sbBzPKL0+%m;Muh`TTqY zKNsR>L&2*R2a5aP(AMT&|a>pX}Ki$_RlEEB1^8sHLF_J>8Z^o-^%5nX9eR?AV)f!-+yl=8@3W^9pkpMvAp>p zev4G|w!>Km14)9(o!m!`m?NC@R;;njlasP2SO4~4FJ8V|xvMK-H>Pf^sQcb#SB*#N zd8yKR!WUg*yXLrO0tM$ZY@gn+;&Re<7+Q27s3*yWBAEt$yDpc z?yM&8ltOU4r!nw~Qe8N8f!8jN3k?tBUUOz1WbUT#1Ku)PLXt{=VOu>q$8hhh>%5GB zcjgR>rh?o@j3szPqN;V>%xJOP?yl^dwzn2fO>2r}`G1;&ZTt`CDTu z_SCS+ff=%iVL5IK+_tvwvB>bEfJqUHYh24U=7~Eza`aibc#NcSxe3p7TFD9Wb>QKm ziD&O8_}6;~&9#_W$#LEF*YA|{b7rH6tx2g?(_<)bpjUmOCS-h*7cZLW9!WHf%_9d^ zD`}JvQZoo9I8uaY36~vS{1is@53g`({W;xk+i+9Sygah1(J0Hs2>_R}Mp@o!y=x3r z4hD}jasArEy+DRbXooFy*XQut#P2sAPC}2wu9LnmNY?_&6xAEdp6R& zeOqa2;|933)0?kyred>`i+WVul9OQ35Dg+is;gfU1duQU0sY2)_fSl5M&_YHtfUQy uvFInCk@=}N{CLGuYW?EWRs`05S=&629d*T@Xz++PQA!(ME{gf{CH(=x(4=7i literal 0 HcmV?d00001 diff --git a/test/unit/fixtures/ssl/truststore.p12 b/test/unit/fixtures/ssl/truststore.p12 new file mode 100644 index 0000000000000000000000000000000000000000..71db48df922c8cca6621efe64100290802bbd711 GIT binary patch literal 1846 zcmV-62g&#_f(J4J0Ru3C2HXY-Duzgg_YDCD0ic2g%>;r5$uNQj#V~>f!3GH`hDe6@ z4FLxRpn?XnFoFiI0s#Opf(EAs2`Yw2hW8Bt2LUi<1_>&LNQU+thDZTr0|Wso1Q6XW3{oiXEI@tj&GrBt7+!#a1~93?wHBqItdg*3t&--IKx-lP zfm+Ti%gRYc$*M3J>A`Q<-K8*iipU347063e=9+1XI z#E@qb!cx3w6Z1DQ%NbHD2^^)Fwna!A+qAlEaRS%1xNi7Z38C+OAc-#WrK6+2m%HTb z^Bl-u8A&A}Y+({|dmrv-_n@Zfuc8HP-+7giI(Qg?IU)gcuT}>?G}hfdGXHOhA&t)A z^60ZM_psmi1Z%3et+^HP*8;_*#h_bBH-UsS7A_N(lXq=k$uqu`Ls^kR%o_G*v$db& z=dJelMH-v0nH@!h03YWP{cqYDo|8)|1o$|acl5D5)J^k)-fQ4!WCFTYo2fp*6X;=Q z+s+FM?{<3rb6vs0_!|klD0%|JAm9ibW7}!oay<^gKgmxd?nb|I=S&|7Fyc;^-f^Z@ z?``H0eI=YSb%H=x|q}-#Zra(`}fNg@FCd7AlzXF;hQMvIr;T#~{ z71nZt@)=5S?e_+D_WhygPULB@58K@mg9#Z?_jp!*m2vc)kp2gVN@D6Sb=!-%kMP1M z;aLuS*6h>9MuFZg^0=S$Uk*>eWMqD-20uY50z+VT*iM862f`?4ni@Tw*ZIcsnW z+Li($va#J?HQs81=2TQFr~**wSdO{SHRRUVo^1;70WV}XZZR$_eV{C8P^cW>Ua*`= zDKv?EtZzQ3L6L!vSC0Xg|3`1!3vmp^u5Qz4Dow)}zhOQ(n8gD!!NltJH`4DH3G?5{ zKLP>rDvc+1e*2nxcc%gy(jc$AVp>UOM#p^}@K+txzx&)`(_s;Bkp6gH1aVc$5T(LU zQg`A9s4Ll~R^VZD?=D}y-NYW#*w#gP+d-P>zgBe!dM}a!5fW{w82&7uA};n?IYwf! z)JGvuyou|EY)+iXTzcb#H~2UtYJjxUZYJ|w`iJv z`q-uFS4}U6B%U7`f7~K8i67Ak^p5da;O-chrfJ7{I0LO0a!EP$O7aWJcUQgC$?<+V zCOg$F!g(r0d}P|de2w`${i%<>qe#JwOt~6LCQC>DNawA8p`dB82!PD*$WIRwHs?Bz z6gd-xAp2NngUBC#+`c&0qAETgz12e&ye6}U`CNNK4n|Af{C(?Jf|EA!`DK?{E4iSb zL^%}VA>%H|I?-dhge{#=JFXyb51ugMC_D%+V(d6$GK*?rppz!~wuP+#pHIwM=e2P# zdMd|zZmrgaK89v+_Mn}RHXwjoPlt`2v>ix_Q|(hm@h@DG9oCx0FUGFk@2*dB^)19- zhQ59cZbk(?ZQG$)2)}5&i1T$=uyp|`5t`K%Jyw<@Z{7e$0otnQKmb`nTCj`qT5)NT z;bI{TZ!AIxj|a1-t!SvTu19LCx@yjMorZ?v1)e=REnzxoLf!;47j9zVOS82^ls0|~ z?GNO^w`!7>kGFV-pGo5^Nm!k-d_J-4;dhW);j~;B_kcA%F5IOamg0aLQf&=2ZUMfO ze*FADa%~ribo&IJ_78c3gOsd_6t5RPr&}GAwmnKT`5xYEJ}h_8OQ`fzSwGEqdX2GR zJavwBzp=R}rik$dB%(wwpd_W|3@PiJBvFTXKXq&H?y)JD&nm;w_NDlJvUtz7s>&~L zR#24=%IPpoFflL<1_@w>NC9O71OfpC00bbn;^$&${K};9Z^qDx1s17L)q2|^r1=G> k(4HFk78!~J6l(w#R;%EcV=2@;1@g@7^vcx1L;?aQ5EZOtWB>pF literal 0 HcmV?d00001 diff --git a/test/unit/request.test.ts b/test/unit/request.test.ts index fd850a40..11d39d63 100644 --- a/test/unit/request.test.ts +++ b/test/unit/request.test.ts @@ -18,7 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import axios from 'axios'; +import fs from 'fs'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; +import path from 'path'; +import * as logging from '../../src/logging'; import { fetch, getHttpAgents, initializeAxios } from '../../src/request'; import { ScannerProperties, ScannerProperty } from '../../src/types'; @@ -32,29 +35,128 @@ beforeEach(() => { describe('request', () => { describe('http-agent', () => { - it('should define proxy url correctly', () => { - const agents = getHttpAgents({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', - [ScannerProperty.SonarScannerProxyHost]: 'proxy.com', + describe('with proxy options', () => { + it('should define proxy url correctly', async () => { + const agents = await getHttpAgents({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarScannerProxyHost]: 'proxy.com', + }); + expect(agents.httpAgent).toBeInstanceOf(HttpProxyAgent); + expect(agents.httpAgent?.proxy.toString()).toBe('https://proxy.com/'); + expect(agents.httpsAgent).toBeInstanceOf(HttpsProxyAgent); + expect(agents.httpsAgent?.proxy.toString()).toBe('https://proxy.com/'); + }); + + it('should not define agents when no proxy is provided', async () => { + const agents = await getHttpAgents({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + }); + expect(agents.httpAgent).toBeUndefined(); + expect(agents.httpsAgent).toBeUndefined(); + expect(agents).toEqual({}); }); - expect(agents.httpAgent).toBeInstanceOf(HttpProxyAgent); - expect(agents.httpAgent?.proxy.toString()).toBe('https://proxy.com/'); - expect(agents.httpsAgent).toBeInstanceOf(HttpsProxyAgent); - expect(agents.httpsAgent?.proxy.toString()).toBe('https://proxy.com/'); }); - it('should not define agents when no proxy is provided', () => { - const agents = getHttpAgents({ + describe('with tls options', () => { + it('should initialize axios with password-protected truststore', async () => { + jest.spyOn(axios, 'create'); + + const truststorePath = path.join(__dirname, 'fixtures', 'ssl', 'truststore.p12'); + const truststorePass = 'password'; + const certificatePath = path.join(__dirname, 'fixtures', 'ssl', 'ca.pem'); + const certificatePem = fs.readFileSync(certificatePath).toString().replace(/\n/g, '\r\n'); + + const { httpsAgent } = await getHttpAgents({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarScannerTruststorePath]: truststorePath, + [ScannerProperty.SonarScannerTruststorePassword]: truststorePass, + }); + + const ca = httpsAgent?.options.ca as string[]; + expect(ca).toHaveLength(1); + expect(ca).toContain(certificatePem); + }); + + it("should not fail if truststore can't be parsed", async () => { + jest.spyOn(axios, 'create'); + jest.spyOn(logging, 'log'); + + const truststorePath = path.join(__dirname, 'fixtures', 'ssl', 'truststore-invalid.p12'); + const truststorePass = 'password'; + + const { httpsAgent } = await getHttpAgents({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarScannerTruststorePath]: truststorePath, + [ScannerProperty.SonarScannerTruststorePassword]: truststorePass, + }); + + expect(httpsAgent).toBeUndefined(); + expect(logging.log).toHaveBeenCalledWith( + logging.LogLevel.WARN, + expect.stringContaining('Failed to load truststore'), + ); + }); + + it('should initialize axios with password-protected empty truststore', async () => { + jest.spyOn(axios, 'create'); + const truststorePath = path.join(__dirname, 'fixtures', 'ssl', 'truststore-empty.p12'); + const truststorePass = 'password'; + + const { httpsAgent } = await getHttpAgents({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarScannerTruststorePath]: truststorePath, + [ScannerProperty.SonarScannerTruststorePassword]: truststorePass, + }); + + const ca = httpsAgent?.options.ca as string[]; + expect(ca).toHaveLength(0); + }); + + it('should initialize axios with password-protected keystore', async () => { + jest.spyOn(axios, 'create'); + const keystorePath = path.join(__dirname, 'fixtures', 'ssl', 'keystore.p12'); + const keystorePass = 'password'; + + const { httpsAgent } = await getHttpAgents({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarScannerKeystorePath]: keystorePath, + [ScannerProperty.SonarScannerKeystorePassword]: keystorePass, + }); + + expect(httpsAgent?.options.pfx).toEqual(fs.readFileSync(keystorePath)); + expect(httpsAgent?.options.passphrase).toBe(keystorePass); + }); + }); + + it('should support combining proxy, truststore and keystore', async () => { + jest.spyOn(axios, 'create'); + const truststorePath = path.join(__dirname, 'fixtures', 'ssl', 'truststore.p12'); + const truststorePass = 'password'; + const certificatePath = path.join(__dirname, 'fixtures', 'ssl', 'ca.pem'); + const certificatePem = fs.readFileSync(certificatePath).toString().replace(/\n/g, '\r\n'); + const keystorePath = path.join(__dirname, 'fixtures', 'ssl', 'keystore.p12'); + const keystorePass = 'password'; + + const { httpsAgent } = await getHttpAgents({ [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarScannerProxyHost]: 'proxy.com', + [ScannerProperty.SonarScannerTruststorePath]: truststorePath, + [ScannerProperty.SonarScannerTruststorePassword]: truststorePass, + [ScannerProperty.SonarScannerKeystorePath]: keystorePath, + [ScannerProperty.SonarScannerKeystorePassword]: keystorePass, }); - expect(agents.httpAgent).toBeUndefined(); - expect(agents.httpsAgent).toBeUndefined(); - expect(agents).toEqual({}); + + const ca = httpsAgent?.options.ca as string[]; + expect(ca).toHaveLength(1); + expect(ca).toContain(certificatePem); + expect(httpsAgent?.options.pfx).toEqual(fs.readFileSync(keystorePath)); + expect(httpsAgent?.options.passphrase).toBe(keystorePass); + expect(httpsAgent?.proxy.toString()).toBe('https://proxy.com/'); }); }); - describe('fetch', () => { - it('should initialize axios', () => { + describe('initializeAxios', () => { + it('should initialize axios', async () => { jest.spyOn(axios, 'create'); const properties: ScannerProperties = { @@ -62,8 +164,9 @@ describe('request', () => { [ScannerProperty.SonarToken]: 'testToken', }; - initializeAxios(properties); + await initializeAxios(properties); + expect(axios.create).toHaveBeenCalledTimes(1); expect(axios.create).toHaveBeenCalledWith({ baseURL: 'https://sonarcloud.io', headers: { @@ -73,7 +176,7 @@ describe('request', () => { }); }); - it('should initialize axios with timeout', () => { + it('should initialize axios with timeout', async () => { jest.spyOn(axios, 'create'); const properties: ScannerProperties = { @@ -82,7 +185,7 @@ describe('request', () => { [ScannerProperty.SonarScannerResponseTimeout]: '23', }; - initializeAxios(properties); + await initializeAxios(properties); expect(axios.create).toHaveBeenCalledWith({ baseURL: 'https://sonarcloud.io', @@ -92,12 +195,14 @@ describe('request', () => { timeout: 23000, }); }); + }); + describe('fetch', () => { it('should throw error if axios is not initialized', () => { expect(() => fetch({})).toThrow('Axios instance is not initialized'); }); - it('should call axios request if axios is initialized', () => { + it('should call axios request if axios is initialized', async () => { const mockedRequest = jest.fn(); jest.spyOn(axios, 'create').mockImplementation( () => @@ -111,7 +216,7 @@ describe('request', () => { [ScannerProperty.SonarToken]: 'testToken', }; - initializeAxios(properties); + await initializeAxios(properties); const config = { url: 'https://sonarcloud.io/api/issues/search' }; From efcd17b42507d28c6c743fd8f1bdfd00a8809f99 Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Wed, 1 May 2024 12:06:21 +0200 Subject: [PATCH 22/35] SCANNPM-5 Update CI tasks (#127) Co-authored-by: Victor --- .cirrus/Dockerfile | 2 +- .npmignore | 142 ++++++++++++++++++++++++++++++++++ jest.config.js | 2 +- package.json | 7 +- scripts/ci-analysis.js | 3 +- scripts/fix-comments.js | 5 -- src/constants.ts | 3 - src/java.ts | 3 +- src/scanner-cli.ts | 9 ++- test/unit/java.test.ts | 2 +- test/unit/properties.test.ts | 25 ++++-- test/unit/scanner-cli.test.ts | 19 +++-- tsconfig.json | 2 +- 13 files changed, 196 insertions(+), 28 deletions(-) create mode 100644 .npmignore diff --git a/.cirrus/Dockerfile b/.cirrus/Dockerfile index b86d30e2..4e035d5a 100644 --- a/.cirrus/Dockerfile +++ b/.cirrus/Dockerfile @@ -3,7 +3,7 @@ FROM ${CIRRUS_AWS_ACCOUNT}.dkr.ecr.eu-central-1.amazonaws.com/base:j17-latest USER root -ARG NODE_VERSION=16 +ARG NODE_VERSION=18 RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && apt-get install -y nodejs=${NODE_VERSION}.* diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..4133b4fe --- /dev/null +++ b/.npmignore @@ -0,0 +1,142 @@ +# Created by https://www.gitignore.io/api/node,SonarQube,intellij+all + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +### Intellij+all Patch ### +# Ignores the whole idea folder +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + + +### VS Code ### +.vscode/ + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +test-report.xml + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# test report file +xunit.xml + + +### SonarQube ### +# SonarQube ignore files. +# +# https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner +# Sonar Scanner working directories +.sonar/ +.scannerwork/ + +# SonarLint working directories, configuration files (including credentials) +.sonarlint/ + +# End of https://www.gitignore.io/api/node,SonarQube,intellij+all + +!test/**/fixtures/**/* + +# MacOS +.DS_Store diff --git a/jest.config.js b/jest.config.js index 7fae4cab..d9929fa1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -27,7 +27,7 @@ module.exports = { moduleFileExtensions: ['js', 'ts', 'json'], moduleDirectories: ['node_modules'], testResultsProcessor: 'jest-sonar-reporter', - testMatch: ['/test/unit/**/*.test.{js,ts}'], + testMatch: ['/test/unit/**/*.test.ts'], testTimeout: 20000, setupFilesAfterEnv: ['/test/setup.ts'], }; diff --git a/package.json b/package.json index d652d745..9fa3e924 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "url": "https://github.com/SonarSource/sonar-scanner-npm/issues" }, "license": "LGPL-3.0-only", - "main": "src/index.js", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", "bin": { "sonar-scanner": "bin/sonar-scanner" }, @@ -67,7 +68,7 @@ "sonar-runner" ], "scripts": { - "build": "npm ci && npm run ts-build && npm run check-format && npm run license && npm test && cd tools/orchestrator && npm run build", + "build": "npm ci && npm run ts-build && npm run check-format && npm run license && npm test && cd tools/orchestrator", "ts-build": "tsc && node scripts/fix-comments.js", "test": "npx jest --coverage", "test-integration": "cd test/integration && npm test", @@ -85,6 +86,6 @@ "arrowParens": "avoid" }, "files": [ - "src/**" + "build/**" ] } diff --git a/scripts/ci-analysis.js b/scripts/ci-analysis.js index cc87821a..60f556ea 100644 --- a/scripts/ci-analysis.js +++ b/scripts/ci-analysis.js @@ -20,7 +20,7 @@ const path = require('path'); // Regular users will call 'require('sonarqube-scanner')' - but not here: eat your own dog food! :-) -const scanner = require('../src').scan; +const scanner = require('../build/src').scan; // We just run an analysis and push it to SonarCloud // (No need to pass the server URL and the token, we're using the Travis @@ -36,6 +36,7 @@ scanner({ 'sonar.tests': 'test', 'sonar.host.url': 'https://sonarcloud.io', 'sonar.javascript.lcov.reportPaths': path.join(__dirname, '..', 'coverage', 'lcov.info'), + 'sonar.verbose': 'true', }, }).catch(err => { process.exitCode = err.status; diff --git a/scripts/fix-comments.js b/scripts/fix-comments.js index 3d8e38a1..b488261d 100644 --- a/scripts/fix-comments.js +++ b/scripts/fix-comments.js @@ -28,11 +28,6 @@ const directoryPath = path.resolve(__dirname, '../build/src'); const fileNames = fs.readdirSync(directoryPath); for (const fileName of fileNames) { - // Skip if not a .js file - if (!fileName.endsWith('.js')) { - continue; - } - // Read the file, drop the license header, re-prepend it and write the file const filePath = path.join(directoryPath, fileName); const fileContent = fs.readFileSync(filePath, 'utf8'); diff --git a/src/constants.ts b/src/constants.ts index 40065eab..9df7b39f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -26,9 +26,6 @@ export const SONARCLOUD_URL = 'https://sonarcloud.io'; export const SONARCLOUD_URL_REGEX = /^(https?:\/\/)?(www\.)?(sonarcloud\.io)/; -export const SONARCLOUD_ENV_REGEX = - /^(https?:\/\/)?(www\.)?([a-zA-Z0-9-]+\.)?(sc-dev\.io|sc-staging\.io|sonarcloud\.io)/; - export const SONARCLOUD_PRODUCTION_URL = 'https://sonarcloud.io'; export const SONARQUBE_JRE_PROVISIONING_MIN_VERSION = '10.6'; diff --git a/src/java.ts b/src/java.ts index 38f9116a..c08ff05b 100644 --- a/src/java.ts +++ b/src/java.ts @@ -76,7 +76,8 @@ export async function serverSupportsJREProvisioning( parameters: ScannerProperties, ): Promise { if (parameters[ScannerProperty.SonarScannerInternalIsSonarCloud] === 'true') { - return true; + //TODO: return to true once SC has the new provisioning mechanism in place + return false; } // SonarQube diff --git a/src/scanner-cli.ts b/src/scanner-cli.ts index ebe53aa0..78cca9ca 100644 --- a/src/scanner-cli.ts +++ b/src/scanner-cli.ts @@ -47,6 +47,11 @@ export function normalizePlatformName(): 'windows' | 'linux' | 'macosx' { export async function tryLocalSonarScannerExecutable(command: string): Promise { return new Promise(resolve => { log(LogLevel.INFO, `Trying to find a local install of the SonarScanner: ${command}`); + + if (!fsExtra.existsSync(command)) { + resolve(false); + return; + } const scannerProcess = spawn(command, ['-v'], { shell: isWindows() }); scannerProcess.on('exit', code => { @@ -66,6 +71,8 @@ export async function tryLocalSonarScannerExecutable(command: string): Promise log(LogLevel.ERROR, buffer.toString())); return new Promise((resolve, reject) => { - process.on('exit', code => { + child.on('exit', code => { if (code === 0) { log(LogLevel.INFO, 'SonarScanner CLI finished successfully'); resolve(); diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index fdcc9a28..94f01b42 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -87,7 +87,7 @@ describe('java', () => { ...MOCKED_PROPERTIES, [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', }), - ).toBe(true); + ).toBe(false); // TODO: return to true once SC has the new provisioning mechanism in place }); it(`should return true for SQ version >= ${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`, async () => { diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index b369c3ba..a96893bd 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -42,6 +42,22 @@ afterEach(() => { }); describe('getProperties', () => { + it('should provide default values', () => { + projectHandler.reset('fake_project_with_no_package_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties({}, projectHandler.getStartTime()); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'https://sonarcloud.io', + 'sonar.scanner.internal.isSonarCloud': 'true', + 'sonar.projectDescription': 'No description.', + 'sonar.sources': '.', + 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, + }); + }); + describe('should handle JS API scan options params correctly', () => { it('should detect custom SonarCloud endpoint', () => { projectHandler.reset('fake_project_with_no_package_file'); @@ -49,9 +65,9 @@ describe('getProperties', () => { const properties = getProperties( { - serverUrl: 'http://localhost/sonarqube', options: { 'sonar.projectKey': 'use-this-project-key', + 'sonar.scanner.sonarcloudUrl': 'https://dev.sc-dev.io', }, }, projectHandler.getStartTime(), @@ -59,17 +75,16 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), - 'sonar.host.url': 'http://localhost/sonarqube', - 'sonar.scanner.internal.isSonarCloud': 'false', + 'sonar.host.url': 'https://dev.sc-dev.io', + 'sonar.scanner.sonarcloudUrl': 'https://dev.sc-dev.io', + 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'use-this-project-key', 'sonar.projectDescription': 'No description.', 'sonar.sources': '.', 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, }); }); - }); - describe('should handle JS API scan options params correctly', () => { it('should detect and use user-provided scan option params', () => { projectHandler.reset('fake_project_with_sonar_properties_file'); projectHandler.setEnvironmentVariables({}); diff --git a/test/unit/scanner-cli.test.ts b/test/unit/scanner-cli.test.ts index 2e4e520c..3a4e3d23 100644 --- a/test/unit/scanner-cli.test.ts +++ b/test/unit/scanner-cli.test.ts @@ -17,10 +17,15 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import * as fsExtra from 'fs-extra'; import { spawn } from 'child_process'; import path from 'path'; import sinon from 'sinon'; -import { SCANNER_CLI_DEFAULT_BIN_NAME, SCANNER_CLI_INSTALL_PATH } from '../../src/constants'; +import { + SCANNER_CLI_DEFAULT_BIN_NAME, + SCANNER_CLI_INSTALL_PATH, + SCANNER_CLI_VERSION, +} from '../../src/constants'; import { extractArchive } from '../../src/file'; import { LogLevel, log } from '../../src/logging'; import { download } from '../../src/request'; @@ -33,6 +38,7 @@ import { import { ScannerProperty } from '../../src/types'; import { ChildProcessMock } from './mocks/ChildProcessMock'; +jest.mock('fs-extra'); jest.mock('child_process'); jest.mock('../../src/request'); jest.mock('../../src/file'); @@ -44,6 +50,7 @@ const MOCK_PROPERTIES = { [ScannerProperty.SonarToken]: 'token', [ScannerProperty.SonarHostUrl]: 'http://localhost:9000', [ScannerProperty.SonarUserHome]: 'path/to/user/home', + [ScannerProperty.SonarScannerCliVersion]: SCANNER_CLI_VERSION, }; beforeEach(() => { @@ -53,6 +60,7 @@ beforeEach(() => { describe('scanner-cli', () => { describe('tryLocalSonarScannerExecutable', () => { it('should detect locally installed scanner-cli', async () => { + jest.spyOn(fsExtra, 'existsSync').mockReturnValue(true); expect(await tryLocalSonarScannerExecutable(SCANNER_CLI_DEFAULT_BIN_NAME)).toBe(true); }); @@ -179,20 +187,21 @@ describe('scanner-cli', () => { }); it('should display SonarScanner CLI output', async () => { + jest.spyOn(process.stdout, 'write'); childProcessHandler.setOutput('the output', 'some error'); await runScannerCli({}, MOCK_PROPERTIES, 'sonar-scanner'); expect(log).toHaveBeenCalledWith(LogLevel.ERROR, 'some error'); - expect(log).toHaveBeenCalledWith(LogLevel.INFO, 'the output'); + expect(process.stdout.write).toHaveBeenCalledWith('the output'); }); it('should reject if SonarScanner CLI fails', async () => { childProcessHandler.setExitCode(1); - await expect(runScannerCli({}, MOCK_PROPERTIES, 'sonar-scanner')).rejects.toBeDefined(); - - expect(log).toHaveBeenCalledWith(LogLevel.ERROR, 'SonarScanner CLI failed with code 1'); + await expect(runScannerCli({}, MOCK_PROPERTIES, 'sonar-scanner')).rejects.toThrow( + 'SonarScanner CLI failed with code 1', + ); }); it('should pass proxy options to scanner', async () => { diff --git a/tsconfig.json b/tsconfig.json index b16944f7..875f4f6d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -49,7 +49,7 @@ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */, // "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ From ec96c64f1308954b1d2438e3639e417764027169 Mon Sep 17 00:00:00 2001 From: 7PH Date: Wed, 1 May 2024 13:46:19 +0200 Subject: [PATCH 23/35] SCANNPM-2 Use new endpoints & checksum/download logic & Do not use axios instance when fetching absolute URLs --- src/constants.ts | 11 ++-- src/file.ts | 16 +++--- src/java.ts | 78 +++++++++++++++----------- src/properties.ts | 30 ++++++---- src/request.ts | 54 ++++++++++++------ src/scanner-engine.ts | 25 +++------ src/types.ts | 35 +++++++----- test/unit/file.test.ts | 25 +++++---- test/unit/java.test.ts | 62 ++++++++++++++------- test/unit/properties.test.ts | 49 ++++++++++++---- test/unit/request.test.ts | 96 +++++++++++++++++++++----------- test/unit/scanner-cli.test.ts | 4 +- test/unit/scanner-engine.test.ts | 50 ++++++++++------- test/unit/utils.test.js | 2 +- 14 files changed, 337 insertions(+), 200 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 9df7b39f..90641226 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -24,9 +24,9 @@ export const SCANNER_BOOTSTRAPPER_NAME = 'ScannerNpm'; export const SONARCLOUD_URL = 'https://sonarcloud.io'; -export const SONARCLOUD_URL_REGEX = /^(https?:\/\/)?(www\.)?(sonarcloud\.io)/; +export const SONARCLOUD_API_BASE_URL = 'https://api.sonarcloud.io'; -export const SONARCLOUD_PRODUCTION_URL = 'https://sonarcloud.io'; +export const SONARCLOUD_URL_REGEX = /^(https?:\/\/)?(www\.)?(sonarcloud\.io)/; export const SONARQUBE_JRE_PROVISIONING_MIN_VERSION = '10.6'; @@ -50,10 +50,11 @@ export const SONAR_PROJECT_FILENAME = 'sonar-project.properties'; export const DEFAULT_SONAR_EXCLUSIONS = 'node_modules/**,bower_components/**,jspm_packages/**,typings/**,lib-cov/**'; -export const API_V2_VERSION_ENDPOINT = '/api/v2/analysis/version'; +export const API_V2_VERSION_ENDPOINT = '/analysis/version'; +export const API_V2_JRE_ENDPOINT = '/analysis/jres'; +export const API_V2_SCANNER_ENGINE_ENDPOINT = '/analysis/engine'; + export const API_OLD_VERSION_ENDPOINT = '/api/server/version'; -export const API_V2_JRE_ENDPOINT = '/api/v2/analysis/jres'; -export const API_V2_SCANNER_ENGINE_ENDPOINT = '/api/v2/analysis/engine'; export const SCANNER_CLI_DEFAULT_BIN_NAME = 'sonar-scanner'; export const SCANNER_CLI_VERSION = '5.0.1.3006'; diff --git a/src/file.ts b/src/file.ts index dbf04d58..0b24a775 100644 --- a/src/file.ts +++ b/src/file.ts @@ -26,14 +26,14 @@ import path from 'path'; import tarStream from 'tar-stream'; import zlib from 'zlib'; import { SONAR_CACHE_DIR, UNARCHIVE_SUFFIX } from './constants'; -import { log, LogLevel } from './logging'; +import { LogLevel, log } from './logging'; import { CacheFileData, ScannerProperties, ScannerProperty } from './types'; export async function getCacheFileLocation( properties: ScannerProperties, - { md5, filename }: CacheFileData, + { checksum, filename }: CacheFileData, ) { - const filePath = path.join(getParentCacheDirectory(properties), md5, filename); + const filePath = path.join(getParentCacheDirectory(properties), checksum, filename); if (fs.existsSync(filePath)) { log(LogLevel.INFO, 'Found Cached: ', filePath); return filePath; @@ -71,7 +71,7 @@ export async function extractArchive(fromPath: string, toPath: string) { extract.on('error', err => { log(LogLevel.ERROR, 'Error extracting tar.gz', err); - reject(err); + reject(err as Error); }); }); @@ -94,7 +94,7 @@ async function generateChecksum(filepath: string) { reject(err); return; } - resolve(crypto.createHash('md5').update(data).digest('hex')); + resolve(crypto.createHash('sha256').update(data).digest('hex')); }); }); } @@ -117,12 +117,12 @@ export async function validateChecksum(filePath: string, expectedChecksum: strin export async function getCacheDirectories( properties: ScannerProperties, - { md5, filename }: CacheFileData, + { checksum, filename }: CacheFileData, ) { - const archivePath = path.join(getParentCacheDirectory(properties), md5, filename); + const archivePath = path.join(getParentCacheDirectory(properties), checksum, filename); const unarchivePath = path.join( getParentCacheDirectory(properties), - md5, + checksum, filename + UNARCHIVE_SUFFIX, ); diff --git a/src/java.ts b/src/java.ts index c08ff05b..63192c07 100644 --- a/src/java.ts +++ b/src/java.ts @@ -35,14 +35,19 @@ import { } from './file'; import { LogLevel, log } from './logging'; import { download, fetch } from './request'; -import { ScannerProperties, ScannerProperty } from './types'; +import { + AnalysisJreMetaData, + AnalysisJresResponseType, + ScannerProperties, + ScannerProperty, +} from './types'; -export async function fetchServerVersion(): Promise { +export async function fetchServerVersion(properties: ScannerProperties): Promise { let version: SemVer | null = null; try { // Try and fetch the new version endpoint first log(LogLevel.DEBUG, `Fetching API V2 ${API_V2_VERSION_ENDPOINT}`); - const response = await fetch({ + const response = await fetch({ url: API_V2_VERSION_ENDPOINT, }); version = semver.coerce(response.data); @@ -53,8 +58,8 @@ export async function fetchServerVersion(): Promise { LogLevel.DEBUG, `Unable to fetch API V2 ${API_V2_VERSION_ENDPOINT}: ${error}. Falling back on ${API_OLD_VERSION_ENDPOINT}`, ); - const response = await fetch({ - url: API_OLD_VERSION_ENDPOINT, + const response = await fetch({ + url: `${properties[ScannerProperty.SonarHostUrl]}${API_OLD_VERSION_ENDPOINT}`, }); version = semver.coerce(response.data); } catch (error: unknown) { @@ -73,9 +78,9 @@ export async function fetchServerVersion(): Promise { } export async function serverSupportsJREProvisioning( - parameters: ScannerProperties, + properties: ScannerProperties, ): Promise { - if (parameters[ScannerProperty.SonarScannerInternalIsSonarCloud] === 'true') { + if (properties[ScannerProperty.SonarScannerInternalIsSonarCloud] === 'true') { //TODO: return to true once SC has the new provisioning mechanism in place return false; } @@ -83,8 +88,8 @@ export async function serverSupportsJREProvisioning( // SonarQube log(LogLevel.DEBUG, 'Detecting SonarQube server version'); const SQServerInfo = - semver.coerce(parameters[ScannerProperty.SonarScannerInternalSqVersion]) ?? - (await fetchServerVersion()); + semver.coerce(properties[ScannerProperty.SonarScannerInternalSqVersion]) ?? + (await fetchServerVersion(properties)); log(LogLevel.INFO, 'SonarQube server version: ', SQServerInfo.version); const supports = semver.satisfies(SQServerInfo, `>=${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`); @@ -94,43 +99,44 @@ export async function serverSupportsJREProvisioning( export async function fetchJRE(properties: ScannerProperties): Promise { log(LogLevel.DEBUG, 'Detecting latest version of JRE'); - const latestJREData = await fetchLatestSupportedJRE(properties); - log(LogLevel.INFO, 'Latest Supported JRE: ', latestJREData); + const jreMetaData = await fetchLatestSupportedJRE(properties); + log(LogLevel.INFO, 'Latest Supported JRE: ', jreMetaData); log(LogLevel.DEBUG, 'Looking for Cached JRE'); - const cachedJRE = await getCacheFileLocation(properties, { - md5: latestJREData.md5, - filename: latestJREData.filename + UNARCHIVE_SUFFIX, + const cachedJrePath = await getCacheFileLocation(properties, { + checksum: jreMetaData.sha256, + filename: jreMetaData.filename + UNARCHIVE_SUFFIX, }); - if (cachedJRE) { + properties[ScannerProperty.SonarScannerWasJreCacheHit] = Boolean(cachedJrePath).toString(); + if (cachedJrePath) { log(LogLevel.INFO, 'Using Cached JRE'); - properties[ScannerProperty.SonarScannerWasJRECacheHit] = 'true'; - - return path.join(cachedJRE, latestJREData.javaPath); - } else { - const { archivePath, unarchivePath: jreDirPath } = await getCacheDirectories( - properties, - latestJREData, - ); - - await download(`${API_V2_JRE_ENDPOINT}/${latestJREData.filename}`, archivePath); - log(LogLevel.INFO, `Downloaded JRE to ${archivePath}`); + return path.join(cachedJrePath, jreMetaData.javaPath); + } - await validateChecksum(archivePath, latestJREData.md5); + // JRE not found in cache. Download it. + const { archivePath, unarchivePath: jreDirPath } = await getCacheDirectories(properties, { + checksum: jreMetaData.sha256, + filename: jreMetaData.filename, + }); - await extractArchive(archivePath, jreDirPath); + // If the JRE has a download URL, download it + const url = jreMetaData.downloadUrl ?? `${API_V2_JRE_ENDPOINT}/${jreMetaData.id}`; - return path.join(jreDirPath, latestJREData.javaPath); - } + await download(url, archivePath); + await validateChecksum(archivePath, jreMetaData.sha256); + await extractArchive(archivePath, jreDirPath); + return path.join(jreDirPath, jreMetaData.javaPath); } -async function fetchLatestSupportedJRE(properties: ScannerProperties) { +async function fetchLatestSupportedJRE( + properties: ScannerProperties, +): Promise { const os = properties[ScannerProperty.SonarScannerOs]; const arch = properties[ScannerProperty.SonarScannerArch]; log(LogLevel.DEBUG, `Downloading JRE for ${os} ${arch} from ${API_V2_JRE_ENDPOINT}`); - const { data } = await fetch({ + const { data } = await fetch({ url: API_V2_JRE_ENDPOINT, params: { os, @@ -138,6 +144,10 @@ async function fetchLatestSupportedJRE(properties: ScannerProperties) { }, }); - log(LogLevel.DEBUG, 'JRE information: ', data); - return data; + if (data.length === 0) { + throw new Error(`No JREs available for your platform ${os} ${arch}`); + } + + log(LogLevel.DEBUG, 'JRE Information', data); + return data[0]; } diff --git a/src/properties.ts b/src/properties.ts index f00b35eb..602ecb08 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -27,6 +27,7 @@ import { ENV_TO_PROPERTY_NAME, ENV_VAR_PREFIX, SCANNER_BOOTSTRAPPER_NAME, + SONARCLOUD_API_BASE_URL, SONARCLOUD_URL, SONARCLOUD_URL_REGEX, SONAR_DIR_DEFAULT, @@ -34,7 +35,7 @@ import { } from './constants'; import { LogLevel, log } from './logging'; import { getArch, getSupportedOS } from './platform'; -import { ScanOptions, ScannerProperties, ScannerProperty, CliArgs } from './types'; +import { CliArgs, ScanOptions, ScannerProperties, ScannerProperty } from './types'; function getDefaultProperties(): ScannerProperties { return { @@ -191,8 +192,8 @@ function getSonarFileProperties(projectBaseDir: string): ScannerProperties { } return properties; - } catch (error: any) { - log(LogLevel.WARN, `Failed to read ${SONAR_PROJECT_FILENAME} file: ${error.message}`); + } catch (error) { + log(LogLevel.WARN, `Failed to read ${SONAR_PROJECT_FILENAME} file: ${error}`); throw error; } } @@ -292,19 +293,26 @@ function getBootstrapperProperties(startTimestampMs: number): ScannerProperties * Get endpoint properties from scanner properties. */ export function getHostProperties(properties: ScannerProperties): ScannerProperties { - const sonarHostUrl = properties[ScannerProperty.SonarHostUrl]; - const sonarCloudUrl = properties[ScannerProperty.SonarScannerSonarCloudURL]; + const sonarHostUrl = properties[ScannerProperty.SonarHostUrl]?.replace(/\/$/, ''); + const sonarApiBaseUrl = properties[ScannerProperty.SonarScannerApiBaseUrl]; + const sonarCloudSpecified = + properties[ScannerProperty.SonarScannerSonarCloudUrl] === sonarHostUrl || + SONARCLOUD_URL_REGEX.exec(sonarHostUrl ?? ''); - if (!sonarHostUrl || SONARCLOUD_URL_REGEX.exec(sonarHostUrl)) { + if (!sonarHostUrl || sonarCloudSpecified) { return { [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', - [ScannerProperty.SonarHostUrl]: sonarCloudUrl ? sonarCloudUrl : SONARCLOUD_URL, + [ScannerProperty.SonarHostUrl]: + properties[ScannerProperty.SonarScannerSonarCloudUrl] ?? SONARCLOUD_URL, + [ScannerProperty.SonarScannerApiBaseUrl]: sonarApiBaseUrl ?? SONARCLOUD_API_BASE_URL, + }; + } else { + return { + [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'false', + [ScannerProperty.SonarHostUrl]: sonarHostUrl, + [ScannerProperty.SonarScannerApiBaseUrl]: sonarApiBaseUrl ?? `${sonarHostUrl}/api/v2`, }; } - return { - [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'false', - [ScannerProperty.SonarHostUrl]: sonarHostUrl, - }; } function getHttpProxyEnvProperties(serverUrl: string): ScannerProperties { diff --git a/src/request.ts b/src/request.ts index 8800b47e..8ca6f0e3 100644 --- a/src/request.ts +++ b/src/request.ts @@ -30,8 +30,14 @@ import { ScannerProperties, ScannerProperty } from './types'; const finished = promisify(stream.finished); -// The axios instance is private to this module -let _axiosInstance: AxiosInstance | null = null; +/** + * Axios instances (private to this module). + * One for sonar host (with auth), one for external requests + */ +let _axiosInstances: { + internal: AxiosInstance; + external: AxiosInstance; +} | null = null; async function extractTruststoreCerts(p12Base64: string, password: string = ''): Promise { // P12/PFX file -> DER -> ASN.1 -> PKCS12 @@ -95,37 +101,51 @@ export async function getHttpAgents( return agents; } +export function resetAxios() { + _axiosInstances = null; +} + export async function initializeAxios(properties: ScannerProperties) { const token = properties[ScannerProperty.SonarToken]; - const baseURL = properties[ScannerProperty.SonarHostUrl]; + const baseURL = properties[ScannerProperty.SonarScannerApiBaseUrl]; const agents = await getHttpAgents(properties); const timeout = Math.floor(parseInt(properties[ScannerProperty.SonarScannerResponseTimeout], 10) || 0) * 1000; - if (!_axiosInstance) { - _axiosInstance = axios.create({ - baseURL, - headers: { - Authorization: `Bearer ${token}`, - }, - timeout, - ...agents, - }); + if (!_axiosInstances) { + _axiosInstances = { + internal: axios.create({ + baseURL, + headers: { + Authorization: `Bearer ${token}`, + }, + timeout, + ...agents, + }), + external: axios.create({ + timeout, + ...agents, + }), + }; } } -export function fetch(config: AxiosRequestConfig) { - if (!_axiosInstance) { +export function fetch(config: AxiosRequestConfig) { + if (!_axiosInstances) { throw new Error('Axios instance is not initialized'); } - - return _axiosInstance.request(config); + // Use external instance for absolute URLs + if (!config.url?.startsWith('/')) { + log(LogLevel.DEBUG, `Not using axios instance for ${config.url}`); + return _axiosInstances.external.request(config); + } + return _axiosInstances.internal.request(config); } export async function download(url: string, destPath: string) { log(LogLevel.DEBUG, `Downloading ${url} to ${destPath}`); - const response = await fetch({ + const response = await fetch({ url, method: 'GET', responseType: 'stream', diff --git a/src/scanner-engine.ts b/src/scanner-engine.ts index 0113ddb0..ef9b2ea6 100644 --- a/src/scanner-engine.ts +++ b/src/scanner-engine.ts @@ -19,6 +19,7 @@ */ import { spawn } from 'child_process'; import fs from 'fs'; +import { API_V2_SCANNER_ENGINE_ENDPOINT } from './constants'; import { extractArchive, getCacheDirectories, @@ -29,7 +30,7 @@ import { LogLevel, log, logWithPrefix } from './logging'; import { proxyUrlToJavaOptions } from './proxy'; import { download, fetch } from './request'; import { - CacheFileData, + AnalysisEngineResponseType, ScanOptions, ScannerLogEntry, ScannerProperties, @@ -38,19 +39,12 @@ import { export async function fetchScannerEngine(properties: ScannerProperties) { log(LogLevel.DEBUG, 'Detecting latest version of Scanner Engine'); - const { data } = await fetch({ - // TODO: replace with /api/v2/analysis/engine - url: '/batch/index', - }); - const [filename, md5] = data.trim().split('|'); + const { data } = await fetch({ url: API_V2_SCANNER_ENGINE_ENDPOINT }); + const { sha256: checksum, filename, downloadUrl } = data; log(LogLevel.INFO, 'Latest Supported Scanner Engine: ', filename); log(LogLevel.DEBUG, 'Looking for Cached Scanner Engine'); - - // TODO: use sha256 instead of md5 - const cacheFileData: CacheFileData = { md5, filename }; - const cachedScannerEngine = await getCacheFileLocation(properties, cacheFileData); - + const cachedScannerEngine = await getCacheFileLocation(properties, { checksum, filename }); if (cachedScannerEngine) { log(LogLevel.INFO, 'Using Cached Scanner Engine'); properties[ScannerProperty.SonarScannerWasEngineCacheHit] = 'true'; @@ -61,16 +55,15 @@ export async function fetchScannerEngine(properties: ScannerProperties) { properties[ScannerProperty.SonarScannerWasEngineCacheHit] = 'false'; const { archivePath, unarchivePath: scannerEnginePath } = await getCacheDirectories(properties, { - md5, + checksum, filename, }); - - // TODO: replace with /api/v2/analysis/engine/ + const url = downloadUrl ?? API_V2_SCANNER_ENGINE_ENDPOINT; log(LogLevel.DEBUG, `Starting download of Scanner Engine`); - await download(`/batch/file?name=${filename}`, archivePath); + await download(url, archivePath); log(LogLevel.INFO, `Downloaded Scanner Engine to ${scannerEnginePath}`); - await validateChecksum(archivePath, md5); + await validateChecksum(archivePath, checksum); log(LogLevel.INFO, `Extracting Scanner Engine to ${scannerEnginePath}`); await extractArchive(archivePath, scannerEnginePath); diff --git a/src/types.ts b/src/types.ts index e6273d03..07185b3f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,17 +19,7 @@ */ import { LogLevel } from './logging'; -export type JreMetaData = { - filename: string; - md5: string; - javaPath: string; -}; - -export type CacheFileData = { md5: string; filename: string }; - -export type JREFullData = JreMetaData & { - jrePath: string; -}; +export type CacheFileData = { checksum: string; filename: string }; export type ScannerLogEntry = { level: LogLevel; @@ -44,13 +34,14 @@ export enum ScannerProperty { SonarExclusions = 'sonar.exclusions', SonarHostUrl = 'sonar.host.url', SonarUserHome = 'sonar.userHome', + SonarScannerApiBaseUrl = 'sonar.scanner.apiBaseUrl', SonarScannerOs = 'sonar.scanner.os', SonarScannerArch = 'sonar.scanner.arch', SonarOrganization = 'sonar.organization', SonarProjectBaseDir = 'sonar.projectBaseDir', - SonarScannerSonarCloudURL = 'sonar.scanner.sonarcloudUrl', + SonarScannerSonarCloudUrl = 'sonar.scanner.sonarcloudUrl', SonarScannerJavaExePath = 'sonar.scanner.javaExePath', - SonarScannerWasJRECacheHit = 'sonar.scanner.wasJRECacheHit', + SonarScannerWasJreCacheHit = 'sonar.scanner.wasJreCacheHit', SonarScannerWasEngineCacheHit = 'sonar.scanner.wasEngineCacheHit', SonarScannerProxyHost = 'sonar.scanner.proxyHost', SonarScannerProxyPort = 'sonar.scanner.proxyPort', @@ -88,3 +79,21 @@ export type CliArgs = { debug?: boolean; define?: string[]; }; + +export type AnalysisJreMetaData = { + id: string; + filename: string; + sha256: string; + javaPath: string; + os: string; + arch: string; + downloadUrl?: string; +}; + +export type AnalysisJresResponseType = AnalysisJreMetaData[]; + +export type AnalysisEngineResponseType = { + filename: string; + sha256: string; + downloadUrl?: string; +}; diff --git a/test/unit/file.test.ts b/test/unit/file.test.ts index 0e4841b3..0d376be6 100644 --- a/test/unit/file.test.ts +++ b/test/unit/file.test.ts @@ -20,9 +20,10 @@ import AdmZip from 'adm-zip'; import fs from 'fs'; import path from 'path'; +import { PassThrough } from 'stream'; import * as tarStream from 'tar-stream'; import * as zlib from 'zlib'; -import { PassThrough } from 'stream'; +import { SONAR_CACHE_DIR } from '../../src/constants'; import { extractArchive, getCacheDirectories, @@ -30,7 +31,6 @@ import { validateChecksum, } from '../../src/file'; import { ScannerProperty } from '../../src/types'; -import { SONAR_CACHE_DIR } from '../../src/constants'; const MOCKED_PROPERTIES = { [ScannerProperty.SonarUserHome]: '/path/to/sonar/user/home', @@ -145,29 +145,29 @@ describe('file', () => { describe('getCacheFileLocation', () => { it('should return the file path if the file exists', async () => { - const md5 = 'md5hash'; + const checksum = 'shahash'; const filename = 'file.txt'; const filePath = path.join( MOCKED_PROPERTIES[ScannerProperty.SonarUserHome], SONAR_CACHE_DIR, - md5, + checksum, filename, ); jest.spyOn(fs, 'existsSync').mockReturnValue(true); - const result = await getCacheFileLocation(MOCKED_PROPERTIES, { md5, filename }); + const result = await getCacheFileLocation(MOCKED_PROPERTIES, { checksum, filename }); expect(result).toEqual(filePath); }); it('should return null if the file does not exist', async () => { - const md5 = 'md5hash'; + const checksum = 'shahash'; const filename = 'file.txt'; jest.spyOn(fs, 'existsSync').mockReturnValue(false); - const result = await getCacheFileLocation(MOCKED_PROPERTIES, { md5, filename }); + const result = await getCacheFileLocation(MOCKED_PROPERTIES, { checksum, filename }); expect(result).toBeNull(); }); @@ -179,7 +179,10 @@ describe('file', () => { .spyOn(fs, 'readFile') .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); - await validateChecksum('path/to/file', 'd10b4c3ff123b26dc068d43a8bef2d23'); + await validateChecksum( + 'path/to/file', + 'e0ac3601005dfa1864f5392aabaf7d898b1b5bab854f1acb4491bcd806b76b0c', + ); expect(fs.readFile).toHaveBeenCalledWith('path/to/file', expect.any(Function)); }); @@ -190,7 +193,7 @@ describe('file', () => { .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); await expect(validateChecksum('path/to/file', 'invalidchecksum')).rejects.toThrow( - 'Checksum verification failed for path/to/file. Expected checksum invalidchecksum but got d10b4c3ff123b26dc068d43a8bef2d23', + 'Checksum verification failed for path/to/file. Expected checksum invalidchecksum but got e0ac3601005dfa1864f5392aabaf7d898b1b5bab854f1acb4491bcd806b76b0c', ); }); @@ -212,7 +215,7 @@ describe('file', () => { jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => true); jest.spyOn(fs, 'mkdirSync'); const { archivePath, unarchivePath } = await getCacheDirectories(MOCKED_PROPERTIES, { - md5: 'md5_test', + checksum: 'md5_test', filename: 'file.txt', }); @@ -225,7 +228,7 @@ describe('file', () => { it('should create the parent cache directory if it does not exist', async () => { jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => false); jest.spyOn(fs, 'mkdirSync').mockImplementationOnce(() => undefined); - await getCacheDirectories(MOCKED_PROPERTIES, { md5: 'md5_test', filename: 'file.txt' }); + await getCacheDirectories(MOCKED_PROPERTIES, { checksum: 'md5_test', filename: 'file.txt' }); expect(fs.existsSync).toHaveBeenCalledWith('/path/to/sonar/user/home/cache/md5_test'); expect(fs.mkdirSync).toHaveBeenCalledWith('/path/to/sonar/user/home/cache/md5_test', { diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index 94f01b42..d1486414 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -25,7 +25,7 @@ import { API_V2_JRE_ENDPOINT, SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../ import * as file from '../../src/file'; import { fetchJRE, fetchServerVersion, serverSupportsJREProvisioning } from '../../src/java'; import * as request from '../../src/request'; -import { JreMetaData, ScannerProperties, ScannerProperty } from '../../src/types'; +import { AnalysisJresResponseType, ScannerProperties, ScannerProperty } from '../../src/types'; const mock = new MockAdapter(axios); @@ -46,36 +46,38 @@ beforeEach(() => { describe('java', () => { describe('version should be detected correctly', () => { it('the SonarQube version should be fetched correctly when new endpoint does not exist', async () => { - mock.onGet('/api/server/version').reply(200, '3.2.2'); + mock.onGet('http://sonarqube.com/api/server/version').reply(200, '3.2.2'); mock.onGet('/api/v2/analysis/version').reply(404, 'Not Found'); - const serverSemver = await fetchServerVersion(); + const serverSemver = await fetchServerVersion(MOCKED_PROPERTIES); expect(serverSemver.toString()).toEqual('3.2.2'); expect(request.fetch).toHaveBeenCalledTimes(2); }); it('the SonarQube version should be fetched correctly using the new endpoint', async () => { - mock.onGet('/api/server/version').reply(200, '3.2.1.12313'); + mock.onGet('http://sonarqube.com/api/server/version').reply(200, '3.2.1.12313'); - const serverSemver = await fetchServerVersion(); + const serverSemver = await fetchServerVersion(MOCKED_PROPERTIES); expect(serverSemver.toString()).toEqual('3.2.1'); }); it('should fail if both endpoints do not work', async () => { - mock.onGet('/api/server/version').reply(404, 'Not Found'); + mock.onGet('http://sonarqube.com/api/server/version').reply(404, 'Not Found'); mock.onGet('/api/v2/server/version').reply(404, 'Not Found'); expect(async () => { - await fetchServerVersion(); + await fetchServerVersion(MOCKED_PROPERTIES); }).rejects.toBeDefined(); }); it('should fail if version can not be parsed', async () => { - mock.onGet('/api/server/version').reply(200, 'FORBIDDEN'); + mock + .onGet('http://sonarqube.com/api/server/version') + .reply(200, 'FORBIDDEN'); expect(async () => { - await fetchServerVersion(); + await fetchServerVersion(MOCKED_PROPERTIES); }).rejects.toBeDefined(); }); }); @@ -91,26 +93,30 @@ describe('java', () => { }); it(`should return true for SQ version >= ${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`, async () => { - mock.onGet('/api/server/version').reply(200, '10.5.0'); + mock.onGet('http://sonarqube.com/api/server/version').reply(200, '10.6.0.2424'); expect(await serverSupportsJREProvisioning(MOCKED_PROPERTIES)).toBe(true); }); it(`should return false for SQ version < ${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`, async () => { // Define the behavior of the GET request - mock.onGet('/api/server/version').reply(200, '9.9.9'); + mock.onGet('http://sonarqube.com/api/server/version').reply(200, '9.9.9'); expect(await serverSupportsJREProvisioning(MOCKED_PROPERTIES)).toBe(false); }); }); describe('when JRE provisioning is supported', () => { - const serverResponse: JreMetaData = { - filename: 'mock-jre.tar.gz', - javaPath: 'jre/bin/java', - md5: 'd41d8cd98f00b204e9800998ecf8427e', - }; + const serverResponse: AnalysisJresResponseType = [ + { + id: 'some-id', + filename: 'mock-jre.tar.gz', + javaPath: 'jre/bin/java', + sha256: 'd41d8cd98f00b204e9800998ecf8427e', + arch: 'arm64', + os: 'linux', + }, + ]; beforeEach(() => { jest.spyOn(file, 'getCacheFileLocation').mockResolvedValue('mocked/path/to/file'); - jest.spyOn(file, 'extractArchive').mockResolvedValue(undefined); mock @@ -123,7 +129,7 @@ describe('java', () => { .reply(200, serverResponse); mock - .onGet(`${API_V2_JRE_ENDPOINT}/${serverResponse.filename}`) + .onGet(`${API_V2_JRE_ENDPOINT}/${serverResponse[0].id}`) .reply(200, fs.createReadStream(path.resolve(__dirname, '../unit/mocks/mock-jre.tar.gz'))); }); @@ -134,7 +140,7 @@ describe('java', () => { expect(request.fetch).toHaveBeenCalledTimes(1); expect(request.download).not.toHaveBeenCalled(); - // check for the cache + // Check for the cache expect(file.getCacheFileLocation).toHaveBeenCalledTimes(1); expect(file.extractArchive).not.toHaveBeenCalled(); @@ -167,7 +173,7 @@ describe('java', () => { expect(file.getCacheFileLocation).toHaveBeenCalledTimes(1); expect(request.download).toHaveBeenCalledWith( - `${API_V2_JRE_ENDPOINT}/${serverResponse.filename}`, + `${API_V2_JRE_ENDPOINT}/${serverResponse[0].id}`, mockCacheDirectories.archivePath, ); @@ -175,6 +181,22 @@ describe('java', () => { expect(file.extractArchive).toHaveBeenCalledTimes(1); }); + + it('should fail if no JRE matches', async () => { + mock + .onGet(API_V2_JRE_ENDPOINT, { + params: { + os: MOCKED_PROPERTIES[ScannerProperty.SonarScannerOs], + arch: MOCKED_PROPERTIES[ScannerProperty.SonarScannerArch], + }, + }) + .reply(200, []); + + // Check that it rejects with a specific error + expect(fetchJRE({ ...MOCKED_PROPERTIES })).rejects.toThrowError( + 'No JREs available for your platform linux arm64', + ); + }); }); }); }); diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index a96893bd..c9df8206 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -21,6 +21,7 @@ import sinon from 'sinon'; import { DEFAULT_SONAR_EXCLUSIONS, SCANNER_BOOTSTRAPPER_NAME, + SONARCLOUD_API_BASE_URL, SONARCLOUD_URL, } from '../../src/constants'; import { LogLevel, log } from '../../src/logging'; @@ -50,7 +51,8 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), - 'sonar.host.url': 'https://sonarcloud.io', + 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.apiBaseUrl': SONARCLOUD_API_BASE_URL, 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectDescription': 'No description.', 'sonar.sources': '.', @@ -67,7 +69,7 @@ describe('getProperties', () => { { options: { 'sonar.projectKey': 'use-this-project-key', - 'sonar.scanner.sonarcloudUrl': 'https://dev.sc-dev.io', + 'sonar.scanner.apiBaseUrl': 'https://dev.sc-dev.io', }, }, projectHandler.getStartTime(), @@ -75,8 +77,8 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), - 'sonar.host.url': 'https://dev.sc-dev.io', - 'sonar.scanner.sonarcloudUrl': 'https://dev.sc-dev.io', + 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.apiBaseUrl': 'https://dev.sc-dev.io', 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'use-this-project-key', 'sonar.projectDescription': 'No description.', @@ -105,6 +107,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.token': 'dummy-token', 'sonar.verbose': 'true', @@ -147,6 +150,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.javascript.lcov.reportPaths': 'coverage/lcov.info', 'sonar.projectKey': 'fake-basic-project', @@ -174,6 +178,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'fake-project', 'sonar.projectName': 'fake-project', @@ -202,6 +207,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectDescription': 'No description.', 'sonar.sources': '.', @@ -223,6 +229,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectDescription': 'No description.', 'sonar.sources': '.', @@ -247,6 +254,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.javascript.lcov.reportPaths': 'jest-coverage/lcov.info', 'sonar.projectKey': 'fake-basic-project', @@ -272,6 +280,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.javascript.lcov.reportPaths': 'nyc-coverage/lcov.info', 'sonar.projectKey': 'fake-basic-project', @@ -312,6 +321,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', @@ -335,7 +345,8 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), - 'sonar.host.url': 'https://sonarqube.com/', + 'sonar.host.url': 'https://sonarqube.com', + 'sonar.scanner.apiBaseUrl': 'https://sonarqube.com/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', @@ -358,6 +369,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.apiBaseUrl': SONARCLOUD_API_BASE_URL, 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', @@ -382,6 +394,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': `${protocol}://localhost/sonarqube`, + 'sonar.scanner.apiBaseUrl': `${protocol}://localhost/sonarqube/api/v2`, 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', @@ -407,6 +420,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.apiBaseUrl': SONARCLOUD_API_BASE_URL, 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', @@ -427,6 +441,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.apiBaseUrl': SONARCLOUD_API_BASE_URL, 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', @@ -452,6 +467,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.apiBaseUrl': SONARCLOUD_API_BASE_URL, 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', @@ -482,6 +498,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', @@ -522,6 +539,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'used', 'sonar.projectName': 'Foo', @@ -568,6 +586,7 @@ describe('getProperties', () => { expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', @@ -595,12 +614,13 @@ describe('getProperties', () => { }, }, projectHandler.getStartTime(), - ['-Dsonar.scanner.proxyHost=use-this-proxy.io'], + { define: ['sonar.scanner.proxyHost=use-this-proxy.io'] }, ); expect(properties).toEqual({ ...projectHandler.getExpectedProperties(), 'sonar.host.url': `${protocol}://localhost/sonarqube`, + 'sonar.scanner.apiBaseUrl': `${protocol}://localhost/sonarqube/api/v2`, 'sonar.scanner.internal.isSonarCloud': 'false', 'sonar.projectKey': 'foo', 'sonar.projectName': 'Foo', @@ -614,10 +634,11 @@ describe('getProperties', () => { }); describe('addHostProperties', () => { - it('should detect SonarCloud', () => { + it('should detect SonarCloud by default', () => { const expected = { [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, + [ScannerProperty.SonarScannerApiBaseUrl]: SONARCLOUD_API_BASE_URL, }; // SonarCloud used by default @@ -626,7 +647,7 @@ describe('addHostProperties', () => { // Backward-compatible use-case expect( getHostProperties({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, }), ).toEqual(expected); @@ -647,13 +668,15 @@ describe('addHostProperties', () => { it('should detect SonarCloud with custom URL', () => { const endpoint = getHostProperties({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io/', - [ScannerProperty.SonarScannerSonarCloudURL]: 'http://that-is-a-sonarcloud-custom-url.com', + [ScannerProperty.SonarHostUrl]: 'http://that-is-a-sonarcloud-custom-url.com', + [ScannerProperty.SonarScannerSonarCloudUrl]: 'http://that-is-a-sonarcloud-custom-url.com', + [ScannerProperty.SonarScannerApiBaseUrl]: 'http://api.that-is-a-sonarcloud-custom-url.com', }); expect(endpoint).toEqual({ [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', [ScannerProperty.SonarHostUrl]: 'http://that-is-a-sonarcloud-custom-url.com', + [ScannerProperty.SonarScannerApiBaseUrl]: 'http://api.that-is-a-sonarcloud-custom-url.com', }); }); @@ -665,18 +688,20 @@ describe('addHostProperties', () => { expect(endpoint).toEqual({ [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'false', [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', + [ScannerProperty.SonarScannerApiBaseUrl]: 'https://next.sonarqube.com/api/v2', }); }); it('should ignore SonarCloud custom URL if sonar host URL does not match sonarcloud', () => { const endpoint = getHostProperties({ [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', - [ScannerProperty.SonarScannerSonarCloudURL]: 'http://that-is-a-sonarcloud-custom-url.com', + [ScannerProperty.SonarScannerSonarCloudUrl]: 'http://that-is-a-sonarcloud-custom-url.com', }); expect(endpoint).toEqual({ [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'false', [ScannerProperty.SonarHostUrl]: 'https://next.sonarqube.com', + [ScannerProperty.SonarScannerApiBaseUrl]: 'https://next.sonarqube.com/api/v2', }); }); }); diff --git a/test/unit/request.test.ts b/test/unit/request.test.ts index 11d39d63..d7679172 100644 --- a/test/unit/request.test.ts +++ b/test/unit/request.test.ts @@ -17,20 +17,23 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import axios from 'axios'; +import axios, { AxiosInstance } from 'axios'; import fs from 'fs'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import path from 'path'; +import { SONARCLOUD_API_BASE_URL, SONARCLOUD_URL } from '../../src/constants'; import * as logging from '../../src/logging'; -import { fetch, getHttpAgents, initializeAxios } from '../../src/request'; -import { ScannerProperties, ScannerProperty } from '../../src/types'; +import { fetch, getHttpAgents, initializeAxios, resetAxios } from '../../src/request'; +import { ScannerProperty } from '../../src/types'; jest.mock('axios', () => ({ - create: jest.fn(), + create: jest.fn().mockReturnValue({ request: jest.fn() }), + request: jest.fn(), })); beforeEach(() => { jest.clearAllMocks(); + resetAxios(); }); describe('request', () => { @@ -38,7 +41,7 @@ describe('request', () => { describe('with proxy options', () => { it('should define proxy url correctly', async () => { const agents = await getHttpAgents({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, [ScannerProperty.SonarScannerProxyHost]: 'proxy.com', }); expect(agents.httpAgent).toBeInstanceOf(HttpProxyAgent); @@ -49,7 +52,7 @@ describe('request', () => { it('should not define agents when no proxy is provided', async () => { const agents = await getHttpAgents({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, }); expect(agents.httpAgent).toBeUndefined(); expect(agents.httpsAgent).toBeUndefined(); @@ -67,7 +70,7 @@ describe('request', () => { const certificatePem = fs.readFileSync(certificatePath).toString().replace(/\n/g, '\r\n'); const { httpsAgent } = await getHttpAgents({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, [ScannerProperty.SonarScannerTruststorePath]: truststorePath, [ScannerProperty.SonarScannerTruststorePassword]: truststorePass, }); @@ -85,7 +88,7 @@ describe('request', () => { const truststorePass = 'password'; const { httpsAgent } = await getHttpAgents({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, [ScannerProperty.SonarScannerTruststorePath]: truststorePath, [ScannerProperty.SonarScannerTruststorePassword]: truststorePass, }); @@ -103,7 +106,7 @@ describe('request', () => { const truststorePass = 'password'; const { httpsAgent } = await getHttpAgents({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, [ScannerProperty.SonarScannerTruststorePath]: truststorePath, [ScannerProperty.SonarScannerTruststorePassword]: truststorePass, }); @@ -118,7 +121,7 @@ describe('request', () => { const keystorePass = 'password'; const { httpsAgent } = await getHttpAgents({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, [ScannerProperty.SonarScannerKeystorePath]: keystorePath, [ScannerProperty.SonarScannerKeystorePassword]: keystorePass, }); @@ -138,7 +141,7 @@ describe('request', () => { const keystorePass = 'password'; const { httpsAgent } = await getHttpAgents({ - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, [ScannerProperty.SonarScannerProxyHost]: 'proxy.com', [ScannerProperty.SonarScannerTruststorePath]: truststorePath, [ScannerProperty.SonarScannerTruststorePassword]: truststorePass, @@ -159,47 +162,80 @@ describe('request', () => { it('should initialize axios', async () => { jest.spyOn(axios, 'create'); - const properties: ScannerProperties = { - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + await initializeAxios({ + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, + [ScannerProperty.SonarScannerApiBaseUrl]: SONARCLOUD_API_BASE_URL, [ScannerProperty.SonarToken]: 'testToken', - }; - - await initializeAxios(properties); + }); - expect(axios.create).toHaveBeenCalledTimes(1); + expect(axios.create).toHaveBeenCalledTimes(2); expect(axios.create).toHaveBeenCalledWith({ - baseURL: 'https://sonarcloud.io', + baseURL: SONARCLOUD_API_BASE_URL, headers: { Authorization: `Bearer testToken`, }, timeout: 0, }); + expect(axios.create).toHaveBeenCalledWith({ + timeout: 0, + }); }); it('should initialize axios with timeout', async () => { jest.spyOn(axios, 'create'); - const properties: ScannerProperties = { - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + await initializeAxios({ + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, + [ScannerProperty.SonarScannerApiBaseUrl]: SONARCLOUD_API_BASE_URL, [ScannerProperty.SonarToken]: 'testToken', [ScannerProperty.SonarScannerResponseTimeout]: '23', - }; - - await initializeAxios(properties); + }); expect(axios.create).toHaveBeenCalledWith({ - baseURL: 'https://sonarcloud.io', + baseURL: SONARCLOUD_API_BASE_URL, headers: { Authorization: `Bearer testToken`, }, timeout: 23000, }); + expect(axios.create).toHaveBeenCalledWith({ + timeout: 23000, + }); }); }); describe('fetch', () => { it('should throw error if axios is not initialized', () => { - expect(() => fetch({})).toThrow('Axios instance is not initialized'); + jest.spyOn(axios, 'request'); + + expect(() => fetch({ url: '/some-url' })).toThrow('Axios instance is not initialized'); + }); + + it('should use correct axios instance based on URL', async () => { + const mockedRequestInternal = jest.fn(); + const mockedRequestExternal = jest.fn(); + jest.spyOn(axios, 'create').mockImplementation( + options => + ({ + request: options?.baseURL ? mockedRequestInternal : mockedRequestExternal, + }) as any as AxiosInstance, + ); + + await initializeAxios({ + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, + [ScannerProperty.SonarToken]: 'testToken', + [ScannerProperty.SonarScannerApiBaseUrl]: SONARCLOUD_API_BASE_URL, + }); + + await fetch({ url: 'https://sonarcloud.io/api/issues/search' }); + await fetch({ url: 'http://sonarcloud.io/api/issues/search' }); + expect(mockedRequestInternal).not.toHaveBeenCalled(); + expect(mockedRequestExternal).toHaveBeenCalledTimes(2); + + await fetch({ url: '/api/issues/search' }); + await fetch({ url: '/issues/search' }); + expect(mockedRequestInternal).toHaveBeenCalledTimes(2); + expect(mockedRequestExternal).toHaveBeenCalledTimes(2); }); it('should call axios request if axios is initialized', async () => { @@ -211,14 +247,12 @@ describe('request', () => { }) as any, ); - const properties: ScannerProperties = { - [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + await initializeAxios({ + [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, [ScannerProperty.SonarToken]: 'testToken', - }; - - await initializeAxios(properties); + }); - const config = { url: 'https://sonarcloud.io/api/issues/search' }; + const config = { url: '/api/issues/search' }; fetch(config); expect(mockedRequest).toHaveBeenCalledWith(config); diff --git a/test/unit/scanner-cli.test.ts b/test/unit/scanner-cli.test.ts index 3a4e3d23..8f69c002 100644 --- a/test/unit/scanner-cli.test.ts +++ b/test/unit/scanner-cli.test.ts @@ -17,8 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import * as fsExtra from 'fs-extra'; import { spawn } from 'child_process'; +import * as fsExtra from 'fs-extra'; import path from 'path'; import sinon from 'sinon'; import { @@ -275,7 +275,7 @@ describe('scanner-cli', () => { describe('normalizePlatformName', function () { it('detect Windows', function () { - const stub = sinon.stub(process, 'platform').value('windows10'); + const stub = sinon.stub(process, 'platform').value('win32'); expect(normalizePlatformName()).toEqual('windows'); stub.restore(); diff --git a/test/unit/scanner-engine.test.ts b/test/unit/scanner-engine.test.ts index 361614ae..e545d20a 100644 --- a/test/unit/scanner-engine.test.ts +++ b/test/unit/scanner-engine.test.ts @@ -21,15 +21,16 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { ChildProcess, spawn } from 'child_process'; +import fs from 'fs'; import sinon from 'sinon'; import { Readable } from 'stream'; +import { API_V2_SCANNER_ENGINE_ENDPOINT } from '../../src/constants'; import * as file from '../../src/file'; +import { logWithPrefix } from '../../src/logging'; import * as request from '../../src/request'; import { fetchScannerEngine, runScannerEngine } from '../../src/scanner-engine'; -import { ScannerProperties, ScannerProperty } from '../../src/types'; +import { AnalysisEngineResponseType, ScannerProperties, ScannerProperty } from '../../src/types'; import { ChildProcessMock } from './mocks/ChildProcessMock'; -import { logWithPrefix } from '../../src/logging'; -import fs from 'fs'; const mock = new MockAdapter(axios); @@ -39,8 +40,8 @@ const MOCKED_PROPERTIES: ScannerProperties = { }; const MOCK_CACHE_DIRECTORIES = { - archivePath: 'mocked/path/to/sonar/cache/md5_test/scanner-engine-1.2.3.zip', - unarchivePath: 'mocked/path/to/sonar/cache/md5_test/scanner-engine-1.2.3.zip_extracted', + archivePath: 'mocked/path/to/sonar/cache/sha_test/scanner-engine-1.2.3.zip', + unarchivePath: 'mocked/path/to/sonar/cache/sha_test/scanner-engine-1.2.3.zip_extracted', }; jest.mock('../../src/constants', () => ({ ...jest.requireActual('../../src/constants'), @@ -60,17 +61,28 @@ beforeEach(() => { describe('scanner-engine', () => { beforeEach(async () => { request.initializeAxios(MOCKED_PROPERTIES); - mock.onGet('/batch/index').reply(200, 'scanner-engine-1.2.3.zip|md5_test'); - mock.onGet('/batch/file?name=scanner-engine-1.2.3.zip').reply(() => { - const readable = new Readable({ - read() { - this.push('md5_test'); - this.push(null); // Indicates end of stream - }, - }); + mock.onGet(API_V2_SCANNER_ENGINE_ENDPOINT).reply(200, { + filename: 'scanner-engine-1.2.3.zip', + sha256: 'sha_test', + } as AnalysisEngineResponseType); + mock + .onGet( + API_V2_SCANNER_ENGINE_ENDPOINT, + undefined, + expect.objectContaining({ + Accept: expect.stringMatching(/application\/octet-stream/), + }), + ) + .reply(() => { + const readable = new Readable({ + read() { + this.push('sha_test'); + this.push(null); // Indicates end of stream + }, + }); - return [200, readable]; - }); + return [200, readable]; + }); jest.spyOn(file, 'getCacheFileLocation').mockResolvedValue(null); jest.spyOn(file, 'extractArchive').mockResolvedValue(); @@ -84,7 +96,7 @@ describe('scanner-engine', () => { await fetchScannerEngine(MOCKED_PROPERTIES); expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { - md5: 'md5_test', + checksum: 'sha_test', filename: 'scanner-engine-1.2.3.zip', }); }); @@ -98,7 +110,7 @@ describe('scanner-engine', () => { const scannerEngine = await fetchScannerEngine(MOCKED_PROPERTIES); expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { - md5: 'md5_test', + checksum: 'sha_test', filename: 'scanner-engine-1.2.3.zip', }); expect(request.download).not.toHaveBeenCalled(); @@ -113,14 +125,14 @@ describe('scanner-engine', () => { const scannerEngine = await fetchScannerEngine(MOCKED_PROPERTIES); expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { - md5: 'md5_test', + checksum: 'sha_test', filename: 'scanner-engine-1.2.3.zip', }); expect(request.download).toHaveBeenCalledTimes(1); expect(file.extractArchive).toHaveBeenCalledTimes(1); expect(scannerEngine).toEqual( - 'mocked/path/to/sonar/cache/md5_test/scanner-engine-1.2.3.zip_extracted', + 'mocked/path/to/sonar/cache/sha_test/scanner-engine-1.2.3.zip_extracted', ); }); }); diff --git a/test/unit/utils.test.js b/test/unit/utils.test.js index 42488966..0b6a1a7d 100644 --- a/test/unit/utils.test.js +++ b/test/unit/utils.test.js @@ -26,7 +26,7 @@ const sinon = require('sinon'); describe('utils', function () { describe('findTargetOS()', function () { it('detect Windows', function () { - const stub = sinon.stub(process, 'platform').value('windows10'); + const stub = sinon.stub(process, 'platform').value('win32'); assert.equal(findTargetOS(), 'windows'); stub.restore(); From 25c9683b500fe5b583f88df0ee2de490791b74ff Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Fri, 3 May 2024 12:34:29 +0200 Subject: [PATCH 24/35] SCANNPM-3 Cleanup/migrate old tests Co-authored-by: Victor Diez --- package.json | 4 +- src/constants.ts | 2 + src/java.ts | 6 + src/properties.ts | 46 +++++-- src/request.ts | 3 +- src/scanner-cli.ts | 16 ++- src/types.ts | 1 + test/integration/package.json | 2 +- test/integration/scanner.test.js | 6 +- test/unit/file.test.ts | 27 ++-- test/unit/index.test.js | 82 ------------ test/unit/java.test.ts | 15 ++- test/unit/mocks/FakeProjectMock.ts | 2 +- test/unit/properties.test.ts | 62 ++++++++- test/unit/scanner-cli.test.ts | 23 +++- test/unit/scanner-engine.test.ts | 5 +- test/unit/sonar-scanner-executable.test.js | 149 --------------------- test/unit/utils.test.js | 66 --------- tools/orchestrator/src/download.ts | 1 + tools/orchestrator/src/sonarqube.ts | 7 +- 20 files changed, 183 insertions(+), 342 deletions(-) delete mode 100644 test/unit/index.test.js delete mode 100644 test/unit/sonar-scanner-executable.test.js delete mode 100644 test/unit/utils.test.js diff --git a/package.json b/package.json index 9fa3e924..e3290777 100644 --- a/package.json +++ b/package.json @@ -68,9 +68,9 @@ "sonar-runner" ], "scripts": { - "build": "npm ci && npm run ts-build && npm run check-format && npm run license && npm test && cd tools/orchestrator", + "build": "npm ci && npm run ts-build && npm run check-format && npm run license && npm test && cd tools/orchestrator && npm run build", "ts-build": "tsc && node scripts/fix-comments.js", - "test": "npx jest --coverage", + "test": "jest --coverage", "test-integration": "cd test/integration && npm test", "format": "prettier --write .", "check-format": "prettier --list-different .", diff --git a/src/constants.ts b/src/constants.ts index 90641226..e779814a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -37,8 +37,10 @@ export const SONAR_CACHE_DIR = 'cache'; export const UNARCHIVE_SUFFIX = '_extracted'; export const ENV_VAR_PREFIX = 'SONAR_SCANNER_'; +export const NPM_CONFIG_ENV_VAR_PREFIX = 'npm_config_sonar_scanner_'; export const ENV_TO_PROPERTY_NAME: [string, ScannerProperty][] = [ + ['SONAR_BINARY_CACHE', ScannerProperty.SonarUserHome], // old deprecated format ['SONAR_TOKEN', ScannerProperty.SonarToken], ['SONAR_HOST_URL', ScannerProperty.SonarHostUrl], ['SONAR_USER_HOME', ScannerProperty.SonarUserHome], diff --git a/src/java.ts b/src/java.ts index 63192c07..b2e544db 100644 --- a/src/java.ts +++ b/src/java.ts @@ -65,6 +65,12 @@ export async function fetchServerVersion(properties: ScannerProperties): Promise } catch (error: unknown) { // If it also failed, give up log(LogLevel.ERROR, `Failed to fetch server version: ${error}`); + + // Inform the user of the host url that has failed, most + log( + LogLevel.ERROR, + `Verify that ${properties[ScannerProperty.SonarHostUrl]} is a valid SonarQube server`, + ); throw error; } } diff --git a/src/properties.ts b/src/properties.ts index 602ecb08..b9bd7243 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -26,6 +26,7 @@ import { DEFAULT_SONAR_EXCLUSIONS, ENV_TO_PROPERTY_NAME, ENV_VAR_PREFIX, + NPM_CONFIG_ENV_VAR_PREFIX, SCANNER_BOOTSTRAPPER_NAME, SONARCLOUD_API_BASE_URL, SONARCLOUD_URL, @@ -33,9 +34,9 @@ import { SONAR_DIR_DEFAULT, SONAR_PROJECT_FILENAME, } from './constants'; -import { LogLevel, log } from './logging'; +import { log, LogLevel } from './logging'; import { getArch, getSupportedOS } from './platform'; -import { CliArgs, ScanOptions, ScannerProperties, ScannerProperty } from './types'; +import { CliArgs, ScannerProperties, ScannerProperty, ScanOptions } from './types'; function getDefaultProperties(): ScannerProperties { return { @@ -61,6 +62,19 @@ function envNameToSonarPropertyNameMapper(envName: string) { return `sonar.scanner.${sonarScannerKey}`; } +/** + * Convert the name of a sonar property from its environment variable form + * (eg npm_config_sonar_scanner_) to its sonar form (eg sonar.scanner.fooBar). + */ +function npmConfigEnvNameToSonarPropertyNameMapper(envName: string) { + // Extract the name and convert to camel case + const sonarScannerKey = envName + .substring(NPM_CONFIG_ENV_VAR_PREFIX.length) + .toLowerCase() + .replace(/_([a-z])/g, g => g[1].toUpperCase()); + return `sonar.scanner.${sonarScannerKey}`; +} + /** * Build the config. */ @@ -76,14 +90,14 @@ function getPackageJsonProperties( } catch (error) { log(LogLevel.INFO, `Unable to read "package.json" file`); return { - 'sonar.exclusions': sonarBaseExclusions, + [ScannerProperty.SonarExclusions]: sonarBaseExclusions, }; } const pkg = JSON.parse(packageData); log(LogLevel.INFO, 'Retrieving info from "package.json" file'); function fileExistsInProjectSync(file: string) { - return fs.existsSync(path.resolve(projectBaseDir, file)); + return fs.existsSync(path.join(projectBaseDir, file)); } function dependenceExists(pkgName: string) { @@ -126,12 +140,12 @@ function getPackageJsonProperties( 'coverage', ); const uniqueCoverageDirs = Array.from(new Set(potentialCoverageDirs)); - packageJsonParams['sonar.exclusions'] = sonarBaseExclusions; + packageJsonParams[ScannerProperty.SonarExclusions] = sonarBaseExclusions; for (const lcovReportDir of uniqueCoverageDirs) { const lcovReportPath = path.posix.join(lcovReportDir, 'lcov.info'); if (fileExistsInProjectSync(lcovReportPath)) { - packageJsonParams['sonar.exclusions'] += - (packageJsonParams['sonar.exclusions'].length > 0 ? ',' : '') + + packageJsonParams[ScannerProperty.SonarExclusions] += + (packageJsonParams[ScannerProperty.SonarExclusions].length > 0 ? ',' : '') + path.posix.join(lcovReportDir, '**'); // https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/ packageJsonParams['sonar.javascript.lcov.reportPaths'] = lcovReportPath; @@ -218,6 +232,10 @@ function getScanOptionsProperties(scanOptions: ScanOptions): ScannerProperties { properties[ScannerProperty.SonarVerbose] = scanOptions.verbose ? 'true' : 'false'; } + if (typeof scanOptions.version !== 'undefined') { + properties[ScannerProperty.SonarScannerCliVersion] = scanOptions.version; + } + return properties; } @@ -245,6 +263,12 @@ function getEnvironmentProperties() { // Get generic environment variables properties = { ...properties, + ...Object.fromEntries( + Object.entries(env) + .filter(([key]) => key.startsWith(NPM_CONFIG_ENV_VAR_PREFIX)) + .filter(([key]) => !jsonEnvVariables.includes(key)) + .map(([key, value]) => [npmConfigEnvNameToSonarPropertyNameMapper(key), value as string]), + ), ...Object.fromEntries( Object.entries(env) .filter(([key]) => key.startsWith(ENV_VAR_PREFIX)) @@ -343,13 +367,13 @@ export function getProperties( startTimestampMs: number, cliArgs?: CliArgs, ): ScannerProperties { - const cliProperties = getCommandLineProperties(cliArgs); const envProperties = getEnvironmentProperties(); const scanOptionsProperties = getScanOptionsProperties(scanOptions); + const cliProperties = getCommandLineProperties(cliArgs); const userProperties: ScannerProperties = { - ...scanOptionsProperties, ...envProperties, + ...scanOptionsProperties, ...cliProperties, }; @@ -384,10 +408,8 @@ export function getProperties( const properties = { ...getDefaultProperties(), // fallback to default if nothing was provided for these properties ...inferredProperties, - ...scanOptionsProperties, ...httpProxyProperties, - ...envProperties, - ...cliProperties, // Highest precedence + ...userProperties, // Highest precedence }; // Hotfix host properties with custom SonarCloud URL diff --git a/src/request.ts b/src/request.ts index 8ca6f0e3..53bf1ece 100644 --- a/src/request.ts +++ b/src/request.ts @@ -142,13 +142,14 @@ export function fetch(config: AxiosRequestConfig) { return _axiosInstances.internal.request(config); } -export async function download(url: string, destPath: string) { +export async function download(url: string, destPath: string, overrides?: AxiosRequestConfig) { log(LogLevel.DEBUG, `Downloading ${url} to ${destPath}`); const response = await fetch({ url, method: 'GET', responseType: 'stream', + ...overrides, }); const totalLength = response.headers['content-length']; diff --git a/src/scanner-cli.ts b/src/scanner-cli.ts index 78cca9ca..f7e6a1fe 100644 --- a/src/scanner-cli.ts +++ b/src/scanner-cli.ts @@ -27,6 +27,7 @@ import { proxyUrlToJavaOptions } from './proxy'; import { download } from './request'; import { ScanOptions, ScannerProperties, ScannerProperty } from './types'; import { isMac, isWindows, isLinux } from './platform'; +import { AxiosRequestConfig } from 'axios'; export function normalizePlatformName(): 'windows' | 'linux' | 'macosx' { if (isWindows()) { @@ -102,9 +103,22 @@ export async function downloadScannerCli(properties: ScannerProperties): Promise // Create parent directory if needed await fsExtra.ensureDir(installDir); + // Add basic auth credentials when used in the UR + let overrides: AxiosRequestConfig | undefined; + if (scannerCliUrl.username && scannerCliUrl.password) { + overrides = { + headers: { + Authorization: + 'Basic ' + + Buffer.from(`${scannerCliUrl.username}:${scannerCliUrl.password}`).toString('base64'), + }, + }; + } + // Download SonarScanner CLI log(LogLevel.INFO, 'Downloading SonarScanner CLI'); - await download(scannerCliUrl.href, archivePath); + log(LogLevel.DEBUG, `Downloading from ${scannerCliUrl.href}`); + await download(scannerCliUrl.href, archivePath, overrides); log(LogLevel.INFO, `Extracting SonarScanner CLI archive`); await extractArchive(archivePath, installDir); diff --git a/src/types.ts b/src/types.ts index 07185b3f..1041e243 100644 --- a/src/types.ts +++ b/src/types.ts @@ -73,6 +73,7 @@ export type ScanOptions = { caPath?: string; logLevel?: string; verbose?: boolean; + version?: string; }; export type CliArgs = { diff --git a/test/integration/package.json b/test/integration/package.json index b7afe3fe..75ee76d8 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -7,7 +7,7 @@ "author": "", "license": "ISC", "scripts": { - "test": "npx jest *.test.js --coverage" + "test": "jest scanner.test.js --coverage" }, "devDependencies": { "chai": "^4.3.10", diff --git a/test/integration/scanner.test.js b/test/integration/scanner.test.js index 9497baa4..fec80ff9 100644 --- a/test/integration/scanner.test.js +++ b/test/integration/scanner.test.js @@ -58,7 +58,11 @@ describe('scanner', function () { options: { 'sonar.projectName': projectKey, 'sonar.projectKey': projectKey, - 'sonar.sources': path.join(__dirname, '/fixtures/fake_project_for_integration/src'), + 'sonar.log.level': 'DEBUG', + 'sonar.sources': path.join( + __dirname.replace(/\\+/g, '/'), + '/fixtures/fake_project_for_integration/src', + ), }, }); await waitForAnalysisFinished(TIMEOUT_MS); diff --git a/test/unit/file.test.ts b/test/unit/file.test.ts index 0d376be6..d207831e 100644 --- a/test/unit/file.test.ts +++ b/test/unit/file.test.ts @@ -33,7 +33,7 @@ import { import { ScannerProperty } from '../../src/types'; const MOCKED_PROPERTIES = { - [ScannerProperty.SonarUserHome]: '/path/to/sonar/user/home', + [ScannerProperty.SonarUserHome]: '/sonar', }; jest.mock('fs'); @@ -77,8 +77,8 @@ describe('file', () => { }); describe('tar.gz', () => { - const mockFilePath = 'path/to/file.tar.gz'; - const mockDestDir = 'path/to/dest'; + const mockFilePath = path.join('path', 'to', 'file.tar.gz'); + const mockDestDir = path.join('path', 'to', 'dest'); const mockFileHeader = { name: 'file.txt', mode: 0o777 }; const mockOn = jest.fn(); const mockPassThroughStream = new PassThrough(); @@ -126,9 +126,12 @@ describe('file', () => { expect(fs.createReadStream).toHaveBeenCalledWith(mockFilePath); expect(zlib.createGunzip).toHaveBeenCalled(); expect(tarStream.extract).toHaveBeenCalled(); - expect(fs.createWriteStream).toHaveBeenCalledWith(`${mockDestDir}/${mockFileHeader.name}`, { - mode: 511, - }); + expect(fs.createWriteStream).toHaveBeenCalledWith( + path.join(mockDestDir, mockFileHeader.name), + { + mode: 511, + }, + ); }); it('should throw if extract fails', async () => { @@ -219,19 +222,21 @@ describe('file', () => { filename: 'file.txt', }); - expect(fs.existsSync).toHaveBeenCalledWith('/path/to/sonar/user/home/cache/md5_test'); + expect(fs.existsSync).toHaveBeenCalledWith(path.join('/', 'sonar', 'cache', 'md5_test')); expect(fs.mkdirSync).not.toHaveBeenCalled(); - expect(archivePath).toEqual('/path/to/sonar/user/home/cache/md5_test/file.txt'); - expect(unarchivePath).toEqual('/path/to/sonar/user/home/cache/md5_test/file.txt_extracted'); + expect(archivePath).toEqual(path.join('/', 'sonar', 'cache', 'md5_test', 'file.txt')); + expect(unarchivePath).toEqual( + path.join('/', 'sonar', 'cache', 'md5_test', 'file.txt_extracted'), + ); }); it('should create the parent cache directory if it does not exist', async () => { jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => false); jest.spyOn(fs, 'mkdirSync').mockImplementationOnce(() => undefined); await getCacheDirectories(MOCKED_PROPERTIES, { checksum: 'md5_test', filename: 'file.txt' }); - expect(fs.existsSync).toHaveBeenCalledWith('/path/to/sonar/user/home/cache/md5_test'); - expect(fs.mkdirSync).toHaveBeenCalledWith('/path/to/sonar/user/home/cache/md5_test', { + expect(fs.existsSync).toHaveBeenCalledWith(path.join('/', 'sonar', 'cache', 'md5_test')); + expect(fs.mkdirSync).toHaveBeenCalledWith(path.join('/', 'sonar', 'cache', 'md5_test'), { recursive: true, }); }); diff --git a/test/unit/index.test.js b/test/unit/index.test.js deleted file mode 100644 index 862b9493..00000000 --- a/test/unit/index.test.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -const assert = require('assert'); -const index = require('../../src/index'); -const { spy, stub, restore } = require('sinon'); - -describe('index', function () { - afterEach(restore); - - describe('::fromParam', () => { - it('should provide the correct identity', function () { - assert.deepEqual(index.fromParam(), [ - '--from=ScannerNpm/' + require('../../package.json').version, - ]); - }); - }); - - describe('::cli', () => { - it('pass the expected arguments to the scan method', () => { - const parameters = { - foo: 'bar', - }; - const cliArguments = ['--foo', 'bar']; - - const scanStub = stub(index, 'scan').resolves(); - const callbackSpy = spy(() => { - assert.equal(scanStub.callCount, 1); - assert.equal(scanStub.firstCall.args[0], parameters); - assert.equal(scanStub.firstCall.args[1], cliArguments); - assert.equal( - scanStub.firstCall.args[2], - false, - 'the localScanner argument is passed as false', - ); - assert.equal(callbackSpy.callCount, 1); - }); - - index.cli(cliArguments, parameters, callbackSpy); - }); - }); - - describe('::customScanner', () => { - it('pass the expected arguments to the scan method', () => { - const parameters = { - foo: 'bar', - }; - - const scanStub = stub(index, 'scan').resolves(null); - const callbackSpy = spy(() => { - assert.equal(scanStub.callCount, 1); - assert.equal(scanStub.firstCall.args[0], parameters); - assert.equal(scanStub.firstCall.args[1].length, 0, 'no CLI arguments are passed'); - assert.equal( - scanStub.firstCall.args[2], - true, - 'the localScanner argument is passed as true', - ); - assert.equal(callbackSpy.callCount, 1); - }); - - index.customScanner(parameters, callbackSpy); - }); - }); -}); diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index d1486414..0df11916 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -21,6 +21,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import fs from 'fs'; import path from 'path'; +import { LogLevel, log } from '../../src/logging'; import { API_V2_JRE_ENDPOINT, SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../../src/constants'; import * as file from '../../src/file'; import { fetchJRE, fetchServerVersion, serverSupportsJREProvisioning } from '../../src/java'; @@ -35,9 +36,9 @@ const MOCKED_PROPERTIES: ScannerProperties = { [ScannerProperty.SonarScannerArch]: 'arm64', }; -beforeEach(() => { +beforeEach(async () => { jest.clearAllMocks(); - request.initializeAxios(MOCKED_PROPERTIES); + await request.initializeAxios(MOCKED_PROPERTIES); mock.reset(); jest.spyOn(request, 'fetch'); jest.spyOn(request, 'download'); @@ -66,9 +67,15 @@ describe('java', () => { mock.onGet('http://sonarqube.com/api/server/version').reply(404, 'Not Found'); mock.onGet('/api/v2/server/version').reply(404, 'Not Found'); - expect(async () => { + await expect(async () => { await fetchServerVersion(MOCKED_PROPERTIES); }).rejects.toBeDefined(); + + // test that we inform the user of the hostUrl being used + expect(log).toHaveBeenCalledWith( + LogLevel.ERROR, + `Verify that ${MOCKED_PROPERTIES[ScannerProperty.SonarHostUrl]} is a valid SonarQube server`, + ); }); it('should fail if version can not be parsed', async () => { @@ -76,7 +83,7 @@ describe('java', () => { .onGet('http://sonarqube.com/api/server/version') .reply(200, 'FORBIDDEN'); - expect(async () => { + await expect(async () => { await fetchServerVersion(MOCKED_PROPERTIES); }).rejects.toBeDefined(); }); diff --git a/test/unit/mocks/FakeProjectMock.ts b/test/unit/mocks/FakeProjectMock.ts index 3a77ede6..4b938a58 100644 --- a/test/unit/mocks/FakeProjectMock.ts +++ b/test/unit/mocks/FakeProjectMock.ts @@ -19,7 +19,7 @@ */ import path from 'path'; import sinon from 'sinon'; -import { SCANNER_BOOTSTRAPPER_NAME, SCANNER_CLI_VERSION } from '../../../src/constants'; +import { SCANNER_BOOTSTRAPPER_NAME } from '../../../src/constants'; const baseEnvVariables = process.env; diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index c9df8206..21f757f5 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -448,7 +448,7 @@ describe('getProperties', () => { 'sonar.projectVersion': '1.0-SNAPSHOT', 'sonar.sources': 'the-sources', }); - expect(log).toHaveBeenLastCalledWith( + expect(log).toHaveBeenCalledWith( LogLevel.WARN, expect.stringMatching(/Failed to parse JSON parameters/), ); @@ -480,6 +480,58 @@ describe('getProperties', () => { 'SONARQUBE_SCANNER_PARAMS is deprecated, please use SONAR_SCANNER_JSON_PARAMS instead', ); }); + + it('should set the [ScannerProperty.SonarScannerCliVersion] for all existing formats', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + npm_config_sonar_scanner_version: '4.8.1.3023', + }); + + // "NPM Config" format `npm_config_sonar_scanner_${property_name}` + const npmConfigProperties = getProperties({}, projectHandler.getStartTime()); + expect(npmConfigProperties[ScannerProperty.SonarScannerCliVersion]).toEqual('4.8.1.3023'); + + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONAR_SCANNER_VERSION: '5.0.0.2966', + }); + + // "SONAR_SCANNER" format `SONAR_SCANNER_${PROPERTY_NAME}` + const SonarScannerProperties = getProperties({}, projectHandler.getStartTime()); + expect(SonarScannerProperties[ScannerProperty.SonarScannerCliVersion]).toEqual('5.0.0.2966'); + + projectHandler.reset('fake_project_with_sonar_properties_file'); + // js scan options format + const jsScanOptionsProperties = getProperties( + { + version: '4.7.0.2747', + }, + projectHandler.getStartTime(), + ); + expect(jsScanOptionsProperties[ScannerProperty.SonarScannerCliVersion]).toEqual('4.7.0.2747'); + }); + + it('should support the old SONAR_BINARY_CACHE environment variable', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONAR_BINARY_CACHE: '/tmp/.sonar/', + }); + + const properties = getProperties({}, projectHandler.getStartTime()); + + expect(properties['sonar.userHome']).toEqual('/tmp/.sonar/'); + }); + + it('should support the old SONAR_SCANNER_MIRROR environment variable', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONAR_SCANNER_MIRROR: 'https://mirror.com/', + }); + + const properties = getProperties({}, projectHandler.getStartTime()); + + expect(properties['sonar.scanner.mirror']).toEqual('https://mirror.com/'); + }); }); describe('should handle command line properties', () => { @@ -514,9 +566,9 @@ describe('getProperties', () => { projectHandler.reset('fake_project_with_sonar_properties_file'); projectHandler.setEnvironmentVariables({ SONAR_TOKEN: 'ignored', - SONAR_HOST_URL: 'http://localhost/sonarqube', + SONAR_HOST_URL: 'http://ignored', SONAR_USER_HOME: '/tmp/used', - SONAR_ORGANIZATION: 'used', + SONAR_ORGANIZATION: 'ignored', SONAR_SCANNER_JSON_PARAMS: JSON.stringify({ 'sonar.userHome': 'ignored', 'sonar.scanner.someVar': 'used', @@ -525,11 +577,11 @@ describe('getProperties', () => { const properties = getProperties( { - serverUrl: 'http://ignored', + serverUrl: 'http://localhost/sonarqube', options: { 'sonar.projectKey': 'used', 'sonar.token': 'ignored', - 'sonar.organization': 'ignored', + 'sonar.organization': 'used', }, }, projectHandler.getStartTime(), diff --git a/test/unit/scanner-cli.test.ts b/test/unit/scanner-cli.test.ts index 8f69c002..c296648a 100644 --- a/test/unit/scanner-cli.test.ts +++ b/test/unit/scanner-cli.test.ts @@ -18,9 +18,9 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { spawn } from 'child_process'; -import * as fsExtra from 'fs-extra'; import path from 'path'; import sinon from 'sinon'; +import * as fsExtra from 'fs-extra'; import { SCANNER_CLI_DEFAULT_BIN_NAME, SCANNER_CLI_INSTALL_PATH, @@ -115,6 +115,7 @@ describe('scanner-cli', () => { SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-linux.zip', ), + undefined, ); expect(extractArchive).toHaveBeenLastCalledWith( path.join( @@ -148,6 +149,7 @@ describe('scanner-cli', () => { SCANNER_CLI_INSTALL_PATH, 'sonar-scanner-5.0.1.3006-windows.zip', ), + undefined, ); expect(extractArchive).toHaveBeenLastCalledWith( path.join( @@ -167,6 +169,25 @@ describe('scanner-cli', () => { stub.restore(); }); + + it('should persist username and password for scanner-cli download when a mirror is used', async () => { + childProcessHandler.setExitCode(1); + sinon.stub(process, 'platform').value('win32'); + await downloadScannerCli({ + ...MOCK_PROPERTIES, + [ScannerProperty.SonarScannerCliMirror]: 'https://myUser:myPassword@mirror.com:80', + }); + + expect(download).toHaveBeenLastCalledWith( + 'https://myUser:myPassword@mirror.com:80/sonar-scanner-cli-5.0.1.3006-windows.zip', + path.join( + MOCK_PROPERTIES[ScannerProperty.SonarUserHome], + SCANNER_CLI_INSTALL_PATH, + 'sonar-scanner-5.0.1.3006-windows.zip', + ), + { headers: { Authorization: 'Basic bXlVc2VyOm15UGFzc3dvcmQ=' } }, + ); + }); }); describe('runScannerCli', function () { diff --git a/test/unit/scanner-engine.test.ts b/test/unit/scanner-engine.test.ts index e545d20a..b1983d6a 100644 --- a/test/unit/scanner-engine.test.ts +++ b/test/unit/scanner-engine.test.ts @@ -60,7 +60,7 @@ beforeEach(() => { describe('scanner-engine', () => { beforeEach(async () => { - request.initializeAxios(MOCKED_PROPERTIES); + await request.initializeAxios(MOCKED_PROPERTIES); mock.onGet(API_V2_SCANNER_ENGINE_ENDPOINT).reply(200, { filename: 'scanner-engine-1.2.3.zip', sha256: 'sha_test', @@ -80,7 +80,6 @@ describe('scanner-engine', () => { this.push(null); // Indicates end of stream }, }); - return [200, readable]; }); @@ -173,7 +172,7 @@ describe('scanner-engine', () => { it('should reject when child process exits with code 1', async () => { childProcessHandler.setExitCode(1); - expect( + await expect( runScannerEngine( '/some/path/to/java', '/some/path/to/scanner-engine', diff --git a/test/unit/sonar-scanner-executable.test.js b/test/unit/sonar-scanner-executable.test.js deleted file mode 100644 index 24a80dab..00000000 --- a/test/unit/sonar-scanner-executable.test.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -const { assert } = require('chai'); -const path = require('path'); -const fs = require('fs'); -const os = require('os'); -const mkdirpSync = require('mkdirp').sync; -const rimraf = require('rimraf'); -const { getScannerExecutable } = require('../../src/sonar-scanner-executable'); -const { DEFAULT_SCANNER_VERSION, getExecutableParams } = require('../../src/config'); -const { buildInstallFolderPath, buildExecutablePath } = require('../../src/utils'); -const { startServer, closeServerPromise } = require('./fixtures/webserver/server'); - -describe('sqScannerExecutable', function () { - describe('Sonar: getScannerExecutable(false)', function () { - it('should throw exception when the download of executable fails', async function () { - process.env.SONAR_SCANNER_MIRROR = 'http://fake.url/sonar-scanner'; - try { - await getScannerExecutable(false, { - basePath: os.tmpdir(), - }); - assert.fail(); - } catch (err) { - console.log(err); - assert.equal(err.message, 'getaddrinfo ENOTFOUND fake.url'); - } - }, 60000); - - describe('when the executable exists', function () { - let filepath; - beforeAll(function () { - filepath = buildExecutablePath( - buildInstallFolderPath(os.tmpdir()), - DEFAULT_SCANNER_VERSION, - ); - mkdirpSync(path.dirname(filepath)); - fs.writeFileSync(filepath, 'echo "hello"'); - fs.chmodSync(filepath, 0o700); - }); - afterAll(function () { - rimraf.sync(filepath); - }); - it('should return the path to it', async function () { - const receivedExecutable = await getScannerExecutable(false, { - basePath: os.tmpdir(), - }); - assert.equal(receivedExecutable, filepath); - }); - }); - - describe('when the executable is downloaded', function () { - let server, config, pathToZip, pathToUnzippedExecutable, expectedPlatformExecutablePath; - const FILENAME = 'test-executable.zip'; - beforeAll(async function () { - server = await startServer(); - config = getExecutableParams({ fileName: FILENAME }); - expectedPlatformExecutablePath = config.platformExecutable; - }); - afterAll(async function () { - await closeServerPromise(server); - pathToZip = path.join(config.installFolder, config.fileName); - pathToUnzippedExecutable = path.join(config.installFolder, 'executable'); - rimraf.sync(pathToZip); - rimraf.sync(pathToUnzippedExecutable); - }); - it('should download the executable, unzip it and return a path to it.', async function () { - const execPath = await getScannerExecutable(false, { - baseUrl: `http://${server.address().address}:${server.address().port}`, - fileName: FILENAME, - }); - assert.equal(execPath, expectedPlatformExecutablePath); - }); - }); - - describe('when providing a self-signed CA certificate', function () { - let caPath; - beforeAll(() => { - caPath = path.join(os.tmpdir(), 'ca.pem'); - fs.writeFileSync(caPath, '-----BEGIN CERTIFICATE-----'); - }); - - it('should fail if the provided path is invalid', async function () { - try { - await getScannerExecutable(false, { caPath: 'invalid-path' }); - assert.fail('should have thrown'); - } catch (e) { - assert.equal(e.message, 'Provided CA certificate path does not exist: invalid-path'); - } - }); - it('should proceed with the download if the provided CA certificate is valid', async function () { - process.env.SONAR_SCANNER_MIRROR = 'http://fake.url/sonar-scanner'; - try { - await getScannerExecutable(false, { - caPath: caPath, - basePath: os.tmpdir(), - }); - assert.fail('should have thrown'); - } catch (e) { - assert.equal(e.message, 'getaddrinfo ENOTFOUND fake.url'); - } - }); - }); - }); - - describe('when providing an invalid CA certificate', function () { - let caPath; - beforeAll(() => { - caPath = path.join(os.tmpdir(), 'ca.pem'); - fs.writeFileSync(caPath, '-----ILLEGAL CERTIFICATE-----'); - }); - - it('should fail if the provided path is invalid', async function () { - try { - await getScannerExecutable(false, { caPath }); - assert.fail('should have thrown'); - } catch (e) { - assert.equal(e.message, 'Invalid CA certificate'); - } - }); - }); - - describe('local: getScannerExecutable(true)', () => { - it('should fail when the executable is not found', async () => { - assert.throws( - getScannerExecutable.bind(null, true), - 'Local install of SonarScanner not found in: sonar-scanner', - ); - //expect(getScannerExecutable(true)).to.eventually.be.rejectedWith('Local install of SonarScanner not found in: sonar-scanner'); - }); - }); -}); diff --git a/test/unit/utils.test.js b/test/unit/utils.test.js deleted file mode 100644 index 0b6a1a7d..00000000 --- a/test/unit/utils.test.js +++ /dev/null @@ -1,66 +0,0 @@ -/* - * sonar-scanner-npm - * Copyright (C) 2022-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -const path = require('node:path'); -const { assert } = require('chai'); -const { findTargetOS, buildInstallFolderPath } = require('../../src/utils'); -const sinon = require('sinon'); - -describe('utils', function () { - describe('findTargetOS()', function () { - it('detect Windows', function () { - const stub = sinon.stub(process, 'platform').value('win32'); - - assert.equal(findTargetOS(), 'windows'); - stub.restore(); - }); - - it('detect Mac', function () { - const stub = sinon.stub(process, 'platform').value('darwin'); - - assert.equal(findTargetOS(), 'macosx'); - stub.restore(); - }); - - it('detect Linux', function () { - const stub = sinon.stub(process, 'platform').value('linux'); - - assert.equal(findTargetOS(), 'linux'); - stub.restore(); - }); - - it('throw if something else', function () { - const stub = sinon.stub(process, 'platform').value('non-existing-os'); - - assert.throws(findTargetOS.bind(null)); - stub.restore(); - }); - }); - - describe('buildInstallFolderPath()', function () { - it('should use SONAR_BINARY_CACHE env when exists', function () { - const basePath = './test-cache'; - assert.equal( - buildInstallFolderPath(basePath), - path.join('test-cache', '.sonar', 'native-sonar-scanner'), - ); - }); - }); -}); diff --git a/tools/orchestrator/src/download.ts b/tools/orchestrator/src/download.ts index 60aabc3f..03c0a385 100644 --- a/tools/orchestrator/src/download.ts +++ b/tools/orchestrator/src/download.ts @@ -73,6 +73,7 @@ function getLatestVersion(): Promise { resolve(version); } catch (error) { console.error('Error while parsing response', responseData); + reject(error); } }); response.on('error', error => { diff --git a/tools/orchestrator/src/sonarqube.ts b/tools/orchestrator/src/sonarqube.ts index ecf95fd9..41559687 100644 --- a/tools/orchestrator/src/sonarqube.ts +++ b/tools/orchestrator/src/sonarqube.ts @@ -20,7 +20,7 @@ import * as path from 'path'; import { ChildProcess, spawn, exec } from 'child_process'; -const axios = require('axios').default; +import axios from 'axios'; const DEFAULT_FOLDER = path.join( __dirname, @@ -70,7 +70,10 @@ export async function startAndReady( */ function start(sqPath: string = DEFAULT_FOLDER) { const pathToBin = getPathForPlatform(sqPath); - return spawn(`${pathToBin}`, ['console'], { stdio: ['inherit', 'pipe', 'inherit'] }); + return spawn(`${pathToBin}`, ['console'], { + stdio: ['inherit', 'pipe', 'inherit'], + shell: process.platform === 'win32', + }); } /** From 938a248c1c879b3b3b8df5227e057f2230a09739 Mon Sep 17 00:00:00 2001 From: 7PH Date: Thu, 2 May 2024 10:52:14 +0200 Subject: [PATCH 25/35] SCANNPM-2 Use which/where.exe to detect SonarScanner CLI presence --- src/constants.ts | 2 + src/process.ts | 51 ++++++++++++++++++++ src/scan.ts | 24 ++++++---- src/scanner-cli.ts | 36 +++----------- test/unit/mocks/ChildProcessMock.ts | 25 +++++++++- test/unit/process.test.ts | 73 +++++++++++++++++++++++++++++ test/unit/scan.test.ts | 39 ++++++++++++--- test/unit/scanner-cli.test.ts | 60 +++++++----------------- 8 files changed, 219 insertions(+), 91 deletions(-) create mode 100644 src/process.ts create mode 100644 test/unit/process.test.ts diff --git a/src/constants.ts b/src/constants.ts index e779814a..dce67a06 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -63,3 +63,5 @@ export const SCANNER_CLI_VERSION = '5.0.1.3006'; export const SCANNER_CLI_MIRROR = 'https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/'; export const SCANNER_CLI_INSTALL_PATH = 'native-sonar-scanner'; + +export const WINDOWS_WHERE_EXE_PATH = 'C:\\Windows\\System32\\where.exe'; diff --git a/src/process.ts b/src/process.ts new file mode 100644 index 00000000..05b3c587 --- /dev/null +++ b/src/process.ts @@ -0,0 +1,51 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { exec } from 'child_process'; +import util from 'util'; +import { WINDOWS_WHERE_EXE_PATH } from './constants'; +import { log, LogLevel } from './logging'; +import { isWindows } from './platform'; + +const execAsync = util.promisify(exec); + +/** + * Verify that a given executable is accessible from the PATH. + * We use where.exe on Windows to check for the existence of the command to avoid + * search path vulnerabilities. Otherwise, Windows would search the current directory + * for the executable. + */ +export async function locateExecutableFromPath(executable: string): Promise { + try { + log(LogLevel.INFO, `Trying to find ${executable}`); + const child = await execAsync( + `${isWindows() ? WINDOWS_WHERE_EXE_PATH : 'which'} ${executable}`, + ); + const stdout = child.stdout?.trim(); + if (stdout.length) { + return stdout; + } + log(LogLevel.INFO, 'Local install of SonarScanner CLI found.'); + return null; + } catch (error) { + log(LogLevel.INFO, `Local install of SonarScanner CLI (${executable}) not found`); + return null; + } +} diff --git a/src/scan.ts b/src/scan.ts index f45ae673..b0d73590 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -21,17 +21,18 @@ import { version } from '../package.json'; import { SCANNER_CLI_DEFAULT_BIN_NAME } from './constants'; import { fetchJRE, serverSupportsJREProvisioning } from './java'; import { LogLevel, log, setLogLevel } from './logging'; +import { locateExecutableFromPath } from './process'; import { getProperties } from './properties'; import { initializeAxios } from './request'; -import { downloadScannerCli, runScannerCli, tryLocalSonarScannerExecutable } from './scanner-cli'; +import { downloadScannerCli, runScannerCli } from './scanner-cli'; import { fetchScannerEngine, runScannerEngine } from './scanner-engine'; -import { ScanOptions, ScannerProperty, CliArgs } from './types'; +import { CliArgs, ScanOptions, ScannerProperty } from './types'; export async function scan(scanOptions: ScanOptions, cliArgs?: CliArgs) { try { await runScan(scanOptions, cliArgs); - } catch (error: any) { - log(LogLevel.ERROR, `An error occurred: ${error?.message ?? error}`); + } catch (error) { + log(LogLevel.ERROR, `An error occurred: ${error}`); } } @@ -67,13 +68,14 @@ async function runScan(scanOptions: ScanOptions, cliArgs?: CliArgs) { log(LogLevel.INFO, `JRE Provisioning ${supportsJREProvisioning ? 'is' : 'is NOT'} supported`); if (!supportsJREProvisioning) { - log(LogLevel.INFO, 'Will download and use sonar-scanner-cli'); + log(LogLevel.INFO, 'Falling back on using sonar-scanner-cli'); if (scanOptions.localScannerCli) { log(LogLevel.INFO, 'Local scanner is requested, will not download sonar-scanner-cli'); - if (!(await tryLocalSonarScannerExecutable(SCANNER_CLI_DEFAULT_BIN_NAME))) { - throw new Error('Local scanner is requested but not found'); + const scannerPath = await locateExecutableFromPath(SCANNER_CLI_DEFAULT_BIN_NAME); + if (!scannerPath) { + throw new Error('SonarScanner CLI not found in PATH'); } - await runScannerCli(scanOptions, properties, SCANNER_CLI_DEFAULT_BIN_NAME); + await runScannerCli(scanOptions, properties, scannerPath); } else { const binPath = await downloadScannerCli(properties); await runScannerCli(scanOptions, properties, binPath); @@ -86,7 +88,11 @@ async function runScan(scanOptions: ScanOptions, cliArgs?: CliArgs) { if (properties[ScannerProperty.SonarScannerJavaExePath]) { javaPath = properties[ScannerProperty.SonarScannerJavaExePath]; } else if (properties[ScannerProperty.SonarScannerSkipJreProvisioning] === 'true') { - javaPath = 'java'; + const absoluteJavaPath = await locateExecutableFromPath('java'); + if (!absoluteJavaPath) { + throw new Error('Java not found in PATH'); + } + javaPath = absoluteJavaPath; } else { javaPath = await fetchJRE(properties); } diff --git a/src/scanner-cli.ts b/src/scanner-cli.ts index f7e6a1fe..add2152b 100644 --- a/src/scanner-cli.ts +++ b/src/scanner-cli.ts @@ -23,10 +23,10 @@ import path from 'path'; import { SCANNER_CLI_INSTALL_PATH, SCANNER_CLI_MIRROR, SCANNER_CLI_VERSION } from './constants'; import { extractArchive } from './file'; import { LogLevel, log } from './logging'; +import { isLinux, isMac, isWindows } from './platform'; import { proxyUrlToJavaOptions } from './proxy'; import { download } from './request'; import { ScanOptions, ScannerProperties, ScannerProperty } from './types'; -import { isMac, isWindows, isLinux } from './platform'; import { AxiosRequestConfig } from 'axios'; export function normalizePlatformName(): 'windows' | 'linux' | 'macosx' { @@ -42,31 +42,6 @@ export function normalizePlatformName(): 'windows' | 'linux' | 'macosx' { throw Error(`Your platform '${process.platform}' is currently not supported.`); } -/** - * Verifies if the provided (or default) command is executable - */ -export async function tryLocalSonarScannerExecutable(command: string): Promise { - return new Promise(resolve => { - log(LogLevel.INFO, `Trying to find a local install of the SonarScanner: ${command}`); - - if (!fsExtra.existsSync(command)) { - resolve(false); - return; - } - const scannerProcess = spawn(command, ['-v'], { shell: isWindows() }); - - scannerProcess.on('exit', code => { - if (code === 0) { - log(LogLevel.INFO, 'Local install of SonarScanner CLI found.'); - resolve(true); - } else { - log(LogLevel.INFO, `Local install of SonarScanner CLI (${command}) not found`); - resolve(false); - } - }); - }); -} - /** * Where to download the SonarScanner CLI */ @@ -86,8 +61,6 @@ export async function downloadScannerCli(properties: ScannerProperties): Promise throw new Error(`Version "${version}" does not have a correct format."`); } - const scannerCliUrl = getScannerCliUrl(properties, version); - // Build paths const binExt = normalizePlatformName() === 'windows' ? '.bat' : ''; const dirName = `sonar-scanner-${version}-${normalizePlatformName()}`; @@ -95,8 +68,7 @@ export async function downloadScannerCli(properties: ScannerProperties): Promise const archivePath = path.join(installDir, `${dirName}.zip`); const binPath = path.join(installDir, dirName, 'bin', `sonar-scanner${binExt}`); - // Try and execute an already downloaded scanner, which should be at the same location - if (await tryLocalSonarScannerExecutable(binPath)) { + if (await fsExtra.exists(binPath)) { return binPath; } @@ -104,6 +76,7 @@ export async function downloadScannerCli(properties: ScannerProperties): Promise await fsExtra.ensureDir(installDir); // Add basic auth credentials when used in the UR + const scannerCliUrl = getScannerCliUrl(properties, version); let overrides: AxiosRequestConfig | undefined; if (scannerCliUrl.username && scannerCliUrl.password) { overrides = { @@ -126,6 +99,9 @@ export async function downloadScannerCli(properties: ScannerProperties): Promise return binPath; } +/** + * @param binPath Absolute path to the scanner CLI executable + */ export async function runScannerCli( scanOptions: ScanOptions, properties: ScannerProperties, diff --git a/test/unit/mocks/ChildProcessMock.ts b/test/unit/mocks/ChildProcessMock.ts index a57fb9e1..8ab37032 100644 --- a/test/unit/mocks/ChildProcessMock.ts +++ b/test/unit/mocks/ChildProcessMock.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { spawn, ChildProcess } from 'child_process'; +import { ChildProcess, exec, spawn } from 'child_process'; export class ChildProcessMock { private exitCode: number = 0; @@ -27,8 +27,11 @@ export class ChildProcessMock { private mock: Partial | null = null; + private commandHistory: string[] = []; + constructor() { jest.mocked(spawn).mockImplementation((this.handleSpawn as any).bind(this)); + jest.mocked(exec).mockImplementation((this.handleExec as any).bind(this)); } setExitCode(exitCode: number) { @@ -44,7 +47,12 @@ export class ChildProcessMock { this.mock = mock; } - handleSpawn() { + getCommandHistory() { + return this.commandHistory; + } + + handleSpawn(command: string) { + this.commandHistory.push(command); return { on: jest.fn().mockImplementation((event, callback) => { if (event === 'exit') { @@ -58,11 +66,24 @@ export class ChildProcessMock { }; } + handleExec( + command: string, + callback: (error?: Error, { stdout, stderr }?: { stdout: string; stderr: string }) => void, + ) { + this.commandHistory.push(command); + const error = this.exitCode === 0 ? undefined : new Error('Command failed by mock'); + callback(error, { + stdout: this.stdout, + stderr: this.stderr, + }); + } + reset() { this.exitCode = 0; this.stdout = ''; this.stderr = ''; this.mock = null; + this.commandHistory = []; jest.clearAllMocks(); } } diff --git a/test/unit/process.test.ts b/test/unit/process.test.ts new file mode 100644 index 00000000..de2b3d18 --- /dev/null +++ b/test/unit/process.test.ts @@ -0,0 +1,73 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import sinon from 'sinon'; +import { SCANNER_CLI_DEFAULT_BIN_NAME, WINDOWS_WHERE_EXE_PATH } from '../../src/constants'; +import { locateExecutableFromPath } from '../../src/process'; +import { ChildProcessMock } from './mocks/ChildProcessMock'; + +jest.mock('fs-extra'); +jest.mock('child_process'); +jest.mock('../../src/request'); +jest.mock('../../src/file'); +jest.mock('../../src/logging'); + +const childProcessHandler = new ChildProcessMock(); + +beforeEach(() => { + childProcessHandler.reset(); +}); + +describe('process', () => { + describe('locateExecutableFromPath', () => { + it('should use windows where.exe when on windows', async () => { + // mock windows with stub + const stub = sinon.stub(process, 'platform').value('win32'); + + childProcessHandler.setOutput('/bin/path/to/stuff\n', ''); + + expect(await locateExecutableFromPath(SCANNER_CLI_DEFAULT_BIN_NAME)).toBe( + '/bin/path/to/stuff', + ); + expect(childProcessHandler.getCommandHistory()).toContain( + `${WINDOWS_WHERE_EXE_PATH} ${SCANNER_CLI_DEFAULT_BIN_NAME}`, + ); + + stub.restore(); + }); + + it('should detect locally installed command', async () => { + childProcessHandler.setOutput('some output\n', ''); + + expect(await locateExecutableFromPath(SCANNER_CLI_DEFAULT_BIN_NAME)).toBe('some output'); + }); + + it('should not detect locally installed command (when exit code is 1)', async () => { + childProcessHandler.setExitCode(1); + + expect(await locateExecutableFromPath(SCANNER_CLI_DEFAULT_BIN_NAME)).toBe(null); + }); + + it('should not detect locally installed command (when empty stdout)', async () => { + childProcessHandler.setOutput('', ''); + + expect(await locateExecutableFromPath(SCANNER_CLI_DEFAULT_BIN_NAME)).toBe(null); + }); + }); +}); diff --git a/test/unit/scan.test.ts b/test/unit/scan.test.ts index 4b28badc..4d049756 100644 --- a/test/unit/scan.test.ts +++ b/test/unit/scan.test.ts @@ -27,13 +27,15 @@ jest.mock('../../src/logging', () => ({ import * as java from '../../src/java'; import * as logging from '../../src/logging'; import * as platform from '../../src/platform'; -import * as scannerEngine from '../../src/scanner-engine'; -import * as scannerCli from '../../src/scanner-cli'; +import * as process from '../../src/process'; import { scan } from '../../src/scan'; +import * as scannerCli from '../../src/scanner-cli'; +import * as scannerEngine from '../../src/scanner-engine'; import { ScannerProperty } from '../../src/types'; jest.mock('../../src/java'); jest.mock('../../src/scanner-cli'); +jest.mock('../../src/process'); jest.mock('../../src/scanner-engine'); jest.mock('../../src/platform'); jest.mock('../../package.json', () => ({ @@ -94,21 +96,21 @@ describe('scan', () => { jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); jest.spyOn(scannerEngine, 'runScannerEngine'); jest.spyOn(scannerCli, 'runScannerCli'); - jest.spyOn(scannerCli, 'tryLocalSonarScannerExecutable').mockResolvedValue(true); + jest.spyOn(process, 'locateExecutableFromPath').mockResolvedValue('/bin/sonar-scanner'); await scan({ serverUrl: 'http://localhost:9000', localScannerCli: true }); expect(scannerCli.downloadScannerCli).not.toHaveBeenCalled(); expect(scannerCli.runScannerCli).toHaveBeenCalled(); const [, , scannerPath] = (scannerCli.runScannerCli as jest.Mock).mock.calls.pop(); - expect(scannerPath).toBe('sonar-scanner'); + expect(scannerPath).toBe('/bin/sonar-scanner'); }); it('should fail if local scanner is requested but not found', async () => { jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(false); jest.spyOn(scannerEngine, 'runScannerEngine'); jest.spyOn(scannerCli, 'runScannerCli'); - jest.spyOn(scannerCli, 'tryLocalSonarScannerExecutable').mockResolvedValue(false); + jest.spyOn(process, 'locateExecutableFromPath').mockResolvedValue(null); await scan({ serverUrl: 'http://localhost:9000', localScannerCli: true }); @@ -116,7 +118,7 @@ describe('scan', () => { expect(scannerCli.runScannerCli).not.toHaveBeenCalled(); expect(logging.log).toHaveBeenCalledWith( logging.LogLevel.ERROR, - expect.stringMatching(/Local scanner is requested but not found/), + expect.stringMatching(/SonarScanner CLI not found in PATH/), ); }); }); @@ -153,6 +155,7 @@ describe('scan', () => { jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(true); jest.spyOn(java, 'fetchJRE'); jest.spyOn(scannerEngine, 'runScannerEngine'); + jest.spyOn(process, 'locateExecutableFromPath').mockResolvedValue('/usr/bin/java'); await scan({ serverUrl: 'http://localhost:9000', @@ -162,8 +165,30 @@ describe('scan', () => { }); expect(java.fetchJRE).not.toHaveBeenCalled(); + expect(process.locateExecutableFromPath).toHaveBeenCalled(); const [javaPath] = (scannerEngine.runScannerEngine as jest.Mock).mock.calls.pop(); - expect(javaPath).toBe('java'); + expect(javaPath).toBe('/usr/bin/java'); + }); + + it('should fail when skipping JRE provisioning without java in PATH', async () => { + jest.spyOn(java, 'serverSupportsJREProvisioning').mockResolvedValue(true); + jest.spyOn(java, 'fetchJRE'); + jest.spyOn(scannerEngine, 'runScannerEngine'); + jest.spyOn(process, 'locateExecutableFromPath').mockResolvedValue(null); + + await scan({ + serverUrl: 'http://localhost:9000', + options: { + [ScannerProperty.SonarScannerSkipJreProvisioning]: 'true', + }, + }); + + expect(scannerEngine.runScannerEngine).not.toHaveBeenCalled(); + expect(scannerCli.runScannerCli).not.toHaveBeenCalled(); + expect(logging.log).toHaveBeenCalledWith( + logging.LogLevel.ERROR, + expect.stringMatching(/Java not found in PATH/), + ); }); }); }); diff --git a/test/unit/scanner-cli.test.ts b/test/unit/scanner-cli.test.ts index c296648a..6a5faf44 100644 --- a/test/unit/scanner-cli.test.ts +++ b/test/unit/scanner-cli.test.ts @@ -18,27 +18,21 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { spawn } from 'child_process'; +import * as fsExtra from 'fs-extra'; import path from 'path'; import sinon from 'sinon'; -import * as fsExtra from 'fs-extra'; -import { - SCANNER_CLI_DEFAULT_BIN_NAME, - SCANNER_CLI_INSTALL_PATH, - SCANNER_CLI_VERSION, -} from '../../src/constants'; +import { SCANNER_CLI_INSTALL_PATH, SCANNER_CLI_VERSION } from '../../src/constants'; import { extractArchive } from '../../src/file'; import { LogLevel, log } from '../../src/logging'; import { download } from '../../src/request'; -import { - downloadScannerCli, - normalizePlatformName, - runScannerCli, - tryLocalSonarScannerExecutable, -} from '../../src/scanner-cli'; +import { downloadScannerCli, normalizePlatformName, runScannerCli } from '../../src/scanner-cli'; import { ScannerProperty } from '../../src/types'; import { ChildProcessMock } from './mocks/ChildProcessMock'; -jest.mock('fs-extra'); +jest.mock('fs-extra', () => ({ + ensureDir: jest.fn(), + exists: jest.fn(), +})); jest.mock('child_process'); jest.mock('../../src/request'); jest.mock('../../src/file'); @@ -58,19 +52,6 @@ beforeEach(() => { }); describe('scanner-cli', () => { - describe('tryLocalSonarScannerExecutable', () => { - it('should detect locally installed scanner-cli', async () => { - jest.spyOn(fsExtra, 'existsSync').mockReturnValue(true); - expect(await tryLocalSonarScannerExecutable(SCANNER_CLI_DEFAULT_BIN_NAME)).toBe(true); - }); - - it('should not detect locally installed scanner-cli', async () => { - childProcessHandler.setExitCode(1); - - expect(await tryLocalSonarScannerExecutable(SCANNER_CLI_DEFAULT_BIN_NAME)).toBe(false); - }); - }); - describe('downloadScannerCli', function () { it('should reject invalid versions', () => { expect( @@ -81,27 +62,27 @@ describe('scanner-cli', () => { }); it('should use already downloaded version', async () => { + const scannerBinPath = path.join( + MOCK_PROPERTIES[ScannerProperty.SonarUserHome], + SCANNER_CLI_INSTALL_PATH, + 'sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner', + ); const stub = sinon.stub(process, 'platform').value('linux'); + jest.spyOn(fsExtra, 'exists').mockImplementation(_path => Promise.resolve(true)); - expect(await downloadScannerCli(MOCK_PROPERTIES)).toBe( - path.join( - MOCK_PROPERTIES[ScannerProperty.SonarUserHome], - SCANNER_CLI_INSTALL_PATH, - 'sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner', - ), - ); + expect(await downloadScannerCli(MOCK_PROPERTIES)).toBe(scannerBinPath); expect(download).not.toHaveBeenCalled(); stub.restore(); }); it('should download SonarScanner CLI if it does not exist on Unix', async () => { - childProcessHandler.setExitCode(1); + jest.spyOn(fsExtra, 'exists').mockImplementation(_path => Promise.resolve(false)); const stub = sinon.stub(process, 'platform').value('linux'); const binPath = await downloadScannerCli(MOCK_PROPERTIES); - expect(await downloadScannerCli(MOCK_PROPERTIES)).toBe( + expect(binPath).toBe( path.join( MOCK_PROPERTIES[ScannerProperty.SonarUserHome], SCANNER_CLI_INSTALL_PATH, @@ -125,20 +106,13 @@ describe('scanner-cli', () => { ), path.join(MOCK_PROPERTIES[ScannerProperty.SonarUserHome], SCANNER_CLI_INSTALL_PATH), ); - expect(binPath).toBe( - path.join( - MOCK_PROPERTIES[ScannerProperty.SonarUserHome], - SCANNER_CLI_INSTALL_PATH, - 'sonar-scanner-5.0.1.3006-linux/bin/sonar-scanner', - ), - ); stub.restore(); }); it('should download SonarScanner CLI if it does not exist on Windows', async () => { - childProcessHandler.setExitCode(1); const stub = sinon.stub(process, 'platform').value('win32'); + jest.spyOn(fsExtra, 'exists').mockImplementation(_path => Promise.resolve(false)); const binPath = await downloadScannerCli(MOCK_PROPERTIES); From 770630d8b18b0e6de3f05046c51ef8bf8cd5cb18 Mon Sep 17 00:00:00 2001 From: 7PH Date: Thu, 2 May 2024 16:00:18 +0200 Subject: [PATCH 26/35] SCANNPM-2 Change wasJreCacheHit from bool to enum and mark it disabled when skipping JRE provisioning explicitly --- src/java.ts | 5 ++++- src/properties.ts | 9 +++++---- src/types.ts | 6 ++++++ test/unit/java.test.ts | 24 +++++++++++++----------- test/unit/mocks/FakeProjectMock.ts | 3 ++- test/unit/properties.test.ts | 6 +++--- 6 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/java.ts b/src/java.ts index b2e544db..7db691c9 100644 --- a/src/java.ts +++ b/src/java.ts @@ -38,6 +38,7 @@ import { download, fetch } from './request'; import { AnalysisJreMetaData, AnalysisJresResponseType, + CacheStatus, ScannerProperties, ScannerProperty, } from './types'; @@ -113,7 +114,9 @@ export async function fetchJRE(properties: ScannerProperties): Promise { checksum: jreMetaData.sha256, filename: jreMetaData.filename + UNARCHIVE_SUFFIX, }); - properties[ScannerProperty.SonarScannerWasJreCacheHit] = Boolean(cachedJrePath).toString(); + properties[ScannerProperty.SonarScannerWasJreCacheHit] = cachedJrePath + ? CacheStatus.Hit + : CacheStatus.Miss; if (cachedJrePath) { log(LogLevel.INFO, 'Using Cached JRE'); return path.join(cachedJrePath, jreMetaData.javaPath); diff --git a/src/properties.ts b/src/properties.ts index b9bd7243..bbfe6970 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -34,9 +34,9 @@ import { SONAR_DIR_DEFAULT, SONAR_PROJECT_FILENAME, } from './constants'; -import { log, LogLevel } from './logging'; +import { LogLevel, log } from './logging'; import { getArch, getSupportedOS } from './platform'; -import { CliArgs, ScannerProperties, ScannerProperty, ScanOptions } from './types'; +import { CacheStatus, CliArgs, ScanOptions, ScannerProperties, ScannerProperty } from './types'; function getDefaultProperties(): ScannerProperties { return { @@ -307,8 +307,9 @@ function getBootstrapperProperties(startTimestampMs: number): ScannerProperties 'sonar.scanner.app': SCANNER_BOOTSTRAPPER_NAME, 'sonar.scanner.appVersion': version, 'sonar.scanner.bootstrapStartTime': startTimestampMs.toString(), - // Bootstrap cache hit/miss is set later after the bootstrapper has run and before scanner engine is started - 'sonar.scanner.wasJreCacheHit': 'false', + // These cache statuses are set during the bootstrapping process. + // We already set them here to prevent them from being overridden. + 'sonar.scanner.wasJreCacheHit': CacheStatus.Disabled, 'sonar.scanner.wasEngineCacheHit': 'false', }; } diff --git a/src/types.ts b/src/types.ts index 1041e243..df86e93c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,3 +98,9 @@ export type AnalysisEngineResponseType = { sha256: string; downloadUrl?: string; }; + +export enum CacheStatus { + Hit = 'hit', + Miss = 'miss', + Disabled = 'disabled', +} diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index 0df11916..72a1f5ca 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -26,7 +26,12 @@ import { API_V2_JRE_ENDPOINT, SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../ import * as file from '../../src/file'; import { fetchJRE, fetchServerVersion, serverSupportsJREProvisioning } from '../../src/java'; import * as request from '../../src/request'; -import { AnalysisJresResponseType, ScannerProperties, ScannerProperty } from '../../src/types'; +import { + AnalysisJresResponseType, + CacheStatus, + ScannerProperties, + ScannerProperty, +} from '../../src/types'; const mock = new MockAdapter(axios); @@ -142,15 +147,14 @@ describe('java', () => { describe('when the JRE is cached', () => { it('should fetch the latest supported JRE and use the cached version', async () => { - await fetchJRE(MOCKED_PROPERTIES); + const properties = { ...MOCKED_PROPERTIES }; + await fetchJRE(properties); expect(request.fetch).toHaveBeenCalledTimes(1); expect(request.download).not.toHaveBeenCalled(); - - // Check for the cache expect(file.getCacheFileLocation).toHaveBeenCalledTimes(1); - expect(file.extractArchive).not.toHaveBeenCalled(); + expect(properties[ScannerProperty.SonarScannerWasJreCacheHit]).toBe(CacheStatus.Hit); }); }); @@ -167,7 +171,8 @@ describe('java', () => { }); it('should download the JRE', async () => { - await fetchJRE({ ...MOCKED_PROPERTIES }); + const properties = { ...MOCKED_PROPERTIES }; + await fetchJRE(properties); expect(request.fetch).toHaveBeenCalledWith({ url: API_V2_JRE_ENDPOINT, @@ -176,17 +181,14 @@ describe('java', () => { arch: MOCKED_PROPERTIES[ScannerProperty.SonarScannerArch], }, }); - expect(file.getCacheFileLocation).toHaveBeenCalledTimes(1); - expect(request.download).toHaveBeenCalledWith( `${API_V2_JRE_ENDPOINT}/${serverResponse[0].id}`, mockCacheDirectories.archivePath, ); - expect(file.validateChecksum).toHaveBeenCalledTimes(1); - expect(file.extractArchive).toHaveBeenCalledTimes(1); + expect(properties[ScannerProperty.SonarScannerWasJreCacheHit]).toBe(CacheStatus.Miss); }); it('should fail if no JRE matches', async () => { @@ -200,7 +202,7 @@ describe('java', () => { .reply(200, []); // Check that it rejects with a specific error - expect(fetchJRE({ ...MOCKED_PROPERTIES })).rejects.toThrowError( + expect(fetchJRE({ ...MOCKED_PROPERTIES })).rejects.toThrow( 'No JREs available for your platform linux arm64', ); }); diff --git a/test/unit/mocks/FakeProjectMock.ts b/test/unit/mocks/FakeProjectMock.ts index 4b938a58..a6d919fc 100644 --- a/test/unit/mocks/FakeProjectMock.ts +++ b/test/unit/mocks/FakeProjectMock.ts @@ -20,6 +20,7 @@ import path from 'path'; import sinon from 'sinon'; import { SCANNER_BOOTSTRAPPER_NAME } from '../../../src/constants'; +import { CacheStatus } from '../../../src/types'; const baseEnvVariables = process.env; @@ -59,7 +60,7 @@ export class FakeProjectMock { 'sonar.scanner.app': SCANNER_BOOTSTRAPPER_NAME, 'sonar.scanner.appVersion': '1.2.3', 'sonar.scanner.wasEngineCacheHit': 'false', - 'sonar.scanner.wasJreCacheHit': 'false', + 'sonar.scanner.wasJreCacheHit': CacheStatus.Disabled, 'sonar.userHome': expect.stringMatching(/\.sonar$/), 'sonar.scanner.os': 'windows', 'sonar.scanner.arch': 'aarch64', diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index 21f757f5..898e2368 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -26,7 +26,7 @@ import { } from '../../src/constants'; import { LogLevel, log } from '../../src/logging'; import { getHostProperties, getProperties } from '../../src/properties'; -import { ScannerProperty } from '../../src/types'; +import { CacheStatus, ScannerProperty } from '../../src/types'; import { FakeProjectMock } from './mocks/FakeProjectMock'; jest.mock('../../src/logging'); @@ -615,7 +615,7 @@ describe('getProperties', () => { 'sonar.scanner.app': 'ignored', 'sonar.scanner.appVersion': 'ignored', 'sonar.scanner.bootstrapStartTime': '0000', - 'sonar.scanner.wasJreCacheHit': 'true', + 'sonar.scanner.wasJreCacheHit': CacheStatus.Hit, 'sonar.scanner.wasEngineCacheHit': 'true', }), }); @@ -627,7 +627,7 @@ describe('getProperties', () => { 'sonar.scanner.app': 'ignored', 'sonar.scanner.appVersion': 'ignored', 'sonar.scanner.bootstrapStartTime': '0000', - 'sonar.scanner.wasJreCacheHit': 'true', + 'sonar.scanner.wasJreCacheHit': CacheStatus.Hit, 'sonar.scanner.wasEngineCacheHit': 'true', }, }, From 8be5bfa35aadaa1786436fb6ed69b38b9ecc3af8 Mon Sep 17 00:00:00 2001 From: Victor Diez Date: Fri, 3 May 2024 16:04:58 +0200 Subject: [PATCH 27/35] SCANNPM-3 Test with Scanner cli v6 Co-authored-by: Benjamin Raymond <31401273+7PH@users.noreply.github.com> --- src/scanner-cli.ts | 17 +++++++++++++++-- test/integration/scanner.test.js | 2 ++ test/unit/scanner-cli.test.ts | 19 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/scanner-cli.ts b/src/scanner-cli.ts index add2152b..60b6a430 100644 --- a/src/scanner-cli.ts +++ b/src/scanner-cli.ts @@ -20,7 +20,13 @@ import { spawn } from 'child_process'; import * as fsExtra from 'fs-extra'; import path from 'path'; -import { SCANNER_CLI_INSTALL_PATH, SCANNER_CLI_MIRROR, SCANNER_CLI_VERSION } from './constants'; +import { + ENV_TO_PROPERTY_NAME, + ENV_VAR_PREFIX, + SCANNER_CLI_INSTALL_PATH, + SCANNER_CLI_MIRROR, + SCANNER_CLI_VERSION, +} from './constants'; import { extractArchive } from './file'; import { LogLevel, log } from './logging'; import { isLinux, isMac, isWindows } from './platform'; @@ -108,12 +114,19 @@ export async function runScannerCli( binPath: string, ) { log(LogLevel.INFO, 'Starting analysis'); + // We filter out env properties that are passed to the scanner + // otherwise, they would supersede the properties passed to the scanner through SONARQUBE_SCANNER_PARAMS + const filteredEnvKeys = ENV_TO_PROPERTY_NAME.map(env => env[0]); + const filteredEnv = Object.entries(process.env) + .filter(([key]) => !filteredEnvKeys.includes(key)) + .filter(([key]) => !key.startsWith(ENV_VAR_PREFIX)); + const child = spawn( binPath, [...(scanOptions.jvmOptions ?? []), ...proxyUrlToJavaOptions(properties)], { env: { - ...process.env, + ...Object.fromEntries(filteredEnv), SONARQUBE_SCANNER_PARAMS: JSON.stringify(properties), }, shell: isWindows(), diff --git a/test/integration/scanner.test.js b/test/integration/scanner.test.js index fec80ff9..5e33e65e 100644 --- a/test/integration/scanner.test.js +++ b/test/integration/scanner.test.js @@ -59,6 +59,8 @@ describe('scanner', function () { 'sonar.projectName': projectKey, 'sonar.projectKey': projectKey, 'sonar.log.level': 'DEBUG', + 'sonar.scanner.version': '6.0.0.4373', + 'sonar.scanner.mirror': `https://${process.env.ARTIFACTORY_PRIVATE_USERNAME}:${process.env.ARTIFACTORY_PRIVATE_PASSWORD}@repox.jfrog.io/artifactory/sonarsource-public-builds/org/sonarsource/scanner/cli/sonar-scanner-cli/6.0.0.4373/`, 'sonar.sources': path.join( __dirname.replace(/\\+/g, '/'), '/fixtures/fake_project_for_integration/src', diff --git a/test/unit/scanner-cli.test.ts b/test/unit/scanner-cli.test.ts index 6a5faf44..f0c751b8 100644 --- a/test/unit/scanner-cli.test.ts +++ b/test/unit/scanner-cli.test.ts @@ -199,6 +199,25 @@ describe('scanner-cli', () => { ); }); + it('should only forward non-scanner env vars to Scanner CLI', async () => { + const stub = sinon.stub(process, 'env').value({ + SONAR_TOKEN: 'sqa_somtoken', + SONAR_SCANNER_SOME_VAR: 'some_value', + CIRRUS_CI_SOME_VAR: 'some_value', + }); + + await runScannerCli({}, MOCK_PROPERTIES, 'sonar-scanner'); + + expect(spawn).toHaveBeenCalledTimes(1); + const [, , options] = (spawn as jest.Mock).mock.calls.pop(); + expect(options.env).toEqual({ + SONARQUBE_SCANNER_PARAMS: JSON.stringify(MOCK_PROPERTIES), + CIRRUS_CI_SOME_VAR: 'some_value', + }); + + stub.restore(); + }); + it('should pass proxy options to scanner', async () => { await runScannerCli( {}, From 1636eed982b6d14159798a2cd296be977e73d6b6 Mon Sep 17 00:00:00 2001 From: Benjamin Raymond <31401273+7PH@users.noreply.github.com> Date: Wed, 8 May 2024 11:31:47 +0200 Subject: [PATCH 28/35] SCANNPM-2 Validation fixes (#140) Co-authored-by: Lucas Paulger --- .npmignore | 142 --------------------------- ca.pem | 0 package.json | 3 +- src/constants.ts | 9 +- src/java.ts | 14 ++- src/properties.ts | 27 ++++- src/request.ts | 3 + src/scanner-engine.ts | 36 ++++--- src/types.ts | 10 +- test/unit/java.test.ts | 18 ++-- test/unit/properties.test.ts | 34 +++++++ test/unit/scanner-engine.test.ts | 40 +++++--- tools/orchestrator/package-lock.json | 42 +++++--- 13 files changed, 174 insertions(+), 204 deletions(-) delete mode 100644 .npmignore delete mode 100644 ca.pem diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 4133b4fe..00000000 --- a/.npmignore +++ /dev/null @@ -1,142 +0,0 @@ -# Created by https://www.gitignore.io/api/node,SonarQube,intellij+all - -### Intellij+all ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff: -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/dictionaries - -# Sensitive or high-churn files: -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.xml -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml - -# Gradle: -.idea/**/gradle.xml -.idea/**/libraries - -# CMake -cmake-build-debug/ - -# Mongo Explorer plugin: -.idea/**/mongoSettings.xml - -## File-based project format: -*.iws - -## Plugin-specific files: - -# IntelliJ -/out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -### Intellij+all Patch ### -# Ignores the whole idea folder -# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 - -.idea/ - - -### VS Code ### -.vscode/ - -### Node ### -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -test-report.xml - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (http://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Typescript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# test report file -xunit.xml - - -### SonarQube ### -# SonarQube ignore files. -# -# https://docs.sonarqube.org/display/SCAN/Analyzing+with+SonarQube+Scanner -# Sonar Scanner working directories -.sonar/ -.scannerwork/ - -# SonarLint working directories, configuration files (including credentials) -.sonarlint/ - -# End of https://www.gitignore.io/api/node,SonarQube,intellij+all - -!test/**/fixtures/**/* - -# MacOS -.DS_Store diff --git a/ca.pem b/ca.pem deleted file mode 100644 index e69de29b..00000000 diff --git a/package.json b/package.json index e3290777..1023a004 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "arrowParens": "avoid" }, "files": [ - "build/**" + "build/**", + "bin/**" ] } diff --git a/src/constants.ts b/src/constants.ts index dce67a06..1e64f903 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -17,7 +17,6 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import path from 'path'; import { ScannerProperty } from './types'; export const SCANNER_BOOTSTRAPPER_NAME = 'ScannerNpm'; @@ -65,3 +64,11 @@ export const SCANNER_CLI_MIRROR = export const SCANNER_CLI_INSTALL_PATH = 'native-sonar-scanner'; export const WINDOWS_WHERE_EXE_PATH = 'C:\\Windows\\System32\\where.exe'; + +export const SCANNER_DEPRECATED_PROPERTIES: ScannerProperty[][] = [ + [ScannerProperty.SonarWsTimeout, ScannerProperty.SonarScannerResponseTimeout], + [ScannerProperty.HttpProxyHost, ScannerProperty.SonarScannerProxyHost], + [ScannerProperty.HttpProxyPort, ScannerProperty.SonarScannerProxyPort], + [ScannerProperty.HttpProxyUser, ScannerProperty.SonarScannerProxyUser], + [ScannerProperty.HttpProxyPassword, ScannerProperty.SonarScannerProxyPassword], +]; diff --git a/src/java.ts b/src/java.ts index 7db691c9..4b32f080 100644 --- a/src/java.ts +++ b/src/java.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ - +import fsExtra from 'fs-extra'; import path from 'path'; import semver, { SemVer } from 'semver'; import { @@ -88,8 +88,7 @@ export async function serverSupportsJREProvisioning( properties: ScannerProperties, ): Promise { if (properties[ScannerProperty.SonarScannerInternalIsSonarCloud] === 'true') { - //TODO: return to true once SC has the new provisioning mechanism in place - return false; + return true; } // SonarQube @@ -107,7 +106,7 @@ export async function serverSupportsJREProvisioning( export async function fetchJRE(properties: ScannerProperties): Promise { log(LogLevel.DEBUG, 'Detecting latest version of JRE'); const jreMetaData = await fetchLatestSupportedJRE(properties); - log(LogLevel.INFO, 'Latest Supported JRE: ', jreMetaData); + log(LogLevel.DEBUG, 'Latest Supported JRE: ', jreMetaData); log(LogLevel.DEBUG, 'Looking for Cached JRE'); const cachedJrePath = await getCacheFileLocation(properties, { @@ -132,7 +131,12 @@ export async function fetchJRE(properties: ScannerProperties): Promise { const url = jreMetaData.downloadUrl ?? `${API_V2_JRE_ENDPOINT}/${jreMetaData.id}`; await download(url, archivePath); - await validateChecksum(archivePath, jreMetaData.sha256); + try { + await validateChecksum(archivePath, jreMetaData.sha256); + } catch (error) { + await fsExtra.remove(archivePath); + throw error; + } await extractArchive(archivePath, jreDirPath); return path.join(jreDirPath, jreMetaData.javaPath); } diff --git a/src/properties.ts b/src/properties.ts index bbfe6970..49693dc1 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -28,6 +28,7 @@ import { ENV_VAR_PREFIX, NPM_CONFIG_ENV_VAR_PREFIX, SCANNER_BOOTSTRAPPER_NAME, + SCANNER_DEPRECATED_PROPERTIES, SONARCLOUD_API_BASE_URL, SONARCLOUD_URL, SONARCLOUD_URL_REGEX, @@ -363,6 +364,28 @@ function getHttpProxyEnvProperties(serverUrl: string): ScannerProperties { return properties; } +function hotfixDeprecatedProperties(properties: ScannerProperties): ScannerProperties { + for (const [oldProp, newProp] of SCANNER_DEPRECATED_PROPERTIES) { + if (typeof properties[oldProp] !== 'undefined') { + if (typeof properties[newProp] === 'undefined') { + log( + LogLevel.WARN, + `Property "${oldProp}" is deprecated and will be removed in a future version. Please use "${newProp}" instead.`, + ); + properties[newProp] = properties[oldProp]; + } else { + log( + LogLevel.WARN, + `Both properties "${oldProp}" and "${newProp}" are set. "${oldProp}" is deprecated and will be removed in a future version. Value of deprecated property "${oldProp}" will be ignored.`, + ); + properties[oldProp] = properties[newProp]; + } + } + } + + return properties; +} + export function getProperties( scanOptions: ScanOptions, startTimestampMs: number, @@ -416,11 +439,11 @@ export function getProperties( // Hotfix host properties with custom SonarCloud URL const hostProperties = getHostProperties(properties); - return { + return hotfixDeprecatedProperties({ ...properties, // Can't be overridden: ...hostProperties, ...getBootstrapperProperties(startTimestampMs), 'sonar.projectBaseDir': projectBaseDir, - }; + }); } diff --git a/src/request.ts b/src/request.ts index 53bf1ece..7dbbcab3 100644 --- a/src/request.ts +++ b/src/request.ts @@ -149,6 +149,9 @@ export async function download(url: string, destPath: string, overrides?: AxiosR url, method: 'GET', responseType: 'stream', + headers: { + Accept: 'application/octet-stream', + }, ...overrides, }); diff --git a/src/scanner-engine.ts b/src/scanner-engine.ts index ef9b2ea6..ae38a79d 100644 --- a/src/scanner-engine.ts +++ b/src/scanner-engine.ts @@ -17,15 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import fsExtra from 'fs-extra'; import { spawn } from 'child_process'; import fs from 'fs'; import { API_V2_SCANNER_ENGINE_ENDPOINT } from './constants'; -import { - extractArchive, - getCacheDirectories, - getCacheFileLocation, - validateChecksum, -} from './file'; +import { getCacheDirectories, getCacheFileLocation, validateChecksum } from './file'; import { LogLevel, log, logWithPrefix } from './logging'; import { proxyUrlToJavaOptions } from './proxy'; import { download, fetch } from './request'; @@ -54,30 +50,33 @@ export async function fetchScannerEngine(properties: ScannerProperties) { properties[ScannerProperty.SonarScannerWasEngineCacheHit] = 'false'; - const { archivePath, unarchivePath: scannerEnginePath } = await getCacheDirectories(properties, { + const { archivePath } = await getCacheDirectories(properties, { checksum, filename, }); const url = downloadUrl ?? API_V2_SCANNER_ENGINE_ENDPOINT; log(LogLevel.DEBUG, `Starting download of Scanner Engine`); await download(url, archivePath); - log(LogLevel.INFO, `Downloaded Scanner Engine to ${scannerEnginePath}`); + log(LogLevel.INFO, `Downloaded Scanner Engine to ${archivePath}`); - await validateChecksum(archivePath, checksum); + try { + await validateChecksum(archivePath, checksum); + } catch (error) { + await fsExtra.remove(archivePath); + throw error; + } - log(LogLevel.INFO, `Extracting Scanner Engine to ${scannerEnginePath}`); - await extractArchive(archivePath, scannerEnginePath); - return scannerEnginePath; + return archivePath; } async function logOutput(message: string) { try { // Try and assume the log comes from the scanner engine const parsed = JSON.parse(message) as ScannerLogEntry; - logWithPrefix(parsed.level, 'ScannerEngine', parsed.formattedMessage); - if (parsed.throwable) { + logWithPrefix(parsed.level, 'ScannerEngine', parsed.message); + if (parsed.stacktrace) { // Console.log without newline - process.stdout.write(parsed.throwable); + process.stdout.write(parsed.stacktrace); } } catch (e) { process.stdout.write(message); @@ -93,7 +92,12 @@ export function runScannerEngine( log(LogLevel.INFO, 'Running the Scanner Engine'); // The scanner engine expects a JSON object of properties attached to a key name "scannerProperties" - const propertiesJSON = JSON.stringify({ scannerProperties: properties }); + const propertiesJSON = JSON.stringify({ + scannerProperties: Object.entries(properties).map(([key, value]) => ({ + key, + value, + })), + }); // Run the scanner-engine const args = [ diff --git a/src/types.ts b/src/types.ts index df86e93c..4912dccd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -23,8 +23,8 @@ export type CacheFileData = { checksum: string; filename: string }; export type ScannerLogEntry = { level: LogLevel; - formattedMessage: string; - throwable?: string; + message: string; + stacktrace?: string; }; export enum ScannerProperty { @@ -58,6 +58,12 @@ export enum ScannerProperty { SonarScannerInternalSqVersion = 'sonar.scanner.internal.sqVersion', SonarScannerCliVersion = 'sonar.scanner.version', SonarScannerCliMirror = 'sonar.scanner.mirror', + // Deprecated properties: + SonarWsTimeout = 'sonar.ws.timeout', + HttpProxyHost = 'http.proxyHost', + HttpProxyPort = 'http.proxyPort', + HttpProxyUser = 'http.proxyUser', + HttpProxyPassword = 'http.proxyPassword', } export type ScannerProperties = { diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index 72a1f5ca..3ba4fcef 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -19,8 +19,7 @@ */ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import fs from 'fs'; -import path from 'path'; +import fsExtra from 'fs-extra'; import { LogLevel, log } from '../../src/logging'; import { API_V2_JRE_ENDPOINT, SONARQUBE_JRE_PROVISIONING_MIN_VERSION } from '../../src/constants'; import * as file from '../../src/file'; @@ -101,7 +100,7 @@ describe('java', () => { ...MOCKED_PROPERTIES, [ScannerProperty.SonarScannerInternalIsSonarCloud]: 'true', }), - ).toBe(false); // TODO: return to true once SC has the new provisioning mechanism in place + ).toBe(true); }); it(`should return true for SQ version >= ${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`, async () => { @@ -139,10 +138,6 @@ describe('java', () => { }, }) .reply(200, serverResponse); - - mock - .onGet(`${API_V2_JRE_ENDPOINT}/${serverResponse[0].id}`) - .reply(200, fs.createReadStream(path.resolve(__dirname, '../unit/mocks/mock-jre.tar.gz'))); }); describe('when the JRE is cached', () => { @@ -191,6 +186,15 @@ describe('java', () => { expect(properties[ScannerProperty.SonarScannerWasJreCacheHit]).toBe(CacheStatus.Miss); }); + it('should remove file when checksum does not match', async () => { + jest.spyOn(file, 'validateChecksum').mockRejectedValue(new Error()); + jest.spyOn(fsExtra, 'remove'); + + await expect(fetchJRE(MOCKED_PROPERTIES)).rejects.toBeDefined(); + + expect(fsExtra.remove).toHaveBeenCalledWith('/mocked-archive-path'); + }); + it('should fail if no JRE matches', async () => { mock .onGet(API_V2_JRE_ENDPOINT, { diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index 898e2368..6123f6c7 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -481,6 +481,40 @@ describe('getProperties', () => { ); }); + it('should warn and replace deprecated properties', () => { + projectHandler.reset('fake_project_with_sonar_properties_file'); + projectHandler.setEnvironmentVariables({ + SONAR_SCANNER_JSON_PARAMS: JSON.stringify({ + 'sonar.ws.timeout': '000', + }), + }); + + const properties = getProperties( + { + options: { + 'sonar.scanner.responseTimeout': '111', + 'http.proxyHost': 'my-proxy.io', + }, + }, + projectHandler.getStartTime(), + ); + + expect(properties).toMatchObject({ + 'sonar.scanner.responseTimeout': '111', // Should not replace the deprecated property because its new version is also present + 'sonar.ws.timeout': '111', + 'sonar.scanner.proxyHost': 'my-proxy.io', // Should replace the deprecated property with the new one + 'http.proxyHost': 'my-proxy.io', + }); + expect(log).toHaveBeenCalledWith( + LogLevel.WARN, + 'Both properties "sonar.ws.timeout" and "sonar.scanner.responseTimeout" are set. "sonar.ws.timeout" is deprecated and will be removed in a future version. Value of deprecated property "sonar.ws.timeout" will be ignored.', + ); + expect(log).toHaveBeenCalledWith( + LogLevel.WARN, + 'Property "http.proxyHost" is deprecated and will be removed in a future version. Please use "sonar.scanner.proxyHost" instead.', + ); + }); + it('should set the [ScannerProperty.SonarScannerCliVersion] for all existing formats', () => { projectHandler.reset('fake_project_with_sonar_properties_file'); projectHandler.setEnvironmentVariables({ diff --git a/test/unit/scanner-engine.test.ts b/test/unit/scanner-engine.test.ts index b1983d6a..2419d9dd 100644 --- a/test/unit/scanner-engine.test.ts +++ b/test/unit/scanner-engine.test.ts @@ -22,6 +22,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { ChildProcess, spawn } from 'child_process'; import fs from 'fs'; +import fsExtra, { copySync } from 'fs-extra'; import sinon from 'sinon'; import { Readable } from 'stream'; import { API_V2_SCANNER_ENGINE_ENDPOINT } from '../../src/constants'; @@ -40,8 +41,8 @@ const MOCKED_PROPERTIES: ScannerProperties = { }; const MOCK_CACHE_DIRECTORIES = { - archivePath: 'mocked/path/to/sonar/cache/sha_test/scanner-engine-1.2.3.zip', - unarchivePath: 'mocked/path/to/sonar/cache/sha_test/scanner-engine-1.2.3.zip_extracted', + archivePath: 'mocked/path/to/sonar/cache/sha_test/scanner-engine-1.2.3.jar', + unarchivePath: 'mocked/path/to/sonar/cache/sha_test/scanner-engine-1.2.3.jar_extracted', }; jest.mock('../../src/constants', () => ({ ...jest.requireActual('../../src/constants'), @@ -62,7 +63,7 @@ describe('scanner-engine', () => { beforeEach(async () => { await request.initializeAxios(MOCKED_PROPERTIES); mock.onGet(API_V2_SCANNER_ENGINE_ENDPOINT).reply(200, { - filename: 'scanner-engine-1.2.3.zip', + filename: 'scanner-engine-1.2.3.jar', sha256: 'sha_test', } as AnalysisEngineResponseType); mock @@ -96,10 +97,21 @@ describe('scanner-engine', () => { expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { checksum: 'sha_test', - filename: 'scanner-engine-1.2.3.zip', + filename: 'scanner-engine-1.2.3.jar', }); }); + it('should remove file when checksum does not match', async () => { + jest.spyOn(file, 'validateChecksum').mockRejectedValue(new Error()); + jest.spyOn(fsExtra, 'remove'); + + await expect(fetchScannerEngine(MOCKED_PROPERTIES)).rejects.toBeDefined(); + + expect(fsExtra.remove).toHaveBeenCalledWith( + 'mocked/path/to/sonar/cache/sha_test/scanner-engine-1.2.3.jar', + ); + }); + describe('when the scanner engine is cached', () => { beforeEach(() => { jest.spyOn(file, 'getCacheFileLocation').mockResolvedValue('mocked/path/to/scanner-engine'); @@ -110,7 +122,7 @@ describe('scanner-engine', () => { expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { checksum: 'sha_test', - filename: 'scanner-engine-1.2.3.zip', + filename: 'scanner-engine-1.2.3.jar', }); expect(request.download).not.toHaveBeenCalled(); expect(file.extractArchive).not.toHaveBeenCalled(); @@ -125,13 +137,12 @@ describe('scanner-engine', () => { expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { checksum: 'sha_test', - filename: 'scanner-engine-1.2.3.zip', + filename: 'scanner-engine-1.2.3.jar', }); expect(request.download).toHaveBeenCalledTimes(1); - expect(file.extractArchive).toHaveBeenCalledTimes(1); expect(scannerEngine).toEqual( - 'mocked/path/to/sonar/cache/sha_test/scanner-engine-1.2.3.zip_extracted', + 'mocked/path/to/sonar/cache/sha_test/scanner-engine-1.2.3.jar', ); }); }); @@ -159,7 +170,10 @@ describe('scanner-engine', () => { expect(write).toHaveBeenCalledTimes(1); expect(write).toHaveBeenCalledWith( JSON.stringify({ - scannerProperties: MOCKED_PROPERTIES, + scannerProperties: Object.entries(MOCKED_PROPERTIES).map(([key, value]) => ({ + key, + value, + })), }), ); expect(spawn).toHaveBeenCalledWith('java', [ @@ -186,13 +200,13 @@ describe('scanner-engine', () => { const stdoutStub = sinon.stub(process.stdout, 'write').value(jest.fn()); const output = [ - JSON.stringify({ level: 'DEBUG', formattedMessage: 'the message' }), - JSON.stringify({ level: 'INFO', formattedMessage: 'another message' }), + JSON.stringify({ level: 'DEBUG', message: 'the message' }), + JSON.stringify({ level: 'INFO', message: 'another message' }), "some non-JSON message which shouldn't crash the bootstrapper", JSON.stringify({ level: 'ERROR', - formattedMessage: 'final message', - throwable: 'this is a throwable', + message: 'final message', + stacktrace: 'this is a stacktrace', }), ]; childProcessHandler.setOutput(output.join('\n')); diff --git a/tools/orchestrator/package-lock.json b/tools/orchestrator/package-lock.json index 66ecfa87..79284900 100644 --- a/tools/orchestrator/package-lock.json +++ b/tools/orchestrator/package-lock.json @@ -21,7 +21,7 @@ }, "node_modules/@types/mkdirp": { "version": "2.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/mkdirp/-/mkdirp-2.0.0.tgz", + "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-2.0.0.tgz", "integrity": "sha512-c/iUqMymAlxLAyIK3u5SzrwkrkyOdv1XDc91T+b5FsY7Jr6ERhUD19jJHOhPW4GD6tmN6mFEorfSdks525pwdQ==", "deprecated": "This is a stub types definition. mkdirp provides its own type definitions, so you do not need this installed.", "dev": true, @@ -31,7 +31,7 @@ }, "node_modules/@types/node": { "version": "20.11.30", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@types/node/-/node-20.11.30.tgz", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz", "integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==", "dev": true, "dependencies": { @@ -40,12 +40,12 @@ }, "node_modules/asynckit": { "version": "0.4.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/asynckit/-/asynckit-0.4.0.tgz", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { "version": "1.6.8", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/axios/-/axios-1.6.8.tgz", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", "dependencies": { "follow-redirects": "^1.15.6", @@ -55,7 +55,7 @@ }, "node_modules/combined-stream": { "version": "1.0.8", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/combined-stream/-/combined-stream-1.0.8.tgz", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dependencies": { "delayed-stream": "~1.0.0" @@ -66,7 +66,7 @@ }, "node_modules/delayed-stream": { "version": "1.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/delayed-stream/-/delayed-stream-1.0.0.tgz", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "engines": { "node": ">=0.4.0" @@ -74,8 +74,14 @@ }, "node_modules/follow-redirects": { "version": "1.15.6", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/follow-redirects/-/follow-redirects-1.15.6.tgz", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], "engines": { "node": ">=4.0" }, @@ -87,7 +93,7 @@ }, "node_modules/form-data": { "version": "4.0.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/form-data/-/form-data-4.0.0.tgz", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "dependencies": { "asynckit": "^0.4.0", @@ -100,7 +106,7 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mime-db/-/mime-db-1.52.0.tgz", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "engines": { "node": ">= 0.6" @@ -108,7 +114,7 @@ }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mime-types/-/mime-types-2.1.35.tgz", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dependencies": { "mime-db": "1.52.0" @@ -119,18 +125,21 @@ }, "node_modules/mkdirp": { "version": "3.0.1", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/mkdirp/-/mkdirp-3.0.1.tgz", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "bin": { "mkdirp": "dist/cjs/src/bin.js" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/prettier": { "version": "3.2.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/prettier/-/prettier-3.2.5.tgz", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "bin": { @@ -138,16 +147,19 @@ }, "engines": { "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/proxy-from-env": { "version": "1.1.0", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/typescript": { "version": "5.4.3", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/typescript/-/typescript-5.4.3.tgz", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz", "integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==", "dev": true, "bin": { @@ -160,7 +172,7 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/undici-types/-/undici-types-5.26.5.tgz", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true } From ee83965fff7365b4a0c36c9b65b99b1cdff6ea62 Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Mon, 13 May 2024 17:05:09 +0200 Subject: [PATCH 29/35] SCANNPM-2 Polish logging and cleanup invalid cache (#141) --- src/constants.ts | 3 + src/file.ts | 34 ++++--- src/index.ts | 4 - src/java.ts | 17 ++-- src/logging.ts | 3 +- src/platform.ts | 9 +- src/properties.ts | 170 +++++++++++++++++++------------ src/proxy.ts | 6 +- src/request.ts | 24 +---- src/scan.ts | 4 +- src/scanner-cli.ts | 2 +- src/scanner-engine.ts | 32 +++--- src/types.ts | 23 ++++- test/unit/file.test.ts | 98 ++++++++++++------ test/unit/java.test.ts | 1 + test/unit/platform.test.ts | 8 +- test/unit/request.test.ts | 16 ++- test/unit/scanner-cli.test.ts | 2 +- test/unit/scanner-engine.test.ts | 10 +- 19 files changed, 288 insertions(+), 178 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 1e64f903..d6efd2ba 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -35,6 +35,9 @@ export const SONAR_CACHE_DIR = 'cache'; export const UNARCHIVE_SUFFIX = '_extracted'; +export const SONAR_SCANNER_ALIAS = 'SonarScanner Engine'; +export const JRE_ALIAS = 'JRE'; + export const ENV_VAR_PREFIX = 'SONAR_SCANNER_'; export const NPM_CONFIG_ENV_VAR_PREFIX = 'npm_config_sonar_scanner_'; diff --git a/src/file.ts b/src/file.ts index 0b24a775..24a5e9e0 100644 --- a/src/file.ts +++ b/src/file.ts @@ -20,8 +20,7 @@ import AdmZip from 'adm-zip'; import crypto from 'crypto'; -import fs from 'fs'; -import * as fsExtra from 'fs-extra'; +import fsExtra from 'fs-extra'; import path from 'path'; import tarStream from 'tar-stream'; import zlib from 'zlib'; @@ -31,20 +30,29 @@ import { CacheFileData, ScannerProperties, ScannerProperty } from './types'; export async function getCacheFileLocation( properties: ScannerProperties, - { checksum, filename }: CacheFileData, + { checksum, filename, alias }: CacheFileData, ) { const filePath = path.join(getParentCacheDirectory(properties), checksum, filename); - if (fs.existsSync(filePath)) { - log(LogLevel.INFO, 'Found Cached: ', filePath); + if (fsExtra.existsSync(filePath)) { + log(LogLevel.DEBUG, alias, 'version found in cache:', filename); + + // validate cache + try { + await validateChecksum(filePath, checksum); + } catch (error) { + await fsExtra.remove(filePath); + throw error; + } + return filePath; } else { - log(LogLevel.INFO, `No Cache found for ${filePath}`); + log(LogLevel.INFO, `No Cache found for ${alias}`); return null; } } export async function extractArchive(fromPath: string, toPath: string) { - log(LogLevel.INFO, `Extracting ${fromPath} to ${toPath}`); + log(LogLevel.DEBUG, `Extracting ${fromPath} to ${toPath}`); if (fromPath.endsWith('.tar.gz')) { const tarFilePath = fromPath; const extract = tarStream.extract(); @@ -57,7 +65,7 @@ export async function extractArchive(fromPath: string, toPath: string) { // Ensure the directory exists await fsExtra.ensureDir(path.dirname(filePath)); - stream.pipe(fs.createWriteStream(filePath, { mode: header.mode })); + stream.pipe(fsExtra.createWriteStream(filePath, { mode: header.mode })); // end of file, move onto next file stream.on('end', next); @@ -75,7 +83,7 @@ export async function extractArchive(fromPath: string, toPath: string) { }); }); - const readStream = fs.createReadStream(tarFilePath); + const readStream = fsExtra.createReadStream(tarFilePath); const gunzip = zlib.createGunzip(); const nextStep = readStream.pipe(gunzip); nextStep.pipe(extract); @@ -89,7 +97,7 @@ export async function extractArchive(fromPath: string, toPath: string) { async function generateChecksum(filepath: string) { return new Promise((resolve, reject) => { - fs.readFile(filepath, (err, data) => { + fsExtra.readFile(filepath, (err, data) => { if (err) { reject(err); return; @@ -101,7 +109,7 @@ async function generateChecksum(filepath: string) { export async function validateChecksum(filePath: string, expectedChecksum: string) { if (expectedChecksum) { - log(LogLevel.INFO, `Verifying checksum ${expectedChecksum}`); + log(LogLevel.DEBUG, `Verifying checksum ${expectedChecksum}`); const checksum = await generateChecksum(filePath); log(LogLevel.DEBUG, `Checksum Value: ${checksum}`); @@ -128,9 +136,9 @@ export async function getCacheDirectories( // Create destination directory if it doesn't exist const parentCacheDirectory = path.dirname(unarchivePath); - if (!fs.existsSync(parentCacheDirectory)) { + if (!fsExtra.existsSync(parentCacheDirectory)) { log(LogLevel.DEBUG, `Creating Cache directory as it doesn't exist: ${parentCacheDirectory}`); - fs.mkdirSync(parentCacheDirectory, { recursive: true }); + fsExtra.mkdirSync(parentCacheDirectory, { recursive: true }); } return { archivePath, unarchivePath }; diff --git a/src/index.ts b/src/index.ts index 926a29da..40eff11d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -22,10 +22,6 @@ import { ScanOptions } from './types'; export { scan }; -/** - * TODO: SCANNPM-8 Ensure backwards compatibility and assess what to re-export - */ - export function customScanner(scanOptions: ScanOptions) { return scan({ ...scanOptions, diff --git a/src/java.ts b/src/java.ts index 4b32f080..9a99870f 100644 --- a/src/java.ts +++ b/src/java.ts @@ -24,6 +24,7 @@ import { API_OLD_VERSION_ENDPOINT, API_V2_JRE_ENDPOINT, API_V2_VERSION_ENDPOINT, + JRE_ALIAS, SONARQUBE_JRE_PROVISIONING_MIN_VERSION, UNARCHIVE_SUFFIX, } from './constants'; @@ -96,7 +97,7 @@ export async function serverSupportsJREProvisioning( const SQServerInfo = semver.coerce(properties[ScannerProperty.SonarScannerInternalSqVersion]) ?? (await fetchServerVersion(properties)); - log(LogLevel.INFO, 'SonarQube server version: ', SQServerInfo.version); + log(LogLevel.INFO, 'SonarQube server version:', SQServerInfo.version); const supports = semver.satisfies(SQServerInfo, `>=${SONARQUBE_JRE_PROVISIONING_MIN_VERSION}`); log(LogLevel.DEBUG, `SonarQube Server v${SQServerInfo} supports JRE provisioning: ${supports}`); @@ -111,26 +112,31 @@ export async function fetchJRE(properties: ScannerProperties): Promise { log(LogLevel.DEBUG, 'Looking for Cached JRE'); const cachedJrePath = await getCacheFileLocation(properties, { checksum: jreMetaData.sha256, - filename: jreMetaData.filename + UNARCHIVE_SUFFIX, + filename: jreMetaData.filename, + alias: JRE_ALIAS, }); properties[ScannerProperty.SonarScannerWasJreCacheHit] = cachedJrePath ? CacheStatus.Hit : CacheStatus.Miss; if (cachedJrePath) { - log(LogLevel.INFO, 'Using Cached JRE'); - return path.join(cachedJrePath, jreMetaData.javaPath); + log(LogLevel.INFO, 'Using JRE from the cache'); + return path.join(cachedJrePath + UNARCHIVE_SUFFIX, jreMetaData.javaPath); } // JRE not found in cache. Download it. const { archivePath, unarchivePath: jreDirPath } = await getCacheDirectories(properties, { checksum: jreMetaData.sha256, filename: jreMetaData.filename, + alias: JRE_ALIAS, }); // If the JRE has a download URL, download it const url = jreMetaData.downloadUrl ?? `${API_V2_JRE_ENDPOINT}/${jreMetaData.id}`; + log(LogLevel.DEBUG, `Starting download of ${JRE_ALIAS}`); await download(url, archivePath); + log(LogLevel.INFO, `Downloaded ${JRE_ALIAS} to ${archivePath}`); + try { await validateChecksum(archivePath, jreMetaData.sha256); } catch (error) { @@ -147,7 +153,7 @@ async function fetchLatestSupportedJRE( const os = properties[ScannerProperty.SonarScannerOs]; const arch = properties[ScannerProperty.SonarScannerArch]; - log(LogLevel.DEBUG, `Downloading JRE for ${os} ${arch} from ${API_V2_JRE_ENDPOINT}`); + log(LogLevel.DEBUG, `Downloading JRE information for ${os} ${arch} from ${API_V2_JRE_ENDPOINT}`); const { data } = await fetch({ url: API_V2_JRE_ENDPOINT, @@ -161,6 +167,5 @@ async function fetchLatestSupportedJRE( throw new Error(`No JREs available for your platform ${os} ${arch}`); } - log(LogLevel.DEBUG, 'JRE Information', data); return data[0]; } diff --git a/src/logging.ts b/src/logging.ts index 3a087111..fe192b89 100644 --- a/src/logging.ts +++ b/src/logging.ts @@ -35,6 +35,7 @@ const logLevelValues = { }; const DEFAULT_LOG_LEVEL = LogLevel.INFO; +const LOG_MESSAGE_PADDING = 7; let logLevel = DEFAULT_LOG_LEVEL; @@ -47,7 +48,7 @@ export function logWithPrefix(level: LogLevel, prefix: string, ...message: unkno return; } - const levelStr = `[${level}]`.padEnd(7); + const levelStr = `[${level}]`.padEnd(LOG_MESSAGE_PADDING); console.log(levelStr, `${prefix}:`, ...message); } diff --git a/src/platform.ts b/src/platform.ts index 559f4f7d..f62b8ccb 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import fs from 'fs'; +import fsExtra from 'fs-extra'; import { LogLevel, log } from './logging'; export function getArch(): NodeJS.Architecture { @@ -47,17 +47,18 @@ function isAlpineLinux(): boolean { } let content: string | undefined; try { - const fileContent = fs.readFileSync('/etc/os-release'); + const fileContent = fsExtra.readFileSync('/etc/os-release'); content = fileContent.toString(); } catch (error) { try { - const fileContent = fs.readFileSync('/usr/lib/os-release'); + const fileContent = fsExtra.readFileSync('/usr/lib/os-release'); content = fileContent.toString(); } catch (error) { log(LogLevel.WARN, 'Failed to read /etc/os-release or /usr/lib/os-release'); } } - return !!content && (content.match(/^ID=([^\u001b\r\n]*)/m) || [])[1] === 'alpine'; + const match = /^ID=([^\r\n]*)/m.exec(content ?? ''); + return !!content && (match ? match[1] === 'alpine' : false); } export function getSupportedOS(): NodeJS.Platform | 'alpine' { diff --git a/src/properties.ts b/src/properties.ts index 49693dc1..ae7cbd46 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import fs from 'fs'; +import fsExtra from 'fs-extra'; import path from 'path'; import { getProxyForUrl } from 'proxy-from-env'; import slugify from 'slugify'; @@ -37,7 +37,14 @@ import { } from './constants'; import { LogLevel, log } from './logging'; import { getArch, getSupportedOS } from './platform'; -import { CacheStatus, CliArgs, ScanOptions, ScannerProperties, ScannerProperty } from './types'; +import { + CacheStatus, + CliArgs, + PackageJson, + ScanOptions, + ScannerProperties, + ScannerProperty, +} from './types'; function getDefaultProperties(): ScannerProperties { return { @@ -84,83 +91,114 @@ function getPackageJsonProperties( sonarBaseExclusions: string, ): ScannerProperties { const packageJsonParams: { [key: string]: string } = {}; - const packageFile = path.join(projectBaseDir, 'package.json'); - let packageData; - try { - packageData = fs.readFileSync(packageFile).toString(); - } catch (error) { - log(LogLevel.INFO, `Unable to read "package.json" file`); + const pkg = readPackageJson(projectBaseDir); + if (!pkg) { return { [ScannerProperty.SonarExclusions]: sonarBaseExclusions, }; } - const pkg = JSON.parse(packageData); + log(LogLevel.INFO, 'Retrieving info from "package.json" file'); + populatePackageParams(packageJsonParams, pkg); + populateCoverageParams(packageJsonParams, pkg, projectBaseDir, sonarBaseExclusions); + populateTestExecutionParams(packageJsonParams, pkg, projectBaseDir); - function fileExistsInProjectSync(file: string) { - return fs.existsSync(path.join(projectBaseDir, file)); - } + return packageJsonParams; +} - function dependenceExists(pkgName: string) { - return ['devDependencies', 'dependencies', 'peerDependencies'].some(function (prop) { - return pkg[prop] && pkgName in pkg[prop]; - }); +function readPackageJson(projectBaseDir: string): PackageJson | null { + const packageFile = path.join(projectBaseDir, 'package.json'); + try { + const packageData = fsExtra.readFileSync(packageFile).toString(); + return JSON.parse(packageData); + } catch (error) { + log(LogLevel.INFO, `Unable to read "package.json" file`); + return null; } +} - if (pkg) { - const invalidCharacterRegex = /[?$*+~.()'"!:@/]/g; - packageJsonParams['sonar.projectKey'] = slugify(pkg.name, { - remove: invalidCharacterRegex, - }); - packageJsonParams['sonar.projectName'] = pkg.name; - packageJsonParams['sonar.projectVersion'] = pkg.version; - if (pkg.description) { - packageJsonParams['sonar.projectDescription'] = pkg.description; - } - if (pkg.homepage) { - packageJsonParams['sonar.links.homepage'] = pkg.homepage; - } - if (pkg.bugs?.url) { - packageJsonParams['sonar.links.issue'] = pkg.bugs.url; - } - if (pkg.repository?.url) { - packageJsonParams['sonar.links.scm'] = pkg.repository.url; - } +function fileExistsInProjectSync(projectBaseDir: string, file: string): boolean { + return fsExtra.existsSync(path.join(projectBaseDir, file)); +} - const potentialCoverageDirs = [ - // jest coverage output directory - // See: http://facebook.github.io/jest/docs/en/configuration.html#coveragedirectory-string - pkg['nyc']?.['report-dir'], - // nyc coverage output directory - // See: https://github.com/istanbuljs/nyc#configuring-nyc - pkg['jest']?.['coverageDirectory'], - ] - .filter(Boolean) - .concat( - // default coverage output directory - 'coverage', - ); - const uniqueCoverageDirs = Array.from(new Set(potentialCoverageDirs)); - packageJsonParams[ScannerProperty.SonarExclusions] = sonarBaseExclusions; - for (const lcovReportDir of uniqueCoverageDirs) { - const lcovReportPath = path.posix.join(lcovReportDir, 'lcov.info'); - if (fileExistsInProjectSync(lcovReportPath)) { - packageJsonParams[ScannerProperty.SonarExclusions] += - (packageJsonParams[ScannerProperty.SonarExclusions].length > 0 ? ',' : '') + - path.posix.join(lcovReportDir, '**'); - // https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/ - packageJsonParams['sonar.javascript.lcov.reportPaths'] = lcovReportPath; - // TODO: use Generic Test Data to remove dependence of SonarJS, it is need transformation lcov to sonar generic coverage format - } - } +function dependenceExists(pkg: PackageJson, pkgName: string): boolean { + return ['devDependencies', 'dependencies', 'peerDependencies'].some(function (prop) { + const dependencyGroup = pkg[prop]; + return ( + typeof dependencyGroup === 'object' && dependencyGroup !== null && pkgName in dependencyGroup + ); + }); +} - if (dependenceExists('mocha-sonarqube-reporter') && fileExistsInProjectSync('xunit.xml')) { - // https://docs.sonarqube.org/display/SONAR/Generic+Test+Data - packageJsonParams['sonar.testExecutionReportPaths'] = 'xunit.xml'; +function populatePackageParams(params: { [key: string]: string | {} }, pkg: PackageJson) { + const invalidCharacterRegex = /[?$*+~.()'"!:@/]/g; + params['sonar.projectKey'] = slugify(pkg.name, { + remove: invalidCharacterRegex, + }); + params['sonar.projectName'] = pkg.name; + params['sonar.projectVersion'] = pkg.version; + if (pkg.description) { + params['sonar.projectDescription'] = pkg.description; + } + if (pkg.homepage) { + params['sonar.links.homepage'] = pkg.homepage; + } + if (pkg.bugs?.url) { + params['sonar.links.issue'] = pkg.bugs.url; + } + if (pkg.repository?.url) { + params['sonar.links.scm'] = pkg.repository.url; + } +} + +function populateCoverageParams( + params: { [key: string]: string }, + pkg: PackageJson, + projectBaseDir: string, + sonarBaseExclusions: string, +) { + const potentialCoverageDirs = [ + // nyc coverage output directory + // See: https://github.com/istanbuljs/nyc#configuring-nyc + pkg['nyc']?.['report-dir'], + // jest coverage output directory + // See: http://facebook.github.io/jest/docs/en/configuration.html#coveragedirectory-string + pkg['jest']?.['coverageDirectory'], + ] + .filter(Boolean) + .concat( + // default coverage output directory + 'coverage', + ); + + const uniqueCoverageDirs = Array.from(new Set(potentialCoverageDirs)); + params[ScannerProperty.SonarExclusions] = sonarBaseExclusions; + for (const lcovReportDir of uniqueCoverageDirs) { + const lcovReportPath = lcovReportDir && path.posix.join(lcovReportDir, 'lcov.info'); + if (lcovReportPath && fileExistsInProjectSync(projectBaseDir, lcovReportPath)) { + params[ScannerProperty.SonarExclusions] += + (params[ScannerProperty.SonarExclusions].length > 0 ? ',' : '') + + path.posix.join(lcovReportDir, '**'); + // TODO: (SCANNPM-34) use Generic Test Data to remove dependence of SonarJS, it is need transformation lcov to sonar generic coverage format + params['sonar.javascript.lcov.reportPaths'] = lcovReportPath; + // https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/test-coverage/javascript-typescript-test-coverage/ } + } +} + +function populateTestExecutionParams( + params: { [key: string]: string }, + pkg: PackageJson, + projectBaseDir: string, +) { + if ( + dependenceExists(pkg, 'mocha-sonarqube-reporter') && + fileExistsInProjectSync(projectBaseDir, 'xunit.xml') + ) { + // https://docs.sonarqube.org/display/SONAR/Generic+Test+Data + params['sonar.testExecutionReportPaths'] = 'xunit.xml'; // TODO: (SCANNPM-13) use `glob` to lookup xunit format files and transformation to sonar generic report format } - return packageJsonParams; } /** @@ -195,7 +233,7 @@ function getSonarFileProperties(projectBaseDir: string): ScannerProperties { try { const sonarPropertiesFile = path.join(projectBaseDir, SONAR_PROJECT_FILENAME); const properties: ScannerProperties = {}; - const data = fs.readFileSync(sonarPropertiesFile).toString(); + const data = fsExtra.readFileSync(sonarPropertiesFile).toString(); const lines = data.split(/\r?\n/); for (const line of lines) { const trimmedLine = line.trim(); diff --git a/src/proxy.ts b/src/proxy.ts index 2c0e4996..24123ca8 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -21,6 +21,9 @@ import { URL } from 'url'; import { LogLevel, log } from './logging'; import { ScannerProperties, ScannerProperty } from './types'; +const DEFAULT_HTTPS_PROXY_PORT = 443; +const DEFAULT_HTTP_PROXY_PORT = 80; + export function getProxyUrl(properties: ScannerProperties): URL | undefined { const proxyHost = properties[ScannerProperty.SonarScannerProxyHost]; const serverUsesHttps = properties[ScannerProperty.SonarHostUrl].startsWith('https'); @@ -29,7 +32,8 @@ export function getProxyUrl(properties: ScannerProperties): URL | undefined { // We assume that the proxy protocol is the same as the endpoint. const protocol = serverUsesHttps ? 'https' : 'http'; const proxyPort = - properties[ScannerProperty.SonarScannerProxyPort] ?? (serverUsesHttps ? 443 : 80); + properties[ScannerProperty.SonarScannerProxyPort] ?? + (serverUsesHttps ? DEFAULT_HTTPS_PROXY_PORT : DEFAULT_HTTP_PROXY_PORT); const proxyUser = properties[ScannerProperty.SonarScannerProxyUser] ?? ''; const proxyPassword = properties[ScannerProperty.SonarScannerProxyPassword] ?? ''; const proxyUrl = new URL( diff --git a/src/request.ts b/src/request.ts index 7dbbcab3..86675aa2 100644 --- a/src/request.ts +++ b/src/request.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; -import fs from 'fs'; +import fsExtra from 'fs-extra'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import https from 'https'; import forge from 'node-forge'; @@ -71,7 +71,7 @@ export async function getHttpAgents( const truststorePath = properties[ScannerProperty.SonarScannerTruststorePath]; if (truststorePath) { log(LogLevel.DEBUG, `Using truststore at ${truststorePath}`); - const p12Base64 = await fs.promises.readFile(truststorePath, { encoding: 'base64' }); + const p12Base64 = await fsExtra.promises.readFile(truststorePath, { encoding: 'base64' }); try { const certs = await extractTruststoreCerts( p12Base64, @@ -87,7 +87,7 @@ export async function getHttpAgents( const keystorePath = properties[ScannerProperty.SonarScannerKeystorePath]; if (keystorePath) { log(LogLevel.DEBUG, `Using keystore at ${keystorePath}`); - httpsAgentOptions.pfx = await fs.promises.readFile(keystorePath); + httpsAgentOptions.pfx = await fsExtra.promises.readFile(keystorePath); httpsAgentOptions.passphrase = properties[ScannerProperty.SonarScannerKeystorePassword] ?? ''; } @@ -155,27 +155,13 @@ export async function download(url: string, destPath: string, overrides?: AxiosR ...overrides, }); - const totalLength = response.headers['content-length']; - - if (totalLength) { - let progress = 0; - - response.data.on('data', (chunk: any) => { - progress += chunk.length; - process.stdout.write( - `\r[INFO] Bootstrapper:: Downloaded ${Math.round((progress / totalLength) * 100)}%`, - ); - }); - } else { - log(LogLevel.INFO, 'Download started'); - } + log(LogLevel.INFO, 'Download starting...'); response.data.on('end', () => { - totalLength && process.stdout.write('\n'); log(LogLevel.INFO, 'Download complete'); }); - const writer = fs.createWriteStream(destPath); + const writer = fsExtra.createWriteStream(destPath); const streamPipeline = promisify(stream.pipeline); await streamPipeline(response.data, writer); response.data.pipe(writer); diff --git a/src/scan.ts b/src/scan.ts index b0d73590..187c84a6 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -63,9 +63,9 @@ async function runScan(scanOptions: ScanOptions, cliArgs?: CliArgs) { log(LogLevel.INFO, `Server URL: ${properties[ScannerProperty.SonarHostUrl]}`); log(LogLevel.INFO, `Version: ${version}`); - log(LogLevel.DEBUG, 'Check if Server supports JRE Provisioning'); + log(LogLevel.DEBUG, 'Check if Server supports JRE provisioning'); const supportsJREProvisioning = await serverSupportsJREProvisioning(properties); - log(LogLevel.INFO, `JRE Provisioning ${supportsJREProvisioning ? 'is' : 'is NOT'} supported`); + log(LogLevel.INFO, `JRE provisioning ${supportsJREProvisioning ? 'is' : 'is NOT'} supported`); if (!supportsJREProvisioning) { log(LogLevel.INFO, 'Falling back on using sonar-scanner-cli'); diff --git a/src/scanner-cli.ts b/src/scanner-cli.ts index 60b6a430..0713e9e7 100644 --- a/src/scanner-cli.ts +++ b/src/scanner-cli.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { spawn } from 'child_process'; -import * as fsExtra from 'fs-extra'; +import fsExtra from 'fs-extra'; import path from 'path'; import { ENV_TO_PROPERTY_NAME, diff --git a/src/scanner-engine.ts b/src/scanner-engine.ts index ae38a79d..52f6b02b 100644 --- a/src/scanner-engine.ts +++ b/src/scanner-engine.ts @@ -19,8 +19,7 @@ */ import fsExtra from 'fs-extra'; import { spawn } from 'child_process'; -import fs from 'fs'; -import { API_V2_SCANNER_ENGINE_ENDPOINT } from './constants'; +import { API_V2_SCANNER_ENGINE_ENDPOINT, SONAR_SCANNER_ALIAS } from './constants'; import { getCacheDirectories, getCacheFileLocation, validateChecksum } from './file'; import { LogLevel, log, logWithPrefix } from './logging'; import { proxyUrlToJavaOptions } from './proxy'; @@ -34,15 +33,19 @@ import { } from './types'; export async function fetchScannerEngine(properties: ScannerProperties) { - log(LogLevel.DEBUG, 'Detecting latest version of Scanner Engine'); + log(LogLevel.DEBUG, `Detecting latest version of ${SONAR_SCANNER_ALIAS}`); const { data } = await fetch({ url: API_V2_SCANNER_ENGINE_ENDPOINT }); const { sha256: checksum, filename, downloadUrl } = data; - log(LogLevel.INFO, 'Latest Supported Scanner Engine: ', filename); + log(LogLevel.DEBUG, `Latest ${SONAR_SCANNER_ALIAS} version:`, filename); - log(LogLevel.DEBUG, 'Looking for Cached Scanner Engine'); - const cachedScannerEngine = await getCacheFileLocation(properties, { checksum, filename }); + log(LogLevel.DEBUG, `Looking for Cached ${SONAR_SCANNER_ALIAS}`); + const cachedScannerEngine = await getCacheFileLocation(properties, { + checksum, + filename, + alias: SONAR_SCANNER_ALIAS, + }); if (cachedScannerEngine) { - log(LogLevel.INFO, 'Using Cached Scanner Engine'); + log(LogLevel.DEBUG, `Using ${SONAR_SCANNER_ALIAS} from the cache`); properties[ScannerProperty.SonarScannerWasEngineCacheHit] = 'true'; return cachedScannerEngine; @@ -53,11 +56,12 @@ export async function fetchScannerEngine(properties: ScannerProperties) { const { archivePath } = await getCacheDirectories(properties, { checksum, filename, + alias: SONAR_SCANNER_ALIAS, }); const url = downloadUrl ?? API_V2_SCANNER_ENGINE_ENDPOINT; - log(LogLevel.DEBUG, `Starting download of Scanner Engine`); + log(LogLevel.DEBUG, `Starting download of ${SONAR_SCANNER_ALIAS}`); await download(url, archivePath); - log(LogLevel.INFO, `Downloaded Scanner Engine to ${archivePath}`); + log(LogLevel.INFO, `Downloaded ${SONAR_SCANNER_ALIAS} to ${archivePath}`); try { await validateChecksum(archivePath, checksum); @@ -89,7 +93,7 @@ export function runScannerEngine( scanOptions: ScanOptions, properties: ScannerProperties, ) { - log(LogLevel.INFO, 'Running the Scanner Engine'); + log(LogLevel.DEBUG, `Running the ${SONAR_SCANNER_ALIAS}`); // The scanner engine expects a JSON object of properties attached to a key name "scannerProperties" const propertiesJSON = JSON.stringify({ @@ -117,13 +121,13 @@ export function runScannerEngine( args, }; log(LogLevel.INFO, 'Dumping data to file and exiting'); - return fs.promises.writeFile(dumpToFile, JSON.stringify(data, null, 2)); + return fsExtra.promises.writeFile(dumpToFile, JSON.stringify(data, null, 2)); } - log(LogLevel.DEBUG, 'Running scanner engine', javaBinPath, ...args); + log(LogLevel.DEBUG, `Running ${SONAR_SCANNER_ALIAS}`, javaBinPath, ...args); const child = spawn(javaBinPath, args); - log(LogLevel.DEBUG, 'Writing properties to scanner engine', propertiesJSON); + log(LogLevel.DEBUG, `Writing properties to ${SONAR_SCANNER_ALIAS}`, propertiesJSON); child.stdin.write(propertiesJSON); child.stdin.end(); @@ -133,7 +137,7 @@ export function runScannerEngine( return new Promise((resolve, reject) => { child.on('exit', code => { if (code === 0) { - log(LogLevel.INFO, 'Scanner engine finished successfully'); + log(LogLevel.DEBUG, 'Scanner engine finished successfully'); resolve(); } else { reject(new Error(`Scanner engine failed with code ${code}`)); diff --git a/src/types.ts b/src/types.ts index 4912dccd..846db68e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,7 +19,7 @@ */ import { LogLevel } from './logging'; -export type CacheFileData = { checksum: string; filename: string }; +export type CacheFileData = { checksum: string; filename: string; alias: string }; export type ScannerLogEntry = { level: LogLevel; @@ -110,3 +110,24 @@ export enum CacheStatus { Miss = 'miss', Disabled = 'disabled', } + +export type PackageJson = { + name: string; + version: string; + scripts?: { [key: string]: string }; + dependencies?: { [key: string]: string }; + devDependencies?: { [key: string]: string }; + [key: string]: unknown; + bugs: { + url: string; + email: string; + }; + repository: { + type: string; + url: string; + }; + jest?: { + coverageDirectory?: string; + }; + nyc?: { 'report-dir'?: string }; +}; diff --git a/test/unit/file.test.ts b/test/unit/file.test.ts index d207831e..996993f4 100644 --- a/test/unit/file.test.ts +++ b/test/unit/file.test.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import AdmZip from 'adm-zip'; -import fs from 'fs'; +import fsExtra from 'fs-extra'; import path from 'path'; import { PassThrough } from 'stream'; import * as tarStream from 'tar-stream'; @@ -36,11 +36,12 @@ const MOCKED_PROPERTIES = { [ScannerProperty.SonarUserHome]: '/sonar', }; -jest.mock('fs'); jest.mock('tar-stream'); jest.mock('zlib'); -jest.mock('fs', () => ({ +jest.mock('fs-extra', () => ({ + ensureDir: jest.fn(), + remove: jest.fn(), createReadStream: jest.fn(), createWriteStream: jest.fn(), existsSync: jest.fn(), @@ -48,10 +49,6 @@ jest.mock('fs', () => ({ mkdirSync: jest.fn(), })); -jest.mock('fs-extra', () => ({ - ensureDir: jest.fn(), -})); - jest.mock('adm-zip', () => { const MockAdmZip = jest.fn(); MockAdmZip.prototype.extractAllTo = jest.fn(); @@ -93,16 +90,16 @@ describe('file', () => { mockPassThroughStream.resume = jest.fn(); mockPassThroughStream.end = jest.fn(); - jest.spyOn(fs, 'createWriteStream').mockReturnValue({ + jest.spyOn(fsExtra, 'createWriteStream').mockReturnValue({ on: jest.fn(), once: jest.fn(), emit: jest.fn(), end: jest.fn(), write: jest.fn(), - } as unknown as fs.WriteStream); + } as unknown as fsExtra.WriteStream); jest - .spyOn(fs, 'createReadStream') - .mockReturnValue({ pipe: jest.fn().mockReturnThis() } as unknown as fs.ReadStream); + .spyOn(fsExtra, 'createReadStream') + .mockReturnValue({ pipe: jest.fn().mockReturnThis() } as unknown as fsExtra.ReadStream); jest .spyOn(tarStream, 'extract') .mockReturnValue({ on: mockOn } as unknown as tarStream.Extract); @@ -123,10 +120,10 @@ describe('file', () => { await extractArchive(mockFilePath, mockDestDir); - expect(fs.createReadStream).toHaveBeenCalledWith(mockFilePath); + expect(fsExtra.createReadStream).toHaveBeenCalledWith(mockFilePath); expect(zlib.createGunzip).toHaveBeenCalled(); expect(tarStream.extract).toHaveBeenCalled(); - expect(fs.createWriteStream).toHaveBeenCalledWith( + expect(fsExtra.createWriteStream).toHaveBeenCalledWith( path.join(mockDestDir, mockFileHeader.name), { mode: 511, @@ -148,7 +145,7 @@ describe('file', () => { describe('getCacheFileLocation', () => { it('should return the file path if the file exists', async () => { - const checksum = 'shahash'; + const checksum = 'e0ac3601005dfa1864f5392aabaf7d898b1b5bab854f1acb4491bcd806b76b0c'; const filename = 'file.txt'; const filePath = path.join( MOCKED_PROPERTIES[ScannerProperty.SonarUserHome], @@ -156,21 +153,53 @@ describe('file', () => { checksum, filename, ); + jest + .spyOn(fsExtra, 'readFile') + .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); + jest.spyOn(fsExtra, 'existsSync').mockReturnValue(true); - jest.spyOn(fs, 'existsSync').mockReturnValue(true); - - const result = await getCacheFileLocation(MOCKED_PROPERTIES, { checksum, filename }); + const result = await getCacheFileLocation(MOCKED_PROPERTIES, { + checksum, + filename, + alias: 'test', + }); expect(result).toEqual(filePath); }); + it('should validate and remove invalid cached file', async () => { + const checksum = 'server-checksum'; + const filename = 'file.txt'; + jest.spyOn(fsExtra, 'existsSync').mockReturnValue(true); + jest + .spyOn(fsExtra, 'readFile') + .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); + jest.spyOn(fsExtra, 'remove'); + + await expect( + getCacheFileLocation(MOCKED_PROPERTIES, { + checksum, + filename, + alias: 'test', + }), + ).rejects.toThrow( + 'Checksum verification failed for /sonar/cache/server-checksum/file.txt. Expected checksum server-checksum but got e0ac3601005dfa1864f5392aabaf7d898b1b5bab854f1acb4491bcd806b76b0c', + ); + + expect(fsExtra.remove).toHaveBeenCalledWith('/sonar/cache/server-checksum/file.txt'); + }); + it('should return null if the file does not exist', async () => { const checksum = 'shahash'; const filename = 'file.txt'; - jest.spyOn(fs, 'existsSync').mockReturnValue(false); + jest.spyOn(fsExtra, 'existsSync').mockReturnValue(false); - const result = await getCacheFileLocation(MOCKED_PROPERTIES, { checksum, filename }); + const result = await getCacheFileLocation(MOCKED_PROPERTIES, { + checksum, + filename, + alias: 'test', + }); expect(result).toBeNull(); }); @@ -179,7 +208,7 @@ describe('file', () => { describe('validateChecksum', () => { it('should read the file of the path provided', async () => { jest - .spyOn(fs, 'readFile') + .spyOn(fsExtra, 'readFile') .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); await validateChecksum( @@ -187,12 +216,12 @@ describe('file', () => { 'e0ac3601005dfa1864f5392aabaf7d898b1b5bab854f1acb4491bcd806b76b0c', ); - expect(fs.readFile).toHaveBeenCalledWith('path/to/file', expect.any(Function)); + expect(fsExtra.readFile).toHaveBeenCalledWith('path/to/file', expect.any(Function)); }); it('should throw an error if the checksum does not match', async () => { jest - .spyOn(fs, 'readFile') + .spyOn(fsExtra, 'readFile') .mockImplementation((path, cb) => cb(null, Buffer.from('file content'))); await expect(validateChecksum('path/to/file', 'invalidchecksum')).rejects.toThrow( @@ -206,7 +235,7 @@ describe('file', () => { it('should throw an error if the file cannot be read', async () => { jest - .spyOn(fs, 'readFile') + .spyOn(fsExtra, 'readFile') .mockImplementation((path, cb) => cb(new Error('File not found'), Buffer.from(''))); await expect(validateChecksum('path/to/file', 'checksum')).rejects.toThrow('File not found'); @@ -215,15 +244,16 @@ describe('file', () => { describe('getCacheDirectories', () => { it('should return the cache directories', async () => { - jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => true); - jest.spyOn(fs, 'mkdirSync'); + jest.spyOn(fsExtra, 'existsSync').mockImplementationOnce(() => true); + jest.spyOn(fsExtra, 'mkdirSync'); const { archivePath, unarchivePath } = await getCacheDirectories(MOCKED_PROPERTIES, { checksum: 'md5_test', filename: 'file.txt', + alias: 'test', }); - expect(fs.existsSync).toHaveBeenCalledWith(path.join('/', 'sonar', 'cache', 'md5_test')); - expect(fs.mkdirSync).not.toHaveBeenCalled(); + expect(fsExtra.existsSync).toHaveBeenCalledWith(path.join('/', 'sonar', 'cache', 'md5_test')); + expect(fsExtra.mkdirSync).not.toHaveBeenCalled(); expect(archivePath).toEqual(path.join('/', 'sonar', 'cache', 'md5_test', 'file.txt')); expect(unarchivePath).toEqual( @@ -231,12 +261,16 @@ describe('file', () => { ); }); it('should create the parent cache directory if it does not exist', async () => { - jest.spyOn(fs, 'existsSync').mockImplementationOnce(() => false); - jest.spyOn(fs, 'mkdirSync').mockImplementationOnce(() => undefined); - await getCacheDirectories(MOCKED_PROPERTIES, { checksum: 'md5_test', filename: 'file.txt' }); + jest.spyOn(fsExtra, 'existsSync').mockImplementationOnce(() => false); + jest.spyOn(fsExtra, 'mkdirSync').mockImplementationOnce(() => undefined); + await getCacheDirectories(MOCKED_PROPERTIES, { + checksum: 'md5_test', + filename: 'file.txt', + alias: 'test', + }); - expect(fs.existsSync).toHaveBeenCalledWith(path.join('/', 'sonar', 'cache', 'md5_test')); - expect(fs.mkdirSync).toHaveBeenCalledWith(path.join('/', 'sonar', 'cache', 'md5_test'), { + expect(fsExtra.existsSync).toHaveBeenCalledWith(path.join('/', 'sonar', 'cache', 'md5_test')); + expect(fsExtra.mkdirSync).toHaveBeenCalledWith(path.join('/', 'sonar', 'cache', 'md5_test'), { recursive: true, }); }); diff --git a/test/unit/java.test.ts b/test/unit/java.test.ts index 3ba4fcef..4b72dd83 100644 --- a/test/unit/java.test.ts +++ b/test/unit/java.test.ts @@ -38,6 +38,7 @@ const MOCKED_PROPERTIES: ScannerProperties = { [ScannerProperty.SonarHostUrl]: 'http://sonarqube.com', [ScannerProperty.SonarScannerOs]: 'linux', [ScannerProperty.SonarScannerArch]: 'arm64', + [ScannerProperty.SonarUserHome]: '/sonar', }; beforeEach(async () => { diff --git a/test/unit/platform.test.ts b/test/unit/platform.test.ts index 6c000602..3920dc21 100644 --- a/test/unit/platform.test.ts +++ b/test/unit/platform.test.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import fs from 'fs'; +import fsExtra from 'fs-extra'; import sinon from 'sinon'; import { log, LogLevel } from '../../src/logging'; import * as platform from '../../src/platform'; @@ -60,7 +60,7 @@ describe('getPlatformInfo', () => { it('detect alpine', () => { const platformStub = sinon.stub(process, 'platform').value('linux'); const archStub = sinon.stub(process, 'arch').value('x64'); - const fsReadStub = sinon.stub(fs, 'readFileSync'); + const fsReadStub = sinon.stub(fsExtra, 'readFileSync'); fsReadStub.withArgs('/etc/os-release').returns('NAME="Alpine Linux"\nID=alpine'); expect(platform.getSupportedOS()).toEqual('alpine'); @@ -74,7 +74,7 @@ describe('getPlatformInfo', () => { it('detect alpine with fallback', () => { const platformStub = sinon.stub(process, 'platform').value('linux'); const archStub = sinon.stub(process, 'arch').value('x64'); - const fsReadStub = sinon.stub(fs, 'readFileSync'); + const fsReadStub = sinon.stub(fsExtra, 'readFileSync'); fsReadStub.withArgs('/usr/lib/os-release').returns('NAME="Alpine Linux"\nID=alpine'); expect(platform.getSupportedOS()).toEqual('alpine'); @@ -88,7 +88,7 @@ describe('getPlatformInfo', () => { it('failed to detect alpine', () => { const platformStub = sinon.stub(process, 'platform').value('linux'); const archStub = sinon.stub(process, 'arch').value('x64'); - const fsReadStub = sinon.stub(fs, 'readFileSync'); + const fsReadStub = sinon.stub(fsExtra, 'readFileSync'); expect(platform.getSupportedOS()).toEqual('linux'); expect(platform.getArch()).toEqual('x64'); diff --git a/test/unit/request.test.ts b/test/unit/request.test.ts index d7679172..540aac1c 100644 --- a/test/unit/request.test.ts +++ b/test/unit/request.test.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import axios, { AxiosInstance } from 'axios'; -import fs from 'fs'; +import fsExtra from 'fs-extra'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import path from 'path'; import { SONARCLOUD_API_BASE_URL, SONARCLOUD_URL } from '../../src/constants'; @@ -67,7 +67,10 @@ describe('request', () => { const truststorePath = path.join(__dirname, 'fixtures', 'ssl', 'truststore.p12'); const truststorePass = 'password'; const certificatePath = path.join(__dirname, 'fixtures', 'ssl', 'ca.pem'); - const certificatePem = fs.readFileSync(certificatePath).toString().replace(/\n/g, '\r\n'); + const certificatePem = fsExtra + .readFileSync(certificatePath) + .toString() + .replace(/\n/g, '\r\n'); const { httpsAgent } = await getHttpAgents({ [ScannerProperty.SonarHostUrl]: SONARCLOUD_URL, @@ -126,7 +129,7 @@ describe('request', () => { [ScannerProperty.SonarScannerKeystorePassword]: keystorePass, }); - expect(httpsAgent?.options.pfx).toEqual(fs.readFileSync(keystorePath)); + expect(httpsAgent?.options.pfx).toEqual(fsExtra.readFileSync(keystorePath)); expect(httpsAgent?.options.passphrase).toBe(keystorePass); }); }); @@ -136,7 +139,10 @@ describe('request', () => { const truststorePath = path.join(__dirname, 'fixtures', 'ssl', 'truststore.p12'); const truststorePass = 'password'; const certificatePath = path.join(__dirname, 'fixtures', 'ssl', 'ca.pem'); - const certificatePem = fs.readFileSync(certificatePath).toString().replace(/\n/g, '\r\n'); + const certificatePem = fsExtra + .readFileSync(certificatePath) + .toString() + .replace(/\n/g, '\r\n'); const keystorePath = path.join(__dirname, 'fixtures', 'ssl', 'keystore.p12'); const keystorePass = 'password'; @@ -152,7 +158,7 @@ describe('request', () => { const ca = httpsAgent?.options.ca as string[]; expect(ca).toHaveLength(1); expect(ca).toContain(certificatePem); - expect(httpsAgent?.options.pfx).toEqual(fs.readFileSync(keystorePath)); + expect(httpsAgent?.options.pfx).toEqual(fsExtra.readFileSync(keystorePath)); expect(httpsAgent?.options.passphrase).toBe(keystorePass); expect(httpsAgent?.proxy.toString()).toBe('https://proxy.com/'); }); diff --git a/test/unit/scanner-cli.test.ts b/test/unit/scanner-cli.test.ts index f0c751b8..9a26b716 100644 --- a/test/unit/scanner-cli.test.ts +++ b/test/unit/scanner-cli.test.ts @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { spawn } from 'child_process'; -import * as fsExtra from 'fs-extra'; +import fsExtra from 'fs-extra'; import path from 'path'; import sinon from 'sinon'; import { SCANNER_CLI_INSTALL_PATH, SCANNER_CLI_VERSION } from '../../src/constants'; diff --git a/test/unit/scanner-engine.test.ts b/test/unit/scanner-engine.test.ts index 2419d9dd..2420abf1 100644 --- a/test/unit/scanner-engine.test.ts +++ b/test/unit/scanner-engine.test.ts @@ -21,11 +21,10 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { ChildProcess, spawn } from 'child_process'; -import fs from 'fs'; -import fsExtra, { copySync } from 'fs-extra'; +import fsExtra from 'fs-extra'; import sinon from 'sinon'; import { Readable } from 'stream'; -import { API_V2_SCANNER_ENGINE_ENDPOINT } from '../../src/constants'; +import { API_V2_SCANNER_ENGINE_ENDPOINT, SONAR_SCANNER_ALIAS } from '../../src/constants'; import * as file from '../../src/file'; import { logWithPrefix } from '../../src/logging'; import * as request from '../../src/request'; @@ -98,6 +97,7 @@ describe('scanner-engine', () => { expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { checksum: 'sha_test', filename: 'scanner-engine-1.2.3.jar', + alias: SONAR_SCANNER_ALIAS, }); }); @@ -123,6 +123,7 @@ describe('scanner-engine', () => { expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { checksum: 'sha_test', filename: 'scanner-engine-1.2.3.jar', + alias: SONAR_SCANNER_ALIAS, }); expect(request.download).not.toHaveBeenCalled(); expect(file.extractArchive).not.toHaveBeenCalled(); @@ -138,6 +139,7 @@ describe('scanner-engine', () => { expect(file.getCacheFileLocation).toHaveBeenCalledWith(MOCKED_PROPERTIES, { checksum: 'sha_test', filename: 'scanner-engine-1.2.3.jar', + alias: SONAR_SCANNER_ALIAS, }); expect(request.download).toHaveBeenCalledTimes(1); @@ -230,7 +232,7 @@ describe('scanner-engine', () => { it('should dump data to file when dumpToFile property is set', async () => { childProcessHandler.setExitCode(1); // Make it so the scanner would fail - const writeFile = jest.spyOn(fs.promises, 'writeFile').mockResolvedValue(); + const writeFile = jest.spyOn(fsExtra.promises, 'writeFile').mockResolvedValue(); await runScannerEngine( '/some/path/to/java', From 1c2efbce54ec6ced51325dd1bf2690fd9a0f4e75 Mon Sep 17 00:00:00 2001 From: 7PH Date: Thu, 23 May 2024 13:29:19 +0200 Subject: [PATCH 30/35] SCANNPM-35 Change default values and logic for inferring package.json and sonar-project.properties --- src/properties.ts | 34 ++++++--------- .../package.json | 7 +++ .../sonar-project.properties | 1 + test/unit/mocks/FakeProjectMock.ts | 3 +- test/unit/properties.test.ts | 43 ++++++++++--------- 5 files changed, 46 insertions(+), 42 deletions(-) create mode 100644 test/unit/fixtures/fake_project_with_package_and_sonar_properties/package.json create mode 100644 test/unit/fixtures/fake_project_with_package_and_sonar_properties/sonar-project.properties diff --git a/src/properties.ts b/src/properties.ts index ae7cbd46..b1ac3021 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -90,7 +90,6 @@ function getPackageJsonProperties( projectBaseDir: string, sonarBaseExclusions: string, ): ScannerProperties { - const packageJsonParams: { [key: string]: string } = {}; const pkg = readPackageJson(projectBaseDir); if (!pkg) { return { @@ -99,6 +98,9 @@ function getPackageJsonProperties( } log(LogLevel.INFO, 'Retrieving info from "package.json" file'); + const packageJsonParams: { [key: string]: string } = { + [ScannerProperty.SonarExclusions]: sonarBaseExclusions, + }; populatePackageParams(packageJsonParams, pkg); populateCoverageParams(packageJsonParams, pkg, projectBaseDir, sonarBaseExclusions); populateTestExecutionParams(packageJsonParams, pkg, projectBaseDir); @@ -227,6 +229,7 @@ function getCommandLineProperties(cliArgs?: CliArgs): ScannerProperties { /** * Parse properties stored in sonar project properties file, if it exists. + * Return an empty object if the file does not exist. */ function getSonarFileProperties(projectBaseDir: string): ScannerProperties { // Read sonar project properties file in project base dir @@ -246,8 +249,8 @@ function getSonarFileProperties(projectBaseDir: string): ScannerProperties { return properties; } catch (error) { - log(LogLevel.WARN, `Failed to read ${SONAR_PROJECT_FILENAME} file: ${error}`); - throw error; + log(LogLevel.DEBUG, `Failed to read ${SONAR_PROJECT_FILENAME} file: ${error}`); + return {}; } } @@ -439,27 +442,16 @@ export function getProperties( ...cliProperties, }; - // Compute default base dir respecting order of precedence we use for the final merge + // Compute default base dir and exclusions respecting order of precedence we use for the final merge const projectBaseDir = userProperties[ScannerProperty.SonarProjectBaseDir] ?? process.cwd(); + const baseSonarExclusions = + userProperties[ScannerProperty.SonarExclusions] ?? DEFAULT_SONAR_EXCLUSIONS; // Infer specific properties from project files - let inferredProperties: ScannerProperties; - try { - inferredProperties = getSonarFileProperties(projectBaseDir); - } catch (error) { - inferredProperties = { - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - }; - - const baseSonarExclusions = - userProperties[ScannerProperty.SonarExclusions] ?? DEFAULT_SONAR_EXCLUSIONS; - - inferredProperties = { - ...inferredProperties, - ...getPackageJsonProperties(projectBaseDir, baseSonarExclusions), - }; - } + const inferredProperties = { + ...getPackageJsonProperties(projectBaseDir, baseSonarExclusions), + ...getSonarFileProperties(projectBaseDir), + }; // Generate proxy properties from HTTP[S]_PROXY env variables, if not already set const httpProxyProperties = getHttpProxyEnvProperties( diff --git a/test/unit/fixtures/fake_project_with_package_and_sonar_properties/package.json b/test/unit/fixtures/fake_project_with_package_and_sonar_properties/package.json new file mode 100644 index 00000000..8c670ebd --- /dev/null +++ b/test/unit/fixtures/fake_project_with_package_and_sonar_properties/package.json @@ -0,0 +1,7 @@ +{ + "name": "that-is-the-project-key", + "version": "1.0.0", + "engines": { + "node": ">= 0.10" + } +} diff --git a/test/unit/fixtures/fake_project_with_package_and_sonar_properties/sonar-project.properties b/test/unit/fixtures/fake_project_with_package_and_sonar_properties/sonar-project.properties new file mode 100644 index 00000000..fc7405af --- /dev/null +++ b/test/unit/fixtures/fake_project_with_package_and_sonar_properties/sonar-project.properties @@ -0,0 +1 @@ +sonar.sources=the-sources diff --git a/test/unit/mocks/FakeProjectMock.ts b/test/unit/mocks/FakeProjectMock.ts index a6d919fc..c50ccdd0 100644 --- a/test/unit/mocks/FakeProjectMock.ts +++ b/test/unit/mocks/FakeProjectMock.ts @@ -19,7 +19,7 @@ */ import path from 'path'; import sinon from 'sinon'; -import { SCANNER_BOOTSTRAPPER_NAME } from '../../../src/constants'; +import { DEFAULT_SONAR_EXCLUSIONS, SCANNER_BOOTSTRAPPER_NAME } from '../../../src/constants'; import { CacheStatus } from '../../../src/types'; const baseEnvVariables = process.env; @@ -55,6 +55,7 @@ export class FakeProjectMock { getExpectedProperties() { return { + 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, 'sonar.projectBaseDir': this.projectPath, 'sonar.scanner.bootstrapStartTime': this.startTimeMs.toString(), 'sonar.scanner.app': SCANNER_BOOTSTRAPPER_NAME, diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index 6123f6c7..a2cdbec4 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -54,9 +54,6 @@ describe('getProperties', () => { 'sonar.host.url': SONARCLOUD_URL, 'sonar.scanner.apiBaseUrl': SONARCLOUD_API_BASE_URL, 'sonar.scanner.internal.isSonarCloud': 'true', - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, }); }); @@ -81,9 +78,6 @@ describe('getProperties', () => { 'sonar.scanner.apiBaseUrl': 'https://dev.sc-dev.io', 'sonar.scanner.internal.isSonarCloud': 'true', 'sonar.projectKey': 'use-this-project-key', - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, }); }); @@ -155,9 +149,7 @@ describe('getProperties', () => { 'sonar.javascript.lcov.reportPaths': 'coverage/lcov.info', 'sonar.projectKey': 'fake-basic-project', 'sonar.projectName': 'fake-basic-project', - 'sonar.projectDescription': 'No description.', 'sonar.projectVersion': '1.0.0', - 'sonar.sources': '.', 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS + ',coverage/**', 'sonar.scanner.app': SCANNER_BOOTSTRAPPER_NAME, 'sonar.scanner.appVersion': '1.2.3', @@ -187,9 +179,7 @@ describe('getProperties', () => { 'sonar.links.homepage': 'https://github.com/fake/project', 'sonar.links.issue': 'https://github.com/fake/project/issues', 'sonar.links.scm': 'git+https://github.com/fake/project.git', - 'sonar.sources': '.', 'sonar.testExecutionReportPaths': 'xunit.xml', - 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, }); }); @@ -209,9 +199,6 @@ describe('getProperties', () => { 'sonar.host.url': 'http://localhost/sonarqube', 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, }); }); @@ -231,9 +218,6 @@ describe('getProperties', () => { 'sonar.host.url': 'http://localhost/sonarqube', 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', 'sonar.scanner.internal.isSonarCloud': 'false', - 'sonar.projectDescription': 'No description.', - 'sonar.sources': '.', - 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS, 'sonar.projectKey': 'myfake-basic-project', 'sonar.projectName': '@my/fake-basic-project', 'sonar.projectVersion': '1.0.0', @@ -259,9 +243,7 @@ describe('getProperties', () => { 'sonar.javascript.lcov.reportPaths': 'jest-coverage/lcov.info', 'sonar.projectKey': 'fake-basic-project', 'sonar.projectName': 'fake-basic-project', - 'sonar.projectDescription': 'No description.', 'sonar.projectVersion': '1.0.0', - 'sonar.sources': '.', 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS + ',jest-coverage/**', }); }); @@ -285,9 +267,7 @@ describe('getProperties', () => { 'sonar.javascript.lcov.reportPaths': 'nyc-coverage/lcov.info', 'sonar.projectKey': 'fake-basic-project', 'sonar.projectName': 'fake-basic-project', - 'sonar.projectDescription': 'No description.', 'sonar.projectVersion': '1.0.0', - 'sonar.sources': '.', 'sonar.exclusions': DEFAULT_SONAR_EXCLUSIONS + ',nyc-coverage/**', }); }); @@ -638,6 +618,29 @@ describe('getProperties', () => { }); }); + it('should correctly merge package.json and sonar-project.properties', () => { + projectHandler.reset('fake_project_with_package_and_sonar_properties'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + serverUrl: 'http://localhost/sonarqube', + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': 'http://localhost/sonarqube', + 'sonar.scanner.apiBaseUrl': 'http://localhost/sonarqube/api/v2', + 'sonar.scanner.internal.isSonarCloud': 'false', + 'sonar.projectKey': 'that-is-the-project-key', + 'sonar.projectName': 'that-is-the-project-key', + 'sonar.projectVersion': '1.0.0', + 'sonar.sources': 'the-sources', + }); + }); + it('does not let user override bootstrapper-only properties', () => { projectHandler.reset('fake_project_with_sonar_properties_file'); projectHandler.setEnvironmentVariables({ From 4b76d52a60329ff0495b784a596723310b1f1a9d Mon Sep 17 00:00:00 2001 From: 7PH Date: Mon, 27 May 2024 10:53:06 +0200 Subject: [PATCH 31/35] SCANNPM-8 Reintroduce default export --- README.md | 9 +++++++-- src/index.ts | 12 ++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e51f53b..63a4d096 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The following example shows how to run an analysis on a JavaScript project, and pushing the results to a SonarQube instance: ```javascript -const scanner = require('sonarqube-scanner'); +const scanner = require('sonarqube-scanner').default; scanner( { @@ -47,7 +47,12 @@ scanner( 'sonar.tests': 'test', }, }, - () => process.exit(), + error => { + if (error) { + console.error(error); + } + process.exit(); + }, ); ``` diff --git a/src/index.ts b/src/index.ts index 40eff11d..8975f632 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,3 +28,15 @@ export function customScanner(scanOptions: ScanOptions) { localScannerCli: true, }); } + +function scanWithCallback(scanOptions: ScanOptions, callback: (error?: unknown) => void) { + return scan(scanOptions) + .then(() => { + callback(); + }) + .catch(error => { + callback(error); + }); +} + +export default scanWithCallback; From b31554fdba84680c2ca6b4dc1d8f9221ae24706e Mon Sep 17 00:00:00 2001 From: Lucas <97296331+lucas-paulger-sonarsource@users.noreply.github.com> Date: Wed, 29 May 2024 13:14:11 +0200 Subject: [PATCH 32/35] SCANNPM-7 Update Readme (#144) --- README.md | 8 +++++--- test/integration/scanner.test.js | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 63a4d096..895dd02c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ This module is analyzed on SonarCloud. ## Installation -_Prerequisite: Node v16+ (otherwise use sonarqube-scanner v2.9.1)_ +_Prerequisite: Node v18+ (for v4 and above)_ + +_Prerequisite: Node v16+ (for v3, otherwise use sonarqube-scanner v2.9.1)_ This package is available on npm as: `sonarqube-scanner` @@ -105,7 +107,7 @@ Similar to the above, you can specify analysis properties and settings using eit #### _I constantly get "Impossible to download and extract binary [...] In such situation, the best solution is to install the standard SonarScanner", what can I do?_ -You can install manually the [standard SonarScanner](https://redirect.sonarsource.com/doc/install-configure-scanner.html), +You can install manually the [standard SonarScanner](https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/scanners/sonarscanner/), which requires to have a Java Runtime Environment available too (Java 8+). It is important to make sure that the SonarScanner `$install_directory/bin` location is added to the system `$PATH` environment variable. This will ensure that `sonar-scanner` command will be resolved by the customScanner, and prevent the error: @@ -161,7 +163,7 @@ Proxy authentication is supported as well, see below. ## Specifying the cache folder By default, the scanner binaries are cached into `$HOME/.sonar/native-sonar-scanner` folder. -To use a custom cache fodler instead of `$HOME`, set `$SONAR_BINARY_CACHE`. +To use a custom cache folder instead of `$HOME`, set `$SONAR_BINARY_CACHE`. **Example:** diff --git a/test/integration/scanner.test.js b/test/integration/scanner.test.js index 5e33e65e..9d01169e 100644 --- a/test/integration/scanner.test.js +++ b/test/integration/scanner.test.js @@ -59,8 +59,8 @@ describe('scanner', function () { 'sonar.projectName': projectKey, 'sonar.projectKey': projectKey, 'sonar.log.level': 'DEBUG', - 'sonar.scanner.version': '6.0.0.4373', - 'sonar.scanner.mirror': `https://${process.env.ARTIFACTORY_PRIVATE_USERNAME}:${process.env.ARTIFACTORY_PRIVATE_PASSWORD}@repox.jfrog.io/artifactory/sonarsource-public-builds/org/sonarsource/scanner/cli/sonar-scanner-cli/6.0.0.4373/`, + 'sonar.scanner.version': '6.0.0.4419', + 'sonar.scanner.mirror': `https://${process.env.ARTIFACTORY_PRIVATE_USERNAME}:${process.env.ARTIFACTORY_PRIVATE_PASSWORD}@repox.jfrog.io/artifactory/sonarsource-public-builds/org/sonarsource/scanner/cli/sonar-scanner-cli/6.0.0.4419/`, 'sonar.sources': path.join( __dirname.replace(/\\+/g, '/'), '/fixtures/fake_project_for_integration/src', From c73a82c6f5e5d3a4f1d2ab6325c859f735fbadcc Mon Sep 17 00:00:00 2001 From: 7PH Date: Wed, 29 May 2024 14:53:15 +0200 Subject: [PATCH 33/35] SCANNPM-7 Fix documentation obsolete caPath & Add link to v3 documentation --- README.md | 8 ++++---- src/types.ts | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 895dd02c..da2edc68 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ This module is analyzed on SonarCloud. [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=SonarSource_sonar-scanner-npm&metric=alert_status)](https://sonarcloud.io/project/overview?id=SonarSource_sonar-scanner-npm) [![Maintainability](https://sonarcloud.io/api/project_badges/measure?project=SonarSource_sonar-scanner-npm&metric=sqale_rating)](https://sonarcloud.io/project/overview?id=SonarSource_sonar-scanner-npm) [![Reliability](https://sonarcloud.io/api/project_badges/measure?project=SonarSource_sonar-scanner-npm&metric=reliability_rating)](https://sonarcloud.io/project/overview?id=SonarSource_sonar-scanner-npm) [![Security](https://sonarcloud.io/api/project_badges/measure?project=SonarSource_sonar-scanner-npm&metric=security_rating)](https://sonarcloud.io/project/overview?id=SonarSource_sonar-scanner-npm) [![Releases](https://img.shields.io/github/release/SonarSource/sonar-scanner-npm.svg)](https://github.com/SonarSource/sonar-scanner-npm/releases) +This is the documentation for v4. If you are using v3, refer to [the v3 documentation](https://github.com/SonarSource/sonar-scanner-npm/tree/37797347a30635647da5a45ed912a9ae77405b85). + ## Installation _Prerequisite: Node v18+ (for v4 and above)_ @@ -41,7 +43,7 @@ const scanner = require('sonarqube-scanner').default; scanner( { serverUrl: 'https://sonarqube.mycompany.com', - token: '019d1e2e04eefdcd0caee1468f39a45e69d33d3f', // use "login" for SQ up to version 9 + token: '019d1e2e04eefdcd0caee1468f39a45e69d33d3f', options: { 'sonar.projectName': 'My App', 'sonar.projectDescription': 'Description for "My App" project...', @@ -64,9 +66,7 @@ scanner( - `parameters` _Map_ - `serverUrl` _String_ (optional) The URL of the SonarQube server. Defaults to http://localhost:9000 - - `login` _String_ (optional) The login used to connect to the SonarQube server up to version 9. Empty by default. - `token` _String_ (optional) The token used to connect to the SonarQube server v10+ or SonarCloud. Empty by default. - - `caPath` _String_ (optional) the path to a CA to pass as `https.request()` [options](https://nodejs.org/api/https.html#https_https_request_options_callback). - `options` _Map_ (optional) Used to pass extra parameters for the analysis. See the [official documentation](http://redirect.sonarsource.com/doc/analysis-parameters.html) for more details. - `callback` _Function_ (optional) Callback (the execution of the analysis is asynchronous). @@ -132,7 +132,7 @@ It needs to be [installed manually](https://laptrinhx.com/docker-for-mac-alpine- Thanks to [Philipp Eschenbach](https://github.com/peh) for troubleshooting this on [issue #59](https://github.com/bellingard/sonar-scanner-npm/issues/59). -## Download From Mirrors +## Download From Mirrors (SQ < 10.6 only) By default, the scanner binaries are downloaded from `https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/`. To use a custom mirror, set `$SONAR_SCANNER_MIRROR`. Or download precise version with `$SONAR_SCANNER_VERSION` diff --git a/src/types.ts b/src/types.ts index 846db68e..4eb02c4e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -76,7 +76,6 @@ export type ScanOptions = { jvmOptions?: string[]; localScannerCli?: boolean; options?: { [key: string]: string }; - caPath?: string; logLevel?: string; verbose?: boolean; version?: string; From df1fe760726287fd3ac84acf1b1f9684322b00dc Mon Sep 17 00:00:00 2001 From: 7PH Date: Wed, 29 May 2024 15:06:25 +0200 Subject: [PATCH 34/35] SCANNPM-36 Never send null or missing values to the scanner engine --- src/properties.ts | 25 +++++++++++++++++++------ test/unit/properties.test.ts | 25 ++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/properties.ts b/src/properties.ts index b1ac3021..528c7daa 100644 --- a/src/properties.ts +++ b/src/properties.ts @@ -427,6 +427,18 @@ function hotfixDeprecatedProperties(properties: ScannerProperties): ScannerPrope return properties; } +function normalizeProperties(properties: ScannerProperties) { + for (const [key, value] of Object.entries(properties)) { + if (value === null) { + properties[key] = ''; + } else if (typeof value === 'undefined') { + delete properties[key]; + } + } + + return properties; +} + export function getProperties( scanOptions: ScanOptions, startTimestampMs: number, @@ -459,21 +471,22 @@ export function getProperties( ); // Merge properties respecting order of precedence - const properties = { + let properties = { ...getDefaultProperties(), // fallback to default if nothing was provided for these properties ...inferredProperties, ...httpProxyProperties, ...userProperties, // Highest precedence }; - // Hotfix host properties with custom SonarCloud URL - const hostProperties = getHostProperties(properties); - - return hotfixDeprecatedProperties({ + properties = hotfixDeprecatedProperties({ ...properties, // Can't be overridden: - ...hostProperties, + ...getHostProperties(properties), // Hotfix host properties with custom SonarCloud URL ...getBootstrapperProperties(startTimestampMs), 'sonar.projectBaseDir': projectBaseDir, }); + + properties = normalizeProperties(properties); + + return properties; } diff --git a/test/unit/properties.test.ts b/test/unit/properties.test.ts index a2cdbec4..7cb5eeab 100644 --- a/test/unit/properties.test.ts +++ b/test/unit/properties.test.ts @@ -57,6 +57,29 @@ describe('getProperties', () => { }); }); + it('should ignore undefined values and convert null to empty strings', () => { + projectHandler.reset('fake_project_with_no_package_file'); + projectHandler.setEnvironmentVariables({}); + + const properties = getProperties( + { + options: { + 'sonar.analysis.mode': undefined as unknown as string, + 'sonar.analysis.mode2': null as unknown as string, + }, + }, + projectHandler.getStartTime(), + ); + + expect(properties).toEqual({ + ...projectHandler.getExpectedProperties(), + 'sonar.host.url': SONARCLOUD_URL, + 'sonar.scanner.apiBaseUrl': SONARCLOUD_API_BASE_URL, + 'sonar.scanner.internal.isSonarCloud': 'true', + 'sonar.analysis.mode2': '', + }); + }); + describe('should handle JS API scan options params correctly', () => { it('should detect custom SonarCloud endpoint', () => { projectHandler.reset('fake_project_with_no_package_file'); @@ -558,7 +581,7 @@ describe('getProperties', () => { serverUrl: 'http://localhost/sonarqube', }, projectHandler.getStartTime(), - { define: ['sonar.token=my-token', '-javaagent:/ignored-value.jar'] }, + { define: ['sonar.token=my-token'] }, ); expect(properties).toEqual({ From c81c9471013a2476c96155880dd695ca313352f9 Mon Sep 17 00:00:00 2001 From: 7PH Date: Wed, 29 May 2024 15:07:27 +0200 Subject: [PATCH 35/35] NO-JIRA Bump version to v4.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56633770..15f6d874 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sonarqube-scanner", - "version": "3.6.0", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sonarqube-scanner", - "version": "3.6.0", + "version": "4.0.0", "license": "LGPL-3.0-only", "dependencies": { "adm-zip": "0.5.12", diff --git a/package.json b/package.json index 1023a004..e7e4c820 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "sonarqube-scanner", "description": "SonarQube/SonarCloud Scanner for the JavaScript world", - "version": "3.6.0", + "version": "4.0.0", "homepage": "https://github.com/SonarSource/sonar-scanner-npm", "author": { "name": "Fabrice Bellingard",