Skip to main content
Glama

MCP Shamash

incremental-scanner.ts9.08 kB
import type { ScanRequest, ScanResult, Finding } from '../types/index.js'; import type { ProjectScanner } from './project-scanner.js'; import { GitAnalyzer } from '../utils/git-analyzer.js'; import { ResultCache } from '../cache/result-cache.js'; import * as path from 'path'; export interface IncrementalScanResult extends ScanResult { incrementalMode: boolean; filesScanned: number; filesFromCache: number; totalFiles: number; gitCommit?: string; } export class IncrementalScanner { private gitAnalyzer: GitAnalyzer; private cache: ResultCache; private projectScanner: ProjectScanner; constructor( projectRoot: string, projectScanner: ProjectScanner, cache?: ResultCache ) { this.gitAnalyzer = new GitAnalyzer(projectRoot); this.projectScanner = projectScanner; this.cache = cache || new ResultCache(); } async scan(request: ScanRequest): Promise<IncrementalScanResult> { const startTime = Date.now(); // Check if incremental scan is possible const shouldIncremental = await this.gitAnalyzer.shouldRunIncrementalScan(); if (!shouldIncremental || request.options?.incremental === false) { // Run full scan console.error('Running full scan (incremental not applicable)'); const fullResult = await this.projectScanner.scan(request); // Save current commit for next incremental scan const currentCommit = await this.gitAnalyzer.getCurrentCommit(); if (currentCommit) { await this.gitAnalyzer.saveLastScanCommit(currentCommit); } return { ...fullResult, incrementalMode: false, filesScanned: -1, // All files filesFromCache: 0, totalFiles: -1, gitCommit: currentCommit, }; } // Get incremental scan scope const scope = await this.gitAnalyzer.getIncrementalScanScope(); if (scope.fullScanRequired) { console.error(`Full scan required: ${scope.reason}`); const fullResult = await this.projectScanner.scan(request); const currentCommit = await this.gitAnalyzer.getCurrentCommit(); if (currentCommit) { await this.gitAnalyzer.saveLastScanCommit(currentCommit); } return { ...fullResult, incrementalMode: false, filesScanned: -1, filesFromCache: 0, totalFiles: -1, gitCommit: currentCommit, errors: fullResult.errors ? [...fullResult.errors, scope.reason!] : [scope.reason!], }; } // Perform incremental scan console.error(`Running incremental scan on ${scope.files.length} changed files`); const allFindings: Finding[] = []; const errors: string[] = []; let tokenUsage = 0; let filesFromCache = 0; // Get cached results for unchanged files const cachedResults = await this.getCachedResults(request); if (cachedResults) { allFindings.push(...cachedResults.findings); filesFromCache = cachedResults.fileCount; tokenUsage += cachedResults.tokenUsage; } // Scan only changed files if (scope.files.length > 0) { const incrementalRequest: ScanRequest = { ...request, options: { ...request.options, incremental: true, }, }; try { const scanResult = await this.scanChangedFiles(incrementalRequest, scope.files); allFindings.push(...scanResult.findings); tokenUsage += scanResult.tokenUsage; if (scanResult.errors) { errors.push(...scanResult.errors); } } catch (error) { const errorMsg = `Incremental scan failed: ${error instanceof Error ? error.message : 'Unknown error'}`; console.error(errorMsg); errors.push(errorMsg); } } // Update last scan commit const currentCommit = await this.gitAnalyzer.getCurrentCommit(); if (currentCommit) { await this.gitAnalyzer.saveLastScanCommit(currentCommit); } // Calculate summary const summary = this.calculateSummary(allFindings); const result: IncrementalScanResult = { scanId: this.generateScanId(), status: errors.length === 0 ? 'success' : 'partial', summary, findings: allFindings, tokenUsage, scanTimeMs: Date.now() - startTime, incrementalMode: true, filesScanned: scope.files.length, filesFromCache, totalFiles: scope.files.length + filesFromCache, gitCommit: currentCommit, errors: errors.length > 0 ? errors : undefined, }; // Cache the incremental results await this.cacheIncrementalResults(result, scope.files); console.error(`Incremental scan completed: ${scope.files.length} files scanned, ${filesFromCache} from cache`); return result; } private async scanChangedFiles(request: ScanRequest, files: string[]): Promise<ScanResult> { // Create a temporary scan request for just the changed files const tempRequest: ScanRequest = { ...request, target: request.target, options: { ...request.options, incremental: true, }, }; // Use the project scanner but limit to specific files // This would require modifying the scanner to accept file lists // For now, we'll scan the whole project but filter results const fullScan = await this.projectScanner.scan(tempRequest); // Filter findings to only those in changed files const relevantFindings = fullScan.findings.filter(finding => { if (!finding.location?.file) return false; const absolutePath = path.isAbsolute(finding.location.file) ? finding.location.file : path.join(request.target, finding.location.file); return files.some(file => absolutePath.includes(file) || file.includes(absolutePath)); }); return { ...fullScan, findings: relevantFindings, tokenUsage: Math.ceil(fullScan.tokenUsage * (relevantFindings.length / Math.max(fullScan.findings.length, 1))), }; } private async getCachedResults(request: ScanRequest): Promise<{ findings: Finding[]; fileCount: number; tokenUsage: number; } | null> { try { // Get the last full scan from cache const lastCommit = await this.gitAnalyzer.getLastScanCommit(); if (!lastCommit) return null; // Try to get cached results for the last commit const cachedData = await this.cache.get('project', request.target, request.tools || [], request.profile); if (cachedData && cachedData.findings) { // Filter out findings from changed files const scope = await this.gitAnalyzer.getIncrementalScanScope(); const changedFiles = new Set(scope.files); const unchangedFindings = cachedData.findings.filter(finding => { if (!finding.location?.file) return true; const absolutePath = path.isAbsolute(finding.location.file) ? finding.location.file : path.join(request.target, finding.location.file); return !changedFiles.has(absolutePath); }); return { findings: unchangedFindings, fileCount: -1, // Unknown from cache tokenUsage: 0, // No new tokens used for cached results }; } } catch (error) { console.error('Failed to get cached results:', error); } return null; } private async cacheIncrementalResults(result: IncrementalScanResult, changedFiles: string[]): Promise<void> { try { // Create a cache entry for this incremental scan const cacheData = { ...result, changedFiles, timestamp: new Date().toISOString(), }; // Store with git commit as part of the key if (result.gitCommit) { await this.cache.set( 'incremental', result.gitCommit, result.findings.map(f => f.type), cacheData, 'incremental' ); } } catch (error) { console.error('Failed to cache incremental results:', error); } } private calculateSummary(findings: Finding[]) { const summary = { vulnerabilities: findings.length, critical: 0, high: 0, medium: 0, low: 0, informational: 0, }; for (const finding of findings) { summary[finding.severity]++; } return summary; } private generateScanId(): string { return `incremental_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } async getIncrementalStats(): Promise<{ lastScanCommit: string | null; currentCommit: string; changesPending: number; incrementalPossible: boolean; }> { const status = await this.gitAnalyzer.getGitStatus(); const incrementalPossible = await this.gitAnalyzer.shouldRunIncrementalScan(); return { lastScanCommit: status.lastScanCommit || null, currentCommit: status.commit, changesPending: status.changes.length + status.uncommittedChanges.length, incrementalPossible, }; } }

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/NeoTecDigital/mcp_shamash'

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