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.
- package/CHANGELOG.md +192 -0
- package/README.md +606 -119
- package/SECURITY.md +25 -11
- package/dist/cli.js +54 -15
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lib/helm.js +28 -1
- package/dist/lib/helmChartWriter.js +27 -8
- package/dist/lib/policy/configurationLoader.d.ts +46 -0
- package/dist/lib/policy/configurationLoader.js +251 -0
- package/dist/lib/policy/errorContextGenerator.d.ts +63 -0
- package/dist/lib/policy/errorContextGenerator.js +302 -0
- package/dist/lib/policy/errors.d.ts +40 -0
- package/dist/lib/policy/errors.js +109 -0
- package/dist/lib/policy/index.d.ts +11 -0
- package/dist/lib/policy/index.js +10 -0
- package/dist/lib/policy/parallelExecutor.d.ts +58 -0
- package/dist/lib/policy/parallelExecutor.js +215 -0
- package/dist/lib/policy/pluginLoader.d.ts +41 -0
- package/dist/lib/policy/pluginLoader.js +220 -0
- package/dist/lib/policy/pluginRegistry.d.ts +14 -0
- package/dist/lib/policy/pluginRegistry.js +69 -0
- package/dist/lib/policy/policyEngine.d.ts +39 -0
- package/dist/lib/policy/policyEngine.js +495 -0
- package/dist/lib/policy/resultAggregator.d.ts +8 -0
- package/dist/lib/policy/resultAggregator.js +138 -0
- package/dist/lib/policy/resultFormatter.d.ts +25 -0
- package/dist/lib/policy/resultFormatter.js +217 -0
- package/dist/lib/policy/types.d.ts +111 -0
- package/dist/lib/policy/types.js +1 -0
- package/dist/lib/policy/validationCache.d.ts +58 -0
- package/dist/lib/policy/validationCache.js +289 -0
- 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/rutter.d.ts +7 -2
- package/dist/lib/rutter.js +177 -8
- package/dist/lib/security.js +4 -4
- package/dist/lib/templates/flexible-subchart.js +18 -7
- package/dist/lib/templates/umbrella-chart.js +29 -18
- package/dist/lib/umbrellaRutter.d.ts +1 -1
- package/dist/lib/umbrellaRutter.js +21 -9
- 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/dist/lib/validation/inputValidator.d.ts +26 -0
- package/dist/lib/validation/inputValidator.js +176 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.js +1 -0
- 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
|
|
40
|
-
|
|
|
41
|
-
|
|
|
42
|
-
| 2.
|
|
43
|
-
| 2.0-2.
|
|
44
|
-
|
|
|
45
|
-
|
|
|
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
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
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
|
|
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/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
|
-
|
|
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`);
|
|
@@ -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
|
+
}
|