Skip to content

Commit 49fdc90

Browse files
committed
feat: send deploy validations report on secret scan
1 parent 3d52534 commit 49fdc90

File tree

11 files changed

+240
-13
lines changed

11 files changed

+240
-13
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/build/src/plugins_core/secrets_scanning/index.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import {
77
logSecretsScanSkipMessage,
88
logSecretsScanSuccessMessage,
99
} from '../../log/messages/core_steps.js'
10+
import { reportValidations } from '../../status/validations.js'
1011
import { CoreStep, CoreStepCondition, CoreStepFunction } from '../types.js'
1112

1213
import {
1314
ScanResults,
15+
SecretScanResult,
1416
getFilePathsToScan,
1517
getSecretKeysToScanFor,
1618
groupScanResultsByKey,
@@ -20,7 +22,15 @@ import {
2022

2123
const tracer = trace.getTracer('secrets-scanning')
2224

23-
const coreStep: CoreStepFunction = async function ({ buildDir, logs, netlifyConfig, explicitSecretKeys, systemLog }) {
25+
const coreStep: CoreStepFunction = async function ({
26+
buildDir,
27+
logs,
28+
netlifyConfig,
29+
explicitSecretKeys,
30+
systemLog,
31+
deployId,
32+
api,
33+
}) {
2434
const stepResults = {}
2535

2636
const passedSecretKeys = (explicitSecretKeys || '').split(',')
@@ -90,6 +100,14 @@ const coreStep: CoreStepFunction = async function ({ buildDir, logs, netlifyConf
90100
},
91101
)
92102

103+
if (deployId) {
104+
const secretScanResult: SecretScanResult = {
105+
scannedFilesCount: scanResults?.scannedFilesCount ?? 0,
106+
secretsScanMatches: scanResults?.matches ?? [],
107+
}
108+
reportValidations({ api, secretScanResult, deployId })
109+
}
110+
93111
if (!scanResults || scanResults.matches.length === 0) {
94112
logSecretsScanSuccessMessage(
95113
logs,

packages/build/src/plugins_core/secrets_scanning/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ interface MatchResult {
2323
file: string
2424
}
2525

26+
export type SecretScanResult = {
27+
scannedFilesCount: number
28+
secretsScanMatches: MatchResult[]
29+
}
30+
2631
/**
2732
* Determine if the user disabled scanning via env var
2833
* @param env current envars

packages/build/src/plugins_core/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type CoreStepFunctionArgs = {
2929
explicitSecretKeys: $TSFixme
3030

3131
buildbotServerSocket: $TSFixme
32+
api?: unknown
3233
}
3334

3435
export type CoreStepFunction = (args: CoreStepFunctionArgs) => Promise<object>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { SecretScanResult } from '../plugins_core/secrets_scanning/utils.js'
2+
3+
// Reports any validations completed on the deploy to the API
4+
export const reportValidations = async function ({
5+
api,
6+
secretScanResult,
7+
deployId,
8+
}: {
9+
api: unknown
10+
secretScanResult: SecretScanResult
11+
deployId: string
12+
}) {
13+
try {
14+
// @ts-expect-error API type is not defined
15+
api.updateDeployValidations({ deploy_id: deployId, body: { secrets_scan: secretScanResult } })
16+
} catch {
17+
// Noop
18+
}
19+
}

packages/build/src/steps/core_step.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export const fireCoreStep = async function ({
4040
edgeFunctionsBootstrapURL,
4141
deployId,
4242
outputFlusher,
43+
api,
4344
}) {
4445
const logsA = outputFlusher ? addOutputFlusher(logs, outputFlusher) : logs
4546

@@ -52,6 +53,7 @@ export const fireCoreStep = async function ({
5253
tags,
5354
metrics,
5455
} = await coreStep({
56+
api,
5557
configPath,
5658
outputConfigPath,
5759
buildDir,

packages/build/src/steps/run_step.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ export const runStep = async function ({
184184
explicitSecretKeys,
185185
edgeFunctionsBootstrapURL,
186186
deployId,
187+
api,
187188
})
188189

189190
const newValues = await getStepReturn({
@@ -346,6 +347,7 @@ const tFireStep = function ({
346347
edgeFunctionsBootstrapURL,
347348
deployId,
348349
extensionMetadata,
350+
api,
349351
}) {
350352
if (coreStep !== undefined) {
351353
return fireCoreStep({
@@ -383,6 +385,7 @@ const tFireStep = function ({
383385
explicitSecretKeys,
384386
edgeFunctionsBootstrapURL,
385387
deployId,
388+
api,
386389
})
387390
}
388391

packages/build/tests/secrets_scanning/snapshots/tests.js.md

Lines changed: 149 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Snapshot report for `tests/secrets_scanning/tests.js`
1+
# Snapshot report for `packages/build/tests/secrets_scanning/tests.js`
22

33
The actual snapshot is saved in `tests.js.snap`.
44

@@ -276,6 +276,154 @@ Generated by [AVA](https://avajs.dev).
276276
277277
(Netlify Build completed in 1ms)`
278278

279+
## secrets scanning, should fail build when it finds secrets in the src and build output and report to API
280+
281+
> Snapshot 1
282+
283+
`␊
284+
Netlify Build ␊
285+
────────────────────────────────────────────────────────────────␊
286+
287+
> Version␊
288+
@netlify/build 1.0.0␊
289+
290+
> Flags␊
291+
debug: false␊
292+
293+
> Current directory␊
294+
packages/build/tests/secrets_scanning/fixtures/src_scanning_env_vars_set_non_empty␊
295+
296+
> Config file␊
297+
packages/build/tests/secrets_scanning/fixtures/src_scanning_env_vars_set_non_empty/netlify.toml␊
298+
299+
> Context␊
300+
production␊
301+
302+
build.command from netlify.toml ␊
303+
────────────────────────────────────────────────────────────────␊
304+
305+
$ cp -r ./src/. ./dist␊
306+
307+
(build.command completed in 1ms)␊
308+
309+
Scanning for secrets in code and build output. ␊
310+
────────────────────────────────────────────────────────────────␊
311+
312+
313+
> Scanning complete. 14 file(s) scanned. Secrets scanning found 32 instance(s) of secrets in build output or repo code.␊
314+
315+
Secret env var "ENV_VAR_1"'s value detected:␊
316+
found value at line 12 in dist/static-files/static-a.txt␊
317+
found value at line 6 in netlify.toml␊
318+
found value at line 12 in src/static-files/static-a.txt␊
319+
Secret env var "ENV_VAR_2"'s value detected:␊
320+
found value at line 1 in dist/some-file.txt␊
321+
found value at line 1 in dist/static-files/static-a.txt␊
322+
found value at line 6 in dist/static-files/static-a.txt␊
323+
found value at line 7 in netlify.toml␊
324+
found value at line 1 in src/some-file.txt␊
325+
found value at line 1 in src/static-files/static-a.txt␊
326+
found value at line 6 in src/static-files/static-a.txt␊
327+
Secret env var "ENV_VAR_3"'s value detected:␊
328+
found value at line 14 in dist/static-files/static-a.txt␊
329+
found value at line 16 in dist/static-files/static-a.txt␊
330+
found value at line 1 in dist/static-files/static-c.txt␊
331+
found value at line 8 in netlify.toml␊
332+
found value at line 14 in src/static-files/static-a.txt␊
333+
found value at line 16 in src/static-files/static-a.txt␊
334+
found value at line 1 in src/static-files/static-c.txt␊
335+
Secret env var "ENV_VAR_4"'s value detected:␊
336+
found value at line 20 in dist/static-files/static-a.txt␊
337+
found value at line 9 in netlify.toml␊
338+
found value at line 20 in src/static-files/static-a.txt␊
339+
Secret env var "ENV_VAR_MULTILINE_A"'s value detected:␊
340+
found value at line 17 in dist/static-files/static-c.txt␊
341+
found value at line 38 in dist/static-files/static-c.txt␊
342+
found value at line 1 in dist/static-files/static-d.txt␊
343+
found value at line 15 in netlify.toml␊
344+
found value at line 17 in src/static-files/static-c.txt␊
345+
found value at line 38 in src/static-files/static-c.txt␊
346+
found value at line 1 in src/static-files/static-d.txt␊
347+
Secret env var "ENV_VAR_MULTILINE_B"'s value detected:␊
348+
found value at line 4 in dist/static-files/static-d.txt␊
349+
found value at line 1 in dist/static-files/static-e.txt␊
350+
found value at line 21 in netlify.toml␊
351+
found value at line 4 in src/static-files/static-d.txt␊
352+
found value at line 1 in src/static-files/static-e.txt␊
353+
354+
To prevent exposing secrets, the build will fail until these secret values are not found in build output or repo files.␊
355+
If these are expected, use SECRETS_SCAN_OMIT_PATHS, SECRETS_SCAN_OMIT_KEYS, or SECRETS_SCAN_ENABLED to prevent detecting.␊
356+
For more information on secrets scanning, see the Netlify Docs: https://ntl.fyi/configure-secrets-scanning␊
357+
358+
Secrets scanning detected secrets in files during build. ␊
359+
────────────────────────────────────────────────────────────────␊
360+
361+
Error message␊
362+
Secrets scanning found secrets in build.␊
363+
364+
Resolved config␊
365+
build:␊
366+
command: cp -r ./src/. ./dist␊
367+
commandOrigin: config␊
368+
environment:␊
369+
- ENV_VAR_1␊
370+
- ENV_VAR_2␊
371+
- ENV_VAR_3␊
372+
- ENV_VAR_4␊
373+
- ENV_VAR_5␊
374+
- ENV_VAR_6␊
375+
- ENV_VAR_7␊
376+
- NOT_SECRET_VAL␊
377+
- ENV_VAR_MULTILINE_A␊
378+
- ENV_VAR_MULTILINE_B␊
379+
- ENV_VAR_MULTI_NOT_SECRET␊
380+
publish: packages/build/tests/secrets_scanning/fixtures/src_scanning_env_vars_set_non_empty/dist␊
381+
publishOrigin: config`
382+
383+
## secrets scanning should report success to API when no secrets are found
384+
385+
> Snapshot 1
386+
387+
`␊
388+
Netlify Build ␊
389+
────────────────────────────────────────────────────────────────␊
390+
391+
> Version␊
392+
@netlify/build 1.0.0␊
393+
394+
> Flags␊
395+
debug: false␊
396+
397+
> Current directory␊
398+
packages/build/tests/secrets_scanning/fixtures/src_scanning_env_vars_no_matches␊
399+
400+
> Config file␊
401+
packages/build/tests/secrets_scanning/fixtures/src_scanning_env_vars_no_matches/netlify.toml␊
402+
403+
> Context␊
404+
production␊
405+
406+
build.command from netlify.toml ␊
407+
────────────────────────────────────────────────────────────────␊
408+
409+
$ cp -r ./src/static-files ./dist␊
410+
411+
(build.command completed in 1ms)␊
412+
413+
Scanning for secrets in code and build output. ␊
414+
────────────────────────────────────────────────────────────────␊
415+
416+
SECRETS_SCAN_OMIT_PATHS override option set to: netlify.toml␊
417+
418+
Secrets scanning complete. 4 file(s) scanned. No secrets detected in build output or repo code!␊
419+
420+
(Secrets scanning completed in 1ms)␊
421+
422+
Netlify Build Complete ␊
423+
────────────────────────────────────────────────────────────────␊
424+
425+
(Netlify Build completed in 1ms)`
426+
279427
## secrets scanning, should fail build when it finds secrets in the src and build output
280428

281429
> Snapshot 1
168 Bytes
Binary file not shown.

packages/build/tests/secrets_scanning/tests.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,22 @@ test('secrets scanning, should skip when secrets passed but SECRETS_SCAN_OMIT_PA
6060
t.assert(normalizeOutput(output).includes('found value at line 1 in src/static-files/notsafefile.js'))
6161
})
6262

63-
test('secrets scanning, should fail build when it finds secrets in the src and build output', async (t) => {
64-
const output = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty')
63+
test('secrets scanning should report success to API when no secrets are found', async (t) => {
64+
const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_no_matches')
6565
.withFlags({
6666
debug: false,
67-
explicitSecretKeys:
68-
'ENV_VAR_MULTILINE_A,ENV_VAR_1,ENV_VAR_2,ENV_VAR_3,ENV_VAR_4,ENV_VAR_5,ENV_VAR_6,ENV_VAR_MULTILINE_B',
67+
explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2',
68+
deployId: 'test',
69+
token: 'test',
6970
})
70-
.runWithBuild()
71-
t.snapshot(normalizeOutput(output))
71+
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })
72+
73+
console.log({ requests })
74+
t.true(requests.length === 1)
75+
const request = requests[0]
76+
t.is(request.method, 'PATCH')
77+
t.is(request.url, '/api/v1/deploys/test/validations_report')
78+
t.deepEqual(request.body, { secrets_scan: { scannedFilesCount: 4, secretsScanMatches: [] } })
7279
})
7380

7481
test('secrets scanning failure should produce an user error', async (t) => {
@@ -83,6 +90,30 @@ test('secrets scanning failure should produce an user error', async (t) => {
8390
t.is(severityCode, 2)
8491
})
8592

93+
test('secrets scanning should report failure to API when secrets are found', async (t) => {
94+
const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty')
95+
.withFlags({
96+
debug: false,
97+
explicitSecretKeys:
98+
'ENV_VAR_MULTILINE_A,ENV_VAR_1,ENV_VAR_2,ENV_VAR_3,ENV_VAR_4,ENV_VAR_5,ENV_VAR_6,ENV_VAR_MULTILINE_B',
99+
deployId: 'test',
100+
token: 'test',
101+
})
102+
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })
103+
104+
t.true(requests.length === 1)
105+
const request = requests[0]
106+
t.is(request.method, 'PATCH')
107+
t.is(request.url, '/api/v1/deploys/test/validations_report')
108+
t.is(request.body.secrets_scan.scannedFilesCount, 14)
109+
t.is(request.body.secrets_scan.secretsScanMatches.length, 32)
110+
t.deepEqual(request.body.secrets_scan.secretsScanMatches[0], {
111+
file: 'netlify.toml',
112+
lineNumber: 6,
113+
key: 'ENV_VAR_1',
114+
})
115+
})
116+
86117
test('secrets scanning, should not fail if the secrets values are not detected in the build output', async (t) => {
87118
const output = await new Fixture('./fixtures/src_scanning_env_vars_no_matches')
88119
.withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' })

0 commit comments

Comments
 (0)