@docupace/pipeline 1.0.123-xray.0 → 1.0.123-xray.2
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 +3 -2
- package/src/tenable/args.js +8 -3
- package/src/tenable/args.test.js +3 -3
- package/src/tenable/docker.js +9 -2
- package/src/tenable/scan.js +37 -2
- package/src/tenable/scan.test.js +71 -0
- package/src/veracode/args.js +67 -0
- package/src/veracode/args.test.js +137 -0
- package/src/veracode/cli.js +16 -0
- package/src/veracode/fsops.js +7 -0
- package/src/veracode/scan.js +120 -0
- package/src/veracode/scan.test.js +246 -0
- package/src/veracode/veracode.js +33 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@docupace/pipeline",
|
|
3
|
-
"version": "1.0.123-xray.
|
|
3
|
+
"version": "1.0.123-xray.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"dp-merge-cli": "src/merge/cli.js",
|
|
15
15
|
"dp-micro-cli": "src/micro/cli.js",
|
|
16
16
|
"dp-xray-cli": "src/xray/cli.js",
|
|
17
|
-
"dp-tenable-cli": "src/tenable/cli.js"
|
|
17
|
+
"dp-tenable-cli": "src/tenable/cli.js",
|
|
18
|
+
"dp-veracode-cli": "src/veracode/cli.js"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|
|
20
21
|
"bash"
|
package/src/tenable/args.js
CHANGED
|
@@ -93,6 +93,11 @@ export function buildPipelineRunInfo({
|
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
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.
|
|
96
101
|
export function buildScanArgs({
|
|
97
102
|
slug,
|
|
98
103
|
scanTarget,
|
|
@@ -120,9 +125,9 @@ export function buildScanArgs({
|
|
|
120
125
|
'--code-repository-url',
|
|
121
126
|
codeRepoUrl,
|
|
122
127
|
'--min-severity',
|
|
123
|
-
minSeverity,
|
|
128
|
+
String(minSeverity).toLowerCase(),
|
|
124
129
|
'--output-file-formats',
|
|
125
|
-
'
|
|
130
|
+
'json,md,junit',
|
|
126
131
|
'--output-path',
|
|
127
132
|
'/tmp/tenable/out',
|
|
128
133
|
'--output-file-name',
|
|
@@ -142,7 +147,7 @@ export function buildScanArgs({
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
if (failOnMinSeverity) {
|
|
145
|
-
args.push('--fail-on-min-severity', failOnMinSeverity);
|
|
150
|
+
args.push('--fail-on-min-severity', failOnMinSeverity.toLowerCase());
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
return args;
|
package/src/tenable/args.test.js
CHANGED
|
@@ -184,9 +184,9 @@ describe('buildScanArgs', () => {
|
|
|
184
184
|
'--code-repository-url',
|
|
185
185
|
'https://bitbucket.org/dp-cloud/my-repo',
|
|
186
186
|
'--min-severity',
|
|
187
|
-
'
|
|
187
|
+
'medium',
|
|
188
188
|
'--output-file-formats',
|
|
189
|
-
'
|
|
189
|
+
'json,md,junit',
|
|
190
190
|
'--output-path',
|
|
191
191
|
'/tmp/tenable/out',
|
|
192
192
|
'--output-file-name',
|
|
@@ -220,7 +220,7 @@ describe('buildScanArgs', () => {
|
|
|
220
220
|
test('appends --fail-on-min-severity only when set', () => {
|
|
221
221
|
expect(buildScanArgs(base)).not.toContain('--fail-on-min-severity');
|
|
222
222
|
expect(buildScanArgs({ ...base, failOnMinSeverity: 'High' })).toEqual(
|
|
223
|
-
expect.arrayContaining(['--fail-on-min-severity', '
|
|
223
|
+
expect.arrayContaining(['--fail-on-min-severity', 'high']),
|
|
224
224
|
);
|
|
225
225
|
});
|
|
226
226
|
});
|
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,7 +98,18 @@ export async function scanOne(target, ctx) {
|
|
|
97
98
|
args: scanArgs,
|
|
98
99
|
volume: `${work}:/tmp/tenable`,
|
|
99
100
|
workdir: '/tmp/tenable',
|
|
100
|
-
|
|
101
|
+
entrypoint: scannerEntrypoint,
|
|
102
|
+
// The output destination goes over BOTH transports on purpose. Tenable's own
|
|
103
|
+
// GitHub action passes output-path and output-file-name only as TENABLE_*
|
|
104
|
+
// env vars, never as flags, so a build of the scanner that ignores the flags
|
|
105
|
+
// would silently fall back to its default path (the working directory) and
|
|
106
|
+
// leave the reports where nothing looks for them.
|
|
107
|
+
env: [
|
|
108
|
+
'TENABLE_API_TOKEN',
|
|
109
|
+
'TENABLE_API_URL',
|
|
110
|
+
'TENABLE_OUTPUT_PATH=/tmp/tenable/out',
|
|
111
|
+
`TENABLE_OUTPUT_FILE_NAME=${slug}-tenable`,
|
|
112
|
+
],
|
|
101
113
|
});
|
|
102
114
|
|
|
103
115
|
// Collect reports even on a non-zero exit — a threshold failure still produces them.
|
|
@@ -105,7 +117,28 @@ export async function scanOne(target, ctx) {
|
|
|
105
117
|
fsops.mkdirp(reportDir);
|
|
106
118
|
fsops.copyReports(`${work}/out`, reportDir);
|
|
107
119
|
} else {
|
|
108
|
-
|
|
120
|
+
// Before declaring the scan reportless, look in the work root: that is the
|
|
121
|
+
// scanner's default output path, so anything it wrote while ignoring the
|
|
122
|
+
// requested one lands beside the archive — and the work dir is deleted a few
|
|
123
|
+
// lines below, taking the reports with it.
|
|
124
|
+
const stray = fsops
|
|
125
|
+
.listDir(work)
|
|
126
|
+
.filter((name) => name !== 'image.tar' && name !== 'out');
|
|
127
|
+
if (stray.length > 0) {
|
|
128
|
+
log(
|
|
129
|
+
`Tenable wrote to the work root rather than the requested output path: ${stray.join(', ')}`,
|
|
130
|
+
);
|
|
131
|
+
fsops.mkdirp(reportDir);
|
|
132
|
+
for (const name of stray) {
|
|
133
|
+
fsops.copyReports(`${work}/${name}`, `${reportDir}/${name}`);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// The scanner exits 0 and prints nothing when it rejects an output format,
|
|
137
|
+
// so an empty work dir is worth reporting with the exit code attached.
|
|
138
|
+
log(
|
|
139
|
+
`Tenable produced no report files for '${slug}' (scanner exit ${exitCode}).`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
109
142
|
}
|
|
110
143
|
|
|
111
144
|
const summaryPath = `${reportDir}/${slug}-tenable.md`;
|
|
@@ -126,6 +159,7 @@ export async function runTenableScan({
|
|
|
126
159
|
reportDir = process.env.TENABLE_REPORT_DIR || 'tenable-report',
|
|
127
160
|
scannerImage = process.env.TENABLE_SCANNER_IMAGE ||
|
|
128
161
|
'tenable/cloud-security-scanner:latest',
|
|
162
|
+
scannerEntrypoint = process.env.TENABLE_SCANNER_ENTRYPOINT || 'tenable',
|
|
129
163
|
minSeverity = process.env.TENABLE_MIN_SEVERITY || 'Medium',
|
|
130
164
|
failOnMinSeverity = process.env.TENABLE_FAIL_ON_MIN_SEVERITY || '',
|
|
131
165
|
tenableImage = process.env.TENABLE_IMAGE || '',
|
|
@@ -173,6 +207,7 @@ export async function runTenableScan({
|
|
|
173
207
|
const scanCtx = {
|
|
174
208
|
reportDir,
|
|
175
209
|
scannerImage,
|
|
210
|
+
scannerEntrypoint,
|
|
176
211
|
shortSha,
|
|
177
212
|
imageNamePrefix: repoSlug,
|
|
178
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();
|
|
@@ -101,6 +123,55 @@ describe('scanOne', () => {
|
|
|
101
123
|
expect(docker.rmi).not.toHaveBeenCalled();
|
|
102
124
|
});
|
|
103
125
|
|
|
126
|
+
test('recovers reports the scanner wrote to the work root instead of out/', async () => {
|
|
127
|
+
const work = '/work/.tenable-work.abc';
|
|
128
|
+
const fsops = makeFakeFsops({
|
|
129
|
+
listDir: jest.fn((dir) =>
|
|
130
|
+
dir === `${work}/out`
|
|
131
|
+
? []
|
|
132
|
+
: ['image.tar', 'out', 'results.json', 'results.md'],
|
|
133
|
+
),
|
|
134
|
+
});
|
|
135
|
+
const ctx = baseCtx({ fsops });
|
|
136
|
+
|
|
137
|
+
await scanOne(
|
|
138
|
+
{ label: 'image', slug: 'image', prebuiltImage: 'my-image:tag' },
|
|
139
|
+
ctx,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// The archive and the empty output dir are not reports; everything else is.
|
|
143
|
+
expect(fsops.copyReports).toHaveBeenCalledTimes(2);
|
|
144
|
+
expect(fsops.copyReports).toHaveBeenCalledWith(
|
|
145
|
+
`${work}/results.json`,
|
|
146
|
+
'tenable-report/results.json',
|
|
147
|
+
);
|
|
148
|
+
expect(fsops.copyReports).toHaveBeenCalledWith(
|
|
149
|
+
`${work}/results.md`,
|
|
150
|
+
'tenable-report/results.md',
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('reports the scanner exit code when nothing was written anywhere', async () => {
|
|
155
|
+
const docker = makeFakeDocker({ run: jest.fn().mockResolvedValue(2) });
|
|
156
|
+
const fsops = makeFakeFsops({
|
|
157
|
+
listDir: jest.fn((dir) =>
|
|
158
|
+
dir === '/work/.tenable-work.abc/out' ? [] : ['image.tar'],
|
|
159
|
+
),
|
|
160
|
+
});
|
|
161
|
+
const log = jest.fn();
|
|
162
|
+
const ctx = baseCtx({ docker, fsops, log });
|
|
163
|
+
|
|
164
|
+
await scanOne(
|
|
165
|
+
{ label: 'image', slug: 'image', prebuiltImage: 'my-image:tag' },
|
|
166
|
+
ctx,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(fsops.copyReports).not.toHaveBeenCalled();
|
|
170
|
+
expect(log.mock.calls.flat().join('\n')).toContain(
|
|
171
|
+
"no report files for 'image' (scanner exit 2)",
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
104
175
|
test('skips a target with no Dockerfile at the expected path', async () => {
|
|
105
176
|
const docker = makeFakeDocker();
|
|
106
177
|
const fsops = makeFakeFsops({ exists: jest.fn().mockReturnValue(false) });
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Pure parsing/formatting logic for the Veracode scan step, ported from the
|
|
2
|
+
// veracode-scan step in bitbucket-pipelines.yml. No fs/process calls here so
|
|
3
|
+
// it's unit-testable.
|
|
4
|
+
|
|
5
|
+
// Mirrors `tr -c 'A-Za-z0-9._-' '-'`: every character outside that set is
|
|
6
|
+
// replaced with '-' (no squeezing of consecutive replacements).
|
|
7
|
+
export function slugify(label) {
|
|
8
|
+
return label.replace(/[^A-Za-z0-9._-]/g, '-');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Mirrors `printf '%s' "${BITBUCKET_COMMIT:-local}" | cut -c1-7`.
|
|
12
|
+
export function shortSha(commit = 'local') {
|
|
13
|
+
return (commit || 'local').slice(0, 7);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// One "label:appname:path1,path2,..." entry from VERACODE_TARGETS. Mirrors
|
|
17
|
+
// `cut -d: -f1/-f2/-f3`: only the first three colon-delimited fields matter,
|
|
18
|
+
// and a 4th+ field (e.g. a colon inside a path) is silently dropped rather
|
|
19
|
+
// than folded into the paths field.
|
|
20
|
+
export function parseVeracodeTargets(targetsStr) {
|
|
21
|
+
return (targetsStr || '')
|
|
22
|
+
.split(';')
|
|
23
|
+
.map((t) => t.trim())
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.map((raw) => {
|
|
26
|
+
const [rawLabel = '', appname = '', pathsField = ''] = raw.split(':');
|
|
27
|
+
const label = rawLabel || 'app';
|
|
28
|
+
const slug = slugify(label);
|
|
29
|
+
const paths = pathsField.split(',').filter((p) => p.length > 0);
|
|
30
|
+
|
|
31
|
+
if (!appname || paths.length === 0) {
|
|
32
|
+
return { label, slug, appname, paths, skipped: true };
|
|
33
|
+
}
|
|
34
|
+
return { label, slug, appname, paths, skipped: false };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function buildZipFilename(slug) {
|
|
39
|
+
return `veracode-${slug}.zip`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildVersion(sha, label) {
|
|
43
|
+
return `sha-${sha}-${label}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// -createprofile false — the app profile must already exist in the Veracode
|
|
47
|
+
// portal. -autoscan true starts the scan once prescan passes.
|
|
48
|
+
export function buildUploadArgs({ vid, vkey, appname, filepath, version }) {
|
|
49
|
+
return [
|
|
50
|
+
'-vid',
|
|
51
|
+
vid,
|
|
52
|
+
'-vkey',
|
|
53
|
+
vkey,
|
|
54
|
+
'-action',
|
|
55
|
+
'uploadandscan',
|
|
56
|
+
'-appname',
|
|
57
|
+
appname,
|
|
58
|
+
'-createprofile',
|
|
59
|
+
'false',
|
|
60
|
+
'-autoscan',
|
|
61
|
+
'true',
|
|
62
|
+
'-filepath',
|
|
63
|
+
filepath,
|
|
64
|
+
'-version',
|
|
65
|
+
version,
|
|
66
|
+
];
|
|
67
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, test } from '@jest/globals';
|
|
2
|
+
import {
|
|
3
|
+
slugify,
|
|
4
|
+
shortSha,
|
|
5
|
+
parseVeracodeTargets,
|
|
6
|
+
buildZipFilename,
|
|
7
|
+
buildVersion,
|
|
8
|
+
buildUploadArgs,
|
|
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('shortSha', () => {
|
|
20
|
+
test('truncates a commit sha to 7 characters', () => {
|
|
21
|
+
expect(shortSha('abcdef1234567')).toBe('abcdef1');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('defaults to "local" when no commit is given', () => {
|
|
25
|
+
expect(shortSha()).toBe('local');
|
|
26
|
+
expect(shortSha('')).toBe('local');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe('parseVeracodeTargets', () => {
|
|
31
|
+
test('parses label:appname:path1,path2 entries', () => {
|
|
32
|
+
const targets = parseVeracodeTargets(
|
|
33
|
+
'frontend:my-app-fe:apps/fe,libs/shared;backend:my-app-be:apps/be',
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
expect(targets).toEqual([
|
|
37
|
+
{
|
|
38
|
+
label: 'frontend',
|
|
39
|
+
slug: 'frontend',
|
|
40
|
+
appname: 'my-app-fe',
|
|
41
|
+
paths: ['apps/fe', 'libs/shared'],
|
|
42
|
+
skipped: false,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
label: 'backend',
|
|
46
|
+
slug: 'backend',
|
|
47
|
+
appname: 'my-app-be',
|
|
48
|
+
paths: ['apps/be'],
|
|
49
|
+
skipped: false,
|
|
50
|
+
},
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('defaults an empty label to "app" and slugifies it', () => {
|
|
55
|
+
const [target] = parseVeracodeTargets(':my-app:src');
|
|
56
|
+
expect(target.label).toBe('app');
|
|
57
|
+
expect(target.slug).toBe('app');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('marks a target with no appname as skipped', () => {
|
|
61
|
+
const [target] = parseVeracodeTargets('web::src');
|
|
62
|
+
expect(target).toEqual({
|
|
63
|
+
label: 'web',
|
|
64
|
+
slug: 'web',
|
|
65
|
+
appname: '',
|
|
66
|
+
paths: ['src'],
|
|
67
|
+
skipped: true,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('marks a target with no paths as skipped', () => {
|
|
72
|
+
const [target] = parseVeracodeTargets('web:my-app:');
|
|
73
|
+
expect(target).toEqual({
|
|
74
|
+
label: 'web',
|
|
75
|
+
slug: 'web',
|
|
76
|
+
appname: 'my-app',
|
|
77
|
+
paths: [],
|
|
78
|
+
skipped: true,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('drops a 4th+ colon-delimited field rather than folding it into paths', () => {
|
|
83
|
+
const [target] = parseVeracodeTargets('web:my-app:src:extra');
|
|
84
|
+
expect(target.paths).toEqual(['src']);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('ignores blank entries between semicolons', () => {
|
|
88
|
+
expect(parseVeracodeTargets('web:my-app:src;;')).toHaveLength(1);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('empty string produces no targets', () => {
|
|
92
|
+
expect(parseVeracodeTargets('')).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('buildZipFilename', () => {
|
|
97
|
+
test('builds a "veracode-<slug>.zip" filename', () => {
|
|
98
|
+
expect(buildZipFilename('web')).toBe('veracode-web.zip');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('buildVersion', () => {
|
|
103
|
+
test('builds "sha-<sha>-<label>", using the raw label rather than its slug', () => {
|
|
104
|
+
expect(buildVersion('abc1234', 'my app')).toBe('sha-abc1234-my app');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('buildUploadArgs', () => {
|
|
109
|
+
test('builds the Veracode wrapper CLI args', () => {
|
|
110
|
+
expect(
|
|
111
|
+
buildUploadArgs({
|
|
112
|
+
vid: 'vid-1',
|
|
113
|
+
vkey: 'vkey-1',
|
|
114
|
+
appname: 'my-app',
|
|
115
|
+
filepath: 'veracode-web.zip',
|
|
116
|
+
version: 'sha-abc1234-web',
|
|
117
|
+
}),
|
|
118
|
+
).toEqual([
|
|
119
|
+
'-vid',
|
|
120
|
+
'vid-1',
|
|
121
|
+
'-vkey',
|
|
122
|
+
'vkey-1',
|
|
123
|
+
'-action',
|
|
124
|
+
'uploadandscan',
|
|
125
|
+
'-appname',
|
|
126
|
+
'my-app',
|
|
127
|
+
'-createprofile',
|
|
128
|
+
'false',
|
|
129
|
+
'-autoscan',
|
|
130
|
+
'true',
|
|
131
|
+
'-filepath',
|
|
132
|
+
'veracode-web.zip',
|
|
133
|
+
'-version',
|
|
134
|
+
'sha-abc1234-web',
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { runVeracodeScan } from './scan.js';
|
|
5
|
+
|
|
6
|
+
const program = new Command();
|
|
7
|
+
|
|
8
|
+
program
|
|
9
|
+
.command('scan')
|
|
10
|
+
.description('Upload and scan one or more app targets with Veracode')
|
|
11
|
+
.action(async () => {
|
|
12
|
+
const { exitCode } = await runVeracodeScan();
|
|
13
|
+
process.exit(exitCode);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import {
|
|
3
|
+
buildUploadArgs,
|
|
4
|
+
buildVersion,
|
|
5
|
+
buildZipFilename,
|
|
6
|
+
parseVeracodeTargets,
|
|
7
|
+
shortSha as computeShortSha,
|
|
8
|
+
} from './args.js';
|
|
9
|
+
import { veracode as defaultVeracode } from './veracode.js';
|
|
10
|
+
import { fsOps as defaultFsOps } from './fsops.js';
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_WRAPPER_JAR = 'veracode-wrapper/VeracodeJavaAPI.jar';
|
|
13
|
+
export const DEFAULT_WRAPPER_URL =
|
|
14
|
+
'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';
|
|
15
|
+
|
|
16
|
+
// Zips one target's paths and submits them as their own uploadandscan call,
|
|
17
|
+
// so one repo with several apps (e.g. a frontend and a backend) gets separate
|
|
18
|
+
// Veracode profiles instead of one scan covering everything.
|
|
19
|
+
export async function scanOne(target, ctx) {
|
|
20
|
+
const { vid, vkey, shortSha, wrapperJar, veracode, fsops, log } = ctx;
|
|
21
|
+
const { label, appname, paths, slug, skipped } = target;
|
|
22
|
+
|
|
23
|
+
if (skipped) {
|
|
24
|
+
log(
|
|
25
|
+
`Target '${label}' is missing an appname or paths in VERACODE_TARGETS — skipping.`,
|
|
26
|
+
);
|
|
27
|
+
return { label, slug, exitCode: 1 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const zipFile = buildZipFilename(slug);
|
|
31
|
+
fsops.removeSync(zipFile);
|
|
32
|
+
|
|
33
|
+
const zipped = await veracode.zip(zipFile, paths);
|
|
34
|
+
if (!zipped.ok) {
|
|
35
|
+
log(`Failed to zip paths for '${label}' — skipping its Veracode scan.`);
|
|
36
|
+
fsops.removeSync(zipFile);
|
|
37
|
+
return { label, slug, exitCode: 1 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const version = buildVersion(shortSha, label);
|
|
41
|
+
const args = buildUploadArgs({
|
|
42
|
+
vid,
|
|
43
|
+
vkey,
|
|
44
|
+
appname,
|
|
45
|
+
filepath: zipFile,
|
|
46
|
+
version,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const uploaded = await veracode.uploadAndScan(wrapperJar, args);
|
|
50
|
+
fsops.removeSync(zipFile);
|
|
51
|
+
|
|
52
|
+
if (!uploaded.ok) {
|
|
53
|
+
log(`ERROR: Veracode scan submission failed for '${label}'.`);
|
|
54
|
+
return { label, slug, exitCode: 1 };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { label, slug, exitCode: 0 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function runVeracodeScan({
|
|
61
|
+
vid = process.env.VERACODE_API_ID || '',
|
|
62
|
+
vkey = process.env.VERACODE_API_KEY || '',
|
|
63
|
+
targetsEnv = process.env.VERACODE_TARGETS || '',
|
|
64
|
+
buildCmd = process.env.VERACODE_BUILD_CMD || '',
|
|
65
|
+
commit = process.env.BITBUCKET_COMMIT || 'local',
|
|
66
|
+
wrapperJar = process.env.VERACODE_WRAPPER_JAR || DEFAULT_WRAPPER_JAR,
|
|
67
|
+
wrapperUrl = process.env.VERACODE_WRAPPER_URL || DEFAULT_WRAPPER_URL,
|
|
68
|
+
veracode = defaultVeracode,
|
|
69
|
+
fsops = defaultFsOps,
|
|
70
|
+
log = console.log,
|
|
71
|
+
} = {}) {
|
|
72
|
+
if (!vid || !vkey) {
|
|
73
|
+
log(
|
|
74
|
+
'ERROR: VERACODE_API_ID / VERACODE_API_KEY not set — cannot run Veracode scan.',
|
|
75
|
+
);
|
|
76
|
+
return { exitCode: 1, results: [] };
|
|
77
|
+
}
|
|
78
|
+
if (!targetsEnv) {
|
|
79
|
+
log('ERROR: VERACODE_TARGETS not set — cannot run Veracode scan.');
|
|
80
|
+
log(
|
|
81
|
+
' Expected: label:appname:path1,path2,...[;label2:appname2:path1,...]',
|
|
82
|
+
);
|
|
83
|
+
return { exitCode: 1, results: [] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// VERACODE_BUILD_CMD lets a consumer produce whatever build artifacts
|
|
87
|
+
// Veracode's SAST needs (compiled bundles, etc.) without a separate
|
|
88
|
+
// pre-build step in the consumer's own pipeline. Its exit code isn't
|
|
89
|
+
// gated on here, matching the pipeline step it's ported from — a failure
|
|
90
|
+
// will simply surface as a failed upload for every target below.
|
|
91
|
+
if (buildCmd) {
|
|
92
|
+
log('Running VERACODE_BUILD_CMD...');
|
|
93
|
+
await veracode.runBuildCmd(buildCmd);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Cached across builds on self-hosted runners; downloaded once.
|
|
97
|
+
if (!fsops.exists(wrapperJar)) {
|
|
98
|
+
fsops.mkdirp(path.dirname(wrapperJar));
|
|
99
|
+
await veracode.download(wrapperUrl, wrapperJar);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sha = computeShortSha(commit);
|
|
103
|
+
const results = [];
|
|
104
|
+
for (const target of parseVeracodeTargets(targetsEnv)) {
|
|
105
|
+
results.push(
|
|
106
|
+
await scanOne(target, {
|
|
107
|
+
vid,
|
|
108
|
+
vkey,
|
|
109
|
+
shortSha: sha,
|
|
110
|
+
wrapperJar,
|
|
111
|
+
veracode,
|
|
112
|
+
fsops,
|
|
113
|
+
log,
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const exitCode = results.some((r) => r.exitCode !== 0) ? 1 : 0;
|
|
119
|
+
return { exitCode, results };
|
|
120
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { describe, expect, jest, test } from '@jest/globals';
|
|
2
|
+
import { scanOne, runVeracodeScan, DEFAULT_WRAPPER_JAR } from './scan.js';
|
|
3
|
+
|
|
4
|
+
function makeFakeVeracode(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
download: jest.fn().mockResolvedValue({ ok: true }),
|
|
7
|
+
runBuildCmd: jest.fn().mockResolvedValue({ ok: true }),
|
|
8
|
+
zip: jest.fn().mockResolvedValue({ ok: true }),
|
|
9
|
+
uploadAndScan: jest.fn().mockResolvedValue({ ok: true }),
|
|
10
|
+
...overrides,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function makeFakeFsops(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
exists: jest.fn().mockReturnValue(true),
|
|
17
|
+
mkdirp: jest.fn(),
|
|
18
|
+
removeSync: jest.fn(),
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const baseCtx = (overrides = {}) => ({
|
|
24
|
+
vid: 'vid-1',
|
|
25
|
+
vkey: 'vkey-1',
|
|
26
|
+
shortSha: 'abc1234',
|
|
27
|
+
wrapperJar: DEFAULT_WRAPPER_JAR,
|
|
28
|
+
veracode: makeFakeVeracode(),
|
|
29
|
+
fsops: makeFakeFsops(),
|
|
30
|
+
log: jest.fn(),
|
|
31
|
+
...overrides,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('scanOne', () => {
|
|
35
|
+
test('zips, uploads and cleans up a normal target', async () => {
|
|
36
|
+
const veracode = makeFakeVeracode();
|
|
37
|
+
const fsops = makeFakeFsops();
|
|
38
|
+
const ctx = baseCtx({ veracode, fsops });
|
|
39
|
+
|
|
40
|
+
const result = await scanOne(
|
|
41
|
+
{
|
|
42
|
+
label: 'web',
|
|
43
|
+
slug: 'web',
|
|
44
|
+
appname: 'my-app',
|
|
45
|
+
paths: ['services/web'],
|
|
46
|
+
skipped: false,
|
|
47
|
+
},
|
|
48
|
+
ctx,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
expect(result).toEqual({ label: 'web', slug: 'web', exitCode: 0 });
|
|
52
|
+
expect(veracode.zip).toHaveBeenCalledWith('veracode-web.zip', [
|
|
53
|
+
'services/web',
|
|
54
|
+
]);
|
|
55
|
+
expect(veracode.uploadAndScan).toHaveBeenCalledWith(
|
|
56
|
+
DEFAULT_WRAPPER_JAR,
|
|
57
|
+
expect.arrayContaining([
|
|
58
|
+
'-appname',
|
|
59
|
+
'my-app',
|
|
60
|
+
'-version',
|
|
61
|
+
'sha-abc1234-web',
|
|
62
|
+
]),
|
|
63
|
+
);
|
|
64
|
+
expect(fsops.removeSync).toHaveBeenCalledWith('veracode-web.zip');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('skips a target missing an appname or paths', async () => {
|
|
68
|
+
const veracode = makeFakeVeracode();
|
|
69
|
+
const ctx = baseCtx({ veracode });
|
|
70
|
+
|
|
71
|
+
const result = await scanOne(
|
|
72
|
+
{ label: 'web', slug: 'web', appname: '', paths: [], skipped: true },
|
|
73
|
+
ctx,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(result).toEqual({ label: 'web', slug: 'web', exitCode: 1 });
|
|
77
|
+
expect(veracode.zip).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('returns exitCode 1 and skips upload when zipping fails', async () => {
|
|
81
|
+
const veracode = makeFakeVeracode({
|
|
82
|
+
zip: jest.fn().mockResolvedValue({ ok: false }),
|
|
83
|
+
});
|
|
84
|
+
const fsops = makeFakeFsops();
|
|
85
|
+
const ctx = baseCtx({ veracode, fsops });
|
|
86
|
+
|
|
87
|
+
const result = await scanOne(
|
|
88
|
+
{
|
|
89
|
+
label: 'web',
|
|
90
|
+
slug: 'web',
|
|
91
|
+
appname: 'my-app',
|
|
92
|
+
paths: ['services/web'],
|
|
93
|
+
skipped: false,
|
|
94
|
+
},
|
|
95
|
+
ctx,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result).toEqual({ label: 'web', slug: 'web', exitCode: 1 });
|
|
99
|
+
expect(veracode.uploadAndScan).not.toHaveBeenCalled();
|
|
100
|
+
expect(fsops.removeSync).toHaveBeenCalledWith('veracode-web.zip');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('returns exitCode 1 when the upload fails, but still cleans up the zip', async () => {
|
|
104
|
+
const veracode = makeFakeVeracode({
|
|
105
|
+
uploadAndScan: jest.fn().mockResolvedValue({ ok: false }),
|
|
106
|
+
});
|
|
107
|
+
const fsops = makeFakeFsops();
|
|
108
|
+
const ctx = baseCtx({ veracode, fsops });
|
|
109
|
+
|
|
110
|
+
const result = await scanOne(
|
|
111
|
+
{
|
|
112
|
+
label: 'web',
|
|
113
|
+
slug: 'web',
|
|
114
|
+
appname: 'my-app',
|
|
115
|
+
paths: ['services/web'],
|
|
116
|
+
skipped: false,
|
|
117
|
+
},
|
|
118
|
+
ctx,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
expect(result).toEqual({ label: 'web', slug: 'web', exitCode: 1 });
|
|
122
|
+
expect(fsops.removeSync).toHaveBeenCalledWith('veracode-web.zip');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('runVeracodeScan', () => {
|
|
127
|
+
test('fails fast when credentials are missing', async () => {
|
|
128
|
+
const log = jest.fn();
|
|
129
|
+
const result = await runVeracodeScan({
|
|
130
|
+
vid: '',
|
|
131
|
+
vkey: '',
|
|
132
|
+
targetsEnv: 'web:my-app:src',
|
|
133
|
+
log,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(result).toEqual({ exitCode: 1, results: [] });
|
|
137
|
+
expect(log).toHaveBeenCalledWith(
|
|
138
|
+
expect.stringContaining('VERACODE_API_ID / VERACODE_API_KEY not set'),
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('fails fast when VERACODE_TARGETS is missing', async () => {
|
|
143
|
+
const log = jest.fn();
|
|
144
|
+
const result = await runVeracodeScan({
|
|
145
|
+
vid: 'vid-1',
|
|
146
|
+
vkey: 'vkey-1',
|
|
147
|
+
targetsEnv: '',
|
|
148
|
+
log,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(result).toEqual({ exitCode: 1, results: [] });
|
|
152
|
+
expect(log).toHaveBeenCalledWith(
|
|
153
|
+
expect.stringContaining('VERACODE_TARGETS not set'),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('runs VERACODE_BUILD_CMD before packaging when set', async () => {
|
|
158
|
+
const veracode = makeFakeVeracode();
|
|
159
|
+
const fsops = makeFakeFsops();
|
|
160
|
+
|
|
161
|
+
await runVeracodeScan({
|
|
162
|
+
vid: 'vid-1',
|
|
163
|
+
vkey: 'vkey-1',
|
|
164
|
+
targetsEnv: 'web:my-app:src',
|
|
165
|
+
buildCmd: 'npm run build',
|
|
166
|
+
veracode,
|
|
167
|
+
fsops,
|
|
168
|
+
log: jest.fn(),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(veracode.runBuildCmd).toHaveBeenCalledWith('npm run build');
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('downloads the wrapper jar only when not already cached', async () => {
|
|
175
|
+
const veracode = makeFakeVeracode();
|
|
176
|
+
const fsops = makeFakeFsops({ exists: jest.fn().mockReturnValue(false) });
|
|
177
|
+
|
|
178
|
+
await runVeracodeScan({
|
|
179
|
+
vid: 'vid-1',
|
|
180
|
+
vkey: 'vkey-1',
|
|
181
|
+
targetsEnv: 'web:my-app:src',
|
|
182
|
+
veracode,
|
|
183
|
+
fsops,
|
|
184
|
+
log: jest.fn(),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(veracode.download).toHaveBeenCalledWith(
|
|
188
|
+
expect.stringContaining('vosp-api-wrappers-java'),
|
|
189
|
+
DEFAULT_WRAPPER_JAR,
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('skips the download when the wrapper jar already exists', async () => {
|
|
194
|
+
const veracode = makeFakeVeracode();
|
|
195
|
+
const fsops = makeFakeFsops({ exists: jest.fn().mockReturnValue(true) });
|
|
196
|
+
|
|
197
|
+
await runVeracodeScan({
|
|
198
|
+
vid: 'vid-1',
|
|
199
|
+
vkey: 'vkey-1',
|
|
200
|
+
targetsEnv: 'web:my-app:src',
|
|
201
|
+
veracode,
|
|
202
|
+
fsops,
|
|
203
|
+
log: jest.fn(),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(veracode.download).not.toHaveBeenCalled();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('scans every target and reports the worst outcome', async () => {
|
|
210
|
+
const veracode = makeFakeVeracode({
|
|
211
|
+
uploadAndScan: jest
|
|
212
|
+
.fn()
|
|
213
|
+
.mockResolvedValueOnce({ ok: true })
|
|
214
|
+
.mockResolvedValueOnce({ ok: false }),
|
|
215
|
+
});
|
|
216
|
+
const fsops = makeFakeFsops();
|
|
217
|
+
|
|
218
|
+
const result = await runVeracodeScan({
|
|
219
|
+
vid: 'vid-1',
|
|
220
|
+
vkey: 'vkey-1',
|
|
221
|
+
targetsEnv: 'web:my-app:apps/web;worker:my-worker:apps/worker',
|
|
222
|
+
veracode,
|
|
223
|
+
fsops,
|
|
224
|
+
log: jest.fn(),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
expect(result.results.map((r) => r.label)).toEqual(['web', 'worker']);
|
|
228
|
+
expect(result.exitCode).toBe(1);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('exits 0 when every target scans successfully', async () => {
|
|
232
|
+
const veracode = makeFakeVeracode();
|
|
233
|
+
const fsops = makeFakeFsops();
|
|
234
|
+
|
|
235
|
+
const result = await runVeracodeScan({
|
|
236
|
+
vid: 'vid-1',
|
|
237
|
+
vkey: 'vkey-1',
|
|
238
|
+
targetsEnv: 'web:my-app:apps/web',
|
|
239
|
+
veracode,
|
|
240
|
+
fsops,
|
|
241
|
+
log: jest.fn(),
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(result.exitCode).toBe(0);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { $ } from 'zx';
|
|
2
|
+
|
|
3
|
+
// Thin wrappers around the external commands the Veracode step shells out to,
|
|
4
|
+
// so scan.js can inject a fake in tests. Every call uses nothrow: a non-zero
|
|
5
|
+
// exit is meaningful data here (a failed build/zip/upload), not an
|
|
6
|
+
// exceptional case.
|
|
7
|
+
export const veracode = {
|
|
8
|
+
async download(url, dest) {
|
|
9
|
+
const result = await $({
|
|
10
|
+
nothrow: true,
|
|
11
|
+
quiet: true,
|
|
12
|
+
})`curl -sSL ${url} -o ${dest}`;
|
|
13
|
+
return { ok: result.exitCode === 0 };
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
// Runs the consumer-supplied VERACODE_BUILD_CMD via `sh -c` so its own
|
|
17
|
+
// shell operators (&&, |, ...) are honored, mirroring the pipeline's
|
|
18
|
+
// `eval "${VERACODE_BUILD_CMD}"`.
|
|
19
|
+
async runBuildCmd(cmd) {
|
|
20
|
+
const result = await $({ nothrow: true })`sh -c ${cmd}`;
|
|
21
|
+
return { ok: result.exitCode === 0 };
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
async zip(zipFile, paths) {
|
|
25
|
+
const result = await $({ nothrow: true })`zip -r ${zipFile} ${paths}`;
|
|
26
|
+
return { ok: result.exitCode === 0 };
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
async uploadAndScan(jarPath, args) {
|
|
30
|
+
const result = await $({ nothrow: true })`java -jar ${jarPath} ${args}`;
|
|
31
|
+
return { ok: result.exitCode === 0 };
|
|
32
|
+
},
|
|
33
|
+
};
|