configorama 1.0.0 → 1.0.2
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.
- package/package.json +1 -1
- package/src/main.js +16 -2
- package/src/metadata.js +7 -0
- package/src/resolvers/valueFromFile.js +16 -3
- package/src/utils/introspection/audit.js +9 -2
- package/src/utils/introspection/model.js +36 -6
- package/src/utils/parsing/enrichMetadata.js +21 -4
- package/src/utils/security/dotenvFileRefs.js +134 -0
- package/src/utils/security/dotenvFileRefs.test.js +47 -0
package/package.json
CHANGED
package/src/main.js
CHANGED
|
@@ -49,6 +49,20 @@ function isNestedFilterArgument(property, matchedString) {
|
|
|
49
49
|
const closeParenIdx = property.indexOf(')', matchIdx)
|
|
50
50
|
return pipeIdx !== -1 && matchIdx > pipeIdx && openParenIdx > pipeIdx && closeParenIdx > matchIdx
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
function resolveConfigFilePath(filePath) {
|
|
54
|
+
const absolutePath = path.resolve(filePath)
|
|
55
|
+
try {
|
|
56
|
+
return fs.realpathSync(absolutePath)
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return absolutePath
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getConfigFileDirectory(filePath) {
|
|
63
|
+
return path.dirname(resolveConfigFilePath(filePath))
|
|
64
|
+
}
|
|
65
|
+
|
|
52
66
|
const dotProp = require('dot-prop')
|
|
53
67
|
|
|
54
68
|
function resolveStaticFilterArg(arg, config) {
|
|
@@ -212,7 +226,7 @@ class Configorama {
|
|
|
212
226
|
}
|
|
213
227
|
|
|
214
228
|
this.safetyPolicy = normalizeSafetyPolicy(this.settings, {
|
|
215
|
-
configDir: options.configDir || (typeof fileOrObject === 'string' ?
|
|
229
|
+
configDir: options.configDir || (typeof fileOrObject === 'string' ? getConfigFileDirectory(fileOrObject) : process.cwd())
|
|
216
230
|
})
|
|
217
231
|
|
|
218
232
|
assertCustomResolversAllowed(options.variableSources, this.safetyPolicy)
|
|
@@ -317,7 +331,7 @@ class Configorama {
|
|
|
317
331
|
assertSafeConfigInput(fileOrObject, this.safetyPolicy)
|
|
318
332
|
// read and parse file
|
|
319
333
|
const fileContents = fs.readFileSync(fileOrObject, 'utf-8')
|
|
320
|
-
const fileDirectory =
|
|
334
|
+
const fileDirectory = getConfigFileDirectory(fileOrObject)
|
|
321
335
|
const fileType = path.extname(fileOrObject)
|
|
322
336
|
|
|
323
337
|
this.configFilePath = fileOrObject
|
package/src/metadata.js
CHANGED
|
@@ -9,6 +9,7 @@ const { normalizePath, extractFilePath, resolveInnerVariables } = require('./uti
|
|
|
9
9
|
const { shouldIgnorePath } = require('./utils/paths/ignorePaths')
|
|
10
10
|
const { findNestedVariables } = require('./utils/variables/findNestedVariables')
|
|
11
11
|
const { splitOnPipe } = require('./utils/strings/splitOnPipe')
|
|
12
|
+
const { applyDotenvFileRefMetadata } = require('./utils/security/dotenvFileRefs')
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Collect metadata about all variables found in the configuration
|
|
@@ -329,6 +330,12 @@ function collectVariableMetadata({
|
|
|
329
330
|
containsVariables,
|
|
330
331
|
exists: fileExists,
|
|
331
332
|
}
|
|
333
|
+
applyDotenvFileRefMetadata(configPathEntry, {
|
|
334
|
+
filePath: absolutePath,
|
|
335
|
+
relativePath: resolvedPath,
|
|
336
|
+
originalVariableString: rawValue,
|
|
337
|
+
resolvedVariableString: resolvedVarString,
|
|
338
|
+
})
|
|
332
339
|
if (globPattern) {
|
|
333
340
|
configPathEntry.pattern = globPattern
|
|
334
341
|
}
|
|
@@ -9,6 +9,7 @@ const { findNestedVariables } = require('../utils/variables/findNestedVariables'
|
|
|
9
9
|
const { makeBox } = require('@davidwells/box-logger')
|
|
10
10
|
const { encodeJsSyntax, decodeJsonInVariable, hasEncodedJson } = require('../utils/encoders/js-fixes')
|
|
11
11
|
const { checkFileAccess } = require('../utils/security/safetyPolicy')
|
|
12
|
+
const { applyDotenvFileRefMetadata, isIniLikeFilePath } = require('../utils/security/dotenvFileRefs')
|
|
12
13
|
|
|
13
14
|
/* File Parsers */
|
|
14
15
|
const YAML = require('../parsers/yaml')
|
|
@@ -85,7 +86,7 @@ function parseFileContents(content, filePath) {
|
|
|
85
86
|
if (ext === 'toml' || ext === 'tml') {
|
|
86
87
|
return TOML.parse(content)
|
|
87
88
|
}
|
|
88
|
-
if (ext
|
|
89
|
+
if (isIniLikeExtension(ext, filePath)) {
|
|
89
90
|
return INI.parse(content)
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -93,6 +94,10 @@ function parseFileContents(content, filePath) {
|
|
|
93
94
|
return content
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
function isIniLikeExtension(ext, filePath) {
|
|
98
|
+
return ext === 'ini' || ext === 'env' || isIniLikeFilePath(filePath)
|
|
99
|
+
}
|
|
100
|
+
|
|
96
101
|
/**
|
|
97
102
|
* Resolves a value from a file reference
|
|
98
103
|
* @param {object} ctx - Context object with instance properties
|
|
@@ -201,6 +206,14 @@ async function getValueFromFile(ctx, variableString, options) {
|
|
|
201
206
|
containsVariables: options.context.value !== options.context.originalSource,
|
|
202
207
|
exists,
|
|
203
208
|
}
|
|
209
|
+
applyDotenvFileRefMetadata(fileRefEntry, {
|
|
210
|
+
filePath: fullFilePath,
|
|
211
|
+
relativePath,
|
|
212
|
+
resolvedPath,
|
|
213
|
+
variableString,
|
|
214
|
+
originalVariableString: options.context.originalSource,
|
|
215
|
+
resolvedVariableString: options.context.value,
|
|
216
|
+
})
|
|
204
217
|
|
|
205
218
|
if (wasOverridden) {
|
|
206
219
|
fileRefEntry.wasOverridden = true
|
|
@@ -407,7 +420,7 @@ ${JSON.stringify(options.context, null, 2)}`,
|
|
|
407
420
|
if (fileExtension === 'toml' || fileExtension === 'tml') {
|
|
408
421
|
valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
|
|
409
422
|
}
|
|
410
|
-
if (fileExtension
|
|
423
|
+
if (isIniLikeExtension(fileExtension, relativePath)) {
|
|
411
424
|
valueToPopulate = INI.toJson(valueToPopulate)
|
|
412
425
|
}
|
|
413
426
|
if (fileExtension === 'tf' || fileExtension === 'hcl') {
|
|
@@ -447,7 +460,7 @@ Please use ":" or "." to reference sub properties. ${deepPropertiesStr}`
|
|
|
447
460
|
return Promise.resolve(valueToPopulate)
|
|
448
461
|
}
|
|
449
462
|
|
|
450
|
-
if (fileExtension
|
|
463
|
+
if (isIniLikeExtension(fileExtension, relativePath)) {
|
|
451
464
|
valueToPopulate = INI.parse(valueToPopulate)
|
|
452
465
|
return Promise.resolve(valueToPopulate)
|
|
453
466
|
}
|
|
@@ -12,7 +12,7 @@ function buildAuditReport(introspection, options = {}) {
|
|
|
12
12
|
|
|
13
13
|
for (const node of introspection.nodes || []) {
|
|
14
14
|
if (!node.risk || node.risk === 'none') continue
|
|
15
|
-
|
|
15
|
+
const finding = {
|
|
16
16
|
id: node.id,
|
|
17
17
|
severity: node.severity || severityForRisk(node.risk),
|
|
18
18
|
risk: node.risk,
|
|
@@ -22,7 +22,12 @@ function buildAuditReport(introspection, options = {}) {
|
|
|
22
22
|
relativePath: node.relativePath,
|
|
23
23
|
configPaths: node.paths || [],
|
|
24
24
|
message: messageForNode(node),
|
|
25
|
-
}
|
|
25
|
+
}
|
|
26
|
+
if (node.sensitive === true) finding.sensitive = true
|
|
27
|
+
if (node.sensitivityReason) finding.sensitivityReason = node.sensitivityReason
|
|
28
|
+
if (node.dotenvFile !== undefined) finding.dotenvFile = node.dotenvFile
|
|
29
|
+
if (node.dotenvReadScope) finding.dotenvReadScope = node.dotenvReadScope
|
|
30
|
+
findings.push(finding)
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
if (options.dotenv === true) {
|
|
@@ -68,6 +73,8 @@ function buildAuditReport(introspection, options = {}) {
|
|
|
68
73
|
function messageForNode(node) {
|
|
69
74
|
if (node.risk === 'executable_code') return 'Reference may execute JavaScript or TypeScript.'
|
|
70
75
|
if (node.risk === 'process_spawn') return 'Reference may spawn a git process.'
|
|
76
|
+
if (node.sensitivityReason === 'dotenv_file' && node.dotenvReadScope === 'full_file') return 'Reference reads an entire dotenv file; resolved output may contain secrets.'
|
|
77
|
+
if (node.sensitivityReason === 'dotenv_file') return 'Reference reads a key from a dotenv file.'
|
|
71
78
|
if (node.risk === 'local_file_read') return 'Reference reads a local file.'
|
|
72
79
|
if (node.risk === 'data_flow_expression') return 'Expression can read resolved config values but is not JavaScript execution.'
|
|
73
80
|
return `Risk surface: ${node.risk}`
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
2
|
const { EXECUTABLE_EXTENSIONS } = require('../security/safetyPolicy')
|
|
3
3
|
const { redactRequirementValue } = require('../redaction/redact')
|
|
4
|
+
const { getDotenvFileRefMetadata } = require('../security/dotenvFileRefs')
|
|
4
5
|
|
|
5
6
|
const SCHEMA_VERSION = 1
|
|
6
7
|
|
|
@@ -42,6 +43,25 @@ function severityForRisk(risk) {
|
|
|
42
43
|
return 'info'
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function severityForNode(risk, metadata) {
|
|
47
|
+
if (metadata && metadata.dotenvFile && metadata.dotenvReadScope === 'full_file') return 'medium'
|
|
48
|
+
return severityForRisk(risk)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function firstOccurrenceVariable(entry) {
|
|
52
|
+
const occurrence = entry && Array.isArray(entry.occurrences) ? entry.occurrences[0] : null
|
|
53
|
+
return occurrence ? (occurrence.varMatch || occurrence.originalString) : null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assignSensitivity(target, metadata) {
|
|
57
|
+
if (!metadata) return target
|
|
58
|
+
if (metadata.sensitive === true) target.sensitive = true
|
|
59
|
+
if (metadata.sensitivityReason) target.sensitivityReason = metadata.sensitivityReason
|
|
60
|
+
if (metadata.dotenvFile !== undefined) target.dotenvFile = metadata.dotenvFile
|
|
61
|
+
if (metadata.dotenvReadScope) target.dotenvReadScope = metadata.dotenvReadScope
|
|
62
|
+
return target
|
|
63
|
+
}
|
|
64
|
+
|
|
45
65
|
function buildIntrospection(enrichedMetadata = {}, options = {}) {
|
|
46
66
|
const uniqueVariables = enrichedMetadata.uniqueVariables || {}
|
|
47
67
|
const requirements = options.requirements || []
|
|
@@ -55,6 +75,11 @@ function buildIntrospection(enrichedMetadata = {}, options = {}) {
|
|
|
55
75
|
const variableType = normalizeVariableType(entry.variableType)
|
|
56
76
|
const requirement = requirementsByVariable.get(variable)
|
|
57
77
|
const risk = riskForVariable(variableType, variable)
|
|
78
|
+
const dotenvMetadata = getDotenvFileRefMetadata({
|
|
79
|
+
...entry,
|
|
80
|
+
variable,
|
|
81
|
+
originalVariableString: firstOccurrenceVariable(entry),
|
|
82
|
+
})
|
|
58
83
|
const node = {
|
|
59
84
|
id: `variable:${variable}`,
|
|
60
85
|
kind: variableType === 'file' || variableType === 'text'
|
|
@@ -64,10 +89,11 @@ function buildIntrospection(enrichedMetadata = {}, options = {}) {
|
|
|
64
89
|
variableType,
|
|
65
90
|
sourceClass: entry.variableSourceType || entry.sourceClass || requirement?.sourceClass || null,
|
|
66
91
|
risk,
|
|
67
|
-
severity:
|
|
92
|
+
severity: severityForNode(risk, dotenvMetadata || entry),
|
|
68
93
|
paths: [...new Set((entry.occurrences || []).map(occ => occ.path).filter(Boolean))].sort(),
|
|
69
|
-
sensitive: requirement ? requirement.sensitive === true :
|
|
94
|
+
sensitive: requirement ? requirement.sensitive === true : entry.sensitive === true,
|
|
70
95
|
}
|
|
96
|
+
assignSensitivity(node, dotenvMetadata || entry)
|
|
71
97
|
if (requirement) {
|
|
72
98
|
node.required = requirement.required
|
|
73
99
|
node.default = redactRequirementValue(requirement, requirement.default)
|
|
@@ -104,16 +130,20 @@ function buildIntrospection(enrichedMetadata = {}, options = {}) {
|
|
|
104
130
|
const fileDeps = enrichedMetadata.fileDependencies || {}
|
|
105
131
|
for (const dep of fileDeps.byConfigPath || []) {
|
|
106
132
|
const id = `file:${dep.relativePath || dep.filePath}`
|
|
133
|
+
const dotenvMetadata = getDotenvFileRefMetadata(dep)
|
|
134
|
+
const risk = EXECUTABLE_EXTENSIONS.has(path.extname(dep.relativePath || dep.filePath || '').toLowerCase()) ? 'executable_code' : 'local_file_read'
|
|
107
135
|
if (!nodes.some(node => node.id === id)) {
|
|
108
|
-
|
|
136
|
+
const node = {
|
|
109
137
|
id,
|
|
110
138
|
kind: EXECUTABLE_EXTENSIONS.has(path.extname(dep.relativePath || dep.filePath || '').toLowerCase()) ? 'executable' : 'file',
|
|
111
139
|
path: dep.filePath,
|
|
112
140
|
relativePath: dep.relativePath,
|
|
113
141
|
exists: dep.exists,
|
|
114
|
-
risk
|
|
115
|
-
severity:
|
|
116
|
-
}
|
|
142
|
+
risk,
|
|
143
|
+
severity: severityForNode(risk, dotenvMetadata || dep),
|
|
144
|
+
}
|
|
145
|
+
assignSensitivity(node, dotenvMetadata || dep)
|
|
146
|
+
nodes.push(node)
|
|
117
147
|
}
|
|
118
148
|
if (dep.location) {
|
|
119
149
|
edges.push({
|
|
@@ -5,6 +5,7 @@ const { normalizePath, extractFilePath, normalizeFileVariable, resolveInnerVaria
|
|
|
5
5
|
const { preResolveString, preResolveSingle } = require('../resolution/preResolveVariable')
|
|
6
6
|
const { parseOneOfFilter } = require('../filters/oneOf')
|
|
7
7
|
const { extractComment } = require('./extractComment')
|
|
8
|
+
const { applyDotenvFileRefMetadata, normalizeDotenvFileVariable } = require('../security/dotenvFileRefs')
|
|
8
9
|
|
|
9
10
|
// Type filters that indicate expected value types
|
|
10
11
|
const TYPE_FILTERS = ['Boolean', 'String', 'Number', 'Array', 'Object', 'Json']
|
|
@@ -109,6 +110,12 @@ function getSourceForType(variableType, variableTypes) {
|
|
|
109
110
|
return typeDef?.source
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
function sameFileRefPath(left, right) {
|
|
114
|
+
if (!left || !right) return false
|
|
115
|
+
const normalize = value => String(value).replace(/^\.\//, '')
|
|
116
|
+
return normalize(left) === normalize(right)
|
|
117
|
+
}
|
|
118
|
+
|
|
112
119
|
/**
|
|
113
120
|
* Enriches variable metadata with resolution tracking data.
|
|
114
121
|
* @param {object} metadata - The metadata object from collectVariableMetadata.
|
|
@@ -388,6 +395,7 @@ async function enrichMetadata(
|
|
|
388
395
|
containsVariables: !!ref.hasInnerVariable,
|
|
389
396
|
exists: details.exists,
|
|
390
397
|
}
|
|
398
|
+
applyDotenvFileRefMetadata(confDetails, details)
|
|
391
399
|
if (ref.pattern) {
|
|
392
400
|
confDetails.pattern = ref.pattern
|
|
393
401
|
}
|
|
@@ -478,7 +486,7 @@ async function enrichMetadata(
|
|
|
478
486
|
}
|
|
479
487
|
|
|
480
488
|
// Normalize file() and text() references
|
|
481
|
-
baseVar = normalizeFileVariable(baseVar)
|
|
489
|
+
baseVar = normalizeDotenvFileVariable(baseVar) || normalizeFileVariable(baseVar)
|
|
482
490
|
|
|
483
491
|
if (!uniqueVariablesMap.has(baseVar)) {
|
|
484
492
|
uniqueVariablesMap.set(baseVar, {
|
|
@@ -536,7 +544,7 @@ async function enrichMetadata(
|
|
|
536
544
|
const siblingBaseVar = detail.valueBeforeFallback || detail.variable
|
|
537
545
|
|
|
538
546
|
// Normalize file/text references for sibling too
|
|
539
|
-
const normalizedSiblingVar = normalizeFileVariable(siblingBaseVar)
|
|
547
|
+
const normalizedSiblingVar = normalizeDotenvFileVariable(siblingBaseVar) || normalizeFileVariable(siblingBaseVar)
|
|
540
548
|
|
|
541
549
|
// Create or get entry for this sibling variable
|
|
542
550
|
if (!uniqueVariablesMap.has(normalizedSiblingVar)) {
|
|
@@ -665,7 +673,7 @@ async function enrichMetadata(
|
|
|
665
673
|
}
|
|
666
674
|
|
|
667
675
|
// Normalize file paths after variable substitution
|
|
668
|
-
resolvedVariable = normalizeFileVariable(resolvedVariable)
|
|
676
|
+
resolvedVariable = normalizeDotenvFileVariable(resolvedVariable) || normalizeFileVariable(resolvedVariable)
|
|
669
677
|
|
|
670
678
|
// Update the variable to the resolved version and update map key
|
|
671
679
|
if (resolvedVariable !== baseVar) {
|
|
@@ -712,13 +720,22 @@ async function enrichMetadata(
|
|
|
712
720
|
|
|
713
721
|
if (!hasVariables) {
|
|
714
722
|
// Look up in fileRefsFound to see if file exists
|
|
715
|
-
const fileRef = fileRefsFound.find(ref => ref.relativePath
|
|
723
|
+
const fileRef = fileRefsFound.find(ref => sameFileRefPath(ref.relativePath, filePath))
|
|
716
724
|
if (fileRef) {
|
|
717
725
|
entry.fileExists = fileRef.exists
|
|
726
|
+
applyDotenvFileRefMetadata(entry, {
|
|
727
|
+
...fileRef,
|
|
728
|
+
variable: entry.variable,
|
|
729
|
+
})
|
|
718
730
|
} else if (configPath) {
|
|
719
731
|
const thePath = path.resolve(path.dirname(configPath), filePath)
|
|
720
732
|
const fileExists = fs.existsSync(thePath)
|
|
721
733
|
entry.fileExists = fileExists
|
|
734
|
+
applyDotenvFileRefMetadata(entry, {
|
|
735
|
+
filePath: thePath,
|
|
736
|
+
relativePath: filePath,
|
|
737
|
+
variable: entry.variable,
|
|
738
|
+
})
|
|
722
739
|
}
|
|
723
740
|
}
|
|
724
741
|
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const path = require('path')
|
|
2
|
+
const { normalizePath } = require('../paths/filePathUtils')
|
|
3
|
+
const { splitCsv } = require('../strings/splitCsv')
|
|
4
|
+
const { trimSurroundingQuotes } = require('../strings/quoteUtils')
|
|
5
|
+
|
|
6
|
+
const DOTENV_SENSITIVITY_REASON = 'dotenv_file'
|
|
7
|
+
|
|
8
|
+
function isDotenvFilePath(filePath) {
|
|
9
|
+
if (!filePath || typeof filePath !== 'string') return false
|
|
10
|
+
const cleanPath = filePath.trim().replace(/^["']|["']$/g, '')
|
|
11
|
+
const baseName = path.basename(cleanPath)
|
|
12
|
+
return baseName === '.env' || baseName.startsWith('.env.')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hasFileAccessor(variableString) {
|
|
16
|
+
const expression = extractFileExpression(variableString)
|
|
17
|
+
if (!expression) return false
|
|
18
|
+
const suffix = String(variableString || '').slice(expression.end).trim()
|
|
19
|
+
return suffix.startsWith(':') || suffix.startsWith('.')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractFileExpression(variableString) {
|
|
23
|
+
const value = String(variableString || '')
|
|
24
|
+
const prefixMatch = /(?:^|[^\w.])((?:file|text)\()/.exec(value)
|
|
25
|
+
if (!prefixMatch) return null
|
|
26
|
+
|
|
27
|
+
const expressionStart = prefixMatch.index + prefixMatch[0].indexOf(prefixMatch[1])
|
|
28
|
+
const openParenIndex = expressionStart + prefixMatch[1].length - 1
|
|
29
|
+
let depth = 1
|
|
30
|
+
let index = openParenIndex + 1
|
|
31
|
+
|
|
32
|
+
while (index < value.length && depth > 0) {
|
|
33
|
+
if (value[index] === '(') depth++
|
|
34
|
+
else if (value[index] === ')') depth--
|
|
35
|
+
index++
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (depth !== 0) return null
|
|
39
|
+
|
|
40
|
+
const fileContent = value.substring(openParenIndex + 1, index - 1).trim()
|
|
41
|
+
if (!fileContent) return null
|
|
42
|
+
|
|
43
|
+
const parts = splitCsv(fileContent, undefined, { protectVariables: true })
|
|
44
|
+
const filePath = trimSurroundingQuotes(parts[0].trim(), false)
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
filePath,
|
|
48
|
+
expression: value.slice(expressionStart, index),
|
|
49
|
+
start: expressionStart,
|
|
50
|
+
end: index,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function extractDotenvPath(source) {
|
|
55
|
+
const candidates = [
|
|
56
|
+
source.relativePath,
|
|
57
|
+
source.resolvedPath,
|
|
58
|
+
source.filePath,
|
|
59
|
+
source.fullFilePath,
|
|
60
|
+
].filter(Boolean)
|
|
61
|
+
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
if (isDotenvFilePath(candidate)) return candidate
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const variableCandidates = [
|
|
67
|
+
source.variable,
|
|
68
|
+
source.variableString,
|
|
69
|
+
source.originalVariableString,
|
|
70
|
+
source.resolvedVariableString,
|
|
71
|
+
].filter(Boolean)
|
|
72
|
+
|
|
73
|
+
for (const candidate of variableCandidates) {
|
|
74
|
+
const extracted = extractFileExpression(String(candidate))
|
|
75
|
+
if (extracted && isDotenvFilePath(extracted.filePath)) {
|
|
76
|
+
return extracted.filePath
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getDotenvFileRefMetadata(source = {}) {
|
|
84
|
+
const dotenvPath = extractDotenvPath(source)
|
|
85
|
+
if (!dotenvPath) return null
|
|
86
|
+
|
|
87
|
+
const scopeSource = source.variableString ||
|
|
88
|
+
source.variable ||
|
|
89
|
+
source.originalVariableString ||
|
|
90
|
+
source.resolvedVariableString ||
|
|
91
|
+
''
|
|
92
|
+
const accessorScope = hasFileAccessor(scopeSource) ? 'key' : null
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
sensitive: true,
|
|
96
|
+
sensitivityReason: DOTENV_SENSITIVITY_REASON,
|
|
97
|
+
dotenvFile: true,
|
|
98
|
+
dotenvReadScope: accessorScope || source.dotenvReadScope || 'full_file',
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeDotenvFileVariable(variableString) {
|
|
103
|
+
const value = String(variableString || '')
|
|
104
|
+
const expression = extractFileExpression(value)
|
|
105
|
+
if (!expression || !isDotenvFilePath(expression.filePath)) return null
|
|
106
|
+
|
|
107
|
+
const normalizedPath = normalizePath(expression.filePath) || expression.filePath
|
|
108
|
+
const normalizedExpression = expression.expression.replace(/\(([\s\S]*)\)$/, `(${normalizedPath})`)
|
|
109
|
+
const suffix = value.slice(expression.end).trim()
|
|
110
|
+
const accessor = suffix.match(/^([:.][\w.[\]-]+)/)
|
|
111
|
+
|
|
112
|
+
return accessor ? `${normalizedExpression}${accessor[1]}` : normalizedExpression
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isIniLikeFilePath(filePath) {
|
|
116
|
+
const ext = path.extname(String(filePath || '')).slice(1).toLowerCase()
|
|
117
|
+
return ext === 'ini' || isDotenvFilePath(filePath)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function applyDotenvFileRefMetadata(target, source) {
|
|
121
|
+
const metadata = getDotenvFileRefMetadata(source)
|
|
122
|
+
if (metadata) Object.assign(target, metadata)
|
|
123
|
+
return target
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
DOTENV_SENSITIVITY_REASON,
|
|
128
|
+
applyDotenvFileRefMetadata,
|
|
129
|
+
getDotenvFileRefMetadata,
|
|
130
|
+
hasFileAccessor,
|
|
131
|
+
normalizeDotenvFileVariable,
|
|
132
|
+
isDotenvFilePath,
|
|
133
|
+
isIniLikeFilePath,
|
|
134
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const {
|
|
4
|
+
getDotenvFileRefMetadata,
|
|
5
|
+
hasFileAccessor,
|
|
6
|
+
isDotenvFilePath,
|
|
7
|
+
normalizeDotenvFileVariable,
|
|
8
|
+
} = require('./dotenvFileRefs')
|
|
9
|
+
|
|
10
|
+
test('dotenv file detection matches .env and .env variants', () => {
|
|
11
|
+
assert.is(isDotenvFilePath('.env'), true)
|
|
12
|
+
assert.is(isDotenvFilePath('./.env.production'), true)
|
|
13
|
+
assert.is(isDotenvFilePath('/tmp/app/.env.local'), true)
|
|
14
|
+
assert.is(isDotenvFilePath('./env.yml'), false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('dotenv accessors are detected without assuming ${} syntax', () => {
|
|
18
|
+
assert.is(hasFileAccessor('file(.env).API_KEY'), true)
|
|
19
|
+
assert.is(hasFileAccessor('file(.env):TOKEN'), true)
|
|
20
|
+
assert.is(hasFileAccessor('${file(.env).API_KEY}'), true)
|
|
21
|
+
assert.is(hasFileAccessor('[[file(.env).API_KEY]]'), true)
|
|
22
|
+
assert.is(hasFileAccessor('file(.env)'), false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('dotenv metadata classifies full-file and key reads', () => {
|
|
26
|
+
assert.equal(getDotenvFileRefMetadata({ variable: 'file(.env)' }), {
|
|
27
|
+
sensitive: true,
|
|
28
|
+
sensitivityReason: 'dotenv_file',
|
|
29
|
+
dotenvFile: true,
|
|
30
|
+
dotenvReadScope: 'full_file',
|
|
31
|
+
})
|
|
32
|
+
assert.equal(getDotenvFileRefMetadata({ variable: '[[file(.env):TOKEN]]' }), {
|
|
33
|
+
sensitive: true,
|
|
34
|
+
sensitivityReason: 'dotenv_file',
|
|
35
|
+
dotenvFile: true,
|
|
36
|
+
dotenvReadScope: 'key',
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('dotenv variable normalization preserves key accessors', () => {
|
|
41
|
+
assert.is(normalizeDotenvFileVariable('file(.env)'), 'file(./.env)')
|
|
42
|
+
assert.is(normalizeDotenvFileVariable('file(.env).API_KEY'), 'file(./.env).API_KEY')
|
|
43
|
+
assert.is(normalizeDotenvFileVariable('file(.env):TOKEN'), 'file(./.env):TOKEN')
|
|
44
|
+
assert.is(normalizeDotenvFileVariable('file(config.yml):value'), null)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test.run()
|