@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
610 lines (536 loc) • 20.7 kB
JavaScript
/**
* S007 Semantic Analyzer - No Plaintext OTP
* Semantic analysis for detecting plaintext OTP storage/transmission using Symbol Table
*
* Advantages over regex:
* - Context-aware analysis (knows function scopes, variable relationships)
* - Cross-file dependency tracking
* - Type information and data flow analysis
* - Reduced false positives through semantic understanding
*/
const SemanticRuleBase = require('../../../core/semantic-rule-base');
class S007SemanticAnalyzer extends SemanticRuleBase {
constructor(ruleId = 'S007') {
super(ruleId, {
category: 'security',
severity: 'error',
description: 'Detects plaintext OTP storage and transmission using semantic analysis',
crossFileAnalysis: true,
requiresTypeChecker: false,
cacheResults: true
});
// OTP-related patterns for semantic analysis
this.otpPatterns = {
// Variable names that suggest OTP usage
variableNames: [
/^(otp|totp|hotp)$/i,
/^(one_?time|onetime)_?(password|pass|code)$/i,
/^(auth|verification|sms|temp|security|access|login)_?code$/i,
/^(two_?factor|2fa|mfa)_?(token|code)$/i,
/^pin_?code$/i
],
// Function names that handle OTP
functionNames: [
/^(generate|create|store|save|send|validate|verify)_?otp$/i,
/^otp_?(generate|create|store|save|send|validate|verify)$/i,
/^(generate|create|store|save|send|validate|verify)_?(auth|verification|sms|temp|security|access|login)_?code$/i,
/^send_?(sms|email|auth)_?code$/i
],
// Method calls that suggest OTP operations
methodNames: [
'sendOtp', 'storeOtp', 'saveOtp', 'generateOtp', 'verifyOtp',
'sendSmsCode', 'sendAuthCode', 'storeAuthCode', 'saveAuthCode'
]
};
// Dangerous storage/transmission patterns
this.dangerousOperations = {
// Database storage methods
storage: [
'save', 'store', 'insert', 'update', 'create', 'persist',
'collection.insertOne', 'collection.updateOne', 'db.save',
'redis.set', 'redis.hset', 'cache.put', 'cache.set'
],
// Network transmission methods
transmission: [
'send', 'emit', 'post', 'put', 'response.json', 'res.send',
'email.send', 'sms.send', 'notify', 'broadcast'
],
// Browser storage
browserStorage: [
'localStorage.setItem', 'sessionStorage.setItem',
'localStorage.set', 'sessionStorage.set'
],
// Logging (usually unsafe for OTP)
logging: [
'console.log', 'console.debug', 'console.info',
'logger.info', 'logger.debug', 'log.info'
]
};
// Safe operations that encrypt/hash
this.safeOperations = [
'bcrypt.hash', 'crypto.createHash', 'crypto.createHmac',
'hash', 'encrypt', 'cipher', 'secure', 'pbkdf2',
'scrypt', 'argon2'
];
}
/**
* Main file analysis using Symbol Table
*/
async analyzeFile(filePath, options = {}) {
try {
if (options.verbose) {
console.log('🧠 Analyzing file:', filePath);
}
const symbolTable = await this.getSymbolTable(filePath);
if (!symbolTable) {
if (options.verbose) {
console.warn(`⚠️ ${this.ruleId}: No symbol table available for ${filePath}`);
}
return;
}
if (options.verbose) {
console.log(`🧠 ${this.ruleId}: Analyzing ${filePath} with Symbol Table`);
}
// Analyze different aspects using semantic information with error handling
try {
await this.analyzeVariableUsage(symbolTable, filePath);
} catch (error) {
if (options.verbose) {
console.warn(`⚠️ ${this.ruleId}: Variable analysis failed for ${filePath}: ${error.message}`);
}
}
try {
await this.analyzeFunctionCalls(symbolTable, filePath);
} catch (error) {
if (options.verbose) {
console.warn(`⚠️ ${this.ruleId}: Function call analysis failed for ${filePath}: ${error.message}`);
}
}
try {
await this.analyzeMethodChains(symbolTable, filePath);
} catch (error) {
if (options.verbose) {
console.warn(`⚠️ ${this.ruleId}: Method chain analysis failed for ${filePath}: ${error.message}`);
}
}
try {
await this.analyzeDataFlow(symbolTable, filePath);
} catch (error) {
if (options.verbose) {
console.warn(`⚠️ ${this.ruleId}: Data flow analysis failed for ${filePath}: ${error.message}`);
}
}
// Cross-file analysis if enabled
if (this.config.crossFileAnalysis) {
try {
await this.analyzeCrossFileReferences(symbolTable, filePath);
} catch (error) {
if (options.verbose) {
console.warn(`⚠️ ${this.ruleId}: Cross-file analysis failed for ${filePath}: ${error.message}`);
}
}
}
} catch (error) {
console.error(`❌ ${this.ruleId}: Semantic analysis failed for ${filePath}: ${error.message}`);
// Don't throw - let the wrapper handle fallback
}
}
/**
* Analyze variable declarations and assignments for OTP patterns
*/
async analyzeVariableUsage(symbolTable, filePath) {
// Check variable declarations with safe array handling
const variables = symbolTable.variables || [];
const constants = symbolTable.constants || [];
const allVariables = [...variables, ...constants];
for (const variable of allVariables) {
if (this.isOtpVariable(variable.name)) {
// Check if this OTP variable is used in dangerous contexts
const dangerousUsages = await this.findDangerousVariableUsages(
symbolTable,
variable.name,
variable.line
);
for (const usage of dangerousUsages) {
this.addViolation({
filePath,
line: usage.line,
column: usage.column || 1,
message: `OTP variable '${variable.name}' is used in ${usage.context} without encryption`,
type: 'semantic_otp_variable_usage',
severity: this.determineSeverity(usage.context),
symbolContext: {
variableName: variable.name,
variableType: variable.type,
usageContext: usage.context,
operation: usage.operation
},
suggestion: this.generateSecuritySuggestion(usage.context),
codeSnippet: usage.codeSnippet
});
}
}
}
}
/**
* Analyze function calls for OTP-related operations
*/
async analyzeFunctionCalls(symbolTable, filePath) {
const functionCalls = symbolTable.functionCalls || [];
for (const functionCall of functionCalls) {
// Check if function name suggests OTP handling
if (this.isOtpFunction(functionCall.functionName)) {
const context = await this.analyzeFunctionContext(symbolTable, functionCall);
if (context.isDangerous && !context.isSecure) {
this.addViolation({
filePath,
line: functionCall.line,
column: functionCall.column || 1,
message: `Function '${functionCall.functionName}' may handle OTP in plaintext`,
type: 'semantic_otp_function_call',
severity: 'warning',
symbolContext: {
functionName: functionCall.functionName,
arguments: functionCall.arguments,
returnType: functionCall.returnType,
context: context
},
suggestion: 'Ensure OTP values are encrypted before processing'
});
}
}
// Check for dangerous operations with OTP arguments
if (this.isDangerousOperation(functionCall.functionName)) {
const hasOtpArguments = await this.checkOtpArguments(symbolTable, functionCall);
if (hasOtpArguments.length > 0) {
this.addViolation({
filePath,
line: functionCall.line,
column: functionCall.column || 1,
message: `Potential plaintext OTP passed to ${functionCall.functionName}`,
type: 'semantic_plaintext_otp_argument',
severity: 'error',
symbolContext: {
functionName: functionCall.functionName,
otpArguments: hasOtpArguments,
operation: this.categorizeDangerousOperation(functionCall.functionName)
},
suggestion: 'Encrypt or hash OTP values before storage/transmission'
});
}
}
}
}
/**
* Analyze method chains for OTP operations
*/
async analyzeMethodChains(symbolTable, filePath) {
const methodCalls = symbolTable.methodCalls || [];
for (const methodCall of methodCalls) {
// Look for patterns like: otpCode.save() or user.storeOtp()
if (this.isOtpRelatedMethodCall(methodCall)) {
const chainContext = await this.analyzeMethodChain(symbolTable, methodCall);
if (chainContext.isUnsafe) {
this.addViolation({
filePath,
line: methodCall.line,
column: methodCall.column || 1,
message: `Method chain exposes OTP in plaintext: ${chainContext.chain}`,
type: 'semantic_method_chain_otp',
severity: 'error',
symbolContext: {
methodChain: chainContext.chain,
objectName: methodCall.objectName,
methodName: methodCall.methodName,
dataFlow: chainContext.dataFlow
},
suggestion: 'Use secure methods for OTP handling in method chains'
});
}
}
}
}
/**
* Analyze data flow to track OTP values through the code
*/
async analyzeDataFlow(symbolTable, filePath) {
// Find all OTP-related variables and track their usage
const variables = symbolTable.variables || [];
const otpVariables = variables.filter(v => this.isOtpVariable(v.name));
for (const otpVar of otpVariables) {
const dataFlow = await this.traceDataFlow(symbolTable, otpVar.name);
// Check if OTP flows to dangerous sinks
const dangerousSinks = dataFlow.filter(flow =>
this.isDangerousOperation(flow.operation) &&
!this.isSafeOperation(flow.operation)
);
for (const sink of dangerousSinks) {
this.addViolation({
filePath,
line: sink.line,
column: sink.column || 1,
message: `OTP variable '${otpVar.name}' flows to unsafe operation '${sink.operation}'`,
type: 'semantic_data_flow_violation',
severity: 'error',
symbolContext: {
sourceVariable: otpVar.name,
dataFlowPath: dataFlow,
dangerousSink: sink,
flowDistance: sink.distance
},
suggestion: 'Ensure OTP is encrypted before reaching this operation'
});
}
}
}
/**
* Cross-file analysis for OTP security
*/
async analyzeCrossFileReferences(symbolTable, filePath) {
// Find OTP-related exports
const exports = symbolTable.exports || [];
const otpExports = exports.filter(exp =>
this.isOtpFunction(exp.name) || this.isOtpVariable(exp.name)
);
for (const otpExport of otpExports) {
// Find where this OTP symbol is imported and used
const crossFileUsages = await this.findCrossFileUsages(otpExport.name, [filePath]);
for (const usage of crossFileUsages) {
const usageSymbolTable = await this.getSymbolTable(usage.filePath);
if (!usageSymbolTable) continue;
// Check if the imported OTP symbol is used safely
const dangerousUsage = await this.analyzeImportedOtpUsage(
usageSymbolTable,
otpExport.name,
usage
);
if (dangerousUsage) {
this.addViolation({
filePath: usage.filePath,
line: usage.line,
column: usage.column || 1,
message: `Imported OTP symbol '${otpExport.name}' used unsafely`,
type: 'semantic_cross_file_otp_violation',
severity: 'warning',
symbolContext: {
exportedFrom: filePath,
importedSymbol: otpExport.name,
usageContext: dangerousUsage
},
crossFileReferences: [{
sourceFile: filePath,
targetFile: usage.filePath,
symbol: otpExport.name,
usageType: dangerousUsage.type
}],
suggestion: 'Ensure consistent OTP security across file boundaries'
});
}
}
}
}
/**
* Helper methods for semantic analysis
*/
isOtpVariable(variableName) {
return this.otpPatterns.variableNames.some(pattern => pattern.test(variableName));
}
isOtpFunction(functionName) {
return this.otpPatterns.functionNames.some(pattern => pattern.test(functionName)) ||
this.otpPatterns.methodNames.includes(functionName);
}
isDangerousOperation(operationName) {
return Object.values(this.dangerousOperations).flat().some(op =>
operationName.includes(op) || operationName === op
);
}
isSafeOperation(operationName) {
return this.safeOperations.some(op => operationName.includes(op));
}
async findDangerousVariableUsages(symbolTable, variableName, declarationLine) {
const usages = [];
// Check function calls that use this variable with safe array access
const functionCalls = symbolTable.functionCalls || [];
const relatedCalls = functionCalls.filter(call =>
call.arguments && call.arguments.some(arg => arg && arg.includes && arg.includes(variableName))
);
for (const call of relatedCalls) {
if (this.isDangerousOperation(call.functionName)) {
usages.push({
line: call.line,
column: call.column,
context: this.categorizeDangerousOperation(call.functionName),
operation: call.functionName,
codeSnippet: `${call.functionName}(${call.arguments.join(', ')})`
});
}
}
return usages;
}
categorizeDangerousOperation(operationName) {
if (this.dangerousOperations.storage.some(op => operationName.includes(op))) {
return 'storage';
}
if (this.dangerousOperations.transmission.some(op => operationName.includes(op))) {
return 'transmission';
}
if (this.dangerousOperations.browserStorage.some(op => operationName.includes(op))) {
return 'browser_storage';
}
if (this.dangerousOperations.logging.some(op => operationName.includes(op))) {
return 'logging';
}
return 'unknown_dangerous';
}
determineSeverity(context) {
const severityMap = {
'storage': 'error',
'transmission': 'error',
'browser_storage': 'warning',
'logging': 'warning',
'unknown_dangerous': 'info'
};
return severityMap[context] || 'warning';
}
generateSecuritySuggestion(context) {
const suggestions = {
'storage': 'Use bcrypt.hash() or crypto.createHash() before storing OTP',
'transmission': 'Encrypt OTP before transmission or use secure channels',
'browser_storage': 'Avoid storing OTP in browser storage, use secure tokens instead',
'logging': 'Never log actual OTP values, log only metadata like generation time',
'unknown_dangerous': 'Review this operation for OTP security best practices'
};
return suggestions[context] || 'Ensure OTP security best practices';
}
async analyzeFunctionContext(symbolTable, functionCall) {
// Analyze the context around the function call
const context = {
isDangerous: false,
isSecure: false,
securityMeasures: []
};
// Check if function is called within a security context
const nearbyLines = this.getNearbyLines(symbolTable, functionCall.line, 3);
for (const line of nearbyLines) {
if (this.safeOperations.some(op => line.text.includes(op))) {
context.isSecure = true;
context.securityMeasures.push(line.text);
}
if (this.isDangerousOperation(line.text)) {
context.isDangerous = true;
}
}
return context;
}
async checkOtpArguments(symbolTable, functionCall) {
const otpArguments = [];
if (!functionCall.arguments) return otpArguments;
for (const arg of functionCall.arguments) {
// Check if argument looks like OTP variable
if (this.isOtpVariable(arg)) {
otpArguments.push({
value: arg,
type: 'variable',
confidence: 'high'
});
}
// Check if argument is a string that looks like OTP
if (this.looksLikeOtpValue(arg)) {
otpArguments.push({
value: arg,
type: 'literal',
confidence: 'medium'
});
}
}
return otpArguments;
}
looksLikeOtpValue(value) {
// Patterns that suggest OTP values
const otpValuePatterns = [
/^['"`]\d{4,8}['"`]$/, // 4-8 digit codes in quotes
/^['"`][A-Z0-9]{4,8}['"`]$/i, // 4-8 alphanumeric codes
/\$\{.*otp.*\}/i, // Template strings with otp
/\+.*otp/i // String concatenation with otp
];
return otpValuePatterns.some(pattern => pattern.test(value));
}
isOtpRelatedMethodCall(methodCall) {
return this.isOtpVariable(methodCall.objectName) ||
this.isOtpFunction(methodCall.methodName) ||
(methodCall.objectName && methodCall.objectName.toLowerCase().includes('otp'));
}
async analyzeMethodChain(symbolTable, methodCall) {
const chain = `${methodCall.objectName}.${methodCall.methodName}()`;
const isUnsafe = this.isDangerousOperation(methodCall.methodName) &&
!this.isSafeOperation(methodCall.methodName);
return {
chain,
isUnsafe,
dataFlow: await this.traceMethodChainDataFlow(symbolTable, methodCall)
};
}
async traceDataFlow(symbolTable, variableName) {
const dataFlow = [];
// Find all usages of this variable in function calls
const functionCalls = symbolTable.functionCalls || [];
for (const call of functionCalls) {
if (call.arguments && call.arguments.some(arg => arg && arg.includes && arg.includes(variableName))) {
dataFlow.push({
line: call.line,
operation: call.functionName,
type: 'function_call',
distance: Math.abs(call.line - this.getVariableDeclarationLine(symbolTable, variableName))
});
}
}
// Find all usages in method calls
const methodCalls = symbolTable.methodCalls || [];
for (const call of methodCalls) {
if (call.arguments && call.arguments.some(arg => arg && arg.includes && arg.includes(variableName))) {
dataFlow.push({
line: call.line,
operation: `${call.objectName}.${call.methodName}`,
type: 'method_call',
distance: Math.abs(call.line - this.getVariableDeclarationLine(symbolTable, variableName))
});
}
}
return dataFlow.sort((a, b) => a.line - b.line);
}
getVariableDeclarationLine(symbolTable, variableName) {
const variables = symbolTable.variables || [];
const constants = symbolTable.constants || [];
const variable = variables.find(v => v.name === variableName) ||
constants.find(v => v.name === variableName);
return variable ? variable.line : 1;
}
async traceMethodChainDataFlow(symbolTable, methodCall) {
// Simplified data flow analysis for method chains
return {
source: methodCall.objectName,
operation: methodCall.methodName,
line: methodCall.line
};
}
async analyzeImportedOtpUsage(symbolTable, symbolName, usage) {
// Check how the imported OTP symbol is used
const functionCalls = symbolTable.functionCalls || [];
const usages = functionCalls.filter(call =>
call.functionName === symbolName ||
(call.arguments && call.arguments.includes(symbolName))
);
for (const use of usages) {
if (this.isDangerousOperation(use.functionName) && !this.isSafeOperation(use.functionName)) {
return {
type: 'dangerous_usage',
operation: use.functionName,
line: use.line,
context: this.categorizeDangerousOperation(use.functionName)
};
}
}
return null;
}
}
module.exports = S007SemanticAnalyzer;