CoolFace
Apppublic

Hossam01/Code-Guardian-Api

sourceHugging Facemitupdated 23h agoView on Hugging Face
1likes
server.js720 linesDownload Raw Back to root
1const express = require('express');2const cors = require('cors');3 4const app = express();5app.use(cors());6// The default body-parser limit is only 100kb — way too small for a 1000+7// line source file. This is what was causing "PayloadTooLargeError" on8// larger scans; it's an app-level limit, not a hosting/storage-tier issue,9// so raising it here fixes it regardless of which host you're on.10app.use(express.json({ limit: '15mb' }));11 12const PORT = process.env.PORT || 7860;13const GROQ_API_KEY = process.env.GROQ_API_KEY;14 15// --- Auth (Supabase) ---16// SUPABASE_URL/ANON_KEY are the same public values baked into the extension17// (safe to duplicate here). SERVICE_ROLE_KEY is SECRET — set it as an18// environment variable in the HF Space settings, never commit it or put it19// in the extension.20const SUPABASE_URL = process.env.SUPABASE_URL || 'https://lfzvntlvjncsgmikwzut.supabase.co';21const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxmenZudGx2am5jc2dtaWt3enV0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODk5NjYyNzksImV4cCI6MjEwNTU0MjI3OX0.xrZnsa5akDVgz_JSTUGJR-PP7Ft2E_s84tzVTTD-1BI';22const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY; // used for usage logging only; optional23 24// Verifies the "Authorization: Bearer <token>" header against Supabase's own25// Auth service (one HTTP call) and attaches req.user on success. This is the26// standard, supported way to check a Supabase session server-side — no need27// to hand-roll JWT verification or store passwords ourselves.28async function requireAuth(req, res, next) {29    const authHeader = req.headers['authorization'] || '';30    const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : null;31 32    if (!token) {33        return res.status(401).json({ error: 'Sign in required. Please sign in to COVRA and try again.' });34    }35 36    try {37        const response = await fetch(`${SUPABASE_URL}/auth/v1/user`, {38            headers: { apikey: SUPABASE_ANON_KEY, Authorization: `Bearer ${token}` }39        });40 41        if (!response.ok) {42            return res.status(401).json({ error: 'Your session has expired. Please sign in to COVRA again.' });43        }44 45        const user = await response.json();46        req.user = { id: user.id, email: user.email };47        req.userToken = token;48        next();49    } catch (e) {50        console.error('Auth check failed:', e.message);51        return res.status(401).json({ error: 'Could not verify your session. Please sign in again.' });52    }53}54 55// Best-effort usage logging into Supabase's `usage_logs` table (see the SQL56// at the bottom of this file). Uses the service role key so it bypasses Row57// Level Security — this is server-to-server, already-trusted code, not a58// user-facing request. Never throws: a logging failure should never break59// an actual scan.60async function logUsageToSupabase(userId, endpoint, usage) {61    if (!SUPABASE_SERVICE_ROLE_KEY || !usage) return;62    try {63        await fetch(`${SUPABASE_URL}/rest/v1/usage_logs`, {64            method: 'POST',65            headers: {66                'Content-Type': 'application/json',67                apikey: SUPABASE_SERVICE_ROLE_KEY,68                Authorization: `Bearer ${SUPABASE_SERVICE_ROLE_KEY}`,69                Prefer: 'return=minimal'70            },71            body: JSON.stringify({72                user_id: userId,73                endpoint,74                prompt_tokens: usage.prompt_tokens,75                completion_tokens: usage.completion_tokens,76                total_tokens: usage.total_tokens77            })78        });79    } catch (e) {80        console.error('Supabase usage log failed (non-blocking):', e.message);81    }82}83 84// Checks the `admins` table (service_role key, bypasses RLS on purpose —85// this is a trusted server-side check, never exposed to the client directly)86async function isAdminUser(userId) {87    if (!SUPABASE_SERVICE_ROLE_KEY) return false;88    try {89        const response = await fetch(90            `${SUPABASE_URL}/rest/v1/admins?user_id=eq.${userId}&select=user_id`,91            { headers: { apikey: SUPABASE_SERVICE_ROLE_KEY, Authorization: `Bearer ${SUPABASE_SERVICE_ROLE_KEY}` } }92        );93        if (!response.ok) return false;94        const rows = await response.json().catch(() => []);95        return Array.isArray(rows) && rows.length > 0;96    } catch (e) {97        console.error('Admin check failed:', e.message);98        return false;99    }100}101 102// Lets the extension ask "who am I, and am I an admin?" right after signing103// in (or when restoring a saved session), so it can show the right UI.104app.get('/me', requireAuth, async (req, res) => {105    const admin = await isAdminUser(req.user.id);106    res.json({ email: req.user.email, isAdmin: admin });107});108 109app.get('/', (req, res) => {110    res.send('🛡️ Code Guardian API Server v5 - Bulletproof Multilingual Filter Active!');111});112 113// After Google sign-in, Supabase redirects the browser HERE with the tokens114// in the URL FRAGMENT (#access_token=...&refresh_token=...) — fragments115// never reach the server (they're client-side only), so this page's job is116// just to run a tiny script that reads the fragment and bounces the browser117// on to VS Code's own vscode:// URI, which is what the extension actually118// catches (see registerUriHandler in extension.js).119app.get('/auth-callback', (req, res) => {120    res.set('Content-Type', 'text/html');121    res.send(`<!DOCTYPE html>122<html>123<head>124    <meta charset="utf-8">125    <title>COVRA — Signing you in…</title>126    <style>127        body { font-family: system-ui, sans-serif; background: #0d1117; color: #c9d1d9; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; text-align: center; }128        .box { max-width: 360px; }129        .shield { font-size: 40px; margin-bottom: 12px; }130        h2 { margin: 0 0 8px; }131        p { color: #8b949e; font-size: 14px; }132    </style>133</head>134<body>135    <div class="box">136        <div class="shield">🛡️</div>137        <h2 id="title">Signing you in to COVRA…</h2>138        <p id="sub">This tab should close automatically. If VS Code doesn't open, come back here and click the link below.</p>139        <p><a id="manual-link" href="#" style="display:none; color:#58a6ff;">Open VS Code</a></p>140    </div>141    <script>142        const params = new URLSearchParams(window.location.hash.replace(/^#/, ''));143        const accessToken = params.get('access_token');144        const refreshToken = params.get('refresh_token');145        const error = params.get('error_description') || params.get('error');146 147        const vscodeUri = error148            ? 'vscode://ShifOo.code-guardian/auth-callback?error=' + encodeURIComponent(error)149            : 'vscode://ShifOo.code-guardian/auth-callback?access_token=' + encodeURIComponent(accessToken || '') +150              '&refresh_token=' + encodeURIComponent(refreshToken || '');151 152        document.getElementById('manual-link').href = vscodeUri;153        document.getElementById('manual-link').style.display = 'inline';154 155        if (error) {156            document.getElementById('title').textContent = 'Sign-in failed';157            document.getElementById('sub').textContent = error;158        } else if (!accessToken) {159            document.getElementById('title').textContent = 'Something went wrong';160            document.getElementById('sub').textContent = 'No session was returned. Please try signing in again from VS Code.';161        } else {162            window.location.href = vscodeUri;163        }164    </script>165</body>166</html>`);167});168 169// 🌐 Simple language detector: checks for Arabic script in the user's message170function detectLanguage(text) {171    const arabicRegex = /[\u0600-\u06FF]/;172    return arabicRegex.test(text) ? 'ar' : 'en';173}174 175// 📊 Logs the REAL token usage Groq reports for every call, so you can176// replace the estimated pricing numbers with actual measured data later.177// Shows up in your HF Space's "Logs" tab — no database needed for now.178function logTokenUsage(endpoint, usage) {179    if (!usage) return;180    console.log(181        `[USAGE] endpoint=${endpoint} prompt_tokens=${usage.prompt_tokens} ` +182        `completion_tokens=${usage.completion_tokens} total_tokens=${usage.total_tokens} ` +183        `at=${new Date().toISOString()}`184    );185}186 187// Wraps every Groq chat-completion call with automatic retry on 429 (rate188// limit) errors — reads the "try again in Xs" hint Groq's own error message189// gives us and waits that long before retrying, instead of failing the190// user's request immediately on a transient limit. 413 ("request too191// large") is NOT retried — retrying the same oversized payload would just192// fail again identically; the caller needs to shrink it instead.193//194// IMPORTANT: always pass an explicit, reasonably tight "max_tokens" in195// payload. Without it, Groq reserves a large default completion budget when196// checking your tokens-per-minute limit, which is what caused the197// "Requested 18851" style 413 errors seen in production — the request's198// ACTUAL content was much smaller than what got reserved for it.199async function callGroqWithRetry(payload, endpointLabel, maxRetries = 2) {200    for (let attempt = 0; attempt <= maxRetries; attempt++) {201        const response = await fetch('https://api.groq.com/openai/v1/chat/completions', {202            method: 'POST',203            headers: { 'Authorization': `Bearer ${GROQ_API_KEY}`, 'Content-Type': 'application/json' },204            body: JSON.stringify(payload)205        });206 207        if (response.status !== 429 || attempt === maxRetries) {208            return response;209        }210 211        const errText = await response.text().catch(() => '');212        const waitMatch = errText.match(/try again in ([\d.]+)s/i);213        const waitSeconds = Math.min(waitMatch ? parseFloat(waitMatch[1]) : 2 * (attempt + 1), 10);214        console.log(`[RETRY] ${endpointLabel}: rate limited, waiting ${waitSeconds}s before retry ${attempt + 1}/${maxRetries}`);215        await new Promise(r => setTimeout(r, waitSeconds * 1000 + 200));216    }217}218 219app.post('/analyze', requireAuth, async (req, res) => {220    // Guards against the exact crash seen in production logs: a request221    // arriving with no parsed JSON body (e.g. wrong Content-Type, an empty222    // POST from a health-check bot) used to throw before we could even223    // respond. Fail gracefully with a normal 400 instead of crashing.224    if (!req.body || typeof req.body !== 'object') {225        return res.status(400).json({ error: 'Request body must be valid JSON with a "code" field.' });226    }227    const { code, language, fileName } = req.body;228 229    if (!code) return res.status(400).json({ error: 'No code was sent for analysis' });230    if (!GROQ_API_KEY) return res.status(500).json({ error: 'GROQ_API_KEY is not configured' });231 232    const originalLines = code.replace(/\r\n/g, '\n').split('\n');233    const numberedCode = originalLines.map((line, idx) => `Line ${idx + 1}: ${line}`).join('\n');234 235    // 🎯 System Prompt with strict rules for False Positives and forcing an empty array when clean236    const systemPrompt = `You are a meticulous, conservative cybersecurity code auditor. Your top priority is AVOIDING FALSE POSITIVES — flagging something safe is worse than missing something minor.237Analyze the provided ${language} code line by line.238CRITICAL RULES FOR FALSE POSITIVES:2391. If a line of code has already been fixed, sanitized, or is secure, DO NOT report it as a vulnerability.2402. Do NOT invent or hallucinate vulnerabilities just to fill the JSON structure.2413. If the code contains NO vulnerabilities, or all previous vulnerabilities have been successfully fixed, you MUST set "vulnerabilities": [] and "securityScore": 100.2424. Recognize and DO NOT flag well-known SAFE patterns, including but not limited to:243   - Parameterized queries / prepared statements (e.g. "?" placeholders with parameters passed separately, ORM query builders, cursor.execute(query, params)).244   - Standard library escaping/sanitization functions used correctly.245   - Environment variables used for secrets (e.g. process.env.X, os.environ.get(...)) — this is the CORRECT way to handle secrets, not a hardcoded credential.246   - Input validated against an allowlist or schema before use.2475. Before including any vulnerability in your final answer, re-read that exact line one more time and ask yourself: "Is this actually exploitable as written, or am I pattern-matching on a keyword?" If you are not at least reasonably confident it is a real, exploitable issue, DO NOT include it.248You MUST respond with a valid JSON object ONLY. Do not wrap the JSON in markdown code blocks.249JSON Structure:250{251  "securityScore": 75,252  "summary": "A summary of the overall code status, in English",253  "vulnerabilities": [254    {255      "id": "VULN-001",256      "title": "Vulnerability name in English",257      "severity": "high",258      "line": 14, 259      "description": "Explanation of the vulnerability in English",260      "fix": "The single replacement line of code. Absolutely NO explanations, NO markdown. Match the exact starting indentation of the original line."261    }262  ]263}264CRITICAL: The "fix" field must contain ONLY pure executable code to replace that specific line. Do not rewrite whole functions or add unrelated imports inside it.`;265 266    try {267        const response = await callGroqWithRetry({268            model: 'openai/gpt-oss-120b',269            messages: [270                { role: 'system', content: systemPrompt },271                { role: 'user', content: `Analyze this code:\n\n${numberedCode}` }272            ],273            temperature: 0.1, // minimize hallucination as much as possible274            max_tokens: 4096, // generous but bounded — also prevents Groq over-reserving TPM budget for this request275            response_format: { type: "json_object" }276        }, '/analyze');277 278        if (!response.ok) {279            const errText = await response.text().catch(() => '');280            console.error('Groq API error (/analyze):', response.status, errText);281            if (response.status === 413) {282                return res.status(413).json({ error: 'This file is too large for the current plan\'s per-minute token limit. Try a smaller file, or upgrade the Groq plan.' });283            }284            if (response.status === 429) {285                return res.status(429).json({ error: 'Rate limit reached. Please wait a few seconds and try again.' });286            }287            return res.status(502).json({ error: `Groq API error (status ${response.status}). Please try again.` });288        }289 290        const data = await response.json().catch(() => null);291        logTokenUsage('/analyze', data?.usage);292        logUsageToSupabase(req.user.id, '/analyze', data?.usage);293        const rawContent = data?.choices?.[0]?.message?.content;294 295        if (!rawContent) {296            console.error('Unexpected Groq response shape (/analyze):', JSON.stringify(data));297            return res.status(502).json({ error: 'The AI returned an unexpected response. Please try again.' });298        }299 300        let parsedJson;301        try {302            parsedJson = JSON.parse(rawContent.trim());303        } catch (parseErr) {304            console.error('Failed to parse Groq JSON (/analyze):', parseErr.message, rawContent);305            return res.status(502).json({ error: 'The AI returned malformed data. Please try analyzing again.' });306        }307 308        if (parsedJson.vulnerabilities && Array.isArray(parsedJson.vulnerabilities)) {309            parsedJson.vulnerabilities = parsedJson.vulnerabilities.filter(v => {310                const lineIdx = parseInt(v.line) - 1;311                if (lineIdx >= 0 && lineIdx < originalLines.length) {312 313                    // 1. Extract the clean original code snippet on the server side314                    v.codeSnippet = originalLines[lineIdx].trim();315 316                    // Filter out phantom lines the AI sometimes invents317                    if (v.codeSnippet === "" || v.codeSnippet.startsWith("if __name__") || v.codeSnippet === "main()") {318                        return false; 319                    }320 321                    // Reject entries missing a meaningful title/description — likely malformed hallucinations322                    if (!v.title || !String(v.title).trim() || !v.description || !String(v.description).trim()) {323                        return false;324                    }325 326                    // Normalize severity to one of the three expected values327                    const validSeverities = ['high', 'medium', 'low'];328                    v.severity = validSeverities.includes(String(v.severity).toLowerCase()) ? String(v.severity).toLowerCase() : 'low';329 330                    // 2. 🔥 Smart Fix Sanitizer331                    if (v.fix) {332                        // Strip any markdown code fences and trim surrounding whitespace for comparison333                        v.fix = v.fix.replace(/```[a-zA-Z]*\n?/g, '').replace(/```/g, '').trim();334 335                        // 🛑 Sanity filter: if the proposed fix is identical to the current line,336                        // the AI is hallucinating and repeating the correct code — drop it immediately!337                        if (v.fix === v.codeSnippet) {338                            return false;339                        }340 341                        // Split the fix into lines and check them342                        const linesOfFix = v.fix.split('\n');343                        const arabicRegex = /[\u0600-\u06FF]/; // regex to detect any Arabic character344 345                        // Remove any line that accidentally contains Arabic text346                        const cleanLines = linesOfFix.filter(line => !arabicRegex.test(line));347 348                        v.fix = cleanLines.join('\n').trim();349                    }350 351                    // 🐛 Bug fix: if the fix ended up empty after cleaning (e.g. it was352                    // entirely non-English text), there is nothing to apply — drop this353                    // finding instead of showing a vulnerability with a broken "Apply Fix".354                    if (!v.fix || !v.fix.trim()) {355                        return false;356                    }357 358                    return true;359                }360                return false;361            });362            363            // Reset score to 100 if the filters wiped out all hallucinated entries and the array is now empty364            if (parsedJson.vulnerabilities.length === 0) {365                parsedJson.securityScore = 100;366                parsedJson.summary = "Your code is completely clean and all vulnerabilities have been successfully fixed!";367            }368        }369 370        // --- Extra layer, package.json only: don't trust the AI's guesses371        // about WHICH dependency versions are vulnerable — cross-check every372        // declared dependency against the real OSV.dev vulnerability database.373        // This both removes confident-sounding-but-wrong AI guesses (e.g. an374        // already-patched version flagged anyway) and catches real CVEs the375        // AI didn't happen to "recall".376        const looksLikePackageJson = (fileName && /(^|[\\/])package\.json$/i.test(fileName))377            || (!fileName && /"dependencies"\s*:/.test(code) && /"name"\s*:/.test(code) && /"version"\s*:/.test(code));378 379        if (looksLikePackageJson) {380            try {381                const parsedPkg = JSON.parse(code);382                const deps = { ...(parsedPkg.dependencies || {}), ...(parsedPkg.devDependencies || {}) };383                const depEntries = Object.entries(deps).slice(0, 25); // safety cap384 385                const osvResults = await Promise.all(386                    depEntries.map(([name, versionRange]) => {387                        const cleanVersion = String(versionRange).replace(/^[\^~>=<]+/, '').trim();388                        return checkDependencyOSV(name, cleanVersion);389                    })390                );391 392                const osvVulnerableNames = new Set(393                    osvResults.filter(d => d.vulnerabilities.length > 0).map(d => d.package)394                );395 396                const findDepForLine = (snippet) =>397                    depEntries.find(([name]) => (snippet || '').includes(`"${name}"`));398 399                // Drop any AI finding about a specific dependency's version that400                // OSV did NOT confirm — this is exactly the node-fetch false401                // positive we caught earlier.402                parsedJson.vulnerabilities = (parsedJson.vulnerabilities || []).filter(v => {403                    const mentionedDep = findDepForLine(v.codeSnippet);404                    if (!mentionedDep) return true; // not a dependency-version finding — leave it alone405                    return osvVulnerableNames.has(mentionedDep[0]);406                });407 408                const alreadyFlagged = new Set(409                    parsedJson.vulnerabilities410                        .map(v => findDepForLine(v.codeSnippet))411                        .filter(Boolean)412                        .map(([name]) => name)413                );414 415                // Add any OSV-confirmed vulnerable dependency the AI missed entirely.416                for (const result of osvResults) {417                    if (result.vulnerabilities.length === 0) continue;418                    if (alreadyFlagged.has(result.package)) continue;419 420                    const lineIdx = originalLines.findIndex(l => l.includes(`"${result.package}"`));421                    if (lineIdx === -1) continue;422 423                    const originalLine = originalLines[lineIdx];424                    const fixLine = result.fixedVersion425                        ? originalLine.replace(/:(\s*)"([\^~>=<]*)[^"]*"/, `:$1"$2${result.fixedVersion}"`)426                        : originalLine; // no confirmed safe version — no-op fix, description explains why427 428                    parsedJson.vulnerabilities.push({429                        id: `OSV-${result.package}`,430                        title: `Known vulnerability in ${result.package}@${result.version}`,431                        severity: 'high',432                        line: lineIdx + 1,433                        description: result.vulnerabilities.map(v => `${v.id}: ${v.summary}`).join(' | ')434                            + (result.fixedVersion ? ` Fixed in ${result.fixedVersion}.` : ' No confirmed fixed version was found in OSV.dev — please check manually before upgrading.'),435                        codeSnippet: originalLine.trim(),436                        fix: fixLine437                    });438                }439 440                if (parsedJson.vulnerabilities.length > 0) {441                    parsedJson.securityScore = Math.min(parsedJson.securityScore ?? 100, 60);442                    parsedJson.summary = 'One or more dependencies have known vulnerabilities confirmed against the OSV.dev database.';443                } else if (looksLikePackageJson) {444                    parsedJson.securityScore = 100;445                    parsedJson.summary = 'No known vulnerabilities found in this file\'s dependencies (cross-checked against OSV.dev).';446                }447            } catch (osvErr) {448                // If package.json fails to parse or OSV.dev is unreachable, fall449                // back silently to whatever the AI already produced above.450                console.error('OSV.dev cross-check for package.json failed:', osvErr.message);451            }452        }453 454        res.json(parsedJson);455 456    } catch (error) {457        res.status(500).json({ error: error.message });458    }459});460 461// Protected chat endpoint462app.post('/chat', requireAuth, async (req, res) => {463    if (!req.body || typeof req.body !== 'object') {464        return res.status(400).json({ error: 'Request body must be valid JSON with a "message" field.' });465    }466    const { message, history, context } = req.body;467    if (!message) return res.status(400).json({ error: 'No message provided' });468 469    // 🌐 Detect the language of the user's message and instruct the model to reply in kind470    const userLang = detectLanguage(message);471    const languageInstruction = userLang === 'ar'472        ? 'The user is writing in Arabic (Egyptian dialect). Reply in Egyptian Arabic.'473        : 'The user is writing in English. Reply in English.';474 475    const systemPrompt = `You are a cybersecurity expert inside the Code Guardian extension, helping the developer smartly and concisely. ${languageInstruction} Context: ${context}`;476 477    if (!GROQ_API_KEY) return res.status(500).json({ error: 'GROQ_API_KEY is not configured' });478 479    try {480        const response = await callGroqWithRetry({481            model: 'openai/gpt-oss-120b',482            messages: [{ role: 'system', content: systemPrompt }, ...(history?.slice(-6) || []), { role: 'user', content: message }],483            max_tokens: 800484        }, '/chat');485 486        if (!response.ok) {487            const errText = await response.text().catch(() => '');488            console.error('Groq API error (/chat):', response.status, errText);489            if (response.status === 429) {490                return res.status(429).json({ error: 'Rate limit reached. Please wait a few seconds and try again.' });491            }492            return res.status(502).json({ error: `Groq API error (status ${response.status}). Please try again.` });493        }494 495        const data = await response.json().catch(() => null);496        logTokenUsage('/chat', data?.usage);497        logUsageToSupabase(req.user.id, '/chat', data?.usage);498        const reply = data?.choices?.[0]?.message?.content;499 500        if (!reply) {501            console.error('Unexpected Groq response shape (/chat):', JSON.stringify(data));502            return res.status(502).json({ error: 'The AI returned an unexpected response. Please try again.' });503        }504 505        res.json({ reply });506    } catch (e) {507        console.error('Unhandled error in /chat:', e);508        res.status(500).json({ error: e.message });509    }510});511 512// ============================================================513// 🧩 Extension Pre-Install / Update Scanner514// ============================================================515// Lightweight, real-time scan (no storage/database — every scan is fresh).516// Checks a VS Code extension's package.json + a sample of its source code517// using the same Groq model, plus its declared npm dependencies against518// the free OSV.dev vulnerability database.519 520// Query OSV.dev for known vulnerabilities in an npm package/version.521// Also extracts the lowest version that fixes the reported issue(s), when522// OSV provides that data, so we can suggest a real, evidence-based upgrade523// target instead of the AI guessing one.524async function checkDependencyOSV(packageName, version) {525    try {526        const response = await fetch('https://api.osv.dev/v1/query', {527            method: 'POST',528            headers: { 'Content-Type': 'application/json' },529            body: JSON.stringify({530                package: { name: packageName, ecosystem: 'npm' },531                version: version532            })533        });534 535        if (!response.ok) return { package: packageName, version, vulnerabilities: [], fixedVersion: null, checked: false };536 537        const data = await response.json().catch(() => null);538        const rawVulns = data?.vulns || [];539 540        const vulns = rawVulns.map(v => ({541            id: v.id,542            summary: v.summary || 'No summary provided.'543        }));544 545        let fixedVersion = null;546        for (const v of rawVulns) {547            for (const affected of (v.affected || [])) {548                for (const range of (affected.ranges || [])) {549                    for (const event of (range.events || [])) {550                        if (event.fixed && (!fixedVersion || compareVersions(event.fixed, fixedVersion) > 0)) {551                            fixedVersion = event.fixed;552                        }553                    }554                }555            }556        }557 558        return { package: packageName, version, vulnerabilities: vulns, fixedVersion, checked: true };559    } catch (e) {560        console.error(`OSV.dev check failed for ${packageName}@${version}:`, e.message);561        return { package: packageName, version, vulnerabilities: [], fixedVersion: null, checked: false };562    }563}564 565// Basic numeric version comparison ("0.5.18" > "0.5.10"), used to pick the566// highest "fixed" version OSV.dev reports across multiple advisories.567function compareVersions(a, b) {568    const pa = String(a).split('.').map(n => parseInt(n, 10) || 0);569    const pb = String(b).split('.').map(n => parseInt(n, 10) || 0);570    for (let i = 0; i < Math.max(pa.length, pb.length); i++) {571        const diff = (pa[i] || 0) - (pb[i] || 0);572        if (diff !== 0) return diff;573    }574    return 0;575}576 577app.post('/scan-extension', requireAuth, async (req, res) => {578    if (!req.body || typeof req.body !== 'object') {579        return res.status(400).json({ error: 'Request body must be valid JSON with a "packageJson" field.' });580    }581    const { packageJson, codeSample, extensionName } = req.body;582 583    if (!packageJson) return res.status(400).json({ error: 'No package.json content was sent for analysis' });584    if (!GROQ_API_KEY) return res.status(500).json({ error: 'GROQ_API_KEY is not configured' });585 586    // --- Step 1: Parse package.json safely ---587    let parsedPkg;588    try {589        parsedPkg = typeof packageJson === 'string' ? JSON.parse(packageJson) : packageJson;590    } catch (e) {591        return res.status(400).json({ error: 'The provided package.json is not valid JSON.' });592    }593 594    const dependencies = { ...(parsedPkg.dependencies || {}), ...(parsedPkg.devDependencies || {}) };595    const depEntries = Object.entries(dependencies).slice(0, 25); // safety cap596 597    // --- Step 2: Check dependencies against OSV.dev in parallel ---598    const depResults = await Promise.all(599        depEntries.map(([name, versionRange]) => {600            const cleanVersion = String(versionRange).replace(/^[\^~>=<]+/, '').trim();601            return checkDependencyOSV(name, cleanVersion);602        })603    );604 605    const vulnerableDeps = depResults.filter(d => d.vulnerabilities.length > 0);606 607    // --- Step 3: AI review of package.json + code sample ---608    const systemPrompt = `You are a meticulous, conservative security auditor reviewing a VS Code extension BEFORE the user installs or updates it. Your top priority is AVOIDING FALSE POSITIVES — flagging a legitimate extension as risky is worse than missing a minor issue.609Analyze the provided package.json and code sample for genuine red flags such as:610- Suspicious or excessive permissions/activation events (e.g. "activationEvents": ["*"] combined with network calls or file system access with no clear legitimate purpose)611- Hardcoded credentials, API keys, or tokens612- Obfuscated or minified code hiding real behavior613- Network calls to unfamiliar or suspicious domains (not the extension's own well-known publisher/service domains)614- Commands that execute arbitrary shell code or download/execute remote payloads615Do NOT flag common, legitimate developer-tool behavior such as: reading workspace files to provide its stated functionality, calling well-known APIs (GitHub, npm, the extension's own documented backend), or using environment variables correctly for configuration.616Before including any finding, ask yourself: "Is this genuinely suspicious, or is it normal behavior for a tool that does what this extension claims to do?" If unsure, do not include it.617You MUST respond with a valid JSON object ONLY, no markdown, structured exactly as:618{619  "riskLevel": "safe" | "warning" | "dangerous",620  "summary": "A short summary in English of the overall assessment",621  "findings": [622    { "title": "Short finding title in English", "severity": "high" | "medium" | "low", "description": "Explanation in English" }623  ]624}625If nothing suspicious is found, return "riskLevel": "safe", "findings": [].`;626 627    const userContent = `Extension name: ${extensionName || 'Unknown'}628package.json:629${JSON.stringify(parsedPkg, null, 2)}630Code sample:631${codeSample ? codeSample.slice(0, 4000) : 'No code sample provided.'}`;632// Note: trimmed from 8000 to 4000 chars — combined with package.json this633// keeps a single request comfortably under the current 8,000 TPM free-tier634// ceiling seen in production logs. Raise this back up once the account is635// confirmed on the Developer tier (250,000 TPM).636 637    try {638        const response = await callGroqWithRetry({639            model: 'openai/gpt-oss-120b',640            messages: [641                { role: 'system', content: systemPrompt },642                { role: 'user', content: userContent }643            ],644            temperature: 0.1,645            max_tokens: 1500,646            response_format: { type: "json_object" }647        }, '/scan-extension');648 649        if (!response.ok) {650            const errText = await response.text().catch(() => '');651            console.error('Groq API error (/scan-extension):', response.status, errText);652            if (response.status === 413) {653                return res.status(413).json({ error: 'This extension\'s code is too large for the current plan\'s per-minute token limit. Try again shortly, or upgrade the Groq plan.' });654            }655            if (response.status === 429) {656                return res.status(429).json({ error: 'Rate limit reached. Please wait a few seconds and try again.' });657            }658            return res.status(502).json({ error: `Groq API error (status ${response.status}). Please try again.` });659        }660 661        const data = await response.json().catch(() => null);662        logTokenUsage('/scan-extension', data?.usage);663        logUsageToSupabase(req.user.id, '/scan-extension', data?.usage);664        const rawContent = data?.choices?.[0]?.message?.content;665 666        if (!rawContent) {667            console.error('Unexpected Groq response shape (/scan-extension):', JSON.stringify(data));668            return res.status(502).json({ error: 'The AI returned an unexpected response. Please try again.' });669        }670 671        let aiResult;672        try {673            aiResult = JSON.parse(rawContent.trim());674        } catch (parseErr) {675            console.error('Failed to parse Groq JSON (/scan-extension):', parseErr.message, rawContent);676            return res.status(502).json({ error: 'The AI returned malformed data. Please try scanning again.' });677        }678 679        // --- Step 4: Validate & sanitize AI findings before combining with dependency check ---680        const validSeverities = ['high', 'medium', 'low'];681        const validRiskLevels = ['safe', 'warning', 'dangerous'];682 683        let cleanFindings = Array.isArray(aiResult.findings) ? aiResult.findings.filter(f => {684            return f && f.title && String(f.title).trim() && f.description && String(f.description).trim();685        }).map(f => ({686            title: String(f.title).trim(),687            severity: validSeverities.includes(String(f.severity).toLowerCase()) ? String(f.severity).toLowerCase() : 'low',688            description: String(f.description).trim()689        })) : [];690 691        let finalRiskLevel = validRiskLevels.includes(aiResult.riskLevel) ? aiResult.riskLevel : (cleanFindings.length > 0 ? 'warning' : 'safe');692 693        // If the AI claims "safe" but still returned findings (contradiction), trust the findings694        if (finalRiskLevel === 'safe' && cleanFindings.length > 0) {695            finalRiskLevel = cleanFindings.some(f => f.severity === 'high') ? 'dangerous' : 'warning';696        }697 698        // Dependency vulnerabilities bump risk level up, but never override "dangerous"699        if (vulnerableDeps.length > 0 && finalRiskLevel === 'safe') {700            finalRiskLevel = 'warning';701        }702 703        res.json({704            extensionName: extensionName || parsedPkg.name || 'Unknown',705            riskLevel: finalRiskLevel,706            summary: aiResult.summary || '',707            findings: cleanFindings,708            dependencyCheck: {709                totalChecked: depResults.length,710                vulnerablePackages: vulnerableDeps711            }712        });713 714    } catch (error) {715        console.error('Unhandled error in /scan-extension:', error);716        res.status(500).json({ error: error.message });717    }718});719 720app.listen(PORT, () => console.log(`🚀 Server fully secured on port ${PORT}`));