tlc-claude-code 1.4.9 → 1.5.2
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/CLAUDE.md +23 -0
- package/CODING-STANDARDS.md +408 -0
- package/bin/install.js +2 -0
- package/dashboard/dist/components/QualityGatePane.d.ts +38 -0
- package/dashboard/dist/components/QualityGatePane.js +31 -0
- package/dashboard/dist/components/QualityGatePane.test.d.ts +1 -0
- package/dashboard/dist/components/QualityGatePane.test.js +147 -0
- package/dashboard/dist/components/orchestration/AgentCard.d.ts +26 -0
- package/dashboard/dist/components/orchestration/AgentCard.js +60 -0
- package/dashboard/dist/components/orchestration/AgentCard.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentCard.test.js +63 -0
- package/dashboard/dist/components/orchestration/AgentControls.d.ts +11 -0
- package/dashboard/dist/components/orchestration/AgentControls.js +20 -0
- package/dashboard/dist/components/orchestration/AgentControls.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentControls.test.js +52 -0
- package/dashboard/dist/components/orchestration/AgentDetail.d.ts +35 -0
- package/dashboard/dist/components/orchestration/AgentDetail.js +37 -0
- package/dashboard/dist/components/orchestration/AgentDetail.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentDetail.test.js +79 -0
- package/dashboard/dist/components/orchestration/AgentList.d.ts +31 -0
- package/dashboard/dist/components/orchestration/AgentList.js +47 -0
- package/dashboard/dist/components/orchestration/AgentList.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/AgentList.test.js +64 -0
- package/dashboard/dist/components/orchestration/CostMeter.d.ts +11 -0
- package/dashboard/dist/components/orchestration/CostMeter.js +28 -0
- package/dashboard/dist/components/orchestration/CostMeter.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/CostMeter.test.js +50 -0
- package/dashboard/dist/components/orchestration/ModelSelector.d.ts +20 -0
- package/dashboard/dist/components/orchestration/ModelSelector.js +12 -0
- package/dashboard/dist/components/orchestration/ModelSelector.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/ModelSelector.test.js +56 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.d.ts +28 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.js +28 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/OrchestrationDashboard.test.js +56 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.d.ts +11 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.js +37 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.test.d.ts +1 -0
- package/dashboard/dist/components/orchestration/QualityIndicator.test.js +52 -0
- package/dashboard/dist/components/orchestration/index.d.ts +8 -0
- package/dashboard/dist/components/orchestration/index.js +8 -0
- package/package.json +1 -1
- package/server/lib/access-control.js +352 -0
- package/server/lib/access-control.test.js +322 -0
- package/server/lib/agents-cancel-command.js +139 -0
- package/server/lib/agents-cancel-command.test.js +180 -0
- package/server/lib/agents-get-command.js +159 -0
- package/server/lib/agents-get-command.test.js +167 -0
- package/server/lib/agents-list-command.js +150 -0
- package/server/lib/agents-list-command.test.js +149 -0
- package/server/lib/agents-logs-command.js +126 -0
- package/server/lib/agents-logs-command.test.js +198 -0
- package/server/lib/agents-retry-command.js +117 -0
- package/server/lib/agents-retry-command.test.js +192 -0
- package/server/lib/budget-limits.js +222 -0
- package/server/lib/budget-limits.test.js +214 -0
- package/server/lib/code-generator.js +291 -0
- package/server/lib/code-generator.test.js +307 -0
- package/server/lib/cost-command.js +290 -0
- package/server/lib/cost-command.test.js +202 -0
- package/server/lib/cost-optimizer.js +404 -0
- package/server/lib/cost-optimizer.test.js +232 -0
- package/server/lib/cost-projections.js +302 -0
- package/server/lib/cost-projections.test.js +217 -0
- package/server/lib/cost-reports.js +277 -0
- package/server/lib/cost-reports.test.js +254 -0
- package/server/lib/cost-tracker.js +216 -0
- package/server/lib/cost-tracker.test.js +302 -0
- package/server/lib/crypto-patterns.js +433 -0
- package/server/lib/crypto-patterns.test.js +346 -0
- package/server/lib/design-command.js +385 -0
- package/server/lib/design-command.test.js +249 -0
- package/server/lib/design-parser.js +237 -0
- package/server/lib/design-parser.test.js +290 -0
- package/server/lib/gemini-vision.js +377 -0
- package/server/lib/gemini-vision.test.js +282 -0
- package/server/lib/input-validator.js +360 -0
- package/server/lib/input-validator.test.js +295 -0
- package/server/lib/litellm-client.js +232 -0
- package/server/lib/litellm-client.test.js +267 -0
- package/server/lib/litellm-command.js +291 -0
- package/server/lib/litellm-command.test.js +260 -0
- package/server/lib/litellm-config.js +273 -0
- package/server/lib/litellm-config.test.js +212 -0
- package/server/lib/model-pricing.js +189 -0
- package/server/lib/model-pricing.test.js +178 -0
- package/server/lib/models-command.js +223 -0
- package/server/lib/models-command.test.js +193 -0
- package/server/lib/optimize-command.js +197 -0
- package/server/lib/optimize-command.test.js +193 -0
- package/server/lib/orchestration-integration.js +206 -0
- package/server/lib/orchestration-integration.test.js +235 -0
- package/server/lib/output-encoder.js +308 -0
- package/server/lib/output-encoder.test.js +312 -0
- package/server/lib/quality-evaluator.js +396 -0
- package/server/lib/quality-evaluator.test.js +337 -0
- package/server/lib/quality-gate-command.js +340 -0
- package/server/lib/quality-gate-command.test.js +321 -0
- package/server/lib/quality-gate-scorer.js +378 -0
- package/server/lib/quality-gate-scorer.test.js +376 -0
- package/server/lib/quality-history.js +265 -0
- package/server/lib/quality-history.test.js +359 -0
- package/server/lib/quality-presets.js +288 -0
- package/server/lib/quality-presets.test.js +269 -0
- package/server/lib/quality-retry.js +323 -0
- package/server/lib/quality-retry.test.js +325 -0
- package/server/lib/quality-thresholds.js +255 -0
- package/server/lib/quality-thresholds.test.js +237 -0
- package/server/lib/secure-auth.js +333 -0
- package/server/lib/secure-auth.test.js +288 -0
- package/server/lib/secure-code-command.js +540 -0
- package/server/lib/secure-code-command.test.js +309 -0
- package/server/lib/secure-errors.js +521 -0
- package/server/lib/secure-errors.test.js +298 -0
- package/server/lib/vision-command.js +372 -0
- package/server/lib/vision-command.test.js +255 -0
- package/server/lib/visual-command.js +350 -0
- package/server/lib/visual-command.test.js +256 -0
- package/server/lib/visual-testing.js +315 -0
- package/server/lib/visual-testing.test.js +357 -0
- package/server/package-lock.json +2 -2
- package/server/package.json +1 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validator Module
|
|
3
|
+
*
|
|
4
|
+
* Input sanitization and validation patterns for secure code generation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create an input validator
|
|
11
|
+
* @param {Object} options - Validator options
|
|
12
|
+
* @returns {Object} Validator instance
|
|
13
|
+
*/
|
|
14
|
+
function createInputValidator(options = {}) {
|
|
15
|
+
return {
|
|
16
|
+
rules: {
|
|
17
|
+
maxLength: options.rules?.maxLength || 10000,
|
|
18
|
+
...options.rules,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sanitize a string input
|
|
25
|
+
* @param {string} input - Input string
|
|
26
|
+
* @param {Object} options - Sanitization options
|
|
27
|
+
* @returns {string} Sanitized string
|
|
28
|
+
*/
|
|
29
|
+
function sanitizeString(input, options = {}) {
|
|
30
|
+
if (input === null || input === undefined) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let result = String(input);
|
|
35
|
+
|
|
36
|
+
// Remove null bytes
|
|
37
|
+
result = result.replace(/\x00/g, '');
|
|
38
|
+
|
|
39
|
+
// Trim whitespace
|
|
40
|
+
result = result.trim();
|
|
41
|
+
|
|
42
|
+
// Normalize unicode
|
|
43
|
+
if (options.normalize) {
|
|
44
|
+
result = result.normalize('NFC');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Escape HTML entities
|
|
48
|
+
if (options.escapeHtml) {
|
|
49
|
+
result = result
|
|
50
|
+
.replace(/&/g, '&')
|
|
51
|
+
.replace(/</g, '<')
|
|
52
|
+
.replace(/>/g, '>')
|
|
53
|
+
.replace(/"/g, '"')
|
|
54
|
+
.replace(/'/g, ''');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Enforce max length
|
|
58
|
+
if (options.maxLength && result.length > options.maxLength) {
|
|
59
|
+
result = result.substring(0, options.maxLength);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate email address
|
|
67
|
+
* @param {string} email - Email to validate
|
|
68
|
+
* @returns {Object} Validation result
|
|
69
|
+
*/
|
|
70
|
+
function validateEmail(email) {
|
|
71
|
+
if (!email) {
|
|
72
|
+
return { valid: false, error: 'Email is required' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Reject dangerous characters
|
|
76
|
+
if (/["<>]/.test(email)) {
|
|
77
|
+
return { valid: false, error: 'Email contains invalid characters' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// RFC 5322 compliant regex (simplified)
|
|
81
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
82
|
+
|
|
83
|
+
if (!emailRegex.test(email)) {
|
|
84
|
+
return { valid: false, error: 'Invalid email format' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { valid: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate file path
|
|
92
|
+
* @param {string} filePath - Path to validate
|
|
93
|
+
* @param {Object} options - Validation options
|
|
94
|
+
* @returns {Object} Validation result
|
|
95
|
+
*/
|
|
96
|
+
function validatePath(filePath, options = {}) {
|
|
97
|
+
const { basePath } = options;
|
|
98
|
+
|
|
99
|
+
if (!filePath) {
|
|
100
|
+
return { valid: false, error: 'Path is required' };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check for null bytes
|
|
104
|
+
if (filePath.includes('\x00')) {
|
|
105
|
+
return { valid: false, error: 'Path contains null bytes' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Normalize the path
|
|
109
|
+
const normalized = path.normalize(filePath);
|
|
110
|
+
|
|
111
|
+
// Check for path traversal
|
|
112
|
+
if (filePath.includes('..') && basePath) {
|
|
113
|
+
const resolved = path.resolve(basePath, filePath);
|
|
114
|
+
const baseResolved = path.resolve(basePath);
|
|
115
|
+
|
|
116
|
+
if (!resolved.startsWith(baseResolved + path.sep) && resolved !== baseResolved) {
|
|
117
|
+
return { valid: false, error: 'Path traversal detected' };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check if within base path (for absolute paths)
|
|
122
|
+
if (basePath && path.isAbsolute(filePath)) {
|
|
123
|
+
const resolved = path.resolve(filePath);
|
|
124
|
+
const baseResolved = path.resolve(basePath);
|
|
125
|
+
|
|
126
|
+
if (!resolved.startsWith(baseResolved + path.sep) && resolved !== baseResolved) {
|
|
127
|
+
return { valid: false, error: 'Path is outside allowed directory' };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { valid: true, normalized };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validate URL
|
|
136
|
+
* @param {string} url - URL to validate
|
|
137
|
+
* @param {Object} options - Validation options
|
|
138
|
+
* @returns {Object} Validation result
|
|
139
|
+
*/
|
|
140
|
+
function validateUrl(url, options = {}) {
|
|
141
|
+
if (!url) {
|
|
142
|
+
return { valid: false, error: 'URL is required' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check dangerous protocols
|
|
146
|
+
const dangerousProtocols = ['javascript:', 'data:', 'vbscript:'];
|
|
147
|
+
const lowerUrl = url.toLowerCase();
|
|
148
|
+
|
|
149
|
+
for (const protocol of dangerousProtocols) {
|
|
150
|
+
if (lowerUrl.startsWith(protocol)) {
|
|
151
|
+
return { valid: false, error: `Dangerous protocol: ${protocol}` };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const parsed = new URL(url);
|
|
157
|
+
|
|
158
|
+
// Check allowed hosts
|
|
159
|
+
if (options.allowedHosts && options.allowedHosts.length > 0) {
|
|
160
|
+
if (!options.allowedHosts.includes(parsed.hostname)) {
|
|
161
|
+
return { valid: false, error: 'Host not in allowed list' };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Block private IP addresses
|
|
166
|
+
if (options.blockPrivate) {
|
|
167
|
+
const privatePatterns = [
|
|
168
|
+
/^192\.168\./,
|
|
169
|
+
/^10\./,
|
|
170
|
+
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
|
|
171
|
+
/^127\./,
|
|
172
|
+
/^localhost$/i,
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
for (const pattern of privatePatterns) {
|
|
176
|
+
if (pattern.test(parsed.hostname)) {
|
|
177
|
+
return { valid: false, error: 'Private IP addresses are blocked' };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return { valid: true };
|
|
183
|
+
} catch {
|
|
184
|
+
return { valid: false, error: 'Invalid URL format' };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check for SQL injection patterns
|
|
190
|
+
* @param {string} input - Input to check
|
|
191
|
+
* @returns {Object} Detection result
|
|
192
|
+
*/
|
|
193
|
+
function preventSqlInjection(input) {
|
|
194
|
+
if (!input) {
|
|
195
|
+
return { dangerous: false, patterns: [] };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const patterns = [];
|
|
199
|
+
|
|
200
|
+
// Check for common SQL injection patterns
|
|
201
|
+
const sqlPatterns = [
|
|
202
|
+
{ regex: /['"]\s*;\s*--/i, name: 'comment-injection' },
|
|
203
|
+
{ regex: /'\s*OR\s+['"]?\d+['"]?\s*=\s*['"]?\d+/i, name: 'or-injection' },
|
|
204
|
+
{ regex: /UNION\s+SELECT/i, name: 'union-injection' },
|
|
205
|
+
{ regex: /DROP\s+TABLE/i, name: 'drop-table' },
|
|
206
|
+
{ regex: /DELETE\s+FROM/i, name: 'delete-injection' },
|
|
207
|
+
{ regex: /INSERT\s+INTO/i, name: 'insert-injection' },
|
|
208
|
+
{ regex: /EXEC\s*\(/i, name: 'exec-injection' },
|
|
209
|
+
{ regex: /xp_cmdshell/i, name: 'cmdshell-injection' },
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
for (const { regex, name } of sqlPatterns) {
|
|
213
|
+
if (regex.test(input)) {
|
|
214
|
+
patterns.push(name);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check for string concatenation in queries
|
|
219
|
+
if (/["']\s*\+\s*["']?[^"']*["']?/.test(input)) {
|
|
220
|
+
patterns.push('string-concatenation');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const dangerous = patterns.length > 0;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
dangerous,
|
|
227
|
+
patterns,
|
|
228
|
+
suggestion: 'Use parameterized queries instead of string concatenation',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check for command injection patterns
|
|
234
|
+
* @param {string} input - Input to check
|
|
235
|
+
* @returns {Object} Detection result
|
|
236
|
+
*/
|
|
237
|
+
function preventCommandInjection(input) {
|
|
238
|
+
if (!input) {
|
|
239
|
+
return { dangerous: false, safePattern: null };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const dangerous = [
|
|
243
|
+
/[;&|]/, // Command chaining
|
|
244
|
+
/\|/, // Pipe
|
|
245
|
+
/`/, // Backtick substitution
|
|
246
|
+
/\$\(/, // $() substitution
|
|
247
|
+
/>\s*\//, // Output redirection
|
|
248
|
+
/<\s*\//, // Input redirection
|
|
249
|
+
/\n/, // Newline
|
|
250
|
+
/\r/, // Carriage return
|
|
251
|
+
].some((pattern) => pattern.test(input));
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
dangerous,
|
|
255
|
+
safePattern: 'Use allowlist validation for filenames and escape shell arguments',
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Generate validation code
|
|
261
|
+
* @param {Object} options - Generation options
|
|
262
|
+
* @returns {string} Generated code
|
|
263
|
+
*/
|
|
264
|
+
function generateValidationCode(options = {}) {
|
|
265
|
+
const { type, language = 'javascript', includeErrors = false, rules = {} } = options;
|
|
266
|
+
|
|
267
|
+
const generators = {
|
|
268
|
+
javascript: {
|
|
269
|
+
email: () => `
|
|
270
|
+
function validateEmail(email) {
|
|
271
|
+
const regex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
|
|
272
|
+
if (!regex.test(email)) {
|
|
273
|
+
${includeErrors ? 'return { valid: false, error: "Invalid email format" };' : 'return false;'}
|
|
274
|
+
}
|
|
275
|
+
${includeErrors ? 'return { valid: true };' : 'return true;'}
|
|
276
|
+
}`,
|
|
277
|
+
path: () => `
|
|
278
|
+
function validatePath(filePath, basePath) {
|
|
279
|
+
const path = require('path');
|
|
280
|
+
const resolved = path.resolve(basePath, filePath);
|
|
281
|
+
if (!resolved.startsWith(path.resolve(basePath))) {
|
|
282
|
+
${includeErrors ? 'return { valid: false, error: "Path traversal detected" };' : 'return false;'}
|
|
283
|
+
}
|
|
284
|
+
${includeErrors ? 'return { valid: true };' : 'return true;'}
|
|
285
|
+
}`,
|
|
286
|
+
custom: () => `
|
|
287
|
+
function validate(input) {
|
|
288
|
+
${rules.minLength ? `if (input.length < ${rules.minLength}) return false;` : ''}
|
|
289
|
+
${rules.maxLength ? `if (input.length > ${rules.maxLength}) return false;` : ''}
|
|
290
|
+
${rules.pattern ? `if (!/${rules.pattern}/.test(input)) return false;` : ''}
|
|
291
|
+
return true;
|
|
292
|
+
}`,
|
|
293
|
+
},
|
|
294
|
+
typescript: {
|
|
295
|
+
email: () => `
|
|
296
|
+
function validateEmail(email: string): { valid: boolean; error?: string } {
|
|
297
|
+
const regex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
|
|
298
|
+
if (!regex.test(email)) {
|
|
299
|
+
return { valid: false, error: "Invalid email format" };
|
|
300
|
+
}
|
|
301
|
+
return { valid: true };
|
|
302
|
+
}`,
|
|
303
|
+
path: () => `
|
|
304
|
+
function validatePath(filePath: string, basePath: string): { valid: boolean; error?: string } {
|
|
305
|
+
const path = require('path');
|
|
306
|
+
const resolved = path.resolve(basePath, filePath);
|
|
307
|
+
if (!resolved.startsWith(path.resolve(basePath))) {
|
|
308
|
+
return { valid: false, error: "Path traversal detected" };
|
|
309
|
+
}
|
|
310
|
+
return { valid: true };
|
|
311
|
+
}`,
|
|
312
|
+
custom: () => `
|
|
313
|
+
function validate(input: string): boolean {
|
|
314
|
+
${rules.minLength ? `if (input.length < ${rules.minLength}) return false;` : ''}
|
|
315
|
+
${rules.maxLength ? `if (input.length > ${rules.maxLength}) return false;` : ''}
|
|
316
|
+
${rules.pattern ? `if (!/${rules.pattern}/.test(input)) return false;` : ''}
|
|
317
|
+
return true;
|
|
318
|
+
}`,
|
|
319
|
+
},
|
|
320
|
+
python: {
|
|
321
|
+
email: () => `
|
|
322
|
+
import re
|
|
323
|
+
|
|
324
|
+
def validate_email(email: str) -> dict:
|
|
325
|
+
pattern = r'^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$'
|
|
326
|
+
if not re.match(pattern, email):
|
|
327
|
+
return {"valid": False, "error": "Invalid email format"}
|
|
328
|
+
return {"valid": True}`,
|
|
329
|
+
path: () => `
|
|
330
|
+
import os
|
|
331
|
+
|
|
332
|
+
def validate_path(file_path: str, base_path: str) -> dict:
|
|
333
|
+
resolved = os.path.abspath(os.path.join(base_path, file_path))
|
|
334
|
+
if not resolved.startswith(os.path.abspath(base_path)):
|
|
335
|
+
return {"valid": False, "error": "Path traversal detected"}
|
|
336
|
+
return {"valid": True}`,
|
|
337
|
+
custom: () => `
|
|
338
|
+
def validate(input: str) -> bool:
|
|
339
|
+
${rules.minLength ? `if len(input) < ${rules.minLength}: return False` : ''}
|
|
340
|
+
${rules.maxLength ? `if len(input) > ${rules.maxLength}: return False` : ''}
|
|
341
|
+
return True`,
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const lang = generators[language] || generators.javascript;
|
|
346
|
+
const gen = lang[type] || lang.email;
|
|
347
|
+
|
|
348
|
+
return gen();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
module.exports = {
|
|
352
|
+
createInputValidator,
|
|
353
|
+
sanitizeString,
|
|
354
|
+
validateEmail,
|
|
355
|
+
validatePath,
|
|
356
|
+
validateUrl,
|
|
357
|
+
preventSqlInjection,
|
|
358
|
+
preventCommandInjection,
|
|
359
|
+
generateValidationCode,
|
|
360
|
+
};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input Validator Tests
|
|
3
|
+
*
|
|
4
|
+
* Input sanitization and validation patterns for secure code generation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { describe, it, beforeEach } = require('node:test');
|
|
8
|
+
const assert = require('node:assert');
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
createInputValidator,
|
|
12
|
+
sanitizeString,
|
|
13
|
+
validateEmail,
|
|
14
|
+
validatePath,
|
|
15
|
+
validateUrl,
|
|
16
|
+
preventSqlInjection,
|
|
17
|
+
preventCommandInjection,
|
|
18
|
+
generateValidationCode,
|
|
19
|
+
} = require('./input-validator.js');
|
|
20
|
+
|
|
21
|
+
describe('Input Validator', () => {
|
|
22
|
+
let validator;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
validator = createInputValidator();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('createInputValidator', () => {
|
|
29
|
+
it('creates validator with default config', () => {
|
|
30
|
+
assert.ok(validator);
|
|
31
|
+
assert.ok(validator.rules);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('accepts custom rules', () => {
|
|
35
|
+
const custom = createInputValidator({
|
|
36
|
+
rules: {
|
|
37
|
+
maxLength: 100,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
assert.strictEqual(custom.rules.maxLength, 100);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('sanitizeString', () => {
|
|
46
|
+
it('removes null bytes', () => {
|
|
47
|
+
const result = sanitizeString('hello\x00world');
|
|
48
|
+
|
|
49
|
+
assert.strictEqual(result, 'helloworld');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('trims whitespace', () => {
|
|
53
|
+
const result = sanitizeString(' hello ');
|
|
54
|
+
|
|
55
|
+
assert.strictEqual(result, 'hello');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('escapes HTML entities', () => {
|
|
59
|
+
const result = sanitizeString('<script>alert("xss")</script>', { escapeHtml: true });
|
|
60
|
+
|
|
61
|
+
assert.ok(!result.includes('<script>'));
|
|
62
|
+
assert.ok(result.includes('<'));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('enforces max length', () => {
|
|
66
|
+
const result = sanitizeString('a'.repeat(1000), { maxLength: 100 });
|
|
67
|
+
|
|
68
|
+
assert.strictEqual(result.length, 100);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('handles unicode normalization', () => {
|
|
72
|
+
const result = sanitizeString('café', { normalize: true });
|
|
73
|
+
|
|
74
|
+
assert.ok(result.includes('caf'));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('validateEmail', () => {
|
|
79
|
+
it('accepts valid email', () => {
|
|
80
|
+
const result = validateEmail('user@example.com');
|
|
81
|
+
|
|
82
|
+
assert.ok(result.valid);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('rejects invalid email', () => {
|
|
86
|
+
const result = validateEmail('not-an-email');
|
|
87
|
+
|
|
88
|
+
assert.strictEqual(result.valid, false);
|
|
89
|
+
assert.ok(result.error);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('rejects email with dangerous characters', () => {
|
|
93
|
+
const result = validateEmail('user"@example.com');
|
|
94
|
+
|
|
95
|
+
assert.strictEqual(result.valid, false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('handles internationalized domains', () => {
|
|
99
|
+
const result = validateEmail('user@münchen.de');
|
|
100
|
+
|
|
101
|
+
assert.ok(result.valid);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('validatePath', () => {
|
|
106
|
+
it('accepts valid path', () => {
|
|
107
|
+
const result = validatePath('/var/data/file.txt', { basePath: '/var/data' });
|
|
108
|
+
|
|
109
|
+
assert.ok(result.valid);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('rejects path traversal', () => {
|
|
113
|
+
const result = validatePath('/var/data/../etc/passwd', { basePath: '/var/data' });
|
|
114
|
+
|
|
115
|
+
assert.strictEqual(result.valid, false);
|
|
116
|
+
assert.ok(result.error.includes('traversal'));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('rejects null bytes in path', () => {
|
|
120
|
+
const result = validatePath('/var/data/file.txt\x00.jpg', { basePath: '/var/data' });
|
|
121
|
+
|
|
122
|
+
assert.strictEqual(result.valid, false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('normalizes path before validation', () => {
|
|
126
|
+
const result = validatePath('/var/data/./subdir/../file.txt', { basePath: '/var/data' });
|
|
127
|
+
|
|
128
|
+
assert.ok(result.valid);
|
|
129
|
+
assert.strictEqual(result.normalized, '/var/data/file.txt');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('rejects paths outside base', () => {
|
|
133
|
+
const result = validatePath('/etc/passwd', { basePath: '/var/data' });
|
|
134
|
+
|
|
135
|
+
assert.strictEqual(result.valid, false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('validateUrl', () => {
|
|
140
|
+
it('accepts valid HTTPS URL', () => {
|
|
141
|
+
const result = validateUrl('https://example.com/path');
|
|
142
|
+
|
|
143
|
+
assert.ok(result.valid);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('rejects javascript protocol', () => {
|
|
147
|
+
const result = validateUrl('javascript:alert(1)');
|
|
148
|
+
|
|
149
|
+
assert.strictEqual(result.valid, false);
|
|
150
|
+
assert.ok(result.error.includes('protocol'));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('rejects data protocol', () => {
|
|
154
|
+
const result = validateUrl('data:text/html,<script>alert(1)</script>');
|
|
155
|
+
|
|
156
|
+
assert.strictEqual(result.valid, false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('validates allowed hosts', () => {
|
|
160
|
+
const result = validateUrl('https://evil.com', {
|
|
161
|
+
allowedHosts: ['example.com', 'trusted.com'],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
assert.strictEqual(result.valid, false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('blocks private IP addresses', () => {
|
|
168
|
+
const result = validateUrl('http://192.168.1.1', { blockPrivate: true });
|
|
169
|
+
|
|
170
|
+
assert.strictEqual(result.valid, false);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('preventSqlInjection', () => {
|
|
175
|
+
it('detects SQL injection patterns', () => {
|
|
176
|
+
const result = preventSqlInjection("'; DROP TABLE users; --");
|
|
177
|
+
|
|
178
|
+
assert.ok(result.dangerous);
|
|
179
|
+
assert.ok(result.patterns.length > 0);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('allows safe strings', () => {
|
|
183
|
+
const result = preventSqlInjection('John Doe');
|
|
184
|
+
|
|
185
|
+
assert.strictEqual(result.dangerous, false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('detects union-based injection', () => {
|
|
189
|
+
const result = preventSqlInjection('1 UNION SELECT * FROM users');
|
|
190
|
+
|
|
191
|
+
assert.ok(result.dangerous);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('generates parameterized query suggestion', () => {
|
|
195
|
+
const result = preventSqlInjection("user_id = '" + "123" + "'");
|
|
196
|
+
|
|
197
|
+
assert.ok(result.suggestion);
|
|
198
|
+
assert.ok(result.suggestion.includes('parameterized'));
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('preventCommandInjection', () => {
|
|
203
|
+
it('detects command chaining', () => {
|
|
204
|
+
const result = preventCommandInjection('file.txt; rm -rf /');
|
|
205
|
+
|
|
206
|
+
assert.ok(result.dangerous);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('detects pipe injection', () => {
|
|
210
|
+
const result = preventCommandInjection('file.txt | cat /etc/passwd');
|
|
211
|
+
|
|
212
|
+
assert.ok(result.dangerous);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('detects backtick injection', () => {
|
|
216
|
+
const result = preventCommandInjection('`cat /etc/passwd`');
|
|
217
|
+
|
|
218
|
+
assert.ok(result.dangerous);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('detects $() injection', () => {
|
|
222
|
+
const result = preventCommandInjection('$(cat /etc/passwd)');
|
|
223
|
+
|
|
224
|
+
assert.ok(result.dangerous);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('allows safe filenames', () => {
|
|
228
|
+
const result = preventCommandInjection('document.pdf');
|
|
229
|
+
|
|
230
|
+
assert.strictEqual(result.dangerous, false);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('suggests safe alternatives', () => {
|
|
234
|
+
const result = preventCommandInjection('user-input');
|
|
235
|
+
|
|
236
|
+
assert.ok(result.safePattern);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe('generateValidationCode', () => {
|
|
241
|
+
it('generates JavaScript validation function', () => {
|
|
242
|
+
const code = generateValidationCode({
|
|
243
|
+
type: 'email',
|
|
244
|
+
language: 'javascript',
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
assert.ok(code.includes('function'));
|
|
248
|
+
assert.ok(code.includes('email'));
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('generates TypeScript validation function', () => {
|
|
252
|
+
const code = generateValidationCode({
|
|
253
|
+
type: 'email',
|
|
254
|
+
language: 'typescript',
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
assert.ok(code.includes('string'));
|
|
258
|
+
assert.ok(code.includes(':'));
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('generates Python validation function', () => {
|
|
262
|
+
const code = generateValidationCode({
|
|
263
|
+
type: 'email',
|
|
264
|
+
language: 'python',
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
assert.ok(code.includes('def'));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('includes error messages', () => {
|
|
271
|
+
const code = generateValidationCode({
|
|
272
|
+
type: 'path',
|
|
273
|
+
language: 'javascript',
|
|
274
|
+
includeErrors: true,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
assert.ok(code.includes('error') || code.includes('Error'));
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('generates validation for custom rules', () => {
|
|
281
|
+
const code = generateValidationCode({
|
|
282
|
+
type: 'custom',
|
|
283
|
+
language: 'javascript',
|
|
284
|
+
rules: {
|
|
285
|
+
minLength: 8,
|
|
286
|
+
maxLength: 100,
|
|
287
|
+
pattern: '^[a-zA-Z0-9]+$',
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
assert.ok(code.includes('8'));
|
|
292
|
+
assert.ok(code.includes('100'));
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|