configorama 1.0.1 → 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/metadata.js +7 -0
- package/src/resolvers/valueFromFile.js +14 -5
- 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/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 (isIniLikeExtension(ext)) {
|
|
89
|
+
if (isIniLikeExtension(ext, filePath)) {
|
|
89
90
|
return INI.parse(content)
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -93,8 +94,8 @@ function parseFileContents(content, filePath) {
|
|
|
93
94
|
return content
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
function isIniLikeExtension(ext) {
|
|
97
|
-
return ext === 'ini' || ext === 'env'
|
|
97
|
+
function isIniLikeExtension(ext, filePath) {
|
|
98
|
+
return ext === 'ini' || ext === 'env' || isIniLikeFilePath(filePath)
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
/**
|
|
@@ -205,6 +206,14 @@ async function getValueFromFile(ctx, variableString, options) {
|
|
|
205
206
|
containsVariables: options.context.value !== options.context.originalSource,
|
|
206
207
|
exists,
|
|
207
208
|
}
|
|
209
|
+
applyDotenvFileRefMetadata(fileRefEntry, {
|
|
210
|
+
filePath: fullFilePath,
|
|
211
|
+
relativePath,
|
|
212
|
+
resolvedPath,
|
|
213
|
+
variableString,
|
|
214
|
+
originalVariableString: options.context.originalSource,
|
|
215
|
+
resolvedVariableString: options.context.value,
|
|
216
|
+
})
|
|
208
217
|
|
|
209
218
|
if (wasOverridden) {
|
|
210
219
|
fileRefEntry.wasOverridden = true
|
|
@@ -411,7 +420,7 @@ ${JSON.stringify(options.context, null, 2)}`,
|
|
|
411
420
|
if (fileExtension === 'toml' || fileExtension === 'tml') {
|
|
412
421
|
valueToPopulate = JSON.stringify(TOML.parse(valueToPopulate))
|
|
413
422
|
}
|
|
414
|
-
if (isIniLikeExtension(fileExtension)) {
|
|
423
|
+
if (isIniLikeExtension(fileExtension, relativePath)) {
|
|
415
424
|
valueToPopulate = INI.toJson(valueToPopulate)
|
|
416
425
|
}
|
|
417
426
|
if (fileExtension === 'tf' || fileExtension === 'hcl') {
|
|
@@ -451,7 +460,7 @@ Please use ":" or "." to reference sub properties. ${deepPropertiesStr}`
|
|
|
451
460
|
return Promise.resolve(valueToPopulate)
|
|
452
461
|
}
|
|
453
462
|
|
|
454
|
-
if (isIniLikeExtension(fileExtension)) {
|
|
463
|
+
if (isIniLikeExtension(fileExtension, relativePath)) {
|
|
455
464
|
valueToPopulate = INI.parse(valueToPopulate)
|
|
456
465
|
return Promise.resolve(valueToPopulate)
|
|
457
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()
|