timonel 3.0.0-beta.1 → 3.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/CHANGELOG.md +186 -0
- package/README.md +182 -126
- package/SECURITY.md +25 -11
- package/dist/cli.js +54 -15
- package/dist/lib/helm.js +28 -1
- package/dist/lib/helmChartWriter.js +27 -8
- package/dist/lib/resources/baseResourceProvider.js +5 -0
- package/dist/lib/resources/cloud/aws/awsResources.js +2 -1
- package/dist/lib/resources/cloud/aws/karpenterResources.js +16 -2
- package/dist/lib/security.js +4 -4
- package/dist/lib/templates/flexible-subchart.js +14 -5
- package/dist/lib/templates/umbrella-chart.js +21 -10
- package/dist/lib/umbrellaRutter.js +19 -7
- package/dist/lib/utils/envVarsLoader.js +13 -7
- package/dist/lib/utils/helmHelpers.js +10 -2
- package/dist/lib/utils/helmYamlSerializer.js +19 -9
- package/dist/lib/utils/logger.js +54 -39
- package/dist/lib/utils/valuesRef.js +9 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -142,8 +142,9 @@ async function cmdInit(name, silent = false) {
|
|
|
142
142
|
if (!SecurityUtils.isValidChartName(validName)) {
|
|
143
143
|
usageAndExit('Invalid chart name. Must be lowercase, start with a letter, and contain only letters, numbers, and dashes.');
|
|
144
144
|
}
|
|
145
|
-
const
|
|
146
|
-
const
|
|
145
|
+
const cwd = process.cwd();
|
|
146
|
+
const base = SecurityUtils.validatePath(path.join(cwd, validName), cwd);
|
|
147
|
+
const chartFile = SecurityUtils.validatePath(path.join(base, 'chart.ts'), cwd);
|
|
147
148
|
fs.mkdirSync(base, { recursive: true });
|
|
148
149
|
const { generateFlexibleSubchartTemplate } = await import('./lib/templates/flexible-subchart.js');
|
|
149
150
|
fs.writeFileSync(chartFile, generateFlexibleSubchartTemplate(validName));
|
|
@@ -152,19 +153,31 @@ async function cmdInit(name, silent = false) {
|
|
|
152
153
|
log(`Run 'tl synth ${validName}' to generate complete Helm chart`, silent);
|
|
153
154
|
}
|
|
154
155
|
async function cmdSynth(chartDirOrOutDir, flags, explicitOutDir) {
|
|
155
|
-
|
|
156
|
+
const cwd = process.cwd();
|
|
157
|
+
let chartDir = cwd;
|
|
156
158
|
let outDir;
|
|
157
|
-
if (chartDirOrOutDir
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
if (chartDirOrOutDir) {
|
|
160
|
+
const resolvedPath = path.resolve(chartDirOrOutDir);
|
|
161
|
+
const validatedPath = SecurityUtils.validatePath(resolvedPath, cwd, { allowAbsolute: true });
|
|
162
|
+
const chartTsPath = SecurityUtils.validatePath(path.join(validatedPath, 'chart.ts'), cwd, {
|
|
163
|
+
allowAbsolute: true,
|
|
164
|
+
});
|
|
165
|
+
if (fs.existsSync(chartTsPath)) {
|
|
166
|
+
chartDir = validatedPath;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
outDir = chartDirOrOutDir;
|
|
170
|
+
}
|
|
162
171
|
}
|
|
163
172
|
if (explicitOutDir) {
|
|
164
173
|
outDir = explicitOutDir;
|
|
165
174
|
}
|
|
166
|
-
const chartFile = path.join(chartDir, 'chart.ts')
|
|
167
|
-
|
|
175
|
+
const chartFile = SecurityUtils.validatePath(path.join(chartDir, 'chart.ts'), cwd, {
|
|
176
|
+
allowAbsolute: true,
|
|
177
|
+
});
|
|
178
|
+
const defaultOutDir = SecurityUtils.validatePath(path.join(chartDir, 'dist'), cwd, {
|
|
179
|
+
allowAbsolute: true,
|
|
180
|
+
});
|
|
168
181
|
if (!fs.existsSync(chartFile)) {
|
|
169
182
|
console.error('chart.ts not found. Run `tl init` first.');
|
|
170
183
|
process.exit(1);
|
|
@@ -177,7 +190,7 @@ async function cmdSynth(chartDirOrOutDir, flags, explicitOutDir) {
|
|
|
177
190
|
if (modifiedContent === originalContent) {
|
|
178
191
|
modifiedContent = originalContent.replace(/chart\.write\(['"][^'"]*['"]\)/, `chart.write('${safeOutDirLiteral}')`);
|
|
179
192
|
}
|
|
180
|
-
const tempChartFile = path.join(chartDir, '.timonel-temp-chart.ts');
|
|
193
|
+
const tempChartFile = SecurityUtils.validatePath(path.join(chartDir, '.timonel-temp-chart.ts'), cwd, { allowAbsolute: true });
|
|
181
194
|
fs.writeFileSync(tempChartFile, modifiedContent);
|
|
182
195
|
const wrapperScript = `
|
|
183
196
|
import { pathToFileURL } from 'url';
|
|
@@ -237,11 +250,34 @@ async function cmdValidate(flags) {
|
|
|
237
250
|
process.exit(result.status ?? 1);
|
|
238
251
|
}
|
|
239
252
|
}
|
|
253
|
+
function validateReleaseName(release) {
|
|
254
|
+
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(release) || release.length > 53) {
|
|
255
|
+
console.error(`Invalid release name: ${SecurityUtils.sanitizeLogMessage(release)}`);
|
|
256
|
+
console.error('Release name must be lowercase alphanumeric with hyphens (max 53 chars)');
|
|
257
|
+
process.exit(1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function validateNamespace(namespace) {
|
|
261
|
+
if (!/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(namespace) || namespace.length > 63) {
|
|
262
|
+
console.error(`Invalid namespace: ${SecurityUtils.sanitizeLogMessage(namespace)}`);
|
|
263
|
+
console.error('Namespace must be lowercase alphanumeric with hyphens (max 63 chars)');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function validateSetFlag(setValue) {
|
|
268
|
+
if (!/^[a-zA-Z0-9._[\]-]+=.+$/.test(setValue)) {
|
|
269
|
+
console.error(`Invalid --set format: ${SecurityUtils.sanitizeLogMessage(setValue)}`);
|
|
270
|
+
console.error('Expected format: key=value');
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
240
274
|
async function cmdDeploy(release, namespace, flags) {
|
|
241
275
|
if (!release)
|
|
242
276
|
usageAndExit('Missing <release>');
|
|
277
|
+
validateReleaseName(release);
|
|
243
278
|
const args = ['upgrade', '--install', release, '.'];
|
|
244
279
|
if (namespace) {
|
|
280
|
+
validateNamespace(namespace);
|
|
245
281
|
args.push('--namespace', namespace);
|
|
246
282
|
}
|
|
247
283
|
if (flags?.env) {
|
|
@@ -256,6 +292,7 @@ async function cmdDeploy(release, namespace, flags) {
|
|
|
256
292
|
}
|
|
257
293
|
if (flags?.set) {
|
|
258
294
|
for (const setValue of flags.set) {
|
|
295
|
+
validateSetFlag(setValue);
|
|
259
296
|
args.push('--set', setValue);
|
|
260
297
|
}
|
|
261
298
|
}
|
|
@@ -465,7 +502,7 @@ async function cmdUmbrellaAdd(subchartPath, silent = false) {
|
|
|
465
502
|
fs.mkdirSync(subchartDir, { recursive: true });
|
|
466
503
|
const relativeSubchartPath = path.relative(chartsRoot, subchartDir) || subchartName;
|
|
467
504
|
const normalizedSubchartPath = relativeSubchartPath.split(path.sep).join('/');
|
|
468
|
-
const chartFile = path.join(subchartDir, 'chart.ts');
|
|
505
|
+
const chartFile = SecurityUtils.validatePath(path.join(subchartDir, 'chart.ts'), chartsRoot);
|
|
469
506
|
const { generateFlexibleSubchartTemplate } = await import('./lib/templates/flexible-subchart.js');
|
|
470
507
|
const subchartContent = generateFlexibleSubchartTemplate(subchartName);
|
|
471
508
|
fs.writeFileSync(chartFile, subchartContent);
|
|
@@ -479,8 +516,9 @@ async function cmdUmbrellaAdd(subchartPath, silent = false) {
|
|
|
479
516
|
log(`Subchart ${subchartName} added to umbrella at path '${normalizedSubchartPath}'`, silent);
|
|
480
517
|
}
|
|
481
518
|
async function cmdUmbrellaSynth(outDir, flags) {
|
|
482
|
-
const
|
|
483
|
-
const
|
|
519
|
+
const cwd = process.cwd();
|
|
520
|
+
const umbrellaFile = SecurityUtils.validatePath(path.join(cwd, UMBRELLA_FILE_NAME), cwd);
|
|
521
|
+
const defaultOutDir = SecurityUtils.validatePath(path.join(cwd, 'dist'), cwd);
|
|
484
522
|
if (!fs.existsSync(umbrellaFile)) {
|
|
485
523
|
console.error('umbrella.ts not found. Run `tl umbrella init` first.');
|
|
486
524
|
process.exit(1);
|
|
@@ -511,7 +549,8 @@ fs.mkdirSync(output, { recursive: true });
|
|
|
511
549
|
await Promise.resolve(runner(output, synthOptions));
|
|
512
550
|
console.log('Umbrella chart written to ' + output);
|
|
513
551
|
`;
|
|
514
|
-
const
|
|
552
|
+
const cwd = process.cwd();
|
|
553
|
+
const wrapperFile = SecurityUtils.validatePath(path.join(cwd, '.timonel-umbrella-wrapper.mjs'), cwd);
|
|
515
554
|
try {
|
|
516
555
|
fs.writeFileSync(wrapperFile, wrapperScript);
|
|
517
556
|
const result = spawnSync(process.execPath, [TSX_CLI_PATH, wrapperFile], {
|
package/dist/lib/helm.js
CHANGED
|
@@ -36,6 +36,12 @@ export function indent(n, expr) {
|
|
|
36
36
|
.join('\n');
|
|
37
37
|
}
|
|
38
38
|
export function template(name, context = '.') {
|
|
39
|
+
if (!isValidHelmPath(name)) {
|
|
40
|
+
throw new Error(`Invalid template name: ${name}`);
|
|
41
|
+
}
|
|
42
|
+
if (!isValidHelmPath(context)) {
|
|
43
|
+
throw new Error(`Invalid template context: ${context}`);
|
|
44
|
+
}
|
|
39
45
|
return `{{ template "${name}" ${context} }}`;
|
|
40
46
|
}
|
|
41
47
|
export function include(name, context = '.') {
|
|
@@ -157,12 +163,33 @@ export function helmRange(collection, content, options = {}) {
|
|
|
157
163
|
if (!collection.startsWith('.')) {
|
|
158
164
|
throw new Error('Collection path must start with "." (e.g., ".Values.items")');
|
|
159
165
|
}
|
|
166
|
+
if (!isValidHelmPath(collection)) {
|
|
167
|
+
throw new Error(`Invalid collection path: ${collection}`);
|
|
168
|
+
}
|
|
160
169
|
const { keyValue = false, keyVar = '$key', valueVar = '$value', indexVar = '$index', itemVar = '$item', } = options;
|
|
170
|
+
const validateVarName = (varName, varType) => {
|
|
171
|
+
if (!varName.startsWith('$')) {
|
|
172
|
+
throw new Error(`${varType} must start with $ (e.g., $key, $value)`);
|
|
173
|
+
}
|
|
174
|
+
if (!/^\$[a-zA-Z_][a-zA-Z0-9_]*$/.test(varName)) {
|
|
175
|
+
throw new Error(`Invalid ${varType}: ${varName}`);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
if (keyValue) {
|
|
179
|
+
validateVarName(keyVar, 'keyVar');
|
|
180
|
+
validateVarName(valueVar, 'valueVar');
|
|
181
|
+
}
|
|
182
|
+
if (options.indexVar !== undefined) {
|
|
183
|
+
validateVarName(indexVar, 'indexVar');
|
|
184
|
+
}
|
|
185
|
+
if (options.itemVar !== undefined) {
|
|
186
|
+
validateVarName(itemVar, 'itemVar');
|
|
187
|
+
}
|
|
161
188
|
let rangeExpression;
|
|
162
189
|
if (keyValue) {
|
|
163
190
|
rangeExpression = `{{- range ${keyVar}, ${valueVar} := ${collection} }}`;
|
|
164
191
|
}
|
|
165
|
-
else if (options.indexVar && options.itemVar) {
|
|
192
|
+
else if (options.indexVar !== undefined && options.itemVar !== undefined) {
|
|
166
193
|
rangeExpression = `{{- range ${indexVar}, ${itemVar} := ${collection} }}`;
|
|
167
194
|
}
|
|
168
195
|
else {
|
|
@@ -35,7 +35,8 @@ export class HelmChartWriter {
|
|
|
35
35
|
timer();
|
|
36
36
|
}
|
|
37
37
|
static createDirectories(outDir) {
|
|
38
|
-
|
|
38
|
+
const templatesDir = SecurityUtils.validatePath(path.join(outDir, 'templates'), outDir);
|
|
39
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
39
40
|
}
|
|
40
41
|
static writeChartYaml(outDir, meta) {
|
|
41
42
|
const chartYaml = dumpHelmAwareYaml({
|
|
@@ -53,13 +54,16 @@ export class HelmChartWriter {
|
|
|
53
54
|
icon: meta.icon,
|
|
54
55
|
dependencies: meta.dependencies,
|
|
55
56
|
});
|
|
56
|
-
|
|
57
|
+
const chartYamlPath = SecurityUtils.validatePath(path.join(outDir, 'Chart.yaml'), outDir);
|
|
58
|
+
fs.writeFileSync(chartYamlPath, chartYaml);
|
|
57
59
|
}
|
|
58
60
|
static writeValuesFiles(outDir, defaultValues, envValues) {
|
|
59
|
-
|
|
61
|
+
const valuesPath = SecurityUtils.validatePath(path.join(outDir, 'values.yaml'), outDir);
|
|
62
|
+
fs.writeFileSync(valuesPath, dumpHelmAwareYaml(defaultValues));
|
|
60
63
|
for (const [env, values] of Object.entries(envValues)) {
|
|
61
64
|
const sanitizedEnv = SecurityUtils.sanitizeEnvironmentName(env);
|
|
62
|
-
|
|
65
|
+
const envValuesPath = SecurityUtils.validatePath(path.join(outDir, `values-${sanitizedEnv}.yaml`), outDir);
|
|
66
|
+
fs.writeFileSync(envValuesPath, dumpHelmAwareYaml(values));
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
static writeAssets(outDir, assets) {
|
|
@@ -77,20 +81,23 @@ export class HelmChartWriter {
|
|
|
77
81
|
.map((h) => [`{{- define "${h.name}" -}}`, h.template.trimEnd(), '{{- end }}', ''].join('\n'))
|
|
78
82
|
.join('\n');
|
|
79
83
|
}
|
|
80
|
-
|
|
84
|
+
const helpersPath = SecurityUtils.validatePath(path.join(outDir, 'templates', '_helpers.tpl'), outDir);
|
|
85
|
+
fs.writeFileSync(helpersPath, content);
|
|
81
86
|
}
|
|
82
87
|
static writeNotes(outDir, notesTpl) {
|
|
83
88
|
if (!notesTpl)
|
|
84
89
|
return;
|
|
85
|
-
|
|
90
|
+
const notesPath = SecurityUtils.validatePath(path.join(outDir, 'templates', 'NOTES.txt'), outDir);
|
|
91
|
+
fs.writeFileSync(notesPath, notesTpl.endsWith('\n') ? notesTpl : notesTpl + '\n');
|
|
86
92
|
}
|
|
87
93
|
static writeSchema(outDir, valuesSchema) {
|
|
88
94
|
if (!valuesSchema)
|
|
89
95
|
return;
|
|
90
|
-
|
|
96
|
+
const schemaPath = SecurityUtils.validatePath(path.join(outDir, 'values.schema.json'), outDir);
|
|
97
|
+
fs.writeFileSync(schemaPath, JSON.stringify(valuesSchema, null, 2) + '\n');
|
|
91
98
|
}
|
|
92
99
|
static writeHelmIgnore(outDir) {
|
|
93
|
-
const helmIgnorePath = path.join(outDir, '.helmignore');
|
|
100
|
+
const helmIgnorePath = SecurityUtils.validatePath(path.join(outDir, '.helmignore'), outDir);
|
|
94
101
|
if (!fs.existsSync(helmIgnorePath)) {
|
|
95
102
|
const helmIgnore = [
|
|
96
103
|
'# VCS',
|
|
@@ -149,7 +156,13 @@ function writeSingleAssetFile(outDir, targetDir, directorySegments, fileBaseName
|
|
|
149
156
|
const chartSubdir = path.join(outDir, targetDir, ...directorySegments);
|
|
150
157
|
SecurityUtils.validatePath(chartSubdir, outDir);
|
|
151
158
|
fs.mkdirSync(chartSubdir, { recursive: true });
|
|
159
|
+
if (fileBaseName.includes('..') || fileBaseName.includes('/') || fileBaseName.includes('\\')) {
|
|
160
|
+
throw new Error(`Invalid fileBaseName: ${fileBaseName}`);
|
|
161
|
+
}
|
|
152
162
|
const filename = `${fileBaseName}.yaml`;
|
|
163
|
+
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
|
164
|
+
throw new Error(`Invalid filename: ${filename}`);
|
|
165
|
+
}
|
|
153
166
|
const absolutePath = path.join(chartSubdir, filename);
|
|
154
167
|
SecurityUtils.validatePath(absolutePath, outDir);
|
|
155
168
|
const processedYaml = postProcessFieldConditionals(yaml);
|
|
@@ -162,8 +175,14 @@ function writeMultipleAssetFiles(outDir, targetDir, directorySegments, fileBaseN
|
|
|
162
175
|
const processedYaml = postProcessFieldConditionals(yaml);
|
|
163
176
|
const parts = splitDocs(processedYaml);
|
|
164
177
|
parts.forEach((doc, index) => {
|
|
178
|
+
if (fileBaseName.includes('..') || fileBaseName.includes('/') || fileBaseName.includes('\\')) {
|
|
179
|
+
throw new Error(`Invalid fileBaseName: ${fileBaseName}`);
|
|
180
|
+
}
|
|
165
181
|
const suffix = parts.length > 1 ? `-${index + 1}` : '';
|
|
166
182
|
const filename = `${fileBaseName}${suffix}.yaml`;
|
|
183
|
+
if (filename.includes('..') || filename.includes('/') || filename.includes('\\')) {
|
|
184
|
+
throw new Error(`Invalid filename: ${filename}`);
|
|
185
|
+
}
|
|
167
186
|
const absolutePath = path.join(chartSubdir, filename);
|
|
168
187
|
SecurityUtils.validatePath(absolutePath, outDir);
|
|
169
188
|
fs.writeFileSync(absolutePath, doc.endsWith('\n') ? doc : `${doc}\n`);
|
|
@@ -48,6 +48,11 @@ export class BaseResourceProvider {
|
|
|
48
48
|
createRootLevelApiObject(name, apiVersion, kind, fields, labels, annotations) {
|
|
49
49
|
this.validateKubernetesName(name, kind);
|
|
50
50
|
this.validateLabels(labels, kind);
|
|
51
|
+
const reservedKeys = ['apiVersion', 'kind', 'metadata'];
|
|
52
|
+
const conflictingKeys = Object.keys(fields).filter((key) => reservedKeys.includes(key));
|
|
53
|
+
if (conflictingKeys.length > 0) {
|
|
54
|
+
throw new Error(`Fields object contains reserved keys: ${conflictingKeys.join(', ')}. These keys cannot be overridden.`);
|
|
55
|
+
}
|
|
51
56
|
return new ApiObject(this.chart, name, {
|
|
52
57
|
apiVersion,
|
|
53
58
|
kind,
|
|
@@ -120,7 +120,8 @@ export class AWSResources extends BaseResourceProvider {
|
|
|
120
120
|
throw new Error(`Ingress path "${path}" must start with /`);
|
|
121
121
|
}
|
|
122
122
|
if (normalizedPath.includes('../') || normalizedPath.includes('./')) {
|
|
123
|
-
|
|
123
|
+
const sanitizedPath = path.replace(/[\r\n]/g, '');
|
|
124
|
+
console.warn(`Warning: Ingress path "${sanitizedPath}" contains path traversal-like sequences`);
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
validatePathType(pathType) {
|
|
@@ -15,8 +15,16 @@ export function isValidDisruptionBudget(budget) {
|
|
|
15
15
|
return nodePattern.test(budget.nodes);
|
|
16
16
|
}
|
|
17
17
|
export function isValidKubernetesDuration(duration) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
try {
|
|
19
|
+
if (typeof duration !== 'string') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const durationPattern = /^(\d+h)?(\d+m)?(\d+s)?$/;
|
|
23
|
+
return durationPattern.test(duration) && duration.length > 0;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
20
28
|
}
|
|
21
29
|
export const DEFAULT_TERMINATION_GRACE_PERIOD = '30s';
|
|
22
30
|
export class KarpenterResources extends BaseResourceProvider {
|
|
@@ -121,9 +129,15 @@ export class KarpenterResources extends BaseResourceProvider {
|
|
|
121
129
|
nodePoolConfig['weight'] = spec.weight;
|
|
122
130
|
}
|
|
123
131
|
if (spec.labels) {
|
|
132
|
+
if (typeof spec.labels !== 'object' || Array.isArray(spec.labels)) {
|
|
133
|
+
throw new Error('Labels must be a valid object');
|
|
134
|
+
}
|
|
124
135
|
nodePoolConfig['labels'] = spec.labels;
|
|
125
136
|
}
|
|
126
137
|
if (spec.annotations) {
|
|
138
|
+
if (typeof spec.annotations !== 'object' || Array.isArray(spec.annotations)) {
|
|
139
|
+
throw new Error('Annotations must be a valid object');
|
|
140
|
+
}
|
|
127
141
|
nodePoolConfig['annotations'] = spec.annotations;
|
|
128
142
|
}
|
|
129
143
|
return this.addKarpenterNodePool(nodePoolConfig);
|
package/dist/lib/security.js
CHANGED
|
@@ -84,7 +84,7 @@ export class SecurityUtils {
|
|
|
84
84
|
}
|
|
85
85
|
const sanitized = env.replace(/[^a-zA-Z0-9-_]/g, '');
|
|
86
86
|
if (sanitized !== env) {
|
|
87
|
-
throw new Error(`Invalid environment name: ${
|
|
87
|
+
throw new Error(`Invalid environment name: ${SecurityUtils.sanitizeLogMessage(env)}`);
|
|
88
88
|
}
|
|
89
89
|
if (sanitized.length === 0 || sanitized.length > 63) {
|
|
90
90
|
throw new Error('Environment name must be 1-63 characters long');
|
|
@@ -150,7 +150,7 @@ export class SecurityUtils {
|
|
|
150
150
|
}
|
|
151
151
|
const nameRegex = /^[A-Z_][A-Z0-9_]*$/;
|
|
152
152
|
if (!nameRegex.test(sanitizedName)) {
|
|
153
|
-
throw new Error(`Invalid environment variable name: ${
|
|
153
|
+
throw new Error(`Invalid environment variable name: ${SecurityUtils.sanitizeLogMessage(name)}`);
|
|
154
154
|
}
|
|
155
155
|
const hasDangerousPattern = (val) => {
|
|
156
156
|
if (val.includes('$(') && val.includes(')'))
|
|
@@ -173,7 +173,7 @@ export class SecurityUtils {
|
|
|
173
173
|
};
|
|
174
174
|
const dangerousPattern = hasDangerousPattern(value);
|
|
175
175
|
if (dangerousPattern) {
|
|
176
|
-
throw new Error(`Environment variable value contains dangerous pattern (${dangerousPattern}): ${
|
|
176
|
+
throw new Error(`Environment variable value contains dangerous pattern (${dangerousPattern}): ${SecurityUtils.sanitizeLogMessage(value)}`);
|
|
177
177
|
}
|
|
178
178
|
if (value.length > 32768) {
|
|
179
179
|
throw new Error('Environment variable value exceeds maximum length (32KB)');
|
|
@@ -194,7 +194,7 @@ export class SecurityUtils {
|
|
|
194
194
|
}
|
|
195
195
|
const tagRegex = /^[a-zA-Z0-9._-]+$/;
|
|
196
196
|
if (!tagRegex.test(trimmedTag)) {
|
|
197
|
-
throw new Error(`Invalid image tag format: ${
|
|
197
|
+
throw new Error(`Invalid image tag format: ${SecurityUtils.sanitizeLogMessage(trimmedTag)}`);
|
|
198
198
|
}
|
|
199
199
|
if (trimmedTag.length > 128) {
|
|
200
200
|
throw new Error('Image tag exceeds maximum length (128 characters)');
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { Chart } from 'cdk8s';
|
|
4
|
+
import { SecurityUtils } from '../security.js';
|
|
4
5
|
import { dumpHelmAwareYaml } from '../utils/helmYamlSerializer.js';
|
|
5
6
|
import { generateHelpersTemplate } from '../utils/helmHelpers.js';
|
|
6
7
|
export class FlexibleSubchart extends Chart {
|
|
@@ -51,13 +52,19 @@ export class FlexibleSubchart extends Chart {
|
|
|
51
52
|
version: this.config.version || '1.0.0',
|
|
52
53
|
appVersion: this.config.version || '1.0.0',
|
|
53
54
|
};
|
|
54
|
-
|
|
55
|
+
const chartYamlPath = SecurityUtils.validatePath(join(outputDir, 'Chart.yaml'), process.cwd(), {
|
|
56
|
+
allowAbsolute: true,
|
|
57
|
+
});
|
|
58
|
+
writeFileSync(chartYamlPath, dumpHelmAwareYaml(subchartYaml));
|
|
55
59
|
const subchartValues = {
|
|
56
60
|
enabled: true,
|
|
57
61
|
...Object.fromEntries(Object.entries(this.config).filter(([key, value]) => key !== 'chart' && typeof value !== 'function' && value !== null && value !== undefined)),
|
|
58
62
|
};
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
const valuesYamlPath = SecurityUtils.validatePath(join(outputDir, 'values.yaml'), process.cwd(), { allowAbsolute: true });
|
|
64
|
+
writeFileSync(valuesYamlPath, dumpHelmAwareYaml(subchartValues));
|
|
65
|
+
const templatesDir = SecurityUtils.validatePath(join(outputDir, 'templates'), process.cwd(), {
|
|
66
|
+
allowAbsolute: true,
|
|
67
|
+
});
|
|
61
68
|
if (!existsSync(templatesDir)) {
|
|
62
69
|
mkdirSync(templatesDir, { recursive: true });
|
|
63
70
|
}
|
|
@@ -65,13 +72,15 @@ export class FlexibleSubchart extends Chart {
|
|
|
65
72
|
includeKubernetes: true,
|
|
66
73
|
includeSprig: true,
|
|
67
74
|
});
|
|
68
|
-
|
|
75
|
+
const helpersTplPath = SecurityUtils.validatePath(join(templatesDir, '_helpers.tpl'), process.cwd(), { allowAbsolute: true });
|
|
76
|
+
writeFileSync(helpersTplPath, helpersTpl);
|
|
69
77
|
this._manifests.forEach((item) => {
|
|
70
78
|
if (item && typeof item === 'object' && 'manifest' in item && 'id' in item) {
|
|
71
79
|
const { manifest, id } = item;
|
|
72
80
|
if (manifest && typeof manifest === 'object') {
|
|
73
81
|
const templateContent = this._generateManifestTemplate(manifest, id);
|
|
74
|
-
|
|
82
|
+
const manifestPath = SecurityUtils.validatePath(join(templatesDir, `${id}.yaml`), process.cwd(), { allowAbsolute: true });
|
|
83
|
+
writeFileSync(manifestPath, templateContent);
|
|
75
84
|
}
|
|
76
85
|
}
|
|
77
86
|
});
|
|
@@ -2,6 +2,7 @@ import { writeFileSync, mkdirSync, existsSync, copyFileSync, readdirSync, rmSync
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
import { App, Chart, ApiObject } from 'cdk8s';
|
|
5
|
+
import { SecurityUtils } from '../security.js';
|
|
5
6
|
import { dumpHelmAwareYaml } from '../utils/helmYamlSerializer.js';
|
|
6
7
|
import { generateHelpersTemplate } from '../utils/helmHelpers.js';
|
|
7
8
|
import { createFlexibleSubchart } from './flexible-subchart.js';
|
|
@@ -227,10 +228,12 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
227
228
|
else if (subchart.chart && typeof subchart.chart === 'object') {
|
|
228
229
|
this._handleObjectChart(subchart, flexibleSubchart);
|
|
229
230
|
}
|
|
230
|
-
|
|
231
|
+
const sanitizedName = subchart.name.replace(/[\r\n]/g, '');
|
|
232
|
+
console.log(`Added flexible subchart: ${sanitizedName}`);
|
|
231
233
|
}
|
|
232
234
|
catch (_error) {
|
|
233
|
-
|
|
235
|
+
const sanitizedName = subchart.name.replace(/[\r\n]/g, '');
|
|
236
|
+
console.warn(`Failed to add subchart ${sanitizedName}:`, _error);
|
|
234
237
|
this._createFallbackSubchart(subchart, index);
|
|
235
238
|
}
|
|
236
239
|
}
|
|
@@ -249,7 +252,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
249
252
|
version: subchart.version || '1.0.0',
|
|
250
253
|
description: `${subchart.name} subchart (fallback)`,
|
|
251
254
|
});
|
|
252
|
-
|
|
255
|
+
const sanitizedName = subchart.name.replace(/[\r\n]/g, '');
|
|
256
|
+
console.log(`Created fallback subchart: ${sanitizedName}`);
|
|
253
257
|
}
|
|
254
258
|
_handleFunctionChart(subchart, index, flexibleSubchart) {
|
|
255
259
|
try {
|
|
@@ -263,7 +267,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
263
267
|
}
|
|
264
268
|
}
|
|
265
269
|
catch (error) {
|
|
266
|
-
|
|
270
|
+
const sanitizedName = subchart.name.replace(/[\r\n]/g, '');
|
|
271
|
+
console.log(`Failed to process subchart ${sanitizedName}:`, error);
|
|
267
272
|
}
|
|
268
273
|
}
|
|
269
274
|
_handleObjectChart(subchart, flexibleSubchart) {
|
|
@@ -338,7 +343,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
338
343
|
}
|
|
339
344
|
}
|
|
340
345
|
catch (error) {
|
|
341
|
-
|
|
346
|
+
const sanitizedId = asset.id.replace(/[\r\n]/g, '');
|
|
347
|
+
console.log(`Failed to parse asset ${sanitizedId}:`, error);
|
|
342
348
|
}
|
|
343
349
|
});
|
|
344
350
|
}
|
|
@@ -375,7 +381,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
375
381
|
includeSprig: true,
|
|
376
382
|
});
|
|
377
383
|
writeFileSync(join(templatesDir, '_helpers.tpl'), helpersTpl);
|
|
378
|
-
|
|
384
|
+
const sanitizedName = subchartName.replace(/[\r\n]/g, '');
|
|
385
|
+
console.log(`Created basic Helm chart structure for subchart: ${sanitizedName}`);
|
|
379
386
|
}
|
|
380
387
|
writeHelmChart(outputDir) {
|
|
381
388
|
if (!existsSync(outputDir)) {
|
|
@@ -412,7 +419,7 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
412
419
|
mkdirSync(chartsDir, { recursive: true });
|
|
413
420
|
}
|
|
414
421
|
this.config.subcharts?.forEach((subchart) => {
|
|
415
|
-
const subchartDir = join(chartsDir, subchart.name);
|
|
422
|
+
const subchartDir = SecurityUtils.validatePath(join(chartsDir, subchart.name), process.cwd(), { allowAbsolute: true });
|
|
416
423
|
if (!existsSync(subchartDir)) {
|
|
417
424
|
mkdirSync(subchartDir, { recursive: true });
|
|
418
425
|
}
|
|
@@ -444,7 +451,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
444
451
|
}
|
|
445
452
|
}
|
|
446
453
|
catch (fallbackError) {
|
|
447
|
-
|
|
454
|
+
const sanitizedName = subchart.name.replace(/[\r\n]/g, '');
|
|
455
|
+
console.warn(`Failed to create subchart ${sanitizedName}:`, fallbackError);
|
|
448
456
|
flexibleSubchart.writeHelmChart(subchartDir);
|
|
449
457
|
}
|
|
450
458
|
}
|
|
@@ -462,7 +470,9 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
462
470
|
flexibleSubchart.writeHelmChart(subchartDir);
|
|
463
471
|
}
|
|
464
472
|
});
|
|
465
|
-
const templatesDir = join(outputDir, 'templates')
|
|
473
|
+
const templatesDir = SecurityUtils.validatePath(join(outputDir, 'templates'), process.cwd(), {
|
|
474
|
+
allowAbsolute: true,
|
|
475
|
+
});
|
|
466
476
|
if (!existsSync(templatesDir)) {
|
|
467
477
|
mkdirSync(templatesDir, { recursive: true });
|
|
468
478
|
}
|
|
@@ -479,7 +489,8 @@ export class UmbrellaChartTemplate extends Chart {
|
|
|
479
489
|
},
|
|
480
490
|
},
|
|
481
491
|
};
|
|
482
|
-
|
|
492
|
+
const namespaceYamlPath = SecurityUtils.validatePath(join(templatesDir, 'namespace.yaml'), process.cwd(), { allowAbsolute: true });
|
|
493
|
+
writeFileSync(namespaceYamlPath, dumpHelmAwareYaml(namespaceYaml));
|
|
483
494
|
const helpersTpl = generateHelpersTemplate('aws', undefined, {
|
|
484
495
|
includeKubernetes: true,
|
|
485
496
|
includeSprig: true,
|
|
@@ -27,19 +27,24 @@ export class UmbrellaRutter {
|
|
|
27
27
|
allowAbsolute: true,
|
|
28
28
|
});
|
|
29
29
|
mkdirSync(validatedOutDir, { recursive: true });
|
|
30
|
-
|
|
30
|
+
const chartsDir = SecurityUtils.validatePath(join(validatedOutDir, 'charts'), process.cwd(), {
|
|
31
|
+
allowAbsolute: true,
|
|
32
|
+
});
|
|
33
|
+
mkdirSync(chartsDir, { recursive: true });
|
|
31
34
|
for (const subchart of this.props.subcharts) {
|
|
32
35
|
if (!SecurityUtils.isValidSubchartName(subchart.name)) {
|
|
33
36
|
throw new Error(`Invalid subchart name: ${SecurityUtils.sanitizeLogMessage(subchart.name)}`);
|
|
34
37
|
}
|
|
35
38
|
const sanitizedName = subchart.name;
|
|
36
|
-
const subchartDir = join(validatedOutDir, 'charts', sanitizedName);
|
|
39
|
+
const subchartDir = SecurityUtils.validatePath(join(validatedOutDir, 'charts', sanitizedName), process.cwd(), { allowAbsolute: true });
|
|
37
40
|
subchart.rutter.write(subchartDir);
|
|
38
41
|
}
|
|
39
42
|
this.writeParentChart(validatedOutDir);
|
|
40
43
|
this.writeParentValues(validatedOutDir);
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
const templatesDir = SecurityUtils.validatePath(join(validatedOutDir, 'templates'), process.cwd(), { allowAbsolute: true });
|
|
45
|
+
mkdirSync(templatesDir, { recursive: true });
|
|
46
|
+
const notesPath = SecurityUtils.validatePath(join(validatedOutDir, 'templates', 'NOTES.txt'), process.cwd(), { allowAbsolute: true });
|
|
47
|
+
writeFileSync(notesPath, this.generateNotesTemplate());
|
|
43
48
|
}
|
|
44
49
|
writeParentChart(outDir) {
|
|
45
50
|
const chartYaml = {
|
|
@@ -63,7 +68,10 @@ export class UmbrellaRutter {
|
|
|
63
68
|
};
|
|
64
69
|
}),
|
|
65
70
|
};
|
|
66
|
-
|
|
71
|
+
const chartYamlPath = SecurityUtils.validatePath(join(outDir, 'Chart.yaml'), process.cwd(), {
|
|
72
|
+
allowAbsolute: true,
|
|
73
|
+
});
|
|
74
|
+
writeFileSync(chartYamlPath, dumpHelmAwareYaml(chartYaml));
|
|
67
75
|
}
|
|
68
76
|
writeParentValues(outDir) {
|
|
69
77
|
const values = {
|
|
@@ -77,12 +85,16 @@ export class UmbrellaRutter {
|
|
|
77
85
|
};
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
|
-
|
|
88
|
+
const valuesYamlPath = SecurityUtils.validatePath(join(outDir, 'values.yaml'), process.cwd(), {
|
|
89
|
+
allowAbsolute: true,
|
|
90
|
+
});
|
|
91
|
+
writeFileSync(valuesYamlPath, dumpHelmAwareYaml(values));
|
|
81
92
|
if (this.props.envValues) {
|
|
82
93
|
for (const [env, envVals] of Object.entries(this.props.envValues)) {
|
|
83
94
|
const sanitizedEnv = SecurityUtils.sanitizeEnvironmentName(env);
|
|
84
95
|
const envValues = this.deepMerge(values, envVals);
|
|
85
|
-
|
|
96
|
+
const envValuesPath = SecurityUtils.validatePath(join(outDir, `values-${sanitizedEnv}.yaml`), process.cwd(), { allowAbsolute: true });
|
|
97
|
+
writeFileSync(envValuesPath, dumpHelmAwareYaml(envValues));
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
100
|
}
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { SecurityUtils } from '../security.js';
|
|
4
5
|
import { createHelmExpression } from './helmControlStructures.js';
|
|
5
6
|
export function loadEnvVarsConfig(options = {}) {
|
|
6
7
|
const { configPath, fallbackConfig = [] } = options;
|
|
7
8
|
try {
|
|
8
|
-
if (configPath
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
if (configPath) {
|
|
10
|
+
const validatedConfigPath = SecurityUtils.validatePath(configPath, process.cwd(), {
|
|
11
|
+
allowAbsolute: true,
|
|
12
|
+
});
|
|
13
|
+
if (existsSync(validatedConfigPath)) {
|
|
14
|
+
const data = readFileSync(validatedConfigPath, 'utf-8');
|
|
15
|
+
return validatedConfigPath.endsWith('.yaml') || validatedConfigPath.endsWith('.yml')
|
|
16
|
+
? parseYaml(data)
|
|
17
|
+
: JSON.parse(data);
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
|
-
const yamlPath = join(process.cwd(), 'env-config.yaml');
|
|
20
|
+
const yamlPath = SecurityUtils.validatePath(join(process.cwd(), 'env-config.yaml'), process.cwd(), { allowAbsolute: true });
|
|
15
21
|
if (existsSync(yamlPath)) {
|
|
16
22
|
const data = readFileSync(yamlPath, 'utf-8');
|
|
17
23
|
return parseYaml(data);
|
|
18
24
|
}
|
|
19
|
-
const jsonPath = join(process.cwd(), 'env-config.json');
|
|
25
|
+
const jsonPath = SecurityUtils.validatePath(join(process.cwd(), 'env-config.json'), process.cwd(), { allowAbsolute: true });
|
|
20
26
|
if (existsSync(jsonPath)) {
|
|
21
27
|
const data = readFileSync(jsonPath, 'utf-8');
|
|
22
28
|
return JSON.parse(data);
|
|
@@ -261,12 +261,20 @@ export const AWS_HELPERS = [
|
|
|
261
261
|
];
|
|
262
262
|
export function formatHelpers(helpers) {
|
|
263
263
|
return helpers
|
|
264
|
-
.map((helper) =>
|
|
264
|
+
.map((helper) => {
|
|
265
|
+
if (!helper.name || typeof helper.name !== 'string') {
|
|
266
|
+
throw new Error('Helper must have a valid name property');
|
|
267
|
+
}
|
|
268
|
+
if (!helper.template || typeof helper.template !== 'string') {
|
|
269
|
+
throw new Error(`Helper '${helper.name}' must have a valid template property`);
|
|
270
|
+
}
|
|
271
|
+
return `{{/*
|
|
265
272
|
${helper.name}
|
|
266
273
|
*/}}
|
|
267
274
|
{{- define "${helper.name}" -}}
|
|
268
275
|
${helper.template}
|
|
269
|
-
{{- end }}
|
|
276
|
+
{{- end }}`;
|
|
277
|
+
})
|
|
270
278
|
.join('\n\n');
|
|
271
279
|
}
|
|
272
280
|
export function getDefaultHelpers(cloudProvider, options) {
|