diff --git a/packages/build/src/plugins_core/secrets_scanning/index.ts b/packages/build/src/plugins_core/secrets_scanning/index.ts index 9341aafd04..f37d81e06d 100644 --- a/packages/build/src/plugins_core/secrets_scanning/index.ts +++ b/packages/build/src/plugins_core/secrets_scanning/index.ts @@ -29,6 +29,7 @@ const coreStep: CoreStepFunction = async function ({ netlifyConfig, explicitSecretKeys, enhancedSecretScan, + featureFlags, systemLog, deployId, api, @@ -37,8 +38,9 @@ const coreStep: CoreStepFunction = async function ({ const passedSecretKeys = (explicitSecretKeys || '').split(',') const envVars = netlifyConfig.build.environment as Record + const useReadLine = !featureFlags?.secret_scanning_no_readline - systemLog?.({ passedSecretKeys, buildDir }) + systemLog?.({ passedSecretKeys, buildDir, useReadLine }) if (!isSecretsScanningEnabled(envVars)) { logSecretsScanSkipMessage(logs, 'Secrets scanning disabled via SECRETS_SCAN_ENABLED flag set to false.') @@ -91,6 +93,7 @@ const coreStep: CoreStepFunction = async function ({ keys: keysToSearchFor, base: buildDir as string, filePaths, + useReadLine, }) secretMatches = scanResults.matches.filter((match) => explicitSecretKeysToScanFor.includes(match.key)) @@ -103,6 +106,7 @@ const coreStep: CoreStepFunction = async function ({ enhancedSecretsScanMatchesCount: enhancedSecretMatches.length, secretsFilesCount: scanResults.scannedFilesCount, keysToSearchFor, + useReadLine, } systemLog?.(attributesForLogsAndSpan) diff --git a/packages/build/src/plugins_core/secrets_scanning/utils.ts b/packages/build/src/plugins_core/secrets_scanning/utils.ts index ba151011ef..c762610733 100644 --- a/packages/build/src/plugins_core/secrets_scanning/utils.ts +++ b/packages/build/src/plugins_core/secrets_scanning/utils.ts @@ -17,6 +17,7 @@ interface ScanArgs { keys: string[] base: string filePaths: string[] + useReadLine: boolean } interface MatchResult { @@ -215,7 +216,13 @@ const omitPathMatches = (relativePath, omitPaths) => { * @param scanArgs {ScanArgs} scan options * @returns promise with all of the scan results, if any */ -export async function scanFilesForKeyValues({ env, keys, filePaths, base }: ScanArgs): Promise { +export async function scanFilesForKeyValues({ + env, + keys, + filePaths, + base, + useReadLine, +}: ScanArgs): Promise { const scanResults: ScanResults = { matches: [], scannedFilesCount: 0, @@ -245,6 +252,8 @@ export async function scanFilesForKeyValues({ env, keys, filePaths, base }: Scan let settledPromises: PromiseSettledResult[] = [] + const searchStream = useReadLine ? searchStreamReadline : searchStreamNoReadline + // process the scanning in batches to not run into memory issues by // processing all files at the same time. while (filePaths.length > 0) { @@ -269,7 +278,14 @@ export async function scanFilesForKeyValues({ env, keys, filePaths, base }: Scan return scanResults } -const searchStream = (basePath: string, file: string, keyValues: Record): Promise => { +/** + * Search stream implementation using node:readline + */ +const searchStreamReadline = ( + basePath: string, + file: string, + keyValues: Record, +): Promise => { return new Promise((resolve, reject) => { const filePath = path.resolve(basePath, file) @@ -391,6 +407,143 @@ const searchStream = (basePath: string, file: string, keyValues: Record, +): Promise => { + return new Promise((resolve, reject) => { + const filePath = path.resolve(basePath, file) + + const inStream = createReadStream(filePath) + const matches: MatchResult[] = [] + + const keyVals: string[] = ([] as string[]).concat(...Object.values(keyValues)) + + const maxValLength = Math.max(0, ...keyVals.map((v) => v.length)) + if (maxValLength === 0) { + // no non-empty values to scan for + return matches + } + + const minValLength = Math.min(...keyVals.map((v) => v.length)) + + function getKeyForValue(val) { + let key = '' + for (const [secretKeyName, valuePermutations] of Object.entries(keyValues)) { + if (valuePermutations.includes(val)) { + key = secretKeyName + } + } + return key + } + + let buffer = '' + + function getCurrentBufferNewLineIndexes() { + const newLinesIndexesInCurrentBuffer = [] as number[] + let newLineIndex = -1 + while ((newLineIndex = buffer.indexOf('\n', newLineIndex + 1)) !== -1) { + newLinesIndexesInCurrentBuffer.push(newLineIndex) + } + + return newLinesIndexesInCurrentBuffer + } + let fileIndex = 0 + let processedLines = 0 + const foundIndexes = new Map>() + const foundLines = new Map>() + inStream.on('data', function (chunk) { + const newChunk = chunk.toString() + + buffer += newChunk + + let newLinesIndexesInCurrentBuffer = null as null | number[] + + if (buffer.length > minValLength) { + for (const valVariant of keyVals) { + let valVariantIndex = -1 + while ((valVariantIndex = buffer.indexOf(valVariant, valVariantIndex + 1)) !== -1) { + const pos = fileIndex + valVariantIndex + let foundIndexesForValVariant = foundIndexes.get(valVariant) + if (!foundIndexesForValVariant?.has(pos)) { + if (newLinesIndexesInCurrentBuffer === null) { + newLinesIndexesInCurrentBuffer = getCurrentBufferNewLineIndexes() + } + + let lineNumber = processedLines + 1 + for (const newLineIndex of newLinesIndexesInCurrentBuffer) { + if (valVariantIndex > newLineIndex) { + lineNumber++ + } else { + break + } + } + + let foundLinesForValVariant = foundLines.get(valVariant) + if (!foundLinesForValVariant?.has(lineNumber)) { + matches.push({ + file, + lineNumber, + key: getKeyForValue(valVariant), + }) + + if (!foundLinesForValVariant) { + foundLinesForValVariant = new Set() + foundLines.set(valVariant, foundLinesForValVariant) + } + foundLinesForValVariant.add(lineNumber) + } + + if (!foundIndexesForValVariant) { + foundIndexesForValVariant = new Set() + foundIndexes.set(valVariant, foundIndexesForValVariant) + } + foundIndexesForValVariant.add(pos) + } + } + } + } + + if (buffer.length > maxValLength) { + const lengthDiff = buffer.length - maxValLength + fileIndex += lengthDiff + if (newLinesIndexesInCurrentBuffer === null) { + newLinesIndexesInCurrentBuffer = getCurrentBufferNewLineIndexes() + } + + // advanced processed lines + for (const newLineIndex of newLinesIndexesInCurrentBuffer) { + if (newLineIndex < lengthDiff) { + processedLines++ + } else { + break + } + } + + // Keep the last part of the buffer to handle split values across chunks + buffer = buffer.slice(-maxValLength) + } + }) + + inStream.on('error', function (error: any) { + if (error?.code === 'EISDIR') { + // file path is a directory - do nothing + resolve(matches) + } else { + reject(error) + } + }) + + inStream.on('close', function () { + resolve(matches) + }) + }) +} + /** * ScanResults are all of the finds for all keys and their disparate locations. Scanning is * async in streams so order can change a lot. Some matches are the result of an env var explictly being marked as secret, diff --git a/packages/build/tests/secrets_scanning/fixtures/src_scanning_large_binary_file/generate.mjs b/packages/build/tests/secrets_scanning/fixtures/src_scanning_large_binary_file/generate.mjs new file mode 100644 index 0000000000..19990b50ed --- /dev/null +++ b/packages/build/tests/secrets_scanning/fixtures/src_scanning_large_binary_file/generate.mjs @@ -0,0 +1,38 @@ +import { randomBytes } from "node:crypto"; +import { createWriteStream, mkdirSync } from "node:fs"; + +mkdirSync('dist', { recursive: true }); + +const writer = createWriteStream('dist/out.txt', { flags: "w" }); + +async function writeLotOfBytesWithoutNewLines() { + const max_size = 128 * 1024 * 1024; // 128MB + const chunk_size = 1024 * 1024; // 1MB + + let bytes_written = 0; + while (bytes_written < max_size) { + const bytes_to_write = Math.min(chunk_size, max_size - bytes_written); + const buffer = randomBytes(bytes_to_write).map((byte) => + // swap LF and CR to something else + byte === 0x0d || byte === 0x0a ? 0x0b : byte + ); + + writer.write(buffer); + bytes_written += bytes_to_write; + } +} + +await writeLotOfBytesWithoutNewLines() +writer.write(process.env.ENV_SECRET) +await writeLotOfBytesWithoutNewLines() + +await new Promise((resolve, reject) => { + writer.close(err => { + if (err) { + reject(err); + } else { + resolve(); + } + }) +}) + diff --git a/packages/build/tests/secrets_scanning/fixtures/src_scanning_large_binary_file/netlify.toml b/packages/build/tests/secrets_scanning/fixtures/src_scanning_large_binary_file/netlify.toml new file mode 100644 index 0000000000..767a56f79f --- /dev/null +++ b/packages/build/tests/secrets_scanning/fixtures/src_scanning_large_binary_file/netlify.toml @@ -0,0 +1,3 @@ +[build] +command = 'node generate.mjs' +publish = "./dist" diff --git a/packages/build/tests/secrets_scanning/snapshots/tests.js.md b/packages/build/tests/secrets_scanning/snapshots/tests.js.md deleted file mode 100644 index a216559ebd..0000000000 --- a/packages/build/tests/secrets_scanning/snapshots/tests.js.md +++ /dev/null @@ -1,344 +0,0 @@ -# Snapshot report for `tests/secrets_scanning/tests.js` - -The actual snapshot is saved in `tests.js.snap`. - -Generated by [AVA](https://avajs.dev). - -## secrets scanning, don't run when secrets are provided/default - -> Snapshot 1 - - `␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - > Version␊ - @netlify/build 1.0.0␊ - ␊ - > Flags␊ - debug: false␊ - ␊ - > Current directory␊ - packages/build/tests/secrets_scanning/fixtures/src_default␊ - ␊ - > Config file␊ - packages/build/tests/secrets_scanning/fixtures/src_default/netlify.toml␊ - ␊ - > Context␊ - production␊ - ␊ - > Loading plugins␊ - - ./plugin@1.0.0 from netlify.toml␊ - ␊ - ./plugin (onPreBuild event) ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - undefined␊ - ␊ - (./plugin onPreBuild completed in 1ms)␊ - ␊ - Netlify Build Complete ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - (Netlify Build completed in 1ms)` - -## secrets scanning, should skip with secrets but SECRETS_SCAN_ENABLED=false - -> Snapshot 1 - - `␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - > Version␊ - @netlify/build 1.0.0␊ - ␊ - > Flags␊ - debug: false␊ - ␊ - > Current directory␊ - packages/build/tests/secrets_scanning/fixtures/src_scanning_disabled␊ - ␊ - > Config file␊ - packages/build/tests/secrets_scanning/fixtures/src_scanning_disabled/netlify.toml␊ - ␊ - > Context␊ - production␊ - ␊ - Scanning for secrets in code and build output. ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - Secrets scanning disabled via SECRETS_SCAN_ENABLED flag set to false.␊ - ␊ - (Secrets scanning completed in 1ms)␊ - ␊ - Netlify Build Complete ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - (Netlify Build completed in 1ms)` - -## secrets scanning, should skip when secrets passed but no env vars set - -> Snapshot 1 - - `␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - > Version␊ - @netlify/build 1.0.0␊ - ␊ - > Flags␊ - debug: false␊ - ␊ - > Current directory␊ - packages/build/tests/secrets_scanning/fixtures/src_default␊ - ␊ - > Config file␊ - packages/build/tests/secrets_scanning/fixtures/src_default/netlify.toml␊ - ␊ - > Context␊ - production␊ - ␊ - > Loading plugins␊ - - ./plugin@1.0.0 from netlify.toml␊ - ␊ - ./plugin (onPreBuild event) ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - undefined␊ - ␊ - (./plugin onPreBuild completed in 1ms)␊ - ␊ - Scanning for secrets in code and build output. ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - Secrets scanning skipped because no env vars marked as secret are set to non-empty/non-trivial values or they are all omitted with SECRETS_SCAN_OMIT_KEYS env var setting.␊ - ␊ - (Secrets scanning completed in 1ms)␊ - ␊ - Netlify Build Complete ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - (Netlify Build completed in 1ms)` - -## secrets scanning, should skip when secrets passed but no non-empty/trivial env vars set - -> Snapshot 1 - - `␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - > Version␊ - @netlify/build 1.0.0␊ - ␊ - > Flags␊ - debug: false␊ - ␊ - > Current directory␊ - packages/build/tests/secrets_scanning/fixtures/src_scanning_env_vars_set_empty␊ - ␊ - > Config file␊ - packages/build/tests/secrets_scanning/fixtures/src_scanning_env_vars_set_empty/netlify.toml␊ - ␊ - > Context␊ - production␊ - ␊ - Scanning for secrets in code and build output. ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - Secrets scanning skipped because no env vars marked as secret are set to non-empty/non-trivial values or they are all omitted with SECRETS_SCAN_OMIT_KEYS env var setting.␊ - ␊ - (Secrets scanning completed in 1ms)␊ - ␊ - Netlify Build Complete ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - (Netlify Build completed in 1ms)` - -## secrets scanning, should skip when secrets passed but SECRETS_SCAN_OMIT_KEYS omits all of them - -> Snapshot 1 - - `␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - > Version␊ - @netlify/build 1.0.0␊ - ␊ - > Flags␊ - debug: false␊ - ␊ - > Current directory␊ - packages/build/tests/secrets_scanning/fixtures/src_scanning_omit_all_keys␊ - ␊ - > Config file␊ - packages/build/tests/secrets_scanning/fixtures/src_scanning_omit_all_keys/netlify.toml␊ - ␊ - > Context␊ - production␊ - ␊ - Scanning for secrets in code and build output. ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - SECRETS_SCAN_OMIT_KEYS override option set to: ENV_VAR_2,ENV_VAR_1␊ - ␊ - Secrets scanning skipped because no env vars marked as secret are set to non-empty/non-trivial values or they are all omitted with SECRETS_SCAN_OMIT_KEYS env var setting.␊ - ␊ - (Secrets scanning completed in 1ms)␊ - ␊ - Netlify Build Complete ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - (Netlify Build completed in 1ms)` - -## secrets scanning, should skip when secrets passed but SECRETS_SCAN_OMIT_PATHS omits all files - -> Snapshot 1 - - `␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - > Version␊ - @netlify/build 1.0.0␊ - ␊ - > Flags␊ - debug: false␊ - ␊ - > Current directory␊ - packages/build/tests/secrets_scanning/fixtures/src_scanning_omit_all_paths␊ - ␊ - > Config file␊ - packages/build/tests/secrets_scanning/fixtures/src_scanning_omit_all_paths/netlify.toml␊ - ␊ - > Context␊ - production␊ - ␊ - build.command from netlify.toml ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - $ cp -r ./src/static-files ./dist␊ - ␊ - (build.command completed in 1ms)␊ - ␊ - Scanning for secrets in code and build output. ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - SECRETS_SCAN_OMIT_PATHS override option set to: /external/path␊ - ␊ - Secrets scanning skipped because there are no files or all files were omitted with SECRETS_SCAN_OMIT_PATHS env var setting.␊ - ␊ - (Secrets scanning completed in 1ms)␊ - ␊ - Netlify Build Complete ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - (Netlify Build completed in 1ms)` - -## secrets scanning, should fail build and report to API when it finds secrets in the src and build output - -> Snapshot 1 - - `␊ - Netlify Build ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - > Version␊ - @netlify/build 1.0.0␊ - ␊ - > Flags␊ - debug: false␊ - deployId: test␊ - ␊ - > Current directory␊ - packages/build/tests/secrets_scanning/fixtures/src_scanning_env_vars_set_non_empty␊ - ␊ - > Config file␊ - packages/build/tests/secrets_scanning/fixtures/src_scanning_env_vars_set_non_empty/netlify.toml␊ - ␊ - > Context␊ - production␊ - ␊ - build.command from netlify.toml ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - $ cp -r ./src/. ./dist␊ - ␊ - (build.command completed in 1ms)␊ - ␊ - Scanning for secrets in code and build output. ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - ␊ - > Scanning complete. 14 file(s) scanned. Secrets scanning found 32 instance(s) of secrets in build output or repo code.␊ - ␊ - Secret env var "ENV_VAR_1"'s value detected:␊ - found value at line 12 in dist/static-files/static-a.txt␊ - found value at line 6 in netlify.toml␊ - found value at line 12 in src/static-files/static-a.txt␊ - Secret env var "ENV_VAR_2"'s value detected:␊ - found value at line 1 in dist/some-file.txt␊ - found value at line 1 in dist/static-files/static-a.txt␊ - found value at line 6 in dist/static-files/static-a.txt␊ - found value at line 7 in netlify.toml␊ - found value at line 1 in src/some-file.txt␊ - found value at line 1 in src/static-files/static-a.txt␊ - found value at line 6 in src/static-files/static-a.txt␊ - Secret env var "ENV_VAR_3"'s value detected:␊ - found value at line 14 in dist/static-files/static-a.txt␊ - found value at line 16 in dist/static-files/static-a.txt␊ - found value at line 1 in dist/static-files/static-c.txt␊ - found value at line 8 in netlify.toml␊ - found value at line 14 in src/static-files/static-a.txt␊ - found value at line 16 in src/static-files/static-a.txt␊ - found value at line 1 in src/static-files/static-c.txt␊ - Secret env var "ENV_VAR_4"'s value detected:␊ - found value at line 20 in dist/static-files/static-a.txt␊ - found value at line 9 in netlify.toml␊ - found value at line 20 in src/static-files/static-a.txt␊ - Secret env var "ENV_VAR_MULTILINE_A"'s value detected:␊ - found value at line 17 in dist/static-files/static-c.txt␊ - found value at line 38 in dist/static-files/static-c.txt␊ - found value at line 1 in dist/static-files/static-d.txt␊ - found value at line 15 in netlify.toml␊ - found value at line 17 in src/static-files/static-c.txt␊ - found value at line 38 in src/static-files/static-c.txt␊ - found value at line 1 in src/static-files/static-d.txt␊ - Secret env var "ENV_VAR_MULTILINE_B"'s value detected:␊ - found value at line 4 in dist/static-files/static-d.txt␊ - found value at line 1 in dist/static-files/static-e.txt␊ - found value at line 21 in netlify.toml␊ - found value at line 4 in src/static-files/static-d.txt␊ - found value at line 1 in src/static-files/static-e.txt␊ - ␊ - To prevent exposing secrets, the build will fail until these secret values are not found in build output or repo files.␊ - If these are expected, use SECRETS_SCAN_OMIT_PATHS, SECRETS_SCAN_OMIT_KEYS, or SECRETS_SCAN_ENABLED to prevent detecting.␊ - For more information on secrets scanning, see the Netlify Docs: https://ntl.fyi/configure-secrets-scanning␊ - ␊ - Secrets scanning detected secrets in files during build. ␊ - ────────────────────────────────────────────────────────────────␊ - ␊ - Error message␊ - Secrets scanning found secrets in build.␊ - ␊ - Resolved config␊ - build:␊ - command: cp -r ./src/. ./dist␊ - commandOrigin: config␊ - environment:␊ - - ENV_VAR_1␊ - - ENV_VAR_2␊ - - ENV_VAR_3␊ - - ENV_VAR_4␊ - - ENV_VAR_5␊ - - ENV_VAR_6␊ - - ENV_VAR_7␊ - - NOT_SECRET_VAL␊ - - ENV_VAR_MULTILINE_A␊ - - ENV_VAR_MULTILINE_B␊ - - ENV_VAR_MULTI_NOT_SECRET␊ - publish: packages/build/tests/secrets_scanning/fixtures/src_scanning_env_vars_set_non_empty/dist␊ - publishOrigin: config` diff --git a/packages/build/tests/secrets_scanning/snapshots/tests.js.snap b/packages/build/tests/secrets_scanning/snapshots/tests.js.snap deleted file mode 100644 index 071e52035e..0000000000 Binary files a/packages/build/tests/secrets_scanning/snapshots/tests.js.snap and /dev/null differ diff --git a/packages/build/tests/secrets_scanning/tests.js b/packages/build/tests/secrets_scanning/tests.js index 2b1a1de273..1396f75064 100644 --- a/packages/build/tests/secrets_scanning/tests.js +++ b/packages/build/tests/secrets_scanning/tests.js @@ -1,255 +1,438 @@ import { Fixture, normalizeOutput } from '@netlify/testing' import test from 'ava' -test("secrets scanning, don't run when secrets are provided/default", async (t) => { - const output = await new Fixture('./fixtures/src_default').withFlags({ debug: false }).runWithBuild() - t.snapshot(normalizeOutput(output)) -}) - -test("secrets scanning, don't run when there are no secrets and enhanced scan not enabled", async (t) => { - const { requests } = await new Fixture('./fixtures/src_default') - .withFlags({ debug: false, explicitSecretKeys: '', deployId: 'test', token: 'test' }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.true(requests.length === 0) -}) - -test("secrets scanning, don't run when no explicit secrets, enhanced scan enabled but no likely secrets", async (t) => { - const { requests } = await new Fixture('./fixtures/src_scanning_no_likely_enhanced_scan_secrets') - .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.true(requests.length === 0) -}) - -test('secrets scanning, run and report result to API when there are no secrets and enhanced scan is enabled with likely secrets', async (t) => { - const { requests } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets') - .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.true(requests.length === 1) - const request = requests[0] - t.is(request.method, 'PATCH') - t.is(request.url, '/api/v1/deploys/test/validations_report') - t.truthy(request.body.secrets_scan.scannedFilesCount) - t.truthy(request.body.secrets_scan.secretsScanMatches) - t.truthy(request.body.secrets_scan.enhancedSecretsScanMatches) -}) - -test('secrets scanning, should skip with secrets but SECRETS_SCAN_ENABLED=false', async (t) => { - const output = await new Fixture('./fixtures/src_scanning_disabled') - .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) - .runWithBuild() - t.snapshot(normalizeOutput(output)) -}) - -test('secrets scanning, should skip with enhanced scan but SECRETS_SCAN_ENABLED=false', async (t) => { - const { requests } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets_disabled') - .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.true(requests.length === 0) -}) - -test('secrets scanning, should skip when secrets passed but no env vars set', async (t) => { - const output = await new Fixture('./fixtures/src_default') - .withFlags({ debug: false, explicitSecretKeys: 'abc,DEF' }) - .runWithBuild() - t.snapshot(normalizeOutput(output)) -}) - -test('secrets scanning, should skip when enhanced scan enabled but no env vars set', async (t) => { - const { requests } = await new Fixture('./fixtures/src_default') - .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.true(requests.length === 0) -}) - -test('secrets scanning, should skip when secrets passed but no non-empty/trivial env vars set', async (t) => { - const output = await new Fixture('./fixtures/src_scanning_env_vars_set_empty') - .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_,2ENV_VAR_3,ENV_VAR_4,ENV_VAR_5' }) - .runWithBuild() - t.snapshot(normalizeOutput(output)) -}) - -test('secrets scanning, should skip when enhanced scan enabled but no non-empty/trivial env vars set', async (t) => { - const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty') - .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.true(requests.length === 0) -}) - -test('secrets scanning, should skip when secrets passed but SECRETS_SCAN_OMIT_KEYS omits all of them', async (t) => { - const output = await new Fixture('./fixtures/src_scanning_omit_all_keys') - .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) - .runWithBuild() - t.snapshot(normalizeOutput(output)) -}) - -test('secrets scanning, should skip when enhanced scan and likely secrets passed but SECRETS_SCAN_OMIT_KEYS omits all of them', async (t) => { - const { requests } = await new Fixture('./fixtures/src_scanning_omit_all_keys') - .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.true(requests.length === 0) -}) - -test('secrets scanning, should skip when secrets passed but SECRETS_SCAN_OMIT_PATHS omits all files', async (t) => { - const output = await new Fixture('./fixtures/src_scanning_omit_all_paths') - .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) - .runWithBuild() - t.snapshot(normalizeOutput(output)) -}) - -test('secrets scanning, should skip when enhanced scan and likely secrets passed but SECRETS_SCAN_OMIT_PATHS omits all files', async (t) => { - const { requests } = await new Fixture('./fixtures/src_scanning_omit_all_paths') - .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.true(requests.length === 0) -}) - -test('secrets scanning, should skip when secrets passed but SECRETS_SCAN_OMIT_PATHS omits globbed files', async (t) => { - const output = await new Fixture('./fixtures/src_scanning_omit_glob_path') - .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) - .runWithBuild() - - t.false(normalizeOutput(output).includes('found value at line 1 in dist/safefile.js')) - t.false(normalizeOutput(output).includes('found value at line 1 in src/static-files/safefile.js')) - - // Ensure SECRETS_SCAN_OMIT_PATHS doesn't exclude more than the defined glob - t.assert(normalizeOutput(output).includes('found value at line 1 in src/static-files/notsafefile.js')) -}) - -test('secrets scanning, should fail build and report to API when it finds secrets in the src and build output', async (t) => { - const { output, requests } = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty') - .withFlags({ - debug: false, - explicitSecretKeys: - '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', - deployId: 'test', - token: 'test', - }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - t.snapshot(normalizeOutput(output)) - - t.true(requests.length === 1) - const request = requests[0] - t.is(request.method, 'PATCH') - t.is(request.url, '/api/v1/deploys/test/validations_report') - t.truthy(request.body.secrets_scan.scannedFilesCount) - t.is(request.body.secrets_scan.secretsScanMatches.length, 32) - t.is(request.body.secrets_scan.enhancedSecretsScanMatches.length, 0) -}) - -test('secrets scanning, should fail build and report to API when enhanced scan finds likely secret in the src and build output', async (t) => { - const { output, requests } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets') - .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.assert(normalizeOutput(output).includes(`Env var "ENV_VAR_1"'s value detected as a likely secret value`)) - t.assert( - normalizeOutput(output).includes( - `the build will fail until these secret values are not found in build output or repo files`, - ), +for (const { testPrefix, featureFlags } of [ + { + testPrefix: 'scanning using node:readline (default) > ', + featureFlags: {}, + }, + { + testPrefix: 'scanning without buffering whole lines (feature flag) > ', + featureFlags: { + secret_scanning_no_readline: true, + }, + }, +]) { + test(testPrefix + "secrets scanning, don't run when no secrets are provided/default", async (t) => { + const output = await new Fixture('./fixtures/src_default').withFlags({ debug: false, featureFlags }).runWithBuild() + t.false(normalizeOutput(output).includes('Scanning for secrets in code and build output')) + }) + + test( + testPrefix + "secrets scanning, don't run when there are no secrets and enhanced scan not enabled", + async (t) => { + const { requests } = await new Fixture('./fixtures/src_default') + .withFlags({ debug: false, explicitSecretKeys: '', deployId: 'test', token: 'test', featureFlags }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.true(requests.length === 0) + }, ) - t.true(requests.length === 1) - const request = requests[0] - t.is(request.method, 'PATCH') - t.is(request.url, '/api/v1/deploys/test/validations_report') - t.truthy(request.body.secrets_scan.scannedFilesCount) - t.is(request.body.secrets_scan.secretsScanMatches.length, 0) - t.is(request.body.secrets_scan.enhancedSecretsScanMatches.length, 1) -}) - -test('secrets scanning should report success to API when no secrets are found', async (t) => { - const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_no_matches') - .withFlags({ - debug: false, - explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2', - deployId: 'test', - token: 'test', - }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.true(requests.length === 1) - const request = requests[0] - t.is(request.method, 'PATCH') - t.is(request.url, '/api/v1/deploys/test/validations_report') - t.truthy(request.body.secrets_scan.scannedFilesCount) - t.truthy(request.body.secrets_scan.secretsScanMatches) - t.truthy(request.body.secrets_scan.enhancedSecretsScanMatches) -}) - -test('secrets scanning, should report success to API when enhanced scans finds no likely secrets', async (t) => { - const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_no_matches') - .withFlags({ - debug: false, - enhancedSecretScan: true, - deployId: 'test', - token: 'test', - }) - .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) - - t.true(requests.length === 1) - const request = requests[0] - t.is(request.method, 'PATCH') - t.is(request.url, '/api/v1/deploys/test/validations_report') - t.truthy(request.body.secrets_scan.scannedFilesCount) - t.truthy(request.body.secrets_scan.secretsScanMatches) - t.truthy(request.body.secrets_scan.enhancedSecretsScanMatches) -}) - -test('secrets scanning failure should produce an user error', async (t) => { - const { severityCode } = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty') - .withFlags({ - debug: false, - explicitSecretKeys: - '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', - }) - .runBuildProgrammatic() - // Severity code of 2 is user error - t.is(severityCode, 2) -}) - -test('secrets scanning, enhanced scanning failure should produce a user error', async (t) => { - const { severityCode } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets') - .withFlags({ - debug: false, - explicitSecretKeys: '', - enhancedSecretScan: true, - }) - .runBuildProgrammatic() - // Severity code of 2 is user error - t.is(severityCode, 2) -}) - -test('secrets scan does not send report to API for local builds', async (t) => { - const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty') - .withFlags({ - debug: false, - explicitSecretKeys: - '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', - deployId: '0', - token: 'test', + + test( + testPrefix + "secrets scanning, don't run when no explicit secrets, enhanced scan enabled but no likely secrets", + async (t) => { + const { requests } = await new Fixture('./fixtures/src_scanning_no_likely_enhanced_scan_secrets') + .withFlags({ + debug: false, + explicitSecretKeys: '', + enhancedSecretScan: true, + deployId: 'test', + token: 'test', + featureFlags, + }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.true(requests.length === 0) + }, + ) + + test( + testPrefix + + 'secrets scanning, run and report result to API when there are no secrets and enhanced scan is enabled with likely secrets', + async (t) => { + const { requests } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets') + .withFlags({ + debug: false, + explicitSecretKeys: '', + enhancedSecretScan: true, + deployId: 'test', + token: 'test', + featureFlags, + }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.true(requests.length === 1) + const request = requests[0] + t.is(request.method, 'PATCH') + t.is(request.url, '/api/v1/deploys/test/validations_report') + t.truthy(request.body.secrets_scan.scannedFilesCount) + t.truthy(request.body.secrets_scan.secretsScanMatches) + t.truthy(request.body.secrets_scan.enhancedSecretsScanMatches) + }, + ) + + test(testPrefix + 'secrets scanning, should skip with secrets but SECRETS_SCAN_ENABLED=false', async (t) => { + const output = await new Fixture('./fixtures/src_scanning_disabled') + .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2', featureFlags }) + .runWithBuild() + t.true(normalizeOutput(output).includes('Secrets scanning disabled via SECRETS_SCAN_ENABLED flag set to false.')) + }) + + test(testPrefix + 'secrets scanning, should skip with enhanced scan but SECRETS_SCAN_ENABLED=false', async (t) => { + const { requests } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets_disabled') + .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.true(requests.length === 0) + }) + + test(testPrefix + 'secrets scanning, should skip when secrets passed but no env vars set', async (t) => { + const output = await new Fixture('./fixtures/src_default') + .withFlags({ debug: false, explicitSecretKeys: 'abc,DEF' }) + .runWithBuild() + t.true( + normalizeOutput(output).includes( + 'Secrets scanning skipped because no env vars marked as secret are set to non-empty/non-trivial values or they are all omitted with SECRETS_SCAN_OMIT_KEYS env var setting.', + ), + ) + }) + + test(testPrefix + 'secrets scanning, should skip when enhanced scan enabled but no env vars set', async (t) => { + const { requests } = await new Fixture('./fixtures/src_default') + .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.true(requests.length === 0) + }) + + test( + testPrefix + 'secrets scanning, should skip when secrets passed but no non-empty/trivial env vars set', + async (t) => { + const output = await new Fixture('./fixtures/src_scanning_env_vars_set_empty') + .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_,2ENV_VAR_3,ENV_VAR_4,ENV_VAR_5' }) + .runWithBuild() + t.true( + normalizeOutput(output).includes( + 'Secrets scanning skipped because no env vars marked as secret are set to non-empty/non-trivial values or they are all omitted with SECRETS_SCAN_OMIT_KEYS env var setting.', + ), + ) + }, + ) + + test( + testPrefix + 'secrets scanning, should skip when enhanced scan enabled but no non-empty/trivial env vars set', + async (t) => { + const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty') + .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.true(requests.length === 0) + }, + ) + + test( + testPrefix + 'secrets scanning, should skip when secrets passed but SECRETS_SCAN_OMIT_KEYS omits all of them', + async (t) => { + const output = await new Fixture('./fixtures/src_scanning_omit_all_keys') + .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) + .runWithBuild() + t.true(normalizeOutput(output).includes('SECRETS_SCAN_OMIT_KEYS override option set to: ENV_VAR_2,ENV_VAR_1')) + t.true( + normalizeOutput(output).includes( + 'Secrets scanning skipped because no env vars marked as secret are set to non-empty/non-trivial values or they are all omitted with SECRETS_SCAN_OMIT_KEYS env var setting.', + ), + ) + }, + ) + + test( + testPrefix + + 'secrets scanning, should skip when enhanced scan and likely secrets passed but SECRETS_SCAN_OMIT_KEYS omits all of them', + async (t) => { + const { requests } = await new Fixture('./fixtures/src_scanning_omit_all_keys') + .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.true(requests.length === 0) + }, + ) + + test( + testPrefix + 'secrets scanning, should skip when secrets passed but SECRETS_SCAN_OMIT_PATHS omits all files', + async (t) => { + const output = await new Fixture('./fixtures/src_scanning_omit_all_paths') + .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) + .runWithBuild() + t.true(normalizeOutput(output).includes('SECRETS_SCAN_OMIT_PATHS override option set to: /external/path')) + t.true( + normalizeOutput(output).includes( + 'Secrets scanning skipped because there are no files or all files were omitted with SECRETS_SCAN_OMIT_PATHS env var setting.', + ), + ) + }, + ) + + test( + testPrefix + + 'secrets scanning, should skip when enhanced scan and likely secrets passed but SECRETS_SCAN_OMIT_PATHS omits all files', + async (t) => { + const { requests } = await new Fixture('./fixtures/src_scanning_omit_all_paths') + .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.true(requests.length === 0) + }, + ) + + test( + testPrefix + 'secrets scanning, should skip when secrets passed but SECRETS_SCAN_OMIT_PATHS omits globbed files', + async (t) => { + const output = await new Fixture('./fixtures/src_scanning_omit_glob_path') + .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) + .runWithBuild() + + t.false(normalizeOutput(output).includes('found value at line 1 in dist/safefile.js')) + t.false(normalizeOutput(output).includes('found value at line 1 in src/static-files/safefile.js')) + + // Ensure SECRETS_SCAN_OMIT_PATHS doesn't exclude more than the defined glob + t.assert(normalizeOutput(output).includes('found value at line 1 in src/static-files/notsafefile.js')) + }, + ) + + test( + testPrefix + + 'secrets scanning, should fail build and report to API when it finds secrets in the src and build output', + async (t) => { + const { output, requests } = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty') + .withFlags({ + debug: false, + explicitSecretKeys: + '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', + deployId: 'test', + token: 'test', + }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + t.true( + normalizeOutput(output).includes( + 'Scanning complete. 14 file(s) scanned. Secrets scanning found 32 instance(s) of secrets in build output or repo code.', + ), + ) + t.true( + normalizeOutput(output).includes( + `Secret env var "ENV_VAR_1"'s value detected:\n` + + ` found value at line 12 in dist/static-files/static-a.txt\n` + + ` found value at line 6 in netlify.toml\n` + + ` found value at line 12 in src/static-files/static-a.txt\n`, + ), + ) + t.true( + normalizeOutput(output).includes( + `Secret env var "ENV_VAR_2"'s value detected:\n` + + ` found value at line 1 in dist/some-file.txt\n` + + ` found value at line 1 in dist/static-files/static-a.txt\n` + + ` found value at line 6 in dist/static-files/static-a.txt\n` + + ` found value at line 7 in netlify.toml\n` + + ` found value at line 1 in src/some-file.txt\n` + + ` found value at line 1 in src/static-files/static-a.txt\n` + + ` found value at line 6 in src/static-files/static-a.txt\n`, + ), + ) + t.true( + normalizeOutput(output).includes( + `Secret env var "ENV_VAR_3"'s value detected:\n` + + ` found value at line 14 in dist/static-files/static-a.txt\n` + + ` found value at line 16 in dist/static-files/static-a.txt\n` + + ` found value at line 1 in dist/static-files/static-c.txt\n` + + ` found value at line 8 in netlify.toml\n` + + ` found value at line 14 in src/static-files/static-a.txt\n` + + ` found value at line 16 in src/static-files/static-a.txt\n` + + ` found value at line 1 in src/static-files/static-c.txt\n`, + ), + ) + t.true( + normalizeOutput(output).includes( + `Secret env var "ENV_VAR_4"'s value detected:\n` + + ` found value at line 20 in dist/static-files/static-a.txt\n` + + ` found value at line 9 in netlify.toml\n` + + ` found value at line 20 in src/static-files/static-a.txt\n`, + ), + ) + t.true( + normalizeOutput(output).includes( + `Secret env var "ENV_VAR_MULTILINE_A"'s value detected:\n` + + ` found value at line 17 in dist/static-files/static-c.txt\n` + + ` found value at line 38 in dist/static-files/static-c.txt\n` + + ` found value at line 1 in dist/static-files/static-d.txt\n` + + ` found value at line 15 in netlify.toml\n` + + ` found value at line 17 in src/static-files/static-c.txt\n` + + ` found value at line 38 in src/static-files/static-c.txt\n` + + ` found value at line 1 in src/static-files/static-d.txt\n`, + ), + ) + t.true( + normalizeOutput(output).includes( + `Secret env var "ENV_VAR_MULTILINE_B"'s value detected:\n` + + ` found value at line 4 in dist/static-files/static-d.txt\n` + + ` found value at line 1 in dist/static-files/static-e.txt\n` + + ` found value at line 21 in netlify.toml\n` + + ` found value at line 4 in src/static-files/static-d.txt\n` + + ` found value at line 1 in src/static-files/static-e.txt\n`, + ), + ) + + t.true(requests.length === 1) + const request = requests[0] + t.is(request.method, 'PATCH') + t.is(request.url, '/api/v1/deploys/test/validations_report') + t.truthy(request.body.secrets_scan.scannedFilesCount) + t.is(request.body.secrets_scan.secretsScanMatches.length, 32) + t.is(request.body.secrets_scan.enhancedSecretsScanMatches.length, 0) + }, + ) + + test( + testPrefix + + 'secrets scanning, should fail build and report to API when enhanced scan finds likely secret in the src and build output', + async (t) => { + const { output, requests } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets') + .withFlags({ debug: false, explicitSecretKeys: '', enhancedSecretScan: true, deployId: 'test', token: 'test' }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.assert(normalizeOutput(output).includes(`Env var "ENV_VAR_1"'s value detected as a likely secret value`)) + t.assert( + normalizeOutput(output).includes( + `the build will fail until these secret values are not found in build output or repo files`, + ), + ) + t.true(requests.length === 1) + const request = requests[0] + t.is(request.method, 'PATCH') + t.is(request.url, '/api/v1/deploys/test/validations_report') + t.truthy(request.body.secrets_scan.scannedFilesCount) + t.is(request.body.secrets_scan.secretsScanMatches.length, 0) + t.is(request.body.secrets_scan.enhancedSecretsScanMatches.length, 1) + }, + ) + + test(testPrefix + 'secrets scanning should report success to API when no secrets are found', async (t) => { + const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_no_matches') + .withFlags({ + debug: false, + explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2', + deployId: 'test', + token: 'test', + }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.true(requests.length === 1) + const request = requests[0] + t.is(request.method, 'PATCH') + t.is(request.url, '/api/v1/deploys/test/validations_report') + t.truthy(request.body.secrets_scan.scannedFilesCount) + t.truthy(request.body.secrets_scan.secretsScanMatches) + t.truthy(request.body.secrets_scan.enhancedSecretsScanMatches) + }) + + test( + testPrefix + 'secrets scanning, should report success to API when enhanced scans finds no likely secrets', + async (t) => { + const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_no_matches') + .withFlags({ + debug: false, + enhancedSecretScan: true, + deployId: 'test', + token: 'test', + }) + .runBuildServer({ path: '/api/v1/deploys/test/validations_report' }) + + t.true(requests.length === 1) + const request = requests[0] + t.is(request.method, 'PATCH') + t.is(request.url, '/api/v1/deploys/test/validations_report') + t.truthy(request.body.secrets_scan.scannedFilesCount) + t.truthy(request.body.secrets_scan.secretsScanMatches) + t.truthy(request.body.secrets_scan.enhancedSecretsScanMatches) + }, + ) + + test(testPrefix + 'secrets scanning failure should produce an user error', async (t) => { + const { severityCode } = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty') + .withFlags({ + debug: false, + explicitSecretKeys: + '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', + }) + .runBuildProgrammatic() + // Severity code of 2 is user error + t.is(severityCode, 2) + }) + + test(testPrefix + 'secrets scanning, enhanced scanning failure should produce a user error', async (t) => { + const { severityCode } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets') + .withFlags({ + debug: false, + explicitSecretKeys: '', + enhancedSecretScan: true, + }) + .runBuildProgrammatic() + // Severity code of 2 is user error + t.is(severityCode, 2) + }) + + test(testPrefix + 'secrets scan does not send report to API for local builds', async (t) => { + const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty') + .withFlags({ + debug: false, + explicitSecretKeys: + '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', + deployId: '0', + token: 'test', + }) + .runBuildServer({ path: '/api/v1/deploys/0/validations_report' }) + + t.true(requests.length === 0) + }) + + test( + testPrefix + 'secrets scanning, should not fail if the secrets values are not detected in the build output', + async (t) => { + const output = await new Fixture('./fixtures/src_scanning_env_vars_no_matches') + .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) + .runWithBuild() + t.true(output.includes(`No secrets detected in build output or repo code!`)) + }, + ) + + test(testPrefix + 'secrets scanning should not scan .cache/ directory', async (t) => { + const output = await new Fixture('./fixtures/src_scanning_omit_cache_path') + .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) + .runWithBuild() + t.true(output.includes(`No secrets detected in build output or repo code!`)) + }) + + if (featureFlags.secret_scanning_no_readline) { + test(testPrefix + 'does not crash if line in scanned file exceed available memory', async (t) => { + const { output } = await new Fixture('./fixtures/src_scanning_large_binary_file') + .withEnv({ + // fixture produces a ~256MB file with single line, so this intentionally limits available memory + // to check if scanner can process it without crashing + NODE_OPTIONS: '--max-old-space-size=128', + }) + .withFlags({ + debug: false, + defaultConfig: JSON.stringify({ build: { environment: { ENV_SECRET: 'this is a secret' } } }), + explicitSecretKeys: 'ENV_SECRET', + featureFlags, + }) + .runBuildBinary() + + t.assert( + normalizeOutput(output).includes( + `Secret env var "ENV_SECRET"'s value detected:\n` + ` found value at line 1 in dist/out.txt\n`, + ), + 'Scanning should find a secret, instead got: ' + normalizeOutput(output), + ) }) - .runBuildServer({ path: '/api/v1/deploys/0/validations_report' }) - - t.true(requests.length === 0) -}) - -test('secrets scanning, should not fail if the secrets values are not detected in the build output', async (t) => { - const output = await new Fixture('./fixtures/src_scanning_env_vars_no_matches') - .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) - .runWithBuild() - t.true(output.includes(`No secrets detected in build output or repo code!`)) -}) - -test('secrets scanning should not scan .cache/ directory', async (t) => { - const output = await new Fixture('./fixtures/src_scanning_omit_cache_path') - .withFlags({ debug: false, explicitSecretKeys: 'ENV_VAR_1,ENV_VAR_2' }) - .runWithBuild() - t.true(output.includes(`No secrets detected in build output or repo code!`)) -}) + } +}