Skip to content

Commit ecbf196

Browse files
henryjuclaude
andcommitted
SQSCANGHA-149 Add scannerBinariesAuthHeader input for authenticated binary downloads
Organisations using private Artifactory mirrors require authentication to download the SonarScanner CLI. This adds an optional scannerBinariesAuthHeader input whose value is forwarded as the Authorization HTTP header to both the binary and GPG signature downloads via tc.downloadTool's built-in auth parameter. No new dependencies are introduced. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7006c44 commit ecbf196

8 files changed

Lines changed: 332 additions & 14 deletions

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,20 @@ This can be useful when the runner executing the action is self-hosted and has r
200200
scannerBinariesUrl: https://my.custom.binaries.url.com/Distribution/sonar-scanner-cli/
201201
```
202202

203+
#### `scannerBinariesAuthHeader`
204+
205+
If the server specified by `scannerBinariesUrl` requires authentication, you can provide an `Authorization` header value using the `scannerBinariesAuthHeader` option.
206+
The value is passed directly as the `Authorization` HTTP header, so you must include the scheme (e.g. `Bearer`, `Basic`):
207+
208+
```yaml
209+
- uses: SonarSource/sonarqube-scan-action@<action version>
210+
with:
211+
scannerBinariesUrl: https://my.custom.binaries.url.com/Distribution/sonar-scanner-cli/
212+
scannerBinariesAuthHeader: ${{ secrets.BINARIES_AUTH_HEADER }}
213+
```
214+
215+
Store the full header value (e.g. `Bearer mytoken`) in the GitHub secret to avoid exposing credentials.
216+
203217
#### `skipSignatureVerification`
204218

205219
By default, the action verifies the OpenPGP signature of the SonarScanner CLI binary before executing it. You can disable this verification using the `skipSignatureVerification` option:

action.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ inputs:
2828
description: Skip GPG signature verification (not recommended for security)
2929
required: false
3030
default: "false"
31+
scannerBinariesAuthHeader:
32+
description: >
33+
Authorization header value to use when downloading the SonarScanner CLI binaries
34+
(e.g. 'Bearer mytoken' or 'Basic base64creds'). Use this when scannerBinariesUrl
35+
points to a private server that requires authentication.
36+
required: false
37+
default: ""
3138
runs:
3239
using: node24
3340
main: dist/index.js

dist/index.js

Lines changed: 14 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/__tests__/index.test.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* sonarqube-scan-action
3+
* Copyright (C) 2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
import assert from "node:assert/strict";
22+
import { describe, it, mock } from "node:test";
23+
24+
function mockDependencies(t, { getInputFn, setSecretFn }) {
25+
t.mock.module("@actions/core", {
26+
namedExports: {
27+
getInput: getInputFn,
28+
getBooleanInput: mock.fn(() => false),
29+
setSecret: setSecretFn,
30+
setFailed: mock.fn(),
31+
info: mock.fn(),
32+
warning: mock.fn(),
33+
},
34+
});
35+
t.mock.module("../install-sonar-scanner.js", {
36+
namedExports: { installSonarScanner: mock.fn(async () => "/scanner") },
37+
});
38+
t.mock.module("../run-sonar-scanner.js", {
39+
namedExports: { runSonarScanner: mock.fn(async () => {}) },
40+
});
41+
t.mock.module("../sanity-checks.js", {
42+
namedExports: {
43+
validateScannerVersion: mock.fn(),
44+
checkSonarToken: mock.fn(),
45+
checkMavenProject: mock.fn(),
46+
checkGradleProject: mock.fn(),
47+
},
48+
});
49+
}
50+
51+
describe("getInputs", () => {
52+
it("should mask scannerBinariesAuthHeader using setSecret when provided", async (t) => {
53+
const setSecretFn = mock.fn();
54+
const getInputFn = mock.fn((name) => name === "scannerBinariesAuthHeader" ? "Bearer mytoken" : "");
55+
56+
mockDependencies(t, { getInputFn, setSecretFn });
57+
58+
await import("../index.js?test=set-secret");
59+
60+
assert.equal(setSecretFn.mock.calls.length, 1);
61+
assert.equal(setSecretFn.mock.calls[0].arguments[0], "Bearer mytoken");
62+
});
63+
64+
it("should not call setSecret when scannerBinariesAuthHeader is not provided", async (t) => {
65+
const setSecretFn = mock.fn();
66+
const getInputFn = mock.fn(() => "");
67+
68+
mockDependencies(t, { getInputFn, setSecretFn });
69+
70+
await import("../index.js?test=no-set-secret");
71+
72+
assert.equal(setSecretFn.mock.calls.length, 0);
73+
});
74+
});
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* sonarqube-scan-action
3+
* Copyright (C) 2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
21+
import assert from "node:assert/strict";
22+
import { describe, it, mock } from "node:test";
23+
24+
const SCANNER_VERSION = "6.2.0.4584";
25+
const BINARIES_URL = "https://my.artifactory.example.com/sonar-scanner-cli";
26+
const BINARY_DOWNLOAD_URL = `${BINARIES_URL}/sonar-scanner-cli-${SCANNER_VERSION}-linux-x64.zip`;
27+
28+
function mockUtils(t) {
29+
t.mock.module("../utils.js", {
30+
namedExports: {
31+
getPlatformFlavor: mock.fn(() => "linux-x64"),
32+
getScannerDownloadURL: mock.fn(() => BINARY_DOWNLOAD_URL),
33+
scannerDirName: mock.fn(() => `sonar-scanner-${SCANNER_VERSION}-linux-x64`),
34+
},
35+
});
36+
}
37+
38+
describe("installSonarScanner", () => {
39+
it("should forward scannerBinariesAuthHeader to both binary and signature downloads", async (t) => {
40+
const downloadCalls = [];
41+
const downloadToolFn = mock.fn(async (url, dest, auth) => {
42+
downloadCalls.push({ url, auth });
43+
return `/tmp/downloaded-${downloadCalls.length}`;
44+
});
45+
46+
mockUtils(t);
47+
48+
t.mock.module("@actions/tool-cache", {
49+
namedExports: {
50+
find: mock.fn(() => null),
51+
downloadTool: downloadToolFn,
52+
extractZip: mock.fn(async () => "/tmp/extracted"),
53+
cacheDir: mock.fn(async () => "/tmp/cached"),
54+
},
55+
});
56+
57+
t.mock.module("@actions/core", {
58+
namedExports: {
59+
info: mock.fn(),
60+
warning: mock.fn(),
61+
addPath: mock.fn(),
62+
},
63+
});
64+
65+
t.mock.module("../gpg-verification.js", {
66+
namedExports: {
67+
verifySignature: mock.fn(async () => {}),
68+
},
69+
});
70+
71+
const { installSonarScanner } = await import(
72+
`../install-sonar-scanner.js?test=auth-header`
73+
);
74+
75+
await installSonarScanner({
76+
scannerVersion: SCANNER_VERSION,
77+
scannerBinariesUrl: BINARIES_URL,
78+
scannerBinariesAuthHeader: "Bearer mytoken",
79+
});
80+
81+
assert.equal(downloadCalls.length, 2, "Should download binary and signature");
82+
assert.equal(downloadCalls[0].auth, "Bearer mytoken", "Binary download should use auth header");
83+
assert.equal(downloadCalls[1].auth, "Bearer mytoken", "Signature download should use auth header");
84+
assert.ok(downloadCalls[1].url.endsWith(".asc"), "Second download should be the signature");
85+
});
86+
87+
it("should not set auth header when scannerBinariesAuthHeader is not provided", async (t) => {
88+
const downloadCalls = [];
89+
const downloadToolFn = mock.fn(async (url, dest, auth) => {
90+
downloadCalls.push({ url, auth });
91+
return `/tmp/downloaded-${downloadCalls.length}`;
92+
});
93+
94+
mockUtils(t);
95+
96+
t.mock.module("@actions/tool-cache", {
97+
namedExports: {
98+
find: mock.fn(() => null),
99+
downloadTool: downloadToolFn,
100+
extractZip: mock.fn(async () => "/tmp/extracted"),
101+
cacheDir: mock.fn(async () => "/tmp/cached"),
102+
},
103+
});
104+
105+
t.mock.module("@actions/core", {
106+
namedExports: {
107+
info: mock.fn(),
108+
warning: mock.fn(),
109+
addPath: mock.fn(),
110+
},
111+
});
112+
113+
t.mock.module("../gpg-verification.js", {
114+
namedExports: {
115+
verifySignature: mock.fn(async () => {}),
116+
},
117+
});
118+
119+
const { installSonarScanner } = await import(
120+
`../install-sonar-scanner.js?test=no-auth-header`
121+
);
122+
123+
await installSonarScanner({
124+
scannerVersion: SCANNER_VERSION,
125+
scannerBinariesUrl: BINARIES_URL,
126+
});
127+
128+
assert.equal(downloadCalls.length, 2);
129+
assert.equal(downloadCalls[0].auth, undefined, "Binary download should have no auth header");
130+
assert.equal(downloadCalls[1].auth, undefined, "Signature download should have no auth header");
131+
});
132+
133+
it("should skip signature download when skipSignatureVerification is true", async (t) => {
134+
const downloadCalls = [];
135+
const downloadToolFn = mock.fn(async (url, dest, auth) => {
136+
downloadCalls.push({ url, auth });
137+
return `/tmp/downloaded-${downloadCalls.length}`;
138+
});
139+
140+
mockUtils(t);
141+
142+
t.mock.module("@actions/tool-cache", {
143+
namedExports: {
144+
find: mock.fn(() => null),
145+
downloadTool: downloadToolFn,
146+
extractZip: mock.fn(async () => "/tmp/extracted"),
147+
cacheDir: mock.fn(async () => "/tmp/cached"),
148+
},
149+
});
150+
151+
t.mock.module("@actions/core", {
152+
namedExports: {
153+
info: mock.fn(),
154+
warning: mock.fn(),
155+
addPath: mock.fn(),
156+
},
157+
});
158+
159+
const { installSonarScanner } = await import(
160+
`../install-sonar-scanner.js?test=skip-sig`
161+
);
162+
163+
await installSonarScanner({
164+
scannerVersion: SCANNER_VERSION,
165+
scannerBinariesUrl: BINARIES_URL,
166+
scannerBinariesAuthHeader: "Bearer mytoken",
167+
skipSignatureVerification: true,
168+
});
169+
170+
assert.equal(downloadCalls.length, 1, "Should only download binary, not signature");
171+
assert.equal(downloadCalls[0].auth, "Bearer mytoken");
172+
});
173+
174+
it("should use cached tool when available and skip download", async (t) => {
175+
const downloadToolFn = mock.fn();
176+
177+
mockUtils(t);
178+
179+
t.mock.module("@actions/tool-cache", {
180+
namedExports: {
181+
find: mock.fn(() => "/tmp/cached-tool"),
182+
downloadTool: downloadToolFn,
183+
extractZip: mock.fn(),
184+
cacheDir: mock.fn(),
185+
},
186+
});
187+
188+
t.mock.module("@actions/core", {
189+
namedExports: {
190+
info: mock.fn(),
191+
warning: mock.fn(),
192+
addPath: mock.fn(),
193+
},
194+
});
195+
196+
const { installSonarScanner } = await import(
197+
`../install-sonar-scanner.js?test=cached`
198+
);
199+
200+
await installSonarScanner({
201+
scannerVersion: SCANNER_VERSION,
202+
scannerBinariesUrl: BINARIES_URL,
203+
});
204+
205+
assert.equal(downloadToolFn.mock.calls.length, 0, "Should not download when cached");
206+
});
207+
});

0 commit comments

Comments
 (0)