vineguard-utils
Version:
Shared utilities for VineGuard - AI-powered testing orchestration
354 lines • 14.8 kB
JavaScript
/**
* Security utilities for VineGuard
* Provides input sanitization, validation, and security configuration
*/
import { createPackageErrorHandler, ValidationError } from './errors.js';
const errorHandler = createPackageErrorHandler('security');
export const DEFAULT_SECURITY_CONFIG = {
maxFileSize: 50 * 1024 * 1024, // 50MB
allowedFileExtensions: [
'.ts', '.js', '.tsx', '.jsx', '.json', '.md', '.yml', '.yaml',
'.spec.ts', '.spec.js', '.test.ts', '.test.js', '.cy.ts', '.cy.js'
],
maxPathDepth: 10,
allowedProtocols: ['http:', 'https:', 'file:'],
rateLimits: {
maxRequestsPerMinute: 100,
maxRequestsPerHour: 1000
},
timeouts: {
defaultTimeoutMs: 30000, // 30 seconds
maxTimeoutMs: 300000 // 5 minutes
}
};
// Input sanitization
export class InputSanitizer {
static HTML_ESCAPE_MAP = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/'
};
static SQL_INJECTION_PATTERNS = [
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE)\b)/gi,
/(--|\/\*|\*\/|;)/g,
/(\bUNION\b.*\bSELECT\b)/gi,
/(\bOR\b.*=.*\bOR\b)/gi,
/('.*OR.*'.*=')/gi
];
static SCRIPT_INJECTION_PATTERNS = [
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
/javascript:/gi,
/vbscript:/gi,
/on\w+\s*=/gi,
/<iframe\b[^>]*>/gi,
/<embed\b[^>]*>/gi,
/<object\b[^>]*>/gi
];
static sanitizeHtml(input) {
return errorHandler.safeExecuteSync(() => {
errorHandler.validator.string(input, 'input');
return input.replace(/[&<>"'\/]/g, (char) => this.HTML_ESCAPE_MAP[char] || char);
});
}
static sanitizePath(input) {
return errorHandler.safeExecuteSync(() => {
errorHandler.validator.string(input, 'input');
// Remove potentially dangerous characters
let sanitized = input
.replace(/\.\./g, '') // Remove directory traversal
.replace(/[<>:"|?*]/g, '') // Remove invalid filename chars
.replace(/[\x00-\x1f\x80-\x9f]/g, '') // Remove control chars
.trim();
// Normalize path separators
sanitized = sanitized.replace(/[\\\/]+/g, '/');
// Remove leading/trailing slashes for relative paths
if (sanitized.startsWith('./') || sanitized.startsWith('../')) {
// Keep relative path indicators
}
else {
sanitized = sanitized.replace(/^\/+/, '');
}
if (sanitized.length === 0) {
throw new ValidationError('Path cannot be empty after sanitization', 'path', input);
}
return sanitized;
});
}
static detectSqlInjection(input) {
return this.SQL_INJECTION_PATTERNS.some(pattern => pattern.test(input));
}
static detectScriptInjection(input) {
return this.SCRIPT_INJECTION_PATTERNS.some(pattern => pattern.test(input));
}
static sanitizeCommand(input) {
return errorHandler.safeExecuteSync(() => {
errorHandler.validator.string(input, 'input');
// Check for potentially dangerous patterns
const dangerousPatterns = [
/[;&|`$(){}[\]]/g, // Shell metacharacters
/\$\([^)]*\)/g, // Command substitution
/`[^`]*`/g, // Backtick command substitution
/(^|\s)(sudo|su|rm|rm\s+-rf|mkfs|dd|:\(\)\{)/gi // Dangerous commands
];
for (const pattern of dangerousPatterns) {
if (pattern.test(input)) {
throw new ValidationError('Command contains potentially dangerous characters or patterns', 'command', input);
}
}
// Basic sanitization
return input
.trim()
.replace(/\s+/g, ' ') // Normalize whitespace
.substring(0, 1000); // Limit length
});
}
static sanitizeEnvironmentValue(input) {
return errorHandler.safeExecuteSync(() => {
errorHandler.validator.string(input, 'input');
// Remove potentially dangerous characters for environment variables
return input
.replace(/[\x00-\x1f\x7f-\x9f]/g, '') // Remove control characters
.replace(/["`$\\]/g, '') // Remove shell special characters
.trim()
.substring(0, 1000); // Limit length
});
}
}
// Security validator
export class SecurityValidator {
static config = DEFAULT_SECURITY_CONFIG;
static setConfig(config) {
this.config = { ...DEFAULT_SECURITY_CONFIG, ...config };
}
static getConfig() {
return { ...this.config };
}
static validateFileSize(size) {
errorHandler.safeExecuteSync(() => {
errorHandler.validator.number(size, 'fileSize');
if (size < 0) {
throw new ValidationError('File size cannot be negative', 'fileSize', size);
}
if (size > this.config.maxFileSize) {
throw new ValidationError(`File size ${size} exceeds maximum allowed size ${this.config.maxFileSize}`, 'fileSize', size);
}
});
}
static validateFileExtension(filename) {
errorHandler.safeExecuteSync(() => {
errorHandler.validator.string(filename, 'filename');
const extension = filename.toLowerCase().split('.').pop();
if (!extension) {
throw new ValidationError('File must have an extension', 'filename', filename);
}
const fullExtension = `.${extension}`;
const isSpecialTest = filename.includes('.spec.') || filename.includes('.test.') || filename.includes('.cy.');
if (isSpecialTest) {
// For test files, check if the base extension is allowed
const parts = filename.toLowerCase().split('.');
if (parts.length >= 2) {
const baseExtension = `.${parts[parts.length - 1]}`;
if (this.config.allowedFileExtensions.includes(baseExtension)) {
return; // Valid test file
}
}
}
if (!this.config.allowedFileExtensions.includes(fullExtension)) {
throw new ValidationError(`File extension ${fullExtension} is not allowed`, 'filename', filename);
}
});
}
static validatePathDepth(filePath) {
errorHandler.safeExecuteSync(() => {
errorHandler.validator.string(filePath, 'filePath');
const sanitizedPath = InputSanitizer.sanitizePath(filePath);
const depth = sanitizedPath.split('/').filter(part => part.length > 0).length;
if (depth > this.config.maxPathDepth) {
throw new ValidationError(`Path depth ${depth} exceeds maximum allowed depth ${this.config.maxPathDepth}`, 'filePath', filePath);
}
});
}
static validateUrl(url) {
errorHandler.safeExecuteSync(() => {
errorHandler.validator.string(url, 'url');
errorHandler.validator.url(url, 'url');
const parsedUrl = new URL(url);
if (!this.config.allowedProtocols.includes(parsedUrl.protocol)) {
throw new ValidationError(`Protocol ${parsedUrl.protocol} is not allowed`, 'url', url);
}
// Check for localhost in production
if (process.env.NODE_ENV === 'production') {
const hostname = parsedUrl.hostname.toLowerCase();
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') {
throw new ValidationError('Localhost URLs are not allowed in production', 'url', url);
}
}
// Check for private IP ranges in production
if (process.env.NODE_ENV === 'production') {
const ip = parsedUrl.hostname;
if (this.isPrivateIP(ip)) {
throw new ValidationError('Private IP addresses are not allowed in production', 'url', url);
}
}
});
}
static validateTimeout(timeoutMs) {
errorHandler.safeExecuteSync(() => {
errorHandler.validator.number(timeoutMs, 'timeout');
if (timeoutMs <= 0) {
throw new ValidationError('Timeout must be positive', 'timeout', timeoutMs);
}
if (timeoutMs > this.config.timeouts.maxTimeoutMs) {
throw new ValidationError(`Timeout ${timeoutMs}ms exceeds maximum allowed timeout ${this.config.timeouts.maxTimeoutMs}ms`, 'timeout', timeoutMs);
}
});
}
static isPrivateIP(ip) {
const privateRanges = [
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./,
/^169\.254\./, // Link-local
/^fc00::/, // IPv6 private
/^fd00::/, // IPv6 private
/^fe80::/ // IPv6 link-local
];
return privateRanges.some(range => range.test(ip));
}
}
// Rate limiter
export class RateLimiter {
static requests = new Map();
static hourlyRequests = new Map();
static isAllowed(identifier) {
return errorHandler.safeExecuteSync(() => {
const now = Date.now();
const config = SecurityValidator.getConfig();
// Check minute limit
const minuteKey = `${identifier}:${Math.floor(now / 60000)}`;
const minuteData = this.requests.get(minuteKey) || { count: 0, resetTime: now + 60000 };
if (minuteData.count >= config.rateLimits.maxRequestsPerMinute) {
return false;
}
// Check hourly limit
const hourKey = `${identifier}:${Math.floor(now / 3600000)}`;
const hourData = this.hourlyRequests.get(hourKey) || { count: 0, resetTime: now + 3600000 };
if (hourData.count >= config.rateLimits.maxRequestsPerHour) {
return false;
}
// Update counts
minuteData.count++;
hourData.count++;
this.requests.set(minuteKey, minuteData);
this.hourlyRequests.set(hourKey, hourData);
// Cleanup old entries
this.cleanup();
return true;
});
}
static cleanup() {
const now = Date.now();
// Clean up minute entries
for (const [key, data] of this.requests.entries()) {
if (data.resetTime < now) {
this.requests.delete(key);
}
}
// Clean up hourly entries
for (const [key, data] of this.hourlyRequests.entries()) {
if (data.resetTime < now) {
this.hourlyRequests.delete(key);
}
}
}
static getRemainingRequests(identifier) {
const now = Date.now();
const config = SecurityValidator.getConfig();
const minuteKey = `${identifier}:${Math.floor(now / 60000)}`;
const hourKey = `${identifier}:${Math.floor(now / 3600000)}`;
const minuteData = this.requests.get(minuteKey) || { count: 0, resetTime: 0 };
const hourData = this.hourlyRequests.get(hourKey) || { count: 0, resetTime: 0 };
return {
minute: Math.max(0, config.rateLimits.maxRequestsPerMinute - minuteData.count),
hour: Math.max(0, config.rateLimits.maxRequestsPerHour - hourData.count)
};
}
}
// Environment variable validator
export class EnvValidator {
static validateRequired(envVars) {
errorHandler.safeExecuteSync(() => {
const missing = [];
for (const envVar of envVars) {
if (!process.env[envVar]) {
missing.push(envVar);
}
}
if (missing.length > 0) {
throw new ValidationError(`Missing required environment variables: ${missing.join(', ')}`, 'environmentVariables', missing);
}
});
}
static sanitizeEnvVar(value) {
if (!value)
return undefined;
return InputSanitizer.sanitizeEnvironmentValue(value);
}
static validateAndSanitize(envVars) {
return errorHandler.safeExecuteSync(() => {
const result = {};
const required = [];
for (const [key, options] of Object.entries(envVars)) {
if (options.required && !process.env[key]) {
required.push(key);
continue;
}
let value = process.env[key];
if (value && options.sanitize) {
value = this.sanitizeEnvVar(value);
}
result[key] = value;
}
if (required.length > 0) {
throw new ValidationError(`Missing required environment variables: ${required.join(', ')}`, 'environmentVariables', required);
}
return result;
});
}
}
// Production security checks
export class ProductionSecurity {
static performSecurityChecks() {
if (process.env.NODE_ENV !== 'production') {
return; // Only run in production
}
errorHandler.safeExecuteSync(() => {
// Check for development artifacts
if (process.env.DEBUG === 'true' || process.env.NODE_ENV === 'development') {
throw new ValidationError('Development settings detected in production environment', 'environment', { DEBUG: process.env.DEBUG, NODE_ENV: process.env.NODE_ENV });
}
// Validate critical environment variables
EnvValidator.validateRequired([
'NODE_ENV'
]);
// Check for insecure configurations
if (process.env.VINEGUARD_ALLOW_INSECURE === 'true') {
throw new ValidationError('Insecure configuration detected in production', 'security', 'VINEGUARD_ALLOW_INSECURE is enabled');
}
});
}
static getSecurityHeaders() {
return {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
'Content-Security-Policy': "default-src 'self'",
'Referrer-Policy': 'strict-origin-when-cross-origin'
};
}
}
//# sourceMappingURL=security.js.map