UNPKG

@sun-asterisk/sunlint

Version:

☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards

238 lines (206 loc) 7.8 kB
/** * AST-based analyzer for S015 - Insecure TLS Certificate Detection * Detects usage of insecure TLS certificate configurations like rejectUnauthorized: false */ const babel = require('@babel/parser'); const traverse = require('@babel/traverse').default; class S015ASTAnalyzer { constructor() { this.ruleId = 'S015'; this.ruleName = 'Insecure TLS Certificate'; this.description = 'Prevent usage of insecure TLS certificate configurations'; } async analyze(files, language, options = {}) { const violations = []; for (const filePath of files) { try { const content = require('fs').readFileSync(filePath, 'utf8'); const fileViolations = await this.analyzeFile(content, filePath, options); violations.push(...fileViolations); } catch (error) { if (options.verbose) { console.warn(`⚠️ S015 AST analysis failed for ${filePath}: ${error.message}`); } } } return violations; } async analyzeFile(content, filePath, options = {}) { const violations = []; try { // Parse with TypeScript/JavaScript support const ast = babel.parse(content, { sourceType: 'module', allowImportExportEverywhere: true, allowReturnOutsideFunction: true, plugins: [ 'typescript', 'jsx', 'objectRestSpread', 'functionBind', 'exportDefaultFrom', 'decorators-legacy', 'classProperties', 'asyncGenerators', 'dynamicImport' ] }); // Traverse AST to find insecure TLS configurations traverse(ast, { Property: (path) => { this.checkTLSProperty(path, violations, filePath); }, ObjectExpression: (path) => { this.checkHTTPSOptions(path, violations, filePath); } }); } catch (parseError) { if (options.verbose) { console.warn(`⚠️ S015 parse failed for ${filePath}: ${parseError.message}`); } // Fall back to regex analysis if AST parsing fails return this.analyzeWithRegex(content, filePath, options); } return violations; } checkTLSProperty(path, violations, filePath) { const node = path.node; // Check for rejectUnauthorized: false if (this.isPropertyKey(node.key, 'rejectUnauthorized')) { if (this.isFalseLiteral(node.value)) { violations.push({ ruleId: this.ruleId, severity: 'error', message: 'Untrusted/self-signed/expired certificate accepted. Only use trusted certificates in production.', line: node.loc ? node.loc.start.line : 1, column: node.loc ? node.loc.start.column + 1 : 1, filePath: filePath, type: 'insecure_tls_config' }); } } // Check for other insecure TLS options const insecureOptions = [ 'checkServerIdentity', 'secureProtocol', 'ciphers', 'secureOptions' ]; if (insecureOptions.some(opt => this.isPropertyKey(node.key, opt))) { if (this.hasInsecureValue(node.value, node.key.name || node.key.value)) { violations.push({ ruleId: this.ruleId, severity: 'warning', message: `Potentially insecure TLS option '${node.key.name || node.key.value}'. Review configuration for security.`, line: node.loc ? node.loc.start.line : 1, column: node.loc ? node.loc.start.column + 1 : 1, filePath: filePath, type: 'potentially_insecure_tls' }); } } } checkHTTPSOptions(path, violations, filePath) { const node = path.node; // Look for HTTPS server configurations const parent = path.parent; if (parent && parent.type === 'CallExpression') { const callee = parent.callee; // Check for https.createServer(options, ...) if (this.isHTTPSCreateServer(callee)) { // Check each property in the options object node.properties.forEach(prop => { if (prop.type === 'ObjectProperty') { if (this.isPropertyKey(prop.key, 'rejectUnauthorized') && this.isFalseLiteral(prop.value)) { violations.push({ ruleId: this.ruleId, severity: 'error', message: 'HTTPS server configured with rejectUnauthorized: false. This disables certificate validation.', line: prop.loc ? prop.loc.start.line : 1, column: prop.loc ? prop.loc.start.column + 1 : 1, filePath: filePath, type: 'https_server_insecure' }); } } }); } } } isPropertyKey(key, expectedName) { return (key.type === 'Identifier' && key.name === expectedName) || (key.type === 'StringLiteral' && key.value === expectedName) || (key.type === 'Literal' && key.value === expectedName); } isFalseLiteral(value) { return (value.type === 'BooleanLiteral' && value.value === false) || (value.type === 'Literal' && value.value === false); } isHTTPSCreateServer(callee) { return (callee.type === 'MemberExpression' && callee.object.name === 'https' && callee.property.name === 'createServer') || (callee.type === 'Identifier' && callee.name === 'createServer'); } hasInsecureValue(value, propertyName) { // Check for potentially insecure values based on property type if (propertyName === 'checkServerIdentity' && this.isFalseLiteral(value)) { return true; } if (propertyName === 'secureProtocol' && value.type === 'StringLiteral') { const insecureProtocols = ['SSLv2', 'SSLv3', 'TLSv1', 'TLSv1_method']; return insecureProtocols.some(protocol => value.value.toLowerCase().includes(protocol.toLowerCase()) ); } if (propertyName === 'ciphers' && value.type === 'StringLiteral') { const weakCiphers = ['NULL', 'RC4', 'DES', 'MD5']; return weakCiphers.some(cipher => value.value.toUpperCase().includes(cipher) ); } return false; } // Fallback regex analysis analyzeWithRegex(content, filePath, options = {}) { const violations = []; const lines = content.split('\n'); lines.forEach((line, index) => { const lineNumber = index + 1; // Check for rejectUnauthorized: false if (/rejectUnauthorized\s*:\s*false/i.test(line)) { violations.push({ ruleId: this.ruleId, severity: 'error', message: 'Untrusted/self-signed/expired certificate accepted. Only use trusted certificates in production.', line: lineNumber, column: line.indexOf('rejectUnauthorized') + 1, filePath: filePath, type: 'insecure_tls_config_regex' }); } // Check for other insecure patterns const insecurePatterns = [ { pattern: /checkServerIdentity\s*:\s*false/i, message: 'Server identity check disabled' }, { pattern: /secureProtocol\s*:\s*['"]SSL/i, message: 'Insecure SSL protocol used' }, { pattern: /secureProtocol\s*:\s*['"]TLSv1['"]/i, message: 'Insecure TLS v1.0 protocol used' } ]; insecurePatterns.forEach(({ pattern, message }) => { if (pattern.test(line)) { violations.push({ ruleId: this.ruleId, severity: 'warning', message: `${message}. Use secure TLS configuration.`, line: lineNumber, column: line.search(pattern) + 1, filePath: filePath, type: 'insecure_tls_pattern_regex' }); } }); }); return violations; } } module.exports = S015ASTAnalyzer;