diff --git a/scripts/check-security.mjs b/scripts/check-security.mjs index 281ff370c5..4732396a5a 100644 --- a/scripts/check-security.mjs +++ b/scripts/check-security.mjs @@ -488,9 +488,11 @@ async function main() { process.exit(0); } -main().catch(error => { +try { + await main(); +} catch (error) { console.error('\n❌ Security check crashed unexpectedly:', error.message); console.error(' Blocking installation as a precaution.'); console.error(' Set ADF_SKIP_SECURITY_CHECK=1 to bypass.\n'); process.exit(1); -}); +} diff --git a/scripts/postinstall-security.mjs b/scripts/postinstall-security.mjs index b90296da47..6ef715ad64 100644 --- a/scripts/postinstall-security.mjs +++ b/scripts/postinstall-security.mjs @@ -168,7 +168,9 @@ async function main() { console.log('='.repeat(70) + '\n'); } -main().catch(error => { +try { + await main(); +} catch (error) { console.error('Post-install failed:', error.message); process.exit(1); -}); +} diff --git a/scripts/preinstall-check.mjs b/scripts/preinstall-check.mjs index eeaf13ee82..b415038ace 100644 --- a/scripts/preinstall-check.mjs +++ b/scripts/preinstall-check.mjs @@ -118,6 +118,10 @@ function writeCache(threats, ranges) { // SECURITY PROVIDERS // ============================================================================ +const MALWARE_KEYWORDS = ['malware', 'malicious', 'compromised', 'supply chain', 'backdoor']; +const OSV_BATCH_SIZE = 1000; +const OSV_TIMEOUT_MS = 30000; + function splitIntoBatches(items, batchSize) { const batches = []; for (let index = 0; index < items.length; index += batchSize) { @@ -126,6 +130,62 @@ function splitIntoBatches(items, batchSize) { return batches; } +function buildOSVQueryPayload(dependencies) { + return { + queries: dependencies.map(dep => ({ + package: { name: dep.name, ecosystem: 'npm' }, + version: dep.version + })) + }; +} + +function isMalwareVulnerability(vulnerability) { + const summary = [vulnerability.summary || '', vulnerability.details || ''].join(' ').toLowerCase(); + return MALWARE_KEYWORDS.some(keyword => summary.includes(keyword)); +} + +function extractMaliciousPackages(vulnerability) { + const packages = []; + + for (const affected of vulnerability.affected || []) { + const isNpmPackage = affected.package?.ecosystem === 'npm' && affected.package?.name; + if (isNpmPackage) { + for (const version of affected.versions || []) { + packages.push(`${affected.package.name}@${version}`); + } + } + } + + return packages; +} + +function processOSVResults(results, maliciousSet) { + for (const result of results) { + for (const vulnerability of result.vulns || []) { + if (isMalwareVulnerability(vulnerability)) { + const packages = extractMaliciousPackages(vulnerability); + packages.forEach(pkg => maliciousSet.add(pkg)); + } + } + } +} + +async function queryOSVBatch(dependencies) { + const response = await fetch('https://api.osv.dev/v1/querybatch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(buildOSVQueryPayload(dependencies)), + signal: AbortSignal.timeout(OSV_TIMEOUT_MS) + }); + + if (!response.ok) { + return []; + } + + const data = await response.json(); + return data.results || []; +} + async function fetchOSV(projectDependencies) { if (!projectDependencies?.length) { return new Set(); @@ -134,44 +194,14 @@ async function fetchOSV(projectDependencies) { const malicious = new Set(); try { - const batches = splitIntoBatches(projectDependencies, 1000); + const batches = splitIntoBatches(projectDependencies, OSV_BATCH_SIZE); for (const batch of batches) { - const response = await fetch('https://api.osv.dev/v1/querybatch', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - queries: batch.map(dep => ({ - package: { name: dep.name, ecosystem: 'npm' }, - version: dep.version - })) - }), - signal: AbortSignal.timeout(30000) - }); - - if (!response.ok) continue; - - const data = await response.json(); - for (const result of data.results || []) { - for (const vuln of result.vulns || []) { - const summary = [vuln.summary || '', vuln.details || ''].join(' ').toLowerCase(); - const isMalware = ['malware', 'malicious', 'compromised', 'supply chain', 'backdoor'] - .some(keyword => summary.includes(keyword)); - - if (isMalware && vuln.affected) { - for (const affected of vuln.affected) { - if (affected.package?.ecosystem === 'npm' && affected.package?.name) { - for (const version of affected.versions || []) { - malicious.add(`${affected.package.name}@${version}`); - } - } - } - } - } - } + const results = await queryOSVBatch(batch); + processOSVResults(results, malicious); } } catch { - // Ignore errors + // Network errors are non-fatal } return malicious; @@ -454,9 +484,11 @@ async function main() { console.log('='.repeat(70) + '\n'); } -main().catch(error => { +try { + await main(); +} catch (error) { console.error('\n❌ Security check crashed unexpectedly:', error.message); console.error(' Blocking installation as a precaution.'); console.error(' Set ADF_SKIP_SECURITY_CHECK=1 to bypass.\n'); process.exit(1); -}); +} diff --git a/scripts/security-providers/osv-provider.mjs b/scripts/security-providers/osv-provider.mjs index b39f0ebba2..c0745ce1e9 100644 --- a/scripts/security-providers/osv-provider.mjs +++ b/scripts/security-providers/osv-provider.mjs @@ -36,32 +36,51 @@ function isMalwareRelated(text) { return MALWARE_KEYWORDS.some(keyword => lowerText.includes(keyword.toLowerCase())); } +function formatRangeWithUpperBound(introduced, upperVersion, upperOperator) { + if (introduced === '0') { + return `${upperOperator} ${upperVersion}`; + } + return `>= ${introduced}, ${upperOperator} ${upperVersion}`; +} + +function formatOpenEndedRange(introduced) { + if (introduced === null || introduced === '0') { + return null; + } + return `>= ${introduced}`; +} + +function processRangeEvent(event, introduced) { + if (event.introduced !== undefined) { + return { introduced: event.introduced, range: null }; + } + + if (event.fixed !== undefined && introduced !== null) { + return { introduced: null, range: formatRangeWithUpperBound(introduced, event.fixed, '<') }; + } + + if (event.last_affected !== undefined && introduced !== null) { + return { introduced: null, range: formatRangeWithUpperBound(introduced, event.last_affected, '<=') }; + } + + return { introduced, range: null }; +} + function extractRangesFromEvents(events) { const ranges = []; let introduced = null; for (const event of events) { - if (event.introduced !== undefined) { - introduced = event.introduced; - } else if (event.fixed !== undefined && introduced !== null) { - if (introduced === '0') { - ranges.push(`< ${event.fixed}`); - } else { - ranges.push(`>= ${introduced}, < ${event.fixed}`); - } - introduced = null; - } else if (event.last_affected !== undefined && introduced !== null) { - if (introduced === '0') { - ranges.push(`<= ${event.last_affected}`); - } else { - ranges.push(`>= ${introduced}, <= ${event.last_affected}`); - } - introduced = null; + const result = processRangeEvent(event, introduced); + introduced = result.introduced; + if (result.range) { + ranges.push(result.range); } } - if (introduced !== null && introduced !== '0') { - ranges.push(`>= ${introduced}`); + const openEndedRange = formatOpenEndedRange(introduced); + if (openEndedRange) { + ranges.push(openEndedRange); } return ranges;