mirror of
https://github.com/Alfresco/alfresco-ng2-components.git
synced 2026-09-09 18:03:21 +00:00
[AAE-46514] - Improved the checks to cover more cases
This commit is contained in:
Vendored
+5
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"meterian.meterian-heidi"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -29,11 +29,13 @@ npm install # or npm ci
|
|||||||
|
|
||||||
This project has built-in supply chain attack protection. When you run `npm install`:
|
This project has built-in supply chain attack protection. When you run `npm install`:
|
||||||
|
|
||||||
1. All packages install with scripts disabled (via `.npmrc`)
|
1. **Preinstall check** scans both `package.json` AND `package-lock.json` against OSV + GitHub Advisory databases
|
||||||
2. Security check runs against OSV + GitHub Advisory databases (109+ known threats)
|
2. If malicious packages detected → installation blocked BEFORE any code runs
|
||||||
3. Only trusted packages (esbuild, nx, husky, etc.) get their scripts executed
|
3. Packages install normally
|
||||||
|
4. **Post-install check** verifies installed packages (defense in depth)
|
||||||
|
5. Only trusted packages (esbuild, nx, husky, etc.) get their native bindings rebuilt
|
||||||
|
|
||||||
If a malicious package is detected, installation is blocked and `node_modules` is deleted.
|
Checking `package.json` catches new dependencies added during upgrades (e.g., `nx migrate`) before the lockfile is updated. If a malicious package is detected at any stage, installation is blocked.
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"version": "8.5.0",
|
"version": "8.5.0",
|
||||||
"author": "Hyland Software, Inc. and its affiliates",
|
"author": "Hyland Software, Inc. and its affiliates",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"preinstall": "node scripts/preinstall-check.mjs",
|
||||||
"prepare": "node scripts/postinstall-security.mjs",
|
"prepare": "node scripts/postinstall-security.mjs",
|
||||||
"bundle:js-api": "nx run js-api:bundle",
|
"bundle:js-api": "nx run js-api:bundle",
|
||||||
"bundle:cli": "nx run cli:bundle",
|
"bundle:cli": "nx run cli:bundle",
|
||||||
|
|||||||
+125
-2
@@ -30,10 +30,10 @@
|
|||||||
* Cache: Results are cached locally for 24 hours to avoid slowing down installs.
|
* Cache: Results are cached locally for 24 hours to avoid slowing down installs.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'fs';
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync } from 'fs';
|
||||||
import { join, dirname } from 'path';
|
import { join, dirname } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { execSync } from 'child_process';
|
import { execSync, spawnSync } from 'child_process';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const ROOT_DIR = join(__dirname, '..');
|
const ROOT_DIR = join(__dirname, '..');
|
||||||
@@ -46,6 +46,9 @@ const OSV_API = 'https://api.osv.dev/v1/query';
|
|||||||
const OSV_BATCH_API = 'https://api.osv.dev/v1/querybatch';
|
const OSV_BATCH_API = 'https://api.osv.dev/v1/querybatch';
|
||||||
const GITHUB_ADVISORY_API = 'https://api.github.com/advisories';
|
const GITHUB_ADVISORY_API = 'https://api.github.com/advisories';
|
||||||
|
|
||||||
|
// Meterian CLI (bundled with VSCode extension, also available via npx)
|
||||||
|
const METERIAN_CLI = '@meterian/cli';
|
||||||
|
|
||||||
// Filter for supply chain attacks (malware, compromised packages)
|
// Filter for supply chain attacks (malware, compromised packages)
|
||||||
// These are the most dangerous - not just vulnerabilities but intentionally malicious
|
// These are the most dangerous - not just vulnerabilities but intentionally malicious
|
||||||
const MALWARE_KEYWORDS = [
|
const MALWARE_KEYWORDS = [
|
||||||
@@ -174,6 +177,104 @@ async function fetchFromOSV(packages) {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findMeterianCli() {
|
||||||
|
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
||||||
|
|
||||||
|
// Check VSCode extensions first (most likely for local dev)
|
||||||
|
const extensionsDir = join(homeDir, '.vscode', 'extensions');
|
||||||
|
if (existsSync(extensionsDir)) {
|
||||||
|
try {
|
||||||
|
const extensions = readdirSync(extensionsDir)
|
||||||
|
.filter(d => /^meterian\.meterian-heidi-\d+\.\d+\.\d+$/.test(d))
|
||||||
|
.sort()
|
||||||
|
.reverse(); // Latest version first
|
||||||
|
|
||||||
|
for (const ext of extensions) {
|
||||||
|
const cliPath = join(extensionsDir, ext, 'packages', 'meterian-cli');
|
||||||
|
if (existsSync(cliPath)) {
|
||||||
|
return cliPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors reading extensions dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other possible locations
|
||||||
|
const locations = [
|
||||||
|
// Local node_modules
|
||||||
|
join(ROOT_DIR, 'node_modules', '@meterian', 'cli'),
|
||||||
|
// Global npm (macOS/Linux)
|
||||||
|
join(homeDir, '.npm-global', 'lib', 'node_modules', '@meterian', 'cli'),
|
||||||
|
// Global npm (alternative)
|
||||||
|
'/usr/local/lib/node_modules/@meterian/cli'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const loc of locations) {
|
||||||
|
if (existsSync(loc)) {
|
||||||
|
return loc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMeterianAvailable() {
|
||||||
|
return findMeterianCli() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkWithMeterian(dependencies) {
|
||||||
|
// Skip if Meterian CLI is not available
|
||||||
|
const cliPath = findMeterianCli();
|
||||||
|
if (!cliPath) {
|
||||||
|
console.log(' ⏭️ Meterian: Not installed, skipping');
|
||||||
|
console.log(' Install VSCode extension "Meterian Security" or run: npm i -g @meterian/cli');
|
||||||
|
return { vulnerable: [], source: 'Meterian' };
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(' 📡 Checking with Meterian CLI...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Format dependencies for Meterian CLI
|
||||||
|
const input = dependencies.map(dep => ({
|
||||||
|
language: 'nodejs',
|
||||||
|
name: dep.name,
|
||||||
|
version: dep.version
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Run Meterian CLI check using the found path
|
||||||
|
const cliScript = join(cliPath, 'src', 'cli.js');
|
||||||
|
const result = spawnSync('node', [cliScript, 'check'], {
|
||||||
|
input: JSON.stringify(input),
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: 60000, // 60 second timeout
|
||||||
|
maxBuffer: 10 * 1024 * 1024
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
console.log(` ⚠ Meterian: ${result.error.message}`);
|
||||||
|
return { vulnerable: [], source: 'Meterian' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status !== 0 && !result.stdout) {
|
||||||
|
console.log(` ⚠ Meterian: CLI returned status ${result.status}`);
|
||||||
|
return { vulnerable: [], source: 'Meterian' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = JSON.parse(result.stdout);
|
||||||
|
console.log(` ✓ Meterian: Found ${output.vulnerable?.length || 0} vulnerable packages`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
vulnerable: output.vulnerable || [],
|
||||||
|
summary: output.summary,
|
||||||
|
source: 'Meterian'
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.log(` ⚠ Meterian: ${e.message}`);
|
||||||
|
return { vulnerable: [], source: 'Meterian' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchFromGitHubAdvisory(packages) {
|
async function fetchFromGitHubAdvisory(packages) {
|
||||||
console.log(' 📡 Fetching from GitHub Advisory...');
|
console.log(' 📡 Fetching from GitHub Advisory...');
|
||||||
const results = {};
|
const results = {};
|
||||||
@@ -395,6 +496,28 @@ async function main() {
|
|||||||
violations.push(...checkLockfileDependencies(lockfile.packages, blockedPackages));
|
violations.push(...checkLockfileDependencies(lockfile.packages, blockedPackages));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check with Meterian CLI for additional vulnerability coverage
|
||||||
|
if (lockfile?.packages) {
|
||||||
|
console.log('');
|
||||||
|
const deps = Object.entries(lockfile.packages)
|
||||||
|
.filter(([path, info]) => path && info.version)
|
||||||
|
.map(([path, info]) => ({
|
||||||
|
name: path.replace(/^node_modules\//, '').replace(/^.*node_modules\//, ''),
|
||||||
|
version: info.version
|
||||||
|
}))
|
||||||
|
.slice(0, 500); // Limit to avoid timeout
|
||||||
|
|
||||||
|
const meterianResult = await checkWithMeterian(deps);
|
||||||
|
for (const vuln of meterianResult.vulnerable || []) {
|
||||||
|
violations.push({
|
||||||
|
package: vuln.name,
|
||||||
|
version: vuln.version,
|
||||||
|
reason: `${vuln.severity}: ${vuln.id}${vuln.safeVersions?.length ? ` (safe: ${vuln.safeVersions[0]})` : ''}`,
|
||||||
|
source: 'Meterian'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Deduplicate
|
// Deduplicate
|
||||||
const uniqueViolations = violations.filter((v, i, arr) =>
|
const uniqueViolations = violations.filter((v, i, arr) =>
|
||||||
arr.findIndex(x => x.package === v.package && x.version === v.version) === i
|
arr.findIndex(x => x.package === v.package && x.version === v.version) === i
|
||||||
|
|||||||
@@ -21,11 +21,12 @@
|
|||||||
* Post-install Security Script
|
* Post-install Security Script
|
||||||
*
|
*
|
||||||
* This runs after `npm install` / `npm ci` (via prepare hook).
|
* This runs after `npm install` / `npm ci` (via prepare hook).
|
||||||
* Since .npmrc has ignore-scripts=true, no package scripts run during install.
|
* Preinstall already checked the lockfile - this is a defense-in-depth
|
||||||
|
* check against installed packages + rebuilds trusted native bindings.
|
||||||
*
|
*
|
||||||
* This script:
|
* This script:
|
||||||
* 1. Runs security check against OSV + GitHub Advisory
|
* 1. Runs security check against OSV + GitHub Advisory (defense in depth)
|
||||||
* 2. If safe, rebuilds only trusted packages that need native bindings
|
* 2. Rebuilds trusted packages that need native bindings
|
||||||
* 3. Sets up husky
|
* 3. Sets up husky
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-install Security Check
|
||||||
|
*
|
||||||
|
* Runs BEFORE packages install. Checks both package.json AND package-lock.json
|
||||||
|
* against OSV + GitHub Advisory databases to block malicious packages
|
||||||
|
* BEFORE their postinstall scripts can execute.
|
||||||
|
*
|
||||||
|
* Checking package.json catches new dependencies added during upgrades
|
||||||
|
* (e.g., nx migrate) before the lockfile is updated.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const ROOT_DIR = join(__dirname, '..');
|
||||||
|
const CACHE_DIR = join(ROOT_DIR, 'node_modules', '.cache', 'security-check');
|
||||||
|
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
|
||||||
|
// Known supply chain attack packages (fallback if APIs fail)
|
||||||
|
const KNOWN_MALICIOUS = new Set([
|
||||||
|
'event-stream@3.3.6',
|
||||||
|
'flatmap-stream@0.1.1',
|
||||||
|
'ua-parser-js@0.7.29',
|
||||||
|
'coa@2.0.3', 'coa@2.0.4', 'coa@2.1.1', 'coa@2.1.3', 'coa@3.0.1', 'coa@3.1.3',
|
||||||
|
'rc@1.2.9', 'rc@1.3.9', 'rc@2.3.9',
|
||||||
|
'colors@1.4.1', 'colors@1.4.2',
|
||||||
|
'faker@5.5.3', 'faker@6.6.6',
|
||||||
|
'node-ipc@10.1.1', 'node-ipc@10.1.2', 'node-ipc@10.1.3',
|
||||||
|
'peacenotwar@9.1.3', 'peacenotwar@9.1.4', 'peacenotwar@9.1.5', 'peacenotwar@9.1.6',
|
||||||
|
'es5-ext@0.10.53', 'es5-ext@0.10.54', 'es5-ext@0.10.55', 'es5-ext@0.10.56',
|
||||||
|
'@primevue/themes@4.3.0', '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3'
|
||||||
|
]);
|
||||||
|
|
||||||
|
function readCache() {
|
||||||
|
const cachePath = join(CACHE_DIR, 'threats.json');
|
||||||
|
if (!existsSync(cachePath)) return null;
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(readFileSync(cachePath, 'utf8'));
|
||||||
|
if (Date.now() - data.timestamp < CACHE_TTL) {
|
||||||
|
return new Set(data.threats);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeCache(threats) {
|
||||||
|
try {
|
||||||
|
if (!existsSync(CACHE_DIR)) {
|
||||||
|
mkdirSync(CACHE_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
writeFileSync(join(CACHE_DIR, 'threats.json'), JSON.stringify({
|
||||||
|
timestamp: Date.now(),
|
||||||
|
threats: [...threats]
|
||||||
|
}));
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchOSV() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://osv-vulnerabilities.storage.googleapis.com/npm/all.zip', {
|
||||||
|
signal: AbortSignal.timeout(10000)
|
||||||
|
});
|
||||||
|
if (!response.ok) return new Set();
|
||||||
|
|
||||||
|
const buffer = await response.arrayBuffer();
|
||||||
|
const text = new TextDecoder().decode(buffer);
|
||||||
|
const malicious = new Set();
|
||||||
|
|
||||||
|
// Parse JSONL format looking for MALWARE type
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
try {
|
||||||
|
const vuln = JSON.parse(line);
|
||||||
|
if (vuln.database_specific?.type === 'MALWARE' && vuln.affected) {
|
||||||
|
for (const affected of vuln.affected) {
|
||||||
|
if (affected.package?.ecosystem === 'npm' && affected.package?.name) {
|
||||||
|
const versions = affected.versions || [];
|
||||||
|
for (const v of versions) {
|
||||||
|
malicious.add(`${affected.package.name}@${v}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* skip invalid lines */ }
|
||||||
|
}
|
||||||
|
return malicious;
|
||||||
|
} catch {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGitHubAdvisory() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://api.github.com/advisories?ecosystem=npm&type=malware&per_page=100', {
|
||||||
|
headers: { 'Accept': 'application/vnd.github+json' },
|
||||||
|
signal: AbortSignal.timeout(10000)
|
||||||
|
});
|
||||||
|
if (!response.ok) return new Set();
|
||||||
|
|
||||||
|
const advisories = await response.json();
|
||||||
|
const malicious = new Set();
|
||||||
|
|
||||||
|
for (const advisory of advisories) {
|
||||||
|
if (advisory.vulnerabilities) {
|
||||||
|
for (const vuln of advisory.vulnerabilities) {
|
||||||
|
if (vuln.package?.ecosystem === 'npm' && vuln.package?.name) {
|
||||||
|
// GitHub uses version ranges, we'll mark the package name
|
||||||
|
// and check ranges in the scan
|
||||||
|
if (vuln.vulnerable_version_range) {
|
||||||
|
malicious.add(`${vuln.package.name}:${vuln.vulnerable_version_range}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return malicious;
|
||||||
|
} catch {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVersionRange(range, version) {
|
||||||
|
// Simple version range parser for GitHub advisory format
|
||||||
|
// Handles: "= 1.0.0", "< 1.0.0", "<= 1.0.0", "> 1.0.0", ">= 1.0.0"
|
||||||
|
if (!range || !version) return false;
|
||||||
|
|
||||||
|
const parts = range.split(',').map(p => p.trim());
|
||||||
|
for (const part of parts) {
|
||||||
|
const match = part.match(/^([<>=]+)\s*(.+)$/);
|
||||||
|
if (!match) {
|
||||||
|
if (part === version) return true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const [, op, rangeVer] = match;
|
||||||
|
const cmp = compareVersions(version, rangeVer);
|
||||||
|
|
||||||
|
if (op === '=' && cmp !== 0) return false;
|
||||||
|
if (op === '<' && cmp >= 0) return false;
|
||||||
|
if (op === '<=' && cmp > 0) return false;
|
||||||
|
if (op === '>' && cmp <= 0) return false;
|
||||||
|
if (op === '>=' && cmp < 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersions(a, b) {
|
||||||
|
const pa = a.split('.').map(Number);
|
||||||
|
const pb = b.split('.').map(Number);
|
||||||
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||||
|
const na = pa[i] || 0;
|
||||||
|
const nb = pb[i] || 0;
|
||||||
|
if (na > nb) return 1;
|
||||||
|
if (na < nb) return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractVersionNumber(versionSpec) {
|
||||||
|
if (!versionSpec) return null;
|
||||||
|
// Remove ^, ~, >=, <=, >, <, = prefixes
|
||||||
|
const match = versionSpec.match(/[\d]+\.[\d]+\.[\d]+(?:-[\w.]+)?/);
|
||||||
|
return match ? match[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPackagesFromPackageJson() {
|
||||||
|
const packageJsonPath = join(ROOT_DIR, 'package.json');
|
||||||
|
if (!existsSync(packageJsonPath)) return [];
|
||||||
|
|
||||||
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
const packages = [];
|
||||||
|
|
||||||
|
const depTypes = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
|
||||||
|
for (const depType of depTypes) {
|
||||||
|
const deps = packageJson[depType] || {};
|
||||||
|
for (const [name, versionSpec] of Object.entries(deps)) {
|
||||||
|
// Skip file: and link: dependencies
|
||||||
|
if (typeof versionSpec === 'string' && !versionSpec.startsWith('file:') && !versionSpec.startsWith('link:')) {
|
||||||
|
const version = extractVersionNumber(versionSpec);
|
||||||
|
if (version) {
|
||||||
|
packages.push({ name, version, source: 'package.json' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPackagesFromLockfile() {
|
||||||
|
const lockfilePath = join(ROOT_DIR, 'package-lock.json');
|
||||||
|
if (!existsSync(lockfilePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const lockfile = JSON.parse(readFileSync(lockfilePath, 'utf8'));
|
||||||
|
const packages = [];
|
||||||
|
|
||||||
|
// npm v2+ lockfile format
|
||||||
|
if (lockfile.packages) {
|
||||||
|
for (const [path, info] of Object.entries(lockfile.packages)) {
|
||||||
|
if (!path || path === '') continue; // skip root
|
||||||
|
const name = path.replace(/^node_modules\//, '').replace(/\/node_modules\//g, '/');
|
||||||
|
if (info.version) {
|
||||||
|
packages.push({ name, version: info.version, source: 'lockfile' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// npm v1 lockfile format
|
||||||
|
if (lockfile.dependencies) {
|
||||||
|
const extractDeps = (deps, prefix = '') => {
|
||||||
|
for (const [name, info] of Object.entries(deps)) {
|
||||||
|
const fullName = prefix ? `${prefix}/${name}` : name;
|
||||||
|
if (info.version) {
|
||||||
|
packages.push({ name: fullName, version: info.version, source: 'lockfile' });
|
||||||
|
}
|
||||||
|
if (info.dependencies) {
|
||||||
|
extractDeps(info.dependencies, fullName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
extractDeps(lockfile.dependencies);
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllPackages() {
|
||||||
|
const packageJsonPkgs = getPackagesFromPackageJson();
|
||||||
|
const lockfilePkgs = getPackagesFromLockfile();
|
||||||
|
|
||||||
|
// Combine and dedupe (lockfile takes precedence for same name)
|
||||||
|
const seen = new Map();
|
||||||
|
for (const pkg of lockfilePkgs) {
|
||||||
|
seen.set(`${pkg.name}@${pkg.version}`, pkg);
|
||||||
|
}
|
||||||
|
for (const pkg of packageJsonPkgs) {
|
||||||
|
const key = `${pkg.name}@${pkg.version}`;
|
||||||
|
if (!seen.has(key)) {
|
||||||
|
seen.set(key, pkg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...seen.values()];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('\n🔒 ADF SECURITY CHECK');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
console.log('Scanning for known supply chain attacks and compromised packages...\n');
|
||||||
|
|
||||||
|
// Get all packages from both package.json and lockfile
|
||||||
|
const packages = getAllPackages();
|
||||||
|
if (packages.length === 0) {
|
||||||
|
console.log(' ⚠️ No packages found to check');
|
||||||
|
console.log('='.repeat(70) + '\n');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromPackageJson = packages.filter(p => p.source === 'package.json').length;
|
||||||
|
const fromLockfile = packages.filter(p => p.source === 'lockfile').length;
|
||||||
|
console.log(` Checking ${packages.length} packages (${fromPackageJson} from package.json, ${fromLockfile} from lockfile)\n`);
|
||||||
|
|
||||||
|
// Try to use cache first
|
||||||
|
let threats = readCache();
|
||||||
|
let fromCache = true;
|
||||||
|
|
||||||
|
if (!threats) {
|
||||||
|
fromCache = false;
|
||||||
|
console.log(' Fetching latest security databases...\n');
|
||||||
|
|
||||||
|
const [osvThreats, ghThreats] = await Promise.all([
|
||||||
|
fetchOSV().then(r => { console.log(` 📡 OSV: ${r.size} malware entries`); return r; }),
|
||||||
|
fetchGitHubAdvisory().then(r => { console.log(` 📡 GitHub Advisory: ${r.size} malware entries`); return r; })
|
||||||
|
]);
|
||||||
|
|
||||||
|
threats = new Set([...KNOWN_MALICIOUS, ...osvThreats]);
|
||||||
|
|
||||||
|
// Store GitHub advisories separately (they have version ranges)
|
||||||
|
const ghRanges = [...ghThreats];
|
||||||
|
|
||||||
|
// Check packages against exact matches and ranges
|
||||||
|
const found = [];
|
||||||
|
|
||||||
|
for (const pkg of packages) {
|
||||||
|
const exact = `${pkg.name}@${pkg.version}`;
|
||||||
|
|
||||||
|
// Check exact match
|
||||||
|
if (threats.has(exact)) {
|
||||||
|
found.push({ ...pkg, source: 'exact match' });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check GitHub Advisory ranges
|
||||||
|
for (const entry of ghRanges) {
|
||||||
|
const [name, range] = entry.split(':');
|
||||||
|
if (pkg.name === name && parseVersionRange(range, pkg.version)) {
|
||||||
|
found.push({ ...pkg, source: 'GitHub Advisory' });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found.length > 0) {
|
||||||
|
console.log('\n' + '!'.repeat(70));
|
||||||
|
console.log('🚨 MALICIOUS PACKAGES DETECTED - BLOCKING INSTALLATION');
|
||||||
|
console.log('!'.repeat(70) + '\n');
|
||||||
|
|
||||||
|
for (const pkg of found) {
|
||||||
|
console.log(` ❌ ${pkg.name}@${pkg.version} (${pkg.source})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nThese packages are known to contain malware or malicious code.');
|
||||||
|
console.log('Installation has been blocked to protect your system.\n');
|
||||||
|
console.log('Actions:');
|
||||||
|
console.log(' 1. Remove these packages from package.json');
|
||||||
|
console.log(' 2. Find safe alternatives');
|
||||||
|
console.log(' 3. Run npm install again\n');
|
||||||
|
console.log('='.repeat(70) + '\n');
|
||||||
|
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the results
|
||||||
|
writeCache(threats);
|
||||||
|
console.log(`\n✅ Security check passed (${threats.size + ghRanges.length} known threats checked)`);
|
||||||
|
} else {
|
||||||
|
// Quick check against cached threats
|
||||||
|
const found = [];
|
||||||
|
for (const pkg of packages) {
|
||||||
|
const exact = `${pkg.name}@${pkg.version}`;
|
||||||
|
if (threats.has(exact)) {
|
||||||
|
found.push(pkg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found.length > 0) {
|
||||||
|
console.log('\n' + '!'.repeat(70));
|
||||||
|
console.log('🚨 MALICIOUS PACKAGES DETECTED - BLOCKING INSTALLATION');
|
||||||
|
console.log('!'.repeat(70) + '\n');
|
||||||
|
|
||||||
|
for (const pkg of found) {
|
||||||
|
console.log(` ❌ ${pkg.name}@${pkg.version}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n='.repeat(70) + '\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Security check passed (cached, ${threats.size} known threats)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('='.repeat(70) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error('Security check error:', err.message);
|
||||||
|
// Don't block on errors - allow install to proceed
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user