mirror of
https://github.com/SonarSource/sonarqube-scan-action.git
synced 2026-10-05 02:51:38 +00:00
SQSCANGHA-140 Add OpenPGP signature verification for scanner downloads (#235)
This commit is contained in:
1 parent
30dbe5c9ee
commit
55e44800a8
17 files changed
+33778
-41
No files matched your search
@@ -0,0 +1,22 @@
|
|||||||
|
# Path to sources
|
||||||
|
sonar.sources=src
|
||||||
|
sonar.exclusions=src/**/__tests__/*
|
||||||
|
# sonar.inclusions=
|
||||||
|
|
||||||
|
# Path to tests
|
||||||
|
sonar.tests=test,src
|
||||||
|
# sonar.test.exclusions=
|
||||||
|
sonar.test.inclusions=src/**/__tests__/*
|
||||||
|
|
||||||
|
# Source encoding
|
||||||
|
# sonar.sourceEncoding=
|
||||||
|
|
||||||
|
# Exclusions for copy-paste detection
|
||||||
|
# sonar.cpd.exclusions=
|
||||||
|
|
||||||
|
# Python version (for python projects only)
|
||||||
|
# sonar.python.version=
|
||||||
|
|
||||||
|
# C++ standard version (for C++ projects only)
|
||||||
|
# If not specified, it defaults to the latest supported standard
|
||||||
|
# sonar.cfamily.reportingCppStandardOverride=c++98|c++11|c++14|c++17|c++20
|
||||||
@@ -24,6 +24,10 @@ inputs:
|
|||||||
description: URL to download the Sonar Scanner CLI binaries from
|
description: URL to download the Sonar Scanner CLI binaries from
|
||||||
required: false
|
required: false
|
||||||
default: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli
|
default: https://binaries.sonarsource.com/Distribution/sonar-scanner-cli
|
||||||
|
skipSignatureVerification:
|
||||||
|
description: Skip GPG signature verification (not recommended for security)
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
runs:
|
runs:
|
||||||
using: node24
|
using: node24
|
||||||
main: dist/index.js
|
main: dist/index.js
|
||||||
Vendored
+31833
File diff suppressed because it is too large.
Load diff
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"file":"exec-zlpfwmpH.js","sources":["../node_modules/@actions/core/lib/utils.js","../node_modules/@actions/core/lib/command.js","../node_modules/@actions/core/lib/file-command.js","../node_modules/@actions/http-client/lib/proxy.js","../node_modules/tunnel/lib/tunnel.js","../node_modules/tunnel/index.js","../node_modules/undici/lib/core/symbols.js","../node_modules/undici/lib/core/errors.js","../node_modules/undici/lib/core/constants.js","../node_modules/undici/lib/core/tree.js","../node_modules/undici/lib/core/util.js","../node_modules/undici/lib/core/diagnostics.js","../node_modules/undici/lib/core/request.js","../node_modules/undici/lib/dispatcher/dispatcher.js","../node_modules/undici/lib/dispatcher/dispatcher-base.js","../node_modules/undici/lib/util/timers.js","../node_modules/undici/lib/core/connect.js","../node_modules/undici/lib/llhttp/utils.js","../node_modules/undici/lib/llhttp/constants.js","../node_modules/undici/lib/llhttp/llhttp-wasm.js","../node_modules/undici/lib/llhttp/llhttp_simd-wasm.js","../node_modules/undici/lib/web/fetch/constants.js","../node_modules/undici/lib/web/fetch/global.js","../node_modules/undici/lib/web/fetch/data-url.js","../node_modules/undici/lib/web/fetch/webidl.js","../node_modules/undici/lib/web/fetch/util.js","../node_modules/undici/lib/web/fetch/symbols.js","../node_modules/undici/lib/web/fetch/file.js","../node_modules/undici/lib/web/fetch/formdata.js","../node_modules/undici/lib/web/fetch/formdata-parser.js","../node_modules/undici/lib/web/fetch/body.js","../node_modules/undici/lib/dispatcher/client-h1.js","../node_modules/undici/lib/dispatcher/client-h2.js","../node_modules/undici/lib/handler/redirect-handler.js","../node_modules/undici/lib/interceptor/redirect-interceptor.js","../node_modules/undici/lib/dispatcher/client.js","../node_modules/undici/lib/dispatcher/fixed-queue.js","../node_modules/undici/lib/dispatcher/pool-stats.js","../node_modules/undici/lib/dispatcher/pool-base.js","../node_modules/undici/lib/dispatcher/pool.js","../node_modules/undici/lib/dispatcher/balanced-pool.js","../node_modules/undici/lib/dispatcher/agent.js","../node_modules/undici/lib/dispatcher/proxy-agent.js","../node_modules/undici/lib/dispatcher/env-http-proxy-agent.js","../node_modules/undici/lib/handler/retry-handler.js","../node_modules/undici/lib/dispatcher/retry-agent.js","../node_modules/undici/lib/api/readable.js","../node_modules/undici/lib/api/util.js","../node_modules/undici/lib/api/api-request.js","../node_modules/undici/lib/api/abort-signal.js","../node_modules/undici/lib/api/api-stream.js","../node_modules/undici/lib/api/api-pipeline.js","../node_modules/undici/lib/api/api-upgrade.js","../node_modules/undici/lib/api/api-connect.js","../node_modules/undici/lib/api/index.js","../node_modules/undici/lib/mock/mock-errors.js","../node_modules/undici/lib/mock/mock-symbols.js","../node_modules/undici/lib/mock/mock-utils.js","../node_modules/undici/lib/mock/mock-interceptor.js","../node_modules/undici/lib/mock/mock-client.js","../node_modules/undici/lib/mock/mock-pool.js","../node_modules/undici/lib/mock/pluralizer.js","../node_modules/undici/lib/mock/pending-interceptors-formatter.js","../node_modules/undici/lib/mock/mock-agent.js","../node_modules/undici/lib/global.js","../node_modules/undici/lib/handler/decorator-handler.js","../node_modules/undici/lib/interceptor/redirect.js","../node_modules/undici/lib/interceptor/retry.js","../node_modules/undici/lib/interceptor/dump.js","../node_modules/undici/lib/interceptor/dns.js","../node_modules/undici/lib/web/fetch/headers.js","../node_modules/undici/lib/web/fetch/response.js","../node_modules/undici/lib/web/fetch/dispatcher-weakref.js","../node_modules/undici/lib/web/fetch/request.js","../node_modules/undici/lib/web/fetch/index.js","../node_modules/undici/lib/web/fileapi/symbols.js","../node_modules/undici/lib/web/fileapi/progressevent.js","../node_modules/undici/lib/web/fileapi/encoding.js","../node_modules/undici/lib/web/fileapi/util.js","../node_modules/undici/lib/web/fileapi/filereader.js","../node_modules/undici/lib/web/cache/symbols.js","../node_modules/undici/lib/web/cache/util.js","../node_modules/undici/lib/web/cache/cache.js","../node_modules/undici/lib/web/cache/cachestorage.js","../node_modules/undici/lib/web/cookies/constants.js","../node_modules/undici/lib/web/cookies/util.js","../node_modules/undici/lib/web/cookies/parse.js","../node_modules/undici/lib/web/cookies/index.js","../node_modules/undici/lib/web/websocket/events.js","../node_modules/undici/lib/web/websocket/constants.js","../node_modules/undici/lib/web/websocket/symbols.js","../node_modules/undici/lib/web/websocket/util.js","../node_modules/undici/lib/web/websocket/frame.js","../node_modules/undici/lib/web/websocket/connection.js","../node_modules/undici/lib/web/websocket/permessage-deflate.js","../node_modules/undici/lib/web/websocket/receiver.js","../node_modules/undici/lib/web/websocket/sender.js","../node_modules/undici/lib/web/websockLine truncated
|
||||||
Vendored
+895
-21
File diff suppressed because it is too large.
Load diff
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"index.js","sources":["../node_modules/semver/internal/constants.js","../node_modules/semver/internal/debug.js","../node_modules/semver/internal/re.js","../node_modules/semver/internal/parse-options.js","../node_modules/semver/internal/identifiers.js","../node_modules/semver/classes/semver.js","../node_modules/semver/functions/parse.js","../node_modules/semver/functions/valid.js","../node_modules/semver/functions/clean.js","../node_modules/semver/functions/inc.js","../node_modules/semver/functions/diff.js","../node_modules/semver/functions/major.js","../node_modules/semver/functions/minor.js","../node_modules/semver/functions/patch.js","../node_modules/semver/functions/prerelease.js","../node_modules/semver/functions/compare.js","../node_modules/semver/functions/rcompare.js","../node_modules/semver/functions/compare-loose.js","../node_modules/semver/functions/compare-build.js","../node_modules/semver/functions/sort.js","../node_modules/semver/functions/rsort.js","../node_modules/semver/functions/gt.js","../node_modules/semver/functions/lt.js","../node_modules/semver/functions/eq.js","../node_modules/semver/functions/neq.js","../node_modules/semver/functions/gte.js","../node_modules/semver/functions/lte.js","../node_modules/semver/functions/cmp.js","../node_modules/semver/functions/coerce.js","../node_modules/semver/internal/lrucache.js","../node_modules/semver/classes/range.js","../node_modules/semver/classes/comparator.js","../node_modules/semver/functions/satisfies.js","../node_modules/semver/ranges/to-comparators.js","../node_modules/semver/ranges/max-satisfying.js","../node_modules/semver/ranges/min-satisfying.js","../node_modules/semver/ranges/min-version.js","../node_modules/semver/ranges/valid.js","../node_modules/semver/ranges/outside.js","../node_modules/semver/ranges/gtr.js","../node_modules/semver/ranges/ltr.js","../node_modules/semver/ranges/intersects.js","../node_modules/semver/ranges/simplify.js","../node_modules/semver/ranges/subset.js","../node_modules/semver/index.js","../node_modules/@actions/tool-cache/lib/manifest.js","../node_modules/@actions/tool-cache/lib/retry-helper.js","../node_modules/@actions/tool-cache/lib/tool-cache.js","../src/main/utils.js","../src/main/install-sonar-scanner.js","../node_modules/string-argv/index.js","../src/main/run-sonar-scanner.js","../src/main/sanity-checks.js","../src/main/index.js"],"sourcesContent":["'use strict'\n\n// Note: this is the semver.org version of the spec that it implements\n// Not necessarily the package version of this code.\nconst SEMVER_SPEC_VERSION = '2.0.0'\n\nconst MAX_LENGTH = 256\nconst MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||\n/* istanbul ignore next */ 9007199254740991\n\n// Max safe segment length for coercion.\nconst MAX_SAFE_COMPONENT_LENGTH = 16\n\n// Max safe length for a build identifier. The max length minus 6 characters for\n// the shortest version with a build 0.0.0+BUILD.\nconst MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6\n\nconst RELEASE_TYPES = [\n 'major',\n 'premajor',\n 'minor',\n 'preminor',\n 'patch',\n 'prepatch',\n 'prerelease',\n]\n\nmodule.exports = {\n MAX_LENGTH,\n MAX_SAFE_COMPONENT_LENGTH,\n MAX_SAFE_BUILD_LENGTH,\n MAX_SAFE_INTEGER,\n RELEASE_TYPES,\n SEMVER_SPEC_VERSION,\n FLAG_INCLUDE_PRERELEASE: 0b001,\n FLAG_LOOSE: 0b010,\n}\n","'use strict'\n\nconst debug = (\n typeof process === 'object' &&\n process.env &&\n process.env.NODE_DEBUG &&\n /\\bsemver\\b/i.test(process.env.NODE_DEBUG)\n) ? (...args) => console.error('SEMVER', ...args)\n : () => {}\n\nmodule.exports = debug\n","'use strict'\n\nconst {\n MAX_SAFE_COMPONENT_LENGTH,\n MAX_SAFE_BUILD_LENGTH,\n MAX_LENGTH,\n} = require('./constants')\nconst debug = require('./debug')\nexports = module.exports = {}\n\n// The actual regexps go on exports.re\nconst re = exports.re = []\nconst safeRe = exports.safeRe = []\nconst src = exports.src = []\nconst safeSrc = exports.safeSrc = []\nconst t = exports.t = {}\nlet R = 0\n\nconst LETTERDASHNUMBER = '[a-zA-Z0-9-]'\n\n// Replace some greedy regex tokens to prevent regex dos issues. These regex are\n// used internally via the safeRe object since all inputs in this library get\n// normalized first to trim and collapse all extra whitespace. The original\n// regexes are exported for userland consumption and lower level usage. A\n// future breaking change could export the safer regex only with a note that\n// all input should have extra whitespace removed.\nconst safeRegexReplacements = [\n ['\\\\s', 1],\n ['\\\\d', MAX_LENGTH],\n [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH],\n]\n\nconst makeSafeRegex = (value) => {\n for (const [token, max] of safeRegexReplacements) {\n value = value\n .split(`${token}*`).join(`${token}{0,${max}}`)\n .split(`${token}+`).join(`${token}{1,${max}}`)\n }\n return value\n}\n\nconst createToken = (name, value, isGlobal) => {\n const safe = makeSafeRegex(value)\n const index = R++\n debug(name, index, value)\n t[name] = indLine truncated
|
{"version":3,"file":"index.js","sources":["../node_modules/semver/internal/constants.js","../node_modules/semver/internal/debug.js","../node_modules/semver/internal/re.js","../node_modules/semver/internal/parse-options.js","../node_modules/semver/internal/identifiers.js","../node_modules/semver/classes/semver.js","../node_modules/semver/functions/parse.js","../node_modules/semver/functions/valid.js","../node_modules/semver/functions/clean.js","../node_modules/semver/functions/inc.js","../node_modules/semver/functions/diff.js","../node_modules/semver/functions/major.js","../node_modules/semver/functions/minor.js","../node_modules/semver/functions/patch.js","../node_modules/semver/functions/prerelease.js","../node_modules/semver/functions/compare.js","../node_modules/semver/functions/rcompare.js","../node_modules/semver/functions/compare-loose.js","../node_modules/semver/functions/compare-build.js","../node_modules/semver/functions/sort.js","../node_modules/semver/functions/rsort.js","../node_modules/semver/functions/gt.js","../node_modules/semver/functions/lt.js","../node_modules/semver/functions/eq.js","../node_modules/semver/functions/neq.js","../node_modules/semver/functions/gte.js","../node_modules/semver/functions/lte.js","../node_modules/semver/functions/cmp.js","../node_modules/semver/functions/coerce.js","../node_modules/semver/internal/lrucache.js","../node_modules/semver/classes/range.js","../node_modules/semver/classes/comparator.js","../node_modules/semver/functions/satisfies.js","../node_modules/semver/ranges/to-comparators.js","../node_modules/semver/ranges/max-satisfying.js","../node_modules/semver/ranges/min-satisfying.js","../node_modules/semver/ranges/min-version.js","../node_modules/semver/ranges/valid.js","../node_modules/semver/ranges/outside.js","../node_modules/semver/ranges/gtr.js","../node_modules/semver/ranges/ltr.js","../node_modules/semver/ranges/intersects.js","../node_modules/semver/ranges/simplify.js","../node_modules/semver/ranges/subset.js","../node_modules/semver/index.js","../node_modules/@actions/tool-cache/lib/manifest.js","../node_modules/@actions/tool-cache/node_modules/@actions/exec/lib/toolrunner.js","../node_modules/@actions/tool-cache/node_modules/@actions/exec/lib/exec.js","../node_modules/@actions/tool-cache/lib/retry-helper.js","../node_modules/@actions/tool-cache/lib/tool-cache.js","../src/main/utils.js","../src/main/gpg-verification.js","../src/main/install-sonar-scanner.js","../node_modules/string-argv/index.js","../src/main/run-sonar-scanner.js","../src/main/sanity-checks.js","../src/main/index.js"],"sourcesContent":["'use strict'\n\n// Note: this is the semver.org version of the spec that it implements\n// Not necessarily the package version of this code.\nconst SEMVER_SPEC_VERSION = '2.0.0'\n\nconst MAX_LENGTH = 256\nconst MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||\n/* istanbul ignore next */ 9007199254740991\n\n// Max safe segment length for coercion.\nconst MAX_SAFE_COMPONENT_LENGTH = 16\n\n// Max safe length for a build identifier. The max length minus 6 characters for\n// the shortest version with a build 0.0.0+BUILD.\nconst MAX_SAFE_BUILD_LENGTH = MAX_LENGTH - 6\n\nconst RELEASE_TYPES = [\n 'major',\n 'premajor',\n 'minor',\n 'preminor',\n 'patch',\n 'prepatch',\n 'prerelease',\n]\n\nmodule.exports = {\n MAX_LENGTH,\n MAX_SAFE_COMPONENT_LENGTH,\n MAX_SAFE_BUILD_LENGTH,\n MAX_SAFE_INTEGER,\n RELEASE_TYPES,\n SEMVER_SPEC_VERSION,\n FLAG_INCLUDE_PRERELEASE: 0b001,\n FLAG_LOOSE: 0b010,\n}\n","'use strict'\n\nconst debug = (\n typeof process === 'object' &&\n process.env &&\n process.env.NODE_DEBUG &&\n /\\bsemver\\b/i.test(process.env.NODE_DEBUG)\n) ? (...args) => console.error('SEMVER', ...args)\n : () => {}\n\nmodule.exports = debug\n","'use strict'\n\nconst {\n MAX_SAFE_COMPONENT_LENGTH,\n MAX_SAFE_BUILD_LENGTH,\n MAX_LENGTH,\n} = require('./constants')\nconst debug = require('./debug')\nexports = module.exports = {}\n\n// The actual regexps go on exports.re\nconst re = exports.re = []\nconst safeRe = exports.safeRe = []\nconst src = exports.src = []\nconst safeSrc = exports.safeSrc = []\nconst t = exports.t = {}\nlet R = 0\n\nconst LETTERDASHNUMBER = '[a-zA-Z0-9-]'\n\n// Replace some greedy regex tokens to prevent regex dos issues. These regex are\n// used internally via the safeRe object since all inputs in this library get\n// normalized first to trim and collapse all extra whitespace. The original\n// regexes are exported for userland consumption and lower level usage. A\n// future breaking change could export the safer regex only with a note that\n// all input should have extra whitespace removed.\nconst safeRegexReplacements = [\n ['\\\\s', 1],\n ['\\\\d', MAX_LENGTH],\n [LETTERDASHNUMBER, MAX_SAFE_BUILD_LENGTH],\n]\n\nconst makeSafeRegex = (value) => {\n for (const [token, max] of safeRegexReplacements) {\n value = value\n .split(`${token}*`).join(`${token}{0,${max}}`)\n .split(`${token}+`).join(`${tokeLine truncated
|
||||||
Vendored
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
import { h as getExecOutput, b as addPath, i as info, j as setOutput, s as setFailed, e as exec, k as startGroup, l as endGroup } from './core-DpWEmnbG.js';
|
import { f as execExports, h as addPath, a as info, n as setOutput, s as setFailed, o as startGroup, p as endGroup } from './exec-zlpfwmpH.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import 'os';
|
import 'os';
|
||||||
@@ -124,7 +124,7 @@ function getSuffixAndName(runnerOS, runnerArch) {
|
|||||||
async function getRealPath(filePath, runnerOS) {
|
async function getRealPath(filePath, runnerOS) {
|
||||||
switch (runnerOS) {
|
switch (runnerOS) {
|
||||||
case "Windows": {
|
case "Windows": {
|
||||||
const windowsResult = await getExecOutput("cygpath", [
|
const windowsResult = await execExports.getExecOutput("cygpath", [
|
||||||
"--absolute",
|
"--absolute",
|
||||||
"--windows",
|
"--windows",
|
||||||
filePath,
|
filePath,
|
||||||
@@ -132,14 +132,14 @@ async function getRealPath(filePath, runnerOS) {
|
|||||||
return windowsResult.stdout.trim();
|
return windowsResult.stdout.trim();
|
||||||
}
|
}
|
||||||
case "Linux": {
|
case "Linux": {
|
||||||
const linuxResult = await getExecOutput("readlink", [
|
const linuxResult = await execExports.getExecOutput("readlink", [
|
||||||
"-f",
|
"-f",
|
||||||
filePath,
|
filePath,
|
||||||
]);
|
]);
|
||||||
return linuxResult.stdout.trim();
|
return linuxResult.stdout.trim();
|
||||||
}
|
}
|
||||||
case "macOS": {
|
case "macOS": {
|
||||||
const macResult = await getExecOutput("greadlink", ["-f", filePath]);
|
const macResult = await execExports.getExecOutput("greadlink", ["-f", filePath]);
|
||||||
return macResult.stdout.trim();
|
return macResult.stdout.trim();
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -169,7 +169,7 @@ async function getRealPath(filePath, runnerOS) {
|
|||||||
async function installMacOSPackages() {
|
async function installMacOSPackages() {
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
info("Installing required packages for macOS");
|
info("Installing required packages for macOS");
|
||||||
await exec("brew", ["install", "coreutils"]);
|
await execExports.exec("brew", ["install", "coreutils"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,10 +207,10 @@ async function downloadAndInstallBuildWrapper(downloadUrl, runnerEnv) {
|
|||||||
fs.mkdirSync(runnerTemp, { recursive: true });
|
fs.mkdirSync(runnerTemp, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
await exec("curl", ["-sSLo", tmpZipPath, downloadUrl]);
|
await execExports.exec("curl", ["-sSLo", tmpZipPath, downloadUrl]);
|
||||||
|
|
||||||
info("Decompressing");
|
info("Decompressing");
|
||||||
await exec("unzip", ["-o", "-d", runnerTemp, tmpZipPath]);
|
await execExports.exec("unzip", ["-o", "-d", runnerTemp, tmpZipPath]);
|
||||||
|
|
||||||
endGroup();
|
endGroup();
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
{"version":3,"file":"install-build-wrapper.js","sources":["../src/install-build-wrapper/utils.js","../src/install-build-wrapper/install-build-wrapper.js"],"sourcesContent":["// SonarQube Scan Action\n// Copyright (C) SonarSource Sàrl\n// mailto:contact AT sonarsource DOT com\n//\n// This program is free software; you can redistribute it and/or\n// modify it under the terms of the GNU Lesser General Public\n// License as published by the Free Software Foundation; either\n// version 3 of the License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n// Lesser General Public License for more details.\n//\n// You should have received a copy of the GNU Lesser General Public License\n// along with this program; if not, write to the Free Software Foundation,\n// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n\nimport * as exec from \"@actions/exec\";\nimport * as path from \"path\";\n\n/**\n * Compute all names and paths related to the build wrapper\n * based on the runner environment\n */\nexport function getBuildWrapperInfo({\n runnerOS,\n runnerArch,\n runnerTemp,\n sonarHostUrl,\n}) {\n const { buildWrapperSuffix, buildWrapperName } = getSuffixAndName(\n runnerOS,\n runnerArch\n );\n\n const buildWrapperDir = `${runnerTemp}/build-wrapper-${buildWrapperSuffix}`;\n const buildWrapperUrl = `${sonarHostUrl}/static/cpp/build-wrapper-${buildWrapperSuffix}.zip`;\n const buildWrapperBin = `${buildWrapperDir}/${buildWrapperName}`;\n\n return {\n buildWrapperUrl,\n buildWrapperDir,\n buildWrapperBin,\n };\n}\n\nfunction getSuffixAndName(runnerOS, runnerArch) {\n if (\n runnerArch !== \"X64\" &&\n !(runnerArch === \"ARM64\" && (runnerOS === \"macOS\" || runnerOS === \"Linux\"))\n ) {\n throw new Error(\n `Architecture '${runnerArch}' is unsupported by build-wrapper`\n );\n }\n\n switch (runnerOS) {\n case \"Windows\":\n return {\n buildWrapperSuffix: \"win-x86\",\n buildWrapperName: \"build-wrapper-win-x86-64.exe\",\n };\n\n case \"Linux\":\n switch (runnerArch) {\n case \"X64\":\n return {\n buildWrapperSuffix: \"linux-x86\",\n buildWrapperName: \"build-wrapper-linux-x86-64\",\n };\n\n case \"ARM64\":\n return {\n buildWrapperSuffix: \"linux-aarch64\",\n buildWrapperName: \"build-wrapper-linux-aarch64\",\n };\n }\n break; // handled before the switch\n\n case \"macOS\":\n return {\n buildWrapperSuffix: \"macosx-x86\",\n buildWrapperName: \"build-wrapper-macosx-x86\",\n };\n\n default:\n throw new Error(`Unsupported runner OS '${runnerOS}'`);\n }\n}\n\nexport async function getRealPath(filePath, runnerOS) {\n switch (runnerOS) {\n case \"Windows\": {\n const windowsResult = await exec.getExecOutput(\"cygpath\", [\n \"--absolute\",\n \"--windows\",\n filePath,\n ]);\n return windowsResult.stdout.trim();\n }\n case \"Linux\": {\n const linuxResult = await exec.getExecOutput(\"readlink\", [\n \"-f\",\n filePath,\n ]);\n return linuxResult.stdout.trim();\n }\n case \"macOS\": {\n const macResult = await exec.getExecOutput(\"greadlink\", [\"-f\", filePath]);\n return macResult.stdout.trim();\n }\n default:\n return path.resolve(filePath);\n }\n}\n","// SonarQube Scan Action\n// Copyright (C) SonarSource Sàrl\n// mailto:contact AT sonarsource DOT com\n//\n// This program is free software; you can redistribute it and/or\n// modify it under the terms of the GNU Lesser General Public\n// License as published by the Free Software Foundation; either\n// version 3 of the License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n// Lesser General Public License for more details.\n//\n// You should have received a copy of the GNU Lesser General Public License\n// along with this program; if not, write to the Free Software Foundation,\n// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n\nimport * as core from \"@actions/core\";\nimport * as exec from \"@actions/exec\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { getBuildWrapperInfo, getRealPath } from \"./utils\";\n\nasync function installMacOSPackages() {\n if (process.platform === \"darwin\") {\n core.info(\"Installing required packages for macOS\");\n await exec.exec(\"brew\", [\"install\", \"coreutils\"]);\n }\n}\n\n/**\n * These RUNNER_XX env variables come from GitHub by default.\n * See hLine truncated
|
{"version":3,"file":"install-build-wrapper.js","sources":["../src/install-build-wrapper/utils.js","../src/install-build-wrapper/install-build-wrapper.js"],"sourcesContent":["// SonarQube Scan Action\n// Copyright (C) SonarSource Sàrl\n// mailto:contact AT sonarsource DOT com\n//\n// This program is free software; you can redistribute it and/or\n// modify it under the terms of the GNU Lesser General Public\n// License as published by the Free Software Foundation; either\n// version 3 of the License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n// Lesser General Public License for more details.\n//\n// You should have received a copy of the GNU Lesser General Public License\n// along with this program; if not, write to the Free Software Foundation,\n// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n\nimport * as exec from \"@actions/exec\";\nimport * as path from \"path\";\n\n/**\n * Compute all names and paths related to the build wrapper\n * based on the runner environment\n */\nexport function getBuildWrapperInfo({\n runnerOS,\n runnerArch,\n runnerTemp,\n sonarHostUrl,\n}) {\n const { buildWrapperSuffix, buildWrapperName } = getSuffixAndName(\n runnerOS,\n runnerArch\n );\n\n const buildWrapperDir = `${runnerTemp}/build-wrapper-${buildWrapperSuffix}`;\n const buildWrapperUrl = `${sonarHostUrl}/static/cpp/build-wrapper-${buildWrapperSuffix}.zip`;\n const buildWrapperBin = `${buildWrapperDir}/${buildWrapperName}`;\n\n return {\n buildWrapperUrl,\n buildWrapperDir,\n buildWrapperBin,\n };\n}\n\nfunction getSuffixAndName(runnerOS, runnerArch) {\n if (\n runnerArch !== \"X64\" &&\n !(runnerArch === \"ARM64\" && (runnerOS === \"macOS\" || runnerOS === \"Linux\"))\n ) {\n throw new Error(\n `Architecture '${runnerArch}' is unsupported by build-wrapper`\n );\n }\n\n switch (runnerOS) {\n case \"Windows\":\n return {\n buildWrapperSuffix: \"win-x86\",\n buildWrapperName: \"build-wrapper-win-x86-64.exe\",\n };\n\n case \"Linux\":\n switch (runnerArch) {\n case \"X64\":\n return {\n buildWrapperSuffix: \"linux-x86\",\n buildWrapperName: \"build-wrapper-linux-x86-64\",\n };\n\n case \"ARM64\":\n return {\n buildWrapperSuffix: \"linux-aarch64\",\n buildWrapperName: \"build-wrapper-linux-aarch64\",\n };\n }\n break; // handled before the switch\n\n case \"macOS\":\n return {\n buildWrapperSuffix: \"macosx-x86\",\n buildWrapperName: \"build-wrapper-macosx-x86\",\n };\n\n default:\n throw new Error(`Unsupported runner OS '${runnerOS}'`);\n }\n}\n\nexport async function getRealPath(filePath, runnerOS) {\n switch (runnerOS) {\n case \"Windows\": {\n const windowsResult = await exec.getExecOutput(\"cygpath\", [\n \"--absolute\",\n \"--windows\",\n filePath,\n ]);\n return windowsResult.stdout.trim();\n }\n case \"Linux\": {\n const linuxResult = await exec.getExecOutput(\"readlink\", [\n \"-f\",\n filePath,\n ]);\n return linuxResult.stdout.trim();\n }\n case \"macOS\": {\n const macResult = await exec.getExecOutput(\"greadlink\", [\"-f\", filePath]);\n return macResult.stdout.trim();\n }\n default:\n return path.resolve(filePath);\n }\n}\n","// SonarQube Scan Action\n// Copyright (C) SonarSource Sàrl\n// mailto:contact AT sonarsource DOT com\n//\n// This program is free software; you can redistribute it and/or\n// modify it under the terms of the GNU Lesser General Public\n// License as published by the Free Software Foundation; either\n// version 3 of the License, or (at your option) any later version.\n//\n// This program is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n// Lesser General Public License for more details.\n//\n// You should have received a copy of the GNU Lesser General Public License\n// along with this program; if not, write to the Free Software Foundation,\n// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n\nimport * as core from \"@actions/core\";\nimport * as exec from \"@actions/exec\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\nimport { getBuildWrapperInfo, getRealPath } from \"./utils\";\n\nasync function installMacOSPackages() {\n if (process.platform === \"darwin\") {\n core.info(\"Installing required packages for macOS\");\n await exec.exec(\"brew\", [\"install\", \"coreutils\"]);\n }\n}\n\n/**\n * These RUNNER_XX env variables come from GitHub by default.\n * See hLine truncated
|
||||||
Generated
+26
-1
@@ -10,6 +10,7 @@
|
|||||||
"license": "LGPL-3.0-only",
|
"license": "LGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "3.0.0",
|
"@actions/core": "3.0.0",
|
||||||
|
"@actions/exec": "2.0.0",
|
||||||
"@actions/github": "9.0.0",
|
"@actions/github": "9.0.0",
|
||||||
"@actions/tool-cache": "4.0.0",
|
"@actions/tool-cache": "4.0.0",
|
||||||
"string-argv": "0.3.2"
|
"string-argv": "0.3.2"
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
"@actions/http-client": "^4.0.0"
|
"@actions/http-client": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/exec": {
|
"node_modules/@actions/core/node_modules/@actions/exec": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@actions/exec/-/exec-3.0.0.tgz",
|
"resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@actions/exec/-/exec-3.0.0.tgz",
|
||||||
"integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
|
"integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
|
||||||
@@ -40,6 +41,21 @@
|
|||||||
"@actions/io": "^3.0.2"
|
"@actions/io": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@actions/exec": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@actions/exec/-/exec-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/io": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/exec/node_modules/@actions/io": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@actions/io/-/io-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@actions/github": {
|
"node_modules/@actions/github": {
|
||||||
"version": "9.0.0",
|
"version": "9.0.0",
|
||||||
"resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@actions/github/-/github-9.0.0.tgz",
|
"resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@actions/github/-/github-9.0.0.tgz",
|
||||||
@@ -94,6 +110,15 @@
|
|||||||
"semver": "^7.7.3"
|
"semver": "^7.7.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@actions/tool-cache/node_modules/@actions/exec": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://repox.jfrog.io/artifactory/api/npm/npm/@actions/exec/-/exec-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/io": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.5",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
|
|||||||
+2
-1
@@ -6,11 +6,12 @@
|
|||||||
"main": "src/main/index.js",
|
"main": "src/main/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup --config rollup.config.js",
|
"build": "rollup --config rollup.config.js",
|
||||||
"test": "node --test"
|
"test": "node --experimental-test-module-mocks --test"
|
||||||
},
|
},
|
||||||
"license": "LGPL-3.0-only",
|
"license": "LGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "3.0.0",
|
"@actions/core": "3.0.0",
|
||||||
|
"@actions/exec": "2.0.0",
|
||||||
"@actions/github": "9.0.0",
|
"@actions/github": "9.0.0",
|
||||||
"@actions/tool-cache": "4.0.0",
|
"@actions/tool-cache": "4.0.0",
|
||||||
"string-argv": "0.3.2"
|
"string-argv": "0.3.2"
|
||||||
|
|||||||
@@ -0,0 +1,486 @@
|
|||||||
|
/*
|
||||||
|
* sonarqube-scan-action
|
||||||
|
* Copyright (C) 2025 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 assert from "node:assert/strict";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import {afterEach, describe, it, mock} from "node:test";
|
||||||
|
import {setupGpgHome,} from "../gpg-verification.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a temporary GPG home directory for testing.
|
||||||
|
* @param {Array} tempDirs - Array to track temp directories for cleanup
|
||||||
|
* @returns {string} Path to the created GPG home directory
|
||||||
|
*/
|
||||||
|
function createTrackedGpgHome(tempDirs) {
|
||||||
|
const gpgHome = setupGpgHome();
|
||||||
|
tempDirs.push(gpgHome);
|
||||||
|
assert.ok(fs.existsSync(gpgHome));
|
||||||
|
return gpgHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("gpg-verification with mocked exec", () => {
|
||||||
|
let tempDirs = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up temporary directories
|
||||||
|
tempDirs.forEach((dir) => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tempDirs = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isGpgAvailable", () => {
|
||||||
|
it("should return true when GPG is available", async (t) => {
|
||||||
|
const execFn = mock.fn(async () => 0);
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isGpgAvailable } = await import("../gpg-verification.js?test=gpg-available");
|
||||||
|
|
||||||
|
const result = await isGpgAvailable();
|
||||||
|
|
||||||
|
assert.equal(result, true);
|
||||||
|
assert.equal(execFn.mock.calls.length, 1);
|
||||||
|
assert.equal(execFn.mock.calls[0].arguments[0], "gpg");
|
||||||
|
assert.deepEqual(execFn.mock.calls[0].arguments[1], ["--version"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when GPG is not available", async (t) => {
|
||||||
|
const execFn = mock.fn(async () => {
|
||||||
|
throw new Error("GPG not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { isGpgAvailable } = await import("../gpg-verification.js?test=gpg-unavailable");
|
||||||
|
|
||||||
|
const result = await isGpgAvailable();
|
||||||
|
|
||||||
|
assert.equal(result, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("runGpgVerify", () => {
|
||||||
|
it("should successfully verify valid signature", async (t) => {
|
||||||
|
const execCalls = [];
|
||||||
|
const execFn = mock.fn(async (command, args) => {
|
||||||
|
execCalls.push({ command, args });
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { runGpgVerify } = await import("../gpg-verification.js?test=verify-success");
|
||||||
|
|
||||||
|
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||||
|
const zipPath = "/tmp/scanner.zip";
|
||||||
|
const signaturePath = "/tmp/scanner.zip.asc";
|
||||||
|
|
||||||
|
await runGpgVerify(zipPath, signaturePath, gpgHome);
|
||||||
|
|
||||||
|
assert.equal(execCalls.length, 1);
|
||||||
|
assert.equal(execCalls[0].command, "gpg");
|
||||||
|
assert.ok(execCalls[0].args.includes("--verify"));
|
||||||
|
assert.ok(execCalls[0].args.includes(signaturePath));
|
||||||
|
assert.ok(execCalls[0].args.includes(zipPath));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when signature verification fails", async (t) => {
|
||||||
|
const execFn = mock.fn(async () => {
|
||||||
|
throw new Error("BAD signature");
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { runGpgVerify } = await import("../gpg-verification.js?test=verify-fail");
|
||||||
|
|
||||||
|
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||||
|
const zipPath = "/tmp/scanner.zip";
|
||||||
|
const signaturePath = "/tmp/scanner.zip.asc";
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => runGpgVerify(zipPath, signaturePath, gpgHome),
|
||||||
|
{
|
||||||
|
message: /GPG signature verification failed - file may be corrupted or tampered/
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should convert Windows paths for GPG", async (t) => {
|
||||||
|
const execCalls = [];
|
||||||
|
const execFn = mock.fn(async (command, args) => {
|
||||||
|
execCalls.push({ command, args });
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { runGpgVerify } = await import("../gpg-verification.js?test=verify-windows");
|
||||||
|
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, "platform", {
|
||||||
|
value: "win32",
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||||
|
const zipPath = String.raw`C:\temp\scanner.zip`;
|
||||||
|
const signaturePath = String.raw`C:\temp\scanner.zip.asc`;
|
||||||
|
|
||||||
|
await runGpgVerify(zipPath, signaturePath, gpgHome);
|
||||||
|
|
||||||
|
// Verify paths were converted to Unix format
|
||||||
|
const args = execCalls[0].args;
|
||||||
|
const homeDirIndex = args.indexOf("--homedir");
|
||||||
|
const zipIndex = args.indexOf("--verify") + 1;
|
||||||
|
|
||||||
|
// Check that Windows paths are converted (should start with /c/ instead of C:\)
|
||||||
|
assert.ok(!args[homeDirIndex + 1].includes("\\"));
|
||||||
|
assert.ok(!args[zipIndex].includes("\\"));
|
||||||
|
assert.ok(!args[zipIndex + 1].includes("\\"));
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, "platform", {
|
||||||
|
value: originalPlatform,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("verifySignature", () => {
|
||||||
|
it("should successfully verify signature with GPG available", async (t) => {
|
||||||
|
const execCalls = [];
|
||||||
|
const execFn = mock.fn(async (command, args) => {
|
||||||
|
execCalls.push({ command, args });
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { verifySignature } = await import("../gpg-verification.js?test=full-verify-success");
|
||||||
|
|
||||||
|
const zipPath = "/tmp/scanner.zip";
|
||||||
|
const signaturePath = "/tmp/scanner.zip.asc";
|
||||||
|
|
||||||
|
await verifySignature(zipPath, signaturePath);
|
||||||
|
|
||||||
|
assert.equal(execCalls.length, 3);
|
||||||
|
assert.deepEqual(execCalls[0].args, ["--version"]);
|
||||||
|
assert.ok(execCalls[1].args.includes("--recv-keys"));
|
||||||
|
assert.ok(execCalls[2].args.includes("--verify"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when GPG is not available", async (t) => {
|
||||||
|
const execFn = mock.fn(async (command, args) => {
|
||||||
|
if (args.includes("--version")) {
|
||||||
|
throw new Error("GPG not found");
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { verifySignature } = await import("../gpg-verification.js?test=no-gpg");
|
||||||
|
|
||||||
|
const zipPath = "/tmp/scanner.zip";
|
||||||
|
const signaturePath = "/tmp/scanner.zip.asc";
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => verifySignature(zipPath, signaturePath),
|
||||||
|
{
|
||||||
|
message: /GPG is not available/
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when signature verification fails", async (t) => {
|
||||||
|
let callCount = 0;
|
||||||
|
const execFn = mock.fn(async () => {
|
||||||
|
callCount++;
|
||||||
|
// First call: gpg --version (success)
|
||||||
|
if (callCount === 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Second call: recv-keys (success)
|
||||||
|
if (callCount === 2) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Third call: verify (failure - bad signature)
|
||||||
|
throw new Error("BAD signature from 679F1EE92B19609DE816FDE81DB198F93525EC1A");
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { verifySignature } = await import("../gpg-verification.js?test=bad-signature");
|
||||||
|
|
||||||
|
const zipPath = "/tmp/scanner.zip";
|
||||||
|
const signaturePath = "/tmp/scanner.zip.asc";
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => verifySignature(zipPath, signaturePath),
|
||||||
|
{
|
||||||
|
message: /GPG signature verification failed - file may be corrupted or tampered/
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should cleanup GPG home directory even on failure", async (t) => {
|
||||||
|
let createdGpgHome;
|
||||||
|
let callCount = 0;
|
||||||
|
|
||||||
|
const execFn = mock.fn(async (command, args) => {
|
||||||
|
callCount++;
|
||||||
|
// First call: gpg --version (success)
|
||||||
|
if (callCount === 1) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Second call: recv-keys (success)
|
||||||
|
if (callCount === 2) {
|
||||||
|
// Capture the GPG home directory from the args
|
||||||
|
const homeDirIndex = args.indexOf("--homedir");
|
||||||
|
if (homeDirIndex !== -1) {
|
||||||
|
createdGpgHome = args[homeDirIndex + 1];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Third call: verify (failure)
|
||||||
|
throw new Error("BAD signature");
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { verifySignature } = await import("../gpg-verification.js?test=cleanup-on-fail");
|
||||||
|
|
||||||
|
const zipPath = "/tmp/scanner.zip";
|
||||||
|
const signaturePath = "/tmp/scanner.zip.asc";
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => verifySignature(zipPath, signaturePath),
|
||||||
|
{
|
||||||
|
message: /GPG signature verification failed/
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(!fs.existsSync(createdGpgHome), "GPG home should have been deleted after failure");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use custom keyserver and fingerprint when provided", async (t) => {
|
||||||
|
const execCalls = [];
|
||||||
|
const execFn = mock.fn(async (command, args) => {
|
||||||
|
execCalls.push({ command, args });
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { verifySignature } = await import("../gpg-verification.js?test=custom-options");
|
||||||
|
|
||||||
|
const zipPath = "/tmp/scanner.zip";
|
||||||
|
const signaturePath = "/tmp/scanner.zip.asc";
|
||||||
|
const customKeyserver = "hkps://custom.keyserver.example.com";
|
||||||
|
const customFingerprint = "ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234";
|
||||||
|
|
||||||
|
await verifySignature(zipPath, signaturePath, {
|
||||||
|
keyserver: customKeyserver,
|
||||||
|
keyFingerprint: customFingerprint,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recvKeysCall = execCalls.find(call => call.args.includes("--recv-keys"));
|
||||||
|
assert.ok(recvKeysCall, "Should have recv-keys call");
|
||||||
|
assert.ok(recvKeysCall.args.includes(customKeyserver));
|
||||||
|
assert.ok(recvKeysCall.args.includes(customFingerprint));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use default keyserver and fingerprint when not provided", async (t) => {
|
||||||
|
const execCalls = [];
|
||||||
|
const execFn = mock.fn(async (command, args) => {
|
||||||
|
execCalls.push({ command, args });
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { verifySignature } = await import("../gpg-verification.js?test=default-options");
|
||||||
|
|
||||||
|
const zipPath = "/tmp/scanner.zip";
|
||||||
|
const signaturePath = "/tmp/scanner.zip.asc";
|
||||||
|
|
||||||
|
await verifySignature(zipPath, signaturePath);
|
||||||
|
|
||||||
|
const recvKeysCall = execCalls.find(call => call.args.includes("--recv-keys"));
|
||||||
|
assert.ok(recvKeysCall, "Should have recv-keys call");
|
||||||
|
assert.ok(recvKeysCall.args.includes("hkps://keyserver.ubuntu.com"));
|
||||||
|
assert.ok(recvKeysCall.args.includes("679F1EE92B19609DE816FDE81DB198F93525EC1A"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("importSonarSourceKey", () => {
|
||||||
|
it("should use fallback keyserver when primary fails", async (t) => {
|
||||||
|
const execCalls = [];
|
||||||
|
|
||||||
|
const execFn = mock.fn(async (command, args) => {
|
||||||
|
execCalls.push({ command, args });
|
||||||
|
|
||||||
|
const argsString = args.join(" ");
|
||||||
|
if (argsString.includes("invalid.keyserver.that.does.not.exist.example.com")) {
|
||||||
|
throw new Error("Failed to import key from invalid keyserver");
|
||||||
|
}
|
||||||
|
if (argsString.includes("keys.openpgp.org")) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { importSonarSourceKey } = await import("../gpg-verification.js?test=fallback");
|
||||||
|
|
||||||
|
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||||
|
const invalidKeyserver = "hkps://invalid.keyserver.that.does.not.exist.example.com";
|
||||||
|
const keyFingerprint = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
|
||||||
|
|
||||||
|
await importSonarSourceKey(gpgHome, keyFingerprint, invalidKeyserver);
|
||||||
|
|
||||||
|
assert.equal(execCalls.length, 2, "Should attempt two keyservers");
|
||||||
|
|
||||||
|
// Verify primary keyserver call
|
||||||
|
assert.equal(execCalls[0].command, "gpg");
|
||||||
|
assert.ok(execCalls[0].args.includes(invalidKeyserver));
|
||||||
|
assert.ok(execCalls[0].args.includes(keyFingerprint));
|
||||||
|
|
||||||
|
// Verify fallback keyserver call
|
||||||
|
assert.equal(execCalls[1].command, "gpg");
|
||||||
|
assert.ok(execCalls[1].args.includes("hkps://keys.openpgp.org"));
|
||||||
|
assert.ok(execCalls[1].args.includes(keyFingerprint));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should succeed with valid keyserver", async (t) => {
|
||||||
|
const execCalls = [];
|
||||||
|
|
||||||
|
const execFn = mock.fn(async (command, args) => {
|
||||||
|
execCalls.push({ command, args });
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { importSonarSourceKey } = await import("../gpg-verification.js?test=valid");
|
||||||
|
|
||||||
|
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||||
|
const keyserver = "hkps://keyserver.ubuntu.com";
|
||||||
|
const keyFingerprint = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
|
||||||
|
|
||||||
|
await importSonarSourceKey(gpgHome, keyFingerprint, keyserver);
|
||||||
|
|
||||||
|
assert.equal(execCalls.length, 1);
|
||||||
|
assert.equal(execCalls[0].command, "gpg");
|
||||||
|
assert.ok(execCalls[0].args.includes(keyserver));
|
||||||
|
assert.ok(execCalls[0].args.includes(keyFingerprint));
|
||||||
|
assert.ok(execCalls[0].args.includes("--recv-keys"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error when both keyservers fail", async (t) => {
|
||||||
|
const execFn = mock.fn(async () => {
|
||||||
|
throw new Error("Connection failed");
|
||||||
|
});
|
||||||
|
|
||||||
|
t.mock.module("@actions/exec", {
|
||||||
|
namedExports: {
|
||||||
|
exec: execFn,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { importSonarSourceKey } = await import("../gpg-verification.js?test=both-fail");
|
||||||
|
|
||||||
|
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||||
|
const keyserver = "hkps://keyserver.ubuntu.com";
|
||||||
|
const keyFingerprint = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => importSonarSourceKey(gpgHome, keyFingerprint, keyserver),
|
||||||
|
{
|
||||||
|
message: /Failed to import SonarSource public key from all keyservers/
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
* sonarqube-scan-action
|
||||||
|
* Copyright (C) 2025 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 { describe, it, afterEach } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import {
|
||||||
|
getGpgCommand,
|
||||||
|
setupGpgHome,
|
||||||
|
cleanupGpgHome,
|
||||||
|
convertToUnixPath,
|
||||||
|
} from "../gpg-verification.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to temporarily mock process.platform for a test.
|
||||||
|
* Automatically restores the original platform value after the test.
|
||||||
|
* @param {string} platform - The platform to mock (e.g., "win32", "linux")
|
||||||
|
* @param {Function} testFn - The test function to run with the mocked platform
|
||||||
|
*/
|
||||||
|
function withMockedPlatform(platform, testFn) {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, "platform", {
|
||||||
|
value: platform,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
testFn();
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(process, "platform", {
|
||||||
|
value: originalPlatform,
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a GPG home directory and track it for cleanup.
|
||||||
|
* @param {Array} tempDirs - Array to track temporary directories for cleanup
|
||||||
|
* @returns {string} The path to the created GPG home directory
|
||||||
|
*/
|
||||||
|
function createTrackedGpgHome(tempDirs) {
|
||||||
|
const gpgHome = setupGpgHome();
|
||||||
|
tempDirs.push(gpgHome);
|
||||||
|
assert.ok(fs.existsSync(gpgHome));
|
||||||
|
return gpgHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to temporarily mock environment variables for a test.
|
||||||
|
* Automatically restores or deletes environment variables after the test.
|
||||||
|
* @param {Object} envVars - Object with environment variable names as keys and values as values
|
||||||
|
* @param {Function} testFn - The async test function to run with the mocked environment
|
||||||
|
*/
|
||||||
|
async function withMockedEnv(envVars, testFn) {
|
||||||
|
const originalValues = {};
|
||||||
|
|
||||||
|
// Save original values and set new ones
|
||||||
|
for (const [key, value] of Object.entries(envVars)) {
|
||||||
|
originalValues[key] = process.env[key];
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await testFn();
|
||||||
|
} finally {
|
||||||
|
// Restore or delete environment variables
|
||||||
|
for (const [key, originalValue] of Object.entries(originalValues)) {
|
||||||
|
if (originalValue === undefined) {
|
||||||
|
delete process.env[key];
|
||||||
|
} else {
|
||||||
|
process.env[key] = originalValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to create a temporary directory.
|
||||||
|
* @returns {string} The path to the created temporary directory
|
||||||
|
*/
|
||||||
|
function createTempDir() {
|
||||||
|
const tempDir = path.join(os.tmpdir(), `test-runner-temp-${Date.now()}`);
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
return tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("gpg-verification", () => {
|
||||||
|
let tempDirs = [];
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Clean up any temporary directories created during tests
|
||||||
|
tempDirs.forEach((dir) => {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(dir)) {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore cleanup errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tempDirs = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getGpgCommand", () => {
|
||||||
|
it("should return 'gpg' as the command", () => {
|
||||||
|
const command = getGpgCommand();
|
||||||
|
assert.equal(command, "gpg");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("convertToUnixPath", () => {
|
||||||
|
it("should convert Windows path with drive letter to Unix path", () => {
|
||||||
|
withMockedPlatform("win32", () => {
|
||||||
|
assert.equal(
|
||||||
|
convertToUnixPath(String.raw`C:\a\_temp\gpg-home`),
|
||||||
|
"/c/a/_temp/gpg-home"
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
convertToUnixPath(String.raw`D:\Users\test\file.txt`),
|
||||||
|
"/d/Users/test/file.txt"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed slashes on Windows", () => {
|
||||||
|
withMockedPlatform("win32", () => {
|
||||||
|
assert.equal(
|
||||||
|
convertToUnixPath(String.raw`C:\a/_temp\gpg-home`),
|
||||||
|
"/c/a/_temp/gpg-home"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return path unchanged on non-Windows platforms", () => {
|
||||||
|
withMockedPlatform("linux", () => {
|
||||||
|
assert.equal(
|
||||||
|
convertToUnixPath("/tmp/gpg-home"),
|
||||||
|
"/tmp/gpg-home"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setupGpgHome", () => {
|
||||||
|
it("should create a temporary GPG home directory", () => {
|
||||||
|
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||||
|
|
||||||
|
assert.ok(fs.statSync(gpgHome).isDirectory());
|
||||||
|
|
||||||
|
// Check directory permissions (on Unix systems)
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
const stats = fs.statSync(gpgHome);
|
||||||
|
const mode = stats.mode & Number.parseInt("777", 8);
|
||||||
|
assert.equal(mode, Number.parseInt("700", 8));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create unique directories on multiple calls", async () => {
|
||||||
|
const gpgHome1 = createTrackedGpgHome(tempDirs);
|
||||||
|
// Small delay to ensure different timestamps
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||||
|
const gpgHome2 = createTrackedGpgHome(tempDirs);
|
||||||
|
|
||||||
|
assert.notEqual(gpgHome1, gpgHome2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use RUNNER_TEMP if available", async () => {
|
||||||
|
const testTemp = createTempDir();
|
||||||
|
|
||||||
|
await withMockedEnv({ RUNNER_TEMP: testTemp }, async () => {
|
||||||
|
const gpgHome = createTrackedGpgHome(tempDirs);
|
||||||
|
assert.ok(gpgHome.startsWith(testTemp));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fs.existsSync(testTemp)) {
|
||||||
|
fs.rmSync(testTemp, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("cleanupGpgHome", () => {
|
||||||
|
it("should remove the GPG home directory", () => {
|
||||||
|
const gpgHome = setupGpgHome();
|
||||||
|
assert.ok(fs.existsSync(gpgHome));
|
||||||
|
|
||||||
|
cleanupGpgHome(gpgHome);
|
||||||
|
assert.ok(!fs.existsSync(gpgHome));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not throw if directory does not exist", () => {
|
||||||
|
const nonExistentDir = path.join(os.tmpdir(), `non-existent-${Date.now()}`);
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
cleanupGpgHome(nonExistentDir);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle cleanup errors gracefully", () => {
|
||||||
|
// This test verifies the function doesn't throw on permission errors
|
||||||
|
// In practice, permission errors are rare in test environments
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
cleanupGpgHome("/invalid/path/that/does/not/exist");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
/*
|
||||||
|
* sonarqube-scan-action
|
||||||
|
* Copyright (C) 2025 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 core from "@actions/core";
|
||||||
|
import * as exec from "@actions/exec";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as os from "node:os";
|
||||||
|
import * as path from "node:path";
|
||||||
|
|
||||||
|
const SONARSOURCE_KEY_FINGERPRINT = "679F1EE92B19609DE816FDE81DB198F93525EC1A";
|
||||||
|
const DEFAULT_KEYSERVER = "hkps://keyserver.ubuntu.com";
|
||||||
|
const FALLBACK_KEYSERVER = "hkps://keys.openpgp.org";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the GPG signature of a downloaded file
|
||||||
|
* @param {string} zipPath - Path to the downloaded ZIP file
|
||||||
|
* @param {string} signaturePath - Path to the .asc signature file
|
||||||
|
* @param {object} options - Verification options
|
||||||
|
* @param {string} options.keyFingerprint - GPG key fingerprint (default: SonarSource key)
|
||||||
|
* @param {string} options.keyserver - Primary keyserver URL (default: keyserver.ubuntu.com, with fallback to keys.openpgp.org)
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @throws {Error} If GPG is unavailable or verification fails
|
||||||
|
*/
|
||||||
|
export async function verifySignature(zipPath, signaturePath, options = {}) {
|
||||||
|
const keyFingerprint = options.keyFingerprint || SONARSOURCE_KEY_FINGERPRINT;
|
||||||
|
const keyserver = options.keyserver || DEFAULT_KEYSERVER;
|
||||||
|
|
||||||
|
if (!(await isGpgAvailable())) {
|
||||||
|
throw new Error(
|
||||||
|
"GPG is not available. Install GPG or set skipSignatureVerification: true"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let gpgHome;
|
||||||
|
try {
|
||||||
|
gpgHome = setupGpgHome();
|
||||||
|
core.debug(`Created temporary GPG home: ${gpgHome}`);
|
||||||
|
|
||||||
|
await importSonarSourceKey(gpgHome, keyFingerprint, keyserver);
|
||||||
|
core.info("✓ SonarSource public key imported successfully");
|
||||||
|
|
||||||
|
await runGpgVerify(zipPath, signaturePath, gpgHome);
|
||||||
|
core.info("✓ GPG signature verification passed");
|
||||||
|
} finally {
|
||||||
|
if (gpgHome) {
|
||||||
|
cleanupGpgHome(gpgHome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if GPG is available on the system
|
||||||
|
* @returns {Promise<boolean>} True if GPG is available
|
||||||
|
*/
|
||||||
|
export async function isGpgAvailable() {
|
||||||
|
try {
|
||||||
|
const gpgCommand = getGpgCommand();
|
||||||
|
await exec.exec(gpgCommand, ["--version"], {
|
||||||
|
silent: true,
|
||||||
|
ignoreReturnCode: false,
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(`GPG not available: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the GPG command for the current platform
|
||||||
|
* @returns {string} GPG command name
|
||||||
|
*/
|
||||||
|
export function getGpgCommand() {
|
||||||
|
// GPG is available as 'gpg' on all GitHub-hosted runners
|
||||||
|
return "gpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Windows path to Unix-style path for GPG
|
||||||
|
* GPG on Windows (from Git for Windows) expects Unix-style paths
|
||||||
|
* @param {string} windowsPath - Windows path (e.g., C:\a\_temp\gpg-home)
|
||||||
|
* @returns {string} Unix-style path (e.g., /c/a/_temp/gpg-home)
|
||||||
|
*/
|
||||||
|
export function convertToUnixPath(windowsPath) {
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
return windowsPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
let unixPath = windowsPath.replaceAll('\\', "/");
|
||||||
|
|
||||||
|
unixPath = unixPath.replace(/^([A-Za-z]):/, (match, drive) => {
|
||||||
|
return `/${drive.toLowerCase()}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return unixPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a temporary GPG home directory
|
||||||
|
* @returns {string} Path to the temporary GPG home directory
|
||||||
|
*/
|
||||||
|
export function setupGpgHome() {
|
||||||
|
const tempDir = process.env.RUNNER_TEMP || os.tmpdir();
|
||||||
|
const gpgHome = path.join(tempDir, `gpg-home-${Date.now()}-${process.pid}`);
|
||||||
|
|
||||||
|
fs.mkdirSync(gpgHome, { recursive: true, mode: 0o700 });
|
||||||
|
|
||||||
|
return gpgHome;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to import a public key from a specific keyserver
|
||||||
|
* @param {string} gpgHome - Path to GPG home directory
|
||||||
|
* @param {string} keyFingerprint - Public key fingerprint
|
||||||
|
* @param {string} keyserver - Keyserver URL
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @throws {Error} If key import fails
|
||||||
|
*/
|
||||||
|
async function tryImportKey(gpgHome, keyFingerprint, keyserver) {
|
||||||
|
const gpgCommand = getGpgCommand();
|
||||||
|
const gpgHomePath = convertToUnixPath(gpgHome);
|
||||||
|
|
||||||
|
await exec.exec(
|
||||||
|
gpgCommand,
|
||||||
|
[
|
||||||
|
"--homedir",
|
||||||
|
gpgHomePath,
|
||||||
|
"--batch",
|
||||||
|
"--keyserver",
|
||||||
|
keyserver,
|
||||||
|
"--recv-keys",
|
||||||
|
keyFingerprint,
|
||||||
|
],
|
||||||
|
{
|
||||||
|
silent: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports the SonarSource public key from a keyserver
|
||||||
|
* @param {string} gpgHome - Path to GPG home directory
|
||||||
|
* @param {string} keyFingerprint - Public key fingerprint
|
||||||
|
* @param {string} keyserver - Keyserver URL
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @throws {Error} If key import fails
|
||||||
|
*/
|
||||||
|
export async function importSonarSourceKey(gpgHome, keyFingerprint, keyserver) {
|
||||||
|
let primaryError;
|
||||||
|
|
||||||
|
try {
|
||||||
|
core.info(`Importing SonarSource public key from ${keyserver}...`);
|
||||||
|
await tryImportKey(gpgHome, keyFingerprint, keyserver);
|
||||||
|
core.info(`Successfully imported key from ${keyserver}`);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
primaryError = error;
|
||||||
|
core.warning(
|
||||||
|
`Failed to import key from ${keyserver}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
core.info(`Attempting fallback keyserver ${FALLBACK_KEYSERVER}...`);
|
||||||
|
await tryImportKey(gpgHome, keyFingerprint, FALLBACK_KEYSERVER);
|
||||||
|
core.info(`Successfully imported key from fallback keyserver ${FALLBACK_KEYSERVER}`);
|
||||||
|
} catch (fallbackError) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to import SonarSource public key from all keyservers. ` +
|
||||||
|
`Primary (${keyserver}): ${primaryError.message}. ` +
|
||||||
|
`Fallback (${FALLBACK_KEYSERVER}): ${fallbackError.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs GPG verification on the downloaded file
|
||||||
|
* @param {string} zipPath - Path to the ZIP file
|
||||||
|
* @param {string} signaturePath - Path to the signature file
|
||||||
|
* @param {string} gpgHome - Path to GPG home directory
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
* @throws {Error} If verification fails
|
||||||
|
*/
|
||||||
|
export async function runGpgVerify(zipPath, signaturePath, gpgHome) {
|
||||||
|
const gpgCommand = getGpgCommand();
|
||||||
|
|
||||||
|
try {
|
||||||
|
core.info("Verifying GPG signature...");
|
||||||
|
await exec.exec(
|
||||||
|
gpgCommand,
|
||||||
|
[
|
||||||
|
"--homedir",
|
||||||
|
convertToUnixPath(gpgHome),
|
||||||
|
"--batch",
|
||||||
|
"--verify",
|
||||||
|
convertToUnixPath(signaturePath),
|
||||||
|
convertToUnixPath(zipPath),
|
||||||
|
],
|
||||||
|
{
|
||||||
|
silent: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`GPG signature verification failed - file may be corrupted or tampered: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up the temporary GPG home directory
|
||||||
|
* @param {string} gpgHome - Path to GPG home directory
|
||||||
|
*/
|
||||||
|
export function cleanupGpgHome(gpgHome) {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(gpgHome)) {
|
||||||
|
fs.rmSync(gpgHome, { recursive: true, force: true });
|
||||||
|
core.debug(`Cleaned up temporary GPG home: ${gpgHome}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(`Failed to cleanup temporary GPG home: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-2
@@ -34,8 +34,9 @@ function getInputs() {
|
|||||||
const projectBaseDir = core.getInput("projectBaseDir");
|
const projectBaseDir = core.getInput("projectBaseDir");
|
||||||
const scannerBinariesUrl = core.getInput("scannerBinariesUrl");
|
const scannerBinariesUrl = core.getInput("scannerBinariesUrl");
|
||||||
const scannerVersion = core.getInput("scannerVersion");
|
const scannerVersion = core.getInput("scannerVersion");
|
||||||
|
const skipSignatureVerification = core.getBooleanInput("skipSignatureVerification");
|
||||||
|
|
||||||
return { args, projectBaseDir, scannerBinariesUrl, scannerVersion };
|
return { args, projectBaseDir, scannerBinariesUrl, scannerVersion, skipSignatureVerification };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +72,7 @@ function runSanityChecks(inputs) {
|
|||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
const { args, projectBaseDir, scannerVersion, scannerBinariesUrl } =
|
const { args, projectBaseDir, scannerVersion, scannerBinariesUrl, skipSignatureVerification } =
|
||||||
getInputs();
|
getInputs();
|
||||||
const runnerEnv = getEnvVariables();
|
const runnerEnv = getEnvVariables();
|
||||||
const { sonarToken } = runnerEnv;
|
const { sonarToken } = runnerEnv;
|
||||||
@@ -81,6 +82,7 @@ async function run() {
|
|||||||
const scannerDir = await installSonarScanner({
|
const scannerDir = await installSonarScanner({
|
||||||
scannerVersion,
|
scannerVersion,
|
||||||
scannerBinariesUrl,
|
scannerBinariesUrl,
|
||||||
|
skipSignatureVerification,
|
||||||
});
|
});
|
||||||
|
|
||||||
await runSonarScanner(args, projectBaseDir, scannerDir, runnerEnv);
|
await runSonarScanner(args, projectBaseDir, scannerDir, runnerEnv);
|
||||||
|
|||||||
@@ -18,13 +18,14 @@
|
|||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import * as tc from "@actions/tool-cache";
|
import * as tc from "@actions/tool-cache";
|
||||||
import * as os from "os";
|
import * as os from "node:os";
|
||||||
import * as path from "path";
|
import * as path from "node:path";
|
||||||
import {
|
import {
|
||||||
getPlatformFlavor,
|
getPlatformFlavor,
|
||||||
getScannerDownloadURL,
|
getScannerDownloadURL,
|
||||||
scannerDirName,
|
scannerDirName,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
import { verifySignature } from "./gpg-verification";
|
||||||
|
|
||||||
const TOOLNAME = "sonar-scanner-cli";
|
const TOOLNAME = "sonar-scanner-cli";
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ const TOOLNAME = "sonar-scanner-cli";
|
|||||||
export async function installSonarScanner({
|
export async function installSonarScanner({
|
||||||
scannerVersion,
|
scannerVersion,
|
||||||
scannerBinariesUrl,
|
scannerBinariesUrl,
|
||||||
|
skipSignatureVerification = false,
|
||||||
}) {
|
}) {
|
||||||
const flavor = getPlatformFlavor(os.platform(), os.arch());
|
const flavor = getPlatformFlavor(os.platform(), os.arch());
|
||||||
|
|
||||||
@@ -54,6 +56,25 @@ export async function installSonarScanner({
|
|||||||
core.info(`Downloading from: ${downloadUrl}`);
|
core.info(`Downloading from: ${downloadUrl}`);
|
||||||
|
|
||||||
const downloadPath = await tc.downloadTool(downloadUrl);
|
const downloadPath = await tc.downloadTool(downloadUrl);
|
||||||
|
|
||||||
|
if (skipSignatureVerification) {
|
||||||
|
core.warning("⚠ Skipping GPG signature verification (not recommended)");
|
||||||
|
} else {
|
||||||
|
const signatureUrl = `${downloadUrl}.asc`;
|
||||||
|
core.info(`Downloading signature from: ${signatureUrl}`);
|
||||||
|
|
||||||
|
let signaturePath;
|
||||||
|
try {
|
||||||
|
signaturePath = await tc.downloadTool(signatureUrl);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to download signature file from ${signatureUrl}: ${error.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await verifySignature(downloadPath, signaturePath);
|
||||||
|
}
|
||||||
|
|
||||||
const extractedPath = await tc.extractZip(downloadPath);
|
const extractedPath = await tc.extractZip(downloadPath);
|
||||||
|
|
||||||
// Find the actual scanner directory inside the extracted folder
|
// Find the actual scanner directory inside the extracted folder
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import * as exec from "@actions/exec";
|
import * as exec from "@actions/exec";
|
||||||
import * as fs from "fs";
|
import * as fs from "node:fs";
|
||||||
import * as os from "os";
|
import * as os from "node:os";
|
||||||
import * as path from "path";
|
import * as path from "node:path";
|
||||||
import { parseArgsStringToArgv } from "string-argv";
|
import { parseArgsStringToArgv } from "string-argv";
|
||||||
|
|
||||||
const KEYTOOL_MAIN_CLASS = "sun.security.tools.keytool.Main";
|
const KEYTOOL_MAIN_CLASS = "sun.security.tools.keytool.Main";
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
// along with this program; if not, write to the Free Software Foundation,
|
// along with this program; if not, write to the Free Software Foundation,
|
||||||
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
// Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "node:fs";
|
||||||
import { join } from "path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
export function validateScannerVersion(version) {
|
export function validateScannerVersion(version) {
|
||||||
if (!version) {
|
if (!version) {
|
||||||
|
|||||||
Reference in new issue
Block a user