@docupace/pipeline 1.0.123-xray.2 → 1.0.123
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/bin/tenable-scan.sh +3 -12
- package/package.json +2 -5
- package/src/changeset/validate.js +0 -10
- package/src/next/util.js +3 -32
- package/.gitignore +0 -103
- package/pnpm-workspace.yaml +0 -3
- package/src/tenable/args.js +0 -157
- package/src/tenable/args.test.js +0 -226
- package/src/tenable/cli.js +0 -24
- package/src/tenable/docker.js +0 -45
- package/src/tenable/fsops.js +0 -15
- package/src/tenable/scan.js +0 -284
- package/src/tenable/scan.test.js +0 -355
- package/src/veracode/args.js +0 -67
- package/src/veracode/args.test.js +0 -137
- package/src/veracode/cli.js +0 -16
- package/src/veracode/fsops.js +0 -7
- package/src/veracode/scan.js +0 -120
- package/src/veracode/scan.test.js +0 -246
- package/src/veracode/veracode.js +0 -33
- package/src/xray/cli.js +0 -37
- package/src/xray/notify.js +0 -107
- package/src/xray/notify.test.js +0 -161
- package/src/xray/report.js +0 -194
- package/src/xray/report.test.js +0 -278
- package/src/xray/slack.js +0 -14
package/bin/tenable-scan.sh
CHANGED
|
@@ -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" "$
|
|
123
|
-
"--output-file-formats" "
|
|
122
|
+
"--min-severity" "${MIN_SEVERITY}"
|
|
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,24 +134,15 @@ scan_one() {
|
|
|
134
134
|
)
|
|
135
135
|
fi
|
|
136
136
|
if [[ -n "${TENABLE_FAIL_ON_MIN_SEVERITY:-}" ]]; then
|
|
137
|
-
scan_args+=("--fail-on-min-severity" "$
|
|
137
|
+
scan_args+=("--fail-on-min-severity" "${TENABLE_FAIL_ON_MIN_SEVERITY}")
|
|
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.
|
|
147
141
|
docker run --rm \
|
|
148
142
|
--workdir /tmp/tenable \
|
|
149
143
|
--volume "${work}:/tmp/tenable" \
|
|
150
144
|
--env TENABLE_API_TOKEN \
|
|
151
145
|
--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}" \
|
|
155
146
|
--pull=always \
|
|
156
147
|
"${SCANNER_IMAGE}" "${scan_args[@]}"
|
|
157
148
|
local scan_exit=$?
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docupace/pipeline",
|
|
3
|
-
"version": "1.0.123
|
|
3
|
+
"version": "1.0.123",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -12,10 +12,7 @@
|
|
|
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"
|
|
16
|
-
"dp-xray-cli": "src/xray/cli.js",
|
|
17
|
-
"dp-tenable-cli": "src/tenable/cli.js",
|
|
18
|
-
"dp-veracode-cli": "src/veracode/cli.js"
|
|
15
|
+
"dp-micro-cli": "src/micro/cli.js"
|
|
19
16
|
},
|
|
20
17
|
"keywords": [
|
|
21
18
|
"bash"
|
|
@@ -1,24 +1,14 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
3
|
import { $ } from 'zx';
|
|
5
4
|
|
|
6
5
|
import { setGitInfo } from '../next/util.js';
|
|
7
6
|
import { versionStatus } from './version.js';
|
|
8
7
|
|
|
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
|
-
|
|
14
8
|
export async function validateChangesets() {
|
|
15
|
-
console.log(`validateChangesets: @docupace/pipeline version = ${PACKAGE_VERSION}`);
|
|
16
9
|
await setGitInfo();
|
|
17
10
|
const destBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH;
|
|
18
|
-
console.log(`validateChangesets: destBranch = ${destBranch}`);
|
|
19
|
-
console.log(`validateChangesets: fetching origin ${destBranch}:${destBranch}`);
|
|
20
11
|
await $`git fetch origin ${destBranch}:${destBranch}`;
|
|
21
|
-
console.log('validateChangesets: fetch succeeded');
|
|
22
12
|
let releases;
|
|
23
13
|
|
|
24
14
|
try {
|
package/src/next/util.js
CHANGED
|
@@ -84,41 +84,12 @@ export async function getChangedAppPackages() {
|
|
|
84
84
|
return appPackages;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
function maskCreds(url) {
|
|
88
|
-
return url.replace(/:[^:@/]+@/, ':***@');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
87
|
export async function setGitInfo() {
|
|
92
|
-
console.log(`setGitInfo: SET_GITOPS_USER=${process.env.SET_GITOPS_USER || '(unset)'}`);
|
|
93
88
|
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
|
-
|
|
97
89
|
await $`git config --global user.email "gitops@docupace.com"`;
|
|
98
90
|
await $`git config --global user.name "Gitops"`;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
);
|
|
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}`;
|
|
123
94
|
}
|
|
124
95
|
}
|
package/.gitignore
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
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/pnpm-workspace.yaml
DELETED
package/src/tenable/args.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
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
|
-
'==============================================================';
|
package/src/tenable/args.test.js
DELETED
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from '@jest/globals';
|
|
2
|
-
import {
|
|
3
|
-
slugify,
|
|
4
|
-
parseScanTargets,
|
|
5
|
-
defaultScanTargets,
|
|
6
|
-
buildCodeMetadata,
|
|
7
|
-
buildPipelineRunInfo,
|
|
8
|
-
buildScanArgs,
|
|
9
|
-
} from './args.js';
|
|
10
|
-
|
|
11
|
-
describe('slugify', () => {
|
|
12
|
-
test('replaces every non [A-Za-z0-9._-] character with a dash', () => {
|
|
13
|
-
expect(slugify('my app!')).toBe('my-app-');
|
|
14
|
-
expect(slugify('my/app')).toBe('my-app');
|
|
15
|
-
expect(slugify('already-safe_v1.2')).toBe('already-safe_v1.2');
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
describe('parseScanTargets', () => {
|
|
20
|
-
test('parses label:auditDir:context:dockerfile entries, ignoring auditDir', () => {
|
|
21
|
-
const targets = parseScanTargets(
|
|
22
|
-
'web:deps:services/web:services/web/Dockerfile.prod;worker::services/worker:',
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
expect(targets).toEqual([
|
|
26
|
-
{
|
|
27
|
-
label: 'web',
|
|
28
|
-
slug: 'web',
|
|
29
|
-
buildContext: 'services/web',
|
|
30
|
-
dockerfile: 'services/web/Dockerfile.prod',
|
|
31
|
-
skipped: false,
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
label: 'worker',
|
|
35
|
-
slug: 'worker',
|
|
36
|
-
buildContext: 'services/worker',
|
|
37
|
-
dockerfile: 'services/worker/Dockerfile',
|
|
38
|
-
skipped: false,
|
|
39
|
-
},
|
|
40
|
-
]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('defaults an empty label to "app" and slugifies it', () => {
|
|
44
|
-
const [target] = parseScanTargets(':deps:ctx:Dockerfile');
|
|
45
|
-
expect(target.label).toBe('app');
|
|
46
|
-
expect(target.slug).toBe('app');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('marks a target with no build context as skipped', () => {
|
|
50
|
-
const [target] = parseScanTargets('web:deps::Dockerfile');
|
|
51
|
-
expect(target).toEqual({
|
|
52
|
-
label: 'web',
|
|
53
|
-
slug: 'web',
|
|
54
|
-
buildContext: '',
|
|
55
|
-
dockerfile: '',
|
|
56
|
-
skipped: true,
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
test('ignores blank entries between semicolons', () => {
|
|
61
|
-
expect(parseScanTargets('web:deps:ctx:Dockerfile;;')).toHaveLength(1);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('empty string produces no targets', () => {
|
|
65
|
-
expect(parseScanTargets('')).toEqual([]);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('defaultScanTargets', () => {
|
|
70
|
-
test('builds a single app target from context + dockerfile', () => {
|
|
71
|
-
expect(defaultScanTargets('.', './Dockerfile')).toBe('app::.:./Dockerfile');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('defaults the dockerfile path to <context>/Dockerfile when unset', () => {
|
|
75
|
-
expect(defaultScanTargets('services/web', '')).toBe(
|
|
76
|
-
'app::services/web:services/web/Dockerfile',
|
|
77
|
-
);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('defaults the context to "." when unset', () => {
|
|
81
|
-
expect(defaultScanTargets('', '')).toBe('app::.:./Dockerfile');
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('buildCodeMetadata', () => {
|
|
86
|
-
test('derives a 7-char short SHA and falls back branch/user', () => {
|
|
87
|
-
const meta = buildCodeMetadata({
|
|
88
|
-
commit: 'abcdef1234567',
|
|
89
|
-
branch: '',
|
|
90
|
-
prDestinationBranch: 'main',
|
|
91
|
-
workspace: 'dp-cloud',
|
|
92
|
-
repoSlug: 'my-repo',
|
|
93
|
-
gitAuthorName: '',
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
expect(meta).toEqual({
|
|
97
|
-
shortSha: 'abcdef1',
|
|
98
|
-
codeBranch: 'main',
|
|
99
|
-
codeRepoUrl: 'https://bitbucket.org/dp-cloud/my-repo',
|
|
100
|
-
codeUser: 'bitbucket-pipelines',
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
test('prefers branch over PR destination branch, and a real author name', () => {
|
|
105
|
-
const meta = buildCodeMetadata({
|
|
106
|
-
commit: 'abcdef1234567',
|
|
107
|
-
branch: 'feature/x',
|
|
108
|
-
prDestinationBranch: 'main',
|
|
109
|
-
gitAuthorName: 'Matt Fields',
|
|
110
|
-
});
|
|
111
|
-
expect(meta.codeBranch).toBe('feature/x');
|
|
112
|
-
expect(meta.codeUser).toBe('Matt Fields');
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe('buildPipelineRunInfo', () => {
|
|
117
|
-
test('defaults trigger to push and builds a run URL from the build number', () => {
|
|
118
|
-
const info = buildPipelineRunInfo({
|
|
119
|
-
buildNumber: '42',
|
|
120
|
-
workspace: 'dp-cloud',
|
|
121
|
-
repoSlug: 'my-repo',
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
expect(info).toEqual({
|
|
125
|
-
runId: '42',
|
|
126
|
-
runTrigger: 'push',
|
|
127
|
-
runUrl: 'https://bitbucket.org/dp-cloud/my-repo/pipelines/results/42',
|
|
128
|
-
includeInScanArgs: true,
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('defaults trigger to pull_request when a PR id is present', () => {
|
|
133
|
-
const info = buildPipelineRunInfo({ buildNumber: '42', prId: '7' });
|
|
134
|
-
expect(info.runTrigger).toBe('pull_request');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test('explicit TENABLE_PIPELINE_RUN_* values win over the Bitbucket-derived ones', () => {
|
|
138
|
-
const info = buildPipelineRunInfo({
|
|
139
|
-
runId: 'custom-1',
|
|
140
|
-
runTrigger: 'manual',
|
|
141
|
-
runUrl: 'https://example.com/run/1',
|
|
142
|
-
buildNumber: '42',
|
|
143
|
-
});
|
|
144
|
-
expect(info).toEqual({
|
|
145
|
-
runId: 'custom-1',
|
|
146
|
-
runTrigger: 'manual',
|
|
147
|
-
runUrl: 'https://example.com/run/1',
|
|
148
|
-
includeInScanArgs: true,
|
|
149
|
-
});
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test('does not include the trio in scan args when no build number/id is available', () => {
|
|
153
|
-
const info = buildPipelineRunInfo({});
|
|
154
|
-
expect(info.includeInScanArgs).toBe(false);
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe('buildScanArgs', () => {
|
|
159
|
-
const base = {
|
|
160
|
-
slug: 'web',
|
|
161
|
-
scanTarget: 'my-repo-web:tenable-abc1234',
|
|
162
|
-
codeBranch: 'main',
|
|
163
|
-
commit: 'abcdef1234567',
|
|
164
|
-
codeUser: 'Matt Fields',
|
|
165
|
-
codeRepoUrl: 'https://bitbucket.org/dp-cloud/my-repo',
|
|
166
|
-
minSeverity: 'Medium',
|
|
167
|
-
runInfo: { includeInScanArgs: false },
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
test('builds the base scanner CLI args', () => {
|
|
171
|
-
expect(buildScanArgs(base)).toEqual([
|
|
172
|
-
'container-image',
|
|
173
|
-
'scan',
|
|
174
|
-
'--archive-path',
|
|
175
|
-
'/tmp/tenable/image.tar',
|
|
176
|
-
'--name',
|
|
177
|
-
'my-repo-web:tenable-abc1234',
|
|
178
|
-
'--code-branch',
|
|
179
|
-
'main',
|
|
180
|
-
'--code-commit-hash',
|
|
181
|
-
'abcdef1234567',
|
|
182
|
-
'--code-commit-user',
|
|
183
|
-
'Matt Fields',
|
|
184
|
-
'--code-repository-url',
|
|
185
|
-
'https://bitbucket.org/dp-cloud/my-repo',
|
|
186
|
-
'--min-severity',
|
|
187
|
-
'medium',
|
|
188
|
-
'--output-file-formats',
|
|
189
|
-
'json,md,junit',
|
|
190
|
-
'--output-path',
|
|
191
|
-
'/tmp/tenable/out',
|
|
192
|
-
'--output-file-name',
|
|
193
|
-
'web-tenable',
|
|
194
|
-
'--no-color',
|
|
195
|
-
]);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test('appends the pipeline-run trio only when includeInScanArgs is true', () => {
|
|
199
|
-
const args = buildScanArgs({
|
|
200
|
-
...base,
|
|
201
|
-
runInfo: {
|
|
202
|
-
includeInScanArgs: true,
|
|
203
|
-
runId: '42',
|
|
204
|
-
runTrigger: 'push',
|
|
205
|
-
runUrl: 'https://bitbucket.org/dp-cloud/my-repo/pipelines/results/42',
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
expect(args).toEqual(
|
|
209
|
-
expect.arrayContaining([
|
|
210
|
-
'--pipeline-run-id',
|
|
211
|
-
'42',
|
|
212
|
-
'--pipeline-run-trigger',
|
|
213
|
-
'push',
|
|
214
|
-
'--pipeline-run-url',
|
|
215
|
-
'https://bitbucket.org/dp-cloud/my-repo/pipelines/results/42',
|
|
216
|
-
]),
|
|
217
|
-
);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('appends --fail-on-min-severity only when set', () => {
|
|
221
|
-
expect(buildScanArgs(base)).not.toContain('--fail-on-min-severity');
|
|
222
|
-
expect(buildScanArgs({ ...base, failOnMinSeverity: 'High' })).toEqual(
|
|
223
|
-
expect.arrayContaining(['--fail-on-min-severity', 'high']),
|
|
224
|
-
);
|
|
225
|
-
});
|
|
226
|
-
});
|
package/src/tenable/cli.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import { $ } from 'zx';
|
|
5
|
-
import { runTenableScan } from './scan.js';
|
|
6
|
-
|
|
7
|
-
const program = new Command();
|
|
8
|
-
|
|
9
|
-
program
|
|
10
|
-
.command('scan')
|
|
11
|
-
.description('Run the Tenable Cloud Security container image scan')
|
|
12
|
-
.action(async () => {
|
|
13
|
-
let gitAuthorName = '';
|
|
14
|
-
const result = await $({
|
|
15
|
-
nothrow: true,
|
|
16
|
-
quiet: true,
|
|
17
|
-
})`git log -1 --pretty=format:%an`;
|
|
18
|
-
if (result.exitCode === 0) gitAuthorName = result.stdout.trim();
|
|
19
|
-
|
|
20
|
-
const { exitCode } = await runTenableScan({ gitAuthorName });
|
|
21
|
-
process.exit(exitCode);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
program.parse(process.argv);
|
package/src/tenable/docker.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { $ } from 'zx';
|
|
2
|
-
|
|
3
|
-
// Thin wrappers around the docker CLI so scan.js can inject a fake in tests.
|
|
4
|
-
// Every call uses nothrow: a non-zero exit is meaningful data here (a failed
|
|
5
|
-
// build, or the scanner itself reporting findings), not an exceptional case.
|
|
6
|
-
export const docker = {
|
|
7
|
-
async buildxAvailable() {
|
|
8
|
-
const result = await $({
|
|
9
|
-
nothrow: true,
|
|
10
|
-
quiet: true,
|
|
11
|
-
})`docker buildx version`;
|
|
12
|
-
return result.exitCode === 0;
|
|
13
|
-
},
|
|
14
|
-
|
|
15
|
-
async build(args) {
|
|
16
|
-
const result = await $({ nothrow: true })`docker build ${args}`;
|
|
17
|
-
return { ok: result.exitCode === 0 };
|
|
18
|
-
},
|
|
19
|
-
|
|
20
|
-
async save(image, tarPath) {
|
|
21
|
-
const result = await $({
|
|
22
|
-
nothrow: true,
|
|
23
|
-
})`docker save ${image} -o ${tarPath}`;
|
|
24
|
-
return { ok: result.exitCode === 0 };
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
// The entrypoint is overridden rather than trusted. Tenable's own GitHub action
|
|
28
|
-
// builds FROM this image purely to replace the entrypoint with a script whose
|
|
29
|
-
// first line is `tenable container-image scan "$@"`, which means the base image's
|
|
30
|
-
// own entrypoint is not that binary. Passing the subcommand as bare arguments
|
|
31
|
-
// hands them to whatever the image does start, and the failure is silent: exit 0,
|
|
32
|
-
// no output, no reports.
|
|
33
|
-
async run({ image, args, volume, workdir, env, entrypoint }) {
|
|
34
|
-
const envFlags = env.flatMap((name) => ['--env', name]);
|
|
35
|
-
const entrypointFlags = entrypoint ? ['--entrypoint', entrypoint] : [];
|
|
36
|
-
const result = await $({
|
|
37
|
-
nothrow: true,
|
|
38
|
-
})`docker run --rm --workdir ${workdir} --volume ${volume} ${envFlags} ${entrypointFlags} --pull=always ${image} ${args}`;
|
|
39
|
-
return result.exitCode;
|
|
40
|
-
},
|
|
41
|
-
|
|
42
|
-
async rmi(image) {
|
|
43
|
-
await $({ nothrow: true, quiet: true })`docker rmi -f ${image}`;
|
|
44
|
-
},
|
|
45
|
-
};
|