@netlify/build 29.15.2 → 29.15.4

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.
@@ -80,7 +80,7 @@ export const logSecretsScanSuccessMessage = function (logs, msg) {
80
80
  log(logs, msg, { color: THEME.highlightWords });
81
81
  };
82
82
  export const logSecretsScanFailBuildMessage = function ({ logs, scanResults, groupedResults }) {
83
- logErrorSubHeader(logs, `Secrets scanning found ${scanResults.matches.length} instance(s) of secrets in build output or repo code.\n`);
83
+ 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`);
84
84
  Object.keys(groupedResults).forEach((key) => {
85
85
  logError(logs, `Secret env var "${key}"'s value detected:`);
86
86
  groupedResults[key]
@@ -1,17 +1,26 @@
1
+ import { trace } from '@opentelemetry/api';
1
2
  import { addErrorInfo } from '../../error/info.js';
3
+ import { log } from '../../log/logger.js';
2
4
  import { logSecretsScanFailBuildMessage, logSecretsScanSkipMessage, logSecretsScanSuccessMessage, } from '../../log/messages/core_steps.js';
3
5
  import { getFilePathsToScan, getSecretKeysToScanFor, groupScanResultsByKey, isSecretsScanningEnabled, scanFilesForKeyValues, } from './utils.js';
6
+ const tracer = trace.getTracer('secrets-scanning');
4
7
  const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecretKeys, systemLog }) {
5
8
  const stepResults = {};
6
9
  const passedSecretKeys = (explicitSecretKeys || '').split(',');
7
10
  const envVars = netlifyConfig.build.environment;
8
- systemLog({ envVars, passedSecretKeys });
11
+ systemLog({ passedSecretKeys, buildDir });
9
12
  if (!isSecretsScanningEnabled(envVars)) {
10
13
  logSecretsScanSkipMessage(logs, 'Secrets scanning disabled via SECRETS_SCAN_ENABLED flag set to false.');
11
14
  return stepResults;
12
15
  }
16
+ // transparently log if there are scanning values being omitted
17
+ if (envVars['SECRETS_SCAN_OMIT_KEYS'] !== undefined) {
18
+ log(logs, `SECRETS_SCAN_OMIT_KEYS override option set to: ${envVars['SECRETS_SCAN_OMIT_KEYS']}\n`);
19
+ }
20
+ if (envVars['SECRETS_SCAN_OMIT_PATHS'] !== undefined) {
21
+ log(logs, `SECRETS_SCAN_OMIT_PATHS override option set to: ${envVars['SECRETS_SCAN_OMIT_PATHS']}\n`);
22
+ }
13
23
  const keysToSearchFor = getSecretKeysToScanFor(envVars, passedSecretKeys);
14
- systemLog({ keysToSearchFor });
15
24
  if (keysToSearchFor.length === 0) {
16
25
  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.');
17
26
  return stepResults;
@@ -20,19 +29,30 @@ const coreStep = async function ({ buildDir, logs, netlifyConfig, explicitSecret
20
29
  // The scanning will look at builddir so that it can review both repo pulled files
21
30
  // and post build files
22
31
  const filePaths = await getFilePathsToScan({ env: envVars, base: buildDir });
23
- systemLog({ buildDir, filePaths });
24
32
  if (filePaths.length === 0) {
25
33
  logSecretsScanSkipMessage(logs, 'Secrets scanning skipped because there are no files or all files were omitted with SECRETS_SCAN_OMIT_PATHS env var setting.');
26
34
  return stepResults;
27
35
  }
28
- const scanResults = await scanFilesForKeyValues({
29
- env: envVars,
30
- keys: keysToSearchFor,
31
- base: buildDir,
32
- filePaths,
36
+ let scanResults;
37
+ await tracer.startActiveSpan('scanning-files', { attributes: { keysToSearchFor, totalFiles: filePaths.length } }, async (span) => {
38
+ scanResults = await scanFilesForKeyValues({
39
+ env: envVars,
40
+ keys: keysToSearchFor,
41
+ base: buildDir,
42
+ filePaths,
43
+ });
44
+ const attributesForLogsAndSpan = {
45
+ secretsScanFoundSecrets: scanResults.matches.length > 0,
46
+ secretsScanMatchesCount: scanResults.matches.length,
47
+ secretsFilesCount: scanResults.scannedFilesCount,
48
+ keysToSearchFor,
49
+ };
50
+ systemLog(attributesForLogsAndSpan);
51
+ span.setAttributes(attributesForLogsAndSpan);
52
+ span.end();
33
53
  });
34
- if (scanResults.matches.length === 0) {
35
- logSecretsScanSuccessMessage(logs, 'Secrets scanning complete. No secrets detected in build output or repo code.');
54
+ if (!scanResults || scanResults.matches.length === 0) {
55
+ logSecretsScanSuccessMessage(logs, `Secrets scanning complete. ${scanResults?.scannedFilesCount} file(s) scanned. No secrets detected in build output or repo code!`);
36
56
  return stepResults;
37
57
  }
38
58
  // at this point we have found matching secrets
@@ -1,4 +1,4 @@
1
- import { createReadStream } from 'node:fs';
1
+ import { createReadStream, promises as fs, existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { createInterface } from 'node:readline';
4
4
  import { fdir } from 'fdir';
@@ -63,13 +63,35 @@ export function getSecretKeysToScanFor(env, secretKeys) {
63
63
  * @returns string[] of relative paths from base of files that should be searched
64
64
  */
65
65
  export async function getFilePathsToScan({ env, base }) {
66
- let files = await new fdir().withRelativePaths().crawl(base).withPromise();
66
+ const omitPathsAlways = ['.git'];
67
+ // node modules is dense and is only useful to scan if the repo itself commits these
68
+ // files. As a simple check to understand if the repo would commit these files, we expect
69
+ // that they would not ignore them from their git settings. So if gitignore includes
70
+ // node_modules anywhere we will omit looking in those folders - this will allow repos
71
+ // that do commit node_modules to still scan them.
72
+ let ignoreNodeModules = false;
73
+ const gitignorePath = path.resolve(base, '.gitignore');
74
+ const gitignoreContents = existsSync(gitignorePath) ? await fs.readFile(gitignorePath, 'utf-8') : '';
75
+ if (gitignoreContents?.includes('node_modules')) {
76
+ ignoreNodeModules = true;
77
+ }
78
+ let files = await new fdir()
79
+ .withRelativePaths()
80
+ .filter((path) => {
81
+ if (ignoreNodeModules && path.includes('node_modules')) {
82
+ return false;
83
+ }
84
+ return true;
85
+ })
86
+ .crawl(base)
87
+ .withPromise();
67
88
  // normalize the path separators to all use the forward slash
68
89
  // this is needed for windows machines and snapshot tests consistency.
69
90
  files = files.map((f) => f.split(path.sep).join('/'));
70
91
  let omitPaths = [];
71
92
  if (typeof env.SECRETS_SCAN_OMIT_PATHS === 'string') {
72
93
  omitPaths = env.SECRETS_SCAN_OMIT_PATHS.split(',')
94
+ .concat(omitPathsAlways)
73
95
  .map((s) => s.trim())
74
96
  .filter(Boolean);
75
97
  }
@@ -92,6 +114,7 @@ const omitPathMatches = (relativePath, omitPaths) => omitPaths.some((oPath) => r
92
114
  export async function scanFilesForKeyValues({ env, keys, filePaths, base }) {
93
115
  const scanResults = {
94
116
  matches: [],
117
+ scannedFilesCount: 0,
95
118
  };
96
119
  const keyValues = keys.reduce((kvs, key) => {
97
120
  let val = env[key];
@@ -107,9 +130,17 @@ export async function scanFilesForKeyValues({ env, keys, filePaths, base }) {
107
130
  }
108
131
  return kvs;
109
132
  }, {});
110
- const settledPromises = await Promise.allSettled(filePaths.map((file) => {
111
- return searchStream(base, file, keyValues);
112
- }));
133
+ scanResults.scannedFilesCount = filePaths.length;
134
+ let settledPromises = [];
135
+ // process the scanning in batches to not run into memory issues by
136
+ // processing all files at the same time.
137
+ while (filePaths.length > 0) {
138
+ const chunkSize = 200;
139
+ const batch = filePaths.splice(0, chunkSize);
140
+ settledPromises = settledPromises.concat(await Promise.allSettled(batch.map((file) => {
141
+ return searchStream(base, file, keyValues);
142
+ })));
143
+ }
113
144
  settledPromises.forEach((result) => {
114
145
  if (result.status === 'fulfilled' && result.value?.length > 0) {
115
146
  scanResults.matches = scanResults.matches.concat(result.value);
@@ -118,10 +149,10 @@ export async function scanFilesForKeyValues({ env, keys, filePaths, base }) {
118
149
  return scanResults;
119
150
  }
120
151
  const searchStream = (basePath, file, keyValues) => {
121
- return new Promise((resolve) => {
152
+ return new Promise((resolve, reject) => {
122
153
  const filePath = path.resolve(basePath, file);
123
154
  const inStream = createReadStream(filePath);
124
- const rl = createInterface(inStream);
155
+ const rl = createInterface({ input: inStream, terminal: false });
125
156
  const matches = [];
126
157
  const keyVals = [].concat(...Object.values(keyValues));
127
158
  function getKeyForValue(val) {
@@ -138,7 +169,6 @@ const searchStream = (basePath, file, keyValues) => {
138
169
  keyVals.forEach((valVariant) => {
139
170
  maxMultiLineCount = Math.max(maxMultiLineCount, valVariant.split('\n').length);
140
171
  });
141
- // search if not multi line or current accumulated lines length is less than num of lines
142
172
  const lines = [];
143
173
  let lineNumber = 0;
144
174
  rl.on('line', function (line) {
@@ -156,7 +186,7 @@ const searchStream = (basePath, file, keyValues) => {
156
186
  }
157
187
  keyVals.forEach((valVariant) => {
158
188
  // matching of single/whole values
159
- if (line.search(new RegExp(valVariant)) >= 0) {
189
+ if (line.includes(valVariant)) {
160
190
  matches.push({
161
191
  file,
162
192
  lineNumber,
@@ -210,6 +240,15 @@ const searchStream = (basePath, file, keyValues) => {
210
240
  });
211
241
  }
212
242
  });
243
+ rl.on('error', function (error) {
244
+ if (error?.code === 'EISDIR') {
245
+ // file path is a directory - do nothing
246
+ resolve(matches);
247
+ }
248
+ else {
249
+ reject(error);
250
+ }
251
+ });
213
252
  rl.on('close', function () {
214
253
  resolve(matches);
215
254
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netlify/build",
3
- "version": "29.15.2",
3
+ "version": "29.15.4",
4
4
  "description": "Netlify build module",
5
5
  "type": "module",
6
6
  "exports": "./lib/core/main.js",
@@ -68,11 +68,11 @@
68
68
  "@netlify/config": "^20.5.2",
69
69
  "@netlify/edge-bundler": "8.16.2",
70
70
  "@netlify/framework-info": "^9.8.10",
71
- "@netlify/functions-utils": "^5.2.15",
71
+ "@netlify/functions-utils": "^5.2.17",
72
72
  "@netlify/git-utils": "^5.1.1",
73
73
  "@netlify/plugins-list": "^6.68.0",
74
74
  "@netlify/run-utils": "^5.1.1",
75
- "@netlify/zip-it-and-ship-it": "9.12.0",
75
+ "@netlify/zip-it-and-ship-it": "9.12.2",
76
76
  "@opentelemetry/api": "^1.4.1",
77
77
  "@sindresorhus/slugify": "^2.0.0",
78
78
  "ansi-escapes": "^6.0.0",
@@ -148,5 +148,5 @@
148
148
  "module": "commonjs"
149
149
  }
150
150
  },
151
- "gitHead": "2bb08a1fb4ad1bca50cee1131b70cbd94a9f08a3"
151
+ "gitHead": "c274972d83e29fcde2496b765c141521ffcdd4c1"
152
152
  }