@netlify/build 33.2.0 → 33.3.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.
@@ -3,4 +3,5 @@ export function getNewEnvChanges(envBefore: any, netlifyConfig: any, netlifyConf
3
3
  };
4
4
  export function setEnvChanges(envChanges: any, currentEnv?: NodeJS.ProcessEnv): {
5
5
  [key: string]: string | undefined;
6
+ TZ?: string;
6
7
  };
@@ -15,8 +15,9 @@ export function logFunctionsToBundle({ logs, userFunctions, userFunctionsSrc, us
15
15
  }): void;
16
16
  export function logSecretsScanSkipMessage(logs: any, msg: any): void;
17
17
  export function logSecretsScanSuccessMessage(logs: any, msg: any): void;
18
- export function logSecretsScanFailBuildMessage({ logs, scanResults, groupedResults }: {
18
+ export function logSecretsScanFailBuildMessage({ logs, scanResults, groupedResults, enhancedScanShouldRunInActiveMode, }: {
19
19
  logs: any;
20
20
  scanResults: any;
21
21
  groupedResults: any;
22
+ enhancedScanShouldRunInActiveMode: any;
22
23
  }): void;
@@ -93,11 +93,11 @@ export const logSecretsScanSkipMessage = function (logs, msg) {
93
93
  export const logSecretsScanSuccessMessage = function (logs, msg) {
94
94
  log(logs, msg, { color: THEME.highlightWords });
95
95
  };
96
- export const logSecretsScanFailBuildMessage = function ({ logs, scanResults, groupedResults }) {
96
+ export const logSecretsScanFailBuildMessage = function ({ logs, scanResults, groupedResults, enhancedScanShouldRunInActiveMode, }) {
97
97
  const { secretMatches, enhancedSecretMatches } = groupedResults;
98
98
  const secretMatchesKeys = Object.keys(secretMatches);
99
99
  const enhancedSecretMatchesKeys = Object.keys(enhancedSecretMatches);
100
- 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`);
100
+ logErrorSubHeader(logs, `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`);
101
101
  // Explicit secret matches
102
102
  secretMatchesKeys.forEach((key) => {
103
103
  logError(logs, `Secret env var "${key}"'s value detected:`);
@@ -113,20 +113,22 @@ export const logSecretsScanFailBuildMessage = function ({ logs, scanResults, gro
113
113
  logError(logs, `\nTo prevent exposing secrets, the build will fail until these secret values are not found in build output or repo files.`);
114
114
  logError(logs, `\nIf these are expected, use SECRETS_SCAN_OMIT_PATHS, SECRETS_SCAN_OMIT_KEYS, or SECRETS_SCAN_ENABLED to prevent detecting.`);
115
115
  }
116
- // Likely secret matches from enhanced scan
117
- enhancedSecretMatchesKeys.forEach((key, index) => {
118
- logError(logs, `${index === 0 && secretMatchesKeys.length ? '\n' : ''}"${key}***" detected as a likely secret:`);
119
- enhancedSecretMatches[key]
120
- .sort((a, b) => {
121
- return a.file > b.file ? 0 : 1;
122
- })
123
- .forEach(({ lineNumber, file }) => {
124
- logError(logs, `found value at line ${lineNumber} in ${file}`, { indent: true });
116
+ if (enhancedScanShouldRunInActiveMode) {
117
+ // Likely secret matches from enhanced scan
118
+ enhancedSecretMatchesKeys.forEach((key, index) => {
119
+ logError(logs, `${index === 0 && secretMatchesKeys.length ? '\n' : ''}"${key}***" detected as a likely secret:`);
120
+ enhancedSecretMatches[key]
121
+ .sort((a, b) => {
122
+ return a.file > b.file ? 0 : 1;
123
+ })
124
+ .forEach(({ lineNumber, file }) => {
125
+ logError(logs, `found value at line ${lineNumber} in ${file}`, { indent: true });
126
+ });
125
127
  });
126
- });
127
- if (enhancedSecretMatchesKeys.length) {
128
- logError(logs, `\nTo prevent exposing secrets, the build will fail until these likely secret values are not found in build output or repo files.`);
129
- logError(logs, `\nIf these are expected, use ENHANCED_SECRETS_SCAN_OMIT_VALUES, or ENHANCED_SECRETS_SCAN_ENABLED to prevent detecting.`);
128
+ if (enhancedSecretMatchesKeys.length) {
129
+ logError(logs, `\nTo prevent exposing secrets, the build will fail until these likely secret values are not found in build output or repo files.`);
130
+ logError(logs, `\nIf these are expected, use SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES, or SECRETS_SCAN_SMART_DETECTION_ENABLED to prevent detecting.`);
131
+ }
130
132
  }
131
133
  logError(logs, `\nFor more information on secrets scanning, see the Netlify Docs: https://ntl.fyi/configure-secrets-scanning`);
132
134
  };
@@ -40,8 +40,7 @@ const coreStep = async function ({ logs, deployId, buildDir, packagePath, consta
40
40
  await pMap(blobsToUpload, async ({ key, contentPath, metadataPath }) => {
41
41
  systemLog(`Uploading blob ${key}`);
42
42
  const { data, metadata } = await getFileWithMetadata(key, contentPath, metadataPath);
43
- const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.length);
44
- await blobStore.set(key, arrayBuffer, { metadata });
43
+ await blobStore.set(key, new Blob([data]), { metadata });
45
44
  }, { concurrency: 10 });
46
45
  }
47
46
  catch (err) {
@@ -47,8 +47,7 @@ const coreStep = async function ({ debug, logs, deployId, buildDir, quiet, packa
47
47
  log(logs, `- Uploading blob ${key}`, { indent: true });
48
48
  }
49
49
  const { data, metadata } = await getFileWithMetadata(key, contentPath, metadataPath);
50
- const arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.length);
51
- await blobStore.set(key, arrayBuffer, { metadata });
50
+ await blobStore.set(key, new Blob([data]), { metadata });
52
51
  }, { concurrency: 10 });
53
52
  }
54
53
  catch (err) {
@@ -5,10 +5,14 @@ import { logSecretsScanFailBuildMessage, logSecretsScanSkipMessage, logSecretsSc
5
5
  import { reportValidations } from '../../status/validations.js';
6
6
  import { getFilePathsToScan, getOmitValuesFromEnhancedScanForEnhancedScanFromEnv, getSecretKeysToScanFor, groupScanResultsByKeyAndScanType, isEnhancedSecretsScanningEnabled, isSecretsScanningEnabled, scanFilesForKeyValues, } from './utils.js';
7
7
  const tracer = trace.getTracer('secrets-scanning');
8
- const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecretKeys, enhancedSecretScan, systemLog, deployId, api, }) {
8
+ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecretKeys, enhancedSecretScan, featureFlags, systemLog, deployId, api, }) {
9
9
  const stepResults = {};
10
10
  const passedSecretKeys = (explicitSecretKeys || '').split(',');
11
11
  const envVars = netlifyConfig.build.environment;
12
+ // When the flag is disabled, we may still run the scan if a secrets scan would otherwise take place anyway
13
+ // In this case, we hide any output to the user and simply gather the information in our logs
14
+ const enhancedScanShouldRunInActiveMode = featureFlags?.enhanced_secret_scan_impacts_builds ?? false;
15
+ const useMinimalChunks = featureFlags?.secret_scanning_minimal_chunks;
12
16
  systemLog?.({ passedSecretKeys, buildDir });
13
17
  if (!isSecretsScanningEnabled(envVars)) {
14
18
  logSecretsScanSkipMessage(logs, 'Secrets scanning disabled via SECRETS_SCAN_ENABLED flag set to false.');
@@ -22,16 +26,21 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
22
26
  log(logs, `SECRETS_SCAN_OMIT_PATHS override option set to: ${envVars['SECRETS_SCAN_OMIT_PATHS']}\n`);
23
27
  }
24
28
  const enhancedScanningEnabledInEnv = isEnhancedSecretsScanningEnabled(envVars);
25
- if (enhancedSecretScan && !enhancedScanningEnabledInEnv) {
26
- logSecretsScanSkipMessage(logs, 'Enhanced secrets detection disabled via ENHANCED_SECRETS_SCAN_ENABLED flag set to false.');
29
+ const enhancedScanConfigured = enhancedSecretScan && enhancedScanningEnabledInEnv;
30
+ if (enhancedSecretScan && enhancedScanShouldRunInActiveMode && !enhancedScanningEnabledInEnv) {
31
+ logSecretsScanSkipMessage(logs, 'Enhanced secrets detection disabled via SECRETS_SCAN_SMART_DETECTION_ENABLED flag set to false.');
27
32
  }
28
- if (enhancedSecretScan &&
29
- enhancedScanningEnabledInEnv &&
30
- envVars['ENHANCED_SECRETS_SCAN_OMIT_VALUES'] !== undefined) {
31
- log(logs, `ENHANCED_SECRETS_SCAN_OMIT_VALUES override option set to: ${envVars['ENHANCED_SECRETS_SCAN_OMIT_VALUES']}\n`);
33
+ if (enhancedScanShouldRunInActiveMode &&
34
+ enhancedScanConfigured &&
35
+ envVars['SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES'] !== undefined) {
36
+ log(logs, `SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES override option set to: ${envVars['SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES']}\n`);
32
37
  }
33
38
  const keysToSearchFor = getSecretKeysToScanFor(envVars, passedSecretKeys);
34
- if (keysToSearchFor.length === 0 && !enhancedSecretScan) {
39
+ // In passive mode, only run the enhanced scan if we have explicit secret keys
40
+ const enhancedScanShouldRun = enhancedScanShouldRunInActiveMode
41
+ ? enhancedScanConfigured
42
+ : enhancedScanConfigured && keysToSearchFor.length > 0;
43
+ if (keysToSearchFor.length === 0 && !enhancedScanShouldRun) {
35
44
  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.');
36
45
  return stepResults;
37
46
  }
@@ -52,8 +61,9 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
52
61
  keys: keysToSearchFor,
53
62
  base: buildDir,
54
63
  filePaths,
55
- enhancedScanning: enhancedSecretScan && enhancedScanningEnabledInEnv,
64
+ enhancedScanning: enhancedScanShouldRun,
56
65
  omitValuesFromEnhancedScan: getOmitValuesFromEnhancedScanForEnhancedScanFromEnv(envVars),
66
+ useMinimalChunks,
57
67
  });
58
68
  secretMatches = scanResults.matches.filter((match) => !match.enhancedMatch);
59
69
  enhancedSecretMatches = scanResults.matches.filter((match) => match.enhancedMatch);
@@ -65,7 +75,8 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
65
75
  secretsFilesCount: scanResults.scannedFilesCount,
66
76
  keysToSearchFor,
67
77
  enhancedPrefixMatches: enhancedSecretMatches.length ? enhancedSecretMatches.map((match) => match.key) : [],
68
- enhancedScanning: enhancedSecretScan && enhancedScanningEnabledInEnv,
78
+ enhancedScanning: enhancedScanShouldRun,
79
+ enhancedScanActiveMode: enhancedScanShouldRunInActiveMode,
69
80
  };
70
81
  systemLog?.(attributesForLogsAndSpan);
71
82
  span.setAttributes(attributesForLogsAndSpan);
@@ -75,11 +86,13 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
75
86
  const secretScanResult = {
76
87
  scannedFilesCount: scanResults?.scannedFilesCount ?? 0,
77
88
  secretsScanMatches: secretMatches ?? [],
78
- enhancedSecretsScanMatches: enhancedSecretMatches ?? [],
89
+ enhancedSecretsScanMatches: enhancedScanShouldRunInActiveMode && enhancedSecretMatches ? enhancedSecretMatches : [],
79
90
  };
80
91
  reportValidations({ api, secretScanResult, deployId, systemLog });
81
92
  }
82
- if (!scanResults || scanResults.matches.length === 0) {
93
+ if (!scanResults ||
94
+ scanResults.matches.length === 0 ||
95
+ (!enhancedScanShouldRunInActiveMode && !secretMatches?.length)) {
83
96
  logSecretsScanSuccessMessage(logs, `Secrets scanning complete. ${scanResults?.scannedFilesCount} file(s) scanned. No secrets detected in build output or repo code!`);
84
97
  return stepResults;
85
98
  }
@@ -89,6 +102,7 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
89
102
  logs,
90
103
  scanResults,
91
104
  groupedResults: groupScanResultsByKeyAndScanType(scanResults),
105
+ enhancedScanShouldRunInActiveMode,
92
106
  });
93
107
  const error = new Error(`Secrets scanning found secrets in build.`);
94
108
  addErrorInfo(error, { type: 'secretScanningFoundSecrets' });
@@ -9,6 +9,7 @@ interface ScanArgs {
9
9
  filePaths: string[];
10
10
  enhancedScanning?: boolean;
11
11
  omitValuesFromEnhancedScan?: unknown[];
12
+ useMinimalChunks: boolean;
12
13
  }
13
14
  interface MatchResult {
14
15
  lineNumber: number;
@@ -49,32 +50,34 @@ export declare function getOmitValuesFromEnhancedScanForEnhancedScanFromEnv(env:
49
50
  */
50
51
  export declare function getSecretKeysToScanFor(env: Record<string, unknown>, secretKeys: string[]): string[];
51
52
  /**
52
- * Checks a line of text for likely secrets based on known prefixes and patterns.
53
+ * Checks a chunk of text for likely secrets based on known prefixes and patterns.
53
54
  * The function works by:
54
- * 1. Splitting the line into tokens using quotes, whitespace, equals signs, colons, and commas as delimiters
55
+ * 1. Splitting the chunk into tokens using quotes, whitespace, equals signs, colons, and commas as delimiters
55
56
  * 2. For each token, checking if it matches our secret pattern:
56
57
  * - Must start (^) with one of our known prefixes (e.g. aws_, github_pat_, etc)
57
58
  * - Must be followed by at least MIN_CHARS_AFTER_PREFIX non-whitespace characters
58
59
  * - Must extend to the end ($) of the token
59
60
  *
60
- * For example, given the line: secretKey='aws_123456789012345678'
61
+ * For example, given the chunk: secretKey='aws_123456789012345678'
61
62
  * 1. It's split into tokens: ['secretKey', 'aws_123456789012345678']
62
63
  * 2. Each token is checked against the regex pattern:
63
64
  * - 'secretKey' doesn't match (doesn't start with a known prefix)
64
65
  * - 'aws_123456789012345678' matches (starts with 'aws_' and has sufficient length)
65
66
  *
66
- * @param line The line of text to check
67
- * @param file The file path where this line was found
68
- * @param lineNumber The line number in the file
69
- * @param omitValuesFromEnhancedScan Optional array of values to exclude from matching
70
- * @returns Array of matches found in the line
71
67
  */
72
- export declare function findLikelySecrets({ line, file, lineNumber, omitValuesFromEnhancedScan, }: {
73
- line: string;
74
- file: string;
75
- lineNumber: number;
68
+ export declare function findLikelySecrets({ text, omitValuesFromEnhancedScan, }: {
69
+ /**
70
+ * Text to check
71
+ */
72
+ text: string;
73
+ /**
74
+ * Optional array of values to exclude from matching
75
+ */
76
76
  omitValuesFromEnhancedScan?: unknown[];
77
- }): MatchResult[];
77
+ }): {
78
+ index: number;
79
+ prefix: string;
80
+ }[];
78
81
  /**
79
82
  * Given the env and base directory, find all file paths to scan. It will look at the
80
83
  * env vars to decide if it should omit certain paths.
@@ -95,7 +98,7 @@ export declare function getFilePathsToScan({ env, base }: {
95
98
  * @param scanArgs {ScanArgs} scan options
96
99
  * @returns promise with all of the scan results, if any
97
100
  */
98
- export declare function scanFilesForKeyValues({ env, keys, filePaths, base, enhancedScanning, omitValuesFromEnhancedScan, }: ScanArgs): Promise<ScanResults>;
101
+ export declare function scanFilesForKeyValues({ env, keys, filePaths, base, enhancedScanning, omitValuesFromEnhancedScan, useMinimalChunks, }: ScanArgs): Promise<ScanResults>;
99
102
  /**
100
103
  * ScanResults are all of the finds for all keys and their disparate locations. Scanning is
101
104
  * async in streams so order can change a lot. Some matches are the result of an env var explictly being marked as secret,
@@ -21,7 +21,7 @@ export function isSecretsScanningEnabled(env) {
21
21
  * @returns
22
22
  */
23
23
  export function isEnhancedSecretsScanningEnabled(env) {
24
- if (env.ENHANCED_SECRETS_SCAN_ENABLED === false || env.ENHANCED_SECRETS_SCAN_ENABLED === 'false') {
24
+ if (env.SECRETS_SCAN_SMART_DETECTION_ENABLED === false || env.SECRETS_SCAN_SMART_DETECTION_ENABLED === 'false') {
25
25
  return false;
26
26
  }
27
27
  return true;
@@ -37,7 +37,7 @@ export function getStringArrayFromEnvValue(env, envVarName) {
37
37
  return omitKeys;
38
38
  }
39
39
  export function getOmitValuesFromEnhancedScanForEnhancedScanFromEnv(env) {
40
- return getStringArrayFromEnvValue(env, 'ENHANCED_SECRETS_SCAN_OMIT_VALUES');
40
+ return getStringArrayFromEnvValue(env, 'SECRETS_SCAN_SMART_DETECTION_OMIT_VALUES');
41
41
  }
42
42
  function filterOmittedKeys(env, envKeys = []) {
43
43
  const omitKeys = getStringArrayFromEnvValue(env, 'SECRETS_SCAN_OMIT_KEYS');
@@ -89,56 +89,49 @@ const MIN_CHARS_AFTER_PREFIX = 12;
89
89
  // Escape special regex characters (like $, *, +, etc) in prefixes so they're treated as literal characters
90
90
  const prefixMatchingRegex = LIKELY_SECRET_PREFIXES.map((p) => p.replace(/[$*+?.()|[\]{}]/g, '\\$&')).join('|');
91
91
  // Build regex pattern for matching secrets with various delimiters and quotes:
92
- // (?:["'`]|^|[=:,]) - match either quotes, start of line, or delimiters (=:,) at the start
92
+ // (?:["'\`]|[=]) - match either quotes, or = at the start
93
93
  // Named capturing groups:
94
94
  // - <token>: captures the entire secret value including its prefix
95
95
  // - <prefix>: captures just the prefix part (e.g. 'aws_', 'github_pat_')
96
96
  // (?:${prefixMatchingRegex}) - non-capturing group containing our escaped prefixes (e.g. aws_|github_pat_|etc)
97
- // [^ "'`=:,]{${MIN_CHARS_AFTER_PREFIX}} - match exactly MIN_CHARS_AFTER_PREFIX chars after the prefix
98
- // [^ "'`=:,]*? - lazily match any additional chars that aren't quotes/delimiters
99
- // (?:["'`]|[ =:,]|$) - end with either quotes, delimiters, whitespace, or end of line
97
+ // [a-zA-Z0-9-]{${MIN_CHARS_AFTER_PREFIX}} - match exactly MIN_CHARS_AFTER_PREFIX chars (alphanumeric or dash) after the prefix
98
+ // [a-zA-Z0-9-]*? - lazily match any additional chars (alphanumeric or dash)
99
+ // (?:["'\`]|$) - end with either quotes or end of line
100
100
  // gi - global and case insensitive flags
101
101
  // Note: Using the global flag (g) means this regex object maintains state between executions.
102
102
  // We would need to reset lastIndex to 0 if we wanted to reuse it on the same string multiple times.
103
- const likelySecretRegex = new RegExp(`(?:["'\`]|^|[=:,]) *(?<token>(?<prefix>${prefixMatchingRegex})[^ "'\`=:,]{${MIN_CHARS_AFTER_PREFIX}}[^ "'\`=:,]*?)(?:["'\`]|[ =:,]|$)`, 'gi');
103
+ const likelySecretRegex = new RegExp(`(?:["'\`]|[=]) *(?<token>(?<prefix>${prefixMatchingRegex})[a-zA-Z0-9-]{${MIN_CHARS_AFTER_PREFIX}}[a-zA-Z0-9-]*?)(?:["'\`]|$)`, 'gi');
104
104
  /**
105
- * Checks a line of text for likely secrets based on known prefixes and patterns.
105
+ * Checks a chunk of text for likely secrets based on known prefixes and patterns.
106
106
  * The function works by:
107
- * 1. Splitting the line into tokens using quotes, whitespace, equals signs, colons, and commas as delimiters
107
+ * 1. Splitting the chunk into tokens using quotes, whitespace, equals signs, colons, and commas as delimiters
108
108
  * 2. For each token, checking if it matches our secret pattern:
109
109
  * - Must start (^) with one of our known prefixes (e.g. aws_, github_pat_, etc)
110
110
  * - Must be followed by at least MIN_CHARS_AFTER_PREFIX non-whitespace characters
111
111
  * - Must extend to the end ($) of the token
112
112
  *
113
- * For example, given the line: secretKey='aws_123456789012345678'
113
+ * For example, given the chunk: secretKey='aws_123456789012345678'
114
114
  * 1. It's split into tokens: ['secretKey', 'aws_123456789012345678']
115
115
  * 2. Each token is checked against the regex pattern:
116
116
  * - 'secretKey' doesn't match (doesn't start with a known prefix)
117
117
  * - 'aws_123456789012345678' matches (starts with 'aws_' and has sufficient length)
118
118
  *
119
- * @param line The line of text to check
120
- * @param file The file path where this line was found
121
- * @param lineNumber The line number in the file
122
- * @param omitValuesFromEnhancedScan Optional array of values to exclude from matching
123
- * @returns Array of matches found in the line
124
119
  */
125
- export function findLikelySecrets({ line, file, lineNumber, omitValuesFromEnhancedScan = [], }) {
126
- if (!line)
120
+ export function findLikelySecrets({ text, omitValuesFromEnhancedScan = [], }) {
121
+ if (!text)
127
122
  return [];
128
123
  const matches = [];
129
124
  let match;
130
125
  const allOmittedValues = [...omitValuesFromEnhancedScan, ...SAFE_LISTED_VALUES];
131
- while ((match = likelySecretRegex.exec(line)) !== null) {
126
+ while ((match = likelySecretRegex.exec(text)) !== null) {
132
127
  const token = match.groups?.token;
133
128
  const prefix = match.groups?.prefix;
134
129
  if (!token || !prefix || allOmittedValues.includes(token)) {
135
130
  continue;
136
131
  }
137
132
  matches.push({
138
- file,
139
- lineNumber,
140
- key: prefix,
141
- enhancedMatch: true,
133
+ prefix,
134
+ index: match.index,
142
135
  });
143
136
  }
144
137
  return matches;
@@ -204,7 +197,7 @@ const omitPathMatches = (relativePath, omitPaths) => {
204
197
  * @param scanArgs {ScanArgs} scan options
205
198
  * @returns promise with all of the scan results, if any
206
199
  */
207
- export async function scanFilesForKeyValues({ env, keys, filePaths, base, enhancedScanning, omitValuesFromEnhancedScan = [], }) {
200
+ export async function scanFilesForKeyValues({ env, keys, filePaths, base, enhancedScanning, omitValuesFromEnhancedScan = [], useMinimalChunks = false, }) {
208
201
  const scanResults = {
209
202
  matches: [],
210
203
  scannedFilesCount: 0,
@@ -225,6 +218,7 @@ export async function scanFilesForKeyValues({ env, keys, filePaths, base, enhanc
225
218
  }, {});
226
219
  scanResults.scannedFilesCount = filePaths.length;
227
220
  let settledPromises = [];
221
+ const searchStream = useMinimalChunks ? searchStreamMinimalChunks : searchStreamReadline;
228
222
  // process the scanning in batches to not run into memory issues by
229
223
  // processing all files at the same time.
230
224
  while (filePaths.length > 0) {
@@ -241,7 +235,10 @@ export async function scanFilesForKeyValues({ env, keys, filePaths, base, enhanc
241
235
  });
242
236
  return scanResults;
243
237
  }
244
- const searchStream = ({ basePath, file, keyValues, enhancedScanning, omitValuesFromEnhancedScan = [], }) => {
238
+ /**
239
+ * Search stream implementation using node:readline
240
+ */
241
+ const searchStreamReadline = ({ basePath, file, keyValues, enhancedScanning, omitValuesFromEnhancedScan = [], }) => {
245
242
  return new Promise((resolve, reject) => {
246
243
  const filePath = path.resolve(basePath, file);
247
244
  const inStream = createReadStream(filePath);
@@ -270,7 +267,12 @@ const searchStream = ({ basePath, file, keyValues, enhancedScanning, omitValuesF
270
267
  lineNumber++;
271
268
  if (typeof line === 'string') {
272
269
  if (enhancedScanning) {
273
- matches.push(...findLikelySecrets({ line, file, lineNumber, omitValuesFromEnhancedScan }));
270
+ matches.push(...findLikelySecrets({ text: line, omitValuesFromEnhancedScan }).map(({ prefix }) => ({
271
+ key: prefix,
272
+ file,
273
+ lineNumber,
274
+ enhancedMatch: true,
275
+ })));
274
276
  }
275
277
  if (maxMultiLineCount > 1) {
276
278
  lines.push(line);
@@ -352,6 +354,184 @@ const searchStream = ({ basePath, file, keyValues, enhancedScanning, omitValuesF
352
354
  });
353
355
  });
354
356
  };
357
+ /**
358
+ * Search stream implementation using just read stream that allows to buffer less content
359
+ */
360
+ const searchStreamMinimalChunks = ({ basePath, file, keyValues, enhancedScanning, omitValuesFromEnhancedScan = [], }) => {
361
+ return new Promise((resolve, reject) => {
362
+ const matches = [];
363
+ const keyVals = [].concat(...Object.values(keyValues));
364
+ // determine longest value that we will search for - needed to determine minimal size of rolling buffer
365
+ const maxValLength = Math.max(0,
366
+ // explicit secrets
367
+ ...keyVals.map((v) => v.length), ...(enhancedScanning
368
+ ? [
369
+ // omitted likely secrets (after finding likely secret we check if it should be omitted, so we need to capture at least size of omitted values)
370
+ ...omitValuesFromEnhancedScan.map((v) => (typeof v === 'string' ? v.length : 0)),
371
+ // minimum length needed to find likely secret
372
+ ...LIKELY_SECRET_PREFIXES.map((v) => v.length + MIN_CHARS_AFTER_PREFIX),
373
+ ]
374
+ : []));
375
+ if (maxValLength === 0) {
376
+ // no non-empty values to scan for
377
+ resolve(matches);
378
+ return;
379
+ }
380
+ const filePath = path.resolve(basePath, file);
381
+ const inStream = createReadStream(filePath);
382
+ function getKeyForValue(val) {
383
+ let key = '';
384
+ for (const [secretKeyName, valuePermutations] of Object.entries(keyValues)) {
385
+ if (valuePermutations.includes(val)) {
386
+ key = secretKeyName;
387
+ }
388
+ }
389
+ return key;
390
+ }
391
+ let buffer = '';
392
+ let newLinesIndexesInCurrentBuffer = null;
393
+ function getCurrentBufferNewLineIndexes() {
394
+ if (newLinesIndexesInCurrentBuffer === null) {
395
+ newLinesIndexesInCurrentBuffer = [];
396
+ let newLineIndex = -1;
397
+ while ((newLineIndex = buffer.indexOf('\n', newLineIndex + 1)) !== -1) {
398
+ newLinesIndexesInCurrentBuffer.push(newLineIndex);
399
+ }
400
+ }
401
+ return newLinesIndexesInCurrentBuffer;
402
+ }
403
+ /**
404
+ * Amount of characters that were fully processed. Used to determine absolute position of current rolling buffer
405
+ * in the file.
406
+ */
407
+ let processedCharacters = 0;
408
+ /**
409
+ * Amount of lines that were fully processed. Used to determine absolute line number of matches in current rolling buffer.
410
+ */
411
+ let processedLines = 0;
412
+ /**
413
+ * Map keeping track of found secrets in current file. Used to prevent reporting same secret+position multiple times.
414
+ * Needed because rolling buffer might retain same secret in multiple passes.
415
+ */
416
+ const foundIndexes = new Map();
417
+ /**
418
+ * We report given secret at most once per line, so we keep track lines we already reported for given secret.
419
+ */
420
+ const foundLines = new Map();
421
+ /**
422
+ * Calculate absolute line number in a file for given match in the current rolling buffer.
423
+ */
424
+ function getLineNumberForMatchInTheBuffer({ indexInBuffer, key }) {
425
+ const absolutePositionInFile = processedCharacters + indexInBuffer;
426
+ // check if we already handled match for given key in this position
427
+ let foundIndexesForKey = foundIndexes.get(key);
428
+ if (!foundIndexesForKey?.has(absolutePositionInFile)) {
429
+ // ensure we track match for this key and position to not report it again in future passes
430
+ if (!foundIndexesForKey) {
431
+ foundIndexesForKey = new Set();
432
+ foundIndexes.set(key, foundIndexesForKey);
433
+ }
434
+ foundIndexesForKey.add(absolutePositionInFile);
435
+ // calculate line number based on amount of fully processed lines and position of line breaks in current buffer
436
+ let lineNumber = processedLines + 1;
437
+ for (const newLineIndex of getCurrentBufferNewLineIndexes()) {
438
+ if (indexInBuffer > newLineIndex) {
439
+ lineNumber++;
440
+ }
441
+ else {
442
+ break;
443
+ }
444
+ }
445
+ // check if we already handled match for given key in this line
446
+ let foundLinesForKey = foundLines.get(key);
447
+ if (!foundLinesForKey?.has(lineNumber)) {
448
+ if (!foundLinesForKey) {
449
+ foundLinesForKey = new Set();
450
+ foundLines.set(key, foundLinesForKey);
451
+ }
452
+ foundLinesForKey.add(lineNumber);
453
+ // only report line number if we didn't report it yet for this key
454
+ return lineNumber;
455
+ }
456
+ }
457
+ }
458
+ function processBuffer() {
459
+ for (const valVariant of keyVals) {
460
+ let indexInBuffer = -1;
461
+ while ((indexInBuffer = buffer.indexOf(valVariant, indexInBuffer + 1)) !== -1) {
462
+ const key = getKeyForValue(valVariant);
463
+ const lineNumber = getLineNumberForMatchInTheBuffer({
464
+ indexInBuffer,
465
+ key,
466
+ });
467
+ if (typeof lineNumber === 'number') {
468
+ matches.push({
469
+ file,
470
+ lineNumber,
471
+ key,
472
+ enhancedMatch: false,
473
+ });
474
+ }
475
+ }
476
+ }
477
+ if (enhancedScanning) {
478
+ const likelySecrets = findLikelySecrets({ text: buffer, omitValuesFromEnhancedScan });
479
+ for (const { index, prefix } of likelySecrets) {
480
+ const lineNumber = getLineNumberForMatchInTheBuffer({
481
+ indexInBuffer: index,
482
+ key: prefix,
483
+ });
484
+ if (typeof lineNumber === 'number') {
485
+ matches.push({
486
+ file,
487
+ lineNumber,
488
+ key: prefix,
489
+ enhancedMatch: true,
490
+ });
491
+ }
492
+ }
493
+ }
494
+ }
495
+ inStream.on('data', function (chunk) {
496
+ buffer += chunk.toString();
497
+ // reset new line positions in current buffer
498
+ newLinesIndexesInCurrentBuffer = null;
499
+ if (buffer.length > maxValLength) {
500
+ // only process if buffer is large enough to contain longest secret, if final chunk isn't large enough
501
+ // it will be processed in `close` event handler
502
+ processBuffer();
503
+ // we will keep maxValLength characters in the buffer, surplus of characters at this point is fully processed
504
+ const charactersInBufferThatWereFullyProcessed = buffer.length - maxValLength;
505
+ processedCharacters += charactersInBufferThatWereFullyProcessed;
506
+ // advance processed lines
507
+ for (const newLineIndex of getCurrentBufferNewLineIndexes()) {
508
+ if (newLineIndex < charactersInBufferThatWereFullyProcessed) {
509
+ processedLines++;
510
+ }
511
+ else {
512
+ break;
513
+ }
514
+ }
515
+ // Keep the last part of the buffer to handle split values across chunks
516
+ buffer = buffer.slice(charactersInBufferThatWereFullyProcessed);
517
+ }
518
+ });
519
+ inStream.on('error', function (error) {
520
+ if (error?.code === 'EISDIR') {
521
+ // file path is a directory - do nothing
522
+ resolve(matches);
523
+ }
524
+ else {
525
+ reject(error);
526
+ }
527
+ });
528
+ inStream.on('close', function () {
529
+ // process any remaining buffer content
530
+ processBuffer();
531
+ resolve(matches);
532
+ });
533
+ });
534
+ };
355
535
  /**
356
536
  * ScanResults are all of the finds for all keys and their disparate locations. Scanning is
357
537
  * async in streams so order can change a lot. Some matches are the result of an env var explictly being marked as secret,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/build",
3
- "version": "33.2.0",
3
+ "version": "33.3.0",
4
4
  "description": "Netlify build module",
5
5
  "type": "module",
6
6
  "exports": "./lib/index.js",
@@ -67,17 +67,17 @@
67
67
  "license": "MIT",
68
68
  "dependencies": {
69
69
  "@bugsnag/js": "^8.0.0",
70
- "@netlify/blobs": "^8.2.0",
71
- "@netlify/cache-utils": "^6.0.2",
72
- "@netlify/config": "^23.0.6",
73
- "@netlify/edge-bundler": "14.0.4",
70
+ "@netlify/blobs": "^9.1.3",
71
+ "@netlify/cache-utils": "^6.0.3",
72
+ "@netlify/config": "^23.0.8",
73
+ "@netlify/edge-bundler": "14.0.5",
74
74
  "@netlify/framework-info": "^10.0.4",
75
- "@netlify/functions-utils": "^6.0.6",
76
- "@netlify/git-utils": "^6.0.1",
77
- "@netlify/opentelemetry-utils": "^2.0.0",
75
+ "@netlify/functions-utils": "^6.0.7",
76
+ "@netlify/git-utils": "^6.0.2",
77
+ "@netlify/opentelemetry-utils": "^2.0.1",
78
78
  "@netlify/plugins-list": "^6.80.0",
79
- "@netlify/run-utils": "^6.0.1",
80
- "@netlify/zip-it-and-ship-it": "12.1.0",
79
+ "@netlify/run-utils": "^6.0.2",
80
+ "@netlify/zip-it-and-ship-it": "12.1.1",
81
81
  "@sindresorhus/slugify": "^2.0.0",
82
82
  "ansi-escapes": "^7.0.0",
83
83
  "chalk": "^5.0.0",
@@ -86,7 +86,7 @@
86
86
  "fdir": "^6.0.1",
87
87
  "figures": "^6.0.0",
88
88
  "filter-obj": "^6.0.0",
89
- "got": "^12.0.0",
89
+ "got": "^13.0.0",
90
90
  "hot-shots": "10.2.1",
91
91
  "indent-string": "^5.0.0",
92
92
  "is-plain-obj": "^4.0.0",
@@ -125,20 +125,20 @@
125
125
  "yargs": "^17.6.0"
126
126
  },
127
127
  "devDependencies": {
128
- "@netlify/nock-udp": "^5.0.0",
128
+ "@netlify/nock-udp": "^5.0.1",
129
129
  "@opentelemetry/api": "~1.8.0",
130
130
  "@opentelemetry/sdk-trace-base": "~1.24.0",
131
- "@types/node": "^14.18.53",
131
+ "@types/node": "^18.0.0",
132
132
  "atob": "^2.1.2",
133
133
  "ava": "^5.0.0",
134
134
  "c8": "^10.0.0",
135
135
  "copyfiles": "^2.4.1",
136
136
  "cpy": "^11.0.0",
137
- "get-node": "^12.0.0",
137
+ "get-node": "^14.2.1",
138
138
  "get-port": "^7.0.0",
139
139
  "has-ansi": "^6.0.0",
140
140
  "moize": "^6.0.0",
141
- "npm-run-all2": "^5.0.0",
141
+ "npm-run-all2": "^6.0.0",
142
142
  "process-exists": "^5.0.0",
143
143
  "sinon": "^20.0.0",
144
144
  "tmp-promise": "^3.0.2",
@@ -158,5 +158,5 @@
158
158
  "engines": {
159
159
  "node": ">=18.14.0"
160
160
  },
161
- "gitHead": "93e0ab6ebe274e7af29a979973fac1882f9046ea"
161
+ "gitHead": "df148594017a78f0f419591da402311ed08e4d64"
162
162
  }