agentsqripts
Version:
Comprehensive static code analysis toolkit for identifying technical debt, security vulnerabilities, performance issues, and code quality problems
271 lines (234 loc) • 7.72 kB
JavaScript
/**
* @file AST-based security analyzer
* @description Advanced security analysis using Abstract Syntax Tree parsing
*/
const acorn = require('acorn');
const walk = require('acorn-walk');
/**
* Tracks data flow through the AST to identify tainted variables
* @param {Object} ast - The parsed AST
* @returns {Set} Set of tainted variable names
*/
function trackTaintedVariables(ast) {
const tainted = new Set();
const assignments = new Map(); // variable -> source
// Common user input sources
const userInputPatterns = [
'req.body', 'req.query', 'req.params', 'req.headers',
'request.body', 'request.query', 'request.params',
'process.argv', 'location.search', 'window.location',
'document.location', 'localStorage', 'sessionStorage'
];
walk.simple(ast, {
// Track variable assignments
VariableDeclarator(node) {
if (node.init) {
const varName = node.id.name;
const source = getExpressionSource(node.init);
// Check if source is user input
if (userInputPatterns.some(pattern => source.includes(pattern))) {
tainted.add(varName);
}
// Track assignment for propagation
assignments.set(varName, source);
}
},
// Track assignment expressions
AssignmentExpression(node) {
if (node.left.type === 'Identifier') {
const varName = node.left.name;
const source = getExpressionSource(node.right);
// Check if source is tainted
if (userInputPatterns.some(pattern => source.includes(pattern))) {
tainted.add(varName);
}
// Propagate taint
if (node.right.type === 'Identifier' && tainted.has(node.right.name)) {
tainted.add(varName);
}
}
}
});
return tainted;
}
/**
* Detects SQL injection vulnerabilities using AST analysis
* @param {Object} ast - The parsed AST
* @param {Set} taintedVars - Set of tainted variable names
* @returns {Array} Array of vulnerability findings
*/
function detectSQLInjection(ast, taintedVars) {
const vulnerabilities = [];
walk.simple(ast, {
// Look for string concatenation with SQL keywords
BinaryExpression(node) {
if (node.operator === '+') {
const left = getExpressionSource(node.left);
const right = getExpressionSource(node.right);
const combined = left + ' + ' + right;
// Check if it's SQL-related
const sqlKeywords = ['SELECT', 'INSERT', 'UPDATE', 'DELETE', 'WHERE', 'FROM'];
const hasSQLKeyword = sqlKeywords.some(kw =>
left.toUpperCase().includes(kw) || right.toUpperCase().includes(kw)
);
if (hasSQLKeyword) {
// Check if any operand is tainted
const isTainted = checkIfExpressionTainted(node, taintedVars);
if (isTainted) {
vulnerabilities.push({
type: 'SQL_INJECTION',
confidence: 'high',
node: node,
message: 'SQL query built with concatenation of user input'
});
}
}
}
},
// Look for template literals with SQL
TemplateLiteral(node) {
const hasSQL = node.quasis.some(q => {
const text = q.value.raw.toUpperCase();
return text.includes('SELECT') || text.includes('INSERT') ||
text.includes('UPDATE') || text.includes('DELETE');
});
if (hasSQL && node.expressions.length > 0) {
// Check if any expression is tainted
const hasTaintedExpression = node.expressions.some(expr =>
checkIfExpressionTainted(expr, taintedVars)
);
if (hasTaintedExpression) {
vulnerabilities.push({
type: 'SQL_INJECTION',
confidence: 'high',
node: node,
message: 'SQL query with interpolated user input'
});
}
}
}
});
return vulnerabilities;
}
/**
* Detects prototype pollution vulnerabilities
* @param {Object} ast - The parsed AST
* @param {Set} taintedVars - Set of tainted variable names
* @returns {Array} Array of vulnerability findings
*/
function detectPrototypePollution(ast, taintedVars) {
const vulnerabilities = [];
walk.simple(ast, {
// Look for dynamic property access with user input
MemberExpression(node) {
if (node.computed && checkIfExpressionTainted(node.property, taintedVars)) {
// Check if it's modifying an object
const parent = node;
if (parent && parent.type === 'AssignmentExpression') {
vulnerabilities.push({
type: 'PROTOTYPE_POLLUTION',
confidence: 'medium',
node: node,
message: 'Dynamic property assignment with user input'
});
}
}
},
// Look for Object.assign with user input
CallExpression(node) {
if (node.callee.type === 'MemberExpression' &&
node.callee.object.name === 'Object' &&
node.callee.property.name === 'assign') {
// Check if any argument is tainted
const hasTaintedArg = node.arguments.some(arg =>
checkIfExpressionTainted(arg, taintedVars)
);
if (hasTaintedArg) {
vulnerabilities.push({
type: 'PROTOTYPE_POLLUTION',
confidence: 'high',
node: node,
message: 'Object.assign with user input can lead to prototype pollution'
});
}
}
}
});
return vulnerabilities;
}
/**
* Helper to get string representation of expression
*/
function getExpressionSource(node) {
if (!node) return '';
switch (node.type) {
case 'Literal':
return String(node.value);
case 'Identifier':
return node.name;
case 'MemberExpression':
const obj = getExpressionSource(node.object);
const prop = node.computed ?
`[${getExpressionSource(node.property)}]` :
`.${node.property.name}`;
return obj + prop;
case 'BinaryExpression':
return getExpressionSource(node.left) + ' ' + node.operator + ' ' + getExpressionSource(node.right);
default:
return node.type;
}
}
/**
* Checks if an expression contains tainted variables
*/
function checkIfExpressionTainted(node, taintedVars) {
if (!node) return false;
let isTainted = false;
walk.simple(node, {
Identifier(n) {
if (taintedVars.has(n.name)) {
isTainted = true;
}
}
});
return isTainted;
}
/**
* Main AST analysis function
* @param {string} code - JavaScript code to analyze
* @param {Object} options - Analysis options
* @returns {Object} Analysis results with vulnerabilities
*/
function analyzeWithAST(code, options = {}) {
try {
// Parse code to AST
const ast = acorn.parse(code, {
ecmaVersion: 2020,
sourceType: 'module',
locations: true,
allowReturnOutsideFunction: true
});
// Track tainted variables
const taintedVars = trackTaintedVariables(ast);
// Run various detectors
const vulnerabilities = [
...detectSQLInjection(ast, taintedVars),
...detectPrototypePollution(ast, taintedVars)
];
// Add location information
return vulnerabilities.map(vuln => ({
...vuln,
line: vuln.node.loc ? vuln.node.loc.start.line : null,
column: vuln.node.loc ? vuln.node.loc.start.column : null
}));
} catch (error) {
// Return empty array if parsing fails
return [];
}
}
module.exports = {
analyzeWithAST,
trackTaintedVariables,
detectSQLInjection,
detectPrototypePollution
};