Skip to content

feat: allow enhanced scan to run without impacting builds #6376

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

53 changes: 30 additions & 23 deletions packages/build/src/log/messages/core_steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,19 @@ export const logSecretsScanSuccessMessage = function (logs, msg) {
log(logs, msg, { color: THEME.highlightWords })
}

export const logSecretsScanFailBuildMessage = function ({ logs, scanResults, groupedResults }) {
export const logSecretsScanFailBuildMessage = function ({
logs,
scanResults,
groupedResults,
enhancedScanShouldRunInActiveMode,
}) {
const { secretMatches, enhancedSecretMatches } = groupedResults
const secretMatchesKeys = Object.keys(secretMatches)
const enhancedSecretMatchesKeys = Object.keys(enhancedSecretMatches)

logErrorSubHeader(
logs,
`Scanning complete. ${scanResults.scannedFilesCount} file(s) scanned. Secrets scanning found ${secretMatchesKeys.length} instance(s) of secrets${enhancedSecretMatchesKeys.length > 0 ? ` and ${enhancedSecretMatchesKeys.length} instance(s) of likely secrets` : ''} in build output or repo code.\n`,
`Scanning complete. ${scanResults.scannedFilesCount} file(s) scanned. Secrets scanning found ${secretMatchesKeys.length} instance(s) of secrets${enhancedSecretMatchesKeys.length > 0 && enhancedScanShouldRunInActiveMode ? ` and ${enhancedSecretMatchesKeys.length} instance(s) of likely secrets` : ''} in build output or repo code.\n`,
)

// Explicit secret matches
Expand All @@ -162,28 +167,30 @@ export const logSecretsScanFailBuildMessage = function ({ logs, scanResults, gro
)
}

// Likely secret matches from enhanced scan
enhancedSecretMatchesKeys.forEach((key, index) => {
logError(logs, `${index === 0 && secretMatchesKeys.length ? '\n' : ''}"${key}***" detected as a likely secret:`)

enhancedSecretMatches[key]
.sort((a, b) => {
return a.file > b.file ? 0 : 1
})
.forEach(({ lineNumber, file }) => {
logError(logs, `found value at line ${lineNumber} in ${file}`, { indent: true })
})
})
if (enhancedScanShouldRunInActiveMode) {
// Likely secret matches from enhanced scan
enhancedSecretMatchesKeys.forEach((key, index) => {
logError(logs, `${index === 0 && secretMatchesKeys.length ? '\n' : ''}"${key}***" detected as a likely secret:`)

enhancedSecretMatches[key]
.sort((a, b) => {
return a.file > b.file ? 0 : 1
})
.forEach(({ lineNumber, file }) => {
logError(logs, `found value at line ${lineNumber} in ${file}`, { indent: true })
})
})

if (enhancedSecretMatchesKeys.length) {
logError(
logs,
`\nTo prevent exposing secrets, the build will fail until these likely secret values are not found in build output or repo files.`,
)
logError(
logs,
`\nIf these are expected, use ENHANCED_SECRETS_SCAN_OMIT_VALUES, or ENHANCED_SECRETS_SCAN_ENABLED to prevent detecting.`,
)
if (enhancedSecretMatchesKeys.length) {
logError(
logs,
`\nTo prevent exposing secrets, the build will fail until these likely secret values are not found in build output or repo files.`,
)
logError(
logs,
`\nIf these are expected, use ENHANCED_SECRETS_SCAN_OMIT_VALUES, or ENHANCED_SECRETS_SCAN_ENABLED to prevent detecting.`,
)
}
}

logError(
Expand Down
33 changes: 25 additions & 8 deletions packages/build/src/plugins_core/secrets_scanning/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ const coreStep: CoreStepFunction = async function ({

const passedSecretKeys = (explicitSecretKeys || '').split(',')
const envVars = netlifyConfig.build.environment as Record<string, unknown>
// When the flag is disabled, we may still run the scan if a secrets scan would otherwise take place anyway
// In this case, we hide any output to the user and simply gather the information in our logs
const enhancedScanShouldRunInActiveMode = featureFlags?.enhanced_secret_scan_impacts_builds ?? false

const useMinimalChunks = featureFlags?.secret_scanning_minimal_chunks

systemLog?.({ passedSecretKeys, buildDir })
Expand All @@ -56,15 +60,17 @@ const coreStep: CoreStepFunction = async function ({
log(logs, `SECRETS_SCAN_OMIT_PATHS override option set to: ${envVars['SECRETS_SCAN_OMIT_PATHS']}\n`)
}
const enhancedScanningEnabledInEnv = isEnhancedSecretsScanningEnabled(envVars)
if (enhancedSecretScan && !enhancedScanningEnabledInEnv) {
const enhancedScanConfigured = enhancedSecretScan && enhancedScanningEnabledInEnv
if (enhancedSecretScan && enhancedScanShouldRunInActiveMode && !enhancedScanningEnabledInEnv) {
logSecretsScanSkipMessage(
logs,
'Enhanced secrets detection disabled via ENHANCED_SECRETS_SCAN_ENABLED flag set to false.',
)
}

if (
enhancedSecretScan &&
enhancedScanningEnabledInEnv &&
enhancedScanShouldRunInActiveMode &&
enhancedScanConfigured &&
envVars['ENHANCED_SECRETS_SCAN_OMIT_VALUES'] !== undefined
) {
log(
Expand All @@ -75,7 +81,11 @@ const coreStep: CoreStepFunction = async function ({

const keysToSearchFor = getSecretKeysToScanFor(envVars, passedSecretKeys)

if (keysToSearchFor.length === 0 && !enhancedSecretScan) {
// In passive mode, only run the enhanced scan if we have explicit secret keys
const enhancedScanShouldRun = enhancedScanShouldRunInActiveMode
? enhancedScanConfigured
: enhancedScanConfigured && keysToSearchFor.length > 0
if (keysToSearchFor.length === 0 && !enhancedScanShouldRun) {
logSecretsScanSkipMessage(
logs,
'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.',
Expand Down Expand Up @@ -109,7 +119,7 @@ const coreStep: CoreStepFunction = async function ({
keys: keysToSearchFor,
base: buildDir as string,
filePaths,
enhancedScanning: enhancedSecretScan && enhancedScanningEnabledInEnv,
enhancedScanning: enhancedScanShouldRun,
omitValuesFromEnhancedScan: getOmitValuesFromEnhancedScanForEnhancedScanFromEnv(envVars),
useMinimalChunks,
})
Expand All @@ -125,7 +135,8 @@ const coreStep: CoreStepFunction = async function ({
secretsFilesCount: scanResults.scannedFilesCount,
keysToSearchFor,
enhancedPrefixMatches: enhancedSecretMatches.length ? enhancedSecretMatches.map((match) => match.key) : [],
enhancedScanning: enhancedSecretScan && enhancedScanningEnabledInEnv,
enhancedScanning: enhancedScanShouldRun,
enhancedScanActiveMode: enhancedScanShouldRunInActiveMode,
}

systemLog?.(attributesForLogsAndSpan)
Expand All @@ -138,12 +149,17 @@ const coreStep: CoreStepFunction = async function ({
const secretScanResult: SecretScanResult = {
scannedFilesCount: scanResults?.scannedFilesCount ?? 0,
secretsScanMatches: secretMatches ?? [],
enhancedSecretsScanMatches: enhancedSecretMatches ?? [],
enhancedSecretsScanMatches:
enhancedScanShouldRunInActiveMode && enhancedSecretMatches ? enhancedSecretMatches : [],
}
reportValidations({ api, secretScanResult, deployId, systemLog })
}

if (!scanResults || scanResults.matches.length === 0) {
if (
!scanResults ||
scanResults.matches.length === 0 ||
(!enhancedScanShouldRunInActiveMode && !secretMatches?.length)
) {
logSecretsScanSuccessMessage(
logs,
`Secrets scanning complete. ${scanResults?.scannedFilesCount} file(s) scanned. No secrets detected in build output or repo code!`,
Expand All @@ -157,6 +173,7 @@ const coreStep: CoreStepFunction = async function ({
logs,
scanResults,
groupedResults: groupScanResultsByKeyAndScanType(scanResults),
enhancedScanShouldRunInActiveMode,
})

const error = new Error(`Secrets scanning found secrets in build.`)
Expand Down
134 changes: 121 additions & 13 deletions packages/build/tests/secrets_scanning/tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,17 @@ for (const { testPrefix, featureFlags } of [
t.true(output.includes(`No secrets detected in build output or repo code!`))
})

// Enhanced secret scanning
// Enhanced secret scanning with enhanced_secret_scan_impacts_builds enabled

test(testPrefix + 'secrets scanning, enhanced scan should not run when disabled', async (t) => {
const { requests } = await new Fixture('./fixtures/src_scanning_disabled')
.withFlags({ debug: false, enhancedSecretScan: true, deployId: 'test', token: 'test', featureFlags })
.withFlags({
debug: false,
enhancedSecretScan: true,
deployId: 'test',
token: 'test',
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })
t.true(requests.length === 0)
})
Expand All @@ -278,7 +284,7 @@ for (const { testPrefix, featureFlags } of [
enhancedSecretScan: true,
deployId: 'test',
token: 'test',
featureFlags,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

Expand All @@ -293,11 +299,11 @@ for (const { testPrefix, featureFlags } of [
const { requests } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets_disabled')
.withFlags({
debug: false,
explicitSecretKeys: '',
explicitSecretKeys: 'ENV_VAR_1',
enhancedSecretScan: true,
deployId: 'test',
token: 'test',
featureFlags,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })
t.true(requests.length === 1)
Expand All @@ -316,7 +322,7 @@ for (const { testPrefix, featureFlags } of [
enhancedSecretScan: true,
deployId: 'test',
token: 'test',
featureFlags,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

Expand All @@ -337,7 +343,7 @@ for (const { testPrefix, featureFlags } of [
enhancedSecretScan: false,
deployId: 'test',
token: 'test',
featureFlags,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

Expand All @@ -353,7 +359,7 @@ for (const { testPrefix, featureFlags } of [
enhancedSecretScan: true,
deployId: 'test',
token: 'test',
featureFlags,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

Expand All @@ -367,7 +373,13 @@ for (const { testPrefix, featureFlags } of [

test(testPrefix + 'secrets scanning, should not find secrets in files without known prefixes', async (t) => {
const { requests } = await new Fixture('./fixtures/src_scanning_no_likely_enhanced_scan_secrets', featureFlags)
.withFlags({ debug: false, enhancedSecretScan: true, deployId: 'test', token: 'test' })
.withFlags({
debug: false,
enhancedSecretScan: true,
deployId: 'test',
token: 'test',
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

t.true(requests.length === 1)
Expand All @@ -389,7 +401,7 @@ for (const { testPrefix, featureFlags } of [
enhancedSecretScan: true,
deployId: 'test',
token: 'test',
featureFlags,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

Expand All @@ -414,7 +426,7 @@ for (const { testPrefix, featureFlags } of [
enhancedSecretScan: true,
deployId: 'test',
token: 'test',
featureFlags,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

Expand Down Expand Up @@ -443,7 +455,7 @@ for (const { testPrefix, featureFlags } of [
enhancedSecretScan: true,
deployId: 'test',
token: 'test',
featureFlags,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

Expand All @@ -463,12 +475,108 @@ for (const { testPrefix, featureFlags } of [
debug: false,
explicitSecretKeys: '',
enhancedSecretScan: true,
featureFlags,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: true },
})
.runBuildProgrammatic()
// Severity code of 2 is user error
t.is(severityCode, 2)
})

// enhanced scanning enabled, but without impact to builds

test(
testPrefix +
'secrets scanning, should not log enhanced scan info when enhanced_secret_scan_impacts_builds is false',
async (t) => {
const { output } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets')
.withFlags({
debug: false,
enhancedSecretScan: true,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: false },
deployId: 'test',
token: 'test',
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

const normalizedOutput = normalizeOutput(output)
t.false(normalizedOutput.includes('detected as a likely secret'))
},
)

test(
testPrefix +
'secrets scanning, should not fail build when enhanced scan finds likely secrets but enhanced_secret_scan_impacts_builds is false',
async (t) => {
const { severityCode } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets')
.withFlags({
debug: false,
enhancedSecretScan: true,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: false },
})
.runBuildProgrammatic()

// Severity code of 0 means success, 2 would be user error
t.is(severityCode, 0)
},
)

test(
testPrefix +
'secrets scanning, should not log omit values message when enhanced_secret_scan_impacts_builds is false',
async (t) => {
const { output } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets_omitted')
.withFlags({
debug: false,
enhancedSecretScan: true,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: false },
deployId: 'test',
token: 'test',
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

t.false(normalizeOutput(output).includes('ENHANCED_SECRETS_SCAN_OMIT_VALUES'))
},
)

test(
testPrefix + 'secrets scanning, should run enhanced scan in passive mode when explicit keys are present',
async (t) => {
const { requests } = await new Fixture('./fixtures/src_scanning_env_vars_set_non_empty')
.withFlags({
debug: false,
explicitSecretKeys: 'ENV_VAR_1',
enhancedSecretScan: true,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: false },
deployId: 'test',
token: 'test',
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

t.true(requests.length === 1)
const request = requests[0]
t.is(request.url, '/api/v1/deploys/test/validations_report')
t.truthy(request.body.secrets_scan.scannedFilesCount)
t.truthy(request.body.secrets_scan.enhancedSecretsScanMatches)
},
)

test(
testPrefix + 'secrets scanning, should not run enhanced scan in passive mode when no explicit keys',
async (t) => {
const { requests } = await new Fixture('./fixtures/src_scanning_likely_enhanced_scan_secrets')
.withFlags({
debug: false,
explicitSecretKeys: '',
enhancedSecretScan: true,
featureFlags: { ...featureFlags, enhanced_secret_scan_impacts_builds: false },
deployId: 'test',
token: 'test',
})
.runBuildServer({ path: '/api/v1/deploys/test/validations_report' })

t.true(requests.length === 0)
},
)
;(featureFlags.secret_scanning_minimal_chunks ? test : test.skip)(
testPrefix + 'does not crash if line in scanned file exceed available memory',
async (t) => {
Expand Down
Loading