@docupace/pipeline 1.0.122 → 1.0.123-xray.1

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/.gitignore ADDED
@@ -0,0 +1,103 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # IDE
4
+ .idea
5
+ *.iml
6
+ # Dependencies
7
+ /node_modules
8
+ /.pnp
9
+ .pnp.js
10
+
11
+ # Testing
12
+ /coverage
13
+
14
+ # Next.js
15
+ /.next/
16
+ /out/
17
+
18
+ # Production
19
+ /build
20
+ /dist
21
+
22
+ # Misc
23
+ .DS_Store
24
+ *.pem
25
+ cypress/screenshots/
26
+
27
+ # Debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+
32
+ # Local env files
33
+ .env*.local
34
+
35
+ # Vercel
36
+ .vercel
37
+
38
+ # Typescript
39
+ *.tsbuildinfo
40
+ next-env.d.ts
41
+
42
+
43
+ # Logs
44
+ logs
45
+ *.log
46
+
47
+ # Runtime data
48
+ pids
49
+ *.pid
50
+ *.seed
51
+
52
+ # Directory for instrumented libs generated by jscoverage/JSCover
53
+ lib-cov
54
+
55
+ # Coverage directory used by tools like istanbul
56
+ coverage
57
+
58
+ # nyc test coverage
59
+ .nyc_output
60
+
61
+ # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
62
+ .grunt
63
+
64
+ # node-waf configuration
65
+ .lock-wscript
66
+
67
+ # Compiled binary addons (http://nodejs.org/api/addons.html)
68
+ build/Release
69
+
70
+ # Dependency directories
71
+ node_modules
72
+ jspm_packages
73
+
74
+ # Optional npm cache directory
75
+ .npm
76
+
77
+ # Optional REPL history
78
+ .node_repl_history
79
+ .next
80
+
81
+ scratch
82
+
83
+ # visual studio code
84
+ .vscode/
85
+
86
+ src/gql/graphql.js
87
+ .yalc/
88
+ yalc.lock
89
+
90
+ .last-run.json
91
+
92
+ test-results
93
+ coverage-reports
94
+ .sonar
95
+ .v8-coverage
96
+ /assets
97
+ tests/lib/data/auth.json
98
+
99
+ failed-tests.json
100
+
101
+ /tmp
102
+
103
+ .claude
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @docupace/pipeline
2
2
 
3
+ ## 1.0.123
4
+
5
+ ### Patch Changes
6
+
7
+ - 49984e2: Add Veracode upload-and-scan and SonarQube scan/PR-quality-gate shared pipelines (veracode-scan, sonar-scan, sonar-pr-gate)
8
+
3
9
  ## 1.0.121
4
10
 
5
11
  ### Patch Changes
@@ -292,6 +292,273 @@ definitions:
292
292
  tenable-scan:
293
293
  - step: *tenable-scan-step
294
294
 
295
+ # Uploads and scans one or more app targets with Veracode:
296
+ # import: pipeline-shared-scripts:master:veracode-scan
297
+ #
298
+ # Required: VERACODE_API_ID, VERACODE_API_KEY, VERACODE_TARGETS
299
+ # VERACODE_TARGETS="label:appname:path1,path2,...[;label2:appname2:...]"
300
+ # (an app profile named <appname> must already exist in the Veracode portal)
301
+ # Optional: VERACODE_BUILD_CMD — eval'd before packaging, so this step is
302
+ # self-contained and needs no separate build step in the consumer pipeline.
303
+ veracode-scan:
304
+ - step:
305
+ name: Veracode Upload and Scan
306
+ runs-on:
307
+ - self.hosted
308
+ - linux.shell
309
+ - onprem
310
+ script:
311
+ - |
312
+ if [ -z "${VERACODE_API_ID:-}" ] || [ -z "${VERACODE_API_KEY:-}" ]; then
313
+ echo "ERROR: VERACODE_API_ID / VERACODE_API_KEY not set — cannot run Veracode scan." >&2
314
+ exit 1
315
+ fi
316
+ if [ -z "${VERACODE_TARGETS:-}" ]; then
317
+ echo "ERROR: VERACODE_TARGETS not set — cannot run Veracode scan." >&2
318
+ echo " Expected: label:appname:path1,path2,...[;label2:appname2:path1,...]" >&2
319
+ exit 1
320
+ fi
321
+ - |
322
+ # VERACODE_BUILD_CMD lets a consumer produce whatever build artifacts
323
+ # Veracode's SAST needs (compiled bundles, etc.) without a separate
324
+ # pre-build step in the consumer's own pipeline — the same
325
+ # self-contained approach the Xray step above uses for its Docker build.
326
+ if [ -n "${VERACODE_BUILD_CMD:-}" ]; then
327
+ echo "Running VERACODE_BUILD_CMD..."
328
+ eval "${VERACODE_BUILD_CMD}"
329
+ fi
330
+ - |
331
+ # Cached across builds on self-hosted runners; downloaded once.
332
+ if [ ! -f veracode-wrapper/VeracodeJavaAPI.jar ]; then
333
+ mkdir -p veracode-wrapper
334
+ curl -sSL "https://repo1.maven.org/maven2/com/veracode/vosp/api/wrappers/vosp-api-wrappers-java/26.3.19.0/vosp-api-wrappers-java-26.3.19.0.jar" \
335
+ -o veracode-wrapper/VeracodeJavaAPI.jar
336
+ fi
337
+ - |
338
+ # Each target is zipped and submitted as its own uploadandscan call,
339
+ # so one repo with several apps (e.g. a frontend and a backend) gets
340
+ # separate Veracode profiles instead of one scan covering everything.
341
+ SHORT_SHA="$(printf '%s' "${BITBUCKET_COMMIT:-local}" | cut -c1-7)"
342
+ failed=0
343
+ OLD_IFS="${IFS}"
344
+ IFS=';'
345
+ set -- ${VERACODE_TARGETS}
346
+ IFS="${OLD_IFS}"
347
+ for target in "$@"; do
348
+ [ -z "${target}" ] && continue
349
+ label="$(printf '%s' "${target}" | cut -d: -f1)"
350
+ appname="$(printf '%s' "${target}" | cut -d: -f2)"
351
+ paths="$(printf '%s' "${target}" | cut -d: -f3)"
352
+ [ -z "${label}" ] && label="app"
353
+ slug="$(printf '%s' "${label}" | tr -c 'A-Za-z0-9._-' '-')"
354
+
355
+ if [ -z "${appname}" ] || [ -z "${paths}" ]; then
356
+ echo "Target '${label}' is missing an appname or paths in VERACODE_TARGETS — skipping." >&2
357
+ failed=1
358
+ continue
359
+ fi
360
+
361
+ zip_file="veracode-${slug}.zip"
362
+ rm -f "${zip_file}"
363
+ OLD_IFS2="${IFS}"
364
+ IFS=','
365
+ set -- ${paths}
366
+ IFS="${OLD_IFS2}"
367
+ zip -r "${zip_file}" "$@"
368
+
369
+ # -createprofile false — the app profile must already exist in the
370
+ # Veracode portal. -autoscan true starts the scan once prescan passes.
371
+ java -jar veracode-wrapper/VeracodeJavaAPI.jar \
372
+ -vid "${VERACODE_API_ID}" \
373
+ -vkey "${VERACODE_API_KEY}" \
374
+ -action uploadandscan \
375
+ -appname "${appname}" \
376
+ -createprofile false \
377
+ -autoscan true \
378
+ -filepath "${zip_file}" \
379
+ -version "sha-${SHORT_SHA}-${label}" || { failed=1; echo "ERROR: Veracode scan submission failed for '${label}'." >&2; }
380
+
381
+ rm -f "${zip_file}"
382
+ done
383
+ [ "${failed}" -ne 0 ] && exit 1
384
+ exit 0
385
+
386
+ # Runs a SonarQube analysis against the current branch, fire-and-forget
387
+ # (no quality-gate enforcement): import: pipeline-shared-scripts:master:sonar-scan
388
+ #
389
+ # Required: SONAR_TOKEN (soft-skips the whole step if unset)
390
+ # Optional: SONAR_HOST_URL (default https://sonarcloud.io), SONAR_DEFAULT_BRANCH
391
+ # (default "main").
392
+ #
393
+ # Single project (default): SONAR_PROJECT_DIR (default ".", the dir holding
394
+ # sonar-project.properties), SONAR_COVERAGE_CMD (eval'd before scanning),
395
+ # SONAR_COVERAGE_REPORT_PATH (if set, required to exist and be non-empty
396
+ # afterward).
397
+ #
398
+ # Multiple projects in one repo (e.g. a frontend and a backend, each its own
399
+ # sonar.projectKey): set SONAR_TARGETS instead of SONAR_PROJECT_DIR —
400
+ # "label:projectDir;label2:projectDir2" (semicolon between targets, mirrors
401
+ # VERACODE_TARGETS' delimiter convention). Each label's own coverage
402
+ # command/report path can be set via SONAR_COVERAGE_CMD_<LABEL> /
403
+ # SONAR_COVERAGE_REPORT_PATH_<LABEL> (label upper-cased, non-alphanumerics
404
+ # replaced with "_" — label "frontend" reads SONAR_COVERAGE_CMD_FRONTEND),
405
+ # falling back to the global SONAR_COVERAGE_CMD/SONAR_COVERAGE_REPORT_PATH
406
+ # when no per-target one is set. A failed target is reported and scanning
407
+ # continues on to the rest; the step fails afterward if any target failed —
408
+ # same pattern as VERACODE_TARGETS.
409
+ #
410
+ # depth: full — Sonar reads git blame to attribute New Code; a shallow clone
411
+ # only sees the tip commit and silently treats everything as new.
412
+ sonar-scan: &sonar-scan-pipeline
413
+ - step: &sonar-scan-step
414
+ name: SonarQube Scan
415
+ runs-on:
416
+ - self.hosted
417
+ - linux.shell
418
+ - onprem
419
+ clone:
420
+ depth: full
421
+ script:
422
+ - |
423
+ if [ -z "${SONAR_TOKEN:-}" ]; then
424
+ echo "Skipping SonarQube scan: SONAR_TOKEN not set"
425
+ exit 0
426
+ fi
427
+ - &sonar-scan-script |
428
+ export DOCKER_HOST=""
429
+ unset DOCKER_CONTEXT
430
+ SONAR_HOST="${SONAR_HOST_URL:-https://sonarcloud.io}"
431
+ DEFAULT_BRANCH="${SONAR_DEFAULT_BRANCH:-main}"
432
+ WORKDIR="$(pwd)"
433
+
434
+ # Bitbucket's built-ins: BITBUCKET_BRANCH / BITBUCKET_PR_ID /
435
+ # BITBUCKET_PR_DESTINATION_BRANCH. An upload with no branch parameter
436
+ # is filed against the project's default branch, so scanning a
437
+ # feature branch without one would silently overwrite that branch's
438
+ # analysis and quality-gate status. Resolved once — every target in
439
+ # this run shares the same branch/PR context.
440
+ BRANCH="${BITBUCKET_BRANCH:-}"
441
+ PR_ID="${BITBUCKET_PR_ID:-}"
442
+ PR_BASE="${BITBUCKET_PR_DESTINATION_BRANCH:-}"
443
+ BRANCH_DOCKER_ARGS=""
444
+ BRANCH_SONAR_ARGS=""
445
+
446
+ if [ -n "${PR_ID}" ]; then
447
+ BRANCH_DOCKER_ARGS="-e PR_ID=${PR_ID}"
448
+ BRANCH_SONAR_ARGS="-Dsonar.pullrequest.key=${PR_ID}"
449
+ [ -n "${BRANCH}" ] && BRANCH_SONAR_ARGS="${BRANCH_SONAR_ARGS} -Dsonar.pullrequest.branch=${BRANCH}"
450
+ [ -n "${PR_BASE}" ] && BRANCH_SONAR_ARGS="${BRANCH_SONAR_ARGS} -Dsonar.pullrequest.base=${PR_BASE}"
451
+ elif [ -n "${BRANCH}" ] && [ "${BRANCH}" != "${DEFAULT_BRANCH}" ]; then
452
+ BRANCH_DOCKER_ARGS="-e BRANCH=${BRANCH}"
453
+ BRANCH_SONAR_ARGS="-Dsonar.branch.name=${BRANCH}"
454
+ elif [ -n "${BITBUCKET_TAG:-}" ] || [ "${BRANCH}" = "${DEFAULT_BRANCH}" ]; then
455
+ echo "Sonar: analysing as the default branch (${DEFAULT_BRANCH})"
456
+ else
457
+ echo "ERROR: cannot tell which branch this analysis belongs to — none of BITBUCKET_PR_ID, BITBUCKET_BRANCH or BITBUCKET_TAG is set." >&2
458
+ echo " Refusing to upload: SonarCloud would file it against '${DEFAULT_BRANCH}' and overwrite that branch's analysis." >&2
459
+ exit 1
460
+ fi
461
+
462
+ if [ "${SONAR_WAIT_FOR_GATE:-false}" = "true" ]; then
463
+ BRANCH_SONAR_ARGS="${BRANCH_SONAR_ARGS} -Dsonar.qualitygate.wait=true -Dsonar.qualitygate.timeout=${SONAR_QUALITYGATE_TIMEOUT:-300}"
464
+ fi
465
+
466
+ # Looks up "${1}_<SLUG>" if set, else prints nothing — POSIX sh has no
467
+ # ${!indirect} expansion, so this shells out through eval instead.
468
+ target_var() {
469
+ eval "printf '%s' \"\${$1_${2}:-}\""
470
+ }
471
+
472
+ # Each target is its own SonarCloud project — like Veracode's per-app
473
+ # profiles, a repo with several components (frontend/backend) needs
474
+ # its own sonar.projectKey per component, not one shared analysis.
475
+ failed=0
476
+ OLD_IFS="${IFS}"
477
+ IFS=';'
478
+ set -- ${SONAR_TARGETS:-default:${SONAR_PROJECT_DIR:-.}}
479
+ IFS="${OLD_IFS}"
480
+
481
+ for target in "$@"; do
482
+ [ -z "${target}" ] && continue
483
+ label="$(printf '%s' "${target}" | cut -d: -f1)"
484
+ project_dir="$(printf '%s' "${target}" | cut -d: -f2)"
485
+ [ -z "${label}" ] && label="default"
486
+ [ -z "${project_dir}" ] && project_dir="."
487
+ slug="$(printf '%s' "${label}" | tr -c 'A-Za-z0-9' '_' | tr 'a-z' 'A-Z')"
488
+
489
+ coverage_cmd="$(target_var SONAR_COVERAGE_CMD "${slug}")"
490
+ [ -z "${coverage_cmd}" ] && coverage_cmd="${SONAR_COVERAGE_CMD:-}"
491
+ coverage_path="$(target_var SONAR_COVERAGE_REPORT_PATH "${slug}")"
492
+ [ -z "${coverage_path}" ] && coverage_path="${SONAR_COVERAGE_REPORT_PATH:-}"
493
+
494
+ # SONAR_COVERAGE_CMD(_<LABEL>) lets a consumer produce a coverage
495
+ # report before the scan runs, without a separate pre-scan step —
496
+ # the same self-contained approach as VERACODE_BUILD_CMD above.
497
+ # SONAR_COVERAGE_REPORT_PATH(_<LABEL>), if set, is required to exist
498
+ # afterward: a run with no coverage report still "succeeds" but
499
+ # silently reports 0% coverage.
500
+ if [ -n "${coverage_cmd}" ]; then
501
+ echo "[${label}] Running coverage command..."
502
+ eval "${coverage_cmd}" || echo "WARN: coverage command exited non-zero for '${label}'; checking for the report anyway."
503
+ fi
504
+ if [ -n "${coverage_path}" ] && [ ! -s "${coverage_path}" ]; then
505
+ echo "ERROR: ${coverage_path} missing or empty for target '${label}' — Sonar would report 0% coverage." >&2
506
+ failed=1
507
+ continue
508
+ fi
509
+
510
+ DOCKER_ARGS="run --rm --platform linux/amd64 -e SONAR_HOST_URL=${SONAR_HOST} -e SONAR_TOKEN ${BRANCH_DOCKER_ARGS} -v ${WORKDIR}:/workspace -w /workspace/${project_dir}"
511
+ SONAR_ARGS="sonar-scanner -Dsonar.host.url=${SONAR_HOST} -Dproject.settings=/workspace/${project_dir}/sonar-project.properties ${BRANCH_SONAR_ARGS}"
512
+
513
+ echo "[${label}] Running: docker ${DOCKER_ARGS} sonarsource/sonar-scanner-cli:latest ${SONAR_ARGS}"
514
+ docker ${DOCKER_ARGS} sonarsource/sonar-scanner-cli:latest ${SONAR_ARGS} || {
515
+ failed=1
516
+ echo "ERROR: Sonar scan failed for target '${label}'." >&2
517
+ }
518
+ done
519
+
520
+ [ "${failed}" -ne 0 ] && exit 1
521
+ exit 0
522
+
523
+ # Enforces the SonarQube quality gate on a PR into SONAR_GATE_BRANCH (default
524
+ # "main"); skips for PRs into any other branch:
525
+ # import: pipeline-shared-scripts:master:sonar-pr-gate
526
+ #
527
+ # Required: SONAR_TOKEN (hard-fails if unset and the gate applies to this PR)
528
+ # Optional: same as sonar-scan, plus SONAR_GATE_BRANCH (default "main") and
529
+ # SONAR_QUALITYGATE_TIMEOUT (default 300 seconds).
530
+ sonar-pr-gate:
531
+ - step:
532
+ name: SonarQube Quality Gate
533
+ runs-on:
534
+ - self.hosted
535
+ - linux.shell
536
+ - onprem
537
+ clone:
538
+ depth: full
539
+ script:
540
+ - |
541
+ DEST="${BITBUCKET_PR_DESTINATION_BRANCH:-}"
542
+ GATE_BRANCH="${SONAR_GATE_BRANCH:-main}"
543
+
544
+ if [ -z "${DEST}" ]; then
545
+ echo "ERROR: BITBUCKET_PR_DESTINATION_BRANCH not set — refusing to run the PR quality gate blind." >&2
546
+ exit 1
547
+ fi
548
+
549
+ if [ "${DEST}" != "${GATE_BRANCH}" ]; then
550
+ echo "Skipping Sonar quality gate: PR targets '${DEST}', not '${GATE_BRANCH}'"
551
+ exit 0
552
+ fi
553
+
554
+ if [ -z "${SONAR_TOKEN:-}" ]; then
555
+ echo "ERROR: SONAR_TOKEN not set — cannot enforce the Sonar quality gate for a PR into ${GATE_BRANCH}." >&2
556
+ exit 1
557
+ fi
558
+
559
+ export SONAR_WAIT_FOR_GATE=true
560
+ - *sonar-scan-script
561
+
295
562
  steps:
296
563
  - step: &version-step
297
564
  runs-on:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docupace/pipeline",
3
- "version": "1.0.122",
3
+ "version": "1.0.123-xray.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -12,7 +12,10 @@
12
12
  "dp-next-cli": "src/next/cli.js",
13
13
  "dp-auth0-cli": "src/auth0/cli.js",
14
14
  "dp-merge-cli": "src/merge/cli.js",
15
- "dp-micro-cli": "src/micro/cli.js"
15
+ "dp-micro-cli": "src/micro/cli.js",
16
+ "dp-xray-cli": "src/xray/cli.js",
17
+ "dp-tenable-cli": "src/tenable/cli.js",
18
+ "dp-veracode-cli": "src/veracode/cli.js"
16
19
  },
17
20
  "keywords": [
18
21
  "bash"
@@ -0,0 +1,3 @@
1
+ allowBuilds:
2
+ esbuild: true
3
+ unrs-resolver: true
@@ -1,14 +1,24 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { fileURLToPath } from 'url';
3
4
  import { $ } from 'zx';
4
5
 
5
6
  import { setGitInfo } from '../next/util.js';
6
7
  import { versionStatus } from './version.js';
7
8
 
9
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
10
+ const PACKAGE_VERSION = JSON.parse(
11
+ fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'),
12
+ ).version;
13
+
8
14
  export async function validateChangesets() {
15
+ console.log(`validateChangesets: @docupace/pipeline version = ${PACKAGE_VERSION}`);
9
16
  await setGitInfo();
10
17
  const destBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
18
+ console.log(`validateChangesets: destBranch = ${destBranch}`);
19
+ console.log(`validateChangesets: fetching origin ${destBranch}:${destBranch}`);
11
20
  await $`git fetch origin ${destBranch}:${destBranch}`;
21
+ console.log('validateChangesets: fetch succeeded');
12
22
  let releases;
13
23
 
14
24
  try {
package/src/next/util.js CHANGED
@@ -84,12 +84,41 @@ export async function getChangedAppPackages() {
84
84
  return appPackages;
85
85
  }
86
86
 
87
+ function maskCreds(url) {
88
+ return url.replace(/:[^:@/]+@/, ':***@');
89
+ }
90
+
87
91
  export async function setGitInfo() {
92
+ console.log(`setGitInfo: SET_GITOPS_USER=${process.env.SET_GITOPS_USER || '(unset)'}`);
88
93
  if (process.env.SET_GITOPS_USER) {
94
+ const before = (await $`git remote get-url origin`).stdout.trim();
95
+ console.log(`setGitInfo: origin before = ${maskCreds(before)}`);
96
+
89
97
  await $`git config --global user.email "gitops@docupace.com"`;
90
98
  await $`git config --global user.name "Gitops"`;
91
- const repoUrl = `git@bitbucket.org:dp-cloud/${process.env.BITBUCKET_REPO_SLUG}.git`;
92
- console.log(`Setting new origin: ${repoUrl}`);
93
- await $`git remote set-url origin ${repoUrl}`;
99
+
100
+ // Some self-hosted runners rewrite `git@bitbucket.org:` (SSH) to a bare,
101
+ // credential-less `https://bitbucket.org/` via a git `insteadOf` rule, so an
102
+ // SSH-form origin silently loses its identity at fetch/push time. Use an
103
+ // HTTPS URL with the token embedded instead — insteadOf doesn't touch it.
104
+ const token =
105
+ process.env.BITBUCKET_STEP_OAUTH_TOKEN || process.env.BITBUCKET_TOKEN;
106
+ if (!token) {
107
+ console.log(
108
+ 'setGitInfo: no BITBUCKET_STEP_OAUTH_TOKEN/BITBUCKET_TOKEN available, leaving origin as-is',
109
+ );
110
+ } else {
111
+ const repoUrl = `https://x-token-auth:${token}@bitbucket.org/dp-cloud/${process.env.BITBUCKET_REPO_SLUG}.git`;
112
+ console.log(`Setting new origin: ${maskCreds(repoUrl)}`);
113
+ await $`git remote set-url origin ${repoUrl}`;
114
+ }
115
+
116
+ const after = (await $`git remote get-url origin`).stdout.trim();
117
+ console.log(`setGitInfo: origin after = ${maskCreds(after)}`);
118
+ } else {
119
+ const current = (await $`git remote get-url origin`).stdout.trim();
120
+ console.log(
121
+ `setGitInfo: SET_GITOPS_USER not set, leaving origin as-is = ${maskCreds(current)}`,
122
+ );
94
123
  }
95
124
  }
@@ -0,0 +1,157 @@
1
+ // Pure parsing/formatting logic for the Tenable scan step, ported from
2
+ // bin/tenable-scan.sh. No fs/docker calls here so it's unit-testable.
3
+
4
+ // Mirrors `tr -c 'A-Za-z0-9._-' '-'`: every character outside that set is
5
+ // replaced with '-' (no squeezing of consecutive replacements).
6
+ export function slugify(label) {
7
+ return label.replace(/[^A-Za-z0-9._-]/g, '-');
8
+ }
9
+
10
+ // Splits `str` on `sep` into exactly `n` fields, folding any extra fields
11
+ // into the last one — matching `read a b c d <<< "$str"` semantics, where a
12
+ // trailing variable absorbs everything left over.
13
+ function splitFixed(str, sep, n) {
14
+ const parts = str.split(sep);
15
+ if (parts.length <= n) {
16
+ return [...parts, ...Array(n - parts.length).fill('')];
17
+ }
18
+ return [...parts.slice(0, n - 1), parts.slice(n - 1).join(sep)];
19
+ }
20
+
21
+ // One "label:auditDir:buildContext:dockerfile" entry from SCAN_TARGETS.
22
+ // auditDir belongs to the Xray step and is ignored here. A target with no
23
+ // build context is returned with skipped: true rather than dropped, so the
24
+ // caller can log why it was skipped (matching the bash script's behavior).
25
+ export function parseScanTargets(scanTargetsStr) {
26
+ return (scanTargetsStr || '')
27
+ .split(';')
28
+ .map((t) => t.trim())
29
+ .filter(Boolean)
30
+ .map((raw) => {
31
+ const [rawLabel, , rawContext, rawDockerfile] = splitFixed(raw, ':', 4);
32
+ const label = rawLabel || 'app';
33
+ const slug = slugify(label);
34
+ if (!rawContext) {
35
+ return { label, slug, buildContext: '', dockerfile: '', skipped: true };
36
+ }
37
+ return {
38
+ label,
39
+ slug,
40
+ buildContext: rawContext,
41
+ dockerfile: rawDockerfile || `${rawContext}/Dockerfile`,
42
+ skipped: false,
43
+ };
44
+ });
45
+ }
46
+
47
+ export function defaultScanTargets(buildContext, dockerfilePath) {
48
+ const ctx = buildContext || '.';
49
+ return `app::${ctx}:${dockerfilePath || `${ctx}/Dockerfile`}`;
50
+ }
51
+
52
+ export function buildCodeMetadata({
53
+ commit = 'local',
54
+ branch = '',
55
+ prDestinationBranch = '',
56
+ workspace = 'unknown',
57
+ repoSlug = 'unknown',
58
+ gitAuthorName = '',
59
+ } = {}) {
60
+ return {
61
+ shortSha: commit.slice(0, 7),
62
+ codeBranch: branch || prDestinationBranch || 'unknown',
63
+ codeRepoUrl: `https://bitbucket.org/${workspace}/${repoSlug}`,
64
+ codeUser: gitAuthorName || 'bitbucket-pipelines',
65
+ };
66
+ }
67
+
68
+ // The scanner requires --pipeline-run-url as soon as any pipeline flag is
69
+ // supplied, so the trio (id/trigger/url) travels together or not at all —
70
+ // includeInScanArgs tells the caller which case applies.
71
+ export function buildPipelineRunInfo({
72
+ runId = '',
73
+ runTrigger = '',
74
+ runUrl = '',
75
+ prId = '',
76
+ buildNumber = '',
77
+ workspace = 'unknown',
78
+ repoSlug = 'unknown',
79
+ } = {}) {
80
+ const resolvedRunId = runId || buildNumber || '';
81
+ const resolvedTrigger = runTrigger || (prId ? 'pull_request' : '') || 'push';
82
+ const resolvedUrl =
83
+ runUrl ||
84
+ (buildNumber
85
+ ? `https://bitbucket.org/${workspace}/${repoSlug}/pipelines/results/${buildNumber}`
86
+ : '');
87
+
88
+ return {
89
+ runId: resolvedRunId,
90
+ runTrigger: resolvedTrigger,
91
+ runUrl: resolvedUrl,
92
+ includeInScanArgs: Boolean(resolvedRunId) && Boolean(resolvedUrl),
93
+ };
94
+ }
95
+
96
+ // The scanner's documented vocabularies are lowercase — severities
97
+ // information|low|medium|high|critical, formats cyclonedx|json|junit|md — and it
98
+ // does not correct a case mismatch or complain about one: it just exports nothing.
99
+ // Callers set these from env vars that read naturally capitalised ("Medium"), so
100
+ // normalise here rather than making every caller remember.
101
+ export function buildScanArgs({
102
+ slug,
103
+ scanTarget,
104
+ codeBranch,
105
+ commit,
106
+ codeUser,
107
+ codeRepoUrl,
108
+ minSeverity,
109
+ runInfo,
110
+ failOnMinSeverity = '',
111
+ }) {
112
+ const args = [
113
+ 'container-image',
114
+ 'scan',
115
+ '--archive-path',
116
+ '/tmp/tenable/image.tar',
117
+ '--name',
118
+ scanTarget,
119
+ '--code-branch',
120
+ codeBranch,
121
+ '--code-commit-hash',
122
+ commit,
123
+ '--code-commit-user',
124
+ codeUser,
125
+ '--code-repository-url',
126
+ codeRepoUrl,
127
+ '--min-severity',
128
+ String(minSeverity).toLowerCase(),
129
+ '--output-file-formats',
130
+ 'json,md,junit',
131
+ '--output-path',
132
+ '/tmp/tenable/out',
133
+ '--output-file-name',
134
+ `${slug}-tenable`,
135
+ '--no-color',
136
+ ];
137
+
138
+ if (runInfo?.includeInScanArgs) {
139
+ args.push(
140
+ '--pipeline-run-id',
141
+ runInfo.runId,
142
+ '--pipeline-run-trigger',
143
+ runInfo.runTrigger,
144
+ '--pipeline-run-url',
145
+ runInfo.runUrl,
146
+ );
147
+ }
148
+
149
+ if (failOnMinSeverity) {
150
+ args.push('--fail-on-min-severity', failOnMinSeverity.toLowerCase());
151
+ }
152
+
153
+ return args;
154
+ }
155
+
156
+ export const SEPARATOR =
157
+ '==============================================================';