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
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { parse as parseYaml } from 'yaml';
|
|
4
|
+
import { SecurityUtils } from '../security.js';
|
|
4
5
|
import { createHelmExpression } from './helmControlStructures.js';
|
|
5
6
|
export function loadEnvVarsConfig(options = {}) {
|
|
6
7
|
const { configPath, fallbackConfig = [] } = options;
|
|
7
8
|
try {
|
|
8
|
-
if (configPath
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
if (configPath) {
|
|
10
|
+
const validatedConfigPath = SecurityUtils.validatePath(configPath, process.cwd(), {
|
|
11
|
+
allowAbsolute: true,
|
|
12
|
+
});
|
|
13
|
+
if (existsSync(validatedConfigPath)) {
|
|
14
|
+
const data = readFileSync(validatedConfigPath, 'utf-8');
|
|
15
|
+
return validatedConfigPath.endsWith('.yaml') || validatedConfigPath.endsWith('.yml')
|
|
16
|
+
? parseYaml(data)
|
|
17
|
+
: JSON.parse(data);
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
|
-
const yamlPath = join(process.cwd(), 'env-config.yaml');
|
|
20
|
+
const yamlPath = SecurityUtils.validatePath(join(process.cwd(), 'env-config.yaml'), process.cwd(), { allowAbsolute: true });
|
|
15
21
|
if (existsSync(yamlPath)) {
|
|
16
22
|
const data = readFileSync(yamlPath, 'utf-8');
|
|
17
23
|
return parseYaml(data);
|
|
18
24
|
}
|
|
19
|
-
const jsonPath = join(process.cwd(), 'env-config.json');
|
|
25
|
+
const jsonPath = SecurityUtils.validatePath(join(process.cwd(), 'env-config.json'), process.cwd(), { allowAbsolute: true });
|
|
20
26
|
if (existsSync(jsonPath)) {
|
|
21
27
|
const data = readFileSync(jsonPath, 'utf-8');
|
|
22
28
|
return JSON.parse(data);
|
|
@@ -261,12 +261,20 @@ export const AWS_HELPERS = [
|
|
|
261
261
|
];
|
|
262
262
|
export function formatHelpers(helpers) {
|
|
263
263
|
return helpers
|
|
264
|
-
.map((helper) =>
|
|
264
|
+
.map((helper) => {
|
|
265
|
+
if (!helper.name || typeof helper.name !== 'string') {
|
|
266
|
+
throw new Error('Helper must have a valid name property');
|
|
267
|
+
}
|
|
268
|
+
if (!helper.template || typeof helper.template !== 'string') {
|
|
269
|
+
throw new Error(`Helper '${helper.name}' must have a valid template property`);
|
|
270
|
+
}
|
|
271
|
+
return `{{/*
|
|
265
272
|
${helper.name}
|
|
266
273
|
*/}}
|
|
267
274
|
{{- define "${helper.name}" -}}
|
|
268
275
|
${helper.template}
|
|
269
|
-
{{- end }}
|
|
276
|
+
{{- end }}`;
|
|
277
|
+
})
|
|
270
278
|
.join('\n\n');
|
|
271
279
|
}
|
|
272
280
|
export function getDefaultHelpers(cloudProvider, options) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Document, Scalar, isMap, isScalar, visit, Pair } from 'yaml';
|
|
2
|
+
import { SecurityUtils } from '../security.js';
|
|
2
3
|
import { isHelmConstruct, isHelmExpression, createHelmExpression, } from './helmControlStructures.js';
|
|
3
4
|
import { isHelmValue, isHelmFieldConditional, isHelmRange, isHelmWith, } from './valuesRef.js';
|
|
4
5
|
const FIELD_CONDITIONAL = '__FIELD_CONDITIONAL__:';
|
|
@@ -542,7 +543,8 @@ export function dumpHelmAwareYaml(obj, options = {}) {
|
|
|
542
543
|
const contentLines = content.split('\n');
|
|
543
544
|
const firstContentLine = contentLines[0]?.trim() || '';
|
|
544
545
|
if (firstContentLine.startsWith(`${fieldKey}:`)) {
|
|
545
|
-
|
|
546
|
+
const escapedFieldKey = fieldKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
547
|
+
contentLines[0] = contentLines[0].replace(new RegExp(`^\\s*${escapedFieldKey}:\\s*`), '');
|
|
546
548
|
content = contentLines.join('\n');
|
|
547
549
|
}
|
|
548
550
|
const contentFormatted = content
|
|
@@ -640,7 +642,7 @@ export function postProcessFieldConditionals(yaml) {
|
|
|
640
642
|
.join('\n');
|
|
641
643
|
result = result.substring(0, secondLastNewline + 1) + processed + result.substring(markerEnd);
|
|
642
644
|
}
|
|
643
|
-
result = result.replace(/:\s*\|[-+]?\s*\n(\
|
|
645
|
+
result = result.replace(/:\s*\|[-+]?\s*\n([ \t]*\{\{)/g, ':\n$1');
|
|
644
646
|
result = result.replace(/"(\{\{[\s\S]*?\}\})"/g, '$1');
|
|
645
647
|
return result;
|
|
646
648
|
}
|
|
@@ -718,13 +720,14 @@ export function parseHelmExpressions(content) {
|
|
|
718
720
|
let match;
|
|
719
721
|
regex.lastIndex = 0;
|
|
720
722
|
while ((match = regex.exec(line)) !== null) {
|
|
723
|
+
const sanitizedExpression = SecurityUtils.sanitizeLogMessage(match[0] || '');
|
|
721
724
|
expressions.push({
|
|
722
725
|
type,
|
|
723
|
-
expression:
|
|
726
|
+
expression: sanitizedExpression,
|
|
724
727
|
startLine: i + 1,
|
|
725
728
|
startCol: match.index + 1,
|
|
726
729
|
endLine: i + 1,
|
|
727
|
-
endCol: match.index + match[0]
|
|
730
|
+
endCol: match.index + (match[0]?.length || 0) + 1,
|
|
728
731
|
});
|
|
729
732
|
if (match.index === regex.lastIndex)
|
|
730
733
|
regex.lastIndex++;
|
|
@@ -780,14 +783,19 @@ function validateFunctionCalls(_content) {
|
|
|
780
783
|
function checkCommonIssues(yaml, warnings) {
|
|
781
784
|
const deprecatedFunctions = ['template'];
|
|
782
785
|
for (const func of deprecatedFunctions) {
|
|
783
|
-
|
|
786
|
+
if (!/^[a-zA-Z0-9_]+$/.test(func)) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const escapedFunc = func.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
790
|
+
const pattern = new RegExp(`\\{\\{[^}]*\\b${escapedFunc}\\b[^}]*\\}\\}`, 'g');
|
|
784
791
|
let match;
|
|
785
792
|
while ((match = pattern.exec(yaml)) !== null) {
|
|
793
|
+
const sanitizedFunc = SecurityUtils.sanitizeLogMessage(func);
|
|
786
794
|
warnings.push({
|
|
787
795
|
type: 'semantic',
|
|
788
|
-
message: `Function '${
|
|
796
|
+
message: `Function '${sanitizedFunc}' is deprecated`,
|
|
789
797
|
expression: match[0],
|
|
790
|
-
suggestion: `Consider avoiding deprecated function '${
|
|
798
|
+
suggestion: `Consider avoiding deprecated function '${sanitizedFunc}'`,
|
|
791
799
|
});
|
|
792
800
|
}
|
|
793
801
|
}
|
|
@@ -798,11 +806,13 @@ function checkQuotedExpressions(yaml, warnings) {
|
|
|
798
806
|
let match;
|
|
799
807
|
pattern.lastIndex = 0;
|
|
800
808
|
while ((match = pattern.exec(yaml)) !== null) {
|
|
809
|
+
const sanitizedMatch = SecurityUtils.sanitizeLogMessage(match[1] || '');
|
|
810
|
+
const sanitizedExpression = SecurityUtils.sanitizeLogMessage(match[0] || '');
|
|
801
811
|
warnings.push({
|
|
802
812
|
type: 'semantic',
|
|
803
813
|
message: 'Helm expression should not be quoted',
|
|
804
|
-
expression:
|
|
805
|
-
suggestion: `Remove quotes around: ${
|
|
814
|
+
expression: sanitizedExpression,
|
|
815
|
+
suggestion: `Remove quotes around: ${sanitizedMatch}`,
|
|
806
816
|
});
|
|
807
817
|
}
|
|
808
818
|
}
|
package/dist/lib/utils/logger.js
CHANGED
|
@@ -180,19 +180,24 @@ export class TimonelLogger {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
requestSerializer(req) {
|
|
183
|
-
|
|
183
|
+
try {
|
|
184
|
+
if (!req || typeof req !== 'object')
|
|
185
|
+
return {};
|
|
186
|
+
const request = req;
|
|
187
|
+
return {
|
|
188
|
+
method: request.method,
|
|
189
|
+
url: SecurityUtils.sanitizeLogMessage(request.url || ''),
|
|
190
|
+
headers: {
|
|
191
|
+
'user-agent': request.headers?.['user-agent'],
|
|
192
|
+
'content-type': request.headers?.['content-type'],
|
|
193
|
+
},
|
|
194
|
+
remoteAddress: request.remoteAddress,
|
|
195
|
+
remotePort: request.remotePort,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
184
199
|
return {};
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
method: request.method,
|
|
188
|
-
url: SecurityUtils.sanitizeLogMessage(request.url || ''),
|
|
189
|
-
headers: {
|
|
190
|
-
'user-agent': request.headers?.['user-agent'],
|
|
191
|
-
'content-type': request.headers?.['content-type'],
|
|
192
|
-
},
|
|
193
|
-
remoteAddress: request.remoteAddress,
|
|
194
|
-
remotePort: request.remotePort,
|
|
195
|
-
};
|
|
200
|
+
}
|
|
196
201
|
}
|
|
197
202
|
responseSerializer(res) {
|
|
198
203
|
if (!res || typeof res !== 'object')
|
|
@@ -256,40 +261,50 @@ export class TimonelLogger {
|
|
|
256
261
|
}
|
|
257
262
|
}
|
|
258
263
|
sanitizeContext(context) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
264
|
+
try {
|
|
265
|
+
const sanitized = {};
|
|
266
|
+
for (const [key, value] of Object.entries(context)) {
|
|
267
|
+
if (TimonelLogger.SENSITIVE_FIELDS.has(key.toLowerCase())) {
|
|
268
|
+
sanitized[key] = '[REDACTED]';
|
|
269
|
+
}
|
|
270
|
+
else if (typeof value === 'string') {
|
|
271
|
+
sanitized[key] = SecurityUtils.sanitizeLogMessage(value);
|
|
272
|
+
}
|
|
273
|
+
else if (value && typeof value === 'object') {
|
|
274
|
+
sanitized[key] = this.sanitizeNestedObject(value, 2);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
sanitized[key] = value;
|
|
278
|
+
}
|
|
272
279
|
}
|
|
280
|
+
return sanitized;
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return {};
|
|
273
284
|
}
|
|
274
|
-
return sanitized;
|
|
275
285
|
}
|
|
276
286
|
sanitizeNestedObject(obj, depth) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const sanitized = {};
|
|
281
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
282
|
-
if (typeof value === 'string') {
|
|
283
|
-
sanitized[key] = SecurityUtils.sanitizeLogMessage(value);
|
|
287
|
+
try {
|
|
288
|
+
if (depth <= 0 || !obj || typeof obj !== 'object') {
|
|
289
|
+
return obj;
|
|
284
290
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
291
|
+
const sanitized = {};
|
|
292
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
293
|
+
if (typeof value === 'string') {
|
|
294
|
+
sanitized[key] = SecurityUtils.sanitizeLogMessage(value);
|
|
295
|
+
}
|
|
296
|
+
else if (value && typeof value === 'object') {
|
|
297
|
+
sanitized[key] = this.sanitizeNestedObject(value, depth - 1);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
sanitized[key] = value;
|
|
301
|
+
}
|
|
290
302
|
}
|
|
303
|
+
return sanitized;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return obj;
|
|
291
307
|
}
|
|
292
|
-
return sanitized;
|
|
293
308
|
}
|
|
294
309
|
child(context) {
|
|
295
310
|
const sanitizedContext = this.sanitizeContext(context);
|
|
@@ -10,6 +10,9 @@ function serializeValue(value) {
|
|
|
10
10
|
return String(value);
|
|
11
11
|
}
|
|
12
12
|
function createCondition(condition) {
|
|
13
|
+
if (!condition || typeof condition !== 'string') {
|
|
14
|
+
throw new Error('Condition must be a non-empty string');
|
|
15
|
+
}
|
|
13
16
|
return {
|
|
14
17
|
[HELM_VALUE_SYMBOL]: true,
|
|
15
18
|
__condition: condition,
|
|
@@ -17,9 +20,15 @@ function createCondition(condition) {
|
|
|
17
20
|
return createCondition(`not (${this.__condition})`);
|
|
18
21
|
},
|
|
19
22
|
and(other) {
|
|
23
|
+
if (!other || typeof other.__condition !== 'string') {
|
|
24
|
+
throw new Error('Invalid HelmCondition provided to and()');
|
|
25
|
+
}
|
|
20
26
|
return createCondition(`and (${this.__condition}) (${other.__condition})`);
|
|
21
27
|
},
|
|
22
28
|
or(other) {
|
|
29
|
+
if (!other || typeof other.__condition !== 'string') {
|
|
30
|
+
throw new Error('Invalid HelmCondition provided to or()');
|
|
31
|
+
}
|
|
23
32
|
return createCondition(`or (${this.__condition}) (${other.__condition})`);
|
|
24
33
|
},
|
|
25
34
|
toString() {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PolicyConfig, PolicyContext, PolicyRule, PluginConfig } from '../../types/index.js';
|
|
2
|
+
export interface ValidationOptions {
|
|
3
|
+
maxStringLength?: number;
|
|
4
|
+
maxArrayLength?: number;
|
|
5
|
+
maxObjectDepth?: number;
|
|
6
|
+
allowedProtocols?: string[];
|
|
7
|
+
sanitizeStrings?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare class InputValidator {
|
|
10
|
+
private options;
|
|
11
|
+
constructor(options?: ValidationOptions);
|
|
12
|
+
validatePolicyConfig(config: unknown): PolicyConfig;
|
|
13
|
+
validatePolicyRule(rule: unknown): PolicyRule;
|
|
14
|
+
validatePolicyContext(context: unknown): PolicyContext;
|
|
15
|
+
validatePluginConfig(config: unknown): PluginConfig;
|
|
16
|
+
private validateStringLength;
|
|
17
|
+
private validateArrayLength;
|
|
18
|
+
private validateObjectDepth;
|
|
19
|
+
private sanitizeString;
|
|
20
|
+
private sanitizeObject;
|
|
21
|
+
}
|
|
22
|
+
export declare const defaultValidator: InputValidator;
|
|
23
|
+
export declare const validatePolicyConfig: (config: unknown) => PolicyConfig;
|
|
24
|
+
export declare const validatePolicyRule: (rule: unknown) => PolicyRule;
|
|
25
|
+
export declare const validatePolicyContext: (context: unknown) => PolicyContext;
|
|
26
|
+
export declare const validatePluginConfig: (config: unknown) => PluginConfig;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { PolicyValidationError } from '../policy/errors.js';
|
|
2
|
+
const DEFAULT_OPTIONS = {
|
|
3
|
+
maxStringLength: 10000,
|
|
4
|
+
maxArrayLength: 1000,
|
|
5
|
+
maxObjectDepth: 10,
|
|
6
|
+
allowedProtocols: ['http', 'https'],
|
|
7
|
+
sanitizeStrings: true,
|
|
8
|
+
};
|
|
9
|
+
export class InputValidator {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
12
|
+
}
|
|
13
|
+
validatePolicyConfig(config) {
|
|
14
|
+
if (!config || typeof config !== 'object') {
|
|
15
|
+
throw new PolicyValidationError('PolicyConfig must be a non-null object');
|
|
16
|
+
}
|
|
17
|
+
const typedConfig = config;
|
|
18
|
+
if (!typedConfig.id || typeof typedConfig.id !== 'string') {
|
|
19
|
+
throw new PolicyValidationError('PolicyConfig.id must be a non-empty string');
|
|
20
|
+
}
|
|
21
|
+
if (!typedConfig.name || typeof typedConfig.name !== 'string') {
|
|
22
|
+
throw new PolicyValidationError('PolicyConfig.name must be a non-empty string');
|
|
23
|
+
}
|
|
24
|
+
if (!Array.isArray(typedConfig.rules)) {
|
|
25
|
+
throw new PolicyValidationError('PolicyConfig.rules must be an array');
|
|
26
|
+
}
|
|
27
|
+
this.validateStringLength(typedConfig.id, 'PolicyConfig.id');
|
|
28
|
+
this.validateStringLength(typedConfig.name, 'PolicyConfig.name');
|
|
29
|
+
this.validateArrayLength(typedConfig.rules, 'PolicyConfig.rules');
|
|
30
|
+
const validatedRules = typedConfig.rules.map((rule, index) => {
|
|
31
|
+
try {
|
|
32
|
+
return this.validatePolicyRule(rule);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new PolicyValidationError(`PolicyConfig.rules[${index}]: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const sanitizedConfig = {
|
|
39
|
+
id: this.sanitizeString(typedConfig.id),
|
|
40
|
+
name: this.sanitizeString(typedConfig.name),
|
|
41
|
+
rules: validatedRules,
|
|
42
|
+
enabled: typeof typedConfig.enabled === 'boolean' ? typedConfig.enabled : true,
|
|
43
|
+
};
|
|
44
|
+
if (typedConfig.description) {
|
|
45
|
+
sanitizedConfig.description = this.sanitizeString(typedConfig.description);
|
|
46
|
+
}
|
|
47
|
+
if (typedConfig.version) {
|
|
48
|
+
sanitizedConfig.version = this.sanitizeString(typedConfig.version);
|
|
49
|
+
}
|
|
50
|
+
return sanitizedConfig;
|
|
51
|
+
}
|
|
52
|
+
validatePolicyRule(rule) {
|
|
53
|
+
if (!rule || typeof rule !== 'object') {
|
|
54
|
+
throw new PolicyValidationError('PolicyRule must be a non-null object');
|
|
55
|
+
}
|
|
56
|
+
const typedRule = rule;
|
|
57
|
+
if (!typedRule.id || typeof typedRule.id !== 'string') {
|
|
58
|
+
throw new PolicyValidationError('PolicyRule.id must be a non-empty string');
|
|
59
|
+
}
|
|
60
|
+
if (!typedRule.type || typeof typedRule.type !== 'string') {
|
|
61
|
+
throw new PolicyValidationError('PolicyRule.type must be a non-empty string');
|
|
62
|
+
}
|
|
63
|
+
if (!typedRule.condition || typeof typedRule.condition !== 'object') {
|
|
64
|
+
throw new PolicyValidationError('PolicyRule.condition must be an object');
|
|
65
|
+
}
|
|
66
|
+
this.validateStringLength(typedRule.id, 'PolicyRule.id');
|
|
67
|
+
this.validateStringLength(typedRule.type, 'PolicyRule.type');
|
|
68
|
+
this.validateObjectDepth(typedRule.condition, 'PolicyRule.condition');
|
|
69
|
+
const sanitizedRule = {
|
|
70
|
+
id: this.sanitizeString(typedRule.id),
|
|
71
|
+
type: this.sanitizeString(typedRule.type),
|
|
72
|
+
condition: this.sanitizeObject(typedRule.condition),
|
|
73
|
+
priority: typeof typedRule.priority === 'number' ? typedRule.priority : 0,
|
|
74
|
+
enabled: typeof typedRule.enabled === 'boolean' ? typedRule.enabled : true,
|
|
75
|
+
};
|
|
76
|
+
if (typedRule.action) {
|
|
77
|
+
sanitizedRule.action = this.sanitizeString(typedRule.action);
|
|
78
|
+
}
|
|
79
|
+
return sanitizedRule;
|
|
80
|
+
}
|
|
81
|
+
validatePolicyContext(context) {
|
|
82
|
+
if (!context || typeof context !== 'object') {
|
|
83
|
+
throw new PolicyValidationError('PolicyContext must be a non-null object');
|
|
84
|
+
}
|
|
85
|
+
const typedContext = context;
|
|
86
|
+
this.validateObjectDepth(typedContext, 'PolicyContext');
|
|
87
|
+
return this.sanitizeObject(typedContext);
|
|
88
|
+
}
|
|
89
|
+
validatePluginConfig(config) {
|
|
90
|
+
if (!config || typeof config !== 'object') {
|
|
91
|
+
throw new PolicyValidationError('PluginConfig must be a non-null object');
|
|
92
|
+
}
|
|
93
|
+
const typedConfig = config;
|
|
94
|
+
if (!typedConfig.name || typeof typedConfig.name !== 'string') {
|
|
95
|
+
throw new PolicyValidationError('PluginConfig.name must be a non-empty string');
|
|
96
|
+
}
|
|
97
|
+
this.validateStringLength(typedConfig.name, 'PluginConfig.name');
|
|
98
|
+
if (typedConfig.config && typeof typedConfig.config === 'object') {
|
|
99
|
+
this.validateObjectDepth(typedConfig.config, 'PluginConfig.config');
|
|
100
|
+
}
|
|
101
|
+
const sanitizedConfig = {
|
|
102
|
+
name: this.sanitizeString(typedConfig.name),
|
|
103
|
+
enabled: typeof typedConfig.enabled === 'boolean' ? typedConfig.enabled : true,
|
|
104
|
+
};
|
|
105
|
+
if (typedConfig.version) {
|
|
106
|
+
sanitizedConfig.version = this.sanitizeString(typedConfig.version);
|
|
107
|
+
}
|
|
108
|
+
if (typedConfig.config) {
|
|
109
|
+
sanitizedConfig.config = this.sanitizeObject(typedConfig.config);
|
|
110
|
+
}
|
|
111
|
+
return sanitizedConfig;
|
|
112
|
+
}
|
|
113
|
+
validateStringLength(value, fieldName) {
|
|
114
|
+
if (value.length > this.options.maxStringLength) {
|
|
115
|
+
throw new PolicyValidationError(`${fieldName} exceeds maximum length of ${this.options.maxStringLength} characters`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
validateArrayLength(value, fieldName) {
|
|
119
|
+
if (value.length > this.options.maxArrayLength) {
|
|
120
|
+
throw new PolicyValidationError(`${fieldName} exceeds maximum length of ${this.options.maxArrayLength} items`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
validateObjectDepth(obj, fieldName, currentDepth = 0) {
|
|
124
|
+
if (currentDepth > this.options.maxObjectDepth) {
|
|
125
|
+
throw new PolicyValidationError(`${fieldName} exceeds maximum object depth of ${this.options.maxObjectDepth}`);
|
|
126
|
+
}
|
|
127
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
128
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
129
|
+
this.validateObjectDepth(value, `${fieldName}.${key}`, currentDepth + 1);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
sanitizeString(value) {
|
|
134
|
+
if (!this.options.sanitizeStrings) {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
let sanitized = value
|
|
138
|
+
.replace(/\0/g, '')
|
|
139
|
+
.split('')
|
|
140
|
+
.filter((char) => {
|
|
141
|
+
const code = char.charCodeAt(0);
|
|
142
|
+
return code > 31 && code !== 127;
|
|
143
|
+
})
|
|
144
|
+
.join('');
|
|
145
|
+
sanitized = sanitized.trim();
|
|
146
|
+
return sanitized;
|
|
147
|
+
}
|
|
148
|
+
sanitizeObject(obj) {
|
|
149
|
+
const sanitized = {};
|
|
150
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
151
|
+
const sanitizedKey = this.sanitizeString(key);
|
|
152
|
+
if (typeof value === 'string') {
|
|
153
|
+
sanitized[sanitizedKey] = this.sanitizeString(value);
|
|
154
|
+
}
|
|
155
|
+
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
156
|
+
sanitized[sanitizedKey] = this.sanitizeObject(value);
|
|
157
|
+
}
|
|
158
|
+
else if (Array.isArray(value)) {
|
|
159
|
+
sanitized[sanitizedKey] = value.map((item) => typeof item === 'string'
|
|
160
|
+
? this.sanitizeString(item)
|
|
161
|
+
: typeof item === 'object' && item !== null
|
|
162
|
+
? this.sanitizeObject(item)
|
|
163
|
+
: item);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
sanitized[sanitizedKey] = value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return sanitized;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export const defaultValidator = new InputValidator();
|
|
173
|
+
export const validatePolicyConfig = (config) => defaultValidator.validatePolicyConfig(config);
|
|
174
|
+
export const validatePolicyRule = (rule) => defaultValidator.validatePolicyRule(rule);
|
|
175
|
+
export const validatePolicyContext = (context) => defaultValidator.validatePolicyContext(context);
|
|
176
|
+
export const validatePluginConfig = (config) => defaultValidator.validatePluginConfig(config);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface PolicyConfig {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
rules: PolicyRule[];
|
|
5
|
+
description?: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface PolicyRule {
|
|
10
|
+
id: string;
|
|
11
|
+
type: string;
|
|
12
|
+
condition: Record<string, unknown>;
|
|
13
|
+
action?: string;
|
|
14
|
+
priority?: number;
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface PolicyContext {
|
|
18
|
+
environment?: string;
|
|
19
|
+
metadata?: Record<string, unknown>;
|
|
20
|
+
[key: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface PluginConfig {
|
|
23
|
+
name: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
config?: Record<string, unknown>;
|
|
26
|
+
enabled?: boolean;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "timonel",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.1.0-beta.1",
|
|
5
5
|
"description": "Timonel: programmatic Helm chart generator using cdk8s (TypeScript)",
|
|
6
6
|
"bin": {
|
|
7
7
|
"timonel": "dist/cli.js",
|
|
@@ -59,6 +59,8 @@
|
|
|
59
59
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
60
60
|
"test:watch": "vitest",
|
|
61
61
|
"test:coverage": "vitest run --coverage",
|
|
62
|
+
"doc:coverage": "tsx scripts/project-doc-coverage.ts",
|
|
63
|
+
"doc:coverage:validate": "tsx scripts/validate-doc-coverage.ts",
|
|
62
64
|
"ci:check": "pnpm typecheck && pnpm lint && pnpm format:check && pnpm build",
|
|
63
65
|
"release": "semantic-release",
|
|
64
66
|
"release:dry": "semantic-release --dry-run",
|
|
@@ -100,28 +102,28 @@
|
|
|
100
102
|
]
|
|
101
103
|
},
|
|
102
104
|
"dependencies": {
|
|
103
|
-
"cdk8s": "^2.70.
|
|
104
|
-
"cdk8s-plus-33": "^2.4.
|
|
105
|
-
"constructs": "^10.4.
|
|
105
|
+
"cdk8s": "^2.70.40",
|
|
106
|
+
"cdk8s-plus-33": "^2.4.17",
|
|
107
|
+
"constructs": "^10.4.4",
|
|
106
108
|
"handlebars": "^4.7.8",
|
|
107
109
|
"pino": "^10.1.0",
|
|
108
|
-
"pino-pretty": "^13.1.
|
|
110
|
+
"pino-pretty": "^13.1.3",
|
|
109
111
|
"ts-node": "^10.9.2",
|
|
110
|
-
"yaml": "^2.8.
|
|
112
|
+
"yaml": "^2.8.2"
|
|
111
113
|
},
|
|
112
114
|
"devDependencies": {
|
|
113
|
-
"@commitlint/cli": "^20.
|
|
114
|
-
"@commitlint/config-conventional": "^20.
|
|
115
|
-
"@eslint/js": "^9.39.
|
|
115
|
+
"@commitlint/cli": "^20.3.0",
|
|
116
|
+
"@commitlint/config-conventional": "^20.3.0",
|
|
117
|
+
"@eslint/js": "^9.39.2",
|
|
116
118
|
"@semantic-release/changelog": "^6.0.3",
|
|
117
119
|
"@semantic-release/exec": "^7.1.0",
|
|
118
120
|
"@semantic-release/git": "^10.0.1",
|
|
119
|
-
"@types/node": "^
|
|
120
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
121
|
-
"@typescript-eslint/parser": "^8.
|
|
122
|
-
"@vitest/coverage-v8": "^
|
|
121
|
+
"@types/node": "^25.0.3",
|
|
122
|
+
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
|
123
|
+
"@typescript-eslint/parser": "^8.51.0",
|
|
124
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
123
125
|
"conventional-changelog-cli": "^5.0.0",
|
|
124
|
-
"eslint": "^9.39.
|
|
126
|
+
"eslint": "^9.39.2",
|
|
125
127
|
"eslint-config-prettier": "^10.1.8",
|
|
126
128
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
127
129
|
"eslint-plugin-import": "^2.32.0",
|
|
@@ -130,13 +132,13 @@
|
|
|
130
132
|
"eslint-plugin-unused-imports": "^4.3.0",
|
|
131
133
|
"husky": "^9.1.7",
|
|
132
134
|
"lint-staged": "^16.2.7",
|
|
133
|
-
"markdownlint": "^0.
|
|
134
|
-
"markdownlint-cli": "^0.
|
|
135
|
-
"prettier": "^3.
|
|
135
|
+
"markdownlint": "^0.40.0",
|
|
136
|
+
"markdownlint-cli": "^0.47.0",
|
|
137
|
+
"prettier": "^3.7.4",
|
|
136
138
|
"semantic-release": "^25.0.2",
|
|
137
|
-
"tsx": "^4.
|
|
139
|
+
"tsx": "^4.21.0",
|
|
138
140
|
"typescript": "^5.9.3",
|
|
139
|
-
"vitest": "^
|
|
141
|
+
"vitest": "^4.0.16"
|
|
140
142
|
},
|
|
141
143
|
"files": [
|
|
142
144
|
"dist",
|