@docupace/pipeline 1.0.127 → 1.0.130
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/.gitleaksignore +9 -0
- package/CHANGELOG.md +8 -0
- package/bin/tenable-scan.sh +12 -3
- package/bitbucket-pipelines.yml +91 -0
- package/package.json +6 -3
- package/pnpm-workspace.yaml +3 -0
- package/src/changeset/validate.js +18 -0
- package/src/next/config.js +1 -1
- package/src/next/deploy.test.js +5 -0
- package/src/next/util.js +45 -3
- package/src/tenable/args.js +157 -0
- package/src/tenable/args.test.js +226 -0
- package/src/tenable/cli.js +24 -0
- package/src/tenable/docker.js +45 -0
- package/src/tenable/fsops.js +15 -0
- package/src/tenable/scan.js +284 -0
- package/src/tenable/scan.test.js +355 -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 +145 -0
- package/src/veracode/scan.test.js +293 -0
- package/src/veracode/veracode.js +45 -0
- package/src/xray/cli.js +37 -0
- package/src/xray/notify.js +107 -0
- package/src/xray/notify.test.js +161 -0
- package/src/xray/report.js +194 -0
- package/src/xray/report.test.js +278 -0
- package/src/xray/slack.js +14 -0
- package/.gitignore +0 -103
- package/testCanny.sh +0 -4
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import { describe, expect, jest, test } from '@jest/globals';
|
|
2
|
+
import { scanOne, runTenableScan } from './scan.js';
|
|
3
|
+
|
|
4
|
+
function makeFakeDocker(overrides = {}) {
|
|
5
|
+
return {
|
|
6
|
+
buildxAvailable: jest.fn().mockResolvedValue(true),
|
|
7
|
+
build: jest.fn().mockResolvedValue({ ok: true }),
|
|
8
|
+
save: jest.fn().mockResolvedValue({ ok: true }),
|
|
9
|
+
run: jest.fn().mockResolvedValue(0),
|
|
10
|
+
rmi: jest.fn().mockResolvedValue(undefined),
|
|
11
|
+
...overrides,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function makeFakeFsops(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
mkdtemp: jest.fn().mockReturnValue('/work/.tenable-work.abc'),
|
|
18
|
+
mkdirp: jest.fn(),
|
|
19
|
+
exists: jest.fn().mockReturnValue(false),
|
|
20
|
+
listDir: jest.fn().mockReturnValue([]),
|
|
21
|
+
copyReports: jest.fn(),
|
|
22
|
+
readFile: jest.fn().mockReturnValue(''),
|
|
23
|
+
removeSync: jest.fn(),
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const baseCtx = (overrides = {}) => ({
|
|
29
|
+
reportDir: 'tenable-report',
|
|
30
|
+
scannerImage: 'tenable/cloud-security-scanner:latest',
|
|
31
|
+
scannerEntrypoint: 'tenable',
|
|
32
|
+
shortSha: 'abc1234',
|
|
33
|
+
imageNamePrefix: 'my-repo',
|
|
34
|
+
codeBranch: 'main',
|
|
35
|
+
commit: 'abc1234567',
|
|
36
|
+
codeUser: 'Matt Fields',
|
|
37
|
+
codeRepoUrl: 'https://bitbucket.org/dp-cloud/my-repo',
|
|
38
|
+
minSeverity: 'Medium',
|
|
39
|
+
runInfo: { includeInScanArgs: false },
|
|
40
|
+
failOnMinSeverity: '',
|
|
41
|
+
docker: makeFakeDocker(),
|
|
42
|
+
fsops: makeFakeFsops(),
|
|
43
|
+
log: jest.fn(),
|
|
44
|
+
...overrides,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('scanOne', () => {
|
|
48
|
+
test('builds, saves, scans and cleans up a normal target', async () => {
|
|
49
|
+
const docker = makeFakeDocker({ run: jest.fn().mockResolvedValue(0) });
|
|
50
|
+
const fsops = makeFakeFsops({
|
|
51
|
+
exists: jest.fn((p) => p === 'services/web/Dockerfile'),
|
|
52
|
+
listDir: jest.fn().mockReturnValue(['web-tenable.json']),
|
|
53
|
+
});
|
|
54
|
+
const ctx = baseCtx({ docker, fsops });
|
|
55
|
+
|
|
56
|
+
const result = await scanOne(
|
|
57
|
+
{
|
|
58
|
+
label: 'web',
|
|
59
|
+
slug: 'web',
|
|
60
|
+
buildContext: 'services/web',
|
|
61
|
+
dockerfile: 'services/web/Dockerfile',
|
|
62
|
+
},
|
|
63
|
+
ctx,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(result).toEqual({ slug: 'web', exitCode: 0 });
|
|
67
|
+
expect(docker.build).toHaveBeenCalledWith(
|
|
68
|
+
expect.arrayContaining([
|
|
69
|
+
'services/web',
|
|
70
|
+
'--file',
|
|
71
|
+
'services/web/Dockerfile',
|
|
72
|
+
'--tag',
|
|
73
|
+
'my-repo-web:tenable-abc1234',
|
|
74
|
+
]),
|
|
75
|
+
);
|
|
76
|
+
expect(docker.save).toHaveBeenCalledWith(
|
|
77
|
+
'my-repo-web:tenable-abc1234',
|
|
78
|
+
expect.stringContaining('image.tar'),
|
|
79
|
+
);
|
|
80
|
+
expect(docker.run).toHaveBeenCalled();
|
|
81
|
+
expect(fsops.copyReports).toHaveBeenCalled();
|
|
82
|
+
expect(docker.rmi).toHaveBeenCalledWith('my-repo-web:tenable-abc1234');
|
|
83
|
+
expect(fsops.removeSync).toHaveBeenCalledWith('/work/.tenable-work.abc');
|
|
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
|
+
|
|
107
|
+
test('scans a pre-built image without building or removing anything', async () => {
|
|
108
|
+
const docker = makeFakeDocker();
|
|
109
|
+
const fsops = makeFakeFsops();
|
|
110
|
+
const ctx = baseCtx({ docker, fsops });
|
|
111
|
+
|
|
112
|
+
const result = await scanOne(
|
|
113
|
+
{ label: 'image', slug: 'image', prebuiltImage: 'my-image:tag' },
|
|
114
|
+
ctx,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(result.exitCode).toBe(0);
|
|
118
|
+
expect(docker.build).not.toHaveBeenCalled();
|
|
119
|
+
expect(docker.save).toHaveBeenCalledWith(
|
|
120
|
+
'my-image:tag',
|
|
121
|
+
expect.any(String),
|
|
122
|
+
);
|
|
123
|
+
expect(docker.rmi).not.toHaveBeenCalled();
|
|
124
|
+
});
|
|
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
|
+
|
|
175
|
+
test('skips a target with no Dockerfile at the expected path', async () => {
|
|
176
|
+
const docker = makeFakeDocker();
|
|
177
|
+
const fsops = makeFakeFsops({ exists: jest.fn().mockReturnValue(false) });
|
|
178
|
+
const ctx = baseCtx({ docker, fsops });
|
|
179
|
+
|
|
180
|
+
const result = await scanOne(
|
|
181
|
+
{
|
|
182
|
+
label: 'web',
|
|
183
|
+
slug: 'web',
|
|
184
|
+
buildContext: 'services/web',
|
|
185
|
+
dockerfile: 'services/web/Dockerfile',
|
|
186
|
+
},
|
|
187
|
+
ctx,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(result).toEqual({ slug: 'web', exitCode: 0, skipped: true });
|
|
191
|
+
expect(docker.build).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('returns exitCode 1 and skips the scan when the docker build fails', async () => {
|
|
195
|
+
const docker = makeFakeDocker({
|
|
196
|
+
build: jest.fn().mockResolvedValue({ ok: false }),
|
|
197
|
+
});
|
|
198
|
+
const fsops = makeFakeFsops({ exists: jest.fn().mockReturnValue(true) });
|
|
199
|
+
const ctx = baseCtx({ docker, fsops });
|
|
200
|
+
|
|
201
|
+
const result = await scanOne(
|
|
202
|
+
{
|
|
203
|
+
label: 'web',
|
|
204
|
+
slug: 'web',
|
|
205
|
+
buildContext: 'services/web',
|
|
206
|
+
dockerfile: 'services/web/Dockerfile',
|
|
207
|
+
},
|
|
208
|
+
ctx,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(result).toEqual({ slug: 'web', exitCode: 1 });
|
|
212
|
+
expect(docker.save).not.toHaveBeenCalled();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('cleans up and returns exitCode 1 when docker save fails', async () => {
|
|
216
|
+
const docker = makeFakeDocker({
|
|
217
|
+
save: jest.fn().mockResolvedValue({ ok: false }),
|
|
218
|
+
});
|
|
219
|
+
const fsops = makeFakeFsops({ exists: jest.fn().mockReturnValue(true) });
|
|
220
|
+
const ctx = baseCtx({ docker, fsops });
|
|
221
|
+
|
|
222
|
+
const result = await scanOne(
|
|
223
|
+
{
|
|
224
|
+
label: 'web',
|
|
225
|
+
slug: 'web',
|
|
226
|
+
buildContext: 'services/web',
|
|
227
|
+
dockerfile: 'services/web/Dockerfile',
|
|
228
|
+
},
|
|
229
|
+
ctx,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(result).toEqual({ slug: 'web', exitCode: 1 });
|
|
233
|
+
expect(docker.run).not.toHaveBeenCalled();
|
|
234
|
+
expect(fsops.removeSync).toHaveBeenCalledWith('/work/.tenable-work.abc');
|
|
235
|
+
expect(docker.rmi).toHaveBeenCalledWith('my-repo-web:tenable-abc1234');
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test('propagates a non-zero scanner exit code (e.g. fail-on-min-severity threshold)', async () => {
|
|
239
|
+
const docker = makeFakeDocker({ run: jest.fn().mockResolvedValue(3) });
|
|
240
|
+
const fsops = makeFakeFsops({ exists: jest.fn().mockReturnValue(true) });
|
|
241
|
+
const ctx = baseCtx({ docker, fsops });
|
|
242
|
+
|
|
243
|
+
const result = await scanOne(
|
|
244
|
+
{
|
|
245
|
+
label: 'web',
|
|
246
|
+
slug: 'web',
|
|
247
|
+
buildContext: 'services/web',
|
|
248
|
+
dockerfile: 'services/web/Dockerfile',
|
|
249
|
+
},
|
|
250
|
+
ctx,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(result.exitCode).toBe(3);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe('runTenableScan', () => {
|
|
258
|
+
test('skips entirely when credentials are missing', async () => {
|
|
259
|
+
const log = jest.fn();
|
|
260
|
+
const result = await runTenableScan({
|
|
261
|
+
tenableApiToken: '',
|
|
262
|
+
tenableApiUrl: '',
|
|
263
|
+
log,
|
|
264
|
+
});
|
|
265
|
+
expect(result).toEqual({ exitCode: 0, skipped: true, results: [] });
|
|
266
|
+
expect(log).toHaveBeenCalledWith(
|
|
267
|
+
expect.stringContaining('not set — skipping'),
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test('scans TENABLE_IMAGE directly, ignoring SCAN_TARGETS', async () => {
|
|
272
|
+
const docker = makeFakeDocker();
|
|
273
|
+
const fsops = makeFakeFsops();
|
|
274
|
+
const result = await runTenableScan({
|
|
275
|
+
tenableApiToken: 'token',
|
|
276
|
+
tenableApiUrl: 'https://tenable.example',
|
|
277
|
+
tenableImage: 'prebuilt:tag',
|
|
278
|
+
scanTargetsEnv: 'web:deps:services/web:Dockerfile',
|
|
279
|
+
docker,
|
|
280
|
+
fsops,
|
|
281
|
+
log: jest.fn(),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(result.exitCode).toBe(0);
|
|
285
|
+
expect(result.results).toHaveLength(1);
|
|
286
|
+
expect(docker.save).toHaveBeenCalledWith(
|
|
287
|
+
'prebuilt:tag',
|
|
288
|
+
expect.any(String),
|
|
289
|
+
);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test('scans every target in SCAN_TARGETS and reports the worst exit code', async () => {
|
|
293
|
+
const fsops = makeFakeFsops({ exists: jest.fn().mockReturnValue(true) });
|
|
294
|
+
const docker = makeFakeDocker({
|
|
295
|
+
run: jest.fn().mockResolvedValueOnce(0).mockResolvedValueOnce(2),
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const result = await runTenableScan({
|
|
299
|
+
tenableApiToken: 'token',
|
|
300
|
+
tenableApiUrl: 'https://tenable.example',
|
|
301
|
+
scanTargetsEnv:
|
|
302
|
+
'web:deps:services/web:Dockerfile;worker:deps:services/worker:Dockerfile',
|
|
303
|
+
docker,
|
|
304
|
+
fsops,
|
|
305
|
+
log: jest.fn(),
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(result.results.map((r) => r.slug)).toEqual(['web', 'worker']);
|
|
309
|
+
expect(result.exitCode).toBe(2);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test('logs and skips a target with no build context, without failing the run', async () => {
|
|
313
|
+
const log = jest.fn();
|
|
314
|
+
const fsops = makeFakeFsops({ exists: jest.fn().mockReturnValue(true) });
|
|
315
|
+
const docker = makeFakeDocker();
|
|
316
|
+
|
|
317
|
+
const result = await runTenableScan({
|
|
318
|
+
tenableApiToken: 'token',
|
|
319
|
+
tenableApiUrl: 'https://tenable.example',
|
|
320
|
+
scanTargetsEnv: 'web:deps::Dockerfile',
|
|
321
|
+
docker,
|
|
322
|
+
fsops,
|
|
323
|
+
log,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(result.exitCode).toBe(0);
|
|
327
|
+
expect(result.results).toHaveLength(0);
|
|
328
|
+
expect(log).toHaveBeenCalledWith(
|
|
329
|
+
expect.stringContaining('no build context'),
|
|
330
|
+
);
|
|
331
|
+
expect(log).toHaveBeenCalledWith(expect.stringContaining('nothing to do'));
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test('falls back to a single default target when SCAN_TARGETS is unset', async () => {
|
|
335
|
+
const log = jest.fn();
|
|
336
|
+
const fsops = makeFakeFsops({ exists: jest.fn().mockReturnValue(true) });
|
|
337
|
+
const docker = makeFakeDocker();
|
|
338
|
+
|
|
339
|
+
const result = await runTenableScan({
|
|
340
|
+
tenableApiToken: 'token',
|
|
341
|
+
tenableApiUrl: 'https://tenable.example',
|
|
342
|
+
scanTargetsEnv: '',
|
|
343
|
+
buildContextFallback: '.',
|
|
344
|
+
dockerfileFallback: './Dockerfile',
|
|
345
|
+
docker,
|
|
346
|
+
fsops,
|
|
347
|
+
log,
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(result.results).toEqual([{ slug: 'app', exitCode: 0 }]);
|
|
351
|
+
expect(log).toHaveBeenCalledWith(
|
|
352
|
+
expect.stringContaining('SCAN_TARGETS not set'),
|
|
353
|
+
);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
@@ -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);
|