configorama 0.11.0 → 1.0.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.
- package/README.md +429 -123
- package/cli.js +282 -49
- package/index.d.ts +43 -1
- package/package.json +5 -1
- package/src/capabilities.js +59 -0
- package/src/capabilities.test.js +44 -0
- package/src/display.js +70 -7
- package/src/display.test.js +82 -0
- package/src/errors.js +73 -0
- package/src/index.js +91 -1
- package/src/main.js +159 -19
- package/src/parsers/esm.js +1 -16
- package/src/parsers/typescript.js +1 -48
- package/src/resolvers/valueFromCron.js +4 -25
- package/src/resolvers/valueFromEval.js +11 -1
- package/src/resolvers/valueFromFile.js +8 -1
- package/src/resolvers/valueFromGit.js +43 -17
- package/src/resolvers/valueFromOptions.js +5 -4
- package/src/utils/filters/filterArgs.js +57 -0
- package/src/utils/filters/oneOf.js +77 -0
- package/src/utils/introspection/audit.js +78 -0
- package/src/utils/introspection/graph.js +43 -0
- package/src/utils/introspection/model.js +150 -0
- package/src/utils/introspection/model.test.js +93 -0
- package/src/utils/parsing/commentAnnotations.js +107 -0
- package/src/utils/parsing/commentAnnotations.test.js +123 -0
- package/src/utils/parsing/enrichMetadata.js +64 -1
- package/src/utils/parsing/enrichMetadata.test.js +84 -0
- package/src/utils/parsing/extractComment.js +145 -0
- package/src/utils/parsing/extractComment.test.js +182 -0
- package/src/utils/parsing/preProcess.js +2 -1
- package/src/utils/paths/findLineForKey.js +2 -2
- package/src/utils/paths/ignorePaths.js +22 -9
- package/src/utils/redaction/redact.js +78 -0
- package/src/utils/redaction/redact.test.js +38 -0
- package/src/utils/redaction/setupRedaction.js +47 -0
- package/src/utils/redaction/setupRedaction.test.js +68 -0
- package/src/utils/requirements/configRequirements.js +351 -0
- package/src/utils/requirements/configRequirements.test.js +380 -0
- package/src/utils/requirements/serializeRequirements.js +120 -0
- package/src/utils/requirements/serializeRequirements.test.js +211 -0
- package/src/utils/security/evalSafety.js +86 -0
- package/src/utils/security/evalSafety.test.js +61 -0
- package/src/utils/security/safetyPolicy.js +110 -0
- package/src/utils/security/safetyPolicy.test.js +29 -0
- package/src/utils/strings/didYouMean.js +70 -0
- package/src/utils/strings/didYouMean.test.js +52 -0
- package/src/utils/strings/formatFunctionArgs.js +6 -1
- package/src/utils/strings/splitByComma.js +5 -0
- package/src/utils/ui/configWizard.js +208 -34
- package/src/utils/ui/createEditorLink.js +17 -1
- package/src/utils/ui/promptDescriptors.js +196 -0
- package/src/utils/ui/promptDescriptors.test.js +162 -0
- package/src/utils/variables/cleanVariable.js +22 -0
- package/src/utils/variables/getVariableType.js +1 -0
- package/types/src/index.d.ts +0 -24
- package/types/src/index.d.ts.map +1 -1
- package/types/src/main.d.ts +16 -8
- package/types/src/main.d.ts.map +1 -1
- package/types/src/resolvers/valueFromFile.d.ts +0 -2
- package/types/src/resolvers/valueFromFile.d.ts.map +1 -1
- package/types/src/resolvers/valueFromGit.d.ts.map +1 -1
- package/types/src/resolvers/valueFromSelf.d.ts +1 -0
- package/types/src/resolvers/valueFromSelf.d.ts.map +1 -0
- package/types/src/utils/parsing/parse.d.ts.map +1 -1
- package/types/src/utils/parsing/preProcess.d.ts.map +1 -1
- package/types/src/utils/paths/findLineForKey.d.ts +0 -9
- package/types/src/utils/paths/findLineForKey.d.ts.map +1 -1
- package/types/src/utils/strings/replaceAll.d.ts.map +1 -1
- package/types/src/utils/variables/variableUtils.d.ts +1 -1
- package/types/src/display.d.ts +0 -62
- package/types/src/display.d.ts.map +0 -1
- package/types/src/metadata.d.ts +0 -28
- package/types/src/metadata.d.ts.map +0 -1
- package/types/src/utils/BoundedMap.d.ts +0 -10
- package/types/src/utils/BoundedMap.d.ts.map +0 -1
- package/types/src/utils/paths/ignorePaths.d.ts +0 -5
- package/types/src/utils/paths/ignorePaths.d.ts.map +0 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/* eslint-disable no-template-curly-in-string */
|
|
2
|
+
const { test } = require('uvu')
|
|
3
|
+
const assert = require('uvu/assert')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
const path = require('path')
|
|
7
|
+
const configorama = require('../../index')
|
|
8
|
+
const { extractComment, findCommentStart, getCommentMarkers } = require('./extractComment')
|
|
9
|
+
|
|
10
|
+
test('extractComment reads trailing inline YAML comments', () => {
|
|
11
|
+
const lines = ['apiKey: ${env:API_KEY} # API key from dashboard']
|
|
12
|
+
assert.equal(extractComment('apiKey', lines, '.yml'), {
|
|
13
|
+
description: 'API key from dashboard',
|
|
14
|
+
descriptionSource: 'comment',
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('extractComment reads leading block comments and drops decoration', () => {
|
|
19
|
+
const lines = [
|
|
20
|
+
'# --------',
|
|
21
|
+
'# Database host',
|
|
22
|
+
'# Used by app startup',
|
|
23
|
+
'host: ${env:DB_HOST}',
|
|
24
|
+
]
|
|
25
|
+
assert.equal(extractComment('host', lines, '.yaml'), {
|
|
26
|
+
description: 'Database host Used by app startup',
|
|
27
|
+
descriptionSource: 'leadingComment',
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('extractComment ignores comment markers inside variables and quotes', () => {
|
|
32
|
+
const lines = ['apiKey: ${env:API_KEY, "abc#123"} # Real comment']
|
|
33
|
+
const start = findCommentStart(lines[0], getCommentMarkers('.yml'))
|
|
34
|
+
assert.is(lines[0].slice(start.index), '# Real comment')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('extractComment uses path-aware lookup for nested duplicate YAML keys', () => {
|
|
38
|
+
const lines = [
|
|
39
|
+
'first:',
|
|
40
|
+
' apiKey: ${env:FIRST_KEY} # First key',
|
|
41
|
+
'second:',
|
|
42
|
+
' apiKey: ${env:SECOND_KEY} # Second key',
|
|
43
|
+
]
|
|
44
|
+
assert.equal(extractComment('second.apiKey', lines, '.yml'), {
|
|
45
|
+
description: 'Second key',
|
|
46
|
+
descriptionSource: 'comment',
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('extractComment skips JSON comments and handles failures without throwing', () => {
|
|
51
|
+
assert.is(extractComment('apiKey', ['"apiKey": "${env:API_KEY}" // ignored'], '.json'), null)
|
|
52
|
+
assert.is(extractComment(null, null, '.yml'), null)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('extractComment handles JSON5 line comments', () => {
|
|
56
|
+
const lines = [
|
|
57
|
+
'{',
|
|
58
|
+
' "apiKey": "${env:API_KEY}" // API key',
|
|
59
|
+
'}',
|
|
60
|
+
]
|
|
61
|
+
assert.equal(extractComment('apiKey', lines, '.json5'), {
|
|
62
|
+
description: 'API key',
|
|
63
|
+
descriptionSource: 'comment',
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('extractComment parses leading YAML annotation tags', () => {
|
|
68
|
+
const lines = [
|
|
69
|
+
'# Stripe live secret key',
|
|
70
|
+
'# @from Stripe dashboard > Developers > API keys',
|
|
71
|
+
'# @sensitive true',
|
|
72
|
+
'stripeSecret: ${env:STRIPE_SECRET_KEY}',
|
|
73
|
+
]
|
|
74
|
+
assert.equal(extractComment('stripeSecret', lines, '.yml'), {
|
|
75
|
+
description: 'Stripe live secret key',
|
|
76
|
+
descriptionSource: 'leadingComment',
|
|
77
|
+
annotations: {
|
|
78
|
+
obtainHint: 'Stripe dashboard > Developers > API keys',
|
|
79
|
+
sensitive: true,
|
|
80
|
+
},
|
|
81
|
+
obtainHint: 'Stripe dashboard > Developers > API keys',
|
|
82
|
+
sensitive: true,
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('extractComment lets explicit @description override plain comment text', () => {
|
|
87
|
+
const lines = [
|
|
88
|
+
'# Plain comment loses to explicit description',
|
|
89
|
+
'# @description Stripe live secret key',
|
|
90
|
+
'stripeSecret: ${env:STRIPE_SECRET_KEY}',
|
|
91
|
+
]
|
|
92
|
+
assert.equal(extractComment('stripeSecret', lines, '.yaml'), {
|
|
93
|
+
description: 'Stripe live secret key',
|
|
94
|
+
descriptionSource: 'commentTag',
|
|
95
|
+
annotations: {
|
|
96
|
+
description: 'Stripe live secret key',
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('extractComment parses inline tags without requiring a description', () => {
|
|
102
|
+
const lines = [
|
|
103
|
+
'stripeSecret: ${env:STRIPE_SECRET_KEY} # @from Stripe dashboard > Developers > API keys',
|
|
104
|
+
]
|
|
105
|
+
assert.equal(extractComment('stripeSecret', lines, '.yaml'), {
|
|
106
|
+
annotations: {
|
|
107
|
+
obtainHint: 'Stripe dashboard > Developers > API keys',
|
|
108
|
+
},
|
|
109
|
+
obtainHint: 'Stripe dashboard > Developers > API keys',
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('extractComment parses JSONC and HCL annotation comments', () => {
|
|
114
|
+
const jsoncLines = [
|
|
115
|
+
'{',
|
|
116
|
+
' // @group Payments',
|
|
117
|
+
' "stripeSecret": "${env:STRIPE_SECRET_KEY}"',
|
|
118
|
+
'}',
|
|
119
|
+
]
|
|
120
|
+
assert.equal(extractComment('stripeSecret', jsoncLines, '.jsonc'), {
|
|
121
|
+
annotations: {
|
|
122
|
+
group: 'Payments',
|
|
123
|
+
},
|
|
124
|
+
group: 'Payments',
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const hclLines = [
|
|
128
|
+
'// Stripe live secret key',
|
|
129
|
+
'// @from Stripe dashboard > Developers > API keys',
|
|
130
|
+
'stripe_secret = "${env:STRIPE_SECRET_KEY}"',
|
|
131
|
+
]
|
|
132
|
+
assert.equal(extractComment('stripe_secret', hclLines, '.hcl'), {
|
|
133
|
+
description: 'Stripe live secret key',
|
|
134
|
+
descriptionSource: 'leadingComment',
|
|
135
|
+
annotations: {
|
|
136
|
+
obtainHint: 'Stripe dashboard > Developers > API keys',
|
|
137
|
+
},
|
|
138
|
+
obtainHint: 'Stripe dashboard > Developers > API keys',
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('extractComment keeps unknown tag-shaped comments as plain description', () => {
|
|
143
|
+
const lines = [
|
|
144
|
+
'# @david rotate this key after launch',
|
|
145
|
+
'stripeSecret: ${env:STRIPE_SECRET_KEY}',
|
|
146
|
+
]
|
|
147
|
+
assert.equal(extractComment('stripeSecret', lines, '.yaml'), {
|
|
148
|
+
description: '@david rotate this key after launch',
|
|
149
|
+
descriptionSource: 'leadingComment',
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('requirements model uses comments when help is absent and keeps help precedence', async () => {
|
|
154
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'configorama-comments-'))
|
|
155
|
+
const configPath = path.join(dir, 'config.yml')
|
|
156
|
+
fs.writeFileSync(configPath, [
|
|
157
|
+
'# API key from dashboard',
|
|
158
|
+
'apiKey: ${env:CONFIGORAMA_COMMENT_API_KEY}',
|
|
159
|
+
'withHelp: ${env:CONFIGORAMA_COMMENT_HELP | help("Help wins")} # Comment loses',
|
|
160
|
+
'nested:',
|
|
161
|
+
' apiKey: ${env:CONFIGORAMA_COMMENT_NESTED} # Nested key',
|
|
162
|
+
].join('\n'))
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const result = await configorama.analyze(configPath, {
|
|
166
|
+
instructions: true,
|
|
167
|
+
options: {}
|
|
168
|
+
})
|
|
169
|
+
const byVariable = Object.fromEntries(result.requirements.map(req => [req.variable, req]))
|
|
170
|
+
|
|
171
|
+
assert.is(byVariable['env:CONFIGORAMA_COMMENT_API_KEY'].description, 'API key from dashboard')
|
|
172
|
+
assert.is(byVariable['env:CONFIGORAMA_COMMENT_API_KEY'].descriptionSource, 'leadingComment')
|
|
173
|
+
assert.is(byVariable['env:CONFIGORAMA_COMMENT_HELP'].description, 'Help wins')
|
|
174
|
+
assert.is(byVariable['env:CONFIGORAMA_COMMENT_HELP'].descriptionSource, 'help')
|
|
175
|
+
assert.is(byVariable['env:CONFIGORAMA_COMMENT_NESTED'].description, 'Nested key')
|
|
176
|
+
assert.is(byVariable['env:CONFIGORAMA_COMMENT_NESTED'].descriptionSource, 'comment')
|
|
177
|
+
} finally {
|
|
178
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test.run()
|
|
@@ -26,7 +26,8 @@ function preProcess(configObject, variableSyntax, variableTypes, options = {}) {
|
|
|
26
26
|
// Extract reference prefixes from variable types, or use defaults
|
|
27
27
|
const refPrefixes = variableTypes && variableTypes.length > 0
|
|
28
28
|
? variableTypes
|
|
29
|
-
.
|
|
29
|
+
.flatMap(v => v.prefixes || [v.prefix || v.type])
|
|
30
|
+
.map(prefix => prefix + ':')
|
|
30
31
|
.filter(p => p !== 'dot.prop:' && p !== 'string:' && p !== 'number:')
|
|
31
32
|
: ['self:', 'opt:', 'env:', 'file:', 'text:', 'deep:']
|
|
32
33
|
|
|
@@ -19,8 +19,8 @@ function findLineForKey(keyToFind, lines, fileType) {
|
|
|
19
19
|
if (fileType === '.yml' || fileType === '.yaml') {
|
|
20
20
|
return new RegExp(`^\\s*${escapedKey}\\s*:`).test(line)
|
|
21
21
|
}
|
|
22
|
-
// TOML: key = or key=
|
|
23
|
-
if (fileType === '.toml') {
|
|
22
|
+
// TOML/HCL: key = or key=
|
|
23
|
+
if (fileType === '.toml' || fileType === '.hcl') {
|
|
24
24
|
return new RegExp(`^\\s*${escapedKey}\\s*=`).test(line)
|
|
25
25
|
}
|
|
26
26
|
// JSON: "key": or "key" :
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const DEFAULT_IGNORE_PATHS = [
|
|
2
2
|
'**.Fn::Sub',
|
|
3
|
+
'**.Fn::Sub.0',
|
|
3
4
|
'**.Properties.Code.ZipFile',
|
|
4
5
|
'**.Properties.FunctionCode',
|
|
5
6
|
'**.Properties.UserData',
|
|
@@ -29,17 +30,29 @@ function patternToSegments(pattern) {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
function matchSegments(patternSegments, pathSegments) {
|
|
32
|
-
|
|
33
|
+
return matchFrom(patternSegments, 0, pathSegments, 0)
|
|
34
|
+
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// Index-based glob match: '**' spans zero-or-more segments (with backtracking),
|
|
37
|
+
// '*' matches one segment, anything else matches literally. Avoids per-call array
|
|
38
|
+
// allocation (no destructuring/slice) that dominated resolution hot paths.
|
|
39
|
+
function matchFrom(pattern, pi, path, si) {
|
|
40
|
+
while (pi < pattern.length) {
|
|
41
|
+
const head = pattern[pi]
|
|
42
|
+
if (head === '**') {
|
|
43
|
+
// '**' consumes zero segments here...
|
|
44
|
+
if (matchFrom(pattern, pi + 1, path, si)) return true
|
|
45
|
+
// ...or one more path segment, still anchored on '**'
|
|
46
|
+
if (si >= path.length) return false
|
|
47
|
+
si++
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
if (si >= path.length) return false
|
|
51
|
+
if (head !== '*' && head !== path[si]) return false
|
|
52
|
+
pi++
|
|
53
|
+
si++
|
|
38
54
|
}
|
|
39
|
-
|
|
40
|
-
if (!pathSegments.length) return false
|
|
41
|
-
if (head !== '*' && head !== pathSegments[0]) return false
|
|
42
|
-
return matchSegments(tail, pathSegments.slice(1))
|
|
55
|
+
return si === path.length
|
|
43
56
|
}
|
|
44
57
|
|
|
45
58
|
function normalizeIgnorePaths(options = {}) {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const dotProp = require('dot-prop')
|
|
2
|
+
|
|
3
|
+
const REDACTED_VALUE = '********'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_SENSITIVE_PATTERNS = [
|
|
6
|
+
/secret/i,
|
|
7
|
+
/password/i,
|
|
8
|
+
/token/i,
|
|
9
|
+
/key/i,
|
|
10
|
+
/credential/i,
|
|
11
|
+
/auth/i,
|
|
12
|
+
/private[-_]?key/i,
|
|
13
|
+
/client[-_]?secret/i,
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
function cloneJson(value) {
|
|
17
|
+
if (value === undefined) return undefined
|
|
18
|
+
return JSON.parse(JSON.stringify(value))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toRegex(pattern) {
|
|
22
|
+
if (pattern instanceof RegExp) return pattern
|
|
23
|
+
return new RegExp(String(pattern), 'i')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getSensitivePatterns(options = {}) {
|
|
27
|
+
const customPatterns = (options.patterns || options.sensitivePatterns || []).map(toRegex)
|
|
28
|
+
return DEFAULT_SENSITIVE_PATTERNS.concat(customPatterns)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isSensitiveName(name, options = {}) {
|
|
32
|
+
if (!name) return false
|
|
33
|
+
const value = String(name)
|
|
34
|
+
return getSensitivePatterns(options).some(pattern => pattern.test(value))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getSensitiveOverride(entries = []) {
|
|
38
|
+
const entry = entries.find(item => item && typeof item.value === 'boolean')
|
|
39
|
+
if (!entry) return null
|
|
40
|
+
return entry.value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isSensitiveVariable(name, options = {}) {
|
|
44
|
+
const override = getSensitiveOverride(options.sensitiveEntries || [])
|
|
45
|
+
if (override !== null) return override
|
|
46
|
+
return isSensitiveName(name, options) || isSensitiveName(options.path, options)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function redactValue(value) {
|
|
50
|
+
if (value === undefined) return undefined
|
|
51
|
+
return REDACTED_VALUE
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function redactObjectByPaths(value, paths = []) {
|
|
55
|
+
const redacted = cloneJson(value)
|
|
56
|
+
for (const configPath of paths || []) {
|
|
57
|
+
if (configPath && dotProp.has(redacted, configPath)) {
|
|
58
|
+
dotProp.set(redacted, configPath, REDACTED_VALUE)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return redacted
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function redactRequirementValue(requirement, value) {
|
|
65
|
+
return requirement && requirement.sensitive === true ? REDACTED_VALUE : value
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
DEFAULT_SENSITIVE_PATTERNS,
|
|
70
|
+
REDACTED_VALUE,
|
|
71
|
+
cloneJson,
|
|
72
|
+
getSensitivePatterns,
|
|
73
|
+
isSensitiveName,
|
|
74
|
+
isSensitiveVariable,
|
|
75
|
+
redactObjectByPaths,
|
|
76
|
+
redactRequirementValue,
|
|
77
|
+
redactValue,
|
|
78
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const {
|
|
4
|
+
isSensitiveVariable,
|
|
5
|
+
redactObjectByPaths,
|
|
6
|
+
REDACTED_VALUE,
|
|
7
|
+
} = require('./redact')
|
|
8
|
+
|
|
9
|
+
test('isSensitiveVariable detects common secret names', () => {
|
|
10
|
+
assert.is(isSensitiveVariable('API_KEY'), true)
|
|
11
|
+
assert.is(isSensitiveVariable('clientSecret'), true)
|
|
12
|
+
assert.is(isSensitiveVariable('private_key'), true)
|
|
13
|
+
assert.is(isSensitiveVariable('region'), false)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('isSensitiveVariable honors annotation overrides', () => {
|
|
17
|
+
assert.is(isSensitiveVariable('API_KEY', {
|
|
18
|
+
sensitiveEntries: [{ value: false, path: 'apiKey' }]
|
|
19
|
+
}), false)
|
|
20
|
+
|
|
21
|
+
assert.is(isSensitiveVariable('region', {
|
|
22
|
+
sensitiveEntries: [{ value: true, path: 'region' }]
|
|
23
|
+
}), true)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('redactObjectByPaths redacts nested config paths', () => {
|
|
27
|
+
const redacted = redactObjectByPaths({
|
|
28
|
+
service: 'demo',
|
|
29
|
+
secrets: {
|
|
30
|
+
apiKey: 'secret-value'
|
|
31
|
+
}
|
|
32
|
+
}, ['secrets.apiKey'])
|
|
33
|
+
|
|
34
|
+
assert.is(redacted.service, 'demo')
|
|
35
|
+
assert.is(redacted.secrets.apiKey, REDACTED_VALUE)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test.run()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const dotProp = require('dot-prop')
|
|
2
|
+
const { REDACTED_VALUE, cloneJson } = require('./redact')
|
|
3
|
+
|
|
4
|
+
function inputSectionForRequirement(requirement) {
|
|
5
|
+
if (!requirement) return null
|
|
6
|
+
if (requirement.variableType === 'option') return 'options'
|
|
7
|
+
if (requirement.variableType === 'env') return 'env'
|
|
8
|
+
if (requirement.variableType === 'self') return 'self'
|
|
9
|
+
if (requirement.variableType === 'dotProp') return 'dotProp'
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function redactUserInputsByRequirements(userInputs, requirements) {
|
|
14
|
+
const redacted = cloneJson(userInputs || {})
|
|
15
|
+
|
|
16
|
+
for (const requirement of requirements || []) {
|
|
17
|
+
if (!requirement || requirement.sensitive !== true) continue
|
|
18
|
+
const section = inputSectionForRequirement(requirement)
|
|
19
|
+
if (!section || !redacted[section]) continue
|
|
20
|
+
if (Object.prototype.hasOwnProperty.call(redacted[section], requirement.name)) {
|
|
21
|
+
redacted[section][requirement.name] = REDACTED_VALUE
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return redacted
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function redactConfigByRequirements(config, requirements) {
|
|
29
|
+
const redacted = cloneJson(config)
|
|
30
|
+
|
|
31
|
+
for (const requirement of requirements || []) {
|
|
32
|
+
if (!requirement || requirement.sensitive !== true) continue
|
|
33
|
+
for (const configPath of requirement.paths || []) {
|
|
34
|
+
if (configPath && dotProp.has(redacted, configPath)) {
|
|
35
|
+
dotProp.set(redacted, configPath, REDACTED_VALUE)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return redacted
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
REDACTED_VALUE,
|
|
45
|
+
redactConfigByRequirements,
|
|
46
|
+
redactUserInputsByRequirements,
|
|
47
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { test } = require('uvu')
|
|
2
|
+
const assert = require('uvu/assert')
|
|
3
|
+
const {
|
|
4
|
+
REDACTED_VALUE,
|
|
5
|
+
redactConfigByRequirements,
|
|
6
|
+
redactUserInputsByRequirements,
|
|
7
|
+
} = require('./setupRedaction')
|
|
8
|
+
|
|
9
|
+
const requirements = [
|
|
10
|
+
{
|
|
11
|
+
name: 'API_KEY',
|
|
12
|
+
variableType: 'env',
|
|
13
|
+
sensitive: true,
|
|
14
|
+
paths: ['secrets.apiKey'],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'PUBLIC_KEY',
|
|
18
|
+
variableType: 'env',
|
|
19
|
+
sensitive: false,
|
|
20
|
+
paths: ['secrets.publicKey'],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'stage',
|
|
24
|
+
variableType: 'option',
|
|
25
|
+
sensitive: false,
|
|
26
|
+
paths: ['stage'],
|
|
27
|
+
},
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
test('redactUserInputsByRequirements redacts only sensitive setup values', () => {
|
|
31
|
+
const inputs = {
|
|
32
|
+
options: { stage: 'prod' },
|
|
33
|
+
env: {
|
|
34
|
+
API_KEY: 'secret-value',
|
|
35
|
+
PUBLIC_KEY: 'public-value',
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
assert.equal(redactUserInputsByRequirements(inputs, requirements), {
|
|
40
|
+
options: { stage: 'prod' },
|
|
41
|
+
env: {
|
|
42
|
+
API_KEY: REDACTED_VALUE,
|
|
43
|
+
PUBLIC_KEY: 'public-value',
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
assert.is(inputs.env.API_KEY, 'secret-value')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('redactConfigByRequirements redacts only sensitive resolved config paths', () => {
|
|
50
|
+
const config = {
|
|
51
|
+
stage: 'prod',
|
|
52
|
+
secrets: {
|
|
53
|
+
apiKey: 'secret-value',
|
|
54
|
+
publicKey: 'public-value',
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
assert.equal(redactConfigByRequirements(config, requirements), {
|
|
59
|
+
stage: 'prod',
|
|
60
|
+
secrets: {
|
|
61
|
+
apiKey: REDACTED_VALUE,
|
|
62
|
+
publicKey: 'public-value',
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
assert.is(config.secrets.apiKey, 'secret-value')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test.run()
|