mcp-adr-analysis-server
Version:
MCP server for analyzing Architectural Decision Records and project architecture
1,199 lines • 60.5 kB
JavaScript
/**
* Tree-sitter Integration for Enterprise DevOps Code Analysis
*
* Provides intelligent code analysis for multi-language DevOps stacks including:
* - TypeScript/JavaScript applications
* - Python microservices
* - Java/Quarkus applications
* - Go cloud-native services
* - Rust system components
* - C/C++ native code
* - Ruby scripts and Rails apps
* - Ansible playbooks and roles
* - Kubernetes/Docker configurations
* - CI/CD pipelines
*
* Enterprise Features:
* - Secret detection in code
* - Architectural boundary validation
* - Multi-language dependency analysis
* - Security pattern recognition
*
* Note: Uses tree-sitter 0.21.x for maximum language compatibility.
* See ADR-017 for version strategy details.
*/
import { readFileSync } from 'fs';
import { extname, basename } from 'path';
/**
* Main Tree-sitter analyzer class for enterprise DevOps stacks
*/
export class TreeSitterAnalyzer {
parsers = new Map();
initialized = false;
constructor() {
this.initializeParsers();
}
/**
* Initialize tree-sitter parsers with graceful fallback
*/
async initializeParsers() {
// Skip tree-sitter initialization in test environment
if (process.env['NODE_ENV'] === 'test' || process.env['JEST_WORKER_ID'] !== undefined) {
this.initialized = false;
return;
}
try {
// Initialize core parsers for enterprise stack (tree-sitter 0.21.x compatible)
// See ADR-017 for version strategy details
await this.loadParser('typescript', 'tree-sitter-typescript');
await this.loadParser('javascript', 'tree-sitter-javascript');
await this.loadParser('python', 'tree-sitter-python');
await this.loadParser('java', 'tree-sitter-java');
await this.loadParser('go', 'tree-sitter-go');
await this.loadParser('rust', 'tree-sitter-rust');
await this.loadParser('c', 'tree-sitter-c');
await this.loadParser('cpp', 'tree-sitter-cpp');
await this.loadParser('ruby', 'tree-sitter-ruby');
await this.loadParser('yaml', 'tree-sitter-yaml');
await this.loadParser('json', 'tree-sitter-json');
await this.loadParser('bash', 'tree-sitter-bash');
await this.loadParser('css', 'tree-sitter-css');
this.initialized = true;
}
catch (error) {
console.warn('Tree-sitter initialization failed, falling back to regex analysis:', error);
this.initialized = false;
}
}
async loadParser(language, packageName) {
// Skip loading parsers in test environment
if (process.env['NODE_ENV'] === 'test' || process.env['JEST_WORKER_ID'] !== undefined) {
return;
}
try {
const TreeSitterModule = await import('tree-sitter');
const TreeSitter = TreeSitterModule.default || TreeSitterModule;
let Parser;
if (language === 'typescript') {
// TypeScript parser exports { typescript, tsx } objects
const tsModule = await import('tree-sitter-typescript');
Parser = tsModule.typescript || tsModule.default?.typescript;
}
else {
const module = await import(packageName);
Parser = module.default || module;
}
const parser = new TreeSitter();
parser.setLanguage(Parser);
this.parsers.set(language, parser);
}
catch (error) {
// Silently skip parser loading errors in test environment
if (process.env['NODE_ENV'] !== 'test' && process.env['JEST_WORKER_ID'] === undefined) {
console.warn(`Failed to load ${language} parser:`, error);
}
}
}
/**
* Analyze code file with tree-sitter intelligence
*/
async analyzeFile(filePath, content) {
const fileContent = content || readFileSync(filePath, 'utf-8');
const language = this.detectLanguage(filePath);
if (!this.initialized || !this.parsers.has(language)) {
return this.fallbackAnalysis(filePath, fileContent, language);
}
try {
const parser = this.parsers.get(language);
const tree = parser.parse(fileContent);
return await this.performIntelligentAnalysis(tree, fileContent, language, filePath);
}
catch (error) {
console.warn(`Tree-sitter analysis failed for ${filePath}, falling back to regex:`, error);
return this.fallbackAnalysis(filePath, fileContent, language);
}
}
/**
* Detect programming language from file extension and content
*/
detectLanguage(filePath) {
const ext = extname(filePath).toLowerCase();
const filename = basename(filePath).toLowerCase();
// DevOps file detection
if (filename === 'dockerfile' || filename.startsWith('dockerfile.'))
return 'dockerfile';
if (filename.endsWith('.tf') || filename.endsWith('.tfvars'))
return 'hcl';
if (filename.includes('docker-compose') && (ext === '.yml' || ext === '.yaml'))
return 'yaml';
if (filename.includes('ansible') || filename.includes('playbook'))
return 'yaml';
if (filename.includes('k8s') || filename.includes('kubernetes'))
return 'yaml';
// Standard language detection
switch (ext) {
case '.ts':
case '.tsx':
return 'typescript';
case '.js':
case '.jsx':
case '.mjs':
return 'javascript';
case '.py':
case '.pyi':
return 'python';
case '.java':
return 'java';
case '.go':
return 'go';
case '.rs':
return 'rust';
case '.c':
case '.h':
return 'c';
case '.cpp':
case '.cc':
case '.cxx':
case '.hpp':
case '.hxx':
return 'cpp';
case '.rb':
case '.rake':
case '.gemspec':
return 'ruby';
case '.css':
return 'css';
case '.yml':
case '.yaml':
return 'yaml';
case '.json':
return 'json';
case '.sh':
case '.bash':
return 'bash';
case '.tf':
case '.tfvars':
// HCL support removed in 0.21.x downgrade - see ADR-017
// Falls back to text-based analysis
return 'hcl';
default:
return 'text';
}
}
/**
* Perform intelligent analysis using tree-sitter AST
*/
async performIntelligentAnalysis(tree, content, language, filePath) {
const lines = content.split('\n');
const result = {
language,
hasSecrets: false,
secrets: [],
imports: [],
functions: [],
variables: [],
infraStructure: [],
securityIssues: [],
architecturalViolations: [],
};
// Language-specific analysis
switch (language) {
case 'python':
await this.analyzePython(tree.rootNode, lines, result);
break;
case 'typescript':
case 'javascript':
await this.analyzeJavaScript(tree.rootNode, lines, result);
break;
case 'java':
await this.analyzeJava(tree.rootNode, lines, result);
break;
case 'go':
await this.analyzeGo(tree.rootNode, lines, result);
break;
case 'rust':
await this.analyzeRust(tree.rootNode, lines, result);
break;
case 'c':
case 'cpp':
await this.analyzeCCpp(tree.rootNode, lines, result);
break;
case 'ruby':
await this.analyzeRuby(tree.rootNode, lines, result);
break;
case 'css':
await this.analyzeCSS(tree.rootNode, lines, result);
break;
case 'yaml':
await this.analyzeYAML(tree.rootNode, lines, result, filePath);
break;
case 'hcl':
// HCL parser removed in 0.21.x - use fallback regex analysis
// See ADR-017 for details
return this.fallbackAnalysis(filePath, content, language);
case 'dockerfile':
await this.analyzeDockerfile(tree.rootNode, lines, result);
break;
case 'bash':
await this.analyzeBash(tree.rootNode, lines, result);
break;
case 'json':
await this.analyzeJSON(tree.rootNode, lines, result);
break;
}
// Universal security analysis
await this.analyzeSecrets(tree.rootNode, lines, result);
result.hasSecrets = result.secrets.length > 0;
return result;
}
/**
* Python-specific analysis for microservices
*/
async analyzePython(node, _lines, result) {
// Find imports
const imports = node
.descendantsOfType('import_statement')
.concat(node.descendantsOfType('import_from_statement'));
for (const importNode of imports) {
const importText = importNode.text;
const location = {
line: importNode.startPosition.row + 1,
column: importNode.startPosition.column,
};
// Check for dangerous imports
const isDangerous = this.checkDangerousImport(importText, 'python');
result.imports.push({
module: importText,
type: 'import',
location,
isExternal: !importText.includes('.'),
isDangerous,
...(isDangerous && { reason: 'Potentially dangerous module' }),
});
}
// Find functions
const functions = node.descendantsOfType('function_definition');
for (const funcNode of functions) {
const nameNode = funcNode.childForFieldName('name');
if (nameNode) {
result.functions.push({
name: nameNode.text,
type: 'function',
parameters: this.extractPythonParameters(funcNode),
location: { line: funcNode.startPosition.row + 1, column: funcNode.startPosition.column },
complexity: this.calculateComplexity(funcNode),
securitySensitive: this.isSecuritySensitive(nameNode.text),
});
}
}
// Find variable assignments with potential secrets
const assignments = node.descendantsOfType('assignment');
for (const assignment of assignments) {
const leftNode = assignment.children[0];
const rightNode = assignment.children[2];
if (leftNode && rightNode) {
const varName = leftNode.text;
const varValue = rightNode.text;
result.variables.push({
name: varName,
type: 'var',
value: varValue,
location: {
line: assignment.startPosition.row + 1,
column: assignment.startPosition.column,
},
isSensitive: this.isSensitiveVariable(varName, varValue),
scope: 'module',
});
}
}
}
/**
* JavaScript/TypeScript analysis for Node.js applications
*/
async analyzeJavaScript(node, _lines, result) {
// Find imports and requires
const imports = node
.descendantsOfType('import_statement')
.concat(node.descendantsOfType('call_expression'));
for (const importNode of imports) {
if (importNode.type === 'call_expression') {
const funcName = importNode.children[0]?.text;
if (funcName === 'require' && importNode.children[1]) {
const moduleNode = importNode.children[1].children[1]; // Get string content
if (moduleNode) {
result.imports.push({
module: moduleNode.text.replace(/['"]/g, ''),
type: 'require',
location: {
line: importNode.startPosition.row + 1,
column: importNode.startPosition.column,
},
isExternal: !moduleNode.text.includes('./'),
isDangerous: this.checkDangerousImport(moduleNode.text, 'javascript'),
});
}
}
}
else {
// ES6 import
const moduleNode = importNode.childForFieldName('source');
if (moduleNode) {
result.imports.push({
module: moduleNode.text.replace(/['"]/g, ''),
type: 'import',
location: {
line: importNode.startPosition.row + 1,
column: importNode.startPosition.column,
},
isExternal: !moduleNode.text.includes('./'),
isDangerous: this.checkDangerousImport(moduleNode.text, 'javascript'),
});
}
}
}
// Find functions and methods
const functions = node
.descendantsOfType('function_declaration')
.concat(node.descendantsOfType('method_definition'), node.descendantsOfType('arrow_function'));
for (const funcNode of functions) {
const nameNode = funcNode.childForFieldName('name');
const name = nameNode?.text || 'anonymous';
result.functions.push({
name,
type: funcNode.type === 'method_definition' ? 'method' : 'function',
parameters: this.extractJSParameters(funcNode),
location: { line: funcNode.startPosition.row + 1, column: funcNode.startPosition.column },
complexity: this.calculateComplexity(funcNode),
securitySensitive: this.isSecuritySensitive(name),
});
}
}
/**
* YAML analysis for Ansible, Kubernetes, Docker Compose
*/
async analyzeYAML(node, _lines, result, filePath) {
// Detect YAML type based on content and filename
const content = _lines.join('\n');
const filename = basename(filePath).toLowerCase();
if (this.isAnsibleFile(filename, content)) {
await this.analyzeAnsibleYAML(node, _lines, result);
}
else if (this.isKubernetesFile(filename, content)) {
await this.analyzeKubernetesYAML(node, _lines, result);
}
else if (this.isDockerComposeFile(filename, content)) {
await this.analyzeDockerComposeYAML(node, _lines, result);
}
}
/**
* Terraform/HCL analysis for infrastructure
* Note: HCL parser removed in 0.21.x downgrade - see ADR-017
* Kept for potential future use when tree-sitter-hcl becomes 0.21.x compatible
*/
// @ts-expect-error Intentionally unused - kept for future HCL support
async _analyzeTerraform(node, _lines, result) {
// Find resource blocks
const resources = node.descendantsOfType('block');
for (const resource of resources) {
const labels = resource.children.filter(child => child.type === 'identifier');
if (labels.length >= 2) {
const resourceType = labels[0]?.text || 'unknown';
const resourceName = labels[1]?.text || 'unnamed';
// Determine provider
let provider = 'aws';
if (resourceType.startsWith('google_'))
provider = 'gcp';
else if (resourceType.startsWith('azurerm_'))
provider = 'azure';
else if (resourceType.startsWith('kubernetes_'))
provider = 'kubernetes';
const securityRisks = this.analyzeTerraformSecurity(resource);
result.infraStructure.push({
resourceType,
name: resourceName,
provider,
configuration: {},
securityRisks,
location: { line: resource.startPosition.row + 1, column: resource.startPosition.column },
});
}
}
}
/**
* Dockerfile analysis for container security
*/
async analyzeDockerfile(_node, lines, result) {
// Analyze each instruction
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('USER ')) {
const user = trimmed.substring(5).trim();
if (user === 'root') {
result.securityIssues.push({
type: 'privilege_escalation',
severity: 'high',
message: 'Running as root user in container',
location: { line: lines.indexOf(line) + 1, column: 0 },
suggestion: 'Create and use a non-root user',
});
}
}
}
}
/**
* Bash script analysis
*/
async analyzeBash(node, _lines, result) {
// Find variable assignments and command substitutions
const assignments = node.descendantsOfType('variable_assignment');
for (const assignment of assignments) {
const nameNode = assignment.childForFieldName('name');
const valueNode = assignment.childForFieldName('value');
if (nameNode && valueNode) {
result.variables.push({
name: nameNode.text,
type: 'var',
value: valueNode.text,
location: {
line: assignment.startPosition.row + 1,
column: assignment.startPosition.column,
},
isSensitive: this.isSensitiveVariable(nameNode.text, valueNode.text),
scope: 'script',
});
}
}
}
/**
* Java analysis for enterprise applications (Quarkus, Spring)
*/
async analyzeJava(node, _lines, result) {
// Find imports
const imports = node.descendantsOfType('import_declaration');
for (const importNode of imports) {
const moduleNode = importNode.childForFieldName('name');
if (moduleNode) {
result.imports.push({
module: moduleNode.text,
type: 'import',
location: {
line: importNode.startPosition.row + 1,
column: importNode.startPosition.column,
},
isExternal: true,
isDangerous: this.checkDangerousImport(moduleNode.text, 'java'),
});
}
}
// Find method declarations
const methods = node.descendantsOfType('method_declaration');
for (const methodNode of methods) {
const nameNode = methodNode.childForFieldName('name');
if (nameNode) {
result.functions.push({
name: nameNode.text,
type: 'method',
parameters: this.extractJavaParameters(methodNode),
location: {
line: methodNode.startPosition.row + 1,
column: methodNode.startPosition.column,
},
complexity: this.calculateComplexity(methodNode),
securitySensitive: this.isSecuritySensitive(nameNode.text),
});
}
}
// Find field declarations (potential secrets)
const fields = node.descendantsOfType('field_declaration');
for (const field of fields) {
const declarator = field.descendantsOfType('variable_declarator')[0];
if (declarator) {
const nameNode = declarator.childForFieldName('name');
const valueNode = declarator.childForFieldName('value');
if (nameNode) {
const varValue = valueNode?.text || '';
result.variables.push({
name: nameNode.text,
type: 'var',
...(varValue && { value: varValue }),
location: {
line: field.startPosition.row + 1,
column: field.startPosition.column,
},
isSensitive: this.isSensitiveVariable(nameNode.text, varValue),
scope: 'class',
});
}
}
}
}
extractJavaParameters(methodNode) {
const paramsNode = methodNode.childForFieldName('parameters');
if (!paramsNode)
return [];
return paramsNode.namedChildren
.filter(child => child.type === 'formal_parameter')
.map(child => {
const nameNode = child.childForFieldName('name');
return nameNode?.text || '';
})
.filter(name => name !== '');
}
/**
* Go analysis for cloud-native services
*/
async analyzeGo(node, _lines, result) {
// Find imports
const importDecls = node.descendantsOfType('import_declaration');
for (const importDecl of importDecls) {
const specs = importDecl.descendantsOfType('import_spec');
for (const spec of specs) {
const pathNode = spec.childForFieldName('path');
if (pathNode) {
const module = pathNode.text.replace(/"/g, '');
result.imports.push({
module,
type: 'import',
location: {
line: spec.startPosition.row + 1,
column: spec.startPosition.column,
},
isExternal: !module.startsWith('./') && !module.startsWith('../'),
isDangerous: this.checkDangerousImport(module, 'go'),
});
}
}
}
// Find function declarations
const functions = node.descendantsOfType('function_declaration');
for (const funcNode of functions) {
const nameNode = funcNode.childForFieldName('name');
if (nameNode) {
result.functions.push({
name: nameNode.text,
type: 'function',
parameters: this.extractGoParameters(funcNode),
location: {
line: funcNode.startPosition.row + 1,
column: funcNode.startPosition.column,
},
complexity: this.calculateComplexity(funcNode),
securitySensitive: this.isSecuritySensitive(nameNode.text),
});
}
}
// Find variable declarations
const varDecls = node.descendantsOfType('var_declaration');
for (const varDecl of varDecls) {
const specs = varDecl.descendantsOfType('var_spec');
for (const spec of specs) {
const nameNode = spec.childForFieldName('name');
const valueNode = spec.childForFieldName('value');
if (nameNode) {
const varValue = valueNode?.text || '';
result.variables.push({
name: nameNode.text,
type: 'var',
...(varValue && { value: varValue }),
location: {
line: spec.startPosition.row + 1,
column: spec.startPosition.column,
},
isSensitive: this.isSensitiveVariable(nameNode.text, varValue),
scope: 'package',
});
}
}
}
}
extractGoParameters(funcNode) {
const paramsNode = funcNode.childForFieldName('parameters');
if (!paramsNode)
return [];
return paramsNode.namedChildren
.filter(child => child.type === 'parameter_declaration')
.map(child => {
const nameNode = child.childForFieldName('name');
return nameNode?.text || '';
})
.filter(name => name !== '');
}
/**
* Rust analysis for system components
*/
async analyzeRust(node, _lines, result) {
// Find use declarations (imports)
const useDecls = node.descendantsOfType('use_declaration');
for (const useDecl of useDecls) {
result.imports.push({
module: useDecl.text.replace(/^use\s+/, '').replace(/;$/, ''),
type: 'import',
location: {
line: useDecl.startPosition.row + 1,
column: useDecl.startPosition.column,
},
isExternal: !useDecl.text.includes('crate::'),
isDangerous: this.checkDangerousImport(useDecl.text, 'rust'),
});
}
// Find function declarations
const functions = node.descendantsOfType('function_item');
for (const funcNode of functions) {
const nameNode = funcNode.childForFieldName('name');
if (nameNode) {
result.functions.push({
name: nameNode.text,
type: 'function',
parameters: this.extractRustParameters(funcNode),
location: {
line: funcNode.startPosition.row + 1,
column: funcNode.startPosition.column,
},
complexity: this.calculateComplexity(funcNode),
securitySensitive: this.isSecuritySensitive(nameNode.text) || funcNode.text.includes('unsafe'),
});
}
}
// Check for unsafe blocks
const unsafeBlocks = node.descendantsOfType('unsafe_block');
for (const block of unsafeBlocks) {
result.securityIssues.push({
type: 'dangerous_function',
severity: 'medium',
message: 'Unsafe block detected',
location: {
line: block.startPosition.row + 1,
column: block.startPosition.column,
},
suggestion: 'Review unsafe code for memory safety issues',
});
}
}
extractRustParameters(funcNode) {
const paramsNode = funcNode.childForFieldName('parameters');
if (!paramsNode)
return [];
return paramsNode.namedChildren
.filter(child => child.type === 'parameter')
.map(child => {
const patternNode = child.childForFieldName('pattern');
return patternNode?.text || '';
})
.filter(name => name !== '');
}
/**
* C/C++ analysis for native code
*/
async analyzeCCpp(node, _lines, result) {
// Find includes
const includes = node.descendantsOfType('preproc_include');
for (const includeNode of includes) {
const pathNode = includeNode.descendantsOfType('string_literal')[0] ||
includeNode.descendantsOfType('system_lib_string')[0];
if (pathNode) {
result.imports.push({
module: pathNode.text.replace(/[<>"]/g, ''),
type: 'include',
location: {
line: includeNode.startPosition.row + 1,
column: includeNode.startPosition.column,
},
isExternal: pathNode.type === 'system_lib_string',
isDangerous: this.checkDangerousImport(pathNode.text, 'c'),
});
}
}
// Find function definitions
const functions = node.descendantsOfType('function_definition');
for (const funcNode of functions) {
const declaratorNode = funcNode.childForFieldName('declarator');
if (declaratorNode) {
const nameNode = declaratorNode.childForFieldName('declarator');
const name = nameNode?.text || declaratorNode.text.split('(')[0] || 'unknown';
result.functions.push({
name,
type: 'function',
parameters: [],
location: {
line: funcNode.startPosition.row + 1,
column: funcNode.startPosition.column,
},
complexity: this.calculateComplexity(funcNode),
securitySensitive: this.isSecuritySensitive(name),
});
}
}
// Check for dangerous functions
const dangerousFunctions = ['strcpy', 'strcat', 'sprintf', 'gets', 'scanf'];
const calls = node.descendantsOfType('call_expression');
for (const call of calls) {
const funcName = call.children[0]?.text;
if (funcName && dangerousFunctions.includes(funcName)) {
result.securityIssues.push({
type: 'dangerous_function',
severity: 'high',
message: `Dangerous function ${funcName} detected`,
location: {
line: call.startPosition.row + 1,
column: call.startPosition.column,
},
suggestion: `Use safe alternatives (${funcName}_s or snprintf)`,
});
}
}
}
/**
* Ruby analysis for scripts and Rails apps
*/
async analyzeRuby(node, _lines, result) {
// Find requires
const calls = node.descendantsOfType('call');
for (const call of calls) {
const methodNode = call.childForFieldName('method');
if (methodNode && (methodNode.text === 'require' || methodNode.text === 'require_relative')) {
const argsNode = call.childForFieldName('arguments');
const argNode = argsNode?.namedChildren[0];
if (argNode) {
result.imports.push({
module: argNode.text.replace(/['"]/g, ''),
type: methodNode.text === 'require' ? 'require' : 'require',
location: {
line: call.startPosition.row + 1,
column: call.startPosition.column,
},
isExternal: methodNode.text === 'require',
isDangerous: this.checkDangerousImport(argNode.text, 'ruby'),
});
}
}
}
// Find method definitions
const methods = node.descendantsOfType('method');
for (const methodNode of methods) {
const nameNode = methodNode.childForFieldName('name');
if (nameNode) {
result.functions.push({
name: nameNode.text,
type: 'method',
parameters: this.extractRubyParameters(methodNode),
location: {
line: methodNode.startPosition.row + 1,
column: methodNode.startPosition.column,
},
complexity: this.calculateComplexity(methodNode),
securitySensitive: this.isSecuritySensitive(nameNode.text),
});
}
}
// Find assignments (potential secrets)
const assignments = node.descendantsOfType('assignment');
for (const assignment of assignments) {
const leftNode = assignment.children[0];
const rightNode = assignment.children[2];
if (leftNode && rightNode) {
result.variables.push({
name: leftNode.text,
type: 'var',
value: rightNode.text,
location: {
line: assignment.startPosition.row + 1,
column: assignment.startPosition.column,
},
isSensitive: this.isSensitiveVariable(leftNode.text, rightNode.text),
scope: 'local',
});
}
}
}
extractRubyParameters(methodNode) {
const paramsNode = methodNode.childForFieldName('parameters');
if (!paramsNode)
return [];
return paramsNode.namedChildren
.filter(child => child.type === 'identifier' ||
child.type === 'optional_parameter' ||
child.type === 'keyword_parameter')
.map(child => child.text)
.filter(name => name !== '');
}
/**
* CSS analysis for stylesheet security
*/
async analyzeCSS(node, _lines, result) {
// Find @import rules
const imports = node.descendantsOfType('import_statement');
for (const importNode of imports) {
const urlNode = importNode.descendantsOfType('call_expression')[0];
const stringNode = importNode.descendantsOfType('string_value')[0];
const target = urlNode?.text || stringNode?.text || '';
if (target) {
result.imports.push({
module: target.replace(/['"url()]/g, ''),
type: 'import',
location: {
line: importNode.startPosition.row + 1,
column: importNode.startPosition.column,
},
isExternal: target.includes('http') || target.includes('//'),
isDangerous: target.includes('http:') && !target.includes('https:'),
});
}
}
// Check for potentially dangerous CSS
const declarations = node.descendantsOfType('declaration');
for (const decl of declarations) {
const propNode = decl.childForFieldName('property');
const valueNode = decl.childForFieldName('value');
if (propNode && valueNode) {
const propName = propNode.text;
const propValue = valueNode.text;
// Check for dangerous expressions
if (propValue.includes('expression(') || propValue.includes('javascript:')) {
result.securityIssues.push({
type: 'insecure_config',
severity: 'high',
message: 'Potentially dangerous CSS expression detected',
location: {
line: decl.startPosition.row + 1,
column: decl.startPosition.column,
},
suggestion: 'Remove JavaScript expressions from CSS',
});
}
// Check for external URLs without HTTPS
if (propValue.includes('url(') &&
propValue.includes('http:') &&
!propValue.includes('https:')) {
result.securityIssues.push({
type: 'insecure_config',
severity: 'medium',
message: `CSS property ${propName} references non-HTTPS URL`,
location: {
line: decl.startPosition.row + 1,
column: decl.startPosition.column,
},
suggestion: 'Use HTTPS for external resources',
});
}
}
}
}
/**
* JSON analysis for configuration files
*/
async analyzeJSON(node, _lines, result) {
// Analyze JSON objects for secrets
const pairs = node.descendantsOfType('pair');
for (const pair of pairs) {
const keyNode = pair.children[0];
const valueNode = pair.children[2];
if (keyNode && valueNode && valueNode.type === 'string') {
const key = keyNode.text.replace(/['"]/g, '');
const value = valueNode.text.replace(/['"]/g, '');
if (this.isSensitiveVariable(key, value)) {
result.secrets.push({
type: this.classifySecret(key, value),
value: value,
location: { line: pair.startPosition.row + 1, column: pair.startPosition.column },
confidence: 0.8,
context: `JSON key: ${key}`,
});
}
}
}
}
/**
* Universal secret detection across all languages
*/
async analyzeSecrets(node, _lines, result) {
// Find all string literals and analyze them
const strings = node
.descendantsOfType('string')
.concat(node.descendantsOfType('string_literal'));
for (const stringNode of strings) {
const text = stringNode.text.replace(/['"]/g, '');
const secretType = this.detectSecretPattern(text);
if (secretType) {
result.secrets.push({
type: secretType,
value: text,
location: {
line: stringNode.startPosition.row + 1,
column: stringNode.startPosition.column,
},
confidence: 0.7,
context: 'String literal',
});
}
}
}
/**
* Fallback analysis using regex when tree-sitter fails
*/
fallbackAnalysis(_filePath, content, language) {
const lines = content.split('\n');
const result = {
language,
hasSecrets: false,
secrets: [],
imports: [],
functions: [],
variables: [],
infraStructure: [],
securityIssues: [],
architecturalViolations: [],
};
// Basic regex-based secret detection
const secretPatterns = [
{
pattern: /["']?(?:api[_-]?key|apikey)["']?\s*[:=\s]\s*["']([^"']+)["']/i,
type: 'api_key',
},
{
pattern: /["']?(?:password|pwd|pass)["']?\s*[:=\s]\s*["']([^"']+)["']/i,
type: 'password',
},
{ pattern: /["']?(?:token|auth)["']?\s*[:=\s]\s*["']([^"']+)["']/i, type: 'token' },
{
pattern: /["']?(?:secret|private[_-]?key)["']?\s*[:=\s]\s*["']([^"']+)["']/i,
type: 'private_key',
},
// AWS Access Key - specific pattern (environment variable style)
{
pattern: /AWS_ACCESS_KEY_ID\s*[:=]\s*["']?((?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA)[A-Z0-9]{16})["']?/i,
type: 'api_key',
},
// AWS Access Key - generic pattern (any variable name with AWS key format)
{
pattern: /["']?((?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA)[A-Z0-9]{16})["']?/,
type: 'api_key',
},
// AWS Secret Key - specific pattern (40 chars)
{
pattern: /AWS_SECRET_ACCESS_KEY\s*[:=]\s*["']?([A-Za-z0-9+/]{40})["']?/i,
type: 'private_key',
},
// Generic AWS secret assignment
{
pattern: /(?:aws_secret|secret_key)\s*[:=]\s*["']([A-Za-z0-9+/]{40})["']/i,
type: 'private_key',
},
// YAML/Dockerfile environment variables with hardcoded values
{
pattern: /(?:value|ENV\s+\w+)\s*[:=]?\s*["']([a-zA-Z0-9_-]{10,})["']/i,
type: 'credential',
},
];
lines.forEach((line, index) => {
secretPatterns.forEach(({ pattern, type }) => {
const match = line.match(pattern);
if (match && match[1] && match[1].length > 8) {
result.secrets.push({
type,
value: match[1],
location: { line: index + 1, column: match.index || 0 },
confidence: 0.6,
context: line.trim(),
});
}
});
// Detect privilege escalation in Dockerfile
if (language === 'dockerfile' && /USER\s+root/i.test(line)) {
result.securityIssues.push({
type: 'privilege_escalation',
severity: 'high',
location: { line: index + 1, column: 0 },
message: 'Running as root user',
suggestion: 'Use a non-root user',
});
}
});
result.hasSecrets = result.secrets.length > 0;
return result;
}
// Helper methods
checkDangerousImport(importText, language) {
const dangerousModules = {
python: ['eval', 'exec', 'subprocess', 'os.system', 'pickle', 'marshal'],
javascript: ['eval', 'child_process', 'vm', 'unsafe-eval'],
java: [
'Runtime.exec',
'ProcessBuilder',
'ScriptEngine',
'Reflection',
'javax.script',
'java.lang.invoke',
],
go: ['os/exec', 'unsafe', 'syscall', 'reflect'],
rust: ['std::process::Command', 'libc', 'std::ffi', 'std::mem::transmute'],
c: ['system', 'exec', 'popen', 'dlopen'],
ruby: ['eval', 'system', 'exec', 'backtick', 'Open3', 'Kernel.system'],
};
const dangerous = dangerousModules[language] || [];
return dangerous.some(module => importText.includes(module));
}
extractPythonParameters(funcNode) {
const paramsNode = funcNode.childForFieldName('parameters');
if (!paramsNode)
return [];
return paramsNode.namedChildren
.filter(child => child.type === 'identifier')
.map(child => child.text);
}
extractJSParameters(funcNode) {
const paramsNode = funcNode.childForFieldName('parameters');
if (!paramsNode)
return [];
return paramsNode.namedChildren
.filter(child => child.type === 'identifier')
.map(child => child.text);
}
calculateComplexity(node) {
// Simple cyclomatic complexity calculation
const complexityNodes = ['if_statement', 'while_statement', 'for_statement', 'try_statement'];
let complexity = 1;
const walk = (n) => {
if (complexityNodes.includes(n.type)) {
complexity++;
}
n.children.forEach(child => walk(child));
};
walk(node);
return complexity;
}
isSecuritySensitive(name) {
const sensitiveNames = [
'auth',
'login',
'password',
'token',
'key',
'secret',
'decrypt',
'hash',
];
return sensitiveNames.some(sensitive => name.toLowerCase().includes(sensitive));
}
isSensitiveVariable(name, value) {
const sensitiveKeys = ['password', 'secret', 'key', 'token', 'auth', 'private', 'credential'];
const nameCheck = sensitiveKeys.some(key => name.toLowerCase().includes(key));
const valueCheck = Boolean(value && value.length > 16 && /^[A-Za-z0-9+/=]+$/.test(value));
return nameCheck || valueCheck;
}
classifySecret(key, _value) {
if (key.toLowerCase().includes('password'))
return 'password';
if (key.toLowerCase().includes('token'))
return 'token';
if (key.toLowerCase().includes('key'))
return 'api_key';
if (key.toLowerCase().includes('private'))
return 'private_key';
return 'credential';
}
detectSecretPattern(text) {
// AWS Access Key (various types)
if (/^(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}$/.test(text)) {
return 'api_key';
}
// AWS Secret Key (40 characters, base64-like) - using boundary detection
if (/^[A-Za-z0-9+/]{40}$/.test(text)) {
return 'private_key';
}
// AWS Session Token (longer, base64-like)
if (/^[A-Za-z0-9+/=]{100,}$/.test(text)) {
return 'token';
}
// JWT Token
if (/^eyJ[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_.+/]*$/.test(text)) {
return 'token';
}
// GitHub Personal Access Token
if (/^ghp_[A-Za-z0-9]{36}$/.test(text)) {
return 'token';
}
// Slack tokens
if (/^xox[baprs]-[0-9]{12}-[0-9]{12}-[a-zA-Z0-9]{24}$/.test(text)) {
return 'token';
}
// Generic API key patterns (32+ alphanumeric)
if (/^[A-Za-z0-9]{32,64}$/.test(text) && text.length >= 32) {
return 'api_key';
}
return null;
}
isAnsibleFile(filename, content) {
return (filename.includes('playbook') ||
filename.includes('ansible') ||
content.includes('hosts:') ||
content.includes('tasks:') ||
content.includes('roles:'));
}
isKubernetesFile(filename, content) {
return (filename.includes('k8s') ||
filename.includes('kubernetes') ||
content.includes('apiVersion:') ||
content.includes('kind:'));
}
isDockerComposeFile(filename, content) {
return (filename.includes('docker-compose') ||
(content.includes('version:') && content.includes('services:')));
}
async analyzeAnsibleYAML(_node, lines, result) {
const content = lines.join('\n');
// Detect Ansible tasks and roles
if (content.includes('tasks:')) {
const taskMatches = content.match(/- name:\s*(.+)/g) || [];
taskMatches.forEach(match => {
const taskName = match.replace(/- name:\s*/, '').trim();
result.functions.push({
name: taskName,
type: 'task',
parameters: [],
location: { line: this.findLineNumber(lines, match), column: 0 },
complexity: 1,
securitySensitive: this.isSecuritySensitiveTask(taskName),
});
});
}
// Detect role imports
const roleMatches = content.match(/roles?:\s*\n([\s\S]*?)(?=\n\w|$)/g) || [];
roleMatches.forEach(roleBlock => {
const roles = roleBlock.match(/- ([\w.-]+)/g) || [];
roles.forEach(role => {
const roleName = role.replace(/- /, '');
result.imports.push({
module: roleName,
type: 'role',
location: { line: this.findLineNumber(lines, role), column: 0 },
isExternal: !roleName.startsWith('./'),
isDangerous: this.isDangerousAnsibleRole(roleName),
});
});
});
// Check for security-sensitive variables
const varMatches = content.match(/\b\w*(?:password|secret|key|token)\w*:\s*["']?([^"'\n]+)["']?/gi) || [];
varMatches.forEach(varMatch => {
const [fullMatch, value] = varMatch.match(/([\w_]+):\s*["']?([^"'\n]+)["']?/) || [];
if (fullMatch && value) {
result.secrets.push({
type: 'credential',
value: value,
location: { line: this.findLineNumber(lines, fullMatch), column: 0 },
confidence: 0.7,
context: 'Ansible variable',
});
}
});
}
async analyzeKubernetesYAML(_node, lines, result) {
const content = lines.join('\n');
// Extract Kubernetes resources
const resourceMatches = content.match(/kind:\s*(\w+)/g) || [];
const apiVersionMatches = content.match(/apiVersion:\s*([\w/]+)/g) || [];
const nameMatches = content.match(/name:\s*([\w.-]+)/g) || [];
if (resourceMatches.length > 0) {
resourceMatches.forEach((kindMatch, index) => {
const kind = kindMatch.replace(/kind:\s*/, '');
const apiVersion = apiVersionMatches[index]