@sun-asterisk/sunlint
Version:
☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards
258 lines (215 loc) • 8.14 kB
JavaScript
/**
* S016 Regex-based Analyzer - Sensitive Data in URL Query Parameters Detection
* Purpose: Fallback pattern matching when symbol analysis fails
*/
class S016RegexBasedAnalyzer {
constructor(semanticEngine = null) {
this.ruleId = 'S016';
this.ruleName = 'Sensitive Data in URL Query Parameters (Regex-Based)';
this.semanticEngine = semanticEngine;
this.verbose = false;
// URL construction patterns (regex)
this.urlConstructionPatterns = [
/new\s+URL\s*\([^)]*\)/gi,
/new\s+URLSearchParams\s*\([^)]*\)/gi,
/window\.location\.href\s*=\s*[^;]+/gi,
/location\.href\s*=\s*[^;]+/gi,
/location\.search\s*[+]?=\s*[^;]+/gi
];
// HTTP client patterns
this.httpClientPatterns = [
/fetch\s*\(\s*[`"'][^`"']*[?][^`"']*[`"']/gi,
/axios\.(get|post|put|delete|patch|request)\s*\([^)]*\)/gi,
/request\.(get|post)\s*\([^)]*\)/gi,
/https?\.(?:get|request)\s*\([^)]*\)/gi
];
// Query string manipulation patterns
this.queryStringPatterns = [
/querystring\.stringify\s*\([^)]*\)/gi,
/qs\.stringify\s*\([^)]*\)/gi,
/URLSearchParams\s*\([^)]*\)/gi,
/\.search\s*[+]?=\s*[^;]+/gi,
/[?&]\w+=[^&\s]+/g
];
// Sensitive data patterns (same as symbol-based)
this.sensitivePatterns = [
// Authentication & Authorization
/\b(?:password|passwd|pwd|pass)\b/gi,
/\b(?:token|jwt|accesstoken|refreshtoken|bearertoken)\b/gi,
/\b(?:secret|secretkey|clientsecret|serversecret)\b/gi,
/\b(?:apikey|api_key|key|privatekey|publickey)\b/gi,
/\b(?:auth|authorization|authenticate)\b/gi,
/\b(?:sessionid|session_id|jsessionid)\b/gi,
/\b(?:csrf|csrftoken|xsrf)\b/gi,
// Financial & Personal
/\b(?:ssn|social|socialsecurity)\b/gi,
/\b(?:creditcard|cardnumber|cardnum|ccnumber)\b/gi,
/\b(?:cvv|cvc|cvd|cid)\b/gi,
/\b(?:pin|pincode)\b/gi,
/\b(?:bankaccount|routing|iban)\b/gi,
// Personal Identifiable Information
/\b(?:email|emailaddress|mail)\b/gi,
/\b(?:phone|phonenumber|mobile|tel)\b/gi,
/\b(?:address|homeaddress|zipcode|postal)\b/gi,
/\b(?:birthdate|birthday|dob)\b/gi,
/\b(?:license|passport|identity)\b/gi
];
// Combined patterns for efficiency
this.allUrlPatterns = [
...this.urlConstructionPatterns,
...this.httpClientPatterns,
...this.queryStringPatterns
];
}
async initialize(semanticEngine = null) {
if (semanticEngine) {
this.semanticEngine = semanticEngine;
}
this.verbose = semanticEngine?.verbose || false;
if (this.verbose) {
console.log(`🔧 [S016 Regex-Based] Analyzer initialized`);
}
}
async analyzeFileBasic(filePath, options = {}) {
const fs = require('fs');
const violations = [];
if (this.verbose) {
console.log(`🔧 [S016 Regex] Starting analysis for: ${filePath}`);
}
try {
const content = fs.readFileSync(filePath, 'utf8');
if (this.verbose) {
console.log(`🔧 [S016 Regex] File content length: ${content.length}`);
}
const lines = content.split('\n');
// Find all URL/query related patterns
const urlMatches = this.findUrlPatterns(content);
if (this.verbose) {
console.log(`🔧 [S016 Regex] Found ${urlMatches.length} URL patterns`);
}
for (const match of urlMatches) {
const matchViolations = this.analyzeUrlMatch(match, lines, filePath);
if (this.verbose && matchViolations.length > 0) {
console.log(`🔧 [S016 Regex] Match violations: ${matchViolations.length}`);
}
violations.push(...matchViolations);
}
if (this.verbose) {
console.log(`🔧 [S016 Regex] Total violations found: ${violations.length}`);
}
return violations;
} catch (error) {
if (this.verbose) {
console.error(`🔧 [S016 Regex] Error analyzing ${filePath}:`, error);
}
return [];
}
}
/**
* Find URL patterns in content using regex
*/
findUrlPatterns(content) {
const matches = [];
for (const pattern of this.allUrlPatterns) {
pattern.lastIndex = 0; // Reset regex
let match;
while ((match = pattern.exec(content)) !== null) {
const fullMatch = match[0];
// Calculate line number
const beforeMatch = content.substring(0, match.index);
const lineNumber = beforeMatch.split('\n').length;
const lineStart = beforeMatch.lastIndexOf('\n') + 1;
const columnNumber = match.index - lineStart + 1;
matches.push({
fullMatch,
lineNumber,
columnNumber,
startIndex: match.index,
pattern: pattern.source
});
}
}
return matches;
}
/**
* Analyze URL match for sensitive data violations
*/
analyzeUrlMatch(match, lines, filePath) {
const violations = [];
const { fullMatch, lineNumber, columnNumber, pattern } = match;
// Check for sensitive data in the matched content
const sensitiveData = this.findSensitiveDataInMatch(fullMatch);
if (sensitiveData.length > 0) {
const patternType = this.identifyPatternType(pattern);
violations.push({
ruleId: this.ruleId,
severity: 'error',
message: 'Sensitive data detected in URL query parameters',
source: this.ruleId,
file: filePath,
line: lineNumber,
column: columnNumber,
description: `[REGEX-FALLBACK] Sensitive patterns detected: ${sensitiveData.join(', ')}. URLs with sensitive data can be exposed in logs, browser history, and network traces.`,
suggestion: 'Move sensitive data to request body (POST/PUT) or use secure headers. For authentication, use proper Authorization header.',
category: 'security',
patternType: patternType,
matchedText: fullMatch.length > 100 ? fullMatch.substring(0, 100) + '...' : fullMatch
});
}
return violations;
}
/**
* Find sensitive data patterns in matched text
*/
findSensitiveDataInMatch(matchText) {
const sensitiveData = [];
for (const pattern of this.sensitivePatterns) {
pattern.lastIndex = 0; // Reset regex
const matches = matchText.match(pattern);
if (matches) {
sensitiveData.push(...matches.map(m => m.toLowerCase()));
}
}
return [...new Set(sensitiveData)]; // Remove duplicates
}
/**
* Identify what type of pattern was matched
*/
identifyPatternType(patternSource) {
if (patternSource.includes('URL') || patternSource.includes('location')) {
return 'url_construction';
} else if (patternSource.includes('fetch') || patternSource.includes('axios') || patternSource.includes('request')) {
return 'http_client';
} else if (patternSource.includes('stringify') || patternSource.includes('search')) {
return 'query_string';
}
return 'unknown';
}
async analyze(files, language, options = {}) {
if (this.verbose) {
console.log(`🔧 [S016 Regex] analyze() called with ${files.length} files, language: ${language}`);
}
const violations = [];
for (const filePath of files) {
try {
if (this.verbose) {
console.log(`🔧 [S016 Regex] Processing file: ${filePath}`);
}
const fileViolations = await this.analyzeFileBasic(filePath, options);
violations.push(...fileViolations);
if (this.verbose) {
console.log(`🔧 [S016 Regex] File ${filePath}: Found ${fileViolations.length} violations`);
}
} catch (error) {
if (this.verbose) {
console.warn(`⚠ [S016 Regex] Analysis failed for ${filePath}:`, error.message);
}
}
}
if (this.verbose) {
console.log(`🔧 [S016 Regex] Total violations found: ${violations.length}`);
}
return violations;
}
}
module.exports = S016RegexBasedAnalyzer;