screwdriver-coverage-sonar 5.1.2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/index.js +106 -21
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -15,6 +15,32 @@ npm install screwdriver-coverage-sonar
|
|
|
15
15
|
npm test
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
## BREAKING CHANGE: `getAccessToken` no longer trusts caller-supplied project identity
|
|
19
|
+
|
|
20
|
+
For security reasons ([PSECBUGS-115276](https://github.com/screwdriver-cd/screwdriver/pull/3518)),
|
|
21
|
+
`getAccessToken` now validates any caller-supplied `projectKey` against an allowlist derived from the
|
|
22
|
+
build's own JWT (`getAuthorizedProjectKeys`), and rejects it with a `403` unless it also matches the
|
|
23
|
+
build's own configured coverage scope — its own pipeline and its own job are both always "authorized," so
|
|
24
|
+
without the scope check a build could still cross between the two. `projectName` and `username` are never
|
|
25
|
+
trusted at all — both are always derived. `username` is checked against the derived value and triggers a
|
|
26
|
+
mismatch warning on its own; `projectName` is accepted only for backward compatibility and is included in
|
|
27
|
+
that warning's message text when one fires, but a stale or bogus `projectName` alone does not trigger it.
|
|
28
|
+
|
|
29
|
+
A caller that still sends only the old resolved tuple (`projectKey`/`username`/`projectName`, no
|
|
30
|
+
`pipelineName`) will still successfully mint a token, but the Git App binding (`configureGitApp`) may be
|
|
31
|
+
skipped for newly created enterprise projects, since it now depends on a resolved `pipelineName` rather than
|
|
32
|
+
a caller-supplied `projectName`. See the `getAccessToken` JSDoc in `index.js` for the full parameter contract.
|
|
33
|
+
|
|
34
|
+
### Deployment order
|
|
35
|
+
|
|
36
|
+
This release requires [screwdriver-cd/screwdriver#3518](https://github.com/screwdriver-cd/screwdriver/pull/3518)
|
|
37
|
+
to already be deployed. Before #3518, the API's `/coverage/token` route only sends `pipelineName` when the
|
|
38
|
+
caller-supplied `projectName` is absent or malformed — so against an un-upgraded API, every `projectKey`
|
|
39
|
+
this plugin receives resolves to an authorized, scope-matching key with no `pipelineName`, and the Git App
|
|
40
|
+
binding is silently skipped for newly created enterprise projects (existing bindings are unaffected). No
|
|
41
|
+
build is rejected and no token request fails; the only symptom is that PR decoration doesn't get configured.
|
|
42
|
+
Deploy #3518 first, then upgrade this dependency.
|
|
43
|
+
|
|
18
44
|
## Related Links
|
|
19
45
|
- See [coverage-base](https://github.com/screwdriver-cd/coverage-base)
|
|
20
46
|
|
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const joi = require('joi');
|
|
7
|
+
const boom = require('@hapi/boom');
|
|
7
8
|
const hoek = require('@hapi/hoek');
|
|
8
9
|
const path = require('path');
|
|
9
10
|
const request = require('screwdriver-request');
|
|
@@ -331,6 +332,41 @@ class CoverageSonar extends CoverageBase {
|
|
|
331
332
|
});
|
|
332
333
|
}
|
|
333
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Determine which Sonar project keys a build is allowed to act on.
|
|
337
|
+
* Derived exclusively from the build JWT, never from caller-supplied input.
|
|
338
|
+
* @method getAuthorizedProjectKeys
|
|
339
|
+
* @param {Object} buildCredentials Information stored in a build JWT
|
|
340
|
+
* @param {String} [buildCredentials.jobId] Screwdriver job ID
|
|
341
|
+
* @param {String} [buildCredentials.pipelineId] Screwdriver pipeline ID
|
|
342
|
+
* @param {String} [buildCredentials.prParentJobId] Screwdriver PR parent job ID
|
|
343
|
+
* @return {Array} Sonar project keys the build may use
|
|
344
|
+
*/
|
|
345
|
+
getAuthorizedProjectKeys({ jobId, pipelineId, prParentJobId }) {
|
|
346
|
+
const ids = [
|
|
347
|
+
['pipeline', pipelineId],
|
|
348
|
+
['job', jobId],
|
|
349
|
+
['job', prParentJobId]
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
return ids.filter(([, id]) => id !== undefined && id !== null).map(([prefix, id]) => `${prefix}:${id}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Resolve the coverage scope: the caller-configured scope when there is one, otherwise the default
|
|
357
|
+
* for this SonarQube edition (pipeline for enterprise, job for everything else).
|
|
358
|
+
* @method _resolveCoverageScope
|
|
359
|
+
* @param {Object} config
|
|
360
|
+
* @param {String} [config.scope] Coverage scope (pipeline or job)
|
|
361
|
+
* @param {Boolean} config.enterpriseEnabled If enterprise is enabled
|
|
362
|
+
* @return {String} Resolved coverage scope
|
|
363
|
+
*/
|
|
364
|
+
_resolveCoverageScope({ scope, enterpriseEnabled }) {
|
|
365
|
+
const userScope = scope && scope !== 'undefined' ? scope : undefined;
|
|
366
|
+
|
|
367
|
+
return userScope || (enterpriseEnabled ? 'pipeline' : 'job');
|
|
368
|
+
}
|
|
369
|
+
|
|
334
370
|
/**
|
|
335
371
|
* Determine Sonar project key, project name, and username based on:
|
|
336
372
|
* - SonarQube edition
|
|
@@ -382,9 +418,7 @@ class CoverageSonar extends CoverageBase {
|
|
|
382
418
|
return projectData;
|
|
383
419
|
}
|
|
384
420
|
|
|
385
|
-
|
|
386
|
-
const userScope = scope && scope !== 'undefined' ? scope : undefined;
|
|
387
|
-
const coverageScope = userScope || (enterpriseEnabled ? 'pipeline' : 'job');
|
|
421
|
+
const coverageScope = this._resolveCoverageScope({ scope, enterpriseEnabled });
|
|
388
422
|
|
|
389
423
|
if (coverageScope === 'pipeline') {
|
|
390
424
|
const componentId = encodeURIComponent(`pipeline:${pipelineId}`);
|
|
@@ -399,8 +433,10 @@ class CoverageSonar extends CoverageBase {
|
|
|
399
433
|
};
|
|
400
434
|
}
|
|
401
435
|
|
|
402
|
-
// Use prParentJobId as ID for PRs
|
|
403
|
-
|
|
436
|
+
// Use prParentJobId as ID for PRs. jobName is what identifies a PR job, so without it a PR build
|
|
437
|
+
// falls back to its own job's project rather than its parent's; callers that cannot resolve jobName
|
|
438
|
+
// must not crash here.
|
|
439
|
+
if (coverageScope === 'job' && enterpriseEnabled && jobName) {
|
|
404
440
|
const prRegex = /^PR-(\d+)(?::([\w-]+))?$/;
|
|
405
441
|
const prNameMatch = jobName.match(prRegex);
|
|
406
442
|
|
|
@@ -426,27 +462,76 @@ class CoverageSonar extends CoverageBase {
|
|
|
426
462
|
* @param {Object} config.buildCredentials Information stored in a build JWT
|
|
427
463
|
* @param {String} [config.jobName] Screwdriver job name
|
|
428
464
|
* @param {String} config.pipelineName Screwdriver pipeline name
|
|
429
|
-
* @param {String} [config.projectKey] Sonar project key
|
|
430
|
-
*
|
|
431
|
-
*
|
|
465
|
+
* @param {String} [config.projectKey] Sonar project key. Rejected with a 403 if it does not name a
|
|
466
|
+
* project the calling build is authorized for (its own pipeline,
|
|
467
|
+
* its own job, or its PR parent job), or if it does not match
|
|
468
|
+
* the build's own configured coverage scope.
|
|
469
|
+
* @param {String} [config.projectName] Ignored; retained for backward compatibility. Always derived,
|
|
470
|
+
* never taken from the caller. Included in the mismatch warning
|
|
471
|
+
* message when username mismatches, but does not itself trigger
|
|
472
|
+
* that warning (pipelineName, its derivation source, is
|
|
473
|
+
* unavailable to legacy callers on every request).
|
|
474
|
+
* @param {String} [config.username] Ignored; retained for backward compatibility and mismatch
|
|
475
|
+
* logging only. Always derived from the project key.
|
|
432
476
|
* @return {Promise} An access token that build can use
|
|
433
477
|
* to talk to coverage server
|
|
434
478
|
*/
|
|
435
479
|
_getAccessToken({ scope, username, projectKey, projectName, jobName, pipelineName, buildCredentials }) {
|
|
436
480
|
const { jobId, pipelineId, prParentJobId, scmContext } = buildCredentials;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
481
|
+
const authorizedProjectKeys = this.getAuthorizedProjectKeys({ jobId, pipelineId, prParentJobId });
|
|
482
|
+
|
|
483
|
+
// A build may only ever request a token for its own pipeline or job.
|
|
484
|
+
if (projectKey && !authorizedProjectKeys.includes(String(projectKey))) {
|
|
485
|
+
logger.error(
|
|
486
|
+
`Rejected coverage token request for project ${projectKey} from build in pipeline ${pipelineId} / ` +
|
|
487
|
+
`job ${jobId}; authorized: ${authorizedProjectKeys.join(', ')}`
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
return Promise.reject(
|
|
491
|
+
boom.forbidden(`Not authorized to request a coverage token for project ${projectKey}`)
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// An authorized key is only trusted when it also matches the build's own configured coverage
|
|
496
|
+
// scope - otherwise a build could use an authorized key for the *other* scope (its own pipeline and
|
|
497
|
+
// its own job are both always "authorized") to cross from job to pipeline scope or back. Reject
|
|
498
|
+
// outright rather than silently re-deriving: a silent mismatch would mint a token for one project
|
|
499
|
+
// while the scanner uploads to another, and commands.txt runs the scanner under `|| true`, so that
|
|
500
|
+
// failure would never surface.
|
|
501
|
+
const coverageScope = this._resolveCoverageScope({ scope, enterpriseEnabled: this.sonarEnterprise });
|
|
502
|
+
|
|
503
|
+
if (projectKey && projectKey.split(':')[0] !== coverageScope) {
|
|
504
|
+
logger.error(
|
|
505
|
+
`Rejected coverage token request for project ${projectKey} from build in pipeline ${pipelineId} / ` +
|
|
506
|
+
`job ${jobId}; does not match this build's coverage scope: ${coverageScope}`
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
return Promise.reject(
|
|
510
|
+
boom.forbidden(`Project ${projectKey} does not match coverage scope ${coverageScope} for this build`)
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// projectName and username are never trusted here, even when projectKey is: every Sonar call below
|
|
515
|
+
// runs with the instance-wide admin token, so a caller-supplied projectName would let a build point
|
|
516
|
+
// another project's Git App binding at an arbitrary repository, and a caller-supplied username would
|
|
517
|
+
// let a build mint a token for an arbitrary Sonar account. Both are always derived instead.
|
|
518
|
+
const projectData = this.getProjectData({
|
|
519
|
+
enterpriseEnabled: this.sonarEnterprise,
|
|
520
|
+
jobId,
|
|
521
|
+
jobName,
|
|
522
|
+
prParentJobId,
|
|
523
|
+
pipelineId,
|
|
524
|
+
pipelineName,
|
|
525
|
+
scope,
|
|
526
|
+
projectKey
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
if (username && username !== projectData.username) {
|
|
530
|
+
logger.warn(
|
|
531
|
+
`Coverage token request for build in pipeline ${pipelineId} / job ${jobId} supplied ` +
|
|
532
|
+
`username=${JSON.stringify(username)}, projectName=${JSON.stringify(projectName)}, but ` +
|
|
533
|
+
`derived username=${JSON.stringify(projectData.username)} instead; using the derived value`
|
|
534
|
+
);
|
|
450
535
|
}
|
|
451
536
|
|
|
452
537
|
const password = uuidv4();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "screwdriver-coverage-sonar",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "SonarQube implementation of the coverage-base class",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"sinon": "^15.0.0"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"@hapi/boom": "^10.0.1",
|
|
44
45
|
"@hapi/hoek": "^11.0.7",
|
|
45
46
|
"joi": "^17.13.3",
|
|
46
47
|
"screwdriver-coverage-base": "^4.0.0",
|