UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

396 lines (334 loc) • 12 kB
#!/usr/bin/env node /** * AI-Enhanced Security Analyzer for ctrl.shift.left * * Uses OpenAI API to provide sophisticated security analysis beyond pattern matching. * Generates nuanced security scores and detailed remediation suggestions with code examples. */ const fs = require('fs'); const path = require('path'); const { OpenAI } = require('openai'); const { promisify } = require('util'); // Configuration let OPENAI_API_KEY = process.env.OPENAI_API_KEY; const MODEL = process.env.OPENAI_MODEL || 'gpt-4'; const MAX_TOKENS = 8000; // For context window management /** * Initialize the OpenAI client * @returns {OpenAI} OpenAI client instance */ function initializeOpenAI() { if (!OPENAI_API_KEY) { console.warn('āš ļø OPENAI_API_KEY environment variable not set. AI-enhanced analysis disabled.'); return null; } try { return new OpenAI({ apiKey: OPENAI_API_KEY }); } catch (error) { console.error(`āŒ Error initializing OpenAI client: ${error.message}`); return null; } } /** * Set the API key programmatically (useful for testing or CLI arguments) * @param {string} apiKey - OpenAI API key */ function setApiKey(apiKey) { OPENAI_API_KEY = apiKey; } /** * Extracts file extension from a file path * @param {string} filePath - Path to the file * @returns {string} File extension */ function getFileExtension(filePath) { return path.extname(filePath).substring(1); } /** * Maps file extensions to programming languages for the AI context * @param {string} extension - File extension * @returns {string} Programming language */ function mapExtensionToLanguage(extension) { const extensionMap = { 'js': 'JavaScript', 'jsx': 'JavaScript (React)', 'ts': 'TypeScript', 'tsx': 'TypeScript (React)', 'py': 'Python', 'java': 'Java', 'rb': 'Ruby', 'php': 'PHP', 'go': 'Go', 'c': 'C', 'cpp': 'C++', 'cs': 'C#', 'html': 'HTML', 'css': 'CSS', 'scss': 'SCSS', 'md': 'Markdown', 'json': 'JSON', 'yml': 'YAML', 'yaml': 'YAML', 'sql': 'SQL' }; return extensionMap[extension] || 'Unknown'; } /** * Creates the prompt for AI security analysis * @param {string} code - The source code * @param {string} language - The programming language * @param {string} filePath - Path to the file being analyzed * @returns {string} The prompt for AI analysis */ function createSecurityAnalysisPrompt(code, language, filePath) { return ` You are a cybersecurity expert with deep knowledge of secure coding practices. Analyze the following ${language} code for security vulnerabilities, quality issues, and best practices: FILE: ${path.basename(filePath)} \`\`\`${language.toLowerCase()} ${code} \`\`\` Conduct a comprehensive security and code quality analysis, focusing on: 1. Security vulnerabilities (XSS, CSRF, SQL injection, etc.) 2. Security best practices violations 3. Input validation issues 4. Authentication and authorization weaknesses 5. Data protection concerns 6. Secrets management problems 7. Insecure dependencies or configurations For each issue found: - Assign a severity level (Critical, High, Medium, Low, Info) - Provide line numbers where the issue occurs - Explain clearly why it's a vulnerability - Provide a specific, ready-to-use code example showing how to fix it Then, calculate an overall security score (0-100) based on the number and severity of issues found. Format your response as JSON with the following structure: { "issues": [ { "title": "Issue title", "severity": "Critical|High|Medium|Low|Info", "line_numbers": [12, 15], "description": "Detailed explanation of the issue", "remediation": "How to fix this issue", "code_example": "Code example showing the fix" } ], "security_score": 85, "summary": "Brief summary of findings" } `; } /** * Analyzes a file for security vulnerabilities using AI * @param {string} filePath - Path to the file to analyze * @returns {Promise<Object>} Analysis results */ async function analyzeFileWithAI(filePath) { const openai = initializeOpenAI(); if (!openai) { return { error: "OpenAI client not initialized. Set the OPENAI_API_KEY environment variable.", fallback: true }; } if (!fs.existsSync(filePath)) { return { error: `File not found: ${filePath}`, fallback: true }; } try { const fileContent = fs.readFileSync(filePath, 'utf8'); const extension = getFileExtension(filePath); const language = mapExtensionToLanguage(extension); // Truncate if too large for context window const truncatedContent = fileContent.length > MAX_TOKENS ? fileContent.substring(0, MAX_TOKENS) + '\n// ... [content truncated for length]' : fileContent; const prompt = createSecurityAnalysisPrompt(truncatedContent, language, filePath); console.log(`šŸ” Analyzing ${path.basename(filePath)} with AI-enhanced security analysis...`); const response = await openai.chat.completions.create({ model: MODEL, messages: [ { role: "system", content: "You are an expert security analyst specialized in detecting vulnerabilities in code." }, { role: "user", content: prompt } ], temperature: 0.1, // Low temperature for more deterministic results max_tokens: 4000 // Reasonable limit for detailed analysis }); const analysisText = response.choices[0].message.content.trim(); // Extract the JSON from the response let analysis; try { // Find JSON in the response if it's wrapped in markdown or other text const jsonMatch = analysisText.match(/```json\n([\s\S]*?)\n```/) || analysisText.match(/```\n([\s\S]*?)\n```/) || analysisText.match(/{[\s\S]*?}/); const jsonStr = jsonMatch ? jsonMatch[0] : analysisText; analysis = JSON.parse(jsonStr.replace(/```json|```/g, '').trim()); } catch (parseError) { console.error(`āŒ Error parsing AI response: ${parseError.message}`); console.log('Raw response:', analysisText); return { error: "Could not parse AI response. See console for details.", raw_response: analysisText, fallback: true }; } return { ...analysis, file_path: filePath, language: language, timestamp: new Date().toISOString() }; } catch (error) { console.error(`āŒ Error during AI analysis: ${error.message}`); return { error: `AI analysis failed: ${error.message}`, fallback: true }; } } /** * Generate a markdown report from AI analysis results * @param {Object} analysis - The analysis results * @returns {string} Markdown report */ function generateMarkdownReport(analysis) { if (analysis.error) { return `# Security Analysis Error\n\nāš ļø ${analysis.error}\n\n${analysis.fallback ? '_Falling back to pattern-based analysis_' : ''}`; } const fileName = path.basename(analysis.file_path); let markdown = `# AI-Enhanced Security Analysis for ${fileName}\n\n`; markdown += `*Generated by Ctrl+Shift+Left on ${new Date().toLocaleString()}*\n\n`; // Security score section with color-coded emoji let scoreEmoji = 'šŸ”“'; if (analysis.security_score >= 90) scoreEmoji = '🟢'; else if (analysis.security_score >= 70) scoreEmoji = '🟢'; else if (analysis.security_score >= 50) scoreEmoji = '🟔'; else if (analysis.security_score >= 30) scoreEmoji = '🟠'; markdown += `## Security Score: ${scoreEmoji} ${analysis.security_score}/100\n\n`; // Summary section if (analysis.summary) { markdown += `## Summary\n\n${analysis.summary}\n\n`; } // Issues by severity if (analysis.issues && analysis.issues.length > 0) { // Group by severity const issuesBySeverity = { 'Critical': [], 'High': [], 'Medium': [], 'Low': [], 'Info': [] }; analysis.issues.forEach(issue => { if (issuesBySeverity[issue.severity]) { issuesBySeverity[issue.severity].push(issue); } else { issuesBySeverity['Info'].push(issue); } }); // Issues summary count markdown += `## Issues Found\n\n`; markdown += `- 🚨 Critical: ${issuesBySeverity.Critical.length}\n`; markdown += `- āš ļø High: ${issuesBySeverity.High.length}\n`; markdown += `- āš ļø Medium: ${issuesBySeverity.Medium.length}\n`; markdown += `- ā„¹ļø Low: ${issuesBySeverity.Low.length}\n`; markdown += `- ā„¹ļø Info: ${issuesBySeverity.Info.length}\n\n`; // Detailed issues markdown += `## Detailed Findings\n\n`; ['Critical', 'High', 'Medium', 'Low', 'Info'].forEach(severity => { if (issuesBySeverity[severity].length > 0) { markdown += `### ${severity} Severity Issues\n\n`; issuesBySeverity[severity].forEach(issue => { markdown += `#### ${issue.title}\n\n`; markdown += `- **Lines**: ${issue.line_numbers.join(', ')}\n`; markdown += `- **Description**: ${issue.description}\n`; markdown += `- **Remediation**: ${issue.remediation}\n\n`; if (issue.code_example) { markdown += `**Fix Example**:\n`; markdown += `\`\`\`${analysis.language.toLowerCase()}\n${issue.code_example}\n\`\`\`\n\n`; } }); } }); } else { markdown += `## No Issues Found\n\nThe AI analysis did not detect any security issues in this file.\n\n`; } // Next steps markdown += `## Next Steps\n\n`; markdown += `1. Review and address the identified issues, prioritizing by severity\n`; markdown += `2. Run follow-up tests to verify your fixes\n`; markdown += `3. Consider adding automated security testing to your CI/CD pipeline\n`; return markdown; } /** * Main analysis function that handles both AI and fallback analysis * @param {string} filePath - Path to file to analyze * @param {Object} options - Analysis options * @returns {Promise<Object>} Analysis results */ async function analyzeWithAI(filePath, options = {}) { const outputFormat = options.format || 'json'; const outputPath = options.output || null; // If API key provided in options, use it if (options.apiKey) { setApiKey(options.apiKey); } console.log(`Starting AI-enhanced security analysis for ${filePath}`); const analysis = await analyzeFileWithAI(filePath); if (outputFormat === 'markdown') { const markdown = generateMarkdownReport(analysis); if (outputPath) { fs.writeFileSync(outputPath, markdown); console.log(`šŸ“ Markdown report written to: ${outputPath}`); } return { ...analysis, markdown }; } if (outputPath) { fs.writeFileSync(outputPath, JSON.stringify(analysis, null, 2)); console.log(`šŸ“ JSON report written to: ${outputPath}`); } return analysis; } // Direct execution from command line if (require.main === module) { const args = process.argv.slice(2); if (args.length === 0) { console.log('Usage: ai-security-analyzer.js <file_path> [--output=file.json] [--format=json|markdown]'); process.exit(1); } const filePath = args[0]; const options = {}; // Parse command line options args.slice(1).forEach(arg => { if (arg.startsWith('--output=')) { options.output = arg.substring('--output='.length); } else if (arg.startsWith('--format=')) { options.format = arg.substring('--format='.length); } else if (arg.startsWith('--api-key=')) { options.apiKey = arg.substring('--api-key='.length); } }); (async () => { try { await analyzeWithAI(filePath, options); } catch (error) { console.error(`Error during analysis: ${error.message}`); process.exit(1); } })(); } module.exports = { analyzeWithAI, generateMarkdownReport, setApiKey };