timonel 3.0.0-beta.1 → 3.1.0-beta.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +192 -0
  2. package/README.md +606 -119
  3. package/SECURITY.md +25 -11
  4. package/dist/cli.js +54 -15
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.js +2 -0
  7. package/dist/lib/helm.js +28 -1
  8. package/dist/lib/helmChartWriter.js +27 -8
  9. package/dist/lib/policy/configurationLoader.d.ts +46 -0
  10. package/dist/lib/policy/configurationLoader.js +251 -0
  11. package/dist/lib/policy/errorContextGenerator.d.ts +63 -0
  12. package/dist/lib/policy/errorContextGenerator.js +302 -0
  13. package/dist/lib/policy/errors.d.ts +40 -0
  14. package/dist/lib/policy/errors.js +109 -0
  15. package/dist/lib/policy/index.d.ts +11 -0
  16. package/dist/lib/policy/index.js +10 -0
  17. package/dist/lib/policy/parallelExecutor.d.ts +58 -0
  18. package/dist/lib/policy/parallelExecutor.js +215 -0
  19. package/dist/lib/policy/pluginLoader.d.ts +41 -0
  20. package/dist/lib/policy/pluginLoader.js +220 -0
  21. package/dist/lib/policy/pluginRegistry.d.ts +14 -0
  22. package/dist/lib/policy/pluginRegistry.js +69 -0
  23. package/dist/lib/policy/policyEngine.d.ts +39 -0
  24. package/dist/lib/policy/policyEngine.js +495 -0
  25. package/dist/lib/policy/resultAggregator.d.ts +8 -0
  26. package/dist/lib/policy/resultAggregator.js +138 -0
  27. package/dist/lib/policy/resultFormatter.d.ts +25 -0
  28. package/dist/lib/policy/resultFormatter.js +217 -0
  29. package/dist/lib/policy/types.d.ts +111 -0
  30. package/dist/lib/policy/types.js +1 -0
  31. package/dist/lib/policy/validationCache.d.ts +58 -0
  32. package/dist/lib/policy/validationCache.js +289 -0
  33. package/dist/lib/resources/baseResourceProvider.js +5 -0
  34. package/dist/lib/resources/cloud/aws/awsResources.js +2 -1
  35. package/dist/lib/resources/cloud/aws/karpenterResources.js +16 -2
  36. package/dist/lib/rutter.d.ts +7 -2
  37. package/dist/lib/rutter.js +177 -8
  38. package/dist/lib/security.js +4 -4
  39. package/dist/lib/templates/flexible-subchart.js +18 -7
  40. package/dist/lib/templates/umbrella-chart.js +29 -18
  41. package/dist/lib/umbrellaRutter.d.ts +1 -1
  42. package/dist/lib/umbrellaRutter.js +21 -9
  43. package/dist/lib/utils/envVarsLoader.js +13 -7
  44. package/dist/lib/utils/helmHelpers.js +10 -2
  45. package/dist/lib/utils/helmYamlSerializer.js +19 -9
  46. package/dist/lib/utils/logger.js +54 -39
  47. package/dist/lib/utils/valuesRef.js +9 -0
  48. package/dist/lib/validation/inputValidator.d.ts +26 -0
  49. package/dist/lib/validation/inputValidator.js +176 -0
  50. package/dist/types/index.d.ts +27 -0
  51. package/dist/types/index.js +1 -0
  52. package/package.json +21 -19
package/SECURITY.md CHANGED
@@ -36,13 +36,14 @@ We provide security updates for the following versions:
36
36
 
37
37
  <!-- markdownlint-disable MD060 -->
38
38
 
39
- | Version | Supported | Security Updates | End of Life |
40
- | ------- | -------------- | ---------------- | ----------- |
41
- | 2.4.0+ | ✅ Current | Full support | TBD |
42
- | 2.3.x | ✅ Supported | Full support | TBD |
43
- | 2.0-2.2 | ⚠️ Limited | Critical only | 2024-12-31 |
44
- | 1.x.x | End of life | None | 2024-06-30 |
45
- | < 1.0 | ❌ End of life | None | 2024-01-01 |
39
+ | Version | Supported | Security Updates | End of Life |
40
+ | ------------ | -------------- | ---------------- | ----------- |
41
+ | 3.0.0-beta.1 | ✅ Current | Full support | TBD |
42
+ | 2.14.x | ✅ Supported | Full support | TBD |
43
+ | 2.8.0-2.13.x | Supported | Full support | 2025-06-30 |
44
+ | 2.0-2.7.x | ⚠️ Limited | Critical only | 2025-03-31 |
45
+ | 1.x.x | ❌ End of life | None | 2024-06-30 |
46
+ | < 1.0 | ❌ End of life | None | 2024-01-01 |
46
47
 
47
48
  **Update policy:**
48
49
 
@@ -83,15 +84,28 @@ We provide security updates for the following versions:
83
84
  ### Security Features
84
85
 
85
86
  - **Input validation**: Comprehensive path traversal prevention and sanitization via SecurityUtils
86
- - **Log injection protection**: All user inputs sanitized before logging
87
- - **Code injection prevention**: Strict validation for dynamic module loading
88
- - **TypeScript strict mode**: Compile-time safety checks
87
+ - CWE-22/23: Path traversal protection with URL-encoded sequence detection
88
+ - CWE-20: Proper input validation for CLI flags (--set, --env)
89
+ - Null byte injection prevention
90
+ - Reserved key validation in resource providers
91
+ - **Log injection protection**: All user inputs sanitized before logging (CWE-117)
92
+ - **Code injection prevention**: Strict validation for dynamic module loading (CWE-94)
93
+ - Regex escaping for dynamic pattern construction
94
+ - Function name validation before template generation
95
+ - **Command injection prevention**: Validated inputs for all shell commands (CWE-78/77/88)
96
+ - Release name validation (RFC 1123 subdomain)
97
+ - Namespace validation (Kubernetes naming rules)
98
+ - --set flag validation with support for nested paths and arrays
99
+ - **TypeScript strict mode**: Compile-time safety checks with all strict options enabled
89
100
  - **No eval()**: Static code generation only
90
101
  - **File system isolation**: Controlled output directory access with path validation
102
+ - Absolute path support with explicit allowAbsolute flag
103
+ - Base directory validation for all file operations
91
104
  - **Helm template validation**: Input validation for all template functions
92
105
  - **Karpenter security**: Secure node pool and scheduling configurations
93
106
  - **Performance optimization**: Efficient algorithms preventing DoS via resource exhaustion
94
- - **OWASP compliance**: Following secure coding guidelines (CWE-22, CWE-94, CWE-117)
107
+ - **OWASP compliance**: Following secure coding guidelines (CWE-22, CWE-23, CWE-94, CWE-117,
108
+ CWE-78, CWE-77, CWE-88, CWE-20)
95
109
 
96
110
  ## Security Considerations for Users
97
111
 
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 base = path.join(process.cwd(), validName);
146
- const chartFile = path.join(base, 'chart.ts');
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
- let chartDir = process.cwd();
156
+ const cwd = process.cwd();
157
+ let chartDir = cwd;
156
158
  let outDir;
157
- if (chartDirOrOutDir && fs.existsSync(path.join(chartDirOrOutDir, 'chart.ts'))) {
158
- chartDir = path.resolve(chartDirOrOutDir);
159
- }
160
- else {
161
- outDir = chartDirOrOutDir;
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
- const defaultOutDir = path.join(chartDir, 'dist');
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 umbrellaFile = path.join(process.cwd(), UMBRELLA_FILE_NAME);
483
- const defaultOutDir = path.join(process.cwd(), 'dist');
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 wrapperFile = path.join(process.cwd(), '.timonel-umbrella-wrapper.mjs');
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/index.d.ts CHANGED
@@ -3,9 +3,12 @@ export * from './lib/helmChartWriter.js';
3
3
  export * from './lib/rutter.js';
4
4
  export * from './lib/security.js';
5
5
  export * from './lib/umbrella.js';
6
+ export * from './lib/policy/index.js';
7
+ export * from './lib/validation/inputValidator.js';
6
8
  export { FlexibleSubchart, createFlexibleSubchart } from './lib/templates/flexible-subchart.js';
7
9
  export { UmbrellaChartTemplate as UmbrellaChart } from './lib/templates/umbrella-chart.js';
8
10
  export type { ChartProps, SubchartProps } from './lib/types.js';
11
+ export type { PolicyConfig, PolicyRule, PolicyContext, PluginConfig } from './types/index.js';
9
12
  export type { AWSALBIngressSpec, AWSEBSStorageClassSpec, AWSEFSStorageClassSpec, AWSIRSAServiceAccountSpec, } from './lib/resources/cloud/aws/awsResources.js';
10
13
  export type { KarpenterDisruption, KarpenterDisruptionBudget, KarpenterEC2NodeClassSpec, KarpenterNodeClaimSpec, KarpenterNodePoolSpec, } from './lib/resources/cloud/aws/karpenterResources.js';
11
14
  export { DEFAULT_TERMINATION_GRACE_PERIOD, isValidDisruptionBudget, isValidKubernetesDuration, KarpenterVersionUtils, } from './lib/resources/cloud/aws/karpenterResources.js';
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ export * from './lib/helmChartWriter.js';
3
3
  export * from './lib/rutter.js';
4
4
  export * from './lib/security.js';
5
5
  export * from './lib/umbrella.js';
6
+ export * from './lib/policy/index.js';
7
+ export * from './lib/validation/inputValidator.js';
6
8
  export { FlexibleSubchart, createFlexibleSubchart } from './lib/templates/flexible-subchart.js';
7
9
  export { UmbrellaChartTemplate as UmbrellaChart } from './lib/templates/umbrella-chart.js';
8
10
  export { DEFAULT_TERMINATION_GRACE_PERIOD, isValidDisruptionBudget, isValidKubernetesDuration, KarpenterVersionUtils, } from './lib/resources/cloud/aws/karpenterResources.js';
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
- fs.mkdirSync(path.join(outDir, 'templates'), { recursive: true });
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
- fs.writeFileSync(path.join(outDir, 'Chart.yaml'), chartYaml);
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
- fs.writeFileSync(path.join(outDir, 'values.yaml'), dumpHelmAwareYaml(defaultValues));
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
- fs.writeFileSync(path.join(outDir, `values-${sanitizedEnv}.yaml`), dumpHelmAwareYaml(values));
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
- fs.writeFileSync(path.join(outDir, 'templates', '_helpers.tpl'), content);
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
- fs.writeFileSync(path.join(outDir, 'templates', 'NOTES.txt'), notesTpl.endsWith('\n') ? notesTpl : notesTpl + '\n');
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
- fs.writeFileSync(path.join(outDir, 'values.schema.json'), JSON.stringify(valuesSchema, null, 2) + '\n');
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`);
@@ -0,0 +1,46 @@
1
+ import type { JSONSchema, PolicyPlugin, ConfigurationLoaderOptions } from './types.js';
2
+ export type ConfigurationSource = 'environment' | 'file' | 'inline' | 'default';
3
+ export interface ConfigurationEntry {
4
+ readonly value: unknown;
5
+ readonly source: ConfigurationSource;
6
+ readonly environment?: string;
7
+ readonly priority: number;
8
+ readonly schema?: JSONSchema;
9
+ }
10
+ export interface PluginConfiguration {
11
+ readonly pluginName: string;
12
+ readonly config: Record<string, unknown>;
13
+ readonly entries: ConfigurationEntry[];
14
+ readonly validated: boolean;
15
+ readonly validationErrors?: string[];
16
+ }
17
+ export interface EnvironmentConfiguration {
18
+ readonly environment: string;
19
+ readonly plugins: Record<string, unknown>;
20
+ readonly global?: Record<string, unknown>;
21
+ }
22
+ export declare class ConfigurationLoader {
23
+ private readonly options;
24
+ private readonly logger;
25
+ private readonly configurations;
26
+ private readonly environmentConfigs;
27
+ constructor(options?: ConfigurationLoaderOptions);
28
+ loadPluginConfiguration(plugin: PolicyPlugin, environment?: string, inlineConfig?: Record<string, unknown>): Promise<PluginConfiguration>;
29
+ private collectConfigurationEntries;
30
+ private validatePluginConfiguration;
31
+ loadEnvironmentConfiguration(environment: string): Promise<EnvironmentConfiguration | undefined>;
32
+ validateConfiguration(config: Record<string, unknown>, schema: JSONSchema, pluginName: string): void;
33
+ addConfigurationEntry(pluginName: string, entry: ConfigurationEntry): void;
34
+ getConfigurationEntries(pluginName: string): ConfigurationEntry[];
35
+ clearCache(): void;
36
+ private loadFileConfiguration;
37
+ private loadEnvironmentVariableConfiguration;
38
+ private loadEnvironmentFromFiles;
39
+ private mergeConfigurations;
40
+ private parseEnvironmentValue;
41
+ private validateAgainstSchema;
42
+ private validateBasicType;
43
+ private validateObjectProperties;
44
+ private validateRequiredProperties;
45
+ private validateEachProperty;
46
+ }
@@ -0,0 +1,251 @@
1
+ import { createLogger } from '../utils/logger.js';
2
+ import { PluginConfigurationError } from './errors.js';
3
+ const DEFAULT_LOADER_OPTIONS = {
4
+ defaultEnvironment: 'development',
5
+ validateSchemas: true,
6
+ allowUnknownProperties: false,
7
+ configurationFiles: [],
8
+ environmentPrefix: 'TIMONEL_POLICY',
9
+ };
10
+ const CONFIGURATION_PRIORITIES = {
11
+ default: 0,
12
+ file: 10,
13
+ environment: 20,
14
+ inline: 30,
15
+ };
16
+ export class ConfigurationLoader {
17
+ constructor(options = {}) {
18
+ this.configurations = new Map();
19
+ this.environmentConfigs = new Map();
20
+ this.options = { ...DEFAULT_LOADER_OPTIONS, ...options };
21
+ this.logger = createLogger('policy-config-loader');
22
+ this.logger.debug('ConfigurationLoader initialized', {
23
+ options: this.options,
24
+ operation: 'config_loader_init',
25
+ });
26
+ }
27
+ async loadPluginConfiguration(plugin, environment, inlineConfig) {
28
+ const targetEnvironment = environment || this.options.defaultEnvironment;
29
+ this.logger.debug('Loading plugin configuration', {
30
+ pluginName: plugin.name,
31
+ environment: targetEnvironment,
32
+ hasInlineConfig: inlineConfig !== undefined,
33
+ operation: 'load_plugin_config',
34
+ });
35
+ const entries = await this.collectConfigurationEntries(plugin, targetEnvironment, inlineConfig);
36
+ const mergedConfig = this.mergeConfigurations(entries);
37
+ const validationResult = this.validatePluginConfiguration(plugin, mergedConfig);
38
+ const result = {
39
+ pluginName: plugin.name,
40
+ config: mergedConfig,
41
+ entries,
42
+ validated: validationResult.validated,
43
+ ...(validationResult.validationErrors && {
44
+ validationErrors: validationResult.validationErrors,
45
+ }),
46
+ };
47
+ this.logger.info('Plugin configuration loaded', {
48
+ pluginName: plugin.name,
49
+ environment: targetEnvironment,
50
+ entryCount: entries.length,
51
+ validated: validationResult.validated,
52
+ hasErrors: validationResult.validationErrors !== undefined,
53
+ operation: 'plugin_config_loaded',
54
+ });
55
+ return result;
56
+ }
57
+ async collectConfigurationEntries(plugin, targetEnvironment, inlineConfig) {
58
+ const entries = [];
59
+ if (plugin.metadata?.defaultConfig) {
60
+ entries.push({
61
+ value: plugin.metadata.defaultConfig,
62
+ source: 'default',
63
+ priority: CONFIGURATION_PRIORITIES.default,
64
+ ...(plugin.configSchema && { schema: plugin.configSchema }),
65
+ });
66
+ }
67
+ const fileConfig = await this.loadFileConfiguration(plugin.name, targetEnvironment);
68
+ if (fileConfig) {
69
+ entries.push({
70
+ value: fileConfig,
71
+ source: 'file',
72
+ environment: targetEnvironment,
73
+ priority: CONFIGURATION_PRIORITIES.file,
74
+ ...(plugin.configSchema && { schema: plugin.configSchema }),
75
+ });
76
+ }
77
+ const envConfig = this.loadEnvironmentVariableConfiguration(plugin.name);
78
+ if (envConfig) {
79
+ entries.push({
80
+ value: envConfig,
81
+ source: 'environment',
82
+ priority: CONFIGURATION_PRIORITIES.environment,
83
+ ...(plugin.configSchema && { schema: plugin.configSchema }),
84
+ });
85
+ }
86
+ if (inlineConfig) {
87
+ entries.push({
88
+ value: inlineConfig,
89
+ source: 'inline',
90
+ priority: CONFIGURATION_PRIORITIES.inline,
91
+ ...(plugin.configSchema && { schema: plugin.configSchema }),
92
+ });
93
+ }
94
+ return entries;
95
+ }
96
+ validatePluginConfiguration(plugin, mergedConfig) {
97
+ let validated = false;
98
+ let validationErrors;
99
+ if (this.options.validateSchemas && plugin.configSchema) {
100
+ try {
101
+ this.validateConfiguration(mergedConfig, plugin.configSchema, plugin.name);
102
+ validated = true;
103
+ }
104
+ catch (error) {
105
+ validationErrors = [error instanceof Error ? error.message : String(error)];
106
+ this.logger.warn('Plugin configuration validation failed', {
107
+ pluginName: plugin.name,
108
+ errors: validationErrors,
109
+ operation: 'config_validation_failed',
110
+ });
111
+ }
112
+ }
113
+ return { validated, ...(validationErrors && { validationErrors }) };
114
+ }
115
+ async loadEnvironmentConfiguration(environment) {
116
+ if (this.environmentConfigs.has(environment)) {
117
+ return this.environmentConfigs.get(environment);
118
+ }
119
+ this.logger.debug('Loading environment configuration', {
120
+ environment,
121
+ operation: 'load_env_config',
122
+ });
123
+ const envConfig = await this.loadEnvironmentFromFiles(environment);
124
+ if (envConfig) {
125
+ this.environmentConfigs.set(environment, envConfig);
126
+ this.logger.info('Environment configuration loaded', {
127
+ environment,
128
+ pluginCount: Object.keys(envConfig.plugins).length,
129
+ hasGlobal: envConfig.global !== undefined,
130
+ operation: 'env_config_loaded',
131
+ });
132
+ }
133
+ return envConfig;
134
+ }
135
+ validateConfiguration(config, schema, pluginName) {
136
+ try {
137
+ this.validateAgainstSchema(config, schema, []);
138
+ }
139
+ catch (error) {
140
+ throw new PluginConfigurationError(`Configuration validation failed for plugin '${pluginName}': ${error instanceof Error ? error.message : String(error)}`, pluginName, undefined, { config, schema, validationError: error });
141
+ }
142
+ }
143
+ addConfigurationEntry(pluginName, entry) {
144
+ if (!this.configurations.has(pluginName)) {
145
+ this.configurations.set(pluginName, []);
146
+ }
147
+ const entries = this.configurations.get(pluginName);
148
+ entries.push(entry);
149
+ entries.sort((a, b) => b.priority - a.priority);
150
+ this.logger.debug('Configuration entry added', {
151
+ pluginName,
152
+ source: entry.source,
153
+ priority: entry.priority,
154
+ operation: 'config_entry_added',
155
+ });
156
+ }
157
+ getConfigurationEntries(pluginName) {
158
+ return this.configurations.get(pluginName) || [];
159
+ }
160
+ clearCache() {
161
+ this.configurations.clear();
162
+ this.environmentConfigs.clear();
163
+ this.logger.debug('Configuration cache cleared', {
164
+ operation: 'config_cache_cleared',
165
+ });
166
+ }
167
+ async loadFileConfiguration(_pluginName, _environment) {
168
+ return undefined;
169
+ }
170
+ loadEnvironmentVariableConfiguration(pluginName) {
171
+ if (!pluginName) {
172
+ return undefined;
173
+ }
174
+ const prefix = `${this.options.environmentPrefix}_${pluginName.toUpperCase().replace(/[^A-Z0-9]/g, '_')}`;
175
+ const config = Object.create(null);
176
+ let hasConfig = false;
177
+ for (const [key, value] of Object.entries(process.env)) {
178
+ if (key.startsWith(prefix + '_')) {
179
+ const configKey = key.substring(prefix.length + 1).toLowerCase();
180
+ config[configKey] = this.parseEnvironmentValue(value);
181
+ hasConfig = true;
182
+ }
183
+ }
184
+ return hasConfig ? config : undefined;
185
+ }
186
+ async loadEnvironmentFromFiles(_environment) {
187
+ return undefined;
188
+ }
189
+ mergeConfigurations(entries) {
190
+ const sortedEntries = [...entries].sort((a, b) => a.priority - b.priority);
191
+ let merged = {};
192
+ for (const entry of sortedEntries) {
193
+ if (entry.value && typeof entry.value === 'object' && !Array.isArray(entry.value)) {
194
+ merged = { ...merged, ...entry.value };
195
+ }
196
+ }
197
+ return merged;
198
+ }
199
+ parseEnvironmentValue(value) {
200
+ if (!value)
201
+ return undefined;
202
+ try {
203
+ return JSON.parse(value);
204
+ }
205
+ catch {
206
+ return value;
207
+ }
208
+ }
209
+ validateAgainstSchema(config, schema, path) {
210
+ this.validateBasicType(config, schema, path);
211
+ this.validateObjectProperties(config, schema, path);
212
+ }
213
+ validateBasicType(config, schema, path) {
214
+ if (schema.type) {
215
+ const actualType = Array.isArray(config) ? 'array' : typeof config;
216
+ if (actualType !== schema.type) {
217
+ throw new Error(`Expected ${schema.type} at ${path.join('.')}, got ${actualType}`);
218
+ }
219
+ }
220
+ }
221
+ validateObjectProperties(config, schema, path) {
222
+ if (schema.type === 'object' &&
223
+ schema.properties &&
224
+ typeof config === 'object' &&
225
+ config !== null) {
226
+ const configObj = config;
227
+ this.validateRequiredProperties(configObj, schema, path);
228
+ this.validateEachProperty(configObj, schema, path);
229
+ }
230
+ }
231
+ validateRequiredProperties(configObj, schema, path) {
232
+ if (schema.required) {
233
+ for (const requiredProp of schema.required) {
234
+ if (!(requiredProp in configObj)) {
235
+ throw new Error(`Missing required property '${requiredProp}' at ${path.join('.')}`);
236
+ }
237
+ }
238
+ }
239
+ }
240
+ validateEachProperty(configObj, schema, path) {
241
+ for (const [propName, propValue] of Object.entries(configObj)) {
242
+ const propSchema = schema.properties?.[propName];
243
+ if (propSchema) {
244
+ this.validateAgainstSchema(propValue, propSchema, [...path, propName]);
245
+ }
246
+ else if (!this.options.allowUnknownProperties && !schema.additionalProperties) {
247
+ throw new Error(`Unknown property '${propName}' at ${path.join('.')}`);
248
+ }
249
+ }
250
+ }
251
+ }