Hossam01/Code-Guardian-Api
1
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}`));