timonel 2.12.1 → 2.12.2-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 +6 -0
- package/dist/cli.js +140 -39
- package/dist/lib/helmChartWriter.d.ts +2 -4
- package/dist/lib/helmChartWriter.js +71 -17
- package/dist/lib/security.d.ts +4 -1
- package/dist/lib/security.js +58 -8
- package/dist/lib/umbrellaRutter.js +3 -1
- package/dist/lib/utils/helmHelpers.d.ts +2 -4
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## [2.12.2-beta.1](https://github.com/KenkoGeek/timonel/compare/v2.12.1...v2.12.2-beta.1) (2025-10-04)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
- **cli:** handle flags after positional arguments ([#142](https://github.com/KenkoGeek/timonel/issues/142)) ([36ac024](https://github.com/KenkoGeek/timonel/commit/36ac024fa7c2a454a184394337a65ebae46a65f9))
|
|
6
|
+
|
|
1
7
|
## [2.12.1](https://github.com/KenkoGeek/timonel/compare/v2.12.0...v2.12.1) (2025-09-28)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
package/dist/cli.js
CHANGED
|
@@ -3,10 +3,66 @@ import { spawnSync } from 'child_process';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createRequire } from 'module';
|
|
6
7
|
import { SecurityUtils } from './lib/security.js';
|
|
7
8
|
import { createLogger } from './lib/utils/logger.js';
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = path.dirname(__filename);
|
|
11
|
+
const esmRequire = createRequire(import.meta.url);
|
|
12
|
+
function getLocalTsxCliPath() {
|
|
13
|
+
try {
|
|
14
|
+
return esmRequire.resolve('tsx/cli');
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const sanitizedMessage = SecurityUtils.sanitizeLogMessage('Unable to locate the local tsx CLI module required to execute commands.');
|
|
18
|
+
console.error(sanitizedMessage);
|
|
19
|
+
if (error instanceof Error && error.message) {
|
|
20
|
+
console.error(SecurityUtils.sanitizeLogMessage(error.message));
|
|
21
|
+
}
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function escapeForSingleQuotedLiteral(value) {
|
|
26
|
+
return value
|
|
27
|
+
.replace(/\\/g, '\\\\')
|
|
28
|
+
.replace(/'/g, "\\'")
|
|
29
|
+
.replace(/\r/g, '\\r')
|
|
30
|
+
.replace(/\n/g, '\\n')
|
|
31
|
+
.replace(/\t/g, '\\t');
|
|
32
|
+
}
|
|
33
|
+
function resolveOutputDirectory(requestedOutDir, baseDir, silent) {
|
|
34
|
+
let validatedOutDir;
|
|
35
|
+
try {
|
|
36
|
+
validatedOutDir = SecurityUtils.validatePath(requestedOutDir, baseDir, {
|
|
37
|
+
allowAbsolute: true,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (!silent) {
|
|
42
|
+
console.error(SecurityUtils.sanitizeLogMessage(error.message));
|
|
43
|
+
}
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (fs.existsSync(validatedOutDir)) {
|
|
47
|
+
try {
|
|
48
|
+
const stats = fs.statSync(validatedOutDir);
|
|
49
|
+
if (!stats.isDirectory()) {
|
|
50
|
+
if (!silent) {
|
|
51
|
+
console.error(`Invalid output path: ${SecurityUtils.sanitizeLogMessage(validatedOutDir)} must be a directory`);
|
|
52
|
+
}
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!silent) {
|
|
58
|
+
console.error(SecurityUtils.sanitizeLogMessage(error.message));
|
|
59
|
+
}
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return validatedOutDir;
|
|
64
|
+
}
|
|
65
|
+
const TSX_CLI_PATH = getLocalTsxCliPath();
|
|
10
66
|
const UMBRELLA_CONFIG_FILE = 'umbrella.config.json';
|
|
11
67
|
const UMBRELLA_FILE_NAME = 'umbrella.ts';
|
|
12
68
|
const PACKAGE_JSON_FILE = 'package.json';
|
|
@@ -95,7 +151,7 @@ async function cmdInit(name, silent = false) {
|
|
|
95
151
|
log(`Generated chart.ts file`, silent);
|
|
96
152
|
log(`Run 'tl synth ${validName}' to generate complete Helm chart`, silent);
|
|
97
153
|
}
|
|
98
|
-
async function cmdSynth(chartDirOrOutDir, flags) {
|
|
154
|
+
async function cmdSynth(chartDirOrOutDir, flags, explicitOutDir) {
|
|
99
155
|
let chartDir = process.cwd();
|
|
100
156
|
let outDir;
|
|
101
157
|
if (chartDirOrOutDir && fs.existsSync(path.join(chartDirOrOutDir, 'chart.ts'))) {
|
|
@@ -104,27 +160,35 @@ async function cmdSynth(chartDirOrOutDir, flags) {
|
|
|
104
160
|
else {
|
|
105
161
|
outDir = chartDirOrOutDir;
|
|
106
162
|
}
|
|
163
|
+
if (explicitOutDir) {
|
|
164
|
+
outDir = explicitOutDir;
|
|
165
|
+
}
|
|
107
166
|
const chartFile = path.join(chartDir, 'chart.ts');
|
|
108
167
|
const defaultOutDir = path.join(chartDir, 'dist');
|
|
109
168
|
if (!fs.existsSync(chartFile)) {
|
|
110
169
|
console.error('chart.ts not found. Run `tl init` first.');
|
|
111
170
|
process.exit(1);
|
|
112
171
|
}
|
|
113
|
-
const
|
|
172
|
+
const requestedOutDir = outDir ?? defaultOutDir;
|
|
173
|
+
const validatedOutDir = resolveOutputDirectory(requestedOutDir, chartDir, flags?.silent);
|
|
114
174
|
const originalContent = fs.readFileSync(chartFile, 'utf8');
|
|
115
|
-
const
|
|
175
|
+
const safeOutDirLiteral = escapeForSingleQuotedLiteral(validatedOutDir);
|
|
176
|
+
let modifiedContent = originalContent.replace(/chart\.writeHelmChart\(['"][^'"]*['"]\)/, `chart.writeHelmChart('${safeOutDirLiteral}')`);
|
|
177
|
+
if (modifiedContent === originalContent) {
|
|
178
|
+
modifiedContent = originalContent.replace(/chart\.write\(['"][^'"]*['"]\)/, `chart.write('${safeOutDirLiteral}')`);
|
|
179
|
+
}
|
|
116
180
|
const tempChartFile = path.join(chartDir, '.timonel-temp-chart.ts');
|
|
117
181
|
fs.writeFileSync(tempChartFile, modifiedContent);
|
|
118
182
|
const wrapperScript = `
|
|
119
183
|
import { pathToFileURL } from 'url';
|
|
120
184
|
|
|
121
185
|
// Import the modified chart file which should execute the synthesis directly
|
|
122
|
-
await import(pathToFileURL(
|
|
186
|
+
await import(pathToFileURL(${JSON.stringify(tempChartFile)}).href);
|
|
123
187
|
`;
|
|
124
188
|
const wrapperFile = path.join(chartDir, '.timonel-wrapper.mjs');
|
|
125
189
|
try {
|
|
126
190
|
fs.writeFileSync(wrapperFile, wrapperScript);
|
|
127
|
-
const result = spawnSync(
|
|
191
|
+
const result = spawnSync(process.execPath, [TSX_CLI_PATH, wrapperFile], {
|
|
128
192
|
stdio: flags?.silent ? 'pipe' : 'inherit',
|
|
129
193
|
encoding: 'utf8',
|
|
130
194
|
cwd: chartDir,
|
|
@@ -421,26 +485,27 @@ async function cmdUmbrellaSynth(outDir, flags) {
|
|
|
421
485
|
console.error('umbrella.ts not found. Run `tl umbrella init` first.');
|
|
422
486
|
process.exit(1);
|
|
423
487
|
}
|
|
424
|
-
const resolvedOutDir = outDir ? path.resolve(outDir) : defaultOutDir;
|
|
425
488
|
const synthMode = flags?.mode ?? 'dependencies';
|
|
426
489
|
if (!UMBRELLA_SYNTH_MODES.includes(synthMode)) {
|
|
427
490
|
usageAndExit('Invalid mode. Use "dependencies" or "inline".', flags?.silent);
|
|
428
491
|
}
|
|
429
|
-
await executeTypeScriptUmbrella(umbrellaFile,
|
|
492
|
+
await executeTypeScriptUmbrella(umbrellaFile, outDir ?? defaultOutDir, synthMode, flags);
|
|
430
493
|
}
|
|
431
494
|
async function executeTypeScriptUmbrella(resolvedPath, outDir, mode, flags) {
|
|
495
|
+
const umbrellaBase = path.dirname(resolvedPath);
|
|
496
|
+
const validatedOutDir = resolveOutputDirectory(outDir, umbrellaBase, flags?.silent);
|
|
432
497
|
const wrapperScript = `
|
|
433
498
|
import { pathToFileURL } from 'url';
|
|
434
499
|
|
|
435
|
-
const mod = await import(pathToFileURL(
|
|
500
|
+
const mod = await import(pathToFileURL(${JSON.stringify(resolvedPath)}).href);
|
|
436
501
|
const runner = mod.default || mod.run || mod.synth;
|
|
437
502
|
if (typeof runner !== 'function') {
|
|
438
503
|
console.error('umbrella.ts must export a default/run/synth function');
|
|
439
504
|
process.exit(1);
|
|
440
505
|
}
|
|
441
506
|
|
|
442
|
-
const output =
|
|
443
|
-
const synthOptions = { mode:
|
|
507
|
+
const output = ${JSON.stringify(validatedOutDir)};
|
|
508
|
+
const synthOptions = { mode: ${JSON.stringify(mode)} };
|
|
444
509
|
const fs = await import('fs');
|
|
445
510
|
fs.mkdirSync(output, { recursive: true });
|
|
446
511
|
await Promise.resolve(runner(output, synthOptions));
|
|
@@ -449,7 +514,7 @@ console.log('Umbrella chart written to ' + output);
|
|
|
449
514
|
const wrapperFile = path.join(process.cwd(), '.timonel-umbrella-wrapper.mjs');
|
|
450
515
|
try {
|
|
451
516
|
fs.writeFileSync(wrapperFile, wrapperScript);
|
|
452
|
-
const result = spawnSync(
|
|
517
|
+
const result = spawnSync(process.execPath, [TSX_CLI_PATH, wrapperFile], {
|
|
453
518
|
stdio: flags?.silent ? 'pipe' : 'inherit',
|
|
454
519
|
encoding: 'utf8',
|
|
455
520
|
});
|
|
@@ -468,45 +533,81 @@ console.log('Umbrella chart written to ' + output);
|
|
|
468
533
|
}
|
|
469
534
|
function parseFlags(args) {
|
|
470
535
|
const flags = {};
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
536
|
+
const positionalArguments = [];
|
|
537
|
+
const takeValue = (flagName) => {
|
|
538
|
+
const value = args.shift();
|
|
539
|
+
if (value === undefined) {
|
|
540
|
+
usageAndExit(`Missing value for ${flagName}`, flags.silent);
|
|
541
|
+
return '';
|
|
542
|
+
}
|
|
543
|
+
return value;
|
|
544
|
+
};
|
|
545
|
+
const flagHandlers = new Map([
|
|
546
|
+
[
|
|
547
|
+
'--dry-run',
|
|
548
|
+
() => {
|
|
475
549
|
flags.dryRun = true;
|
|
476
|
-
|
|
477
|
-
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
[
|
|
553
|
+
'--silent',
|
|
554
|
+
() => {
|
|
478
555
|
flags.silent = true;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
556
|
+
},
|
|
557
|
+
],
|
|
558
|
+
[
|
|
559
|
+
'--env',
|
|
560
|
+
() => {
|
|
561
|
+
const envValue = takeValue('--env');
|
|
562
|
+
if (envValue) {
|
|
483
563
|
flags.env = envValue;
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
],
|
|
567
|
+
[
|
|
568
|
+
'--set',
|
|
569
|
+
() => {
|
|
570
|
+
const setValue = takeValue('--set');
|
|
488
571
|
if (setValue) {
|
|
489
572
|
flags.set = flags.set || [];
|
|
490
573
|
flags.set.push(setValue);
|
|
491
574
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
575
|
+
},
|
|
576
|
+
],
|
|
577
|
+
[
|
|
578
|
+
'--mode',
|
|
579
|
+
() => {
|
|
580
|
+
const modeValue = takeValue('--mode');
|
|
581
|
+
if (!UMBRELLA_SYNTH_MODES.includes(modeValue)) {
|
|
497
582
|
usageAndExit('Invalid mode. Use "dependencies" or "inline".', flags.silent);
|
|
498
583
|
}
|
|
499
584
|
flags.mode = modeValue;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
585
|
+
},
|
|
586
|
+
],
|
|
587
|
+
]);
|
|
588
|
+
const helpFlags = new Set(['--help', '-h']);
|
|
589
|
+
while (args.length > 0) {
|
|
590
|
+
const token = args.shift();
|
|
591
|
+
if (token === undefined) {
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
if (!token.startsWith('-')) {
|
|
595
|
+
positionalArguments.push(token);
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (helpFlags.has(token)) {
|
|
599
|
+
usageAndExit(undefined, flags.silent);
|
|
600
|
+
return flags;
|
|
601
|
+
}
|
|
602
|
+
const handler = flagHandlers.get(token);
|
|
603
|
+
if (handler) {
|
|
604
|
+
handler();
|
|
605
|
+
continue;
|
|
508
606
|
}
|
|
607
|
+
usageAndExit(`Unknown flag: ${token}`, flags.silent);
|
|
509
608
|
}
|
|
609
|
+
args.length = 0;
|
|
610
|
+
args.push(...positionalArguments);
|
|
510
611
|
return flags;
|
|
511
612
|
}
|
|
512
613
|
async function executeCommand(command, args, flags) {
|
|
@@ -515,7 +616,7 @@ async function executeCommand(command, args, flags) {
|
|
|
515
616
|
await cmdInit(args[0], flags.silent);
|
|
516
617
|
break;
|
|
517
618
|
case 'synth':
|
|
518
|
-
await cmdSynth(args[0], flags);
|
|
619
|
+
await cmdSynth(args[0], flags, args[1]);
|
|
519
620
|
break;
|
|
520
621
|
case 'validate':
|
|
521
622
|
await cmdValidate(flags);
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { HelperDefinition as ExternalHelperDefinition } from './utils/helmHelpers/types.js';
|
|
2
|
+
export type HelperDefinition = ExternalHelperDefinition;
|
|
1
3
|
export interface HelmChartMeta {
|
|
2
4
|
name: string;
|
|
3
5
|
version: string;
|
|
@@ -43,10 +45,6 @@ export interface HelmChartWriteOptions {
|
|
|
43
45
|
notesTpl?: string;
|
|
44
46
|
valuesSchema?: Record<string, unknown>;
|
|
45
47
|
}
|
|
46
|
-
export interface HelperDefinition {
|
|
47
|
-
name: string;
|
|
48
|
-
body: string;
|
|
49
|
-
}
|
|
50
48
|
export declare class HelmChartWriter {
|
|
51
49
|
static write(opts: HelmChartWriteOptions): void;
|
|
52
50
|
private static createDirectories;
|
|
@@ -15,7 +15,9 @@ export class HelmChartWriter {
|
|
|
15
15
|
assetCount: assets.length,
|
|
16
16
|
operation: 'helm_write_start',
|
|
17
17
|
});
|
|
18
|
-
const validatedOutDir = SecurityUtils.validatePath(outDir, process.cwd()
|
|
18
|
+
const validatedOutDir = SecurityUtils.validatePath(outDir, process.cwd(), {
|
|
19
|
+
allowAbsolute: true,
|
|
20
|
+
});
|
|
19
21
|
this.createDirectories(validatedOutDir);
|
|
20
22
|
this.writeChartYaml(validatedOutDir, meta);
|
|
21
23
|
this.writeValuesFiles(validatedOutDir, defaultValues, envValues);
|
|
@@ -72,7 +74,7 @@ export class HelmChartWriter {
|
|
|
72
74
|
}
|
|
73
75
|
else if (Array.isArray(helpersTpl)) {
|
|
74
76
|
content = helpersTpl
|
|
75
|
-
.map((h) => [`{{- define "${h.name}" -}}`, h.
|
|
77
|
+
.map((h) => [`{{- define "${h.name}" -}}`, h.template.trimEnd(), '{{- end }}', ''].join('\n'))
|
|
76
78
|
.join('\n');
|
|
77
79
|
}
|
|
78
80
|
fs.writeFileSync(path.join(outDir, 'templates', '_helpers.tpl'), content);
|
|
@@ -124,18 +126,14 @@ function splitDocs(yamlStr) {
|
|
|
124
126
|
}
|
|
125
127
|
function writeAssets(outDir, assets) {
|
|
126
128
|
for (const asset of assets) {
|
|
127
|
-
const sanitizedId = asset.id.replace(/[^a-zA-Z0-9-_]/g, '');
|
|
128
|
-
if (sanitizedId !== asset.id) {
|
|
129
|
-
console.error(`Invalid asset ID detected: ${SecurityUtils.sanitizeLogMessage(asset.id)}`);
|
|
130
|
-
throw new Error(`Invalid asset ID: ${SecurityUtils.sanitizeLogMessage(asset.id)}`);
|
|
131
|
-
}
|
|
132
129
|
const targetDir = getTargetDirectory(asset.target);
|
|
133
130
|
try {
|
|
131
|
+
const { directorySegments, fileBaseName } = resolveAssetPath(asset.id);
|
|
134
132
|
if (asset.singleFile) {
|
|
135
|
-
writeSingleAssetFile(outDir, targetDir,
|
|
133
|
+
writeSingleAssetFile(outDir, targetDir, directorySegments, fileBaseName, asset.yaml);
|
|
136
134
|
}
|
|
137
135
|
else {
|
|
138
|
-
writeMultipleAssetFiles(outDir, targetDir,
|
|
136
|
+
writeMultipleAssetFiles(outDir, targetDir, directorySegments, fileBaseName, asset.yaml);
|
|
139
137
|
}
|
|
140
138
|
}
|
|
141
139
|
catch (error) {
|
|
@@ -147,16 +145,72 @@ function writeAssets(outDir, assets) {
|
|
|
147
145
|
function getTargetDirectory(target) {
|
|
148
146
|
return target === 'crds' ? 'crds' : 'templates';
|
|
149
147
|
}
|
|
150
|
-
function writeSingleAssetFile(outDir, targetDir,
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
fs.
|
|
148
|
+
function writeSingleAssetFile(outDir, targetDir, directorySegments, fileBaseName, yaml) {
|
|
149
|
+
const chartSubdir = path.join(outDir, targetDir, ...directorySegments);
|
|
150
|
+
SecurityUtils.validatePath(chartSubdir, outDir);
|
|
151
|
+
fs.mkdirSync(chartSubdir, { recursive: true });
|
|
152
|
+
const filename = `${fileBaseName}.yaml`;
|
|
153
|
+
const absolutePath = path.join(chartSubdir, filename);
|
|
154
|
+
SecurityUtils.validatePath(absolutePath, outDir);
|
|
155
|
+
fs.writeFileSync(absolutePath, yaml.endsWith('\n') ? yaml : `${yaml}\n`);
|
|
154
156
|
}
|
|
155
|
-
function writeMultipleAssetFiles(outDir, targetDir,
|
|
157
|
+
function writeMultipleAssetFiles(outDir, targetDir, directorySegments, fileBaseName, yaml) {
|
|
158
|
+
const chartSubdir = path.join(outDir, targetDir, ...directorySegments);
|
|
159
|
+
SecurityUtils.validatePath(chartSubdir, outDir);
|
|
160
|
+
fs.mkdirSync(chartSubdir, { recursive: true });
|
|
156
161
|
const parts = splitDocs(yaml);
|
|
157
162
|
parts.forEach((doc, index) => {
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
const suffix = parts.length > 1 ? `-${index + 1}` : '';
|
|
164
|
+
const filename = `${fileBaseName}${suffix}.yaml`;
|
|
165
|
+
const absolutePath = path.join(chartSubdir, filename);
|
|
166
|
+
SecurityUtils.validatePath(absolutePath, outDir);
|
|
167
|
+
fs.writeFileSync(absolutePath, doc.endsWith('\n') ? doc : `${doc}\n`);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function resolveAssetPath(assetId) {
|
|
171
|
+
if (!assetId || typeof assetId !== 'string') {
|
|
172
|
+
throw new Error('Asset ID must be a non-empty string');
|
|
173
|
+
}
|
|
174
|
+
if (assetId.includes('\0')) {
|
|
175
|
+
throw new Error('Asset ID cannot contain null bytes');
|
|
176
|
+
}
|
|
177
|
+
const normalized = assetId.replace(/\\+/g, '/');
|
|
178
|
+
const rawSegments = normalized.split('/');
|
|
179
|
+
if (rawSegments.length === 0) {
|
|
180
|
+
throw new Error('Asset ID must resolve to at least one segment');
|
|
181
|
+
}
|
|
182
|
+
const segments = rawSegments.map((segment) => {
|
|
183
|
+
if (!segment || segment.trim().length === 0) {
|
|
184
|
+
throw new Error('Asset path segments cannot be empty');
|
|
185
|
+
}
|
|
186
|
+
if (segment === '.' || segment === '..') {
|
|
187
|
+
throw new Error('Asset path segments cannot be relative references');
|
|
188
|
+
}
|
|
189
|
+
if (/[<>:"|?*]/.test(segment)) {
|
|
190
|
+
throw new Error('Asset path contains unsupported characters');
|
|
191
|
+
}
|
|
192
|
+
const hasControlCharacters = Array.from(segment).some((character) => {
|
|
193
|
+
const codePoint = character.codePointAt(0);
|
|
194
|
+
if (typeof codePoint !== 'number') {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
if (codePoint < 0x20 || codePoint === 0x7f) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
if (codePoint >= 0x80 && codePoint <= 0x9f) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
});
|
|
205
|
+
if (hasControlCharacters) {
|
|
206
|
+
throw new Error('Asset path contains control characters');
|
|
207
|
+
}
|
|
208
|
+
return segment;
|
|
161
209
|
});
|
|
210
|
+
const fileBaseName = segments[segments.length - 1];
|
|
211
|
+
const directorySegments = segments.slice(0, -1);
|
|
212
|
+
if (!fileBaseName) {
|
|
213
|
+
throw new Error('Asset ID must include a filename segment');
|
|
214
|
+
}
|
|
215
|
+
return { directorySegments, fileBaseName };
|
|
162
216
|
}
|
package/dist/lib/security.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
export interface PathValidationOptions {
|
|
2
|
+
allowAbsolute?: boolean;
|
|
3
|
+
}
|
|
1
4
|
export declare class SecurityUtils {
|
|
2
|
-
static validatePath(inputPath: string, allowedBasePath: string): string;
|
|
5
|
+
static validatePath(inputPath: string, allowedBasePath: string, options?: PathValidationOptions): string;
|
|
3
6
|
static sanitizeLogMessage(message: string): string;
|
|
4
7
|
static sanitizeEnvironmentName(env: string): string;
|
|
5
8
|
static isValidTypeScriptFile(filePath: string): boolean;
|
package/dist/lib/security.js
CHANGED
|
@@ -1,18 +1,68 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
export class SecurityUtils {
|
|
3
|
-
static validatePath(inputPath, allowedBasePath) {
|
|
3
|
+
static validatePath(inputPath, allowedBasePath, options) {
|
|
4
4
|
if (!inputPath || typeof inputPath !== 'string') {
|
|
5
5
|
throw new Error('Invalid path: path must be a non-empty string');
|
|
6
6
|
}
|
|
7
|
-
if (inputPath.includes('
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
if (inputPath.includes('\0')) {
|
|
8
|
+
throw new Error('Invalid path: null bytes are not permitted');
|
|
9
|
+
}
|
|
10
|
+
const allowAbsolute = Boolean(options?.allowAbsolute);
|
|
11
|
+
const candidates = new Set();
|
|
12
|
+
const registerCandidate = (value) => {
|
|
13
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
14
|
+
candidates.add(value);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const addCandidate = (value) => {
|
|
18
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
registerCandidate(value);
|
|
22
|
+
try {
|
|
23
|
+
const normalized = value.normalize('NFKC');
|
|
24
|
+
registerCandidate(normalized);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
addCandidate(inputPath);
|
|
30
|
+
let decoded = inputPath;
|
|
31
|
+
for (let iteration = 0; iteration < 5; iteration += 1) {
|
|
32
|
+
try {
|
|
33
|
+
const nextDecoded = decodeURIComponent(decoded);
|
|
34
|
+
if (nextDecoded === decoded) {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
decoded = nextDecoded;
|
|
38
|
+
addCandidate(decoded);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const containsTraversalSequence = (candidate) => {
|
|
45
|
+
const lowered = candidate.toLowerCase();
|
|
46
|
+
if (lowered.includes('%2e%2f') || lowered.includes('%2e%5c') || lowered.includes('%2f%2e')) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
const normalized = candidate.replace(/\\+/g, '/');
|
|
50
|
+
const segments = normalized.split('/');
|
|
51
|
+
return segments.some((segment) => {
|
|
52
|
+
const loweredSegment = segment.toLowerCase();
|
|
53
|
+
return loweredSegment === '..' || loweredSegment === '%2e%2e';
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
for (const candidate of candidates) {
|
|
57
|
+
if (containsTraversalSequence(candidate)) {
|
|
58
|
+
throw new Error('Invalid path: path traversal sequences detected');
|
|
59
|
+
}
|
|
12
60
|
}
|
|
13
61
|
const resolvedInput = path.resolve(inputPath);
|
|
14
62
|
const resolvedBase = path.resolve(allowedBasePath);
|
|
15
|
-
|
|
63
|
+
const relativePath = path.relative(resolvedBase, resolvedInput);
|
|
64
|
+
const isInsideBase = relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
65
|
+
if (!(allowAbsolute && path.isAbsolute(inputPath)) && !isInsideBase) {
|
|
16
66
|
throw new Error(`Invalid path: path must be within ${resolvedBase}`);
|
|
17
67
|
}
|
|
18
68
|
return resolvedInput;
|
|
@@ -41,7 +91,7 @@ export class SecurityUtils {
|
|
|
41
91
|
return sanitized;
|
|
42
92
|
}
|
|
43
93
|
static isValidTypeScriptFile(filePath) {
|
|
44
|
-
const allowedExtensions = ['.ts', '.tsx'];
|
|
94
|
+
const allowedExtensions = ['.ts', '.tsx', '.cts', '.mts'];
|
|
45
95
|
const ext = path.extname(filePath);
|
|
46
96
|
return allowedExtensions.includes(ext);
|
|
47
97
|
}
|
|
@@ -16,7 +16,9 @@ export class UmbrellaRutter {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
write(outDir) {
|
|
19
|
-
const validatedOutDir = SecurityUtils.validatePath(outDir, process.cwd()
|
|
19
|
+
const validatedOutDir = SecurityUtils.validatePath(outDir, process.cwd(), {
|
|
20
|
+
allowAbsolute: true,
|
|
21
|
+
});
|
|
20
22
|
mkdirSync(validatedOutDir, { recursive: true });
|
|
21
23
|
mkdirSync(join(validatedOutDir, 'charts'), { recursive: true });
|
|
22
24
|
for (const subchart of this.props.subcharts) {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
template: string;
|
|
4
|
-
}
|
|
1
|
+
import type { HelperDefinition } from './helmHelpers/types.js';
|
|
2
|
+
export type { HelperDefinition } from './helmHelpers/types.js';
|
|
5
3
|
export declare const STANDARD_HELPERS: HelperDefinition[];
|
|
6
4
|
export declare const FILE_ACCESS_HELPERS: HelperDefinition[];
|
|
7
5
|
export declare const TEMPLATE_FUNCTION_HELPERS: HelperDefinition[];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "timonel",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.12.1",
|
|
4
|
+
"version": "2.12.2-beta.1",
|
|
5
5
|
"description": "Timonel: programmatic Helm chart generator using cdk8s (TypeScript)",
|
|
6
6
|
"bin": {
|
|
7
7
|
"timonel": "dist/cli.js",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"constructs": "^10.4.2",
|
|
93
93
|
"handlebars": "^4.7.8",
|
|
94
94
|
"js-yaml": "^4.1.0",
|
|
95
|
-
"pino": "^
|
|
95
|
+
"pino": "^10.0.0",
|
|
96
96
|
"pino-pretty": "^13.1.1",
|
|
97
97
|
"ts-node": "^10.9.2",
|
|
98
98
|
"yaml": "^2.8.1"
|