ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
137 lines (116 loc) • 5.01 kB
text/typescript
import { Command } from 'commander';
import chalk from 'chalk';
import ora from 'ora';
import path from 'path';
import fs from 'fs/promises';
import { glob } from 'glob';
import { LLMService } from '../core/llmService';
/**
* Semantic search command for finding potential security issues
*/
export function searchCommand(program: Command): void {
program
.command('search')
.description('Search codebase for potential security issues using semantic search')
.argument('<query>', 'Search query (e.g., "sql injection", "xss vulnerability")')
.option('-d, --directory <directory>', 'Directory to search', '.')
.option('-e, --extensions <extensions>', 'File extensions to include (comma separated)', 'js,jsx,ts,tsx,html,css')
.option('-l, --limit <limit>', 'Maximum number of results to return', '10')
.option('-o, --output <file>', 'Output results to a JSON file')
.action(async (query: string, options) => {
const spinner = ora('Searching codebase...').start();
try {
// Initialize LLM service for semantic search
const llmService = new LLMService();
// Parse extensions
const extensions = options.extensions.split(',').map((ext: string) => ext.trim());
const extensionPattern = extensions.length > 0 ?
`**/*.{${extensions.join(',')}}` :
'**/*.{js,jsx,ts,tsx,html,css}';
// Resolve search directory
const searchDir = path.resolve(process.cwd(), options.directory);
// Find all matching files
spinner.text = 'Finding files to search...';
const files = await glob(extensionPattern, {
cwd: searchDir,
ignore: [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/.git/**'
],
absolute: true
});
if (files.length === 0) {
spinner.fail('No files found to search.');
return;
}
spinner.text = `Searching ${files.length} files for "${query}"...`;
// Read all files content
const searchResults = [];
for (const file of files) {
try {
const content = await fs.readFile(file, 'utf8');
const relativePath = path.relative(process.cwd(), file);
// Perform semantic search on content
const matches = await llmService.performSemanticSearch(content, query);
if (matches.length > 0) {
// Add each match with file context
for (const match of matches) {
searchResults.push({
file: relativePath,
...match
});
}
}
} catch (error) {
// Skip files that can't be read
continue;
}
}
// Sort results by relevance score
searchResults.sort((a, b) => b.score - a.score);
// Limit results
const limit = parseInt(options.limit, 10);
const limitedResults = searchResults.slice(0, limit);
if (limitedResults.length === 0) {
spinner.succeed('Search completed. No matches found.');
return;
}
// Format results for display
spinner.succeed(`Found ${limitedResults.length} matches.`);
console.log('\nSearch Results:');
for (let i = 0; i < limitedResults.length; i++) {
const result = limitedResults[i];
console.log(`\n${chalk.cyan(`${i + 1}. ${result.file}`)}`);
console.log(` ${chalk.yellow('Relevance:')} ${Math.round(result.score * 100)}%`);
console.log(` ${chalk.yellow('Line:')} ${result.line || 'N/A'}`);
// Display snippet with highlighting if available
if (result.snippet) {
console.log(` ${chalk.yellow('Snippet:')}`);
console.log(` ${result.snippet.trim().split('\n').join('\n ')}`);
}
// Display security context if available
if (result.securityContext) {
console.log(` ${chalk.yellow('Security Context:')}`);
console.log(` ${result.securityContext}`);
}
}
// Save results to file if requested
if (options.output) {
const outputPath = path.resolve(process.cwd(), options.output);
await fs.writeFile(outputPath, JSON.stringify({
query,
timestamp: new Date().toISOString(),
totalFiles: files.length,
matches: limitedResults
}, null, 2), 'utf8');
console.log(`\n${chalk.green('✓')} Results saved to ${outputPath}`);
}
} catch (error) {
spinner.fail(`Search failed: ${(error as Error).message}`);
console.error(chalk.red((error as Error).stack));
process.exit(1);
}
});
}