ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
675 lines (580 loc) • 23.3 kB
JavaScript
/**
* Ctrl+Shift+Left Enhanced Security Analyzer
* A direct command-line tool for analyzing security risks in code with AI capabilities
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Try to import the AI security analyzer
let aiAnalyzer;
try {
aiAnalyzer = require('../src/ai-security-analyzer');
console.log('✅ AI-enhanced security analysis module loaded successfully.');
} catch (error) {
console.log('⚠️ AI security analyzer not available, using pattern-based analysis only.');
console.log(` ${error.message}`);
aiAnalyzer = null;
}
// Parse output path from command line arguments
function parseOutputPath() {
// Look for --output=<path> in the command line arguments
const outputArg = process.argv.find(arg => arg.startsWith('--output='));
if (outputArg) {
const outputPath = outputArg.split('=')[1];
// Check if the path is absolute or relative
if (path.isAbsolute(outputPath)) {
return outputPath;
} else {
// Convert relative path to absolute
return path.resolve(process.cwd(), outputPath);
}
}
return null;
}
// Configuration
let TARGET_FILE = process.argv[2] || './demo/src/components/PaymentForm.tsx';
// Generate default output paths relative to current working directory
const DEFAULT_OUTPUT_FILE = path.resolve(process.cwd(), 'security-report.md');
const DEFAULT_REPORTS_DIR = path.resolve(process.cwd(), 'reports/security');
// Get output path from command line or environment variables or use defaults
let OUTPUT_PATH = parseOutputPath();
let OUTPUT_FILE = OUTPUT_PATH ||
process.env.CTRLSHIFTLEFT_OUTPUT_PATH ||
DEFAULT_OUTPUT_FILE;
// Ensure reports directory is relative to the specified output or working directory
let REPORTS_DIR = OUTPUT_PATH ?
path.dirname(OUTPUT_PATH) :
process.env.CTRLSHIFTLEFT_REPORTS_DIR ||
DEFAULT_REPORTS_DIR;
const AI_MODE = process.argv.includes('--ai') || process.env.CTRLSHIFTLEFT_AI_ANALYSIS === 'true';
// Parse options from command-line arguments
let USE_AI = AI_MODE && aiAnalyzer !== null;
const API_KEY = process.env.OPENAI_API_KEY || null;
// Ensure all required directories exist
function ensureDirectoriesExist() {
// Ensure output directory for the main report exists
const outputDir = path.dirname(OUTPUT_FILE);
if (!fs.existsSync(outputDir)) {
console.log(`Creating output directory: ${outputDir}`);
fs.mkdirSync(outputDir, { recursive: true });
}
// Ensure security-reports directory exists
if (!fs.existsSync(REPORTS_DIR)) {
console.log(`Creating reports directory: ${REPORTS_DIR}`);
fs.mkdirSync(REPORTS_DIR, { recursive: true });
}
// Try to load paths configuration from .ctrlshiftleft directory
let customPaths = [];
try {
const ctrlshiftleftDir = path.resolve(process.cwd(), '.ctrlshiftleft');
const pathsFile = path.join(ctrlshiftleftDir, 'paths.js');
if (fs.existsSync(pathsFile)) {
const pathsConfig = require(pathsFile);
if (pathsConfig.OUTPUT_DIR) {
customPaths.push(pathsConfig.OUTPUT_DIR);
}
}
} catch (error) {
console.log(`Note: No custom paths loaded - ${error.message}`);
}
// Create other potential directories (with project-relative paths)
const dirs = [
path.resolve(process.cwd(), 'reports/security'),
path.resolve(process.cwd(), 'security-reports'),
path.resolve(process.cwd(), 'tests'),
...customPaths
];
dirs.forEach(dir => {
if (!fs.existsSync(dir)) {
console.log(`Creating directory: ${dir}`);
fs.mkdirSync(dir, { recursive: true });
}
});
}
// Load modules using require for backward compatibility
let reactPatterns = [];
let apiPatterns = [];
let configModule = null;
let config = {
security: {
customPatterns: [],
disabledPatterns: [],
frameworks: {
react: { enabled: true },
nextjs: { enabled: true },
express: { enabled: true }
}
}
};
// Try to load the enhanced modules - falls back gracefully if not available
try {
// Load the React security patterns
const reactModule = require('../src/utils/reactSecurityPatterns');
if (reactModule && reactModule.getAllReactPatterns) {
reactPatterns = reactModule.getAllReactPatterns();
console.log(`✅ Loaded ${reactPatterns.length} React-specific security patterns`);
}
} catch (error) {
console.log('ℹ️ React security patterns not available, using core patterns only');
}
try {
// Load the API route analyzer patterns
const apiModule = require('../src/utils/apiRouteAnalyzer');
if (apiModule && apiModule.getAllApiPatterns) {
apiPatterns = apiModule.getAllApiPatterns();
console.log(`✅ Loaded ${apiPatterns.length} API route security patterns`);
}
} catch (error) {
console.log('ℹ️ API route analyzer not available, using core patterns only');
}
try {
// Load the configuration module
configModule = require('../src/utils/configLoader');
if (configModule && configModule.loadConfig) {
config = configModule.loadConfig();
console.log('✅ Loaded custom security configuration');
// Create sample config if it doesn't exist
if (configModule.createSampleConfig) {
configModule.createSampleConfig();
}
}
} catch (error) {
console.log('ℹ️ Custom configuration not available, using default settings');
}
// Core security patterns to scan for
const CORE_SECURITY_PATTERNS = [
{
id: 'password-plaintext',
pattern: /password/i,
severity: 'HIGH',
title: 'Unprotected Password Field',
description: 'Password fields should be properly secured with encryption or hashing',
remediation: 'Ensure passwords are never stored in plaintext and are properly hashed before storage'
},
{
id: 'xss-innerhtml',
pattern: /innerHTML|dangerouslySetInnerHTML/,
severity: 'CRITICAL',
title: 'Potential XSS Vulnerability',
description: 'Using innerHTML or dangerouslySetInnerHTML can lead to cross-site scripting attacks',
remediation: 'Use safer alternatives like textContent or sanitize input before using'
},
{
id: 'storage-sensitive',
pattern: /localStorage|sessionStorage/,
severity: 'MEDIUM',
title: 'Client-side Storage of Sensitive Data',
description: 'Storing sensitive information in localStorage or sessionStorage is insecure',
remediation: 'Use secure storage mechanisms or encrypted tokens for sensitive data'
},
{
id: 'network-request',
pattern: /fetch\s*\(/,
severity: 'INFO',
title: 'Network Request',
description: 'Network requests should be properly secured and validated',
remediation: 'Ensure proper CORS settings and validate all input and output'
},
{
id: 'eval-usage',
pattern: /eval\s*\(/,
severity: 'CRITICAL',
title: 'Use of eval()',
description: 'Using eval() can execute arbitrary code and lead to injection attacks',
remediation: 'Avoid using eval() completely. Use safer alternatives like JSON.parse() or Function constructors if absolutely necessary'
},
{
pattern: /document\.write/,
severity: 'HIGH',
title: 'Use of document.write',
description: 'document.write can lead to XSS vulnerabilities and is considered bad practice',
remediation: 'Use DOM manipulation methods like createElement and appendChild instead'
},
{
pattern: /(?:\.|\[(["']))(cookie)\1?\]/,
severity: 'MEDIUM',
title: 'Cookie Manipulation',
description: 'Insecure cookie manipulation can lead to session hijacking',
remediation: 'Use secure and httpOnly flags for sensitive cookies'
},
{
pattern: /jwt/i,
severity: 'INFO',
title: 'JWT Usage',
description: 'JWT tokens should be properly validated and handled securely',
remediation: 'Ensure proper validation of JWT tokens and use secure storage'
}
];
// Get combined security patterns based on file type and user config
function getSecurityPatterns(filePath) {
// Start with core patterns
let patterns = [...CORE_SECURITY_PATTERNS];
// Determine file type for specialized patterns
const fileExt = path.extname(filePath).toLowerCase();
const isReactFile = /\.(jsx|tsx)$/.test(fileExt);
const isApiFile = /(\/api\/|\/routes\/|\/controllers\/)/.test(filePath) ||
/\.(api|controller|route)\.(js|ts)$/.test(filePath);
// Add React patterns if it's a React file
if (isReactFile && config.security.frameworks.react.enabled && reactPatterns.length > 0) {
console.log('📋 Adding React-specific security patterns');
patterns = [...patterns, ...reactPatterns];
}
// Add API patterns if it's an API file
if (isApiFile && apiPatterns.length > 0) {
// Determine which framework patterns to include
const frameworks = [];
if (config.security.frameworks.nextjs.enabled) {
frameworks.push('nextjs');
}
if (config.security.frameworks.express.enabled) {
frameworks.push('express');
}
frameworks.push('node'); // Always include generic Node.js patterns
console.log(`📋 Adding API security patterns for frameworks: ${frameworks.join(', ')}`);
patterns = [...patterns, ...apiPatterns.filter(p =>
!p.framework || p.framework.some(f => frameworks.includes(f))
)];
}
// Add custom patterns from config
if (config.security.customPatterns && config.security.customPatterns.length > 0) {
console.log(`📋 Adding ${config.security.customPatterns.length} custom security patterns`);
patterns = [...patterns, ...config.security.customPatterns];
}
// Filter out disabled patterns
if (config.security.disabledPatterns && config.security.disabledPatterns.length > 0) {
patterns = patterns.filter(pattern =>
!pattern.id || !config.security.disabledPatterns.includes(pattern.id)
);
console.log(`🔍 Filtered out ${config.security.disabledPatterns.length} disabled patterns`);
}
return patterns;
}
// Analyze a file for security vulnerabilities
function analyzeFile(filePath) {
// Check if file exists
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`);
return [];
}
console.log(`Analyzing file: ${filePath}`);
// Get appropriate security patterns for this file
const securityPatterns = getSecurityPatterns(filePath);
// Read file content
const content = fs.readFileSync(filePath, 'utf-8');
const issues = [];
// Check for security issues
securityPatterns.forEach(pattern => {
const matches = content.match(pattern.pattern);
if (matches) {
issues.push({
pattern: pattern.pattern,
severity: pattern.severity,
title: pattern.title,
description: pattern.description,
remediation: pattern.remediation,
category: pattern.category || 'general',
matches: matches.length
});
}
});
return issues;
}
// Generate a markdown report from pattern-based analysis
function generateReport(filePath, issues) {
const fileName = path.basename(filePath);
const severityGroups = {
CRITICAL: [],
HIGH: [],
MEDIUM: [],
LOW: [],
INFO: []
};
// Group issues by category
const categoryGroups = {};
// Sort issues into severity and category groups
issues.forEach(issue => {
// Add to severity group
severityGroups[issue.severity].push(issue);
// Add to category group
const category = issue.category || 'general';
if (!categoryGroups[category]) {
categoryGroups[category] = [];
}
categoryGroups[category].push(issue);
});
// Create report header
let report = '# Security Analysis for ' + path.basename(filePath) + '\n\n';
report += `*Generated by Ctrl+Shift+Left v1.4.0 on ${new Date().toLocaleString()}*\n\n`;
// Summary
report += '## Summary\n\n';
const criticalCount = severityGroups.CRITICAL.length;
const highCount = severityGroups.HIGH.length;
const mediumCount = severityGroups.MEDIUM.length;
const lowCount = severityGroups.LOW.length;
const infoCount = severityGroups.INFO.length;
report += `- 🚨 Critical: ${criticalCount}\n`;
report += `- ⚠️ High: ${highCount}\n`;
report += `- ⚠️ Medium: ${mediumCount}\n`;
report += `- ℹ️ Low: ${lowCount}\n`;
report += `- ℹ️ Info: ${infoCount}\n\n`;
// Category summary if multiple categories exist
if (Object.keys(categoryGroups).length > 1) {
report += '### Categories\n\n';
Object.keys(categoryGroups).sort().forEach(category => {
const categoryIssues = categoryGroups[category];
const criticalInCategory = categoryIssues.filter(i => i.severity === 'CRITICAL').length;
// Determine category icon based on highest severity
let categoryIcon = '📋';
if (criticalInCategory > 0) {
categoryIcon = '🚨';
} else if (categoryIssues.some(i => i.severity === 'HIGH')) {
categoryIcon = '⚠️';
}
report += `${categoryIcon} **${category}**: ${categoryIssues.length} issues\n`;
});
report += '\n';
}
// Detailed issues by severity
report += '## Issues by Severity\n\n';
['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'].forEach(severity => {
if (severityGroups[severity].length > 0) {
const severityIcon = severity === 'CRITICAL' ? '🚨' :
severity === 'HIGH' ? '⚠️' :
severity === 'MEDIUM' ? '⚠️' : '📋';
report += `### ${severityIcon} ${severity} Severity\n\n`;
severityGroups[severity].forEach(issue => {
// Format pattern as more readable regex
const patternStr = issue.pattern.toString();
report += `#### ${issue.title}\n\n`;
if (issue.matches && issue.matches > 1) {
report += `- **Found**: ${issue.matches} occurrences\n`;
}
report += `- **Pattern**: \`${patternStr}\`\n`;
report += `- **Description**: ${issue.description}\n`;
report += `- **Remediation**: ${issue.remediation}\n`;
if (issue.category && issue.category !== 'general') {
report += `- **Category**: ${issue.category}\n`;
}
report += '\n';
});
}
});
// Detailed issues by category if multiple categories exist
if (Object.keys(categoryGroups).length > 1) {
report += '## Issues by Category\n\n';
Object.keys(categoryGroups).sort().forEach(category => {
if (category === 'general') return; // Skip general category as it's already covered
report += `### ${category}\n\n`;
// Group by severity within category
const categoryBySeverity = {
CRITICAL: [],
HIGH: [],
MEDIUM: [],
LOW: [],
INFO: []
};
categoryGroups[category].forEach(issue => {
categoryBySeverity[issue.severity].push(issue);
});
['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'].forEach(severity => {
categoryBySeverity[severity].forEach(issue => {
report += `#### ${issue.title} (${severity})\n\n`;
report += `- **Description**: ${issue.description}\n`;
report += `- **Remediation**: ${issue.remediation}\n\n`;
});
});
});
}
// Add test generation recommendation
report += '## Next Steps\n\n';
report += '1. Review and fix the identified security issues\n';
report += '2. Generate tests to verify fixes using `npx ctrlshiftleft gen <file> --output tests`\n';
report += '3. Run the generated tests to validate your changes\n';
if (criticalCount > 0 || highCount > 0) {
report += '4. Run security scan again to verify all critical and high issues are resolved\n';
}
return report;
}
// Combine pattern-based and AI analysis
async function enhancedAnalysis(filePath) {
// Pattern-based analysis
const patternIssues = analyzeFile(filePath);
console.log(`Pattern-based analysis found ${patternIssues.length} potential issues.`);
// AI-powered analysis (if available and enabled)
if (USE_AI) {
console.log('🧠 Using AI-enhanced security analysis...');
try {
// Set the API key if provided
if (API_KEY) {
aiAnalyzer.setApiKey(API_KEY);
}
// Prepare output paths
const fileName = path.basename(filePath, path.extname(filePath));
const aiReportPath = path.join(REPORTS_DIR, `${fileName}-ai.md`);
// Run AI analysis
const aiResult = await aiAnalyzer.analyzeWithAI(filePath, {
format: 'markdown',
output: aiReportPath
});
if (aiResult.error) {
console.warn(`⚠️ AI analysis encountered an error: ${aiResult.error}`);
console.log('Using pattern-based analysis results only.');
return generateReport(filePath, patternIssues);
}
// Successfully generated AI report
console.log(`🎉 AI-enhanced analysis complete. Report written to: ${aiReportPath}`);
// Return the AI-generated markdown directly
return aiResult.markdown;
} catch (error) {
console.error(`❌ Error during AI analysis: ${error.message}`);
console.log('Falling back to pattern-based analysis...');
return generateReport(filePath, patternIssues);
}
} else {
// Use pattern-based analysis only
return generateReport(filePath, patternIssues);
}
}
/**
* Maximum number of retries for network operations
*/
const MAX_RETRIES = 3;
/**
* Retry a function with exponential backoff
* @param {Function} fn - Function to retry
* @param {number} maxRetries - Maximum number of retries
* @param {number} initialDelay - Initial delay in ms
* @returns {Promise<any>} - Result of the function
*/
async function retryWithBackoff(fn, maxRetries = MAX_RETRIES, initialDelay = 500) {
let retries = 0;
let lastError;
while (retries <= maxRetries) {
try {
return await fn();
} catch (error) {
lastError = error;
if (retries === maxRetries) break;
// Calculate delay with exponential backoff
const delay = initialDelay * Math.pow(2, retries);
console.log(`Operation failed, retrying in ${delay}ms... (${retries + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
retries++;
}
}
throw lastError;
}
// Main function
async function main() {
// Extract command line arguments
const args = process.argv.slice(2);
// Parse arguments
let targetFilePath = TARGET_FILE;
let outputFilePath = OUTPUT_FILE;
let useAiMode = USE_AI;
let configPath = null;
// Extract target file (first non-option argument)
const nonOptionArgs = args.filter(arg => !arg.startsWith('--'));
if (nonOptionArgs.length > 0) {
targetFilePath = nonOptionArgs[0];
}
// Parse option arguments
args.forEach(arg => {
if (arg.startsWith('--output=')) {
outputFilePath = arg.substring('--output='.length);
} else if (arg === '--ai') {
useAiMode = true;
} else if (arg.startsWith('--config=')) {
configPath = arg.substring('--config='.length);
} else if (arg === '--no-ai') {
useAiMode = false;
}
});
// If config module is available, use it to determine output format and directory
if (configModule && configModule.loadConfig) {
try {
// Load config from specified path or default location
const customConfig = configPath ?
configModule.loadConfig(path.dirname(configPath)) :
configModule.loadConfig();
// Apply output configuration if not explicitly overridden by command line
if (customConfig.security?.output && args.every(arg => !arg.startsWith('--output='))) {
const format = customConfig.security.output.format || 'markdown';
const directory = customConfig.security.output.directory || './security-reports';
// Create output directory if it doesn't exist
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
// Generate output filename
const fileBaseName = path.basename(targetFilePath, path.extname(targetFilePath));
outputFilePath = path.join(directory, `${fileBaseName}-security.${format === 'markdown' ? 'md' : format}`);
}
} catch (configError) {
console.warn(`Warning: Could not load configuration: ${configError.message}`);
console.warn('Using default settings instead.');
}
}
console.log('Ctrl+Shift+Left Security Analyzer');
console.log('=================================');
console.log(`Analysis mode: ${useAiMode ? '🧠 AI-enhanced' : '🔍 Pattern-based'}`);
console.log(`Target: ${targetFilePath}`);
console.log(`Output: ${outputFilePath}`);
// Ensure output directories exist
try {
const outputDir = path.dirname(outputFilePath);
if (!fs.existsSync(outputDir)) {
console.log(`Creating directory: ${outputDir}`);
fs.mkdirSync(outputDir, { recursive: true });
}
} catch (dirError) {
console.error(`Error creating output directory: ${dirError.message}`);
console.log('Falling back to default output location...');
outputFilePath = path.join(process.cwd(), 'security-report.md');
}
// Update global variables to use the new values
TARGET_FILE = targetFilePath;
OUTPUT_FILE = outputFilePath;
USE_AI = useAiMode;
try {
// Generate report with retry logic for network operations
const report = await retryWithBackoff(async () => {
return await enhancedAnalysis(TARGET_FILE);
});
// Write report to file
try {
fs.writeFileSync(OUTPUT_FILE, report);
} catch (writeError) {
console.error(`Error writing report: ${writeError.message}`);
const fallbackPath = path.join(process.cwd(), 'security-report.md');
console.log(`Attempting to write to fallback location: ${fallbackPath}`);
fs.writeFileSync(fallbackPath, report);
OUTPUT_FILE = fallbackPath;
}
// Generate relative path for displaying to user
const relativeOutputPath = path.relative(process.cwd(), OUTPUT_FILE);
// Print summary with paths
console.log(`Analysis complete. Report written to: ${relativeOutputPath}`);
console.log(`Absolute path: ${OUTPUT_FILE}`);
// Only try to open the report if not running in CI/CD environment
if (!process.env.CI && !process.env.GITHUB_ACTIONS && process.stdout.isTTY) {
try {
// OS-specific command to open file
const openCommand = process.platform === 'darwin' ? 'open' :
process.platform === 'win32' ? 'start "" ' :
'xdg-open';
execSync(`${openCommand} "${OUTPUT_FILE}"`);
} catch (openError) {
console.log('Could not automatically open the report.');
}
}
} catch (error) {
console.error(`Error during analysis: ${error.message}`);
process.exit(1);
}
}
// Run the analyzer
main().catch(error => {
console.error(`Unhandled error: ${error.message}`);
process.exit(1);
});