@docupace/pipeline 1.0.128 → 1.0.131

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.
@@ -0,0 +1,9 @@
1
+ # Historical audit trail only. Because the Secrets Scan step clones at
2
+ # depth: 1, gitleaks only ever sees the latest commit — these fingerprints
3
+ # (tied to specific historical commit SHAs) will never actually be re-scanned
4
+ # and do not suppress future findings. The real fix for these two was
5
+ # removing the credential from testCanny.sh; see that commit for details.
6
+ #
7
+ # Canny API key in testCanny.sh, revoked (curl confirmed: "invalid api key").
8
+ 2337b1f58f2e5fec0d71ca22305573505c84daee:testCanny.sh:generic-api-key:4
9
+ dfa078fd93eeb339d32ec4067a247195caef24cd:testCanny.sh:generic-api-key:4
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @docupace/pipeline
2
2
 
3
+ ## 1.0.130
4
+
5
+ ### Patch Changes
6
+
7
+ - cfa3fe9: Add a shared secrets-scan pipeline definition (importable as pipeline-shared-scripts:master:secrets-scan, also runnable manually via custom.secrets-scan in this repo) that runs Atlassian's git-secrets-scan pipe (gitleaks) with clone depth 1, so only the latest commit is scanned rather than full history
8
+ - cfa3fe9: Add dp-tenable-cli, dp-veracode-cli, and dp-xray-cli Node CLIs porting the Tenable scan, Veracode upload-and-scan, and JFrog Xray Slack-notify bash logic
9
+ - cfa3fe9: Fix setGitInfo to fall back to BITBUCKET_APP_PASSWORD (an API token, paired with the static x-bitbucket-api-token-auth username, matching getRepUrl in next/config.js) when BITBUCKET_STEP_OAUTH_TOKEN/BITBUCKET_TOKEN isn't set, fixing changeset validation failing to authenticate against Bitbucket on runners that rewrite the SSH origin to a credential-less HTTPS URL
10
+
3
11
  ## 1.0.123
4
12
 
5
13
  ### Patch Changes
@@ -119,8 +119,8 @@ scan_one() {
119
119
  "--code-commit-hash" "${COMMIT}"
120
120
  "--code-commit-user" "${CODE_USER}"
121
121
  "--code-repository-url" "${CODE_REPO_URL}"
122
- "--min-severity" "${MIN_SEVERITY}"
123
- "--output-file-formats" "Json,Md,JUnit"
122
+ "--min-severity" "$(printf '%s' "${MIN_SEVERITY}" | tr 'A-Z' 'a-z')"
123
+ "--output-file-formats" "json,md,junit"
124
124
  "--output-path" "/tmp/tenable/out"
125
125
  "--output-file-name" "${slug}-tenable"
126
126
  "--no-color"
@@ -134,15 +134,24 @@ scan_one() {
134
134
  )
135
135
  fi
136
136
  if [[ -n "${TENABLE_FAIL_ON_MIN_SEVERITY:-}" ]]; then
137
- scan_args+=("--fail-on-min-severity" "${TENABLE_FAIL_ON_MIN_SEVERITY}")
137
+ scan_args+=("--fail-on-min-severity" "$(printf '%s' "${TENABLE_FAIL_ON_MIN_SEVERITY}" | tr 'A-Z' 'a-z')")
138
138
  fi
139
139
 
140
140
  echo "Running Tenable scan for '${slug}' (min-severity=${MIN_SEVERITY}${TENABLE_FAIL_ON_MIN_SEVERITY:+, fail-on=${TENABLE_FAIL_ON_MIN_SEVERITY}})..."
141
+ # --entrypoint is not optional: Tenable's own GitHub action builds FROM this image
142
+ # solely to replace the entrypoint with a script that runs `tenable container-image
143
+ # scan "$@"`, so the image's own entrypoint is not that binary. Handing the
144
+ # subcommand to whatever it does start fails silently — exit 0, no output, no
145
+ # reports. The binary also takes its output destination from the environment (the
146
+ # action passes no --output-path flag at all), so it is set both ways.
141
147
  docker run --rm \
142
148
  --workdir /tmp/tenable \
143
149
  --volume "${work}:/tmp/tenable" \
144
150
  --env TENABLE_API_TOKEN \
145
151
  --env TENABLE_API_URL \
152
+ --env "TENABLE_OUTPUT_PATH=/tmp/tenable/out" \
153
+ --env "TENABLE_OUTPUT_FILE_NAME=${slug}-tenable" \
154
+ --entrypoint "${TENABLE_SCANNER_ENTRYPOINT:-tenable}" \
146
155
  --pull=always \
147
156
  "${SCANNER_IMAGE}" "${scan_args[@]}"
148
157
  local scan_exit=$?
@@ -453,6 +453,69 @@ definitions:
453
453
  [ "${failed}" -ne 0 ] && exit 1
454
454
  exit 0
455
455
 
456
+ # Scans only the latest commit(s) for hardcoded secrets using Atlassian's
457
+ # git-secrets-scan pipe (gitleaks under the hood):
458
+ # import: pipeline-shared-scripts:master:secrets-scan
459
+ #
460
+ # No `runs-on` here deliberately: Bitbucket's Pipes launcher requires a
461
+ # runner in Docker execution mode, which this workspace's on-prem
462
+ # self-hosted runners don't provide (they're shell-mode only) — omitting
463
+ # `runs-on` falls back to Bitbucket's own cloud runners, which do support
464
+ # Pipes.
465
+ #
466
+ # Per https://support.atlassian.com/bitbucket-cloud/kb/how-to-scan-only-the-latest-commits-for-secrets-using-the-git-secrets-scan-pipe/
467
+ # the pipe itself has no "latest commit only" flag — gitleaks' default `git`
468
+ # mode scans the whole history it can see. The scope is controlled entirely
469
+ # by the step's own `clone: depth`, which is why this step overrides the
470
+ # repo-wide default (depth: 2) down to depth: 1: with a depth-1 clone there
471
+ # is only one commit for gitleaks to see, so it scans just that one. This
472
+ # also means the step never manages BITBUCKET_PREVIOUS_COMMIT/HEAD^ ranges
473
+ # itself — a deeper clone here would silently widen the scan.
474
+ #
475
+ # Optional: SECRETS_SCAN_EXTRA_ARGS (extra gitleaks flags, e.g. a custom
476
+ # --gitleaks-ignore-path or --config — appended after the default
477
+ # --redact below, so a repo can add flags without losing redaction; do
478
+ # NOT pass --exit-code 0, that would make the pipe always exit 0 and
479
+ # silently defeat the registry-push gate that depends on this step
480
+ # failing on a real find), SECRETS_SCAN_CREATE_REPORT (default "true" —
481
+ # set "false" to skip the Code Insights report), SECRETS_SCAN_DEBUG
482
+ # (default "false").
483
+ secrets-scan: &def-secrets-scan
484
+ - step:
485
+ name: Secrets Scan (latest commit)
486
+ clone:
487
+ depth: 1
488
+ script:
489
+ # bitbucket-pipelines.yml variable substitution only does a plain
490
+ # ${VAR} lookup (empty string if unset) — it doesn't support shell
491
+ # ${VAR:-default} fallback syntax, so defaults are applied here
492
+ # instead, before the pipe reads them.
493
+ #
494
+ # --redact is on by default: gitleaks-report.json below is kept
495
+ # as a build artifact for triage, and without --redact its
496
+ # `Secret` field would hold the actual matched credential in
497
+ # plain text — turning a transient leak into a persistent,
498
+ # downloadable copy of it for anyone with repo read access.
499
+ - |
500
+ export SECRETS_SCAN_CREATE_REPORT="${SECRETS_SCAN_CREATE_REPORT:-true}"
501
+ export SECRETS_SCAN_DEBUG="${SECRETS_SCAN_DEBUG:-false}"
502
+ export SECRETS_SCAN_EXTRA_ARGS="--redact ${SECRETS_SCAN_EXTRA_ARGS}"
503
+ - pipe: atlassian/git-secrets-scan:3.2.0
504
+ variables:
505
+ GITLEAKS_EXTRA_ARGS: ${SECRETS_SCAN_EXTRA_ARGS}
506
+ CREATE_REPORT: ${SECRETS_SCAN_CREATE_REPORT}
507
+ DEBUG: ${SECRETS_SCAN_DEBUG}
508
+ # Gitleaks writes this report before the pipe attempts its Bitbucket
509
+ # Code Insights upload, so it's captured here even if that upload
510
+ # fails (a known bug in the pipe: it can crash on a 400 from
511
+ # Bitbucket's annotations API for certain file paths) or the step
512
+ # otherwise fails on a real finding. `artifacts:` is collected
513
+ # regardless of step exit code — this is how to see which secrets
514
+ # were found (file, line, rule, fingerprint) with the matched
515
+ # secret value itself redacted by --redact above.
516
+ artifacts:
517
+ - gitleaks-report.json
518
+
456
519
  # Runs a SonarQube analysis against the current branch, fire-and-forget
457
520
  # (no quality-gate enforcement): import: pipeline-shared-scripts:master:sonar-scan
458
521
  #
@@ -725,11 +788,39 @@ pipelines:
725
788
  xray-scan: *def-xray-scan
726
789
  tenable-scan: *def-tenable-scan
727
790
  veracode-scan: *def-veracode-scan
791
+ secrets-scan: *def-secrets-scan
728
792
  sonar-scan: *def-sonar-scan
729
793
  sonar-pr-gate: *def-sonar-pr-gate
730
794
 
731
795
  pull-requests:
732
796
  '**':
797
+ # Inlined rather than aliased to *def-secrets-scan: that anchor is bound
798
+ # to a whole pipeline (a list with one step), and YAML's merge key only
799
+ # splices mappings, not list items, so it can't be spliced into this
800
+ # list. Keep this step's content in sync with the secrets-scan
801
+ # definition above if it changes.
802
+ - step:
803
+ name: Secrets Scan (latest commit)
804
+ # No `runs-on` deliberately — see the secrets-scan definition above
805
+ # for why (Pipes need a Docker-mode runner, which the on-prem
806
+ # runners here don't provide; omitting runs-on falls back to
807
+ # Bitbucket's cloud runners).
808
+ clone:
809
+ depth: 1
810
+ script:
811
+ - |
812
+ export SECRETS_SCAN_CREATE_REPORT="${SECRETS_SCAN_CREATE_REPORT:-true}"
813
+ export SECRETS_SCAN_DEBUG="${SECRETS_SCAN_DEBUG:-false}"
814
+ export SECRETS_SCAN_EXTRA_ARGS="--redact ${SECRETS_SCAN_EXTRA_ARGS}"
815
+ - pipe: atlassian/git-secrets-scan:3.2.0
816
+ variables:
817
+ GITLEAKS_EXTRA_ARGS: ${SECRETS_SCAN_EXTRA_ARGS}
818
+ CREATE_REPORT: ${SECRETS_SCAN_CREATE_REPORT}
819
+ DEBUG: ${SECRETS_SCAN_DEBUG}
820
+ # See the secrets-scan definition above for why this is captured
821
+ # as an artifact regardless of the step's exit code.
822
+ artifacts:
823
+ - gitleaks-report.json
733
824
  - step:
734
825
  runs-on:
735
826
  - self.hosted
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@docupace/pipeline",
3
- "version": "1.0.128",
3
+ "version": "1.0.131",
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"
@@ -4,11 +4,14 @@ import * as path from 'node:path'; // ESM-compatible import
4
4
  import { cloneRepository, pushDeployConfig } from '../next/deploy.js';
5
5
  import { getAuth0AuthToken } from './configure.js';
6
6
  import { getVaultSecrets } from './vault.js';
7
- import { getVaultSecretsConfigs } from '../next/config.js';
7
+ import {
8
+ connectDeployRepoName,
9
+ getVaultSecretsConfigs,
10
+ } from '../next/config.js';
8
11
 
9
12
  export async function getAuth0Metadata(site, env, auth0Tenant, jiraTicket) {
10
13
  try {
11
- const connectDeployRepoPath = await cloneRepository('connect-deploy');
14
+ const connectDeployRepoPath = await cloneRepository(connectDeployRepoName);
12
15
 
13
16
  const envMap = {
14
17
  dev: 'dev',
@@ -34,6 +34,7 @@ jest.unstable_mockModule('./vault.js', () => ({
34
34
 
35
35
  jest.unstable_mockModule('../next/config.js', () => ({
36
36
  getVaultSecretsConfigs: jest.fn(),
37
+ connectDeployRepoName: 'dp-eks-connect-ui-gitops',
37
38
  }));
38
39
 
39
40
  // ----- Imports AFTER mocks -----
@@ -43,7 +44,9 @@ const axios = (await import('axios')).default;
43
44
  const { cloneRepository, pushDeployConfig } = await import('../next/deploy.js');
44
45
  const { getAuth0AuthToken } = await import('./configure.js');
45
46
  const { getVaultSecrets } = await import('./vault.js');
46
- const { getVaultSecretsConfigs } = await import('../next/config.js');
47
+ const { getVaultSecretsConfigs, connectDeployRepoName } = await import(
48
+ '../next/config.js'
49
+ );
47
50
 
48
51
  // ----- Shared Setup -----
49
52
  beforeAll(() => {
@@ -113,7 +116,7 @@ describe('getAuth0Metadata', () => {
113
116
 
114
117
  await getAuth0Metadata(site, env, auth0Tenant);
115
118
 
116
- expect(cloneRepository).toHaveBeenCalledWith('connect-deploy');
119
+ expect(cloneRepository).toHaveBeenCalledWith(connectDeployRepoName);
117
120
  expect(fs.readFile).toHaveBeenCalledWith('conf/application.conf', 'utf-8');
118
121
  expect(fs.ensureDir).toHaveBeenCalledWith('conf/saml');
119
122
  expect(fs.writeFile).toHaveBeenCalledWith(
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { $ } from 'zx';
4
+ import path from 'path';
4
5
  import { validateChangesets } from './validate.js';
5
6
  import dotenv from 'dotenv';
6
7
  import { isMonoRepo, setGitInfo } from '../next/util.js';
@@ -19,6 +20,26 @@ const program = new Command();
19
20
 
20
21
  const args = process.argv;
21
22
 
23
+ async function runPostVersionScript(scriptPath, releases) {
24
+ if (!scriptPath || !releases?.length) {
25
+ return;
26
+ }
27
+
28
+ const resolvedScriptPath = path.resolve(process.cwd(), scriptPath);
29
+ console.log(`📌 Running post-version script: ${resolvedScriptPath}`);
30
+
31
+ if (isMonoRepo()) {
32
+ for (const release of getAppReleases(releases)) {
33
+ await $`node ${resolvedScriptPath} ${release.newVersion}`;
34
+ }
35
+ } else {
36
+ const newVersion = releases[0]?.newVersion;
37
+ if (newVersion) {
38
+ await $`node ${resolvedScriptPath} ${newVersion}`;
39
+ }
40
+ }
41
+ }
42
+
22
43
  async function commitTagCommand(releases) {
23
44
  console.log('📌 Checking for updates to commit...');
24
45
 
@@ -157,12 +178,17 @@ program
157
178
  '--install-latest',
158
179
  'Install the latest version of @docupace/pipeline instead of the current version',
159
180
  )
181
+ .option(
182
+ '--post-version-script <path>',
183
+ 'Path to a JS file to run after the version is bumped but before it is committed, tagged, and pushed. Called with the new version as its first argument.',
184
+ )
160
185
  .action(async (options) => {
161
186
  console.log('📦 Running all steps...');
162
187
  try {
163
188
  const releases = await versionCommand(false, options.installLatest);
164
189
  await updateJiraWithVersion(releases);
165
190
  await updateWikiWithVersion(releases);
191
+ await runPostVersionScript(options.postVersionScript, releases);
166
192
  await commitTagCommand(releases);
167
193
  } catch (error) {
168
194
  console.error('❌ Version-commit-tag failed:', error);
@@ -1,14 +1,32 @@
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(
10
+ path.dirname(fileURLToPath(import.meta.url)),
11
+ '..',
12
+ '..',
13
+ );
14
+ const PACKAGE_VERSION = JSON.parse(
15
+ fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'),
16
+ ).version;
17
+
8
18
  export async function validateChangesets() {
19
+ console.log(
20
+ `validateChangesets: @docupace/pipeline version = ${PACKAGE_VERSION}`,
21
+ );
9
22
  await setGitInfo();
10
23
  const destBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
24
+ console.log(`validateChangesets: destBranch = ${destBranch}`);
25
+ console.log(
26
+ `validateChangesets: fetching origin ${destBranch}:${destBranch}`,
27
+ );
11
28
  await $`git fetch origin ${destBranch}:${destBranch}`;
29
+ console.log('validateChangesets: fetch succeeded');
12
30
  let releases;
13
31
 
14
32
  try {
@@ -144,7 +144,7 @@ export const getBitbucketInfo = () => {
144
144
  };
145
145
 
146
146
  export const getRepUrl = (repoName) => {
147
- const { username, password, workspace } = getBitbucketInfo();
147
+ const { password, workspace } = getBitbucketInfo();
148
148
  return `https://x-bitbucket-api-token-auth:${password}@bitbucket.org/${workspace}/${repoName}.git`;
149
149
  };
150
150
 
@@ -172,3 +172,5 @@ export const coreSiteEnvMap = {
172
172
  };
173
173
 
174
174
  export const autoDeployConfigName = 'auto-deploy.yaml';
175
+
176
+ export const connectDeployRepoName = 'dp-eks-connect-ui-gitops';
@@ -3,6 +3,7 @@ import fs from 'fs-extra';
3
3
  import path from 'path';
4
4
  import { $ } from 'zx';
5
5
  import { getVaultSecrets } from '../auth0/vault.js';
6
+ import { connectDeployRepoName } from './config.js';
6
7
 
7
8
  process.env.COMMON_VAULT_PATH =
8
9
  process.env.COMMON_VAULT_PATH || 'secret/data/common/config';
@@ -109,9 +110,9 @@ export async function connectClone() {
109
110
  process.env.AUTH0_DOMAIN = `docupace-${idpEnvMap[process.env.BUILD_ENVIRONMENT]}-${process.env.BUILD_SITE}.${process.env.AUTH0_REGION_DOMAIN}`;
110
111
 
111
112
  const projectRoot = process.cwd();
112
- // 4) Clone 'connect-deploy' into CONNECT_DIR
113
+ // 4) Clone connect deploy repo into CONNECT_DIR
113
114
 
114
- await gitCheckout('connect-deploy', process.env.CONNECT_DIR);
115
+ await gitCheckout(connectDeployRepoName, process.env.CONNECT_DIR);
115
116
 
116
117
  await gitCheckout(
117
118
  `${siteSettingsEnvMap[connectToEnv]}-site-settings`,
@@ -7,6 +7,7 @@ import semver from 'semver';
7
7
  import { $ } from 'zx';
8
8
  import {
9
9
  appInfo,
10
+ connectDeployRepoName,
10
11
  environmentConfig,
11
12
  getDefaultAppInfo,
12
13
  getRepUrl,
@@ -1188,7 +1189,7 @@ export async function deployFromSite(
1188
1189
  );
1189
1190
  if (getComparedVersion) {
1190
1191
  console.log('Version to deploy is: ', getComparedVersion);
1191
- const connectDeployPath = await cloneRepository('connect-deploy');
1192
+ const connectDeployPath = await cloneRepository(connectDeployRepoName);
1192
1193
  // update target version set to git-commit-${getCommparedVersion}}
1193
1194
  await updateAndCommitConnectVersion(
1194
1195
  toEnv,
@@ -54,6 +54,7 @@ jest.unstable_mockModule('./config.js', () => ({
54
54
  deployAttributes: ['image.tag'],
55
55
  })),
56
56
  autoDeployConfigName: 'auto-deploy.yaml',
57
+ connectDeployRepoName: 'dp-eks-connect-ui-gitops',
57
58
  }));
58
59
 
59
60
  jest.unstable_mockModule('./copy.js', () => ({
package/src/next/util.js CHANGED
@@ -84,12 +84,54 @@ 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(
93
+ `setGitInfo: SET_GITOPS_USER=${process.env.SET_GITOPS_USER || '(unset)'}`,
94
+ );
88
95
  if (process.env.SET_GITOPS_USER) {
96
+ const before = (await $`git remote get-url origin`).stdout.trim();
97
+ console.log(`setGitInfo: origin before = ${maskCreds(before)}`);
98
+
89
99
  await $`git config --global user.email "gitops@docupace.com"`;
90
100
  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}`;
101
+
102
+ // Some self-hosted runners rewrite `git@bitbucket.org:` (SSH) to a bare,
103
+ // credential-less `https://bitbucket.org/` via a git `insteadOf` rule, so an
104
+ // SSH-form origin silently loses its identity at fetch/push time. Use an
105
+ // HTTPS URL with the token embedded instead — insteadOf doesn't touch it.
106
+ // Not every step has BITBUCKET_STEP_OAUTH_TOKEN (it depends on step/repo
107
+ // OAuth config), so fall back to BITBUCKET_APP_PASSWORD (an API token,
108
+ // despite the name — app passwords are deprecated) paired with the
109
+ // static x-bitbucket-api-token-auth username, matching getRepUrl() in
110
+ // ../next/config.js.
111
+ const token =
112
+ process.env.BITBUCKET_STEP_OAUTH_TOKEN || process.env.BITBUCKET_TOKEN;
113
+ const apiToken = process.env.BITBUCKET_APP_PASSWORD;
114
+ let repoUrl;
115
+ if (token) {
116
+ repoUrl = `https://x-token-auth:${token}@bitbucket.org/dp-cloud/${process.env.BITBUCKET_REPO_SLUG}.git`;
117
+ } else if (apiToken) {
118
+ repoUrl = `https://x-bitbucket-api-token-auth:${apiToken}@bitbucket.org/dp-cloud/${process.env.BITBUCKET_REPO_SLUG}.git`;
119
+ }
120
+ if (!repoUrl) {
121
+ console.log(
122
+ 'setGitInfo: no BITBUCKET_STEP_OAUTH_TOKEN/BITBUCKET_TOKEN or BITBUCKET_APP_PASSWORD available, leaving origin as-is',
123
+ );
124
+ } else {
125
+ console.log(`Setting new origin: ${maskCreds(repoUrl)}`);
126
+ await $`git remote set-url origin ${repoUrl}`;
127
+ }
128
+
129
+ const after = (await $`git remote get-url origin`).stdout.trim();
130
+ console.log(`setGitInfo: origin after = ${maskCreds(after)}`);
131
+ } else {
132
+ const current = (await $`git remote get-url origin`).stdout.trim();
133
+ console.log(
134
+ `setGitInfo: SET_GITOPS_USER not set, leaving origin as-is = ${maskCreds(current)}`,
135
+ );
94
136
  }
95
137
  }
@@ -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
+ '==============================================================';