sicua
Version:
A tool for analyzing project structure and dependencies
411 lines (410 loc) • 16.7 kB
JavaScript
"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;