mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
295 lines
8.3 KiB
JavaScript
295 lines
8.3 KiB
JavaScript
/*!
|
|
* @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;
|
|
}
|