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
@@ -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 && existsSync(configPath)) {
9
- const data = readFileSync(configPath, 'utf-8');
10
- return configPath.endsWith('.yaml') || configPath.endsWith('.yml')
11
- ? parseYaml(data)
12
- : JSON.parse(data);
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
- contentLines[0] = contentLines[0].replace(new RegExp(`^\\s*${fieldKey}:\\s*`), '');
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(\s*\{\{)/g, ':\n$1');
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: match[0],
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].length + 1,
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
- const pattern = new RegExp(`\\{\\{[^}]*\\b${func}\\b[^}]*\\}\\}`, 'g');
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 '${func}' is deprecated`,
796
+ message: `Function '${sanitizedFunc}' is deprecated`,
789
797
  expression: match[0],
790
- suggestion: `Consider avoiding deprecated function '${func}'`,
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: match[0],
805
- suggestion: `Remove quotes around: ${match[1]}`,
814
+ expression: sanitizedExpression,
815
+ suggestion: `Remove quotes around: ${sanitizedMatch}`,
806
816
  });
807
817
  }
808
818
  }
@@ -180,19 +180,24 @@ export class TimonelLogger {
180
180
  }
181
181
  }
182
182
  requestSerializer(req) {
183
- if (!req || typeof req !== 'object')
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
- const request = req;
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
- const sanitized = {};
260
- for (const [key, value] of Object.entries(context)) {
261
- if (TimonelLogger.SENSITIVE_FIELDS.has(key.toLowerCase())) {
262
- sanitized[key] = '[REDACTED]';
263
- }
264
- else if (typeof value === 'string') {
265
- sanitized[key] = SecurityUtils.sanitizeLogMessage(value);
266
- }
267
- else if (value && typeof value === 'object') {
268
- sanitized[key] = this.sanitizeNestedObject(value, 2);
269
- }
270
- else {
271
- sanitized[key] = value;
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
- if (depth <= 0 || !obj || typeof obj !== 'object') {
278
- return obj;
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
- else if (value && typeof value === 'object') {
286
- sanitized[key] = this.sanitizeNestedObject(value, depth - 1);
287
- }
288
- else {
289
- sanitized[key] = value;
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.0.0-beta.1",
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.28",
104
- "cdk8s-plus-33": "^2.4.6",
105
- "constructs": "^10.4.3",
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.2",
110
+ "pino-pretty": "^13.1.3",
109
111
  "ts-node": "^10.9.2",
110
- "yaml": "^2.8.1"
112
+ "yaml": "^2.8.2"
111
113
  },
112
114
  "devDependencies": {
113
- "@commitlint/cli": "^20.1.0",
114
- "@commitlint/config-conventional": "^20.0.0",
115
- "@eslint/js": "^9.39.1",
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": "^24.10.1",
120
- "@typescript-eslint/eslint-plugin": "^8.47.0",
121
- "@typescript-eslint/parser": "^8.47.0",
122
- "@vitest/coverage-v8": "^3.2.4",
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.1",
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.39.0",
134
- "markdownlint-cli": "^0.46.0",
135
- "prettier": "^3.6.2",
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.20.6",
139
+ "tsx": "^4.21.0",
138
140
  "typescript": "^5.9.3",
139
- "vitest": "^3.2.4"
141
+ "vitest": "^4.0.16"
140
142
  },
141
143
  "files": [
142
144
  "dist",