UNPKG

sicua

Version:

A tool for analyzing project structure and dependencies

411 lines (410 loc) 16.7 kB
"use strict"; /** * AST traversal utilities for security vulnerability detection */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ASTTraverser = void 0; const typescript_1 = __importDefault(require("typescript")); const general_constants_1 = require("../constants/general.constants"); const security_constants_1 = require("../constants/security.constants"); class ASTTraverser { /** * Find all nodes of a specific kind in a source file */ static findNodesByKind(sourceFile, kind, predicate) { const results = []; function visit(node) { if (node.kind === kind) { const typedNode = node; if (!predicate || predicate(typedNode)) { results.push(typedNode); } } typescript_1.default.forEachChild(node, visit); } visit(sourceFile); return results; } static findNodesByKindInNode(node, kind, predicate) { const results = []; function visit(currentNode) { if (currentNode.kind === kind) { const typedNode = currentNode; if (!predicate || predicate(typedNode)) { results.push(typedNode); } } typescript_1.default.forEachChild(currentNode, visit); } visit(node); return results; } /** * Find the nearest parent node that matches a given predicate */ static findNearestParent(node, predicate) { let current = node.parent; while (current) { if (predicate(current)) { return current; } current = current.parent; } return undefined; } /** * Find nodes matching AST pattern conditions */ static findNodesMatchingPattern(sourceFile, pattern) { const matches = []; const targetKind = this.getNodeKindFromType(pattern.nodeType); if (!targetKind) { return matches; } const self = this; function visit(node) { if (node.kind === targetKind) { if (!pattern.conditions || self.matchesConditions(node, pattern.conditions)) { const location = self.getNodeLocation(node, sourceFile); const nodeText = self.getNodeText(node, sourceFile); matches.push({ match: nodeText, startIndex: node.getStart(sourceFile), endIndex: node.getEnd(), line: location.line, column: location.column, context: self.getNodeContext(node, sourceFile), }); } } typescript_1.default.forEachChild(node, visit); } visit(sourceFile); return matches; } /** * Check if a node matches the specified conditions */ static matchesConditions(node, conditions) { // Check parent type condition if (conditions.parentType && node.parent) { const expectedParentKind = this.getNodeKindFromType(conditions.parentType); if (expectedParentKind && node.parent.kind !== expectedParentKind) { return false; } } // Check child node condition if (conditions.hasChild) { const expectedChildKind = this.getNodeKindFromType(conditions.hasChild); if (expectedChildKind) { let hasExpectedChild = false; typescript_1.default.forEachChild(node, (child) => { if (child.kind === expectedChildKind) { hasExpectedChild = true; } }); if (!hasExpectedChild) { return false; } } } // Check property conditions if (conditions.properties) { for (const [prop, expectedValue] of Object.entries(conditions.properties)) { const actualValue = node[prop]; if (actualValue !== expectedValue) { return false; } } } // Check custom validator if (conditions.customValidator) { return conditions.customValidator(node); } return true; } /** * Get TypeScript SyntaxKind from string type */ static getNodeKindFromType(nodeType) { const kindMap = { CallExpression: typescript_1.default.SyntaxKind.CallExpression, MemberExpression: typescript_1.default.SyntaxKind.PropertyAccessExpression, StringLiteral: typescript_1.default.SyntaxKind.StringLiteral, Identifier: typescript_1.default.SyntaxKind.Identifier, PropertyAccessExpression: typescript_1.default.SyntaxKind.PropertyAccessExpression, VariableDeclaration: typescript_1.default.SyntaxKind.VariableDeclaration, JSXElement: typescript_1.default.SyntaxKind.JsxElement, JSXAttribute: typescript_1.default.SyntaxKind.JsxAttribute, }; return kindMap[nodeType]; } /** * Get the line and column position of a node */ static getNodeLocation(node, sourceFile) { const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)); return { line: line + 1, column: character + 1 }; } /** * Get the text content of a node */ static getNodeText(node, sourceFile) { return node.getText(sourceFile); } /** * Get surrounding context for a node */ static getNodeContext(node, sourceFile, contextLines = 3) { const startPos = node.getStart(sourceFile); const fullText = sourceFile.getFullText(); const startLocation = sourceFile.getLineAndCharacterOfPosition(startPos); const lines = fullText.split("\n"); const startLine = Math.max(0, startLocation.line - contextLines); const endLine = Math.min(lines.length - 1, startLocation.line + contextLines); return lines.slice(startLine, endLine + 1).join("\n"); } /** * Find all call expressions with a specific function name */ static findCallExpressions(sourceFile, functionName) { return this.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.CallExpression, (node) => { if (typescript_1.default.isIdentifier(node.expression)) { return node.expression.text === functionName; } if (typescript_1.default.isPropertyAccessExpression(node.expression)) { return (typescript_1.default.isIdentifier(node.expression.name) && node.expression.name.text === functionName); } return false; }); } /** * Find all string literals containing a specific pattern */ static findStringLiteralsWithPattern(sourceFile, pattern) { return this.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.StringLiteral, (node) => pattern.test(node.text)); } /** * Find all property access expressions (e.g., obj.prop) */ static findPropertyAccess(sourceFile, objectName, propertyName) { return this.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.PropertyAccessExpression, (node) => { let matches = true; if (objectName && typescript_1.default.isIdentifier(node.expression)) { matches = matches && node.expression.text === objectName; } if (propertyName && typescript_1.default.isIdentifier(node.name)) { matches = matches && node.name.text === propertyName; } return matches; }); } /** * Find all JSX elements with specific tag names */ static findJSXElements(sourceFile, tagName) { const results = []; // Find JsxElement nodes const jsxElements = this.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.JsxElement, tagName ? (node) => { const openingElement = node.openingElement; if (typescript_1.default.isIdentifier(openingElement.tagName)) { return openingElement.tagName.text === tagName; } return false; } : undefined); // Find JsxSelfClosingElement nodes const jsxSelfClosing = this.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.JsxSelfClosingElement, tagName ? (node) => { if (typescript_1.default.isIdentifier(node.tagName)) { return node.tagName.text === tagName; } return false; } : undefined); return [...jsxElements, ...jsxSelfClosing]; } /** * Check if a node is inside a specific function */ static isNodeInFunction(node, functionName) { let parent = node.parent; while (parent) { if (typescript_1.default.isFunctionDeclaration(parent) && parent.name?.text === functionName) { return true; } if (typescript_1.default.isVariableDeclaration(parent) && typescript_1.default.isIdentifier(parent.name) && parent.name.text === functionName && parent.initializer && (typescript_1.default.isFunctionExpression(parent.initializer) || typescript_1.default.isArrowFunction(parent.initializer))) { return true; } parent = parent.parent; } return false; } /** * Extract string value from various node types */ static extractStringValue(node) { if (typescript_1.default.isStringLiteral(node)) { return node.text; } if (typescript_1.default.isNoSubstitutionTemplateLiteral(node)) { return node.text; } if (typescript_1.default.isTemplateExpression(node)) { // For template expressions, try to extract static parts let result = node.head.text; for (const span of node.templateSpans) { if (typescript_1.default.isStringLiteral(span.literal) || typescript_1.default.isNoSubstitutionTemplateLiteral(span.literal)) { result += span.getText(); } else { return null; // Can't extract if it has dynamic parts } } return result; } return null; } /** * Check if a node is within a specific type of function context */ static isInFunctionWithPattern(node, patterns) { let current = node.parent; while (current) { let functionName; if (typescript_1.default.isFunctionDeclaration(current) && current.name) { functionName = current.name.text; } else if (typescript_1.default.isMethodDeclaration(current) && typescript_1.default.isIdentifier(current.name)) { functionName = current.name.text; } else if (typescript_1.default.isVariableDeclaration(current) && typescript_1.default.isIdentifier(current.name) && current.initializer && (typescript_1.default.isFunctionExpression(current.initializer) || typescript_1.default.isArrowFunction(current.initializer))) { functionName = current.name.text; } if (functionName) { const lowerName = functionName.toLowerCase(); if (patterns.some((pattern) => pattern.test(lowerName))) { return true; } } current = current.parent; } return false; } /** * Get variable assignment context for better analysis */ static getVariableAssignmentContext(node) { let current = node.parent; while (current) { // Variable declaration if (typescript_1.default.isVariableDeclaration(current) && typescript_1.default.isIdentifier(current.name)) { return { variableName: current.name.text, assignmentType: "declaration", isInFunction: this.findNearestFunctionName(current), }; } // Assignment expression if (typescript_1.default.isBinaryExpression(current) && current.operatorToken.kind === typescript_1.default.SyntaxKind.EqualsToken && typescript_1.default.isIdentifier(current.left)) { return { variableName: current.left.text, assignmentType: "assignment", isInFunction: this.findNearestFunctionName(current), }; } // Property assignment if (typescript_1.default.isPropertyAssignment(current) && typescript_1.default.isIdentifier(current.name)) { return { variableName: current.name.text, assignmentType: "property", isInFunction: this.findNearestFunctionName(current), }; } // Function parameter if (typescript_1.default.isParameter(current) && typescript_1.default.isIdentifier(current.name)) { return { variableName: current.name.text, assignmentType: "parameter", isInFunction: this.findNearestFunctionName(current), }; } current = current.parent; } return { assignmentType: "none" }; } /** * Find the nearest function name for context */ static findNearestFunctionName(node) { let current = node.parent; while (current) { if (typescript_1.default.isFunctionDeclaration(current) && current.name) { return current.name.text; } if (typescript_1.default.isMethodDeclaration(current) && typescript_1.default.isIdentifier(current.name)) { return current.name.text; } if (typescript_1.default.isVariableDeclaration(current) && typescript_1.default.isIdentifier(current.name) && current.initializer && (typescript_1.default.isFunctionExpression(current.initializer) || typescript_1.default.isArrowFunction(current.initializer))) { return current.name.text; } current = current.parent; } return undefined; } /** * Check if a node is in a test or development context */ static isInTestOrDevContext(node, sourceFile) { const filePath = sourceFile.fileName; // Check file path indicators if (general_constants_1.TEST_FILE_INDICATORS.some((indicator) => filePath.includes(indicator))) { return true; } // Check surrounding context const context = this.getNodeContext(node, sourceFile, 5); const lowerContext = context.toLowerCase(); return general_constants_1.DEV_CONTEXT_PATTERNS.some((pattern) => pattern.test(lowerContext)); } /** * Enhanced context extraction with better semantic understanding */ static getSemanticContext(node, sourceFile) { const context = this.getNodeContext(node, sourceFile, 3); const lowerContext = context.toLowerCase(); const variableAssignment = this.getVariableAssignmentContext(node); const functionName = this.findNearestFunctionName(node); return { isUIContext: general_constants_1.UI_PATTERNS.some((pattern) => pattern.test(lowerContext)), isSecurityContext: security_constants_1.SECURITY_PATTERNS.some((pattern) => pattern.test(lowerContext)), isTestContext: this.isInTestOrDevContext(node, sourceFile), functionContext: functionName, variableContext: variableAssignment.variableName, }; } } exports.ASTTraverser = ASTTraverser;