UNPKG

@sun-asterisk/sunlint

Version:

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

538 lines (475 loc) 14.3 kB
/** * S044 Symbol-Based Analyzer - Re-authentication Required for Sensitive Operations * Uses TypeScript compiler API for semantic analysis */ const ts = require("typescript"); class S044SymbolBasedAnalyzer { constructor(semanticEngine = null) { this.semanticEngine = semanticEngine; this.ruleId = "S044"; this.category = "security"; // Sensitive operations that require re-authentication this.sensitiveOperations = [ "changePassword", "updatePassword", "resetPassword", "changeEmail", "updateEmail", "changeProfile", "updateProfile", "deleteAccount", "deactivateAccount", "changePhoneNumber", "updatePhoneNumber", "changeSecurityQuestion", "updateSecurityQuestion", "changeTwoFactorSettings", "updateTwoFactorSettings", "changeBillingInfo", "updateBillingInfo", "changePaymentMethod", "updatePaymentMethod" ]; // Re-authentication methods/guards this.reAuthMethods = [ "verifyPassword", "confirmPassword", "reAuthenticate", "verifyCurrentPassword", "validatePassword", "checkPassword", "authenticateUser", "verifyIdentity" ]; // Re-authentication middleware/guards this.reAuthMiddleware = [ "requireReAuth", "requireReAuthentication", "verifyReAuth", "checkReAuth", "validateReAuth", "ReAuthGuard", "ReAuthenticationGuard", "VerifyReAuthGuard" ]; // NestJS decorators for re-authentication this.nestjsDecorators = [ "RequireReAuth", "ReAuthenticationRequired", "VerifyReAuth", "UseGuards(ReAuthGuard)", "UseGuards(ReAuthenticationGuard)", "UseGuards(VerifyReAuthGuard)" ]; // Express route patterns that are sensitive this.sensitiveRoutePatterns = [ "/change-password", "/update-password", "/change-email", "/update-email", "/change-profile", "/update-profile", "/delete-account", "/deactivate-account", "/change-phone", "/update-phone", "/change-security-question", "/update-security-question", "/change-two-factor", "/update-two-factor", "/change-billing", "/update-billing", "/change-payment", "/update-payment" ]; // Exclude patterns (login, register, etc.) this.excludePatterns = [ "login", "register", "logout", "forgot-password", "reset-password-request", "signin", "signup", "signout" ]; } /** * Initialize analyzer with semantic engine */ async initialize(semanticEngine) { this.semanticEngine = semanticEngine; if (this.verbose) { console.log(`🔍 [${this.ruleId}] Symbol: Semantic engine initialized`); } } async analyze(filePath) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Starting analysis for ${filePath}` ); } if (!this.semanticEngine) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: No semantic engine available, skipping` ); } return []; } try { const sourceFile = this.semanticEngine.getSourceFile(filePath); if (!sourceFile) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: No source file found, trying ts-morph fallback` ); } return await this.analyzeTsMorph(filePath); } if (this.verbose) { console.log(`🔧 [${this.ruleId}] Source file found, analyzing...`); } return await this.analyzeSourceFile(sourceFile, filePath); } catch (error) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Error in analysis:`, error.message ); } return []; } } async analyzeTsMorph(filePath) { try { if (this.verbose) { console.log(`🔍 [${this.ruleId}] Symbol: Starting ts-morph analysis`); } const { Project } = require("ts-morph"); const project = new Project(); const sourceFile = project.addSourceFileAtPath(filePath); return await this.analyzeSourceFile(sourceFile, filePath); } catch (error) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: ts-morph analysis failed:`, error.message ); } return []; } } async analyzeSourceFile(sourceFile, filePath) { const violations = []; try { if (this.verbose) { console.log(`🔍 [${this.ruleId}] Symbol: Starting symbol-based analysis`); } // Analyze method declarations const methodDeclarations = sourceFile.getDescendantsOfKind ? sourceFile.getDescendantsOfKind( require("typescript").SyntaxKind.MethodDeclaration ) : []; for (const methodNode of methodDeclarations) { try { const violation = this.analyzeMethodDeclaration(methodNode, sourceFile); if (violation) { violations.push(violation); } } catch (error) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Error analyzing method declaration:`, error.message ); } } } // Analyze function declarations const functionDeclarations = sourceFile.getDescendantsOfKind ? sourceFile.getDescendantsOfKind( require("typescript").SyntaxKind.FunctionDeclaration ) : []; for (const functionNode of functionDeclarations) { try { const violation = this.analyzeFunctionDeclaration(functionNode, sourceFile); if (violation) { violations.push(violation); } } catch (error) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Error analyzing function declaration:`, error.message ); } } } // Analyze call expressions (for Express routes) const callExpressions = sourceFile.getDescendantsOfKind ? sourceFile.getDescendantsOfKind( require("typescript").SyntaxKind.CallExpression ) : []; for (const callNode of callExpressions) { try { const violation = this.analyzeCallExpression(callNode, sourceFile); if (violation) { violations.push(violation); } } catch (error) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Error analyzing call expression:`, error.message ); } } } if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Analysis completed. Found ${violations.length} violations` ); } return violations; } catch (error) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Error in source file analysis:`, error.message ); } return []; } } analyzeMethodDeclaration(methodNode, sourceFile) { try { const methodName = methodNode.getName(); if (!this.isSensitiveOperation(methodName)) { return null; } // Only check methods in controller classes (not service classes) const parentClass = methodNode.getParent(); if (!parentClass || !this.isControllerClass(parentClass)) { return null; } if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Sensitive method detected: ${methodName}` ); } // Check if method has re-authentication decorators const hasReAuthDecorator = this.hasReAuthDecorator(methodNode); if (hasReAuthDecorator) { return null; // Method is properly protected } // Check if method calls re-authentication methods const hasReAuthCall = this.hasReAuthMethodCall(methodNode); if (hasReAuthCall) { return null; // Method calls re-authentication } return this.createViolation( sourceFile, methodNode, `Sensitive operation '${methodName}' requires re-authentication before execution` ); } catch (error) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Error analyzing method declaration:`, error.message ); } return null; } } analyzeFunctionDeclaration(functionNode, sourceFile) { try { const functionName = functionNode.getName(); if (!functionName || !this.isSensitiveOperation(functionName)) { return null; } if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Sensitive function detected: ${functionName}` ); } // Check if function calls re-authentication methods const hasReAuthCall = this.hasReAuthMethodCall(functionNode); if (hasReAuthCall) { return null; // Function calls re-authentication } return this.createViolation( sourceFile, functionNode, `Sensitive operation '${functionName}' requires re-authentication before execution` ); } catch (error) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Error analyzing function declaration:`, error.message ); } return null; } } analyzeCallExpression(callNode, sourceFile) { try { const expression = callNode.getExpression(); const methodName = this.getMethodName(expression); // Check if this is an Express route definition if (this.isExpressRouteDefinition(callNode)) { const routePath = this.getRoutePath(callNode); if (this.isSensitiveRoute(routePath)) { const hasReAuthMiddleware = this.hasReAuthMiddleware(callNode); if (!hasReAuthMiddleware) { return this.createViolation( sourceFile, callNode, `Sensitive route '${routePath}' requires re-authentication middleware` ); } } } return null; } catch (error) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Error analyzing call expression:`, error.message ); } return null; } } isSensitiveOperation(methodName) { return this.sensitiveOperations.some(operation => methodName.toLowerCase().includes(operation.toLowerCase()) ); } isSensitiveRoute(routePath) { if (!routePath) return false; return this.sensitiveRoutePatterns.some(pattern => routePath.toLowerCase().includes(pattern.toLowerCase()) ); } isExpressRouteDefinition(callNode) { try { const expression = callNode.getExpression(); const methodName = this.getMethodName(expression); const expressMethods = ['get', 'post', 'put', 'patch', 'delete']; return expressMethods.includes(methodName.toLowerCase()); } catch (error) { return false; } } getRoutePath(callNode) { try { const args = callNode.getArguments(); if (args.length > 0) { const firstArg = args[0]; if (firstArg.getKind() === require("typescript").SyntaxKind.StringLiteral) { return firstArg.getText().replace(/['"]/g, ''); } } return null; } catch (error) { return null; } } hasReAuthDecorator(node) { try { const decorators = node.getDecorators ? node.getDecorators() : []; return decorators.some(decorator => { const decoratorText = decorator.getText(); return this.nestjsDecorators.some(pattern => decoratorText.includes(pattern) ); }); } catch (error) { return false; } } hasReAuthMethodCall(node) { try { const callExpressions = node.getDescendantsOfKind ? node.getDescendantsOfKind(require("typescript").SyntaxKind.CallExpression) : []; return callExpressions.some(callNode => { const methodName = this.getMethodName(callNode.getExpression()); return this.reAuthMethods.includes(methodName); }); } catch (error) { return false; } } hasReAuthMiddleware(callNode) { try { const args = callNode.getArguments(); if (args.length < 2) return false; // Check if any argument is a re-authentication middleware for (let i = 1; i < args.length; i++) { const arg = args[i]; const argText = arg.getText(); if (this.reAuthMiddleware.some(middleware => argText.includes(middleware) )) { return true; } } return false; } catch (error) { return false; } } getMethodName(expression) { try { const ts = require("typescript"); if (expression.getKind() === ts.SyntaxKind.PropertyAccessExpression) { return expression.getNameNode().getText(); } if (expression.getKind() === ts.SyntaxKind.Identifier) { return expression.getText(); } return ""; } catch (error) { return ""; } } isControllerClass(classNode) { try { const className = classNode.getName(); return className && className.toLowerCase().includes('controller'); } catch (error) { return false; } } createViolation(sourceFile, node, message) { try { const start = node.getStart(); const lineAndChar = sourceFile.getLineAndColumnAtPos(start); return { rule: this.ruleId, source: sourceFile.getFilePath(), category: this.category, line: lineAndChar.line, column: lineAndChar.column, message: message, severity: "error", }; } catch (error) { if (this.verbose) { console.log( `🔍 [${this.ruleId}] Symbol: Error creating violation:`, error.message ); } return null; } } } module.exports = S044SymbolBasedAnalyzer;