@netlify/build 32.1.4 → 32.2.0

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.
@@ -32,7 +32,7 @@ export declare const startBuild: (flags: Partial<BuildFlags>) => {
32
32
  };
33
33
  };
34
34
  export declare const execBuild: any;
35
- export declare const runAndReportBuild: ({ pluginsOptions, netlifyConfig, defaultConfig, configOpts, siteInfo, configPath, outputConfigPath, headersPath, redirectsPath, packagePath, buildDir, repositoryRoot, nodePath, packageJson, userNodeVersion, childEnv, context, branch, buildbotServerSocket, constants, dry, mode, api, token, errorMonitor, deployId, errorParams, logs, debug, systemLog, systemLogFile, verbose, timers, sendStatus, saveConfig, testOpts, featureFlags, timeline, devCommand, quiet, integrations, explicitSecretKeys, edgeFunctionsBootstrapURL, eventHandlers, }: {
35
+ export declare const runAndReportBuild: ({ pluginsOptions, netlifyConfig, defaultConfig, configOpts, siteInfo, configPath, outputConfigPath, headersPath, redirectsPath, packagePath, buildDir, repositoryRoot, nodePath, packageJson, userNodeVersion, childEnv, context, branch, buildbotServerSocket, constants, dry, mode, api, token, errorMonitor, deployId, errorParams, logs, debug, systemLog, systemLogFile, verbose, timers, sendStatus, saveConfig, testOpts, featureFlags, timeline, devCommand, quiet, integrations, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, eventHandlers, }: {
36
36
  pluginsOptions: any;
37
37
  netlifyConfig: any;
38
38
  defaultConfig: any;
@@ -75,6 +75,7 @@ export declare const runAndReportBuild: ({ pluginsOptions, netlifyConfig, defaul
75
75
  quiet: any;
76
76
  integrations: any;
77
77
  explicitSecretKeys: any;
78
+ enhancedSecretScan: any;
78
79
  edgeFunctionsBootstrapURL: any;
79
80
  eventHandlers: any;
80
81
  }) => Promise<{
package/lib/core/build.js CHANGED
@@ -33,7 +33,7 @@ export const startBuild = function (flags) {
33
33
  const errorMonitor = startErrorMonitor({ flags: { debug, systemLogFile, ...flagsA }, logs, bugsnagKey });
34
34
  return { ...flagsA, debug, systemLogFile, errorMonitor, logs, timers };
35
35
  };
36
- const tExecBuild = async function ({ config, defaultConfig, cachedConfig, cachedConfigPath, outputConfigPath, cwd, packagePath, repositoryRoot, apiHost, token, siteId, accountId, context, branch, baseRelDir, env: envOpt, debug, systemLogFile, verbose, nodePath, functionsDistDir, edgeFunctionsDistDir, cacheDir, dry, mode, offline, deployId, buildId, testOpts, errorMonitor, errorParams, logs, timers, buildbotServerSocket, sendStatus, saveConfig, featureFlags, timeline, devCommand, quiet, framework, explicitSecretKeys, edgeFunctionsBootstrapURL, eventHandlers, }) {
36
+ const tExecBuild = async function ({ config, defaultConfig, cachedConfig, cachedConfigPath, outputConfigPath, cwd, packagePath, repositoryRoot, apiHost, token, siteId, accountId, context, branch, baseRelDir, env: envOpt, debug, systemLogFile, verbose, nodePath, functionsDistDir, edgeFunctionsDistDir, cacheDir, dry, mode, offline, deployId, buildId, testOpts, errorMonitor, errorParams, logs, timers, buildbotServerSocket, sendStatus, saveConfig, featureFlags, timeline, devCommand, quiet, framework, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, eventHandlers, }) {
37
37
  const configOpts = getConfigOpts({
38
38
  config,
39
39
  defaultConfig,
@@ -139,6 +139,7 @@ const tExecBuild = async function ({ config, defaultConfig, cachedConfig, cached
139
139
  quiet,
140
140
  integrations,
141
141
  explicitSecretKeys,
142
+ enhancedSecretScan,
142
143
  edgeFunctionsBootstrapURL,
143
144
  eventHandlers,
144
145
  });
@@ -155,7 +156,7 @@ const tExecBuild = async function ({ config, defaultConfig, cachedConfig, cached
155
156
  };
156
157
  export const execBuild = measureDuration(tExecBuild, 'total', { parentTag: 'build_site' });
157
158
  // Runs a build then report any plugin statuses
158
- export const runAndReportBuild = async function ({ pluginsOptions, netlifyConfig, defaultConfig, configOpts, siteInfo, configPath, outputConfigPath, headersPath, redirectsPath, packagePath, buildDir, repositoryRoot, nodePath, packageJson, userNodeVersion, childEnv, context, branch, buildbotServerSocket, constants, dry, mode, api, token, errorMonitor, deployId, errorParams, logs, debug, systemLog, systemLogFile, verbose, timers, sendStatus, saveConfig, testOpts, featureFlags, timeline, devCommand, quiet, integrations, explicitSecretKeys, edgeFunctionsBootstrapURL, eventHandlers, }) {
159
+ export const runAndReportBuild = async function ({ pluginsOptions, netlifyConfig, defaultConfig, configOpts, siteInfo, configPath, outputConfigPath, headersPath, redirectsPath, packagePath, buildDir, repositoryRoot, nodePath, packageJson, userNodeVersion, childEnv, context, branch, buildbotServerSocket, constants, dry, mode, api, token, errorMonitor, deployId, errorParams, logs, debug, systemLog, systemLogFile, verbose, timers, sendStatus, saveConfig, testOpts, featureFlags, timeline, devCommand, quiet, integrations, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, eventHandlers, }) {
159
160
  try {
160
161
  const { stepsCount, netlifyConfig: netlifyConfigA, statuses, pluginsOptions: pluginsOptionsA, failedPlugins, timers: timersA, configMutations, metrics, } = await initAndRunBuild({
161
162
  pluginsOptions,
@@ -200,6 +201,7 @@ export const runAndReportBuild = async function ({ pluginsOptions, netlifyConfig
200
201
  quiet,
201
202
  integrations,
202
203
  explicitSecretKeys,
204
+ enhancedSecretScan,
203
205
  edgeFunctionsBootstrapURL,
204
206
  eventHandlers,
205
207
  });
@@ -262,7 +264,7 @@ export const runAndReportBuild = async function ({ pluginsOptions, netlifyConfig
262
264
  }
263
265
  };
264
266
  // Initialize plugin processes then runs a build
265
- const initAndRunBuild = async function ({ pluginsOptions, netlifyConfig, defaultConfig, configOpts, siteInfo, configPath, outputConfigPath, headersPath, redirectsPath, buildDir, packagePath, repositoryRoot, nodePath, packageJson, userNodeVersion, childEnv, context, branch, dry, mode, api, token, errorMonitor, deployId, errorParams, logs, debug, systemLog, systemLogFile, verbose, sendStatus, saveConfig, timers, testOpts, buildbotServerSocket, constants, featureFlags, timeline, devCommand, quiet, integrations, explicitSecretKeys, edgeFunctionsBootstrapURL, eventHandlers, }) {
267
+ const initAndRunBuild = async function ({ pluginsOptions, netlifyConfig, defaultConfig, configOpts, siteInfo, configPath, outputConfigPath, headersPath, redirectsPath, buildDir, packagePath, repositoryRoot, nodePath, packageJson, userNodeVersion, childEnv, context, branch, dry, mode, api, token, errorMonitor, deployId, errorParams, logs, debug, systemLog, systemLogFile, verbose, sendStatus, saveConfig, timers, testOpts, buildbotServerSocket, constants, featureFlags, timeline, devCommand, quiet, integrations, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, eventHandlers, }) {
266
268
  const pluginsEnv = {
267
269
  ...childEnv,
268
270
  ...getBlobsEnvironmentContext({ api, deployId: deployId, siteId: siteInfo?.id, token }),
@@ -351,6 +353,7 @@ const initAndRunBuild = async function ({ pluginsOptions, netlifyConfig, default
351
353
  devCommand,
352
354
  quiet,
353
355
  explicitSecretKeys,
356
+ enhancedSecretScan,
354
357
  edgeFunctionsBootstrapURL,
355
358
  eventHandlers,
356
359
  });
@@ -386,7 +389,7 @@ const initAndRunBuild = async function ({ pluginsOptions, netlifyConfig, default
386
389
  };
387
390
  // Load plugin main files, retrieve their event handlers then runs them,
388
391
  // together with the build command
389
- const runBuild = async function ({ childProcesses, pluginsOptions, netlifyConfig, defaultConfig, configOpts, packageJson, configPath, outputConfigPath, userNodeVersion, headersPath, redirectsPath, buildDir, repositoryRoot, packagePath, nodePath, childEnv, context, branch, dry, buildbotServerSocket, constants, mode, api, errorMonitor, deployId, errorParams, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, timeline, devCommand, quiet, explicitSecretKeys, edgeFunctionsBootstrapURL, eventHandlers, }) {
392
+ const runBuild = async function ({ childProcesses, pluginsOptions, netlifyConfig, defaultConfig, configOpts, packageJson, configPath, outputConfigPath, userNodeVersion, headersPath, redirectsPath, buildDir, repositoryRoot, packagePath, nodePath, childEnv, context, branch, dry, buildbotServerSocket, constants, mode, api, errorMonitor, deployId, errorParams, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, timeline, devCommand, quiet, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, eventHandlers, }) {
390
393
  const { pluginsSteps, timers: timersA } = await loadPlugins({
391
394
  pluginsOptions,
392
395
  childProcesses,
@@ -439,6 +442,7 @@ const runBuild = async function ({ childProcesses, pluginsOptions, netlifyConfig
439
442
  quiet,
440
443
  userNodeVersion,
441
444
  explicitSecretKeys,
445
+ enhancedSecretScan,
442
446
  edgeFunctionsBootstrapURL,
443
447
  });
444
448
  return {
@@ -182,4 +182,9 @@ export const FLAGS: {
182
182
  describe: string;
183
183
  hidden: boolean;
184
184
  };
185
+ enhancedSecretScan: {
186
+ boolean: boolean;
187
+ hidden: boolean;
188
+ describe: string;
189
+ };
185
190
  };
package/lib/core/flags.js CHANGED
@@ -216,4 +216,9 @@ Default: false`,
216
216
  describe: 'Env var keys that are marked as secret explicitly.',
217
217
  hidden: true,
218
218
  },
219
+ enhancedSecretScan: {
220
+ boolean: true,
221
+ hidden: true,
222
+ describe: 'Scan for potential secrets in all env vars',
223
+ },
219
224
  };
@@ -51,6 +51,7 @@ const INTERNAL_FLAGS = [
51
51
  'systemLogFile',
52
52
  'timeline',
53
53
  'explicitSecretKeys',
54
+ 'enhancedSecretScan',
54
55
  'edgeFunctionsBootstrapURL',
55
56
  'eventHandlers',
56
57
  ];
@@ -94,10 +94,23 @@ export const logSecretsScanSuccessMessage = function (logs, msg) {
94
94
  log(logs, msg, { color: THEME.highlightWords });
95
95
  };
96
96
  export const logSecretsScanFailBuildMessage = function ({ logs, scanResults, groupedResults }) {
97
+ const { secretMatches, enhancedSecretMatches } = groupedResults;
97
98
  logErrorSubHeader(logs, `Scanning complete. ${scanResults.scannedFilesCount} file(s) scanned. Secrets scanning found ${scanResults.matches.length} instance(s) of secrets in build output or repo code.\n`);
98
- Object.keys(groupedResults).forEach((key) => {
99
+ // Explicit secret matches
100
+ Object.keys(secretMatches).forEach((key) => {
99
101
  logError(logs, `Secret env var "${key}"'s value detected:`);
100
- groupedResults[key]
102
+ secretMatches[key]
103
+ .sort((a, b) => {
104
+ return a.file > b.file ? 0 : 1;
105
+ })
106
+ .forEach(({ lineNumber, file }) => {
107
+ logError(logs, `found value at line ${lineNumber} in ${file}`, { indent: true });
108
+ });
109
+ });
110
+ // Likely secret matches from enhanced scan
111
+ Object.keys(enhancedSecretMatches).forEach((key) => {
112
+ logError(logs, `Env var "${key}"'s value detected as a likely secret value:`);
113
+ enhancedSecretMatches[key]
101
114
  .sort((a, b) => {
102
115
  return a.file > b.file ? 0 : 1;
103
116
  })
@@ -3,9 +3,9 @@ import { addErrorInfo } from '../../error/info.js';
3
3
  import { log } from '../../log/logger.js';
4
4
  import { logSecretsScanFailBuildMessage, logSecretsScanSkipMessage, logSecretsScanSuccessMessage, } from '../../log/messages/core_steps.js';
5
5
  import { reportValidations } from '../../status/validations.js';
6
- import { getFilePathsToScan, getSecretKeysToScanFor, groupScanResultsByKey, isSecretsScanningEnabled, scanFilesForKeyValues, } from './utils.js';
6
+ import { getFilePathsToScan, getNonSecretKeysToScanFor, getSecretKeysToScanFor, groupScanResultsByKeyAndScanType, isSecretsScanningEnabled, scanFilesForKeyValues, } from './utils.js';
7
7
  const tracer = trace.getTracer('secrets-scanning');
8
- const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecretKeys, systemLog, deployId, api, }) {
8
+ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecretKeys, enhancedSecretScan, systemLog, deployId, api, }) {
9
9
  const stepResults = {};
10
10
  const passedSecretKeys = (explicitSecretKeys || '').split(',');
11
11
  const envVars = netlifyConfig.build.environment;
@@ -21,9 +21,14 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
21
21
  if (envVars['SECRETS_SCAN_OMIT_PATHS'] !== undefined) {
22
22
  log(logs, `SECRETS_SCAN_OMIT_PATHS override option set to: ${envVars['SECRETS_SCAN_OMIT_PATHS']}\n`);
23
23
  }
24
- const keysToSearchFor = getSecretKeysToScanFor(envVars, passedSecretKeys);
24
+ const explicitSecretKeysToScanFor = getSecretKeysToScanFor(envVars, passedSecretKeys);
25
+ const potentialSecretKeysToScanFor = enhancedSecretScan ? getNonSecretKeysToScanFor(envVars, passedSecretKeys) : [];
26
+ const keysToSearchFor = explicitSecretKeysToScanFor.concat(potentialSecretKeysToScanFor);
25
27
  if (keysToSearchFor.length === 0) {
26
- 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.');
28
+ const msg = enhancedSecretScan
29
+ ? 'Secrets scanning skipped because no env vars are set to non-empty/non-trivial values or they are all omitted with SECRETS_SCAN_OMIT_KEYS env var setting.'
30
+ : '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.';
31
+ logSecretsScanSkipMessage(logs, msg);
27
32
  return stepResults;
28
33
  }
29
34
  // buildDir is the repository root or the base folder
@@ -35,6 +40,8 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
35
40
  return stepResults;
36
41
  }
37
42
  let scanResults;
43
+ let secretMatches;
44
+ let enhancedSecretMatches;
38
45
  await tracer.startActiveSpan('scanning-files', { attributes: { keysToSearchFor, totalFiles: filePaths.length } }, async (span) => {
39
46
  scanResults = await scanFilesForKeyValues({
40
47
  env: envVars,
@@ -42,9 +49,13 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
42
49
  base: buildDir,
43
50
  filePaths,
44
51
  });
52
+ secretMatches = scanResults.matches.filter((match) => explicitSecretKeysToScanFor.includes(match.key));
53
+ enhancedSecretMatches = scanResults.matches.filter((match) => potentialSecretKeysToScanFor.includes(match.key));
45
54
  const attributesForLogsAndSpan = {
46
- secretsScanFoundSecrets: scanResults.matches.length > 0,
47
- secretsScanMatchesCount: scanResults.matches.length,
55
+ secretsScanFoundSecrets: secretMatches.length > 0,
56
+ enhancedSecretsScanFoundSecrets: enhancedSecretMatches.length > 0,
57
+ secretsScanMatchesCount: secretMatches.length,
58
+ enhancedSecretsScanMatchesCount: enhancedSecretMatches.length,
48
59
  secretsFilesCount: scanResults.scannedFilesCount,
49
60
  keysToSearchFor,
50
61
  };
@@ -55,7 +66,8 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
55
66
  if (deployId !== '0') {
56
67
  const secretScanResult = {
57
68
  scannedFilesCount: scanResults?.scannedFilesCount ?? 0,
58
- secretsScanMatches: scanResults?.matches ?? [],
69
+ secretsScanMatches: secretMatches ?? [],
70
+ enhancedSecretsScanMatches: enhancedSecretMatches ?? [],
59
71
  };
60
72
  reportValidations({ api, secretScanResult, deployId, systemLog });
61
73
  }
@@ -65,15 +77,22 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
65
77
  }
66
78
  // at this point we have found matching secrets
67
79
  // Output the results and fail the build
68
- logSecretsScanFailBuildMessage({ logs, scanResults, groupedResults: groupScanResultsByKey(scanResults) });
80
+ logSecretsScanFailBuildMessage({
81
+ logs,
82
+ scanResults,
83
+ groupedResults: groupScanResultsByKeyAndScanType(scanResults, potentialSecretKeysToScanFor),
84
+ });
69
85
  const error = new Error(`Secrets scanning found secrets in build.`);
70
86
  addErrorInfo(error, { type: 'secretScanningFoundSecrets' });
71
87
  throw error;
72
88
  };
73
- // We run this core step if the build was run with explicit secret keys. This
74
- // is passed from BB to build so only accounts that are allowed to have explicit
75
- // secrets and actually have them will have them.
76
- const hasExplicitSecretsKeys = function ({ explicitSecretKeys }) {
89
+ // We run this core step if the build was run with explicit secret keys or if enhanced secret scanning is enabled.
90
+ // This is passed from BB to build so only accounts that are allowed to have explicit
91
+ // secrets and actually have them / have enhanced secret scanning enabled will have them.
92
+ const hasExplicitSecretsKeysOrEnhancedScanningEnabled = function ({ explicitSecretKeys, enhancedSecretScan, }) {
93
+ if (enhancedSecretScan) {
94
+ return true;
95
+ }
77
96
  if (typeof explicitSecretKeys !== 'string') {
78
97
  return false;
79
98
  }
@@ -85,5 +104,5 @@ export const scanForSecrets = {
85
104
  coreStepId: 'secrets_scanning',
86
105
  coreStepName: 'Secrets scanning',
87
106
  coreStepDescription: () => 'Scanning for secrets in code and build output.',
88
- condition: hasExplicitSecretsKeys,
107
+ condition: hasExplicitSecretsKeysOrEnhancedScanningEnabled,
89
108
  };
@@ -16,6 +16,7 @@ interface MatchResult {
16
16
  export type SecretScanResult = {
17
17
  scannedFilesCount: number;
18
18
  secretsScanMatches: MatchResult[];
19
+ enhancedSecretsScanMatches: MatchResult[];
19
20
  };
20
21
  /**
21
22
  * Determine if the user disabled scanning via env var
@@ -36,6 +37,16 @@ export declare function isSecretsScanningEnabled(env: Record<string, unknown>):
36
37
  * @returns string[]
37
38
  */
38
39
  export declare function getSecretKeysToScanFor(env: Record<string, unknown>, secretKeys: string[]): string[];
40
+ /**
41
+ * given the explicit secret keys and env vars, return the list of non-secret keys which should be scanned in the enhanced secret scan
42
+ * (i.e. any that look very likely to be secrets).
43
+ * This will also filter out keys passed in the SECRETS_SCAN_OMIT_KEYS env var.
44
+ *
45
+ * @param env env vars list
46
+ * @param secretKeys
47
+ * @returns string[]
48
+ */
49
+ export declare function getNonSecretKeysToScanFor(env: Record<string, unknown>, secretKeys: string[]): string[];
39
50
  /**
40
51
  * Given the env and base directory, find all file paths to scan. It will look at the
41
52
  * env vars to decide if it should omit certain paths.
@@ -59,13 +70,21 @@ export declare function getFilePathsToScan({ env, base }: {
59
70
  export declare function scanFilesForKeyValues({ env, keys, filePaths, base }: ScanArgs): Promise<ScanResults>;
60
71
  /**
61
72
  * ScanResults are all of the finds for all keys and their disparate locations. Scanning is
62
- * async in streams so order can change a lot. This function groups the results into an object
63
- * where the keys are the env var keys and the values are all match results for that key
73
+ * async in streams so order can change a lot. Some matches are the result of an env var explictly being marked as secret,
74
+ * while others are part of the enhanced secret scan.
75
+ *
76
+ * This function groups the results into an object where the results are separate into the secretMatches and enhancedSecretMatches,
77
+ * their value being an object where the keys are the env var keys and the values are all match results for that key.
64
78
  *
65
79
  * @param scanResults
66
80
  * @returns
67
81
  */
68
- export declare function groupScanResultsByKey(scanResults: ScanResults): {
69
- [key: string]: MatchResult[];
82
+ export declare function groupScanResultsByKeyAndScanType(scanResults: ScanResults, enhancedScanKeys?: string[]): {
83
+ secretMatches: {
84
+ [key: string]: MatchResult[];
85
+ };
86
+ enhancedSecretMatches: {
87
+ [key: string]: MatchResult[];
88
+ };
70
89
  };
71
90
  export {};
@@ -14,6 +14,40 @@ export function isSecretsScanningEnabled(env) {
14
14
  }
15
15
  return true;
16
16
  }
17
+ function filterOmittedKeys(env, envKeys = []) {
18
+ if (typeof env.SECRETS_SCAN_OMIT_KEYS !== 'string') {
19
+ return envKeys;
20
+ }
21
+ const omitKeys = env.SECRETS_SCAN_OMIT_KEYS.split(',')
22
+ .map((s) => s.trim())
23
+ .filter(Boolean);
24
+ return envKeys.filter((key) => !omitKeys.includes(key));
25
+ }
26
+ /**
27
+ * Trivial values are values that are:
28
+ * - empty or short strings
29
+ * - string forms of booleans
30
+ * - booleans
31
+ * - numbers or objects with fewer than 4 chars
32
+ */
33
+ function isValueTrivial(val) {
34
+ if (typeof val === 'string') {
35
+ // string forms of booleans
36
+ if (val === 'true' || val === 'false') {
37
+ return true;
38
+ }
39
+ // trivial values are empty or short strings
40
+ return val.trim().length < 4;
41
+ }
42
+ if (typeof val === 'boolean') {
43
+ // booleans are always considered trivial
44
+ return true;
45
+ }
46
+ if (typeof val === 'number' || typeof val === 'object') {
47
+ return JSON.stringify(val).length < 4;
48
+ }
49
+ return !val;
50
+ }
17
51
  /**
18
52
  * given the explicit secret keys and env vars, return the list of secret keys which have non-empty or non-trivial values. This
19
53
  * will also filter out keys passed in the SECRETS_SCAN_OMIT_KEYS env var.
@@ -27,34 +61,52 @@ export function isSecretsScanningEnabled(env) {
27
61
  * @returns string[]
28
62
  */
29
63
  export function getSecretKeysToScanFor(env, secretKeys) {
30
- let omitKeys = [];
31
- if (typeof env.SECRETS_SCAN_OMIT_KEYS === 'string') {
32
- omitKeys = env.SECRETS_SCAN_OMIT_KEYS.split(',')
33
- .map((s) => s.trim())
34
- .filter(Boolean);
35
- }
36
- return secretKeys.filter((key) => {
37
- if (omitKeys.includes(key)) {
38
- return false;
39
- }
64
+ const filteredSecretKeys = filterOmittedKeys(env, secretKeys);
65
+ return filteredSecretKeys.filter((key) => !isValueTrivial(env[key]));
66
+ }
67
+ /**
68
+ * given the explicit secret keys and env vars, return the list of non-secret keys which should be scanned in the enhanced secret scan
69
+ * (i.e. any that look very likely to be secrets).
70
+ * This will also filter out keys passed in the SECRETS_SCAN_OMIT_KEYS env var.
71
+ *
72
+ * @param env env vars list
73
+ * @param secretKeys
74
+ * @returns string[]
75
+ */
76
+ export function getNonSecretKeysToScanFor(env, secretKeys) {
77
+ const nonSecretKeys = Object.keys(env).filter((key) => !secretKeys.includes(key));
78
+ const filteredNonSecretKeys = filterOmittedKeys(env, nonSecretKeys);
79
+ const nonSecretKeysToScanFor = filteredNonSecretKeys.filter((key) => {
40
80
  const val = env[key];
41
- if (typeof val === 'string') {
42
- // string forms of booleans
43
- if (val === 'true' || val === 'false') {
44
- return false;
45
- }
46
- // non-trivial/non-empty values only
47
- return val.trim().length > 4;
48
- }
49
- else if (typeof val === 'boolean') {
50
- // booleans are trivial values
81
+ if (isValueTrivial(val)) {
51
82
  return false;
52
83
  }
53
- else if (typeof val === 'number' || typeof val === 'object') {
54
- return JSON.stringify(val).length > 4;
55
- }
56
- return !!val;
84
+ return isLikelySecretValue(val);
57
85
  });
86
+ return nonSecretKeysToScanFor;
87
+ }
88
+ const AWS_PREFIXES = ['aws_', 'asia'];
89
+ const SLACK_PREFIXES = ['xoxb-', 'xwfp-', 'xoxb-', 'xoxp-', 'xapp-'];
90
+ const LIKELY_SECRET_PREFIXES = ['pk_', 'sk_', 'pat_', 'db_', 'github_pat_', ...AWS_PREFIXES, ...SLACK_PREFIXES];
91
+ const LIKELY_SECRET_MIN_LENGTH = 16;
92
+ /**
93
+ * When the enhanced secret scan is run, we check any env vars _not_ marked as secret if they are highly likely to be secret values.
94
+ * For now, this means the value is a string of at least 16 chars and starts with one of the known prefixes.
95
+ *
96
+ * @param val env var value
97
+ * @returns boolean
98
+ */
99
+ function isLikelySecretValue(val) {
100
+ if (typeof val !== 'string') {
101
+ return false;
102
+ }
103
+ if (val.length < LIKELY_SECRET_MIN_LENGTH) {
104
+ return false;
105
+ }
106
+ if (LIKELY_SECRET_PREFIXES.some((prefix) => val.toLowerCase().startsWith(prefix))) {
107
+ return true;
108
+ }
109
+ return false;
58
110
  }
59
111
  /**
60
112
  * Given the env and base directory, find all file paths to scan. It will look at the
@@ -262,41 +314,58 @@ const searchStream = (basePath, file, keyValues) => {
262
314
  };
263
315
  /**
264
316
  * ScanResults are all of the finds for all keys and their disparate locations. Scanning is
265
- * async in streams so order can change a lot. This function groups the results into an object
266
- * where the keys are the env var keys and the values are all match results for that key
317
+ * async in streams so order can change a lot. Some matches are the result of an env var explictly being marked as secret,
318
+ * while others are part of the enhanced secret scan.
319
+ *
320
+ * This function groups the results into an object where the results are separate into the secretMatches and enhancedSecretMatches,
321
+ * their value being an object where the keys are the env var keys and the values are all match results for that key.
267
322
  *
268
323
  * @param scanResults
269
324
  * @returns
270
325
  */
271
- export function groupScanResultsByKey(scanResults) {
272
- const matchesByKeys = {};
326
+ export function groupScanResultsByKeyAndScanType(scanResults, enhancedScanKeys = []) {
327
+ const secretMatchesByKeys = {};
328
+ const enhancedSecretMatchesByKeys = {};
273
329
  scanResults.matches.forEach((matchResult) => {
274
- if (!matchesByKeys[matchResult.key]) {
275
- matchesByKeys[matchResult.key] = [];
330
+ const isEnhancedCheck = enhancedScanKeys.includes(matchResult.key);
331
+ if (isEnhancedCheck) {
332
+ if (!enhancedSecretMatchesByKeys[matchResult.key]) {
333
+ enhancedSecretMatchesByKeys[matchResult.key] = [];
334
+ }
335
+ enhancedSecretMatchesByKeys[matchResult.key].push(matchResult);
336
+ }
337
+ else {
338
+ if (!secretMatchesByKeys[matchResult.key]) {
339
+ secretMatchesByKeys[matchResult.key] = [];
340
+ }
341
+ secretMatchesByKeys[matchResult.key].push(matchResult);
276
342
  }
277
- matchesByKeys[matchResult.key].push(matchResult);
278
343
  });
279
344
  // sort results to get a consistent output and logically ordered match results
280
- Object.keys(matchesByKeys).forEach((key) => {
281
- matchesByKeys[key].sort((a, b) => {
282
- // sort by file name first
283
- if (a.file > b.file) {
284
- return 1;
285
- }
286
- // sort by line number second
287
- if (a.file === b.file) {
288
- if (a.lineNumber > b.lineNumber) {
345
+ const sortMatches = (matchesByKeys) => {
346
+ Object.keys(matchesByKeys).forEach((key) => {
347
+ matchesByKeys[key].sort((a, b) => {
348
+ // sort by file name first
349
+ if (a.file > b.file) {
289
350
  return 1;
290
351
  }
291
- if (a.lineNumber === b.lineNumber) {
292
- return 0;
352
+ // sort by line number second
353
+ if (a.file === b.file) {
354
+ if (a.lineNumber > b.lineNumber) {
355
+ return 1;
356
+ }
357
+ if (a.lineNumber === b.lineNumber) {
358
+ return 0;
359
+ }
360
+ return -1;
293
361
  }
294
362
  return -1;
295
- }
296
- return -1;
363
+ });
297
364
  });
298
- });
299
- return matchesByKeys;
365
+ };
366
+ sortMatches(secretMatchesByKeys);
367
+ sortMatches(enhancedSecretMatchesByKeys);
368
+ return { secretMatches: secretMatchesByKeys, enhancedSecretMatches: enhancedSecretMatchesByKeys };
300
369
  }
301
370
  function isMultiLineVal(v) {
302
371
  return typeof v === 'string' && v.includes('\n');
@@ -25,6 +25,7 @@ export type CoreStepFunctionArgs = {
25
25
  featureFlags?: Record<string, any>;
26
26
  netlifyConfig: NetlifyConfig;
27
27
  explicitSecretKeys: $TSFixme;
28
+ enhancedSecretScan: boolean;
28
29
  buildbotServerSocket: $TSFixme;
29
30
  api: DynamicMethods;
30
31
  };
@@ -1,4 +1,4 @@
1
- export declare const fireCoreStep: ({ coreStep, coreStepId, coreStepName, configPath, outputConfigPath, buildDir, repositoryRoot, packagePath, constants, buildbotServerSocket, events, logs, quiet, nodePath, childEnv, context, branch, envChanges, errorParams, configOpts, netlifyConfig, defaultConfig, configMutations, headersPath, redirectsPath, featureFlags, debug, systemLog, saveConfig, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, deployId, outputFlusher, api, }: {
1
+ export declare const fireCoreStep: ({ coreStep, coreStepId, coreStepName, configPath, outputConfigPath, buildDir, repositoryRoot, packagePath, constants, buildbotServerSocket, events, logs, quiet, nodePath, childEnv, context, branch, envChanges, errorParams, configOpts, netlifyConfig, defaultConfig, configMutations, headersPath, redirectsPath, featureFlags, debug, systemLog, saveConfig, userNodeVersion, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, deployId, outputFlusher, api, }: {
2
2
  coreStep: any;
3
3
  coreStepId: any;
4
4
  coreStepName: any;
@@ -30,6 +30,7 @@ export declare const fireCoreStep: ({ coreStep, coreStepId, coreStepName, config
30
30
  saveConfig: any;
31
31
  userNodeVersion: any;
32
32
  explicitSecretKeys: any;
33
+ enhancedSecretScan: any;
33
34
  edgeFunctionsBootstrapURL: any;
34
35
  deployId: any;
35
36
  outputFlusher: any;
@@ -3,7 +3,7 @@ import { addErrorInfo, isBuildError } from '../error/info.js';
3
3
  import { addOutputFlusher } from '../log/logger.js';
4
4
  import { updateNetlifyConfig, listConfigSideFiles } from './update_config.js';
5
5
  // Fire a core step
6
- export const fireCoreStep = async function ({ coreStep, coreStepId, coreStepName, configPath, outputConfigPath, buildDir, repositoryRoot, packagePath, constants, buildbotServerSocket, events, logs, quiet, nodePath, childEnv, context, branch, envChanges, errorParams, configOpts, netlifyConfig, defaultConfig, configMutations, headersPath, redirectsPath, featureFlags, debug, systemLog, saveConfig, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, deployId, outputFlusher, api, }) {
6
+ export const fireCoreStep = async function ({ coreStep, coreStepId, coreStepName, configPath, outputConfigPath, buildDir, repositoryRoot, packagePath, constants, buildbotServerSocket, events, logs, quiet, nodePath, childEnv, context, branch, envChanges, errorParams, configOpts, netlifyConfig, defaultConfig, configMutations, headersPath, redirectsPath, featureFlags, debug, systemLog, saveConfig, userNodeVersion, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, deployId, outputFlusher, api, }) {
7
7
  const logsA = outputFlusher ? addOutputFlusher(logs, outputFlusher) : logs;
8
8
  try {
9
9
  const configSideFiles = await listConfigSideFiles([headersPath, redirectsPath]);
@@ -35,6 +35,7 @@ export const fireCoreStep = async function ({ coreStep, coreStepId, coreStepName
35
35
  saveConfig,
36
36
  userNodeVersion,
37
37
  explicitSecretKeys,
38
+ enhancedSecretScan,
38
39
  edgeFunctionsBootstrapURL,
39
40
  deployId,
40
41
  });
@@ -1,4 +1,4 @@
1
- export declare const runStep: ({ event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, coreStepQuiet, pluginPackageJson, loadedFrom, origin, condition, configPath, outputConfigPath, buildDir, packagePath, repositoryRoot, nodePath, index, childEnv, context, branch, envChanges, constants, steps, buildbotServerSocket, events, mode, api, errorMonitor, deployId, errorParams, error, failedPlugins, configOpts, netlifyConfig, defaultConfig, configMutations, headersPath, redirectsPath, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, extensionMetadata, }: {
1
+ export declare const runStep: ({ event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, coreStepQuiet, pluginPackageJson, loadedFrom, origin, condition, configPath, outputConfigPath, buildDir, packagePath, repositoryRoot, nodePath, index, childEnv, context, branch, envChanges, constants, steps, buildbotServerSocket, events, mode, api, errorMonitor, deployId, errorParams, error, failedPlugins, configOpts, netlifyConfig, defaultConfig, configMutations, headersPath, redirectsPath, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, extensionMetadata, }: {
2
2
  event: any;
3
3
  childProcess: any;
4
4
  packageName: any;
@@ -50,6 +50,7 @@ export declare const runStep: ({ event, childProcess, packageName, coreStep, cor
50
50
  quiet: any;
51
51
  userNodeVersion: any;
52
52
  explicitSecretKeys: any;
53
+ enhancedSecretScan: any;
53
54
  edgeFunctionsBootstrapURL: any;
54
55
  extensionMetadata: any;
55
56
  }) => Promise<{}>;
@@ -11,7 +11,7 @@ import { firePluginStep } from './plugin.js';
11
11
  import { getStepReturn } from './return.js';
12
12
  const tracer = trace.getTracer('steps');
13
13
  // Run a step (core, build command or plugin)
14
- export const runStep = async function ({ event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, coreStepQuiet, pluginPackageJson, loadedFrom, origin, condition, configPath, outputConfigPath, buildDir, packagePath, repositoryRoot, nodePath, index, childEnv, context, branch, envChanges, constants, steps, buildbotServerSocket, events, mode, api, errorMonitor, deployId, errorParams, error, failedPlugins, configOpts, netlifyConfig, defaultConfig, configMutations, headersPath, redirectsPath, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, extensionMetadata, }) {
14
+ export const runStep = async function ({ event, childProcess, packageName, coreStep, coreStepId, coreStepName, coreStepDescription, coreStepQuiet, pluginPackageJson, loadedFrom, origin, condition, configPath, outputConfigPath, buildDir, packagePath, repositoryRoot, nodePath, index, childEnv, context, branch, envChanges, constants, steps, buildbotServerSocket, events, mode, api, errorMonitor, deployId, errorParams, error, failedPlugins, configOpts, netlifyConfig, defaultConfig, configMutations, headersPath, redirectsPath, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, extensionMetadata, }) {
15
15
  // Add relevant attributes to the upcoming span context
16
16
  const attributes = {
17
17
  'build.execution.step.name': coreStepName,
@@ -45,6 +45,7 @@ export const runStep = async function ({ event, childProcess, packageName, coreS
45
45
  buildDir,
46
46
  saveConfig,
47
47
  explicitSecretKeys,
48
+ enhancedSecretScan,
48
49
  deployId,
49
50
  featureFlags,
50
51
  });
@@ -104,6 +105,7 @@ export const runStep = async function ({ event, childProcess, packageName, coreS
104
105
  featureFlags,
105
106
  userNodeVersion,
106
107
  explicitSecretKeys,
108
+ enhancedSecretScan,
107
109
  edgeFunctionsBootstrapURL,
108
110
  deployId,
109
111
  api,
@@ -170,7 +172,7 @@ export const runStep = async function ({ event, childProcess, packageName, coreS
170
172
  // or available. However, one might be created by a build plugin, in which case,
171
173
  // those core plugins should be triggered. We use a dynamic `condition()` to
172
174
  // model this behavior.
173
- const shouldRunStep = async function ({ event, packageName, error, packagePath, failedPlugins, netlifyConfig, condition, constants, buildbotServerSocket, buildDir, saveConfig, explicitSecretKeys, deployId, featureFlags = {}, }) {
175
+ const shouldRunStep = async function ({ event, packageName, error, packagePath, failedPlugins, netlifyConfig, condition, constants, buildbotServerSocket, buildDir, saveConfig, explicitSecretKeys, enhancedSecretScan, deployId, featureFlags = {}, }) {
174
176
  if (failedPlugins.includes(packageName) ||
175
177
  (condition !== undefined &&
176
178
  !(await condition({
@@ -181,6 +183,7 @@ const shouldRunStep = async function ({ event, packageName, error, packagePath,
181
183
  netlifyConfig,
182
184
  saveConfig,
183
185
  explicitSecretKeys,
186
+ enhancedSecretScan,
184
187
  deployId,
185
188
  featureFlags,
186
189
  })))) {
@@ -199,7 +202,7 @@ const getFireStep = function (packageName, coreStepId, event) {
199
202
  const parentTag = normalizeTagName(packageName);
200
203
  return measureDuration(tFireStep, event, { parentTag, category: 'pluginEvent' });
201
204
  };
202
- const tFireStep = function ({ defaultConfig, event, childProcess, packageName, pluginPackageJson, loadedFrom, outputFlusher, origin, coreStep, coreStepId, coreStepName, configPath, outputConfigPath, buildDir, repositoryRoot, packagePath, nodePath, childEnv, context, branch, envChanges, constants, steps, buildbotServerSocket, events, error, logs, debug, quiet, systemLog, verbose, saveConfig, errorParams, configOpts, netlifyConfig, configMutations, headersPath, redirectsPath, featureFlags, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, deployId, extensionMetadata, api, }) {
205
+ const tFireStep = function ({ defaultConfig, event, childProcess, packageName, pluginPackageJson, loadedFrom, outputFlusher, origin, coreStep, coreStepId, coreStepName, configPath, outputConfigPath, buildDir, repositoryRoot, packagePath, nodePath, childEnv, context, branch, envChanges, constants, steps, buildbotServerSocket, events, error, logs, debug, quiet, systemLog, verbose, saveConfig, errorParams, configOpts, netlifyConfig, configMutations, headersPath, redirectsPath, featureFlags, userNodeVersion, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, deployId, extensionMetadata, api, }) {
203
206
  if (coreStep !== undefined) {
204
207
  return fireCoreStep({
205
208
  coreStep,
@@ -234,6 +237,7 @@ const tFireStep = function ({ defaultConfig, event, childProcess, packageName, p
234
237
  saveConfig,
235
238
  userNodeVersion,
236
239
  explicitSecretKeys,
240
+ enhancedSecretScan,
237
241
  edgeFunctionsBootstrapURL,
238
242
  deployId,
239
243
  api,
@@ -1,4 +1,4 @@
1
- export function runSteps({ defaultConfig, steps, buildbotServerSocket, events, configPath, outputConfigPath, headersPath, redirectsPath, buildDir, packagePath, repositoryRoot, nodePath, childEnv, context, branch, constants, mode, api, errorMonitor, deployId, errorParams, netlifyConfig, configOpts, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, }: {
1
+ export function runSteps({ defaultConfig, steps, buildbotServerSocket, events, configPath, outputConfigPath, headersPath, redirectsPath, buildDir, packagePath, repositoryRoot, nodePath, childEnv, context, branch, constants, mode, api, errorMonitor, deployId, errorParams, netlifyConfig, configOpts, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, }: {
2
2
  defaultConfig: any;
3
3
  steps: any;
4
4
  buildbotServerSocket: any;
@@ -33,6 +33,7 @@ export function runSteps({ defaultConfig, steps, buildbotServerSocket, events, c
33
33
  quiet: any;
34
34
  userNodeVersion: any;
35
35
  explicitSecretKeys: any;
36
+ enhancedSecretScan: any;
36
37
  edgeFunctionsBootstrapURL: any;
37
38
  }): Promise<{
38
39
  stepsCount: number;
@@ -7,7 +7,7 @@ import { runStep } from './run_step.js';
7
7
  // list of `failedPlugins` (that ran `utils.build.failPlugin()`).
8
8
  // If an error arises, runs `onError` events.
9
9
  // Runs `onEnd` events at the end, whether an error was thrown or not.
10
- export const runSteps = async function ({ defaultConfig, steps, buildbotServerSocket, events, configPath, outputConfigPath, headersPath, redirectsPath, buildDir, packagePath, repositoryRoot, nodePath, childEnv, context, branch, constants, mode, api, errorMonitor, deployId, errorParams, netlifyConfig, configOpts, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, edgeFunctionsBootstrapURL, }) {
10
+ export const runSteps = async function ({ defaultConfig, steps, buildbotServerSocket, events, configPath, outputConfigPath, headersPath, redirectsPath, buildDir, packagePath, repositoryRoot, nodePath, childEnv, context, branch, constants, mode, api, errorMonitor, deployId, errorParams, netlifyConfig, configOpts, logs, debug, systemLog, verbose, saveConfig, timers, testOpts, featureFlags, quiet, userNodeVersion, explicitSecretKeys, enhancedSecretScan, edgeFunctionsBootstrapURL, }) {
11
11
  const { index: stepsCount, error: errorA, netlifyConfig: netlifyConfigC, statuses: statusesB, failedPlugins: failedPluginsA, timers: timersC, configMutations: configMutationsB, metrics: metricsC, } = await pReduce(steps, async ({ index, error, failedPlugins, envChanges, netlifyConfig: netlifyConfigA, configMutations, headersPath: headersPathA, redirectsPath: redirectsPathA, statuses, timers: timersA, metrics: metricsA, }, { event, childProcess, packageName, extensionMetadata, coreStep, coreStepId, coreStepName, coreStepDescription, pluginPackageJson, loadedFrom, origin, condition, quiet: coreStepQuiet, }) => {
12
12
  const { newIndex = index, newError = error, failedPlugin = [], newEnvChanges = {}, netlifyConfig: netlifyConfigB = netlifyConfigA, configMutations: configMutationsA = configMutations, headersPath: headersPathB = headersPathA, redirectsPath: redirectsPathB = redirectsPathA, newStatus, timers: timersB = timersA, metrics: metricsB = [], } = await runStep({
13
13
  event,
@@ -62,6 +62,7 @@ export const runSteps = async function ({ defaultConfig, steps, buildbotServerSo
62
62
  quiet,
63
63
  userNodeVersion,
64
64
  explicitSecretKeys,
65
+ enhancedSecretScan,
65
66
  edgeFunctionsBootstrapURL,
66
67
  });
67
68
  const statusesA = addStatus({ newStatus, statuses, event, packageName, pluginPackageJson });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/build",
3
- "version": "32.1.4",
3
+ "version": "32.2.0",
4
4
  "description": "Netlify build module",
5
5
  "type": "module",
6
6
  "exports": "./lib/index.js",
@@ -159,5 +159,5 @@
159
159
  "engines": {
160
160
  "node": "^14.16.0 || >=16.0.0"
161
161
  },
162
- "gitHead": "03eae326c598d4d31b28fe8f20038c5d9508f772"
162
+ "gitHead": "fa49ed28e63e0cb14e1fdd8e9f0a812999c3314e"
163
163
  }