@netlify/build 29.15.2 → 29.15.3
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({
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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.
|
|
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.
|
|
3
|
+
"version": "29.15.3",
|
|
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.
|
|
71
|
+
"@netlify/functions-utils": "^5.2.16",
|
|
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.
|
|
75
|
+
"@netlify/zip-it-and-ship-it": "9.12.1",
|
|
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": "
|
|
151
|
+
"gitHead": "fcb303b3cddd35a4a503f88bb685d6d824bd719b"
|
|
152
152
|
}
|