const fs = require('fs'); const path = require('path'); require('dotenv').config({ path: path.resolve(__dirname, '../../src/.env') }); /** * @fileoverview Automatisierungsskript für den Multi-Repository SAP-Deprecation Scan. * Durchsucht lokale Unterordner (Repositories) rekursiv nach SAP- und REST-Endpunkten, * gleicht diese über die FastAPI ab und generiert einen zusammenfassenden Report. * @author MBT * @version 1.3 (Zeigt in Details nur veränderte/abgekündigte Endpunkte) */ // --- KONFIGURATION --- const BASE_PATH = './'; const FASTAPI_URL = 'http://127.0.0.1:8000/informations/bulk'; const OUTPUT_FILE = './sap_api_scan_report.txt'; const IGNORED_DIRS = ['node_modules', '.git', '__pycache__', 'venv', 'dist', 'build', '.idea', '.vscode']; const headers = { "X-API-Key": process.env.READONLY_APIKEY }; // Optimierte Regex (Case-Insensitive) const ENDPOINT_REGEX = /\b(?:\/?)(?:v\d+|sap)\/[a-zA-Z0-9_\-\/{}]*(?:\/[a-zA-Z0-9_\-\/{}]+)*/i; // --- FUNKTIONEN --- function getRepositories(baseDir) { return fs.readdirSync(baseDir).filter(file => { const fullPath = path.join(baseDir, file); return fs.statSync(fullPath).isDirectory() && !IGNORED_DIRS.includes(file); }); } function walkSync(dir, filelist = []) { const files = fs.readdirSync(dir); files.forEach(file => { const filepath = path.join(dir, file); const stat = fs.statSync(filepath); if (stat.isDirectory()) { if (!IGNORED_DIRS.includes(file)) { walkSync(filepath, filelist); } } else { filelist.push(filepath); } }); return filelist; } function scanRepositoryForEndpoints(repoPath, repoName) { console.log(`\n🔍 [${repoName}] Starte rekursiven Scan im Ordner: ${path.resolve(repoPath)}...`); const files = walkSync(repoPath); const endpointLocations = {}; let checkedFilesCount = 0; files.forEach(file => { if ( file.endsWith('scan_repo.js') || file.endsWith('scan_all_repos.js') || file.endsWith('.txt') || file.endsWith('.json') || file.endsWith('.md') || file.includes(path.basename(OUTPUT_FILE)) ) { return; } checkedFilesCount++; try { const content = fs.readFileSync(file, 'utf8'); if (!/sap|v\d+/i.test(content)) { return; } const lines = content.split(/\r?\n/); lines.forEach((lineText, index) => { const lineNumber = index + 1; const match = lineText.match(ENDPOINT_REGEX); if (match) { const endpoint = match[0].trim(); if (endpoint.length >= 4) { if (!endpointLocations[endpoint]) { endpointLocations[endpoint] = []; } const relativePath = path.relative(BASE_PATH, file); endpointLocations[endpoint].push({ file: relativePath, line: lineNumber }); console.log(` ✨ Treffer in [${repoName}]: "${endpoint}" gefunden in ${relativePath}:${lineNumber}`); } } }); } catch (err) { if (err.code !== 'EISDIR' && !file.match(/\.(png|jpg|jpeg|gif|ico|zip|tar|gz|pdf|exe|dll)$/i)) { console.warn(`⚠️ Konnte Datei nicht lesen: ${file} (${err.message})`); } } }); console.log(`🏁 [${repoName}] Scan beendet. ${checkedFilesCount} Dateien durchsucht. ${Object.keys(endpointLocations).length} eindeutige Endpunkte gefunden.`); return endpointLocations; } async function checkEndpoints(endpoints, batchSize = 25) { const consolidatedResult = { total_checked: 0, results: [] }; for (let i = 0; i < endpoints.length; i += batchSize) { const batch = endpoints.slice(i, i + batchSize); console.log(` ⏳ Sende Batch ${Math.floor(i / batchSize) + 1} (${batch.length} Endpunkte)...`); const params = new URLSearchParams(); batch.forEach(ep => params.append("endpoints", ep)); try { const response = await fetch(`${FASTAPI_URL}?${params.toString()}`, { method: "GET", headers: headers }); if (!response.ok) { throw new Error(`Server meldete Status ${response.status}`); } const batchJson = await response.json(); consolidatedResult.total_checked += batchJson.total_checked || batch.length; if (batchJson.results && Array.isArray(batchJson.results)) { consolidatedResult.results.push(...batchJson.results); } } catch (error) { throw new Error(`Fehler in Batch ab Index ${i}: ${error.message}`); } } return consolidatedResult; } /** * Steuert den Scan-Workflow und baut die aggregierte Zusammenfassung sowie den gefilterten Detailbericht. */ async function runMultiRepoScan() { const repos = getRepositories(BASE_PATH); if (repos.length === 0) { console.log("⚠️ Keine Scan-fähigen Unterordner (Repositories) gefunden!"); return; } console.log(`📂 ${repos.length} Repositories für den Scan gefunden.`); // Statistik-Zähler für die Zusammenfassung const stats = { totalRepos: repos.length, reposWithEndpoints: 0, totalEndpointsFound: 0, statusActive: 0, statusDeprecated: 0, statusChanged: 0, statusUnknown: 0 }; let detailedReportBody = ''; // Schleife über alle gefundenen Repository-Ordner for (const repoName of repos) { const repoPath = path.join(BASE_PATH, repoName); console.log(`\n--------------------------------------------------`); console.log(`📦 Verarbeite Repository: ${repoName.toUpperCase()}`); console.log(`--------------------------------------------------`); const endpointLocations = scanRepositoryForEndpoints(repoPath, repoName); const uniqueEndpoints = Object.keys(endpointLocations); if (uniqueEndpoints.length === 0) { console.log(`ℹ️ Keine SAP-Endpunkte in [${repoName}] gefunden.`); continue; } stats.reposWithEndpoints++; stats.totalEndpointsFound += uniqueEndpoints.length; console.log(`🚀 ${uniqueEndpoints.length} Endpunkte in [${repoName}] gefunden. Sende Abfrage an FastAPI...`); try { const apiResult = await checkEndpoints(uniqueEndpoints); let repoIssuesContent = ''; // Puffer für problematische Endpunkte in diesem spezifischen Repo apiResult.results.forEach(item => { const messageLower = item.message.toLowerCase(); const statusLower = (item.status || '').toLowerCase(); // Status für die Statistik auswerten const isNoHit = messageLower.includes("keine sap-änderungen oder deprecations gefunden"); let isIssue = false; if (isNoHit || statusLower === 'active' || statusLower === 'ok') { stats.statusActive++; } else if (statusLower.includes('deprecat')) { stats.statusDeprecated++; isIssue = true; } else if (statusLower.includes('chang') || statusLower.includes('modif')) { stats.statusChanged++; isIssue = true; } else { stats.statusUnknown++; isIssue = true; // Unbekannte Stati sicherheitshalber auch auflisten } // Nur wenn ein Problem/eine Änderung vorliegt, wird der Endpunkt im Detailbericht aufgelistet if (isIssue) { const locations = endpointLocations[item.endpoint] || []; const locationString = locations .map(loc => `${loc.file}:${loc.line}`) .join(', '); let logLine = `[❗Warning] Endpoint: ${item.endpoint}\n`; logLine += ` Files: ${locationString}\n`; logLine += ` Status: ${item.status}\n`; logLine += ` Details: ${item.message}\n`; logLine += ` Deprecated in Quality: ${item.deprecated[0]}\n`; logLine += ` Deprecated in Production: ${item.deprecated[1]}\n`; logLine += ` Decommissioned in Quality: ${item.decommissioned[0]}\n`; logLine += ` Decommissioned in Production: ${item.decommissioned[1]}\n`; console.log(logLine); repoIssuesContent += logLine + "\n"; } }); // Falls dieses Repo veränderte/abgekündigte Endpunkte hatte, hänge sie an den Report an if (repoIssuesContent.length > 0) { detailedReportBody += `📦 REPOSITORY: ${repoName}\n`; detailedReportBody += `--------------------------------------------------\n`; detailedReportBody += repoIssuesContent; detailedReportBody += `\n`; } } catch (error) { console.error(`❌ Fehler bei API-Abfrage für [${repoName}]:`, error.message); detailedReportBody += `📦 REPOSITORY: ${repoName}\n -> API-Fehler bei der Abfrage: ${error.message}\n\n`; } } // Zusammenbau des finalen Berichts (Zusammenfassung ganz oben!) let finalReportContent = `======================================================================\n`; finalReportContent += ` GLOBAL SAP DEPRECATION SCAN REPORT - ${new Date().toLocaleString()}\n`; finalReportContent += `======================================================================\n\n`; finalReportContent += `📊 SUMMARY:\n`; finalReportContent += `----------------------------------------------------------------------\n`; finalReportContent += ` Total Repositories Scanned: ${stats.totalRepos}\n`; finalReportContent += ` Repositories with Findings: ${stats.reposWithEndpoints}\n`; finalReportContent += ` Total Endpoints Detected: ${stats.totalEndpointsFound}\n\n`; finalReportContent += ` ENDPOINT STATUS OVERVIEW:\n`; finalReportContent += ` 🟢 Active / Unchanged Endpoints: ${stats.statusActive}\n`; finalReportContent += ` 🟡 Modified Endpoints (Changed): ${stats.statusChanged}\n`; finalReportContent += ` 🔴 Obsolete Endpoints (Deprecated): ${stats.statusDeprecated}\n`; if (stats.statusUnknown > 0) { finalReportContent += ` ⚪ Unknown / Unclear Status: ${stats.statusUnknown}\n`; } finalReportContent += `======================================================================\n\n\n`; // Details (filtered issues only!) appended below finalReportContent += `📝 ACTIONABLE DETAILS & DETECTED ISSUES (Filtered):\n`; finalReportContent += ` (Only modified or deprecated endpoints are listed below)\n`; finalReportContent += `======================================================================\n\n`; if (detailedReportBody.length === 0) { finalReportContent += `🎉 Excellent! No modified or deprecated endpoints were found in any of the repositories.\n`; } else { finalReportContent += detailedReportBody; } // Gesamtergebnis in Datei schreiben try { fs.writeFileSync(OUTPUT_FILE, finalReportContent, 'utf8'); console.log(`\n💾 Der globale Bericht wurde erfolgreich gespeichert: ${path.resolve(OUTPUT_FILE)}`); } catch (err) { console.error("❌ Bericht konnte nicht geschrieben werden:", err.message); } } // --- START --- runMultiRepoScan();