@docupace/pipeline 1.0.123-xray.3 → 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.
@@ -1,15 +0,0 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
-
4
- // The work dir is created inside the current directory (not os.tmpdir())
5
- // deliberately: it must be bind-mountable for docker, and on some hosts the
6
- // system temp dir is a symlink docker won't follow.
7
- export const fsOps = {
8
- mkdtemp: () => fs.mkdtempSync(path.join(process.cwd(), '.tenable-work.')),
9
- mkdirp: (dir) => fs.mkdirpSync(dir),
10
- exists: (p) => fs.existsSync(p),
11
- listDir: (dir) => (fs.existsSync(dir) ? fs.readdirSync(dir) : []),
12
- copyReports: (from, to) => fs.copySync(from, to),
13
- readFile: (p) => fs.readFileSync(p, 'utf8'),
14
- removeSync: (p) => fs.removeSync(p),
15
- };
@@ -1,284 +0,0 @@
1
- import {
2
- buildCodeMetadata,
3
- buildPipelineRunInfo,
4
- buildScanArgs,
5
- defaultScanTargets,
6
- parseScanTargets,
7
- SEPARATOR,
8
- } from './args.js';
9
- import { docker as defaultDocker } from './docker.js';
10
- import { fsOps as defaultFsOps } from './fsops.js';
11
-
12
- // Scans one target: builds (or reuses a pre-built) image, exports it to a
13
- // tar, and runs it through the scanner container, which has no access to the
14
- // host docker daemon so it can't resolve an image by name alone.
15
- export async function scanOne(target, ctx) {
16
- const {
17
- reportDir,
18
- scannerImage,
19
- scannerEntrypoint,
20
- shortSha,
21
- imageNamePrefix,
22
- codeBranch,
23
- commit,
24
- codeUser,
25
- codeRepoUrl,
26
- minSeverity,
27
- runInfo,
28
- failOnMinSeverity,
29
- docker,
30
- fsops,
31
- log,
32
- } = ctx;
33
- const { label, slug, buildContext, dockerfile, prebuiltImage } = target;
34
-
35
- let scanTarget;
36
- let builtImage = '';
37
-
38
- if (prebuiltImage) {
39
- scanTarget = prebuiltImage;
40
- log(`Scanning pre-existing image ${scanTarget}`);
41
- } else if (fsops.exists(dockerfile)) {
42
- scanTarget = `${imageNamePrefix}-${slug}:tenable-${shortSha}`;
43
- log(
44
- `Building ${scanTarget} from ${dockerfile} (context ${buildContext})...`,
45
- );
46
- const useLoad = await docker.buildxAvailable();
47
- const built = await docker.build([
48
- buildContext,
49
- '--file',
50
- dockerfile,
51
- '--tag',
52
- scanTarget,
53
- ...(useLoad ? ['--load'] : []),
54
- ]);
55
- if (!built.ok) {
56
- log(`Docker build failed for '${slug}' — skipping its Tenable scan.`);
57
- return { slug, exitCode: 1 };
58
- }
59
- builtImage = scanTarget;
60
- } else {
61
- log(`No Dockerfile at ${dockerfile} — nothing to scan for '${label}'.`);
62
- return { slug, exitCode: 0, skipped: true };
63
- }
64
-
65
- const work = fsops.mkdtemp();
66
-
67
- log(`Exporting ${scanTarget} to an archive...`);
68
- const saved = await docker.save(scanTarget, `${work}/image.tar`);
69
- if (!saved.ok) {
70
- log(`docker save failed for '${slug}' — skipping its Tenable scan.`);
71
- fsops.removeSync(work);
72
- if (builtImage) await docker.rmi(builtImage);
73
- return { slug, exitCode: 1 };
74
- }
75
-
76
- // --output-path must already exist; the scanner validates it before scanning.
77
- fsops.mkdirp(`${work}/out`);
78
-
79
- const scanArgs = buildScanArgs({
80
- slug,
81
- scanTarget,
82
- codeBranch,
83
- commit,
84
- codeUser,
85
- codeRepoUrl,
86
- minSeverity,
87
- runInfo,
88
- failOnMinSeverity,
89
- });
90
-
91
- log(
92
- `Running Tenable scan for '${slug}' (min-severity=${minSeverity}${
93
- failOnMinSeverity ? `, fail-on=${failOnMinSeverity}` : ''
94
- })...`,
95
- );
96
- const exitCode = await docker.run({
97
- image: scannerImage,
98
- args: scanArgs,
99
- volume: `${work}:/tmp/tenable`,
100
- workdir: '/tmp/tenable',
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
- ],
113
- });
114
-
115
- // Collect reports even on a non-zero exit — a threshold failure still produces them.
116
- if (fsops.listDir(`${work}/out`).length > 0) {
117
- fsops.mkdirp(reportDir);
118
- fsops.copyReports(`${work}/out`, reportDir);
119
- } else {
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
- }
142
- }
143
-
144
- const summaryPath = `${reportDir}/${slug}-tenable.md`;
145
- if (fsops.exists(summaryPath)) {
146
- log(`--- Tenable summary: ${slug} ---`);
147
- log(fsops.readFile(summaryPath).split('\n').slice(0, 60).join('\n'));
148
- }
149
-
150
- fsops.removeSync(work);
151
- if (builtImage) await docker.rmi(builtImage);
152
-
153
- return { slug, exitCode };
154
- }
155
-
156
- export async function runTenableScan({
157
- tenableApiToken = process.env.TENABLE_API_TOKEN,
158
- tenableApiUrl = process.env.TENABLE_API_URL,
159
- reportDir = process.env.TENABLE_REPORT_DIR || 'tenable-report',
160
- scannerImage = process.env.TENABLE_SCANNER_IMAGE ||
161
- 'tenable/cloud-security-scanner:latest',
162
- scannerEntrypoint = process.env.TENABLE_SCANNER_ENTRYPOINT || 'tenable',
163
- minSeverity = process.env.TENABLE_MIN_SEVERITY || 'Medium',
164
- failOnMinSeverity = process.env.TENABLE_FAIL_ON_MIN_SEVERITY || '',
165
- tenableImage = process.env.TENABLE_IMAGE || '',
166
- scanTargetsEnv = process.env.SCAN_TARGETS || '',
167
- buildContextFallback = process.env.XRAY_BUILD_CONTEXT || '.',
168
- dockerfileFallback = process.env.XRAY_DOCKERFILE_PATH || '',
169
- commit = process.env.BITBUCKET_COMMIT || 'local',
170
- branch = process.env.BITBUCKET_BRANCH || '',
171
- prDestinationBranch = process.env.BITBUCKET_PR_DESTINATION_BRANCH || '',
172
- workspace = process.env.BITBUCKET_WORKSPACE || 'unknown',
173
- repoSlug = process.env.BITBUCKET_REPO_SLUG || 'unknown',
174
- gitAuthorName = '',
175
- prId = process.env.BITBUCKET_PR_ID || '',
176
- buildNumber = process.env.BITBUCKET_BUILD_NUMBER || '',
177
- runId = process.env.TENABLE_PIPELINE_RUN_ID || '',
178
- runTrigger = process.env.TENABLE_PIPELINE_RUN_TRIGGER || '',
179
- runUrl = process.env.TENABLE_PIPELINE_RUN_URL || '',
180
- docker = defaultDocker,
181
- fsops = defaultFsOps,
182
- log = console.log,
183
- } = {}) {
184
- if (!tenableApiToken || !tenableApiUrl) {
185
- log('TENABLE_API_TOKEN / TENABLE_API_URL not set — skipping Tenable scan.');
186
- return { exitCode: 0, skipped: true, results: [] };
187
- }
188
-
189
- const { shortSha, codeBranch, codeRepoUrl, codeUser } = buildCodeMetadata({
190
- commit,
191
- branch,
192
- prDestinationBranch,
193
- workspace,
194
- repoSlug,
195
- gitAuthorName,
196
- });
197
- const runInfo = buildPipelineRunInfo({
198
- runId,
199
- runTrigger,
200
- runUrl,
201
- prId,
202
- buildNumber,
203
- workspace,
204
- repoSlug,
205
- });
206
-
207
- const scanCtx = {
208
- reportDir,
209
- scannerImage,
210
- scannerEntrypoint,
211
- shortSha,
212
- imageNamePrefix: repoSlug,
213
- codeBranch,
214
- commit,
215
- codeUser,
216
- codeRepoUrl,
217
- minSeverity,
218
- runInfo,
219
- failOnMinSeverity,
220
- docker,
221
- fsops,
222
- log,
223
- };
224
-
225
- const results = [];
226
-
227
- // TENABLE_IMAGE names an already-built image, which is inherently single-target.
228
- if (tenableImage) {
229
- results.push(
230
- await scanOne(
231
- { label: 'image', slug: 'image', prebuiltImage: tenableImage },
232
- scanCtx,
233
- ),
234
- );
235
- } else {
236
- const targetsStr =
237
- scanTargetsEnv ||
238
- defaultScanTargets(buildContextFallback, dockerfileFallback);
239
- if (!scanTargetsEnv) {
240
- log(`SCAN_TARGETS not set — scanning a single target: ${targetsStr}`);
241
- }
242
-
243
- let scanned = 0;
244
- for (const target of parseScanTargets(targetsStr)) {
245
- if (target.skipped) {
246
- log(
247
- `Target '${target.label}' has no build context — skipping its image scan.`,
248
- );
249
- continue;
250
- }
251
- log(SEPARATOR);
252
- log(`Tenable target '${target.label}'`);
253
- log(SEPARATOR);
254
- results.push(await scanOne(target, scanCtx));
255
- scanned += 1;
256
- }
257
-
258
- if (scanned === 0) {
259
- log('No scannable targets in SCAN_TARGETS — nothing to do.');
260
- }
261
- }
262
-
263
- // Report the worst outcome across targets, so one clean app can't mask
264
- // another's failure.
265
- const exitCode = results.reduce(
266
- (worst, r) => (r.exitCode !== 0 ? r.exitCode : worst),
267
- 0,
268
- );
269
-
270
- if (fsops.listDir(reportDir).length > 0) {
271
- log(`Reports written to ${reportDir}/:`);
272
- log(fsops.listDir(reportDir).join('\n'));
273
- }
274
-
275
- if (exitCode !== 0) {
276
- log(
277
- failOnMinSeverity
278
- ? `Tenable scan exited ${exitCode} — findings at or above ${failOnMinSeverity}, or a scan itself failed.`
279
- : `Tenable scan exited ${exitCode}.`,
280
- );
281
- }
282
-
283
- return { exitCode, skipped: false, results };
284
- }
@@ -1,355 +0,0 @@
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
- });