/*! * @license * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * Malware checker - queries OSV and GitHub Advisory databases for known malware. */ const MALWARE_KEYWORDS = ['malware', 'malicious', 'compromised', 'supply chain', 'backdoor']; const OSV_API_URL = 'https://api.osv.dev/v1/querybatch'; const OSV_SINGLE_API_URL = 'https://api.osv.dev/v1/query'; const GITHUB_API_URL = 'https://api.github.com/advisories?ecosystem=npm&type=malware&per_page=100'; const REQUEST_TIMEOUT_MS = 10000; const BATCH_SIZE = 1000; // ============================================================================ // UTILITY FUNCTIONS // ============================================================================ function containsMalwareKeyword(text) { const lowerText = text.toLowerCase(); return MALWARE_KEYWORDS.some(keyword => lowerText.includes(keyword)); } function splitIntoBatches(items, batchSize) { const batches = []; for (let index = 0; index < items.length; index += batchSize) { batches.push(items.slice(index, index + batchSize)); } return batches; } function deduplicateFindings(findings) { return findings.filter((finding, index, array) => array.findIndex(other => other.name === finding.name && other.version === finding.version ) === index ); } // ============================================================================ // OSV API // ============================================================================ function buildOsvQuery(packageName, packageVersion) { return { package: { name: packageName, ecosystem: 'npm' }, version: packageVersion }; } function buildOsvBatchPayload(packages) { return { queries: packages.map(pkg => buildOsvQuery(pkg.name, pkg.version)) }; } function extractMalwareFromOsvVulnerability(vulnerability) { const summaryText = [vulnerability.summary || '', vulnerability.details || ''].join(' '); if (containsMalwareKeyword(summaryText)) { return vulnerability.summary || 'Malware detected'; } return null; } async function fetchOsvBatch(packages) { const response = await fetch(OSV_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(buildOsvBatchPayload(packages)), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS * 3) }); if (!response.ok) { return null; } return response.json(); } async function fetchOsvSingle(packageName, packageVersion) { const response = await fetch(OSV_SINGLE_API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(buildOsvQuery(packageName, packageVersion)), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }); if (!response.ok) { return null; } return response.json(); } function processBatchResults(batchResults, packages) { const findings = []; const results = batchResults.results || []; for (let index = 0; index < results.length; index++) { const vulnerabilities = results[index].vulns || []; for (const vulnerability of vulnerabilities) { const malwareReason = extractMalwareFromOsvVulnerability(vulnerability); if (malwareReason) { findings.push({ name: packages[index].name, version: packages[index].version, reason: malwareReason, source: 'OSV' }); } } } return findings; } export async function checkPackagesWithOsv(packages) { if (!packages.length) { return []; } const allFindings = []; const batches = splitIntoBatches(packages, BATCH_SIZE); for (const batch of batches) { try { const batchResults = await fetchOsvBatch(batch); if (batchResults) { const findings = processBatchResults(batchResults, batch); allFindings.push(...findings); } } catch { // Network errors are non-fatal } } return allFindings; } export async function checkSinglePackageWithOsv(packageName, packageVersion) { try { const data = await fetchOsvSingle(packageName, packageVersion); if (!data) { return null; } for (const vulnerability of data.vulns || []) { const malwareReason = extractMalwareFromOsvVulnerability(vulnerability); if (malwareReason) { return malwareReason; } } } catch { // Network errors are non-fatal } return null; } // ============================================================================ // GITHUB ADVISORY API // ============================================================================ async function fetchGitHubAdvisories() { const response = await fetch(GITHUB_API_URL, { headers: { 'Accept': 'application/vnd.github+json' }, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }); if (!response.ok) { return null; } return response.json(); } function extractMalwarePackageNames(advisories) { const malwarePackageNames = new Set(); for (const advisory of advisories) { const vulnerabilities = advisory.vulnerabilities || []; for (const vulnerability of vulnerabilities) { const packageName = vulnerability.package?.name; if (packageName) { malwarePackageNames.add(packageName); } } } return malwarePackageNames; } export async function checkPackagesWithGitHub(packages) { if (!packages.length) { return []; } const findings = []; try { const advisories = await fetchGitHubAdvisories(); if (!advisories) { return findings; } const malwarePackageNames = extractMalwarePackageNames(advisories); for (const pkg of packages) { if (malwarePackageNames.has(pkg.name)) { findings.push({ name: pkg.name, version: pkg.version, reason: 'Known malware package', source: 'GitHub Advisory' }); } } } catch { // Network errors are non-fatal } return findings; } export async function checkSinglePackageWithGitHub(packageName) { try { const advisories = await fetchGitHubAdvisories(); if (!advisories) { return null; } for (const advisory of advisories) { const vulnerabilities = advisory.vulnerabilities || []; for (const vulnerability of vulnerabilities) { if (vulnerability.package?.name === packageName) { return advisory.summary || 'Malware advisory'; } } } } catch { // Network errors are non-fatal } return null; } // ============================================================================ // COMBINED CHECK // ============================================================================ export async function checkPackagesForMalware(packages) { const [osvFindings, githubFindings] = await Promise.all([ checkPackagesWithOsv(packages), checkPackagesWithGitHub(packages) ]); const allFindings = [...osvFindings, ...githubFindings]; return deduplicateFindings(allFindings); } export async function checkSinglePackageForMalware(packageName, packageVersion) { const [osvResult, githubResult] = await Promise.all([ checkSinglePackageWithOsv(packageName, packageVersion), checkSinglePackageWithGitHub(packageName) ]); return osvResult || githubResult || null; }