@docupace/pipeline 1.0.123-xray.1 → 1.0.123-xray.3
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 +12 -3
- package/package.json +1 -1
- package/src/tenable/docker.js +9 -2
- package/src/tenable/scan.js +4 -0
- package/src/tenable/scan.test.js +22 -0
- package/src/veracode/scan.js +31 -4
- package/src/veracode/scan.test.js +50 -3
- package/src/veracode/veracode.js +12 -0
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" "${MIN_SEVERITY}"
|
|
123
|
-
"--output-file-formats" "
|
|
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=$?
|
package/package.json
CHANGED
package/src/tenable/docker.js
CHANGED
|
@@ -24,11 +24,18 @@ export const docker = {
|
|
|
24
24
|
return { ok: result.exitCode === 0 };
|
|
25
25
|
},
|
|
26
26
|
|
|
27
|
-
|
|
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 }) {
|
|
28
34
|
const envFlags = env.flatMap((name) => ['--env', name]);
|
|
35
|
+
const entrypointFlags = entrypoint ? ['--entrypoint', entrypoint] : [];
|
|
29
36
|
const result = await $({
|
|
30
37
|
nothrow: true,
|
|
31
|
-
})`docker run --rm --workdir ${workdir} --volume ${volume} ${envFlags} --pull=always ${image} ${args}`;
|
|
38
|
+
})`docker run --rm --workdir ${workdir} --volume ${volume} ${envFlags} ${entrypointFlags} --pull=always ${image} ${args}`;
|
|
32
39
|
return result.exitCode;
|
|
33
40
|
},
|
|
34
41
|
|
package/src/tenable/scan.js
CHANGED
|
@@ -16,6 +16,7 @@ export async function scanOne(target, ctx) {
|
|
|
16
16
|
const {
|
|
17
17
|
reportDir,
|
|
18
18
|
scannerImage,
|
|
19
|
+
scannerEntrypoint,
|
|
19
20
|
shortSha,
|
|
20
21
|
imageNamePrefix,
|
|
21
22
|
codeBranch,
|
|
@@ -97,6 +98,7 @@ export async function scanOne(target, ctx) {
|
|
|
97
98
|
args: scanArgs,
|
|
98
99
|
volume: `${work}:/tmp/tenable`,
|
|
99
100
|
workdir: '/tmp/tenable',
|
|
101
|
+
entrypoint: scannerEntrypoint,
|
|
100
102
|
// The output destination goes over BOTH transports on purpose. Tenable's own
|
|
101
103
|
// GitHub action passes output-path and output-file-name only as TENABLE_*
|
|
102
104
|
// env vars, never as flags, so a build of the scanner that ignores the flags
|
|
@@ -157,6 +159,7 @@ export async function runTenableScan({
|
|
|
157
159
|
reportDir = process.env.TENABLE_REPORT_DIR || 'tenable-report',
|
|
158
160
|
scannerImage = process.env.TENABLE_SCANNER_IMAGE ||
|
|
159
161
|
'tenable/cloud-security-scanner:latest',
|
|
162
|
+
scannerEntrypoint = process.env.TENABLE_SCANNER_ENTRYPOINT || 'tenable',
|
|
160
163
|
minSeverity = process.env.TENABLE_MIN_SEVERITY || 'Medium',
|
|
161
164
|
failOnMinSeverity = process.env.TENABLE_FAIL_ON_MIN_SEVERITY || '',
|
|
162
165
|
tenableImage = process.env.TENABLE_IMAGE || '',
|
|
@@ -204,6 +207,7 @@ export async function runTenableScan({
|
|
|
204
207
|
const scanCtx = {
|
|
205
208
|
reportDir,
|
|
206
209
|
scannerImage,
|
|
210
|
+
scannerEntrypoint,
|
|
207
211
|
shortSha,
|
|
208
212
|
imageNamePrefix: repoSlug,
|
|
209
213
|
codeBranch,
|
package/src/tenable/scan.test.js
CHANGED
|
@@ -28,6 +28,7 @@ function makeFakeFsops(overrides = {}) {
|
|
|
28
28
|
const baseCtx = (overrides = {}) => ({
|
|
29
29
|
reportDir: 'tenable-report',
|
|
30
30
|
scannerImage: 'tenable/cloud-security-scanner:latest',
|
|
31
|
+
scannerEntrypoint: 'tenable',
|
|
31
32
|
shortSha: 'abc1234',
|
|
32
33
|
imageNamePrefix: 'my-repo',
|
|
33
34
|
codeBranch: 'main',
|
|
@@ -82,6 +83,27 @@ describe('scanOne', () => {
|
|
|
82
83
|
expect(fsops.removeSync).toHaveBeenCalledWith('/work/.tenable-work.abc');
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
test('drives the scanner through the tenable binary, not the image entrypoint', async () => {
|
|
87
|
+
const docker = makeFakeDocker();
|
|
88
|
+
const ctx = baseCtx({ docker });
|
|
89
|
+
|
|
90
|
+
await scanOne(
|
|
91
|
+
{ label: 'image', slug: 'image', prebuiltImage: 'my-image:tag' },
|
|
92
|
+
ctx,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const call = docker.run.mock.calls[0][0];
|
|
96
|
+
expect(call.entrypoint).toBe('tenable');
|
|
97
|
+
// The binary reads its output destination from the environment; the action
|
|
98
|
+
// that ships with the scanner passes no --output-path flag at all.
|
|
99
|
+
expect(call.env).toEqual(
|
|
100
|
+
expect.arrayContaining([
|
|
101
|
+
'TENABLE_OUTPUT_PATH=/tmp/tenable/out',
|
|
102
|
+
'TENABLE_OUTPUT_FILE_NAME=image-tenable',
|
|
103
|
+
]),
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
85
107
|
test('scans a pre-built image without building or removing anything', async () => {
|
|
86
108
|
const docker = makeFakeDocker();
|
|
87
109
|
const fsops = makeFakeFsops();
|
package/src/veracode/scan.js
CHANGED
|
@@ -57,10 +57,14 @@ export async function scanOne(target, ctx) {
|
|
|
57
57
|
return { label, slug, exitCode: 0 };
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// Commands the step shells out to on the host, rather than inside a container.
|
|
61
|
+
export const REQUIRED_COMMANDS = ['java', 'zip'];
|
|
62
|
+
|
|
60
63
|
export async function runVeracodeScan({
|
|
61
64
|
vid = process.env.VERACODE_API_ID || '',
|
|
62
65
|
vkey = process.env.VERACODE_API_KEY || '',
|
|
63
66
|
targetsEnv = process.env.VERACODE_TARGETS || '',
|
|
67
|
+
requireCredentials = process.env.VERACODE_REQUIRE_CREDENTIALS === 'true',
|
|
64
68
|
buildCmd = process.env.VERACODE_BUILD_CMD || '',
|
|
65
69
|
commit = process.env.BITBUCKET_COMMIT || 'local',
|
|
66
70
|
wrapperJar = process.env.VERACODE_WRAPPER_JAR || DEFAULT_WRAPPER_JAR,
|
|
@@ -69,18 +73,41 @@ export async function runVeracodeScan({
|
|
|
69
73
|
fsops = defaultFsOps,
|
|
70
74
|
log = console.log,
|
|
71
75
|
} = {}) {
|
|
76
|
+
// Absent credentials mean the repo or runner was never set up for Veracode,
|
|
77
|
+
// which is not the same thing as a broken configuration: skip, the way the
|
|
78
|
+
// Tenable step does, so a consumer can add the step before the credentials
|
|
79
|
+
// exist. VERACODE_REQUIRE_CREDENTIALS=true turns it back into a hard failure
|
|
80
|
+
// once a repo expects the scan to run on every build.
|
|
72
81
|
if (!vid || !vkey) {
|
|
82
|
+
if (requireCredentials) {
|
|
83
|
+
log(
|
|
84
|
+
'ERROR: VERACODE_API_ID / VERACODE_API_KEY not set and VERACODE_REQUIRE_CREDENTIALS=true — failing.',
|
|
85
|
+
);
|
|
86
|
+
return { exitCode: 1, skipped: false, results: [] };
|
|
87
|
+
}
|
|
73
88
|
log(
|
|
74
|
-
'
|
|
89
|
+
'VERACODE_API_ID / VERACODE_API_KEY not set — skipping Veracode scan.',
|
|
75
90
|
);
|
|
76
|
-
return { exitCode:
|
|
91
|
+
return { exitCode: 0, skipped: true, results: [] };
|
|
77
92
|
}
|
|
93
|
+
|
|
94
|
+
// Credentials present but no targets is a broken configuration, so this stays
|
|
95
|
+
// a hard failure however the credentials were supplied.
|
|
78
96
|
if (!targetsEnv) {
|
|
79
97
|
log('ERROR: VERACODE_TARGETS not set — cannot run Veracode scan.');
|
|
80
98
|
log(
|
|
81
99
|
' Expected: label:appname:path1,path2,...[;label2:appname2:path1,...]',
|
|
82
100
|
);
|
|
83
|
-
return { exitCode: 1, results: [] };
|
|
101
|
+
return { exitCode: 1, skipped: false, results: [] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const cmd of REQUIRED_COMMANDS) {
|
|
105
|
+
if (!(await veracode.commandExists(cmd))) {
|
|
106
|
+
log(
|
|
107
|
+
`ERROR: ${cmd} is not on PATH — the Veracode step packages and uploads with it.`,
|
|
108
|
+
);
|
|
109
|
+
return { exitCode: 1, skipped: false, results: [] };
|
|
110
|
+
}
|
|
84
111
|
}
|
|
85
112
|
|
|
86
113
|
// VERACODE_BUILD_CMD lets a consumer produce whatever build artifacts
|
|
@@ -116,5 +143,5 @@ export async function runVeracodeScan({
|
|
|
116
143
|
}
|
|
117
144
|
|
|
118
145
|
const exitCode = results.some((r) => r.exitCode !== 0) ? 1 : 0;
|
|
119
|
-
return { exitCode, results };
|
|
146
|
+
return { exitCode, skipped: false, results };
|
|
120
147
|
}
|
|
@@ -3,6 +3,7 @@ import { scanOne, runVeracodeScan, DEFAULT_WRAPPER_JAR } from './scan.js';
|
|
|
3
3
|
|
|
4
4
|
function makeFakeVeracode(overrides = {}) {
|
|
5
5
|
return {
|
|
6
|
+
commandExists: jest.fn().mockResolvedValue(true),
|
|
6
7
|
download: jest.fn().mockResolvedValue({ ok: true }),
|
|
7
8
|
runBuildCmd: jest.fn().mockResolvedValue({ ok: true }),
|
|
8
9
|
zip: jest.fn().mockResolvedValue({ ok: true }),
|
|
@@ -124,36 +125,82 @@ describe('scanOne', () => {
|
|
|
124
125
|
});
|
|
125
126
|
|
|
126
127
|
describe('runVeracodeScan', () => {
|
|
127
|
-
test('
|
|
128
|
+
test('skips without failing when credentials are missing', async () => {
|
|
128
129
|
const log = jest.fn();
|
|
130
|
+
const veracode = makeFakeVeracode();
|
|
129
131
|
const result = await runVeracodeScan({
|
|
130
132
|
vid: '',
|
|
131
133
|
vkey: '',
|
|
132
134
|
targetsEnv: 'web:my-app:src',
|
|
135
|
+
veracode,
|
|
133
136
|
log,
|
|
134
137
|
});
|
|
135
138
|
|
|
136
|
-
expect(result).toEqual({ exitCode:
|
|
139
|
+
expect(result).toEqual({ exitCode: 0, skipped: true, results: [] });
|
|
140
|
+
expect(veracode.zip).not.toHaveBeenCalled();
|
|
137
141
|
expect(log).toHaveBeenCalledWith(
|
|
138
142
|
expect.stringContaining('VERACODE_API_ID / VERACODE_API_KEY not set'),
|
|
139
143
|
);
|
|
140
144
|
});
|
|
141
145
|
|
|
146
|
+
test('fails on missing credentials when VERACODE_REQUIRE_CREDENTIALS is set', async () => {
|
|
147
|
+
const log = jest.fn();
|
|
148
|
+
const result = await runVeracodeScan({
|
|
149
|
+
vid: '',
|
|
150
|
+
vkey: '',
|
|
151
|
+
targetsEnv: 'web:my-app:src',
|
|
152
|
+
requireCredentials: true,
|
|
153
|
+
veracode: makeFakeVeracode(),
|
|
154
|
+
log,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(result).toEqual({ exitCode: 1, skipped: false, results: [] });
|
|
158
|
+
expect(log).toHaveBeenCalledWith(
|
|
159
|
+
expect.stringContaining('VERACODE_REQUIRE_CREDENTIALS=true'),
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
142
163
|
test('fails fast when VERACODE_TARGETS is missing', async () => {
|
|
143
164
|
const log = jest.fn();
|
|
144
165
|
const result = await runVeracodeScan({
|
|
145
166
|
vid: 'vid-1',
|
|
146
167
|
vkey: 'vkey-1',
|
|
147
168
|
targetsEnv: '',
|
|
169
|
+
veracode: makeFakeVeracode(),
|
|
148
170
|
log,
|
|
149
171
|
});
|
|
150
172
|
|
|
151
|
-
expect(result).toEqual({ exitCode: 1, results: [] });
|
|
173
|
+
expect(result).toEqual({ exitCode: 1, skipped: false, results: [] });
|
|
152
174
|
expect(log).toHaveBeenCalledWith(
|
|
153
175
|
expect.stringContaining('VERACODE_TARGETS not set'),
|
|
154
176
|
);
|
|
155
177
|
});
|
|
156
178
|
|
|
179
|
+
test.each(['java', 'zip'])(
|
|
180
|
+
'fails before packaging when %s is not on PATH',
|
|
181
|
+
async (missing) => {
|
|
182
|
+
const log = jest.fn();
|
|
183
|
+
const veracode = makeFakeVeracode({
|
|
184
|
+
commandExists: jest.fn(async (cmd) => cmd !== missing),
|
|
185
|
+
});
|
|
186
|
+
const result = await runVeracodeScan({
|
|
187
|
+
vid: 'vid-1',
|
|
188
|
+
vkey: 'vkey-1',
|
|
189
|
+
targetsEnv: 'web:my-app:src',
|
|
190
|
+
veracode,
|
|
191
|
+
fsops: makeFakeFsops(),
|
|
192
|
+
log,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(result).toEqual({ exitCode: 1, skipped: false, results: [] });
|
|
196
|
+
expect(veracode.zip).not.toHaveBeenCalled();
|
|
197
|
+
expect(veracode.download).not.toHaveBeenCalled();
|
|
198
|
+
expect(log).toHaveBeenCalledWith(
|
|
199
|
+
expect.stringContaining(`${missing} is not on PATH`),
|
|
200
|
+
);
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
|
|
157
204
|
test('runs VERACODE_BUILD_CMD before packaging when set', async () => {
|
|
158
205
|
const veracode = makeFakeVeracode();
|
|
159
206
|
const fsops = makeFakeFsops();
|
package/src/veracode/veracode.js
CHANGED
|
@@ -5,6 +5,18 @@ import { $ } from 'zx';
|
|
|
5
5
|
// exit is meaningful data here (a failed build/zip/upload), not an
|
|
6
6
|
// exceptional case.
|
|
7
7
|
export const veracode = {
|
|
8
|
+
// The wrapper is a jar and packaging is done by `zip`, so this step — alone
|
|
9
|
+
// among the scan steps — depends on host tooling rather than a container
|
|
10
|
+
// image. Checked up front so a missing JRE reads as a missing JRE instead of
|
|
11
|
+
// surfacing as a failed upload several minutes later.
|
|
12
|
+
async commandExists(cmd) {
|
|
13
|
+
const result = await $({
|
|
14
|
+
nothrow: true,
|
|
15
|
+
quiet: true,
|
|
16
|
+
})`command -v ${cmd}`;
|
|
17
|
+
return result.exitCode === 0;
|
|
18
|
+
},
|
|
19
|
+
|
|
8
20
|
async download(url, dest) {
|
|
9
21
|
const result = await $({
|
|
10
22
|
nothrow: true,
|