UNPKG

@sun-asterisk/sunlint

Version:

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

315 lines (266 loc) 10.4 kB
/** * C018 Symbol-based Analyzer - Advanced Do not throw generic errors * Purpose: Use AST + Symbol Resolution to analyze log content quality in catch blocks */ const { SyntaxKind } = require('ts-morph'); class C018SymbolBasedAnalyzer { constructor(semanticEngine = null) { this.ruleId = 'C018'; this.ruleName = 'Error Always provide detailed messages and context. (Symbol-Based)'; this.semanticEngine = semanticEngine; this.verbose = false; // Sensitive data patterns to flag (more specific to avoid false positives) this.sensitivePatterns = [ 'password', 'passwd', 'pwd', 'pass', 'token', 'jwt', 'secret', 'privatekey', 'publickey', 'apikey', 'accesskey', 'ssn', 'social', 'creditcard', 'cardnumber', 'cvv', 'pin', 'authorization', 'bearer' ]; // Ensure error messages should explain what happened, why, and in what context this.explanationPatterns = [ 'because', 'due to', 'failed to', 'cannot', 'invalid', 'missing', 'not found', ]; this.guidancePatterns = [ 'please', 'ensure', 'make sure', 'check', 'try', 'use', ]; } async initialize(semanticEngine = null) { if (semanticEngine) { this.semanticEngine = semanticEngine; } this.verbose = semanticEngine?.verbose || false; if (process.env.SUNLINT_DEBUG) { console.log(`🔧 [C018 Symbol-Based] Analyzer initialized, verbose: ${this.verbose}`); } } async analyzeFileBasic(filePath, options = {}) { // This is the main entry point called by the hybrid analyzer return await this.analyzeFileWithSymbols(filePath, options); } async analyzeFileWithSymbols(filePath, options = {}) { const violations = []; // Enable verbose mode if requested const verbose = options.verbose || this.verbose; if (!this.semanticEngine?.project) { if (verbose) { console.warn('[C018 Symbol-Based] No semantic engine available, skipping analysis'); } return violations; } if (verbose) { console.log(`🔍 [C018 Symbol-Based] Starting analysis for ${filePath}`); } try { const sourceFile = this.semanticEngine.project.getSourceFile(filePath); if (!sourceFile) { return violations; } // Find all try-catch statements in the file const tryCatchStatements = sourceFile.getDescendantsOfKind(SyntaxKind.TryStatement); if (verbose) { console.log(`🔍 [C018 Symbol-Based] Found ${tryCatchStatements.length} try-catch statements`); } for (const tryStatement of tryCatchStatements) { const catchClause = tryStatement.getCatchClause(); if (catchClause) { const catchViolations = this.analyzeCatchBlock(catchClause, sourceFile, filePath, verbose); violations.push(...catchViolations); } } if (verbose) { console.log(`🔍 [C018 Symbol-Based] Total violations found: ${violations.length}`); } return violations; } catch (error) { if (verbose) { console.warn(`[C018 Symbol-Based] Analysis failed for ${filePath}:`, error.message); } return violations; } } /** * Analyze catch block for logging context violations */ analyzeCatchBlock(catchClause, sourceFile, filePath, verbose = false) { const violations = []; if (verbose) { console.log(`🔍 [C018 Symbol-Based] Analyzing catch block in ${filePath}`); } // Get catch parameter (e, error, err, etc.) const catchParameter = catchClause.getVariableDeclaration(); const errorVarName = catchParameter?.getName() || 'e'; if (verbose) { console.log(`🔍 [C018 Symbol-Based] Error variable name: ${errorVarName}`); } // Find all log calls within catch block const catchBlock = catchClause.getBlock(); const throwStatements = catchBlock.getDescendantsOfKind(SyntaxKind.ThrowStatement); if (verbose) { console.log(`🔍 [C018 Symbol-Based] Error variable name: ${errorVarName}`); } if (throwStatements.length === 0) { // No logging found - but this is C029's concern, not C018 // We only analyze existing logs for quality return violations; } // Analyze each log call for context quality for (const throwStatement of throwStatements) { if (verbose) { console.log(`🔍 [C018 Symbol-Based] Analyzing throwStatement call: ${throwStatement.getText()}`); } const throwViolations = this.analyzeThrowCall(throwStatement, errorVarName, sourceFile, filePath, verbose); violations.push(...throwViolations); } return violations; } /** * Analyze individual log call for context quality */ analyzeThrowCall(throwStatement, errorVarName, sourceFile, filePath, verbose = false) { const violations = []; const lineNumber = throwStatement.getStartLineNumber(); const columnNumber = throwStatement.getStart() - throwStatement.getStartLinePos(); const exp = throwStatement.getExpression(); if (!exp) { return violations; // No arguments to analyze; } // Case: throw e (identifier) if (exp.getKind() === SyntaxKind.Identifier) { violations.push({ ruleId: this.ruleId, severity: 'error', message: 'Throwing caught error directly without context', source: this.ruleId, file: filePath, line: lineNumber, column: columnNumber, description: `[SYMBOL-BASED] Caught error thrown directly without additional context. Use structured error objects.`, suggestion: 'Use structured error objects with context instead of throwing caught errors directly', category: 'error-handling' }); } const args = []; // Case: throw new Error("...") if (exp.getKind() === SyntaxKind.NewExpression) { const newExp = exp.asKind(SyntaxKind.NewExpression); const arg = newExp.getArguments().map(arg => arg); args.push(...arg); } const analysis = this.analyzeThrowArguments(args, errorVarName, verbose); // Analyze throw structure and content // Check for violations if (!analysis.isStructured) { violations.push({ ruleId: this.ruleId, severity: 'warning', message: 'Error logging should use structured format (object) instead of string concatenation', source: this.ruleId, file: filePath, line: lineNumber, column: columnNumber, description: `[SYMBOL-BASED] Non-structured logging detected. Use object format for better parsing and monitoring.`, suggestion: 'Use logger.error("message", { error: e.message, context: {...} }) instead of string concatenation', category: 'error-handling' }); } if (analysis.hasSensitiveData) { violations.push({ ruleId: this.ruleId, severity: 'error', message: 'Error logging contains potentially sensitive data', source: this.ruleId, file: filePath, line: lineNumber, column: columnNumber, description: `[SYMBOL-BASED] Sensitive patterns detected: ${analysis.sensitivePatterns.join(', ')}. Mask or exclude sensitive data.`, suggestion: 'Mask sensitive data: password.substring(0,2) + "***" or exclude entirely', category: 'security' }); } if (!analysis.hasExplanation) { violations.push({ ruleId: this.ruleId, severity: 'warning', message: 'Error logging should explain what happened', source: this.ruleId, file: filePath, line: lineNumber, column: columnNumber, description: `[SYMBOL-BASED] Error message should explain what happened, why, and in what context.`, suggestion: 'Use structured error objects with context: { message: "Error occurred", context: "Request failed because todo something." } }', category: 'error-handling' }); } if (!analysis.hasGuidance) { violations.push({ ruleId: this.ruleId, severity: 'warning', message: 'Error logging should provide guidance on what to do next', source: this.ruleId, file: filePath, line: lineNumber, column: columnNumber, description: `[SYMBOL-BASED] Error message should provide guidance on what to do next.`, suggestion: 'Use structured error objects with guidance: { message: "Error occurred", guidance: "Please check the input data and try again." }', category: 'error-handling' }); } return violations; } /** * Analyze log arguments for structure, context, and sensitive data */ analyzeThrowArguments(args, errorVarName, verbose = false) { const analysis = { isStructured: false, hasSensitiveData: false, hasExplanation: false, hasGuidance: false, sensitivePatterns: [] }; // Check if any argument is an object (structured logging) for (const arg of args) { if (arg.getKind() === SyntaxKind.ObjectLiteralExpression) { analysis.isStructured = true; analysis.hasExplanation = true; // Assume structured logs have explanations analysis.hasGuidance = true; // Assume structured logs have guidance break; } } // If not structured, check for string concatenation patterns if (!analysis.isStructured) { for (const arg of args) { const argText = arg.getText().toLowerCase(); this.validateForSensitiveDataInText(argText, analysis); this.validateErrorMessage(argText, analysis); } } return analysis; } /** * Check text for sensitive data patterns */ validateForSensitiveDataInText(text, analysis) { for (const pattern of this.sensitivePatterns) { if (text.includes(pattern)) { analysis.hasSensitiveData = true; analysis.sensitivePatterns.push(pattern); } } } validateErrorMessage(text, analysis) { // Rule 1: Explanation for (const patternE of this.explanationPatterns) { if (text.includes(patternE)) { analysis.hasExplanation = true; } } // Rule 2: Guidance for (const patternG of this.guidancePatterns) { if (text.includes(patternG)) { analysis.hasGuidance = true; } } } } module.exports = C018SymbolBasedAnalyzer;