UNPKG

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