Skip to main content
Glama
cicd-analyzer.js24.5 kB
/** * CI/CD Configuration Analyzer * Analyzes CI/CD pipeline configurations for best practices, security, and optimization opportunities */ import { promisify } from 'util'; import { exec } from 'child_process'; import { existsSync, readFileSync, readdirSync, statSync } from 'fs'; import { join, dirname, basename, extname } from 'path'; const execAsync = promisify(exec); /** * CI/CD Configuration Analyzer Tool */ export const cicdAnalyzerTool = { name: 'cicd_configuration_analyzer', description: 'Analyzes CI/CD pipeline configurations for best practices, security issues, and optimization opportunities. Supports GitHub Actions, GitLab CI, Jenkins, and other major platforms.', inputSchema: { type: 'object', properties: { projectPath: { type: 'string', description: 'Path to the project directory containing CI/CD configurations', default: '.', }, configPath: { type: 'string', description: 'Specific path to CI/CD configuration file', }, platform: { type: 'string', enum: ['auto', 'github-actions', 'gitlab-ci', 'jenkins', 'azure-devops', 'circleci', 'travis-ci'], default: 'auto', description: 'CI/CD platform to analyze (auto-detect if not specified)', }, checkSecurity: { type: 'boolean', default: true, description: 'Perform security-focused analysis', }, checkPerformance: { type: 'boolean', default: true, description: 'Analyze for performance optimizations', }, checkCompliance: { type: 'boolean', default: true, description: 'Check compliance with best practices', }, strictness: { type: 'string', enum: ['lenient', 'standard', 'strict'], default: 'standard', description: 'Analysis strictness level', }, includeSecrets: { type: 'boolean', default: true, description: 'Analyze secret and environment variable usage', }, limit: { type: 'number', description: 'Maximum number of issues to return (default: 50)', default: 50, minimum: 1, maximum: 500, }, offset: { type: 'number', description: 'Starting index for pagination (default: 0)', default: 0, minimum: 0, }, }, required: [], }, }; /** * Handle CI/CD configuration analysis */ export async function handleCICDAnalyzer(request) { try { const { projectPath = '.', configPath, platform = 'auto', checkSecurity = true, checkPerformance = true, checkCompliance = true, strictness = 'standard', includeSecrets = true, limit = 50, offset = 0, } = request.params; // Discover CI/CD configurations const configs = await discoverCICDConfigs(projectPath, configPath, platform); if (configs.length === 0) { return { content: [{ type: 'text', text: '❌ No CI/CD configuration files found', }], }; } const analysis = { platform: configs[0].platform, configFiles: configs, issues: [], security: { secrets: [], vulnerabilities: [], recommendations: [], }, performance: { optimizations: [], bottlenecks: [], }, compliance: { score: 100, violations: [], bestPractices: [], }, metrics: { totalJobs: 0, totalSteps: 0, estimatedDuration: 0, parallelizationScore: 0, }, }; // Analyze each configuration file for (const config of configs) { await analyzeCICDConfig(config, analysis, { checkSecurity, checkPerformance, checkCompliance, strictness, includeSecrets, }); } // Generate overall recommendations generateCICDRecommendations(analysis); // Calculate scores calculateCICDScores(analysis); // Filter and paginate const filteredIssues = analysis.issues.slice(offset, offset + limit); // Build response const response = { analysis: { platform: analysis.platform, configFiles: configs.length, totalIssues: analysis.issues.length, }, configs: analysis.configFiles, issues: filteredIssues, security: checkSecurity ? analysis.security : null, performance: checkPerformance ? analysis.performance : null, compliance: checkCompliance ? analysis.compliance : null, metrics: analysis.metrics, summary: { critical: analysis.issues.filter(i => i.severity === 'critical').length, high: analysis.issues.filter(i => i.severity === 'high').length, medium: analysis.issues.filter(i => i.severity === 'medium').length, low: analysis.issues.filter(i => i.severity === 'low').length, }, pagination: { offset, limit, total: analysis.issues.length, hasMore: offset + limit < analysis.issues.length, }, }; return { content: [{ type: 'text', text: JSON.stringify(response, null, 2), }], }; } catch (error) { console.error('[CICDAnalyzer] Error:', error); return { content: [{ type: 'text', text: `❌ CI/CD analysis failed: ${error.message}`, }], }; } } /** * Discover CI/CD configuration files */ async function discoverCICDConfigs(projectPath, configPath, platform) { const configs = []; if (configPath && existsSync(configPath)) { // Specific config file provided const content = readFileSync(configPath, 'utf8'); const detectedPlatform = detectPlatform(configPath, content); configs.push({ path: configPath, platform: detectedPlatform, content, size: statSync(configPath).size, }); return configs; } // Auto-discover configurations const cicdPaths = [ // GitHub Actions { pattern: '.github/workflows/*.yml', platform: 'github-actions' }, { pattern: '.github/workflows/*.yaml', platform: 'github-actions' }, // GitLab CI { pattern: '.gitlab-ci.yml', platform: 'gitlab-ci' }, { pattern: '.gitlab-ci.yaml', platform: 'gitlab-ci' }, // Jenkins { pattern: 'Jenkinsfile', platform: 'jenkins' }, { pattern: 'jenkins.yml', platform: 'jenkins' }, // Azure DevOps { pattern: 'azure-pipelines.yml', platform: 'azure-devops' }, { pattern: '.azure/azure-pipelines.yml', platform: 'azure-devops' }, // CircleCI { pattern: '.circleci/config.yml', platform: 'circleci' }, // Travis CI { pattern: '.travis.yml', platform: 'travis-ci' }, // Generic { pattern: 'ci.yml', platform: 'generic' }, { pattern: 'pipeline.yml', platform: 'generic' }, ]; for (const { pattern, platform: configPlatform } of cicdPaths) { if (platform !== 'auto' && platform !== configPlatform) continue; try { if (pattern.includes('*')) { // Handle wildcard patterns const [dirPath, filePattern] = pattern.split('/').slice(-2); const fullDirPath = join(projectPath, pattern.substring(0, pattern.lastIndexOf('/'))); if (existsSync(fullDirPath)) { const files = readdirSync(fullDirPath); const extension = filePattern.split('.').pop(); for (const file of files) { if (file.endsWith(`.${extension}`)) { const filePath = join(fullDirPath, file); const content = readFileSync(filePath, 'utf8'); configs.push({ path: filePath, platform: configPlatform, content, size: statSync(filePath).size, }); } } } } else { // Single file pattern const filePath = join(projectPath, pattern); if (existsSync(filePath)) { const content = readFileSync(filePath, 'utf8'); configs.push({ path: filePath, platform: configPlatform, content, size: statSync(filePath).size, }); } } } catch (error) { // Skip files we can't read continue; } } return configs; } /** * Detect CI/CD platform from file path and content */ function detectPlatform(filePath, content) { if (filePath.includes('.github/workflows')) return 'github-actions'; if (filePath.includes('.gitlab-ci')) return 'gitlab-ci'; if (filePath.includes('Jenkinsfile')) return 'jenkins'; if (filePath.includes('azure-pipelines')) return 'azure-devops'; if (filePath.includes('.circleci')) return 'circleci'; if (filePath.includes('.travis')) return 'travis-ci'; // Content-based detection if (content.includes('runs-on:') || content.includes('uses:')) return 'github-actions'; if (content.includes('script:') && content.includes('stage:')) return 'gitlab-ci'; if (content.includes('pipeline {') || content.includes('node {')) return 'jenkins'; if (content.includes('trigger:') && content.includes('pool:')) return 'azure-devops'; if (content.includes('version: 2') && content.includes('workflows:')) return 'circleci'; return 'generic'; } /** * Analyze CI/CD configuration */ async function analyzeCICDConfig(config, analysis, options) { try { const parsed = parseConfig(config.content, config.platform); // Platform-specific analysis switch (config.platform) { case 'github-actions': await analyzeGitHubActions(parsed, config, analysis, options); break; case 'gitlab-ci': await analyzeGitLabCI(parsed, config, analysis, options); break; case 'jenkins': await analyzeJenkins(parsed, config, analysis, options); break; case 'azure-devops': await analyzeAzureDevOps(parsed, config, analysis, options); break; case 'circleci': await analyzeCircleCI(parsed, config, analysis, options); break; case 'travis-ci': await analyzeTravisCI(parsed, config, analysis, options); break; default: await analyzeGenericCI(parsed, config, analysis, options); } // Common analysis across platforms await analyzeCommonPatterns(config, analysis, options); } catch (error) { analysis.issues.push({ type: 'parse_error', severity: 'high', message: `Failed to parse configuration: ${error.message}`, file: config.path, recommendation: 'Check configuration syntax', }); } } /** * Parse configuration based on format */ function parseConfig(content, platform) { try { // Simple YAML parsing (in production, use proper YAML parser) if (platform === 'jenkins' && content.includes('pipeline {')) { // Jenkinsfile is Groovy, not YAML return { raw: content, type: 'groovy' }; } return parseSimpleYAML(content); } catch (error) { return { raw: content, parseError: error.message }; } } /** * Simple YAML parser for CI/CD configs */ function parseSimpleYAML(content) { const lines = content.split('\n'); const result = {}; const stack = [result]; let currentIndent = 0; for (const line of lines) { if (!line.trim() || line.trim().startsWith('#')) continue; const indent = line.match(/^\s*/)[0].length; const content = line.trim(); if (content.includes(':')) { const [key, value] = content.split(':', 2); const cleanKey = key.trim(); const cleanValue = value?.trim() || ''; // Adjust stack based on indentation while (stack.length > 1 && indent <= currentIndent) { stack.pop(); currentIndent -= 2; } const current = stack[stack.length - 1]; if (cleanValue) { current[cleanKey] = cleanValue; } else { current[cleanKey] = {}; stack.push(current[cleanKey]); currentIndent = indent; } } } return result; } /** * Analyze GitHub Actions */ async function analyzeGitHubActions(parsed, config, analysis, options) { const content = config.content; // Count jobs and steps const jobs = Object.keys(parsed.jobs || {}); analysis.metrics.totalJobs += jobs.length; for (const jobName of jobs) { const job = parsed.jobs[jobName]; const steps = job.steps || []; analysis.metrics.totalSteps += steps.length; // Check for long-running jobs if (steps.length > 20) { analysis.issues.push({ type: 'long_job', severity: 'medium', message: `Job '${jobName}' has many steps (${steps.length})`, file: config.path, job: jobName, recommendation: 'Consider breaking into multiple jobs or using job matrices', }); } } // Security checks if (options.checkSecurity) { // Check for third-party actions without version pinning const actionPattern = /uses:\s*([^@\s]+)(?:@([^\s]+))?/g; let match; while ((match = actionPattern.exec(content)) !== null) { const [, action, version] = match; if (!version || version === 'main' || version === 'master') { analysis.security.vulnerabilities.push({ type: 'unpinned_action', severity: 'medium', message: `Action '${action}' not pinned to specific version`, file: config.path, action, recommendation: 'Pin actions to specific commit SHA or semantic version', }); } } // Check for secrets in workflow files if (options.includeSecrets) { analyzeSecretsUsage(content, config, analysis); } } // Performance checks if (options.checkPerformance) { // Check for missing caching if (!content.includes('actions/cache') && (content.includes('npm install') || content.includes('pip install'))) { analysis.performance.bottlenecks.push({ type: 'missing_cache', severity: 'medium', message: 'Missing dependency caching', file: config.path, recommendation: 'Add actions/cache to cache dependencies', }); } // Check for inefficient checkout if (content.includes('actions/checkout') && !content.includes('fetch-depth')) { analysis.performance.optimizations.push({ type: 'shallow_checkout', priority: 'low', message: 'Consider shallow checkout for faster clone', file: config.path, recommendation: 'Add fetch-depth: 1 for shallow checkout', }); } } } /** * Analyze GitLab CI */ async function analyzeGitLabCI(parsed, config, analysis, options) { const content = config.content; // Count stages and jobs const stages = parsed.stages || []; const jobs = Object.keys(parsed).filter(key => !['stages', 'variables', 'before_script', 'after_script', 'image'].includes(key) ); analysis.metrics.totalJobs += jobs.length; // Check for missing stages if (stages.length === 0) { analysis.issues.push({ type: 'missing_stages', severity: 'medium', message: 'No stages defined in GitLab CI', file: config.path, recommendation: 'Define stages for better pipeline organization', }); } // Security checks if (options.checkSecurity) { // Check for privileged mode if (content.includes('privileged: true')) { analysis.security.vulnerabilities.push({ type: 'privileged_runner', severity: 'high', message: 'Using privileged Docker mode', file: config.path, recommendation: 'Avoid privileged mode unless absolutely necessary', }); } } } /** * Analyze Jenkins pipeline */ async function analyzeJenkins(parsed, config, analysis, options) { const content = config.content; // Basic Jenkinsfile analysis if (content.includes('pipeline {')) { // Declarative pipeline const stageMatches = content.match(/stage\s*\(\s*['"][^'"]+['"]\s*\)/g) || []; analysis.metrics.totalJobs += stageMatches.length; if (stageMatches.length === 0) { analysis.issues.push({ type: 'no_stages', severity: 'high', message: 'No stages defined in Jenkins pipeline', file: config.path, recommendation: 'Define stages for pipeline structure', }); } } // Security checks if (options.checkSecurity) { if (content.includes('sh ') && !content.includes('returnStatus')) { analysis.security.vulnerabilities.push({ type: 'shell_injection_risk', severity: 'medium', message: 'Shell commands without return status checking', file: config.path, recommendation: 'Use returnStatus or returnStdout for safer shell execution', }); } } } /** * Analyze Azure DevOps */ async function analyzeAzureDevOps(parsed, config, analysis, options) { const content = config.content; // Count jobs and steps const jobs = parsed.jobs || []; analysis.metrics.totalJobs += Array.isArray(jobs) ? jobs.length : 1; // Check for missing pool specification if (!parsed.pool && !content.includes('pool:')) { analysis.issues.push({ type: 'missing_pool', severity: 'medium', message: 'No agent pool specified', file: config.path, recommendation: 'Specify agent pool for consistent builds', }); } } /** * Analyze CircleCI */ async function analyzeCircleCI(parsed, config, analysis, options) { const content = config.content; // Check version if (!parsed.version || parsed.version < 2) { analysis.issues.push({ type: 'outdated_version', severity: 'high', message: 'Using outdated CircleCI version', file: config.path, recommendation: 'Upgrade to CircleCI 2.1 for latest features', }); } // Count jobs const jobs = parsed.jobs || {}; analysis.metrics.totalJobs += Object.keys(jobs).length; } /** * Analyze Travis CI */ async function analyzeTravisCI(parsed, config, analysis, options) { const content = config.content; // Check for deprecated features if (content.includes('sudo:')) { analysis.issues.push({ type: 'deprecated_feature', severity: 'medium', message: 'Using deprecated sudo configuration', file: config.path, recommendation: 'Use containerized builds instead', }); } } /** * Analyze generic CI configuration */ async function analyzeGenericCI(parsed, config, analysis, options) { // Basic analysis for unknown CI platforms const content = config.content; if (content.length < 50) { analysis.issues.push({ type: 'minimal_config', severity: 'low', message: 'Very minimal CI configuration', file: config.path, recommendation: 'Consider adding more comprehensive build steps', }); } } /** * Analyze common patterns across all platforms */ async function analyzeCommonPatterns(config, analysis, options) { const content = config.content; // Check for hardcoded values const hardcodedPatterns = [ { pattern: /https?:\/\/[^\s]+/, message: 'Hardcoded URL', recommendation: 'Use environment variables for URLs' }, { pattern: /\/[a-zA-Z0-9\/]+\.(com|org|net)/, message: 'Hardcoded domain', recommendation: 'Use environment variables for domains' }, { pattern: /[0-9]+\.[0-9]+\.[0-9]+/, message: 'Hardcoded version', recommendation: 'Use variables for version numbers' }, ]; for (const { pattern, message, recommendation } of hardcodedPatterns) { if (pattern.test(content)) { analysis.issues.push({ type: 'hardcoded_value', severity: 'low', message, file: config.path, recommendation, }); } } // Check for missing error handling if (!content.includes('try') && !content.includes('catch') && (content.includes('curl') || content.includes('wget'))) { analysis.issues.push({ type: 'missing_error_handling', severity: 'medium', message: 'External commands without error handling', file: config.path, recommendation: 'Add error handling for external commands', }); } // Check for excessive logging const debugLines = (content.match(/echo|print|debug|verbose/gi) || []).length; if (debugLines > 10) { analysis.issues.push({ type: 'excessive_logging', severity: 'low', message: `Many debug/logging statements (${debugLines})`, file: config.path, recommendation: 'Consider reducing debug output for cleaner logs', }); } } /** * Analyze secrets usage */ function analyzeSecretsUsage(content, config, analysis) { // GitHub Actions secrets pattern const secretsPattern = /\$\{\{\s*secrets\.([A-Z_]+)\s*\}\}/g; let match; while ((match = secretsPattern.exec(content)) !== null) { const secretName = match[1]; analysis.security.secrets.push({ name: secretName, file: config.path, type: 'github_secret', }); // Check for common insecure secret names const insecureNames = ['PASSWORD', 'TOKEN', 'KEY', 'SECRET']; if (insecureNames.some(name => secretName.includes(name)) && secretName.length < 10) { analysis.security.vulnerabilities.push({ type: 'generic_secret_name', severity: 'low', message: `Generic secret name: ${secretName}`, file: config.path, recommendation: 'Use more specific secret names', }); } } // Environment variables that might contain secrets const envPattern = /\$\{?([A-Z_]+)\}?/g; const suspiciousEnvVars = []; while ((match = envPattern.exec(content)) !== null) { const varName = match[1]; if (['API_KEY', 'SECRET', 'PASSWORD', 'TOKEN'].some(secret => varName.includes(secret))) { suspiciousEnvVars.push(varName); } } if (suspiciousEnvVars.length > 0) { analysis.security.recommendations.push({ type: 'secret_management', message: `Found ${suspiciousEnvVars.length} potential secrets in environment variables`, recommendation: 'Use secure secret management instead of environment variables', variables: suspiciousEnvVars, }); } } /** * Generate CI/CD recommendations */ function generateCICDRecommendations(analysis) { const recommendations = []; // Based on platform switch (analysis.platform) { case 'github-actions': recommendations.push({ category: 'security', title: 'GitHub Actions Security', items: [ 'Pin action versions to specific commits', 'Use GITHUB_TOKEN instead of personal tokens when possible', 'Minimize repository permissions for GITHUB_TOKEN', ], }); break; case 'gitlab-ci': recommendations.push({ category: 'performance', title: 'GitLab CI Optimization', items: [ 'Use GitLab CI cache for dependencies', 'Leverage parallel jobs with needs keyword', 'Use Docker layer caching for faster builds', ], }); break; } // General recommendations recommendations.push({ category: 'best_practices', title: 'CI/CD Best Practices', items: [ 'Use fail-fast strategies to reduce build times', 'Implement proper error handling and notifications', 'Use matrix builds for testing multiple configurations', 'Keep build logs clean and informative', ], }); analysis.recommendations = recommendations; } /** * Calculate CI/CD scores */ function calculateCICDScores(analysis) { // Compliance score let complianceScore = 100; analysis.issues.forEach(issue => { switch (issue.severity) { case 'critical': complianceScore -= 20; break; case 'high': complianceScore -= 15; break; case 'medium': complianceScore -= 8; break; case 'low': complianceScore -= 3; break; } }); analysis.compliance.score = Math.max(0, complianceScore); // Parallelization score (simple heuristic) if (analysis.metrics.totalJobs > 1) { analysis.metrics.parallelizationScore = Math.min(100, analysis.metrics.totalJobs * 20); } // Estimate duration (very rough) analysis.metrics.estimatedDuration = analysis.metrics.totalSteps * 2; // 2 minutes per step average }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/moikas-code/moidvk'

If you have feedback or need assistance with the MCP directory API, please join our Discord server