@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
JavaScript
/**
* 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;