From 31afc6f6127f66d3ee80a168548504b9d3abc3ef Mon Sep 17 00:00:00 2001 From: VitoAlbano Date: Fri, 29 May 2026 13:14:36 +0100 Subject: [PATCH] [AAE-46514] - Fixed copilot comments --- scripts/check-security.mjs | 232 ++++++++++++++++++++++--------- scripts/postinstall-security.mjs | 8 +- 2 files changed, 173 insertions(+), 67 deletions(-) diff --git a/scripts/check-security.mjs b/scripts/check-security.mjs index b8f550383d..56be6acfa7 100644 --- a/scripts/check-security.mjs +++ b/scripts/check-security.mjs @@ -33,7 +33,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; -import { execSync, spawnSync } from 'child_process'; +import { spawnSync } from 'child_process'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT_DIR = join(__dirname, '..'); @@ -132,44 +132,55 @@ function extractVersionsFromOSV(vuln) { return [...new Set(versions)]; } -async function fetchFromOSV(packages) { +async function fetchFromOSV(projectDependencies) { console.log(' 📡 Fetching from OSV (Google)...'); const results = {}; - try { - const response = await fetch(OSV_BATCH_API, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - queries: Object.keys(packages).map(name => ({ - package: { name, ecosystem: 'npm' } - })) - }) - }); + if (!projectDependencies || projectDependencies.length === 0) { + console.log(' ⚠ OSV: No dependencies to check'); + return results; + } - if (response.ok) { - const data = await response.json(); - for (const result of data.results || []) { - for (const vuln of result.vulns || []) { - const summary = [vuln.summary || '', vuln.details || ''].join(' '); - if (isMalwareRelated(summary)) { - const pkgName = vuln.affected?.[0]?.package?.name; - if (pkgName) { - const versions = extractVersionsFromOSV(vuln); - if (versions.length > 0) { - if (!results[pkgName]) { - results[pkgName] = { versions: [], reason: '' }; + try { + // Query OSV for the project's actual dependencies (in batches of 1000) + const batchSize = 1000; + for (let i = 0; i < projectDependencies.length; i += batchSize) { + const batch = projectDependencies.slice(i, i + batchSize); + const response = await fetch(OSV_BATCH_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + queries: batch.map(dep => ({ + package: { name: dep.name, ecosystem: 'npm' }, + version: dep.version + })) + }) + }); + + if (response.ok) { + const data = await response.json(); + for (const result of data.results || []) { + for (const vuln of result.vulns || []) { + const summary = [vuln.summary || '', vuln.details || ''].join(' '); + if (isMalwareRelated(summary)) { + const pkgName = vuln.affected?.[0]?.package?.name; + if (pkgName) { + const versions = extractVersionsFromOSV(vuln); + if (versions.length > 0) { + if (!results[pkgName]) { + results[pkgName] = { versions: [], reason: '' }; + } + results[pkgName].versions.push(...versions); + results[pkgName].reason = vuln.summary || 'Malware detected by OSV'; + results[pkgName].source = 'OSV'; } - results[pkgName].versions.push(...versions); - results[pkgName].reason = vuln.summary || 'Malware detected by OSV'; - results[pkgName].source = 'OSV'; } } } } } - console.log(` ✓ OSV: Found ${Object.keys(results).length} malware entries`); } + console.log(` ✓ OSV: Found ${Object.keys(results).length} malware entries`); } catch (e) { console.log(` ⚠ OSV: Could not fetch (${e.message})`); } @@ -275,7 +286,7 @@ async function checkWithMeterian(dependencies) { } } -async function fetchFromGitHubAdvisory(packages) { +async function fetchFromGitHubAdvisory() { console.log(' 📡 Fetching from GitHub Advisory...'); const results = {}; @@ -301,28 +312,23 @@ async function fetchFromGitHubAdvisory(packages) { for (const vuln of advisory.vulnerabilities || []) { const pkgName = vuln.package?.name; if (pkgName && vuln.package?.ecosystem === 'npm') { - // Extract affected versions - const versions = []; - if (vuln.vulnerable_version_range) { - // Parse version range like "= 1.95.6" or ">= 1.0.0, < 1.0.1" - const exactMatch = vuln.vulnerable_version_range.match(/^= (.+)$/); - if (exactMatch) { - versions.push(exactMatch[1]); - } - } - if (vuln.first_patched_version?.identifier) { - // This tells us the fixed version, not the bad ones - // We'd need to know the introduced version too + if (!results[pkgName]) { + results[pkgName] = { versions: [], versionRanges: [], reason: '' }; } - if (versions.length > 0 || isMalwareRelated(summary)) { - if (!results[pkgName]) { - results[pkgName] = { versions: [], reason: '' }; + if (vuln.vulnerable_version_range) { + // Store the full version range for later matching + results[pkgName].versionRanges.push(vuln.vulnerable_version_range); + + // Also extract exact versions where possible + const exactMatch = vuln.vulnerable_version_range.match(/^=\s*(.+)$/); + if (exactMatch) { + results[pkgName].versions.push(exactMatch[1].trim()); } - results[pkgName].versions.push(...versions); - results[pkgName].reason = advisory.summary || 'Malware detected by GitHub Advisory'; - results[pkgName].source = 'GitHub'; } + + results[pkgName].reason = advisory.summary || 'Malware detected by GitHub Advisory'; + results[pkgName].source = 'GitHub'; } } } @@ -337,7 +343,51 @@ async function fetchFromGitHubAdvisory(packages) { return results; } -async function fetchMalwareList() { +function getProjectDependencies() { + const dependencies = []; + const lockfilePath = join(ROOT_DIR, 'package-lock.json'); + + if (existsSync(lockfilePath)) { + try { + const lockfile = JSON.parse(readFileSync(lockfilePath, 'utf8')); + if (lockfile.packages) { + for (const [path, info] of Object.entries(lockfile.packages)) { + if (!path || path === '' || !info.version) continue; + const name = path.replace(/^node_modules\//, '').replace(/^.*node_modules\//, ''); + dependencies.push({ name, version: info.version }); + } + } + } catch { + // Ignore parse errors + } + } + + // Also check package.json for dependencies not yet in lockfile + const packageJsonPath = join(ROOT_DIR, 'package.json'); + if (existsSync(packageJsonPath)) { + try { + const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + const allDeps = { + ...packageJson.dependencies, + ...packageJson.devDependencies + }; + for (const [name, versionSpec] of Object.entries(allDeps)) { + if (typeof versionSpec === 'string' && !versionSpec.startsWith('file:')) { + const version = versionSpec.replace(/^[\^~>=<]*/, '').split(' ')[0]; + if (version && !dependencies.some(d => d.name === name && d.version === version)) { + dependencies.push({ name, version }); + } + } + } + } catch { + // Ignore parse errors + } + } + + return dependencies; +} + +async function fetchMalwareList(projectDependencies) { // Check cache first if (existsSync(CACHE_FILE)) { try { @@ -345,7 +395,7 @@ async function fetchMalwareList() { if (Date.now() - cache.timestamp < CACHE_TTL_MS) { console.log(' Using cached security database...'); console.log(` (${cache.sources?.join(', ') || 'fallback'} - cached ${Math.round((Date.now() - cache.timestamp) / 60000)} min ago)`); - return cache.packages; + return { packages: cache.packages, sources: cache.sources || ['fallback'] }; } } catch { // Cache corrupted, will refetch @@ -358,8 +408,8 @@ async function fetchMalwareList() { const packages = { ...FALLBACK_BLOCKED_PACKAGES }; const sources = ['fallback']; - // Fetch from OSV - const osvResults = await fetchFromOSV(packages); + // Fetch from OSV using the project's actual dependencies + const osvResults = await fetchFromOSV(projectDependencies); if (Object.keys(osvResults).length > 0) { sources.push('OSV'); for (const [name, data] of Object.entries(osvResults)) { @@ -372,14 +422,15 @@ async function fetchMalwareList() { } } - // Fetch from GitHub Advisory - const ghResults = await fetchFromGitHubAdvisory(packages); + // Fetch from GitHub Advisory (returns all known npm malware, limited to first page) + const ghResults = await fetchFromGitHubAdvisory(); if (Object.keys(ghResults).length > 0) { sources.push('GitHub'); for (const [name, data] of Object.entries(ghResults)) { if (packages[name]) { // Merge versions - packages[name].versions = [...new Set([...packages[name].versions, ...data.versions])]; + packages[name].versions = [...new Set([...packages[name].versions, ...(data.versions || [])])]; + packages[name].versionRanges = [...new Set([...(packages[name].versionRanges || []), ...(data.versionRanges || [])])]; } else { packages[name] = data; } @@ -402,7 +453,7 @@ async function fetchMalwareList() { // Cache write failed, continue anyway } - return packages; + return { packages, sources }; } // ============================================================================ @@ -421,8 +472,61 @@ function readJsonFile(filePath) { } } -function checkVersion(version, blockedVersions) { - return blockedVersions.some(blocked => blocked === version); +function compareVersions(a, b) { + const partsA = a.replace(/^v/, '').split('.').map(p => parseInt(p, 10) || 0); + const partsB = b.replace(/^v/, '').split('.').map(p => parseInt(p, 10) || 0); + const len = Math.max(partsA.length, partsB.length); + for (let i = 0; i < len; i++) { + const numA = partsA[i] || 0; + const numB = partsB[i] || 0; + if (numA > numB) return 1; + if (numA < numB) return -1; + } + return 0; +} + +function matchesVersionRange(version, range) { + // Parse version range like ">= 1.0.0, < 1.0.1" or "= 1.95.6" + const conditions = range.split(',').map(c => c.trim()); + + for (const condition of conditions) { + const match = condition.match(/^(>=|<=|>|<|=)?\s*(.+)$/); + if (!match) continue; + + const [, operator = '=', targetVersion] = match; + const cmp = compareVersions(version, targetVersion.trim()); + + switch (operator) { + case '=': + if (cmp !== 0) return false; + break; + case '>': + if (cmp <= 0) return false; + break; + case '>=': + if (cmp < 0) return false; + break; + case '<': + if (cmp >= 0) return false; + break; + case '<=': + if (cmp > 0) return false; + break; + } + } + return true; +} + +function checkVersion(version, blockedEntry) { + // Check exact version matches + if (blockedEntry.versions?.some(blocked => blocked === version)) { + return true; + } + // Check version ranges (from GitHub Advisory) + if (blockedEntry.versionRanges?.some(range => matchesVersionRange(version, range))) { + return true; + } + return false; } function checkDependencies(deps, blockedPackages, source) { @@ -432,7 +536,7 @@ function checkDependencies(deps, blockedPackages, source) { for (const [name, versionSpec] of Object.entries(deps)) { if (blockedPackages[name]) { const version = versionSpec.replace(/^[\^~>=<]*/, '').split(' ')[0]; - if (checkVersion(version, blockedPackages[name].versions)) { + if (checkVersion(version, blockedPackages[name])) { violations.push({ package: name, version, @@ -454,7 +558,7 @@ function checkLockfileDependencies(packages, blockedPackages) { const name = path.replace(/^node_modules\//, '').replace(/^.*node_modules\//, ''); if (blockedPackages[name] && info.version) { - if (checkVersion(info.version, blockedPackages[name].versions)) { + if (checkVersion(info.version, blockedPackages[name])) { violations.push({ package: name, version: info.version, @@ -477,9 +581,9 @@ async function main() { console.log('='.repeat(70)); console.log('Scanning for known supply chain attacks and compromised packages...\n'); - // Fetch blocked packages list - const blockedPackages = await fetchMalwareList(); - const threatCount = Object.keys(blockedPackages).length; + // Get project dependencies and fetch blocked packages list + const projectDependencies = getProjectDependencies(); + const { packages: blockedPackages, sources } = await fetchMalwareList(projectDependencies); const violations = []; @@ -572,7 +676,9 @@ async function main() { process.exit(1); } - console.log(`✅ Security check passed (${threatCount} known threats checked)`); + const packageCount = Object.keys(blockedPackages).length; + const sourceList = sources.join(' + '); + console.log(`✅ Security check passed (checked against ${sourceList}, ${packageCount} blocked packages)`); console.log('='.repeat(70) + '\n'); process.exit(0); } diff --git a/scripts/postinstall-security.mjs b/scripts/postinstall-security.mjs index 18c63e9b10..05f0d2bf92 100644 --- a/scripts/postinstall-security.mjs +++ b/scripts/postinstall-security.mjs @@ -20,9 +20,9 @@ /** * Post-install Security Script * - * This runs after `npm install` / `npm ci` (via prepare hook). - * Preinstall already checked the lockfile - this is a defense-in-depth - * check against installed packages + rebuilds trusted native bindings. + * Runs after `npm install` / `npm ci` via the prepare hook. + * The preinstall hook already checked package.json and lockfile - this is + * a defense-in-depth check against installed packages. * * This script: * 1. Runs security check against OSV + GitHub Advisory (defense in depth) @@ -138,7 +138,7 @@ async function main() { trustedInstalled.forEach(pkg => console.log(` ✓ ${pkg}`)); console.log(''); - run(`npm rebuild ${trustedInstalled.join(' ')}`); + run(`npm rebuild --ignore-scripts=false ${trustedInstalled.join(' ')}`); } else { console.log('No trusted packages require rebuilding.\n'); }